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 {
2040                    working_directory,
2041                    local: false,
2042                }
2043                .boxed_clone(),
2044                cx,
2045            )
2046        }
2047    }
2048
2049    fn reveal_entry_for_selection(
2050        &mut self,
2051        editor: Entity<Editor>,
2052        window: &mut Window,
2053        cx: &mut Context<Self>,
2054    ) {
2055        if !self.active
2056            || !OutlinePanelSettings::get_global(cx).auto_reveal_entries
2057            || self.focus_handle.contains_focused(window, cx)
2058        {
2059            return;
2060        }
2061        let project = self.project.clone();
2062        self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| {
2063            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
2064            let entry_with_selection =
2065                outline_panel.update_in(cx, |outline_panel, window, cx| {
2066                    outline_panel.location_for_editor_selection(&editor, window, cx)
2067                })?;
2068            let Some(entry_with_selection) = entry_with_selection else {
2069                outline_panel.update(cx, |outline_panel, cx| {
2070                    outline_panel.selected_entry = SelectedEntry::None;
2071                    cx.notify();
2072                })?;
2073                return Ok(());
2074            };
2075            let related_buffer_entry = match &entry_with_selection {
2076                PanelEntry::Fs(FsEntry::File(FsEntryFile {
2077                    worktree_id,
2078                    buffer_id,
2079                    ..
2080                })) => project.update(cx, |project, cx| {
2081                    let entry_id = project
2082                        .buffer_for_id(*buffer_id, cx)
2083                        .and_then(|buffer| buffer.read(cx).entry_id(cx));
2084                    project
2085                        .worktree_for_id(*worktree_id, cx)
2086                        .zip(entry_id)
2087                        .and_then(|(worktree, entry_id)| {
2088                            let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2089                            Some((worktree, entry))
2090                        })
2091                }),
2092                PanelEntry::Outline(outline_entry) => {
2093                    let (buffer_id, excerpt_id) = outline_entry.ids();
2094                    outline_panel.update(cx, |outline_panel, cx| {
2095                        outline_panel
2096                            .collapsed_entries
2097                            .remove(&CollapsedEntry::ExternalFile(buffer_id));
2098                        outline_panel
2099                            .collapsed_entries
2100                            .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
2101                        let project = outline_panel.project.read(cx);
2102                        let entry_id = project
2103                            .buffer_for_id(buffer_id, cx)
2104                            .and_then(|buffer| buffer.read(cx).entry_id(cx));
2105
2106                        entry_id.and_then(|entry_id| {
2107                            project
2108                                .worktree_for_entry(entry_id, cx)
2109                                .and_then(|worktree| {
2110                                    let worktree_id = worktree.read(cx).id();
2111                                    outline_panel
2112                                        .collapsed_entries
2113                                        .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2114                                    let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
2115                                    Some((worktree, entry))
2116                                })
2117                        })
2118                    })?
2119                }
2120                PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
2121                PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
2122                    .start
2123                    .text_anchor
2124                    .buffer_id
2125                    .or(match_range.end.text_anchor.buffer_id)
2126                    .map(|buffer_id| {
2127                        outline_panel.update(cx, |outline_panel, cx| {
2128                            outline_panel
2129                                .collapsed_entries
2130                                .remove(&CollapsedEntry::ExternalFile(buffer_id));
2131                            let project = project.read(cx);
2132                            let entry_id = project
2133                                .buffer_for_id(buffer_id, cx)
2134                                .and_then(|buffer| buffer.read(cx).entry_id(cx));
2135
2136                            entry_id.and_then(|entry_id| {
2137                                project
2138                                    .worktree_for_entry(entry_id, cx)
2139                                    .and_then(|worktree| {
2140                                        let worktree_id = worktree.read(cx).id();
2141                                        outline_panel
2142                                            .collapsed_entries
2143                                            .remove(&CollapsedEntry::File(worktree_id, buffer_id));
2144                                        let entry =
2145                                            worktree.read(cx).entry_for_id(entry_id)?.clone();
2146                                        Some((worktree, entry))
2147                                    })
2148                            })
2149                        })
2150                    })
2151                    .transpose()?
2152                    .flatten(),
2153                _ => return anyhow::Ok(()),
2154            };
2155            if let Some((worktree, buffer_entry)) = related_buffer_entry {
2156                outline_panel.update(cx, |outline_panel, cx| {
2157                    let worktree_id = worktree.read(cx).id();
2158                    let mut dirs_to_expand = Vec::new();
2159                    {
2160                        let mut traversal = worktree.read(cx).traverse_from_path(
2161                            true,
2162                            true,
2163                            true,
2164                            buffer_entry.path.as_ref(),
2165                        );
2166                        let mut current_entry = buffer_entry;
2167                        loop {
2168                            if current_entry.is_dir()
2169                                && outline_panel
2170                                    .collapsed_entries
2171                                    .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
2172                            {
2173                                dirs_to_expand.push(current_entry.id);
2174                            }
2175
2176                            if traversal.back_to_parent()
2177                                && let Some(parent_entry) = traversal.entry()
2178                            {
2179                                current_entry = parent_entry.clone();
2180                                continue;
2181                            }
2182                            break;
2183                        }
2184                    }
2185                    for dir_to_expand in dirs_to_expand {
2186                        project
2187                            .update(cx, |project, cx| {
2188                                project.expand_entry(worktree_id, dir_to_expand, cx)
2189                            })
2190                            .unwrap_or_else(|| Task::ready(Ok(())))
2191                            .detach_and_log_err(cx)
2192                    }
2193                })?
2194            }
2195
2196            outline_panel.update_in(cx, |outline_panel, window, cx| {
2197                outline_panel.select_entry(entry_with_selection, false, window, cx);
2198                outline_panel.update_cached_entries(None, window, cx);
2199            })?;
2200
2201            anyhow::Ok(())
2202        });
2203    }
2204
2205    fn render_excerpt(
2206        &self,
2207        excerpt: &OutlineEntryExcerpt,
2208        depth: usize,
2209        window: &mut Window,
2210        cx: &mut Context<OutlinePanel>,
2211    ) -> Option<Stateful<Div>> {
2212        let item_id = ElementId::from(excerpt.id.to_proto() as usize);
2213        let is_active = match self.selected_entry() {
2214            Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => {
2215                selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id
2216            }
2217            _ => false,
2218        };
2219        let has_outlines = self
2220            .excerpts
2221            .get(&excerpt.buffer_id)
2222            .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines {
2223                ExcerptOutlines::Outlines(outlines) => Some(outlines),
2224                ExcerptOutlines::Invalidated(outlines) => Some(outlines),
2225                ExcerptOutlines::NotFetched => None,
2226            })
2227            .is_some_and(|outlines| !outlines.is_empty());
2228        let is_expanded = !self
2229            .collapsed_entries
2230            .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
2231        let color = entry_label_color(is_active);
2232        let icon = if has_outlines {
2233            FileIcons::get_chevron_icon(is_expanded, cx)
2234                .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2235        } else {
2236            None
2237        }
2238        .unwrap_or_else(empty_icon);
2239
2240        let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?;
2241        let label_element = Label::new(label)
2242            .single_line()
2243            .color(color)
2244            .into_any_element();
2245
2246        Some(self.entry_element(
2247            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())),
2248            item_id,
2249            depth,
2250            icon,
2251            is_active,
2252            label_element,
2253            window,
2254            cx,
2255        ))
2256    }
2257
2258    fn excerpt_label(
2259        &self,
2260        buffer_id: BufferId,
2261        range: &ExcerptRange<language::Anchor>,
2262        cx: &App,
2263    ) -> Option<String> {
2264        let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
2265        let excerpt_range = range.context.to_point(&buffer_snapshot);
2266        Some(format!(
2267            "Lines {}- {}",
2268            excerpt_range.start.row + 1,
2269            excerpt_range.end.row + 1,
2270        ))
2271    }
2272
2273    fn render_outline(
2274        &self,
2275        outline: &OutlineEntryOutline,
2276        depth: usize,
2277        string_match: Option<&StringMatch>,
2278        window: &mut Window,
2279        cx: &mut Context<Self>,
2280    ) -> Stateful<Div> {
2281        let item_id = ElementId::from(SharedString::from(format!(
2282            "{:?}|{:?}{:?}|{:?}",
2283            outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text,
2284        )));
2285
2286        let label_element = outline::render_item(
2287            &outline.outline,
2288            string_match
2289                .map(|string_match| string_match.ranges().collect::<Vec<_>>())
2290                .unwrap_or_default(),
2291            cx,
2292        )
2293        .into_any_element();
2294
2295        let is_active = match self.selected_entry() {
2296            Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => {
2297                outline == selected && outline.outline == selected.outline
2298            }
2299            _ => false,
2300        };
2301
2302        let has_children = self
2303            .outline_children_cache
2304            .get(&outline.buffer_id)
2305            .and_then(|children_map| {
2306                let key = (outline.outline.range.clone(), outline.outline.depth);
2307                children_map.get(&key)
2308            })
2309            .copied()
2310            .unwrap_or(false);
2311        let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Outline(
2312            outline.buffer_id,
2313            outline.excerpt_id,
2314            outline.outline.range.clone(),
2315        ));
2316
2317        let icon = if has_children {
2318            FileIcons::get_chevron_icon(is_expanded, cx)
2319                .map(|icon_path| {
2320                    Icon::from_path(icon_path)
2321                        .color(entry_label_color(is_active))
2322                        .into_any_element()
2323                })
2324                .unwrap_or_else(empty_icon)
2325        } else {
2326            empty_icon()
2327        };
2328
2329        self.entry_element(
2330            PanelEntry::Outline(OutlineEntry::Outline(outline.clone())),
2331            item_id,
2332            depth,
2333            icon,
2334            is_active,
2335            label_element,
2336            window,
2337            cx,
2338        )
2339    }
2340
2341    fn render_entry(
2342        &self,
2343        rendered_entry: &FsEntry,
2344        depth: usize,
2345        string_match: Option<&StringMatch>,
2346        window: &mut Window,
2347        cx: &mut Context<Self>,
2348    ) -> Stateful<Div> {
2349        let settings = OutlinePanelSettings::get_global(cx);
2350        let is_active = match self.selected_entry() {
2351            Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
2352            _ => false,
2353        };
2354        let (item_id, label_element, icon) = match rendered_entry {
2355            FsEntry::File(FsEntryFile {
2356                worktree_id, entry, ..
2357            }) => {
2358                let name = self.entry_name(worktree_id, entry, cx);
2359                let color =
2360                    entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
2361                let icon = if settings.file_icons {
2362                    FileIcons::get_icon(entry.path.as_std_path(), cx)
2363                        .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2364                } else {
2365                    None
2366                };
2367                (
2368                    ElementId::from(entry.id.to_proto() as usize),
2369                    HighlightedLabel::new(
2370                        name,
2371                        string_match
2372                            .map(|string_match| string_match.positions.clone())
2373                            .unwrap_or_default(),
2374                    )
2375                    .color(color)
2376                    .into_any_element(),
2377                    icon.unwrap_or_else(empty_icon),
2378                )
2379            }
2380            FsEntry::Directory(directory) => {
2381                let name = self.entry_name(&directory.worktree_id, &directory.entry, cx);
2382
2383                let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir(
2384                    directory.worktree_id,
2385                    directory.entry.id,
2386                ));
2387                let color = entry_git_aware_label_color(
2388                    directory.entry.git_summary,
2389                    directory.entry.is_ignored,
2390                    is_active,
2391                );
2392                let icon = if settings.folder_icons {
2393                    FileIcons::get_folder_icon(is_expanded, directory.entry.path.as_std_path(), cx)
2394                } else {
2395                    FileIcons::get_chevron_icon(is_expanded, cx)
2396                }
2397                .map(Icon::from_path)
2398                .map(|icon| icon.color(color).into_any_element());
2399                (
2400                    ElementId::from(directory.entry.id.to_proto() as usize),
2401                    HighlightedLabel::new(
2402                        name,
2403                        string_match
2404                            .map(|string_match| string_match.positions.clone())
2405                            .unwrap_or_default(),
2406                    )
2407                    .color(color)
2408                    .into_any_element(),
2409                    icon.unwrap_or_else(empty_icon),
2410                )
2411            }
2412            FsEntry::ExternalFile(external_file) => {
2413                let color = entry_label_color(is_active);
2414                let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) {
2415                    Some(buffer_snapshot) => match buffer_snapshot.file() {
2416                        Some(file) => {
2417                            let path = file.path();
2418                            let icon = if settings.file_icons {
2419                                FileIcons::get_icon(path.as_std_path(), cx)
2420                            } else {
2421                                None
2422                            }
2423                            .map(Icon::from_path)
2424                            .map(|icon| icon.color(color).into_any_element());
2425                            (icon, file_name(path.as_std_path()))
2426                        }
2427                        None => (None, "Untitled".to_string()),
2428                    },
2429                    None => (None, "Unknown buffer".to_string()),
2430                };
2431                (
2432                    ElementId::from(external_file.buffer_id.to_proto() as usize),
2433                    HighlightedLabel::new(
2434                        name,
2435                        string_match
2436                            .map(|string_match| string_match.positions.clone())
2437                            .unwrap_or_default(),
2438                    )
2439                    .color(color)
2440                    .into_any_element(),
2441                    icon.unwrap_or_else(empty_icon),
2442                )
2443            }
2444        };
2445
2446        self.entry_element(
2447            PanelEntry::Fs(rendered_entry.clone()),
2448            item_id,
2449            depth,
2450            icon,
2451            is_active,
2452            label_element,
2453            window,
2454            cx,
2455        )
2456    }
2457
2458    fn render_folded_dirs(
2459        &self,
2460        folded_dir: &FoldedDirsEntry,
2461        depth: usize,
2462        string_match: Option<&StringMatch>,
2463        window: &mut Window,
2464        cx: &mut Context<OutlinePanel>,
2465    ) -> Stateful<Div> {
2466        let settings = OutlinePanelSettings::get_global(cx);
2467        let is_active = match self.selected_entry() {
2468            Some(PanelEntry::FoldedDirs(selected_dirs)) => {
2469                selected_dirs.worktree_id == folded_dir.worktree_id
2470                    && selected_dirs.entries == folded_dir.entries
2471            }
2472            _ => false,
2473        };
2474        let (item_id, label_element, icon) = {
2475            let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx);
2476
2477            let is_expanded = folded_dir.entries.iter().all(|dir| {
2478                !self
2479                    .collapsed_entries
2480                    .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id))
2481            });
2482            let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored);
2483            let git_status = folded_dir
2484                .entries
2485                .first()
2486                .map(|entry| entry.git_summary)
2487                .unwrap_or_default();
2488            let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
2489            let icon = if settings.folder_icons {
2490                FileIcons::get_folder_icon(is_expanded, &Path::new(&name), cx)
2491            } else {
2492                FileIcons::get_chevron_icon(is_expanded, cx)
2493            }
2494            .map(Icon::from_path)
2495            .map(|icon| icon.color(color).into_any_element());
2496            (
2497                ElementId::from(
2498                    folded_dir
2499                        .entries
2500                        .last()
2501                        .map(|entry| entry.id.to_proto())
2502                        .unwrap_or_else(|| folded_dir.worktree_id.to_proto())
2503                        as usize,
2504                ),
2505                HighlightedLabel::new(
2506                    name,
2507                    string_match
2508                        .map(|string_match| string_match.positions.clone())
2509                        .unwrap_or_default(),
2510                )
2511                .color(color)
2512                .into_any_element(),
2513                icon.unwrap_or_else(empty_icon),
2514            )
2515        };
2516
2517        self.entry_element(
2518            PanelEntry::FoldedDirs(folded_dir.clone()),
2519            item_id,
2520            depth,
2521            icon,
2522            is_active,
2523            label_element,
2524            window,
2525            cx,
2526        )
2527    }
2528
2529    fn render_search_match(
2530        &mut self,
2531        multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
2532        match_range: &Range<editor::Anchor>,
2533        render_data: &Arc<OnceLock<SearchData>>,
2534        kind: SearchKind,
2535        depth: usize,
2536        string_match: Option<&StringMatch>,
2537        window: &mut Window,
2538        cx: &mut Context<Self>,
2539    ) -> Option<Stateful<Div>> {
2540        let search_data = match render_data.get() {
2541            Some(search_data) => search_data,
2542            None => {
2543                if let ItemsDisplayMode::Search(search_state) = &mut self.mode
2544                    && let Some(multi_buffer_snapshot) = multi_buffer_snapshot
2545                {
2546                    search_state
2547                        .highlight_search_match_tx
2548                        .try_send(HighlightArguments {
2549                            multi_buffer_snapshot: multi_buffer_snapshot.clone(),
2550                            match_range: match_range.clone(),
2551                            search_data: Arc::clone(render_data),
2552                        })
2553                        .ok();
2554                }
2555                return None;
2556            }
2557        };
2558        let search_matches = string_match
2559            .iter()
2560            .flat_map(|string_match| string_match.ranges())
2561            .collect::<Vec<_>>();
2562        let match_ranges = if search_matches.is_empty() {
2563            &search_data.search_match_indices
2564        } else {
2565            &search_matches
2566        };
2567        let label_element = outline::render_item(
2568            &OutlineItem {
2569                depth,
2570                annotation_range: None,
2571                range: search_data.context_range.clone(),
2572                text: search_data.context_text.clone(),
2573                source_range_for_text: search_data.context_range.clone(),
2574                highlight_ranges: search_data
2575                    .highlights_data
2576                    .get()
2577                    .cloned()
2578                    .unwrap_or_default(),
2579                name_ranges: search_data.search_match_indices.clone(),
2580                body_range: Some(search_data.context_range.clone()),
2581            },
2582            match_ranges.iter().cloned(),
2583            cx,
2584        );
2585        let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
2586        let entire_label = h_flex()
2587            .justify_center()
2588            .p_0()
2589            .when(search_data.truncated_left, |parent| {
2590                parent.child(truncated_contents_label())
2591            })
2592            .child(label_element)
2593            .when(search_data.truncated_right, |parent| {
2594                parent.child(truncated_contents_label())
2595            })
2596            .into_any_element();
2597
2598        let is_active = match self.selected_entry() {
2599            Some(PanelEntry::Search(SearchEntry {
2600                match_range: selected_match_range,
2601                ..
2602            })) => match_range == selected_match_range,
2603            _ => false,
2604        };
2605        Some(self.entry_element(
2606            PanelEntry::Search(SearchEntry {
2607                kind,
2608                match_range: match_range.clone(),
2609                render_data: render_data.clone(),
2610            }),
2611            ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2612            depth,
2613            empty_icon(),
2614            is_active,
2615            entire_label,
2616            window,
2617            cx,
2618        ))
2619    }
2620
2621    fn entry_element(
2622        &self,
2623        rendered_entry: PanelEntry,
2624        item_id: ElementId,
2625        depth: usize,
2626        icon_element: AnyElement,
2627        is_active: bool,
2628        label_element: gpui::AnyElement,
2629        window: &mut Window,
2630        cx: &mut Context<OutlinePanel>,
2631    ) -> Stateful<Div> {
2632        let settings = OutlinePanelSettings::get_global(cx);
2633        div()
2634            .text_ui(cx)
2635            .id(item_id.clone())
2636            .on_click({
2637                let clicked_entry = rendered_entry.clone();
2638                cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| {
2639                    if event.is_right_click() || event.first_focus() {
2640                        return;
2641                    }
2642
2643                    let change_focus = event.click_count() > 1;
2644                    outline_panel.toggle_expanded(&clicked_entry, window, cx);
2645
2646                    outline_panel.scroll_editor_to_entry(
2647                        &clicked_entry,
2648                        true,
2649                        change_focus,
2650                        window,
2651                        cx,
2652                    );
2653                })
2654            })
2655            .cursor_pointer()
2656            .child(
2657                ListItem::new(item_id)
2658                    .indent_level(depth)
2659                    .indent_step_size(px(settings.indent_size))
2660                    .toggle_state(is_active)
2661                    .child(
2662                        h_flex()
2663                            .child(h_flex().w(px(16.)).justify_center().child(icon_element))
2664                            .child(h_flex().h_6().child(label_element).ml_1()),
2665                    )
2666                    .on_secondary_mouse_down(cx.listener(
2667                        move |outline_panel, event: &MouseDownEvent, window, cx| {
2668                            // Stop propagation to prevent the catch-all context menu for the project
2669                            // panel from being deployed.
2670                            cx.stop_propagation();
2671                            outline_panel.deploy_context_menu(
2672                                event.position,
2673                                rendered_entry.clone(),
2674                                window,
2675                                cx,
2676                            )
2677                        },
2678                    )),
2679            )
2680            .border_1()
2681            .border_r_2()
2682            .rounded_none()
2683            .hover(|style| {
2684                if is_active {
2685                    style
2686                } else {
2687                    let hover_color = cx.theme().colors().ghost_element_hover;
2688                    style.bg(hover_color).border_color(hover_color)
2689                }
2690            })
2691            .when(
2692                is_active && self.focus_handle.contains_focused(window, cx),
2693                |div| div.border_color(cx.theme().colors().panel_focused_border),
2694            )
2695    }
2696
2697    fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
2698        match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2699            Some(worktree) => {
2700                let worktree = worktree.read(cx);
2701                match worktree.snapshot().root_entry() {
2702                    Some(root_entry) => {
2703                        if root_entry.id == entry.id {
2704                            file_name(worktree.abs_path().as_ref())
2705                        } else {
2706                            let path = worktree.absolutize(entry.path.as_ref());
2707                            file_name(&path)
2708                        }
2709                    }
2710                    None => {
2711                        let path = worktree.absolutize(entry.path.as_ref());
2712                        file_name(&path)
2713                    }
2714                }
2715            }
2716            None => file_name(entry.path.as_std_path()),
2717        }
2718    }
2719
2720    fn update_fs_entries(
2721        &mut self,
2722        active_editor: Entity<Editor>,
2723        debounce: Option<Duration>,
2724        window: &mut Window,
2725        cx: &mut Context<Self>,
2726    ) {
2727        if !self.active {
2728            return;
2729        }
2730
2731        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2732        let active_multi_buffer = active_editor.read(cx).buffer().clone();
2733        let new_entries = self.new_entries_for_fs_update.clone();
2734        let repo_snapshots = self.project.update(cx, |project, cx| {
2735            project.git_store().read(cx).repo_snapshots(cx)
2736        });
2737        self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
2738            if let Some(debounce) = debounce {
2739                cx.background_executor().timer(debounce).await;
2740            }
2741
2742            let mut new_collapsed_entries = HashSet::default();
2743            let mut new_unfolded_dirs = HashMap::default();
2744            let mut root_entries = HashSet::default();
2745            let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2746            let Ok(buffer_excerpts) = outline_panel.update(cx, |outline_panel, cx| {
2747                let git_store = outline_panel.project.read(cx).git_store().clone();
2748                new_collapsed_entries = outline_panel.collapsed_entries.clone();
2749                new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
2750                let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2751
2752                multi_buffer_snapshot.excerpts().fold(
2753                    HashMap::default(),
2754                    |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
2755                        let buffer_id = buffer_snapshot.remote_id();
2756                        let file = File::from_dyn(buffer_snapshot.file());
2757                        let entry_id = file.and_then(|file| file.project_entry_id());
2758                        let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2759                        let is_new = new_entries.contains(&excerpt_id)
2760                            || !outline_panel.excerpts.contains_key(&buffer_id);
2761                        let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx);
2762                        let status = git_store
2763                            .read(cx)
2764                            .repository_and_path_for_buffer_id(buffer_id, cx)
2765                            .and_then(|(repo, path)| {
2766                                Some(repo.read(cx).status_for_path(&path)?.status)
2767                            });
2768                        buffer_excerpts
2769                            .entry(buffer_id)
2770                            .or_insert_with(|| {
2771                                (is_new, is_folded, Vec::new(), entry_id, worktree, status)
2772                            })
2773                            .2
2774                            .push(excerpt_id);
2775
2776                        let outlines = match outline_panel
2777                            .excerpts
2778                            .get(&buffer_id)
2779                            .and_then(|excerpts| excerpts.get(&excerpt_id))
2780                        {
2781                            Some(old_excerpt) => match &old_excerpt.outlines {
2782                                ExcerptOutlines::Outlines(outlines) => {
2783                                    ExcerptOutlines::Outlines(outlines.clone())
2784                                }
2785                                ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2786                                ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2787                            },
2788                            None => ExcerptOutlines::NotFetched,
2789                        };
2790                        new_excerpts.entry(buffer_id).or_default().insert(
2791                            excerpt_id,
2792                            Excerpt {
2793                                range: excerpt_range,
2794                                outlines,
2795                            },
2796                        );
2797                        buffer_excerpts
2798                    },
2799                )
2800            }) else {
2801                return;
2802            };
2803
2804            let Some((
2805                new_collapsed_entries,
2806                new_unfolded_dirs,
2807                new_fs_entries,
2808                new_depth_map,
2809                new_children_count,
2810            )) = cx
2811                .background_spawn(async move {
2812                    let mut processed_external_buffers = HashSet::default();
2813                    let mut new_worktree_entries =
2814                        BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
2815                    let mut worktree_excerpts = HashMap::<
2816                        WorktreeId,
2817                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2818                    >::default();
2819                    let mut external_excerpts = HashMap::default();
2820
2821                    for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in
2822                        buffer_excerpts
2823                    {
2824                        if is_folded {
2825                            match &worktree {
2826                                Some(worktree) => {
2827                                    new_collapsed_entries
2828                                        .insert(CollapsedEntry::File(worktree.id(), buffer_id));
2829                                }
2830                                None => {
2831                                    new_collapsed_entries
2832                                        .insert(CollapsedEntry::ExternalFile(buffer_id));
2833                                }
2834                            }
2835                        } else if is_new {
2836                            match &worktree {
2837                                Some(worktree) => {
2838                                    new_collapsed_entries
2839                                        .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2840                                }
2841                                None => {
2842                                    new_collapsed_entries
2843                                        .remove(&CollapsedEntry::ExternalFile(buffer_id));
2844                                }
2845                            }
2846                        }
2847
2848                        if let Some(worktree) = worktree {
2849                            let worktree_id = worktree.id();
2850                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2851
2852                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2853                                Some(entry) => {
2854                                    let entry = GitEntry {
2855                                        git_summary: status
2856                                            .map(|status| status.summary())
2857                                            .unwrap_or_default(),
2858                                        entry,
2859                                    };
2860                                    let mut traversal = GitTraversal::new(
2861                                        &repo_snapshots,
2862                                        worktree.traverse_from_path(
2863                                            true,
2864                                            true,
2865                                            true,
2866                                            entry.path.as_ref(),
2867                                        ),
2868                                    );
2869
2870                                    let mut entries_to_add = HashMap::default();
2871                                    worktree_excerpts
2872                                        .entry(worktree_id)
2873                                        .or_default()
2874                                        .insert(entry.id, (buffer_id, excerpts));
2875                                    let mut current_entry = entry;
2876                                    loop {
2877                                        if current_entry.is_dir() {
2878                                            let is_root =
2879                                                worktree.root_entry().map(|entry| entry.id)
2880                                                    == Some(current_entry.id);
2881                                            if is_root {
2882                                                root_entries.insert(current_entry.id);
2883                                                if auto_fold_dirs {
2884                                                    unfolded_dirs.insert(current_entry.id);
2885                                                }
2886                                            }
2887                                            if is_new {
2888                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
2889                                                    worktree_id,
2890                                                    current_entry.id,
2891                                                ));
2892                                            }
2893                                        }
2894
2895                                        let new_entry_added = entries_to_add
2896                                            .insert(current_entry.id, current_entry)
2897                                            .is_none();
2898                                        if new_entry_added
2899                                            && traversal.back_to_parent()
2900                                            && let Some(parent_entry) = traversal.entry()
2901                                        {
2902                                            current_entry = parent_entry.to_owned();
2903                                            continue;
2904                                        }
2905                                        break;
2906                                    }
2907                                    new_worktree_entries
2908                                        .entry(worktree_id)
2909                                        .or_insert_with(HashMap::default)
2910                                        .extend(entries_to_add);
2911                                }
2912                                None => {
2913                                    if processed_external_buffers.insert(buffer_id) {
2914                                        external_excerpts
2915                                            .entry(buffer_id)
2916                                            .or_insert_with(Vec::new)
2917                                            .extend(excerpts);
2918                                    }
2919                                }
2920                            }
2921                        } else if processed_external_buffers.insert(buffer_id) {
2922                            external_excerpts
2923                                .entry(buffer_id)
2924                                .or_insert_with(Vec::new)
2925                                .extend(excerpts);
2926                        }
2927                    }
2928
2929                    let mut new_children_count =
2930                        HashMap::<WorktreeId, HashMap<Arc<RelPath>, FsChildren>>::default();
2931
2932                    let worktree_entries = new_worktree_entries
2933                        .into_iter()
2934                        .map(|(worktree_id, entries)| {
2935                            let mut entries = entries.into_values().collect::<Vec<_>>();
2936                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2937                            (worktree_id, entries)
2938                        })
2939                        .flat_map(|(worktree_id, entries)| {
2940                            {
2941                                entries
2942                                    .into_iter()
2943                                    .filter_map(|entry| {
2944                                        if auto_fold_dirs && let Some(parent) = entry.path.parent()
2945                                        {
2946                                            let children = new_children_count
2947                                                .entry(worktree_id)
2948                                                .or_default()
2949                                                .entry(Arc::from(parent))
2950                                                .or_default();
2951                                            if entry.is_dir() {
2952                                                children.dirs += 1;
2953                                            } else {
2954                                                children.files += 1;
2955                                            }
2956                                        }
2957
2958                                        if entry.is_dir() {
2959                                            Some(FsEntry::Directory(FsEntryDirectory {
2960                                                worktree_id,
2961                                                entry,
2962                                            }))
2963                                        } else {
2964                                            let (buffer_id, excerpts) = worktree_excerpts
2965                                                .get_mut(&worktree_id)
2966                                                .and_then(|worktree_excerpts| {
2967                                                    worktree_excerpts.remove(&entry.id)
2968                                                })?;
2969                                            Some(FsEntry::File(FsEntryFile {
2970                                                worktree_id,
2971                                                buffer_id,
2972                                                entry,
2973                                                excerpts,
2974                                            }))
2975                                        }
2976                                    })
2977                                    .collect::<Vec<_>>()
2978                            }
2979                        })
2980                        .collect::<Vec<_>>();
2981
2982                    let mut visited_dirs = Vec::new();
2983                    let mut new_depth_map = HashMap::default();
2984                    let new_visible_entries = external_excerpts
2985                        .into_iter()
2986                        .sorted_by_key(|(id, _)| *id)
2987                        .map(|(buffer_id, excerpts)| {
2988                            FsEntry::ExternalFile(FsEntryExternalFile {
2989                                buffer_id,
2990                                excerpts,
2991                            })
2992                        })
2993                        .chain(worktree_entries)
2994                        .filter(|visible_item| {
2995                            match visible_item {
2996                                FsEntry::Directory(directory) => {
2997                                    let parent_id = back_to_common_visited_parent(
2998                                        &mut visited_dirs,
2999                                        &directory.worktree_id,
3000                                        &directory.entry,
3001                                    );
3002
3003                                    let mut depth = 0;
3004                                    if !root_entries.contains(&directory.entry.id) {
3005                                        if auto_fold_dirs {
3006                                            let children = new_children_count
3007                                                .get(&directory.worktree_id)
3008                                                .and_then(|children_count| {
3009                                                    children_count.get(&directory.entry.path)
3010                                                })
3011                                                .copied()
3012                                                .unwrap_or_default();
3013
3014                                            if !children.may_be_fold_part()
3015                                                || (children.dirs == 0
3016                                                    && visited_dirs
3017                                                        .last()
3018                                                        .map(|(parent_dir_id, _)| {
3019                                                            new_unfolded_dirs
3020                                                                .get(&directory.worktree_id)
3021                                                                .is_none_or(|unfolded_dirs| {
3022                                                                    unfolded_dirs
3023                                                                        .contains(parent_dir_id)
3024                                                                })
3025                                                        })
3026                                                        .unwrap_or(true))
3027                                            {
3028                                                new_unfolded_dirs
3029                                                    .entry(directory.worktree_id)
3030                                                    .or_default()
3031                                                    .insert(directory.entry.id);
3032                                            }
3033                                        }
3034
3035                                        depth = parent_id
3036                                            .and_then(|(worktree_id, id)| {
3037                                                new_depth_map.get(&(worktree_id, id)).copied()
3038                                            })
3039                                            .unwrap_or(0)
3040                                            + 1;
3041                                    };
3042                                    visited_dirs
3043                                        .push((directory.entry.id, directory.entry.path.clone()));
3044                                    new_depth_map
3045                                        .insert((directory.worktree_id, directory.entry.id), depth);
3046                                }
3047                                FsEntry::File(FsEntryFile {
3048                                    worktree_id,
3049                                    entry: file_entry,
3050                                    ..
3051                                }) => {
3052                                    let parent_id = back_to_common_visited_parent(
3053                                        &mut visited_dirs,
3054                                        worktree_id,
3055                                        file_entry,
3056                                    );
3057                                    let depth = if root_entries.contains(&file_entry.id) {
3058                                        0
3059                                    } else {
3060                                        parent_id
3061                                            .and_then(|(worktree_id, id)| {
3062                                                new_depth_map.get(&(worktree_id, id)).copied()
3063                                            })
3064                                            .unwrap_or(0)
3065                                            + 1
3066                                    };
3067                                    new_depth_map.insert((*worktree_id, file_entry.id), depth);
3068                                }
3069                                FsEntry::ExternalFile(..) => {
3070                                    visited_dirs.clear();
3071                                }
3072                            }
3073
3074                            true
3075                        })
3076                        .collect::<Vec<_>>();
3077
3078                    anyhow::Ok((
3079                        new_collapsed_entries,
3080                        new_unfolded_dirs,
3081                        new_visible_entries,
3082                        new_depth_map,
3083                        new_children_count,
3084                    ))
3085                })
3086                .await
3087                .log_err()
3088            else {
3089                return;
3090            };
3091
3092            outline_panel
3093                .update_in(cx, |outline_panel, window, cx| {
3094                    outline_panel.new_entries_for_fs_update.clear();
3095                    outline_panel.excerpts = new_excerpts;
3096                    outline_panel.collapsed_entries = new_collapsed_entries;
3097                    outline_panel.unfolded_dirs = new_unfolded_dirs;
3098                    outline_panel.fs_entries = new_fs_entries;
3099                    outline_panel.fs_entries_depth = new_depth_map;
3100                    outline_panel.fs_children_count = new_children_count;
3101                    outline_panel.update_non_fs_items(window, cx);
3102
3103                    // Only update cached entries if we don't have outlines to fetch
3104                    // If we do have outlines to fetch, let fetch_outdated_outlines handle the update
3105                    if outline_panel.excerpt_fetch_ranges(cx).is_empty() {
3106                        outline_panel.update_cached_entries(debounce, window, cx);
3107                    }
3108
3109                    cx.notify();
3110                })
3111                .ok();
3112        });
3113    }
3114
3115    fn replace_active_editor(
3116        &mut self,
3117        new_active_item: Box<dyn ItemHandle>,
3118        new_active_editor: Entity<Editor>,
3119        window: &mut Window,
3120        cx: &mut Context<Self>,
3121    ) {
3122        self.clear_previous(window, cx);
3123
3124        let default_expansion_depth =
3125            OutlinePanelSettings::get_global(cx).expand_outlines_with_depth;
3126        // We'll apply the expansion depth after outlines are loaded
3127        self.pending_default_expansion_depth = Some(default_expansion_depth);
3128
3129        let buffer_search_subscription = cx.subscribe_in(
3130            &new_active_editor,
3131            window,
3132            |outline_panel: &mut Self,
3133             _,
3134             e: &SearchEvent,
3135             window: &mut Window,
3136             cx: &mut Context<Self>| {
3137                if matches!(e, SearchEvent::MatchesInvalidated) {
3138                    let update_cached_items = outline_panel.update_search_matches(window, cx);
3139                    if update_cached_items {
3140                        outline_panel.selected_entry.invalidate();
3141                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
3142                    }
3143                };
3144                outline_panel.autoscroll(cx);
3145            },
3146        );
3147        self.active_item = Some(ActiveItem {
3148            _buffer_search_subscription: buffer_search_subscription,
3149            _editor_subscription: subscribe_for_editor_events(&new_active_editor, window, cx),
3150            item_handle: new_active_item.downgrade_item(),
3151            active_editor: new_active_editor.downgrade(),
3152        });
3153        self.new_entries_for_fs_update
3154            .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
3155        self.selected_entry.invalidate();
3156        self.update_fs_entries(new_active_editor, None, window, cx);
3157    }
3158
3159    fn clear_previous(&mut self, window: &mut Window, cx: &mut App) {
3160        self.fs_entries_update_task = Task::ready(());
3161        self.outline_fetch_tasks.clear();
3162        self.cached_entries_update_task = Task::ready(());
3163        self.reveal_selection_task = Task::ready(Ok(()));
3164        self.filter_editor
3165            .update(cx, |editor, cx| editor.clear(window, cx));
3166        self.collapsed_entries.clear();
3167        self.unfolded_dirs.clear();
3168        self.active_item = None;
3169        self.fs_entries.clear();
3170        self.fs_entries_depth.clear();
3171        self.fs_children_count.clear();
3172        self.excerpts.clear();
3173        self.cached_entries = Vec::new();
3174        self.selected_entry = SelectedEntry::None;
3175        self.pinned = false;
3176        self.mode = ItemsDisplayMode::Outline;
3177        self.pending_default_expansion_depth = None;
3178    }
3179
3180    fn location_for_editor_selection(
3181        &self,
3182        editor: &Entity<Editor>,
3183        window: &mut Window,
3184        cx: &mut Context<Self>,
3185    ) -> Option<PanelEntry> {
3186        let selection = editor.update(cx, |editor, cx| {
3187            editor
3188                .selections
3189                .newest::<language::Point>(&editor.display_snapshot(cx))
3190                .head()
3191        });
3192        let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
3193        let multi_buffer = editor.read(cx).buffer();
3194        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
3195        let (excerpt_id, buffer, _) = editor
3196            .read(cx)
3197            .buffer()
3198            .read(cx)
3199            .excerpt_containing(selection, cx)?;
3200        let buffer_id = buffer.read(cx).remote_id();
3201
3202        if editor.read(cx).is_buffer_folded(buffer_id, cx) {
3203            return self
3204                .fs_entries
3205                .iter()
3206                .find(|fs_entry| match fs_entry {
3207                    FsEntry::Directory(..) => false,
3208                    FsEntry::File(FsEntryFile {
3209                        buffer_id: other_buffer_id,
3210                        ..
3211                    })
3212                    | FsEntry::ExternalFile(FsEntryExternalFile {
3213                        buffer_id: other_buffer_id,
3214                        ..
3215                    }) => buffer_id == *other_buffer_id,
3216                })
3217                .cloned()
3218                .map(PanelEntry::Fs);
3219        }
3220
3221        let selection_display_point = selection.to_display_point(&editor_snapshot);
3222
3223        match &self.mode {
3224            ItemsDisplayMode::Search(search_state) => search_state
3225                .matches
3226                .iter()
3227                .rev()
3228                .min_by_key(|&(match_range, _)| {
3229                    let match_display_range =
3230                        match_range.clone().to_display_points(&editor_snapshot);
3231                    let start_distance = if selection_display_point < match_display_range.start {
3232                        match_display_range.start - selection_display_point
3233                    } else {
3234                        selection_display_point - match_display_range.start
3235                    };
3236                    let end_distance = if selection_display_point < match_display_range.end {
3237                        match_display_range.end - selection_display_point
3238                    } else {
3239                        selection_display_point - match_display_range.end
3240                    };
3241                    start_distance + end_distance
3242                })
3243                .and_then(|(closest_range, _)| {
3244                    self.cached_entries.iter().find_map(|cached_entry| {
3245                        if let PanelEntry::Search(SearchEntry { match_range, .. }) =
3246                            &cached_entry.entry
3247                        {
3248                            if match_range == closest_range {
3249                                Some(cached_entry.entry.clone())
3250                            } else {
3251                                None
3252                            }
3253                        } else {
3254                            None
3255                        }
3256                    })
3257                }),
3258            ItemsDisplayMode::Outline => self.outline_location(
3259                buffer_id,
3260                excerpt_id,
3261                multi_buffer_snapshot,
3262                editor_snapshot,
3263                selection_display_point,
3264            ),
3265        }
3266    }
3267
3268    fn outline_location(
3269        &self,
3270        buffer_id: BufferId,
3271        excerpt_id: ExcerptId,
3272        multi_buffer_snapshot: editor::MultiBufferSnapshot,
3273        editor_snapshot: editor::EditorSnapshot,
3274        selection_display_point: DisplayPoint,
3275    ) -> Option<PanelEntry> {
3276        let excerpt_outlines = self
3277            .excerpts
3278            .get(&buffer_id)
3279            .and_then(|excerpts| excerpts.get(&excerpt_id))
3280            .into_iter()
3281            .flat_map(|excerpt| excerpt.iter_outlines())
3282            .flat_map(|outline| {
3283                let range = multi_buffer_snapshot
3284                    .anchor_range_in_excerpt(excerpt_id, outline.range.clone())?;
3285                Some((
3286                    range.start.to_display_point(&editor_snapshot)
3287                        ..range.end.to_display_point(&editor_snapshot),
3288                    outline,
3289                ))
3290            })
3291            .collect::<Vec<_>>();
3292
3293        let mut matching_outline_indices = Vec::new();
3294        let mut children = HashMap::default();
3295        let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
3296
3297        for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
3298            if outline_range
3299                .to_inclusive()
3300                .contains(&selection_display_point)
3301            {
3302                matching_outline_indices.push(i);
3303            } else if (outline_range.start.row()..outline_range.end.row())
3304                .to_inclusive()
3305                .contains(&selection_display_point.row())
3306            {
3307                matching_outline_indices.push(i);
3308            }
3309
3310            while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
3311                if parent_outline.depth >= outline.depth
3312                    || !parent_range.contains(&outline_range.start)
3313                {
3314                    parents_stack.pop();
3315                } else {
3316                    break;
3317                }
3318            }
3319            if let Some((_, _, parent_index)) = parents_stack.last_mut() {
3320                children
3321                    .entry(*parent_index)
3322                    .or_insert_with(Vec::new)
3323                    .push(i);
3324            }
3325            parents_stack.push((outline_range, outline, i));
3326        }
3327
3328        let outline_item = matching_outline_indices
3329            .into_iter()
3330            .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
3331            .filter(|(i, _)| {
3332                children
3333                    .get(i)
3334                    .map(|children| {
3335                        children.iter().all(|child_index| {
3336                            excerpt_outlines
3337                                .get(*child_index)
3338                                .map(|(child_range, _)| child_range.start > selection_display_point)
3339                                .unwrap_or(false)
3340                        })
3341                    })
3342                    .unwrap_or(true)
3343            })
3344            .min_by_key(|(_, (outline_range, outline))| {
3345                let distance_from_start = if outline_range.start > selection_display_point {
3346                    outline_range.start - selection_display_point
3347                } else {
3348                    selection_display_point - outline_range.start
3349                };
3350                let distance_from_end = if outline_range.end > selection_display_point {
3351                    outline_range.end - selection_display_point
3352                } else {
3353                    selection_display_point - outline_range.end
3354                };
3355
3356                (
3357                    cmp::Reverse(outline.depth),
3358                    distance_from_start + distance_from_end,
3359                )
3360            })
3361            .map(|(_, (_, outline))| *outline)
3362            .cloned();
3363
3364        let closest_container = match outline_item {
3365            Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
3366                buffer_id,
3367                excerpt_id,
3368                outline,
3369            })),
3370            None => {
3371                self.cached_entries.iter().rev().find_map(|cached_entry| {
3372                    match &cached_entry.entry {
3373                        PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
3374                            if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id {
3375                                Some(cached_entry.entry.clone())
3376                            } else {
3377                                None
3378                            }
3379                        }
3380                        PanelEntry::Fs(
3381                            FsEntry::ExternalFile(FsEntryExternalFile {
3382                                buffer_id: file_buffer_id,
3383                                excerpts: file_excerpts,
3384                            })
3385                            | FsEntry::File(FsEntryFile {
3386                                buffer_id: file_buffer_id,
3387                                excerpts: file_excerpts,
3388                                ..
3389                            }),
3390                        ) => {
3391                            if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
3392                                Some(cached_entry.entry.clone())
3393                            } else {
3394                                None
3395                            }
3396                        }
3397                        _ => None,
3398                    }
3399                })?
3400            }
3401        };
3402        Some(closest_container)
3403    }
3404
3405    fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3406        let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
3407        if excerpt_fetch_ranges.is_empty() {
3408            return;
3409        }
3410
3411        let syntax_theme = cx.theme().syntax().clone();
3412        let first_update = Arc::new(AtomicBool::new(true));
3413        for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
3414            for (excerpt_id, excerpt_range) in excerpt_ranges {
3415                let syntax_theme = syntax_theme.clone();
3416                let buffer_snapshot = buffer_snapshot.clone();
3417                let first_update = first_update.clone();
3418                self.outline_fetch_tasks.insert(
3419                    (buffer_id, excerpt_id),
3420                    cx.spawn_in(window, async move |outline_panel, cx| {
3421                        let buffer_language = buffer_snapshot.language().cloned();
3422                        let fetched_outlines = cx
3423                            .background_spawn(async move {
3424                                let mut outlines = buffer_snapshot.outline_items_containing(
3425                                    excerpt_range.context,
3426                                    false,
3427                                    Some(&syntax_theme),
3428                                );
3429                                outlines.retain(|outline| {
3430                                    buffer_language.is_none()
3431                                        || buffer_language.as_ref()
3432                                            == buffer_snapshot.language_at(outline.range.start)
3433                                });
3434
3435                                let outlines_with_children = outlines
3436                                    .windows(2)
3437                                    .filter_map(|window| {
3438                                        let current = &window[0];
3439                                        let next = &window[1];
3440                                        if next.depth > current.depth {
3441                                            Some((current.range.clone(), current.depth))
3442                                        } else {
3443                                            None
3444                                        }
3445                                    })
3446                                    .collect::<HashSet<_>>();
3447
3448                                (outlines, outlines_with_children)
3449                            })
3450                            .await;
3451
3452                        let (fetched_outlines, outlines_with_children) = fetched_outlines;
3453
3454                        outline_panel
3455                            .update_in(cx, |outline_panel, window, cx| {
3456                                let pending_default_depth =
3457                                    outline_panel.pending_default_expansion_depth.take();
3458
3459                                let debounce =
3460                                    if first_update.fetch_and(false, atomic::Ordering::AcqRel) {
3461                                        None
3462                                    } else {
3463                                        Some(UPDATE_DEBOUNCE)
3464                                    };
3465
3466                                if let Some(excerpt) = outline_panel
3467                                    .excerpts
3468                                    .entry(buffer_id)
3469                                    .or_default()
3470                                    .get_mut(&excerpt_id)
3471                                {
3472                                    excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
3473
3474                                    if let Some(default_depth) = pending_default_depth
3475                                        && let ExcerptOutlines::Outlines(outlines) =
3476                                            &excerpt.outlines
3477                                    {
3478                                        outlines
3479                                            .iter()
3480                                            .filter(|outline| {
3481                                                (default_depth == 0
3482                                                    || outline.depth >= default_depth)
3483                                                    && outlines_with_children.contains(&(
3484                                                        outline.range.clone(),
3485                                                        outline.depth,
3486                                                    ))
3487                                            })
3488                                            .for_each(|outline| {
3489                                                outline_panel.collapsed_entries.insert(
3490                                                    CollapsedEntry::Outline(
3491                                                        buffer_id,
3492                                                        excerpt_id,
3493                                                        outline.range.clone(),
3494                                                    ),
3495                                                );
3496                                            });
3497                                    }
3498
3499                                    // Even if no outlines to check, we still need to update cached entries
3500                                    // to show the outline entries that were just fetched
3501                                    outline_panel.update_cached_entries(debounce, window, cx);
3502                                }
3503                            })
3504                            .ok();
3505                    }),
3506                );
3507            }
3508        }
3509    }
3510
3511    fn is_singleton_active(&self, cx: &App) -> bool {
3512        self.active_editor()
3513            .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton())
3514    }
3515
3516    fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
3517        self.outline_fetch_tasks.clear();
3518        let mut ids = ids.iter().collect::<HashSet<_>>();
3519        for excerpts in self.excerpts.values_mut() {
3520            ids.retain(|id| {
3521                if let Some(excerpt) = excerpts.get_mut(id) {
3522                    excerpt.invalidate_outlines();
3523                    false
3524                } else {
3525                    true
3526                }
3527            });
3528            if ids.is_empty() {
3529                break;
3530            }
3531        }
3532    }
3533
3534    fn excerpt_fetch_ranges(
3535        &self,
3536        cx: &App,
3537    ) -> HashMap<
3538        BufferId,
3539        (
3540            BufferSnapshot,
3541            HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
3542        ),
3543    > {
3544        self.fs_entries
3545            .iter()
3546            .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
3547                match fs_entry {
3548                    FsEntry::File(FsEntryFile {
3549                        buffer_id,
3550                        excerpts: file_excerpts,
3551                        ..
3552                    })
3553                    | FsEntry::ExternalFile(FsEntryExternalFile {
3554                        buffer_id,
3555                        excerpts: file_excerpts,
3556                    }) => {
3557                        let excerpts = self.excerpts.get(buffer_id);
3558                        for &file_excerpt in file_excerpts {
3559                            if let Some(excerpt) = excerpts
3560                                .and_then(|excerpts| excerpts.get(&file_excerpt))
3561                                .filter(|excerpt| excerpt.should_fetch_outlines())
3562                            {
3563                                match excerpts_to_fetch.entry(*buffer_id) {
3564                                    hash_map::Entry::Occupied(mut o) => {
3565                                        o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
3566                                    }
3567                                    hash_map::Entry::Vacant(v) => {
3568                                        if let Some(buffer_snapshot) =
3569                                            self.buffer_snapshot_for_id(*buffer_id, cx)
3570                                        {
3571                                            v.insert((buffer_snapshot, HashMap::default()))
3572                                                .1
3573                                                .insert(file_excerpt, excerpt.range.clone());
3574                                        }
3575                                    }
3576                                }
3577                            }
3578                        }
3579                    }
3580                    FsEntry::Directory(..) => {}
3581                }
3582                excerpts_to_fetch
3583            })
3584    }
3585
3586    fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option<BufferSnapshot> {
3587        let editor = self.active_editor()?;
3588        Some(
3589            editor
3590                .read(cx)
3591                .buffer()
3592                .read(cx)
3593                .buffer(buffer_id)?
3594                .read(cx)
3595                .snapshot(),
3596        )
3597    }
3598
3599    fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option<PathBuf> {
3600        match entry {
3601            PanelEntry::Fs(
3602                FsEntry::File(FsEntryFile { buffer_id, .. })
3603                | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }),
3604            ) => self
3605                .buffer_snapshot_for_id(*buffer_id, cx)
3606                .and_then(|buffer_snapshot| {
3607                    let file = File::from_dyn(buffer_snapshot.file())?;
3608                    Some(file.worktree.read(cx).absolutize(&file.path))
3609                }),
3610            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3611                worktree_id, entry, ..
3612            })) => Some(
3613                self.project
3614                    .read(cx)
3615                    .worktree_for_id(*worktree_id, cx)?
3616                    .read(cx)
3617                    .absolutize(&entry.path),
3618            ),
3619            PanelEntry::FoldedDirs(FoldedDirsEntry {
3620                worktree_id,
3621                entries: dirs,
3622                ..
3623            }) => dirs.last().and_then(|entry| {
3624                self.project
3625                    .read(cx)
3626                    .worktree_for_id(*worktree_id, cx)
3627                    .map(|worktree| worktree.read(cx).absolutize(&entry.path))
3628            }),
3629            PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
3630        }
3631    }
3632
3633    fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<RelPath>> {
3634        match entry {
3635            FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3636                let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
3637                Some(buffer_snapshot.file()?.path().clone())
3638            }
3639            FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()),
3640            FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()),
3641        }
3642    }
3643
3644    fn update_cached_entries(
3645        &mut self,
3646        debounce: Option<Duration>,
3647        window: &mut Window,
3648        cx: &mut Context<OutlinePanel>,
3649    ) {
3650        if !self.active {
3651            return;
3652        }
3653
3654        let is_singleton = self.is_singleton_active(cx);
3655        let query = self.query(cx);
3656        self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
3657            if let Some(debounce) = debounce {
3658                cx.background_executor().timer(debounce).await;
3659            }
3660            let Some(new_cached_entries) = outline_panel
3661                .update_in(cx, |outline_panel, window, cx| {
3662                    outline_panel.generate_cached_entries(is_singleton, query, window, cx)
3663                })
3664                .ok()
3665            else {
3666                return;
3667            };
3668            let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
3669            outline_panel
3670                .update_in(cx, |outline_panel, window, cx| {
3671                    outline_panel.cached_entries = new_cached_entries;
3672                    outline_panel.max_width_item_index = max_width_item_index;
3673                    if (outline_panel.selected_entry.is_invalidated()
3674                        || matches!(outline_panel.selected_entry, SelectedEntry::None))
3675                        && let Some(new_selected_entry) =
3676                            outline_panel.active_editor().and_then(|active_editor| {
3677                                outline_panel.location_for_editor_selection(
3678                                    &active_editor,
3679                                    window,
3680                                    cx,
3681                                )
3682                            })
3683                    {
3684                        outline_panel.select_entry(new_selected_entry, false, window, cx);
3685                    }
3686
3687                    outline_panel.autoscroll(cx);
3688                    cx.notify();
3689                })
3690                .ok();
3691        });
3692    }
3693
3694    fn generate_cached_entries(
3695        &self,
3696        is_singleton: bool,
3697        query: Option<String>,
3698        window: &mut Window,
3699        cx: &mut Context<Self>,
3700    ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
3701        let project = self.project.clone();
3702        let Some(active_editor) = self.active_editor() else {
3703            return Task::ready((Vec::new(), None));
3704        };
3705        cx.spawn_in(window, async move |outline_panel, cx| {
3706            let mut generation_state = GenerationState::default();
3707
3708            let Ok(()) = outline_panel.update(cx, |outline_panel, cx| {
3709                let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
3710                let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
3711                let track_matches = query.is_some();
3712
3713                #[derive(Debug)]
3714                struct ParentStats {
3715                    path: Arc<RelPath>,
3716                    folded: bool,
3717                    expanded: bool,
3718                    depth: usize,
3719                }
3720                let mut parent_dirs = Vec::<ParentStats>::new();
3721                for entry in outline_panel.fs_entries.clone() {
3722                    let is_expanded = outline_panel.is_expanded(&entry);
3723                    let (depth, should_add) = match &entry {
3724                        FsEntry::Directory(directory_entry) => {
3725                            let mut should_add = true;
3726                            let is_root = project
3727                                .read(cx)
3728                                .worktree_for_id(directory_entry.worktree_id, cx)
3729                                .is_some_and(|worktree| {
3730                                    worktree.read(cx).root_entry() == Some(&directory_entry.entry)
3731                                });
3732                            let folded = auto_fold_dirs
3733                                && !is_root
3734                                && outline_panel
3735                                    .unfolded_dirs
3736                                    .get(&directory_entry.worktree_id)
3737                                    .is_none_or(|unfolded_dirs| {
3738                                        !unfolded_dirs.contains(&directory_entry.entry.id)
3739                                    });
3740                            let fs_depth = outline_panel
3741                                .fs_entries_depth
3742                                .get(&(directory_entry.worktree_id, directory_entry.entry.id))
3743                                .copied()
3744                                .unwrap_or(0);
3745                            while let Some(parent) = parent_dirs.last() {
3746                                if !is_root && directory_entry.entry.path.starts_with(&parent.path)
3747                                {
3748                                    break;
3749                                }
3750                                parent_dirs.pop();
3751                            }
3752                            let auto_fold = match parent_dirs.last() {
3753                                Some(parent) => {
3754                                    parent.folded
3755                                        && Some(parent.path.as_ref())
3756                                            == directory_entry.entry.path.parent()
3757                                        && outline_panel
3758                                            .fs_children_count
3759                                            .get(&directory_entry.worktree_id)
3760                                            .and_then(|entries| {
3761                                                entries.get(&directory_entry.entry.path)
3762                                            })
3763                                            .copied()
3764                                            .unwrap_or_default()
3765                                            .may_be_fold_part()
3766                                }
3767                                None => false,
3768                            };
3769                            let folded = folded || auto_fold;
3770                            let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
3771                                Some(parent) => {
3772                                    let parent_folded = parent.folded;
3773                                    let parent_expanded = parent.expanded;
3774                                    let new_depth = if parent_folded {
3775                                        parent.depth
3776                                    } else {
3777                                        parent.depth + 1
3778                                    };
3779                                    parent_dirs.push(ParentStats {
3780                                        path: directory_entry.entry.path.clone(),
3781                                        folded,
3782                                        expanded: parent_expanded && is_expanded,
3783                                        depth: new_depth,
3784                                    });
3785                                    (new_depth, parent_expanded, parent_folded)
3786                                }
3787                                None => {
3788                                    parent_dirs.push(ParentStats {
3789                                        path: directory_entry.entry.path.clone(),
3790                                        folded,
3791                                        expanded: is_expanded,
3792                                        depth: fs_depth,
3793                                    });
3794                                    (fs_depth, true, false)
3795                                }
3796                            };
3797
3798                            if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take()
3799                            {
3800                                if folded
3801                                    && directory_entry.worktree_id == folded_dirs.worktree_id
3802                                    && directory_entry.entry.path.parent()
3803                                        == folded_dirs
3804                                            .entries
3805                                            .last()
3806                                            .map(|entry| entry.path.as_ref())
3807                                {
3808                                    folded_dirs.entries.push(directory_entry.entry.clone());
3809                                    folded_dirs_entry = Some((folded_depth, folded_dirs))
3810                                } else {
3811                                    if !is_singleton {
3812                                        let start_of_collapsed_dir_sequence = !parent_expanded
3813                                            && parent_dirs
3814                                                .iter()
3815                                                .rev()
3816                                                .nth(folded_dirs.entries.len() + 1)
3817                                                .is_none_or(|parent| parent.expanded);
3818                                        if start_of_collapsed_dir_sequence
3819                                            || parent_expanded
3820                                            || query.is_some()
3821                                        {
3822                                            if parent_folded {
3823                                                folded_dirs
3824                                                    .entries
3825                                                    .push(directory_entry.entry.clone());
3826                                                should_add = false;
3827                                            }
3828                                            let new_folded_dirs =
3829                                                PanelEntry::FoldedDirs(folded_dirs.clone());
3830                                            outline_panel.push_entry(
3831                                                &mut generation_state,
3832                                                track_matches,
3833                                                new_folded_dirs,
3834                                                folded_depth,
3835                                                cx,
3836                                            );
3837                                        }
3838                                    }
3839
3840                                    folded_dirs_entry = if parent_folded {
3841                                        None
3842                                    } else {
3843                                        Some((
3844                                            depth,
3845                                            FoldedDirsEntry {
3846                                                worktree_id: directory_entry.worktree_id,
3847                                                entries: vec![directory_entry.entry.clone()],
3848                                            },
3849                                        ))
3850                                    };
3851                                }
3852                            } else if folded {
3853                                folded_dirs_entry = Some((
3854                                    depth,
3855                                    FoldedDirsEntry {
3856                                        worktree_id: directory_entry.worktree_id,
3857                                        entries: vec![directory_entry.entry.clone()],
3858                                    },
3859                                ));
3860                            }
3861
3862                            let should_add =
3863                                should_add && parent_expanded && folded_dirs_entry.is_none();
3864                            (depth, should_add)
3865                        }
3866                        FsEntry::ExternalFile(..) => {
3867                            if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() {
3868                                let parent_expanded = parent_dirs
3869                                    .iter()
3870                                    .rev()
3871                                    .find(|parent| {
3872                                        folded_dir
3873                                            .entries
3874                                            .iter()
3875                                            .all(|entry| entry.path != parent.path)
3876                                    })
3877                                    .is_none_or(|parent| parent.expanded);
3878                                if !is_singleton && (parent_expanded || query.is_some()) {
3879                                    outline_panel.push_entry(
3880                                        &mut generation_state,
3881                                        track_matches,
3882                                        PanelEntry::FoldedDirs(folded_dir),
3883                                        folded_depth,
3884                                        cx,
3885                                    );
3886                                }
3887                            }
3888                            parent_dirs.clear();
3889                            (0, true)
3890                        }
3891                        FsEntry::File(file) => {
3892                            if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3893                                let parent_expanded = parent_dirs
3894                                    .iter()
3895                                    .rev()
3896                                    .find(|parent| {
3897                                        folded_dirs
3898                                            .entries
3899                                            .iter()
3900                                            .all(|entry| entry.path != parent.path)
3901                                    })
3902                                    .is_none_or(|parent| parent.expanded);
3903                                if !is_singleton && (parent_expanded || query.is_some()) {
3904                                    outline_panel.push_entry(
3905                                        &mut generation_state,
3906                                        track_matches,
3907                                        PanelEntry::FoldedDirs(folded_dirs),
3908                                        folded_depth,
3909                                        cx,
3910                                    );
3911                                }
3912                            }
3913
3914                            let fs_depth = outline_panel
3915                                .fs_entries_depth
3916                                .get(&(file.worktree_id, file.entry.id))
3917                                .copied()
3918                                .unwrap_or(0);
3919                            while let Some(parent) = parent_dirs.last() {
3920                                if file.entry.path.starts_with(&parent.path) {
3921                                    break;
3922                                }
3923                                parent_dirs.pop();
3924                            }
3925                            match parent_dirs.last() {
3926                                Some(parent) => {
3927                                    let new_depth = parent.depth + 1;
3928                                    (new_depth, parent.expanded)
3929                                }
3930                                None => (fs_depth, true),
3931                            }
3932                        }
3933                    };
3934
3935                    if !is_singleton
3936                        && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3937                    {
3938                        outline_panel.push_entry(
3939                            &mut generation_state,
3940                            track_matches,
3941                            PanelEntry::Fs(entry.clone()),
3942                            depth,
3943                            cx,
3944                        );
3945                    }
3946
3947                    match outline_panel.mode {
3948                        ItemsDisplayMode::Search(_) => {
3949                            if is_singleton || query.is_some() || (should_add && is_expanded) {
3950                                outline_panel.add_search_entries(
3951                                    &mut generation_state,
3952                                    &active_editor,
3953                                    entry.clone(),
3954                                    depth,
3955                                    query.clone(),
3956                                    is_singleton,
3957                                    cx,
3958                                );
3959                            }
3960                        }
3961                        ItemsDisplayMode::Outline => {
3962                            let excerpts_to_consider =
3963                                if is_singleton || query.is_some() || (should_add && is_expanded) {
3964                                    match &entry {
3965                                        FsEntry::File(FsEntryFile {
3966                                            buffer_id,
3967                                            excerpts,
3968                                            ..
3969                                        })
3970                                        | FsEntry::ExternalFile(FsEntryExternalFile {
3971                                            buffer_id,
3972                                            excerpts,
3973                                            ..
3974                                        }) => Some((*buffer_id, excerpts)),
3975                                        _ => None,
3976                                    }
3977                                } else {
3978                                    None
3979                                };
3980                            if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider
3981                                && !active_editor.read(cx).is_buffer_folded(buffer_id, cx)
3982                            {
3983                                outline_panel.add_excerpt_entries(
3984                                    &mut generation_state,
3985                                    buffer_id,
3986                                    entry_excerpts,
3987                                    depth,
3988                                    track_matches,
3989                                    is_singleton,
3990                                    query.as_deref(),
3991                                    cx,
3992                                );
3993                            }
3994                        }
3995                    }
3996
3997                    if is_singleton
3998                        && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3999                        && !generation_state.entries.iter().any(|item| {
4000                            matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
4001                        })
4002                    {
4003                        outline_panel.push_entry(
4004                            &mut generation_state,
4005                            track_matches,
4006                            PanelEntry::Fs(entry.clone()),
4007                            0,
4008                            cx,
4009                        );
4010                    }
4011                }
4012
4013                if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
4014                    let parent_expanded = parent_dirs
4015                        .iter()
4016                        .rev()
4017                        .find(|parent| {
4018                            folded_dirs
4019                                .entries
4020                                .iter()
4021                                .all(|entry| entry.path != parent.path)
4022                        })
4023                        .is_none_or(|parent| parent.expanded);
4024                    if parent_expanded || query.is_some() {
4025                        outline_panel.push_entry(
4026                            &mut generation_state,
4027                            track_matches,
4028                            PanelEntry::FoldedDirs(folded_dirs),
4029                            folded_depth,
4030                            cx,
4031                        );
4032                    }
4033                }
4034            }) else {
4035                return (Vec::new(), None);
4036            };
4037
4038            let Some(query) = query else {
4039                return (
4040                    generation_state.entries,
4041                    generation_state
4042                        .max_width_estimate_and_index
4043                        .map(|(_, index)| index),
4044                );
4045            };
4046
4047            let mut matched_ids = match_strings(
4048                &generation_state.match_candidates,
4049                &query,
4050                true,
4051                true,
4052                usize::MAX,
4053                &AtomicBool::default(),
4054                cx.background_executor().clone(),
4055            )
4056            .await
4057            .into_iter()
4058            .map(|string_match| (string_match.candidate_id, string_match))
4059            .collect::<HashMap<_, _>>();
4060
4061            let mut id = 0;
4062            generation_state.entries.retain_mut(|cached_entry| {
4063                let retain = match matched_ids.remove(&id) {
4064                    Some(string_match) => {
4065                        cached_entry.string_match = Some(string_match);
4066                        true
4067                    }
4068                    None => false,
4069                };
4070                id += 1;
4071                retain
4072            });
4073
4074            (
4075                generation_state.entries,
4076                generation_state
4077                    .max_width_estimate_and_index
4078                    .map(|(_, index)| index),
4079            )
4080        })
4081    }
4082
4083    fn push_entry(
4084        &self,
4085        state: &mut GenerationState,
4086        track_matches: bool,
4087        entry: PanelEntry,
4088        depth: usize,
4089        cx: &mut App,
4090    ) {
4091        let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry {
4092            match folded_dirs_entry.entries.len() {
4093                0 => {
4094                    debug_panic!("Empty folded dirs receiver");
4095                    return;
4096                }
4097                1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
4098                    worktree_id: folded_dirs_entry.worktree_id,
4099                    entry: folded_dirs_entry.entries[0].clone(),
4100                })),
4101                _ => entry,
4102            }
4103        } else {
4104            entry
4105        };
4106
4107        if track_matches {
4108            let id = state.entries.len();
4109            match &entry {
4110                PanelEntry::Fs(fs_entry) => {
4111                    if let Some(file_name) = self
4112                        .relative_path(fs_entry, cx)
4113                        .and_then(|path| Some(path.file_name()?.to_string()))
4114                    {
4115                        state
4116                            .match_candidates
4117                            .push(StringMatchCandidate::new(id, &file_name));
4118                    }
4119                }
4120                PanelEntry::FoldedDirs(folded_dir_entry) => {
4121                    let dir_names = self.dir_names_string(
4122                        &folded_dir_entry.entries,
4123                        folded_dir_entry.worktree_id,
4124                        cx,
4125                    );
4126                    {
4127                        state
4128                            .match_candidates
4129                            .push(StringMatchCandidate::new(id, &dir_names));
4130                    }
4131                }
4132                PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state
4133                    .match_candidates
4134                    .push(StringMatchCandidate::new(id, &outline_entry.outline.text)),
4135                PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {}
4136                PanelEntry::Search(new_search_entry) => {
4137                    if let Some(search_data) = new_search_entry.render_data.get() {
4138                        state
4139                            .match_candidates
4140                            .push(StringMatchCandidate::new(id, &search_data.context_text));
4141                    }
4142                }
4143            }
4144        }
4145
4146        let width_estimate = self.width_estimate(depth, &entry, cx);
4147        if Some(width_estimate)
4148            > state
4149                .max_width_estimate_and_index
4150                .map(|(estimate, _)| estimate)
4151        {
4152            state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
4153        }
4154        state.entries.push(CachedEntry {
4155            depth,
4156            entry,
4157            string_match: None,
4158        });
4159    }
4160
4161    fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String {
4162        let dir_names_segment = entries
4163            .iter()
4164            .map(|entry| self.entry_name(&worktree_id, entry, cx))
4165            .collect::<PathBuf>();
4166        dir_names_segment.to_string_lossy().into_owned()
4167    }
4168
4169    fn query(&self, cx: &App) -> Option<String> {
4170        let query = self.filter_editor.read(cx).text(cx);
4171        if query.trim().is_empty() {
4172            None
4173        } else {
4174            Some(query)
4175        }
4176    }
4177
4178    fn is_expanded(&self, entry: &FsEntry) -> bool {
4179        let entry_to_check = match entry {
4180            FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
4181                CollapsedEntry::ExternalFile(*buffer_id)
4182            }
4183            FsEntry::File(FsEntryFile {
4184                worktree_id,
4185                buffer_id,
4186                ..
4187            }) => CollapsedEntry::File(*worktree_id, *buffer_id),
4188            FsEntry::Directory(FsEntryDirectory {
4189                worktree_id, entry, ..
4190            }) => CollapsedEntry::Dir(*worktree_id, entry.id),
4191        };
4192        !self.collapsed_entries.contains(&entry_to_check)
4193    }
4194
4195    fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
4196        if !self.active {
4197            return false;
4198        }
4199
4200        let mut update_cached_items = false;
4201        update_cached_items |= self.update_search_matches(window, cx);
4202        self.fetch_outdated_outlines(window, cx);
4203        if update_cached_items {
4204            self.selected_entry.invalidate();
4205        }
4206        update_cached_items
4207    }
4208
4209    fn update_search_matches(
4210        &mut self,
4211        window: &mut Window,
4212        cx: &mut Context<OutlinePanel>,
4213    ) -> bool {
4214        if !self.active {
4215            return false;
4216        }
4217
4218        let project_search = self
4219            .active_item()
4220            .and_then(|item| item.downcast::<ProjectSearchView>());
4221        let project_search_matches = project_search
4222            .as_ref()
4223            .map(|project_search| project_search.read(cx).get_matches(cx))
4224            .unwrap_or_default();
4225
4226        let buffer_search = self
4227            .active_item()
4228            .as_deref()
4229            .and_then(|active_item| {
4230                self.workspace
4231                    .upgrade()
4232                    .and_then(|workspace| workspace.read(cx).pane_for(active_item))
4233            })
4234            .and_then(|pane| {
4235                pane.read(cx)
4236                    .toolbar()
4237                    .read(cx)
4238                    .item_of_type::<BufferSearchBar>()
4239            });
4240        let buffer_search_matches = self
4241            .active_editor()
4242            .map(|active_editor| {
4243                active_editor.update(cx, |editor, cx| editor.get_matches(window, cx))
4244            })
4245            .unwrap_or_default();
4246
4247        let mut update_cached_entries = false;
4248        if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
4249            if matches!(self.mode, ItemsDisplayMode::Search(_)) {
4250                self.mode = ItemsDisplayMode::Outline;
4251                update_cached_entries = true;
4252            }
4253        } else {
4254            let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
4255                (
4256                    SearchKind::Project,
4257                    project_search_matches,
4258                    project_search
4259                        .map(|project_search| project_search.read(cx).search_query_text(cx))
4260                        .unwrap_or_default(),
4261                )
4262            } else {
4263                (
4264                    SearchKind::Buffer,
4265                    buffer_search_matches,
4266                    buffer_search
4267                        .map(|buffer_search| buffer_search.read(cx).query(cx))
4268                        .unwrap_or_default(),
4269                )
4270            };
4271
4272            let mut previous_matches = HashMap::default();
4273            update_cached_entries = match &mut self.mode {
4274                ItemsDisplayMode::Search(current_search_state) => {
4275                    let update = current_search_state.query != new_search_query
4276                        || current_search_state.kind != kind
4277                        || current_search_state.matches.is_empty()
4278                        || current_search_state.matches.iter().enumerate().any(
4279                            |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
4280                        );
4281                    if current_search_state.kind == kind {
4282                        previous_matches.extend(current_search_state.matches.drain(..));
4283                    }
4284                    update
4285                }
4286                ItemsDisplayMode::Outline => true,
4287            };
4288            self.mode = ItemsDisplayMode::Search(SearchState::new(
4289                kind,
4290                new_search_query,
4291                previous_matches,
4292                new_search_matches,
4293                cx.theme().syntax().clone(),
4294                window,
4295                cx,
4296            ));
4297        }
4298        update_cached_entries
4299    }
4300
4301    fn add_excerpt_entries(
4302        &mut self,
4303        state: &mut GenerationState,
4304        buffer_id: BufferId,
4305        entries_to_add: &[ExcerptId],
4306        parent_depth: usize,
4307        track_matches: bool,
4308        is_singleton: bool,
4309        query: Option<&str>,
4310        cx: &mut Context<Self>,
4311    ) {
4312        if let Some(excerpts) = self.excerpts.get(&buffer_id) {
4313            let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx);
4314
4315            for &excerpt_id in entries_to_add {
4316                let Some(excerpt) = excerpts.get(&excerpt_id) else {
4317                    continue;
4318                };
4319                let excerpt_depth = parent_depth + 1;
4320                self.push_entry(
4321                    state,
4322                    track_matches,
4323                    PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
4324                        buffer_id,
4325                        id: excerpt_id,
4326                        range: excerpt.range.clone(),
4327                    })),
4328                    excerpt_depth,
4329                    cx,
4330                );
4331
4332                let mut outline_base_depth = excerpt_depth + 1;
4333                if is_singleton {
4334                    outline_base_depth = 0;
4335                    state.clear();
4336                } else if query.is_none()
4337                    && self
4338                        .collapsed_entries
4339                        .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
4340                {
4341                    continue;
4342                }
4343
4344                let mut last_depth_at_level: Vec<Option<Range<Anchor>>> = vec![None; 10];
4345
4346                let all_outlines: Vec<_> = excerpt.iter_outlines().collect();
4347
4348                let mut outline_has_children = HashMap::default();
4349                let mut visible_outlines = Vec::new();
4350                let mut collapsed_state: Option<(usize, Range<Anchor>)> = None;
4351
4352                for (i, &outline) in all_outlines.iter().enumerate() {
4353                    let has_children = all_outlines
4354                        .get(i + 1)
4355                        .map(|next| next.depth > outline.depth)
4356                        .unwrap_or(false);
4357
4358                    outline_has_children
4359                        .insert((outline.range.clone(), outline.depth), has_children);
4360
4361                    let mut should_include = true;
4362
4363                    if let Some((collapsed_depth, collapsed_range)) = &collapsed_state {
4364                        if outline.depth <= *collapsed_depth {
4365                            collapsed_state = None;
4366                        } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() {
4367                            let outline_start = outline.range.start;
4368                            if outline_start
4369                                .cmp(&collapsed_range.start, buffer_snapshot)
4370                                .is_ge()
4371                                && outline_start
4372                                    .cmp(&collapsed_range.end, buffer_snapshot)
4373                                    .is_lt()
4374                            {
4375                                should_include = false; // Skip - inside collapsed range
4376                            } else {
4377                                collapsed_state = None;
4378                            }
4379                        }
4380                    }
4381
4382                    // Check if this outline itself is collapsed
4383                    if should_include
4384                        && self.collapsed_entries.contains(&CollapsedEntry::Outline(
4385                            buffer_id,
4386                            excerpt_id,
4387                            outline.range.clone(),
4388                        ))
4389                    {
4390                        collapsed_state = Some((outline.depth, outline.range.clone()));
4391                    }
4392
4393                    if should_include {
4394                        visible_outlines.push(outline);
4395                    }
4396                }
4397
4398                self.outline_children_cache
4399                    .entry(buffer_id)
4400                    .or_default()
4401                    .extend(outline_has_children);
4402
4403                for outline in visible_outlines {
4404                    let outline_entry = OutlineEntryOutline {
4405                        buffer_id,
4406                        excerpt_id,
4407                        outline: outline.clone(),
4408                    };
4409
4410                    if outline.depth < last_depth_at_level.len() {
4411                        last_depth_at_level[outline.depth] = Some(outline.range.clone());
4412                        // Clear deeper levels when we go back to a shallower depth
4413                        for d in (outline.depth + 1)..last_depth_at_level.len() {
4414                            last_depth_at_level[d] = None;
4415                        }
4416                    }
4417
4418                    self.push_entry(
4419                        state,
4420                        track_matches,
4421                        PanelEntry::Outline(OutlineEntry::Outline(outline_entry)),
4422                        outline_base_depth + outline.depth,
4423                        cx,
4424                    );
4425                }
4426            }
4427        }
4428    }
4429
4430    fn add_search_entries(
4431        &mut self,
4432        state: &mut GenerationState,
4433        active_editor: &Entity<Editor>,
4434        parent_entry: FsEntry,
4435        parent_depth: usize,
4436        filter_query: Option<String>,
4437        is_singleton: bool,
4438        cx: &mut Context<Self>,
4439    ) {
4440        let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
4441            return;
4442        };
4443
4444        let kind = search_state.kind;
4445        let related_excerpts = match &parent_entry {
4446            FsEntry::Directory(_) => return,
4447            FsEntry::ExternalFile(external) => &external.excerpts,
4448            FsEntry::File(file) => &file.excerpts,
4449        }
4450        .iter()
4451        .copied()
4452        .collect::<HashSet<_>>();
4453
4454        let depth = if is_singleton { 0 } else { parent_depth + 1 };
4455        let new_search_matches = search_state
4456            .matches
4457            .iter()
4458            .filter(|(match_range, _)| {
4459                related_excerpts.contains(&match_range.start.excerpt_id)
4460                    || related_excerpts.contains(&match_range.end.excerpt_id)
4461            })
4462            .filter(|(match_range, _)| {
4463                let editor = active_editor.read(cx);
4464                let snapshot = editor.buffer().read(cx).snapshot(cx);
4465                if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start)
4466                    && editor.is_buffer_folded(buffer_id, cx)
4467                {
4468                    return false;
4469                }
4470                if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end)
4471                    && editor.is_buffer_folded(buffer_id, cx)
4472                {
4473                    return false;
4474                }
4475                true
4476            });
4477
4478        let new_search_entries = new_search_matches
4479            .map(|(match_range, search_data)| SearchEntry {
4480                match_range: match_range.clone(),
4481                kind,
4482                render_data: Arc::clone(search_data),
4483            })
4484            .collect::<Vec<_>>();
4485        for new_search_entry in new_search_entries {
4486            self.push_entry(
4487                state,
4488                filter_query.is_some(),
4489                PanelEntry::Search(new_search_entry),
4490                depth,
4491                cx,
4492            );
4493        }
4494    }
4495
4496    fn active_editor(&self) -> Option<Entity<Editor>> {
4497        self.active_item.as_ref()?.active_editor.upgrade()
4498    }
4499
4500    fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
4501        self.active_item.as_ref()?.item_handle.upgrade()
4502    }
4503
4504    fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
4505        self.active_item().is_none_or(|active_item| {
4506            !self.pinned && active_item.item_id() != new_active_item.item_id()
4507        })
4508    }
4509
4510    pub fn toggle_active_editor_pin(
4511        &mut self,
4512        _: &ToggleActiveEditorPin,
4513        window: &mut Window,
4514        cx: &mut Context<Self>,
4515    ) {
4516        self.pinned = !self.pinned;
4517        if !self.pinned
4518            && let Some((active_item, active_editor)) = self
4519                .workspace
4520                .upgrade()
4521                .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
4522            && self.should_replace_active_item(active_item.as_ref())
4523        {
4524            self.replace_active_editor(active_item, active_editor, window, cx);
4525        }
4526
4527        cx.notify();
4528    }
4529
4530    fn selected_entry(&self) -> Option<&PanelEntry> {
4531        match &self.selected_entry {
4532            SelectedEntry::Invalidated(entry) => entry.as_ref(),
4533            SelectedEntry::Valid(entry, _) => Some(entry),
4534            SelectedEntry::None => None,
4535        }
4536    }
4537
4538    fn select_entry(
4539        &mut self,
4540        entry: PanelEntry,
4541        focus: bool,
4542        window: &mut Window,
4543        cx: &mut Context<Self>,
4544    ) {
4545        if focus {
4546            self.focus_handle.focus(window, cx);
4547        }
4548        let ix = self
4549            .cached_entries
4550            .iter()
4551            .enumerate()
4552            .find(|(_, cached_entry)| &cached_entry.entry == &entry)
4553            .map(|(i, _)| i)
4554            .unwrap_or_default();
4555
4556        self.selected_entry = SelectedEntry::Valid(entry, ix);
4557
4558        self.autoscroll(cx);
4559        cx.notify();
4560    }
4561
4562    fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4563        let item_text_chars = match entry {
4564            PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4565                .buffer_snapshot_for_id(external.buffer_id, cx)
4566                .and_then(|snapshot| Some(snapshot.file()?.path().file_name()?.len()))
4567                .unwrap_or_default(),
4568            PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4569                .entry
4570                .path
4571                .file_name()
4572                .map(|name| name.len())
4573                .unwrap_or_default(),
4574            PanelEntry::Fs(FsEntry::File(file)) => file
4575                .entry
4576                .path
4577                .file_name()
4578                .map(|name| name.len())
4579                .unwrap_or_default(),
4580            PanelEntry::FoldedDirs(folded_dirs) => {
4581                folded_dirs
4582                    .entries
4583                    .iter()
4584                    .map(|dir| {
4585                        dir.path
4586                            .file_name()
4587                            .map(|name| name.len())
4588                            .unwrap_or_default()
4589                    })
4590                    .sum::<usize>()
4591                    + folded_dirs.entries.len().saturating_sub(1) * "/".len()
4592            }
4593            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4594                .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
4595                .map(|label| label.len())
4596                .unwrap_or_default(),
4597            PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(),
4598            PanelEntry::Search(search) => search
4599                .render_data
4600                .get()
4601                .map(|data| data.context_text.len())
4602                .unwrap_or_default(),
4603        };
4604
4605        (item_text_chars + depth) as u64
4606    }
4607
4608    fn render_main_contents(
4609        &mut self,
4610        query: Option<String>,
4611        show_indent_guides: bool,
4612        indent_size: f32,
4613        window: &mut Window,
4614        cx: &mut Context<Self>,
4615    ) -> impl IntoElement {
4616        let contents = if self.cached_entries.is_empty() {
4617            let header = if query.is_some() {
4618                "No matches for query"
4619            } else {
4620                "No outlines available"
4621            };
4622
4623            v_flex()
4624                .id("empty-outline-state")
4625                .gap_0p5()
4626                .flex_1()
4627                .justify_center()
4628                .size_full()
4629                .child(h_flex().justify_center().child(Label::new(header)))
4630                .when_some(query, |panel, query| {
4631                    panel.child(
4632                        h_flex()
4633                            .px_0p5()
4634                            .justify_center()
4635                            .bg(cx.theme().colors().element_selected.opacity(0.2))
4636                            .child(Label::new(query)),
4637                    )
4638                })
4639                .child(h_flex().justify_center().child({
4640                    let keystroke = match self.position(window, cx) {
4641                        DockPosition::Left => window.keystroke_text_for(&workspace::ToggleLeftDock),
4642                        DockPosition::Bottom => {
4643                            window.keystroke_text_for(&workspace::ToggleBottomDock)
4644                        }
4645                        DockPosition::Right => {
4646                            window.keystroke_text_for(&workspace::ToggleRightDock)
4647                        }
4648                    };
4649                    Label::new(format!("Toggle Panel With {keystroke}")).color(Color::Muted)
4650                }))
4651        } else {
4652            let list_contents = {
4653                let items_len = self.cached_entries.len();
4654                let multi_buffer_snapshot = self
4655                    .active_editor()
4656                    .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4657                uniform_list(
4658                    "entries",
4659                    items_len,
4660                    cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
4661                        outline_panel.rendered_entries_len = range.end - range.start;
4662                        let entries = outline_panel.cached_entries.get(range);
4663                        entries
4664                            .map(|entries| entries.to_vec())
4665                            .unwrap_or_default()
4666                            .into_iter()
4667                            .filter_map(|cached_entry| match cached_entry.entry {
4668                                PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4669                                    &entry,
4670                                    cached_entry.depth,
4671                                    cached_entry.string_match.as_ref(),
4672                                    window,
4673                                    cx,
4674                                )),
4675                                PanelEntry::FoldedDirs(folded_dirs_entry) => {
4676                                    Some(outline_panel.render_folded_dirs(
4677                                        &folded_dirs_entry,
4678                                        cached_entry.depth,
4679                                        cached_entry.string_match.as_ref(),
4680                                        window,
4681                                        cx,
4682                                    ))
4683                                }
4684                                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4685                                    outline_panel.render_excerpt(
4686                                        &excerpt,
4687                                        cached_entry.depth,
4688                                        window,
4689                                        cx,
4690                                    )
4691                                }
4692                                PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4693                                    Some(outline_panel.render_outline(
4694                                        &entry,
4695                                        cached_entry.depth,
4696                                        cached_entry.string_match.as_ref(),
4697                                        window,
4698                                        cx,
4699                                    ))
4700                                }
4701                                PanelEntry::Search(SearchEntry {
4702                                    match_range,
4703                                    render_data,
4704                                    kind,
4705                                    ..
4706                                }) => outline_panel.render_search_match(
4707                                    multi_buffer_snapshot.as_ref(),
4708                                    &match_range,
4709                                    &render_data,
4710                                    kind,
4711                                    cached_entry.depth,
4712                                    cached_entry.string_match.as_ref(),
4713                                    window,
4714                                    cx,
4715                                ),
4716                            })
4717                            .collect()
4718                    }),
4719                )
4720                .with_sizing_behavior(ListSizingBehavior::Infer)
4721                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4722                .with_width_from_item(self.max_width_item_index)
4723                .track_scroll(&self.scroll_handle)
4724                .when(show_indent_guides, |list| {
4725                    list.with_decoration(
4726                        ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
4727                            .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| {
4728                                let entries = outline_panel.cached_entries.get(range);
4729                                if let Some(entries) = entries {
4730                                    entries.iter().map(|item| item.depth).collect()
4731                                } else {
4732                                    smallvec::SmallVec::new()
4733                                }
4734                            })
4735                            .with_render_fn(cx.entity(), move |outline_panel, params, _, _| {
4736                                const LEFT_OFFSET: Pixels = px(14.);
4737
4738                                let indent_size = params.indent_size;
4739                                let item_height = params.item_height;
4740                                let active_indent_guide_ix = find_active_indent_guide_ix(
4741                                    outline_panel,
4742                                    &params.indent_guides,
4743                                );
4744
4745                                params
4746                                    .indent_guides
4747                                    .into_iter()
4748                                    .enumerate()
4749                                    .map(|(ix, layout)| {
4750                                        let bounds = Bounds::new(
4751                                            point(
4752                                                layout.offset.x * indent_size + LEFT_OFFSET,
4753                                                layout.offset.y * item_height,
4754                                            ),
4755                                            size(px(1.), layout.length * item_height),
4756                                        );
4757                                        ui::RenderedIndentGuide {
4758                                            bounds,
4759                                            layout,
4760                                            is_active: active_indent_guide_ix == Some(ix),
4761                                            hitbox: None,
4762                                        }
4763                                    })
4764                                    .collect()
4765                            }),
4766                    )
4767                })
4768            };
4769
4770            v_flex()
4771                .flex_shrink()
4772                .size_full()
4773                .child(list_contents.size_full().flex_shrink())
4774                .custom_scrollbars(
4775                    Scrollbars::for_settings::<OutlinePanelSettings>()
4776                        .tracked_scroll_handle(&self.scroll_handle.clone())
4777                        .with_track_along(
4778                            ScrollAxes::Horizontal,
4779                            cx.theme().colors().panel_background,
4780                        )
4781                        .tracked_entity(cx.entity_id()),
4782                    window,
4783                    cx,
4784                )
4785        }
4786        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4787            deferred(
4788                anchored()
4789                    .position(*position)
4790                    .anchor(gpui::Corner::TopLeft)
4791                    .child(menu.clone()),
4792            )
4793            .with_priority(1)
4794        }));
4795
4796        v_flex().w_full().flex_1().overflow_hidden().child(contents)
4797    }
4798
4799    fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4800        let (icon, icon_tooltip) = if pinned {
4801            (IconName::Unpin, "Unpin Outline")
4802        } else {
4803            (IconName::Pin, "Pin Active Outline")
4804        };
4805
4806        let has_query = self.query(cx).is_some();
4807
4808        h_flex()
4809            .p_2()
4810            .h(Tab::container_height(cx))
4811            .justify_between()
4812            .border_b_1()
4813            .border_color(cx.theme().colors().border)
4814            .child(
4815                h_flex()
4816                    .w_full()
4817                    .gap_1p5()
4818                    .child(
4819                        Icon::new(IconName::MagnifyingGlass)
4820                            .size(IconSize::Small)
4821                            .color(Color::Muted),
4822                    )
4823                    .child(self.filter_editor.clone()),
4824            )
4825            .child(
4826                h_flex()
4827                    .when(has_query, |this| {
4828                        this.child(
4829                            IconButton::new("clear_filter", IconName::Close)
4830                                .shape(IconButtonShape::Square)
4831                                .tooltip(Tooltip::text("Clear Filter"))
4832                                .on_click(cx.listener(|outline_panel, _, window, cx| {
4833                                    outline_panel.filter_editor.update(cx, |editor, cx| {
4834                                        editor.set_text("", window, cx);
4835                                    });
4836                                    cx.notify();
4837                                })),
4838                        )
4839                    })
4840                    .child(
4841                        IconButton::new("pin_button", icon)
4842                            .tooltip(Tooltip::text(icon_tooltip))
4843                            .shape(IconButtonShape::Square)
4844                            .on_click(cx.listener(|outline_panel, _, window, cx| {
4845                                outline_panel.toggle_active_editor_pin(
4846                                    &ToggleActiveEditorPin,
4847                                    window,
4848                                    cx,
4849                                );
4850                            })),
4851                    ),
4852            )
4853    }
4854
4855    fn buffers_inside_directory(
4856        &self,
4857        dir_worktree: WorktreeId,
4858        dir_entry: &GitEntry,
4859    ) -> HashSet<BufferId> {
4860        if !dir_entry.is_dir() {
4861            debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4862            return HashSet::default();
4863        }
4864
4865        self.fs_entries
4866            .iter()
4867            .skip_while(|fs_entry| match fs_entry {
4868                FsEntry::Directory(directory) => {
4869                    directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4870                }
4871                _ => true,
4872            })
4873            .skip(1)
4874            .take_while(|fs_entry| match fs_entry {
4875                FsEntry::ExternalFile(..) => false,
4876                FsEntry::Directory(directory) => {
4877                    directory.worktree_id == dir_worktree
4878                        && directory.entry.path.starts_with(&dir_entry.path)
4879                }
4880                FsEntry::File(file) => {
4881                    file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4882                }
4883            })
4884            .filter_map(|fs_entry| match fs_entry {
4885                FsEntry::File(file) => Some(file.buffer_id),
4886                _ => None,
4887            })
4888            .collect()
4889    }
4890}
4891
4892fn workspace_active_editor(
4893    workspace: &Workspace,
4894    cx: &App,
4895) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4896    let active_item = workspace.active_item(cx)?;
4897    let active_editor = active_item
4898        .act_as::<Editor>(cx)
4899        .filter(|editor| editor.read(cx).mode().is_full())?;
4900    Some((active_item, active_editor))
4901}
4902
4903fn back_to_common_visited_parent(
4904    visited_dirs: &mut Vec<(ProjectEntryId, Arc<RelPath>)>,
4905    worktree_id: &WorktreeId,
4906    new_entry: &Entry,
4907) -> Option<(WorktreeId, ProjectEntryId)> {
4908    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4909        match new_entry.path.parent() {
4910            Some(parent_path) => {
4911                if parent_path == visited_path.as_ref() {
4912                    return Some((*worktree_id, *visited_dir_id));
4913                }
4914            }
4915            None => {
4916                break;
4917            }
4918        }
4919        visited_dirs.pop();
4920    }
4921    None
4922}
4923
4924fn file_name(path: &Path) -> String {
4925    let mut current_path = path;
4926    loop {
4927        if let Some(file_name) = current_path.file_name() {
4928            return file_name.to_string_lossy().into_owned();
4929        }
4930        match current_path.parent() {
4931            Some(parent) => current_path = parent,
4932            None => return path.to_string_lossy().into_owned(),
4933        }
4934    }
4935}
4936
4937impl Panel for OutlinePanel {
4938    fn persistent_name() -> &'static str {
4939        "Outline Panel"
4940    }
4941
4942    fn panel_key() -> &'static str {
4943        OUTLINE_PANEL_KEY
4944    }
4945
4946    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4947        match OutlinePanelSettings::get_global(cx).dock {
4948            DockSide::Left => DockPosition::Left,
4949            DockSide::Right => DockPosition::Right,
4950        }
4951    }
4952
4953    fn position_is_valid(&self, position: DockPosition) -> bool {
4954        matches!(position, DockPosition::Left | DockPosition::Right)
4955    }
4956
4957    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4958        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
4959            let dock = match position {
4960                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
4961                DockPosition::Right => DockSide::Right,
4962            };
4963            settings.outline_panel.get_or_insert_default().dock = Some(dock);
4964        });
4965    }
4966
4967    fn size(&self, _: &Window, cx: &App) -> Pixels {
4968        self.width
4969            .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
4970    }
4971
4972    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
4973        self.width = size;
4974        cx.notify();
4975        cx.defer_in(window, |this, _, cx| {
4976            this.serialize(cx);
4977        });
4978    }
4979
4980    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4981        OutlinePanelSettings::get_global(cx)
4982            .button
4983            .then_some(IconName::ListTree)
4984    }
4985
4986    fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4987        Some("Outline Panel")
4988    }
4989
4990    fn toggle_action(&self) -> Box<dyn Action> {
4991        Box::new(ToggleFocus)
4992    }
4993
4994    fn starts_open(&self, _window: &Window, _: &App) -> bool {
4995        self.active
4996    }
4997
4998    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
4999        cx.spawn_in(window, async move |outline_panel, cx| {
5000            outline_panel
5001                .update_in(cx, |outline_panel, window, cx| {
5002                    let old_active = outline_panel.active;
5003                    outline_panel.active = active;
5004                    if old_active != active {
5005                        if active
5006                            && let Some((active_item, active_editor)) =
5007                                outline_panel.workspace.upgrade().and_then(|workspace| {
5008                                    workspace_active_editor(workspace.read(cx), cx)
5009                                })
5010                        {
5011                            if outline_panel.should_replace_active_item(active_item.as_ref()) {
5012                                outline_panel.replace_active_editor(
5013                                    active_item,
5014                                    active_editor,
5015                                    window,
5016                                    cx,
5017                                );
5018                            } else {
5019                                outline_panel.update_fs_entries(active_editor, None, window, cx)
5020                            }
5021                            return;
5022                        }
5023
5024                        if !outline_panel.pinned {
5025                            outline_panel.clear_previous(window, cx);
5026                        }
5027                    }
5028                    outline_panel.serialize(cx);
5029                })
5030                .ok();
5031        })
5032        .detach()
5033    }
5034
5035    fn activation_priority(&self) -> u32 {
5036        5
5037    }
5038}
5039
5040impl Focusable for OutlinePanel {
5041    fn focus_handle(&self, cx: &App) -> FocusHandle {
5042        self.filter_editor.focus_handle(cx)
5043    }
5044}
5045
5046impl EventEmitter<Event> for OutlinePanel {}
5047
5048impl EventEmitter<PanelEvent> for OutlinePanel {}
5049
5050impl Render for OutlinePanel {
5051    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
5052        let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
5053            (project.is_local(), project.is_via_remote_server())
5054        });
5055        let query = self.query(cx);
5056        let pinned = self.pinned;
5057        let settings = OutlinePanelSettings::get_global(cx);
5058        let indent_size = settings.indent_size;
5059        let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
5060
5061        let search_query = match &self.mode {
5062            ItemsDisplayMode::Search(search_query) => Some(search_query),
5063            _ => None,
5064        };
5065
5066        let search_query_text = search_query.map(|sq| sq.query.to_string());
5067
5068        v_flex()
5069            .id("outline-panel")
5070            .size_full()
5071            .overflow_hidden()
5072            .relative()
5073            .key_context(self.dispatch_context(window, cx))
5074            .on_action(cx.listener(Self::open_selected_entry))
5075            .on_action(cx.listener(Self::cancel))
5076            .on_action(cx.listener(Self::scroll_up))
5077            .on_action(cx.listener(Self::scroll_down))
5078            .on_action(cx.listener(Self::select_next))
5079            .on_action(cx.listener(Self::scroll_cursor_center))
5080            .on_action(cx.listener(Self::scroll_cursor_top))
5081            .on_action(cx.listener(Self::scroll_cursor_bottom))
5082            .on_action(cx.listener(Self::select_previous))
5083            .on_action(cx.listener(Self::select_first))
5084            .on_action(cx.listener(Self::select_last))
5085            .on_action(cx.listener(Self::select_parent))
5086            .on_action(cx.listener(Self::expand_selected_entry))
5087            .on_action(cx.listener(Self::collapse_selected_entry))
5088            .on_action(cx.listener(Self::expand_all_entries))
5089            .on_action(cx.listener(Self::collapse_all_entries))
5090            .on_action(cx.listener(Self::copy_path))
5091            .on_action(cx.listener(Self::copy_relative_path))
5092            .on_action(cx.listener(Self::toggle_active_editor_pin))
5093            .on_action(cx.listener(Self::unfold_directory))
5094            .on_action(cx.listener(Self::fold_directory))
5095            .on_action(cx.listener(Self::open_excerpts))
5096            .on_action(cx.listener(Self::open_excerpts_split))
5097            .when(is_local, |el| {
5098                el.on_action(cx.listener(Self::reveal_in_finder))
5099            })
5100            .when(is_local || is_via_ssh, |el| {
5101                el.on_action(cx.listener(Self::open_in_terminal))
5102            })
5103            .on_mouse_down(
5104                MouseButton::Right,
5105                cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
5106                    if let Some(entry) = outline_panel.selected_entry().cloned() {
5107                        outline_panel.deploy_context_menu(event.position, entry, window, cx)
5108                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
5109                        outline_panel.deploy_context_menu(
5110                            event.position,
5111                            PanelEntry::Fs(entry),
5112                            window,
5113                            cx,
5114                        )
5115                    }
5116                }),
5117            )
5118            .track_focus(&self.focus_handle)
5119            .child(self.render_filter_footer(pinned, cx))
5120            .when_some(search_query_text, |outline_panel, query_text| {
5121                outline_panel.child(
5122                    h_flex()
5123                        .py_1p5()
5124                        .px_2()
5125                        .h(Tab::container_height(cx))
5126                        .gap_0p5()
5127                        .border_b_1()
5128                        .border_color(cx.theme().colors().border_variant)
5129                        .child(Label::new("Searching:").color(Color::Muted))
5130                        .child(Label::new(query_text)),
5131                )
5132            })
5133            .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
5134    }
5135}
5136
5137fn find_active_indent_guide_ix(
5138    outline_panel: &OutlinePanel,
5139    candidates: &[IndentGuideLayout],
5140) -> Option<usize> {
5141    let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
5142        return None;
5143    };
5144    let target_depth = outline_panel
5145        .cached_entries
5146        .get(*target_ix)
5147        .map(|cached_entry| cached_entry.depth)?;
5148
5149    let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
5150        .cached_entries
5151        .get(target_ix + 1)
5152        .filter(|cached_entry| cached_entry.depth > target_depth)
5153        .map(|entry| entry.depth)
5154    {
5155        (target_ix + 1, target_depth.saturating_sub(1))
5156    } else {
5157        (*target_ix, target_depth.saturating_sub(1))
5158    };
5159
5160    candidates
5161        .iter()
5162        .enumerate()
5163        .find(|(_, guide)| {
5164            guide.offset.y <= target_ix
5165                && target_ix < guide.offset.y + guide.length
5166                && guide.offset.x == target_depth
5167        })
5168        .map(|(ix, _)| ix)
5169}
5170
5171fn subscribe_for_editor_events(
5172    editor: &Entity<Editor>,
5173    window: &mut Window,
5174    cx: &mut Context<OutlinePanel>,
5175) -> Subscription {
5176    let debounce = Some(UPDATE_DEBOUNCE);
5177    cx.subscribe_in(
5178        editor,
5179        window,
5180        move |outline_panel, editor, e: &EditorEvent, window, cx| {
5181            if !outline_panel.active {
5182                return;
5183            }
5184            match e {
5185                EditorEvent::SelectionsChanged { local: true } => {
5186                    outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5187                    cx.notify();
5188                }
5189                EditorEvent::ExcerptsAdded { excerpts, .. } => {
5190                    outline_panel
5191                        .new_entries_for_fs_update
5192                        .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5193                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5194                }
5195                EditorEvent::ExcerptsRemoved { ids, .. } => {
5196                    let mut ids = ids.iter().collect::<HashSet<_>>();
5197                    for excerpts in outline_panel.excerpts.values_mut() {
5198                        excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5199                        if ids.is_empty() {
5200                            break;
5201                        }
5202                    }
5203                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5204                }
5205                EditorEvent::ExcerptsExpanded { ids } => {
5206                    outline_panel.invalidate_outlines(ids);
5207                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5208                    if update_cached_items {
5209                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5210                    }
5211                }
5212                EditorEvent::ExcerptsEdited { ids } => {
5213                    outline_panel.invalidate_outlines(ids);
5214                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5215                    if update_cached_items {
5216                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5217                    }
5218                }
5219                EditorEvent::BufferFoldToggled { ids, .. } => {
5220                    outline_panel.invalidate_outlines(ids);
5221                    let mut latest_unfolded_buffer_id = None;
5222                    let mut latest_folded_buffer_id = None;
5223                    let mut ignore_selections_change = false;
5224                    outline_panel.new_entries_for_fs_update.extend(
5225                        ids.iter()
5226                            .filter(|id| {
5227                                outline_panel
5228                                    .excerpts
5229                                    .iter()
5230                                    .find_map(|(buffer_id, excerpts)| {
5231                                        if excerpts.contains_key(id) {
5232                                            ignore_selections_change |= outline_panel
5233                                                .preserve_selection_on_buffer_fold_toggles
5234                                                .remove(buffer_id);
5235                                            Some(buffer_id)
5236                                        } else {
5237                                            None
5238                                        }
5239                                    })
5240                                    .map(|buffer_id| {
5241                                        if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5242                                            latest_folded_buffer_id = Some(*buffer_id);
5243                                            false
5244                                        } else {
5245                                            latest_unfolded_buffer_id = Some(*buffer_id);
5246                                            true
5247                                        }
5248                                    })
5249                                    .unwrap_or(true)
5250                            })
5251                            .copied(),
5252                    );
5253                    if !ignore_selections_change
5254                        && let Some(entry_to_select) = latest_unfolded_buffer_id
5255                            .or(latest_folded_buffer_id)
5256                            .and_then(|toggled_buffer_id| {
5257                                outline_panel.fs_entries.iter().find_map(
5258                                    |fs_entry| match fs_entry {
5259                                        FsEntry::ExternalFile(external) => {
5260                                            if external.buffer_id == toggled_buffer_id {
5261                                                Some(fs_entry.clone())
5262                                            } else {
5263                                                None
5264                                            }
5265                                        }
5266                                        FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5267                                            if *buffer_id == toggled_buffer_id {
5268                                                Some(fs_entry.clone())
5269                                            } else {
5270                                                None
5271                                            }
5272                                        }
5273                                        FsEntry::Directory(..) => None,
5274                                    },
5275                                )
5276                            })
5277                            .map(PanelEntry::Fs)
5278                    {
5279                        outline_panel.select_entry(entry_to_select, true, window, cx);
5280                    }
5281
5282                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5283                }
5284                EditorEvent::Reparsed(buffer_id) => {
5285                    if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5286                        for excerpt in excerpts.values_mut() {
5287                            excerpt.invalidate_outlines();
5288                        }
5289                    }
5290                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5291                    if update_cached_items {
5292                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5293                    }
5294                }
5295                EditorEvent::TitleChanged => {
5296                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5297                }
5298                _ => {}
5299            }
5300        },
5301    )
5302}
5303
5304fn empty_icon() -> AnyElement {
5305    h_flex()
5306        .size(IconSize::default().rems())
5307        .invisible()
5308        .flex_none()
5309        .into_any_element()
5310}
5311
5312#[derive(Debug, Default)]
5313struct GenerationState {
5314    entries: Vec<CachedEntry>,
5315    match_candidates: Vec<StringMatchCandidate>,
5316    max_width_estimate_and_index: Option<(u64, usize)>,
5317}
5318
5319impl GenerationState {
5320    fn clear(&mut self) {
5321        self.entries.clear();
5322        self.match_candidates.clear();
5323        self.max_width_estimate_and_index = None;
5324    }
5325}
5326
5327#[cfg(test)]
5328mod tests {
5329    use db::indoc;
5330    use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5331    use language::rust_lang;
5332    use pretty_assertions::assert_eq;
5333    use project::FakeFs;
5334    use search::{
5335        buffer_search,
5336        project_search::{self, perform_project_search},
5337    };
5338    use serde_json::json;
5339    use util::path;
5340    use workspace::{OpenOptions, OpenVisible, ToolbarItemView};
5341
5342    use super::*;
5343
5344    const SELECTED_MARKER: &str = "  <==== selected";
5345
5346    #[gpui::test(iterations = 10)]
5347    async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5348        init_test(cx);
5349
5350        let fs = FakeFs::new(cx.background_executor.clone());
5351        let root = path!("/rust-analyzer");
5352        populate_with_test_ra_project(&fs, root).await;
5353        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5354        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5355        let workspace = add_outline_panel(&project, cx).await;
5356        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5357        let outline_panel = outline_panel(&workspace, cx);
5358        outline_panel.update_in(cx, |outline_panel, window, cx| {
5359            outline_panel.set_active(true, window, cx)
5360        });
5361
5362        workspace
5363            .update(cx, |workspace, window, cx| {
5364                ProjectSearchView::deploy_search(
5365                    workspace,
5366                    &workspace::DeploySearch::default(),
5367                    window,
5368                    cx,
5369                )
5370            })
5371            .unwrap();
5372        let search_view = workspace
5373            .update(cx, |workspace, _, cx| {
5374                workspace
5375                    .active_pane()
5376                    .read(cx)
5377                    .items()
5378                    .find_map(|item| item.downcast::<ProjectSearchView>())
5379                    .expect("Project search view expected to appear after new search event trigger")
5380            })
5381            .unwrap();
5382
5383        let query = "param_names_for_lifetime_elision_hints";
5384        perform_project_search(&search_view, query, cx);
5385        search_view.update(cx, |search_view, cx| {
5386            search_view
5387                .results_editor()
5388                .update(cx, |results_editor, cx| {
5389                    assert_eq!(
5390                        results_editor.display_text(cx).match_indices(query).count(),
5391                        9
5392                    );
5393                });
5394        });
5395
5396        let all_matches = r#"rust-analyzer/
5397  crates/
5398    ide/src/
5399      inlay_hints/
5400        fn_lifetime_fn.rs
5401          search: match config.«param_names_for_lifetime_elision_hints» {
5402          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5403          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5404          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5405      inlay_hints.rs
5406        search: pub «param_names_for_lifetime_elision_hints»: bool,
5407        search: «param_names_for_lifetime_elision_hints»: self
5408      static_index.rs
5409        search: «param_names_for_lifetime_elision_hints»: false,
5410    rust-analyzer/src/
5411      cli/
5412        analysis_stats.rs
5413          search: «param_names_for_lifetime_elision_hints»: true,
5414      config.rs
5415        search: «param_names_for_lifetime_elision_hints»: self"#
5416            .to_string();
5417
5418        let select_first_in_all_matches = |line_to_select: &str| {
5419            assert!(
5420                all_matches.contains(line_to_select),
5421                "`{line_to_select}` was not found in all matches `{all_matches}`"
5422            );
5423            all_matches.replacen(
5424                line_to_select,
5425                &format!("{line_to_select}{SELECTED_MARKER}"),
5426                1,
5427            )
5428        };
5429
5430        cx.executor()
5431            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5432        cx.run_until_parked();
5433        outline_panel.update(cx, |outline_panel, cx| {
5434            assert_eq!(
5435                display_entries(
5436                    &project,
5437                    &snapshot(outline_panel, cx),
5438                    &outline_panel.cached_entries,
5439                    outline_panel.selected_entry(),
5440                    cx,
5441                ),
5442                select_first_in_all_matches(
5443                    "search: match config.«param_names_for_lifetime_elision_hints» {"
5444                )
5445            );
5446        });
5447
5448        outline_panel.update_in(cx, |outline_panel, window, cx| {
5449            outline_panel.select_parent(&SelectParent, window, cx);
5450            assert_eq!(
5451                display_entries(
5452                    &project,
5453                    &snapshot(outline_panel, cx),
5454                    &outline_panel.cached_entries,
5455                    outline_panel.selected_entry(),
5456                    cx,
5457                ),
5458                select_first_in_all_matches("fn_lifetime_fn.rs")
5459            );
5460        });
5461        outline_panel.update_in(cx, |outline_panel, window, cx| {
5462            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5463        });
5464        cx.executor()
5465            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5466        cx.run_until_parked();
5467        outline_panel.update(cx, |outline_panel, cx| {
5468            assert_eq!(
5469                display_entries(
5470                    &project,
5471                    &snapshot(outline_panel, cx),
5472                    &outline_panel.cached_entries,
5473                    outline_panel.selected_entry(),
5474                    cx,
5475                ),
5476                format!(
5477                    r#"rust-analyzer/
5478  crates/
5479    ide/src/
5480      inlay_hints/
5481        fn_lifetime_fn.rs{SELECTED_MARKER}
5482      inlay_hints.rs
5483        search: pub «param_names_for_lifetime_elision_hints»: bool,
5484        search: «param_names_for_lifetime_elision_hints»: self
5485      static_index.rs
5486        search: «param_names_for_lifetime_elision_hints»: false,
5487    rust-analyzer/src/
5488      cli/
5489        analysis_stats.rs
5490          search: «param_names_for_lifetime_elision_hints»: true,
5491      config.rs
5492        search: «param_names_for_lifetime_elision_hints»: self"#,
5493                )
5494            );
5495        });
5496
5497        outline_panel.update_in(cx, |outline_panel, window, cx| {
5498            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5499        });
5500        cx.executor()
5501            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5502        cx.run_until_parked();
5503        outline_panel.update_in(cx, |outline_panel, window, cx| {
5504            outline_panel.select_parent(&SelectParent, window, cx);
5505            assert_eq!(
5506                display_entries(
5507                    &project,
5508                    &snapshot(outline_panel, cx),
5509                    &outline_panel.cached_entries,
5510                    outline_panel.selected_entry(),
5511                    cx,
5512                ),
5513                select_first_in_all_matches("inlay_hints/")
5514            );
5515        });
5516
5517        outline_panel.update_in(cx, |outline_panel, window, cx| {
5518            outline_panel.select_parent(&SelectParent, window, cx);
5519            assert_eq!(
5520                display_entries(
5521                    &project,
5522                    &snapshot(outline_panel, cx),
5523                    &outline_panel.cached_entries,
5524                    outline_panel.selected_entry(),
5525                    cx,
5526                ),
5527                select_first_in_all_matches("ide/src/")
5528            );
5529        });
5530
5531        outline_panel.update_in(cx, |outline_panel, window, cx| {
5532            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5533        });
5534        cx.executor()
5535            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5536        cx.run_until_parked();
5537        outline_panel.update(cx, |outline_panel, cx| {
5538            assert_eq!(
5539                display_entries(
5540                    &project,
5541                    &snapshot(outline_panel, cx),
5542                    &outline_panel.cached_entries,
5543                    outline_panel.selected_entry(),
5544                    cx,
5545                ),
5546                format!(
5547                    r#"rust-analyzer/
5548  crates/
5549    ide/src/{SELECTED_MARKER}
5550    rust-analyzer/src/
5551      cli/
5552        analysis_stats.rs
5553          search: «param_names_for_lifetime_elision_hints»: true,
5554      config.rs
5555        search: «param_names_for_lifetime_elision_hints»: self"#,
5556                )
5557            );
5558        });
5559        outline_panel.update_in(cx, |outline_panel, window, cx| {
5560            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5561        });
5562        cx.executor()
5563            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5564        cx.run_until_parked();
5565        outline_panel.update(cx, |outline_panel, cx| {
5566            assert_eq!(
5567                display_entries(
5568                    &project,
5569                    &snapshot(outline_panel, cx),
5570                    &outline_panel.cached_entries,
5571                    outline_panel.selected_entry(),
5572                    cx,
5573                ),
5574                select_first_in_all_matches("ide/src/")
5575            );
5576        });
5577    }
5578
5579    #[gpui::test(iterations = 10)]
5580    async fn test_item_filtering(cx: &mut TestAppContext) {
5581        init_test(cx);
5582
5583        let fs = FakeFs::new(cx.background_executor.clone());
5584        let root = path!("/rust-analyzer");
5585        populate_with_test_ra_project(&fs, root).await;
5586        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5587        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5588        let workspace = add_outline_panel(&project, cx).await;
5589        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5590        let outline_panel = outline_panel(&workspace, cx);
5591        outline_panel.update_in(cx, |outline_panel, window, cx| {
5592            outline_panel.set_active(true, window, cx)
5593        });
5594
5595        workspace
5596            .update(cx, |workspace, window, cx| {
5597                ProjectSearchView::deploy_search(
5598                    workspace,
5599                    &workspace::DeploySearch::default(),
5600                    window,
5601                    cx,
5602                )
5603            })
5604            .unwrap();
5605        let search_view = workspace
5606            .update(cx, |workspace, _, cx| {
5607                workspace
5608                    .active_pane()
5609                    .read(cx)
5610                    .items()
5611                    .find_map(|item| item.downcast::<ProjectSearchView>())
5612                    .expect("Project search view expected to appear after new search event trigger")
5613            })
5614            .unwrap();
5615
5616        let query = "param_names_for_lifetime_elision_hints";
5617        perform_project_search(&search_view, query, cx);
5618        search_view.update(cx, |search_view, cx| {
5619            search_view
5620                .results_editor()
5621                .update(cx, |results_editor, cx| {
5622                    assert_eq!(
5623                        results_editor.display_text(cx).match_indices(query).count(),
5624                        9
5625                    );
5626                });
5627        });
5628        let all_matches = r#"rust-analyzer/
5629  crates/
5630    ide/src/
5631      inlay_hints/
5632        fn_lifetime_fn.rs
5633          search: match config.«param_names_for_lifetime_elision_hints» {
5634          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5635          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5636          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5637      inlay_hints.rs
5638        search: pub «param_names_for_lifetime_elision_hints»: bool,
5639        search: «param_names_for_lifetime_elision_hints»: self
5640      static_index.rs
5641        search: «param_names_for_lifetime_elision_hints»: false,
5642    rust-analyzer/src/
5643      cli/
5644        analysis_stats.rs
5645          search: «param_names_for_lifetime_elision_hints»: true,
5646      config.rs
5647        search: «param_names_for_lifetime_elision_hints»: self"#
5648            .to_string();
5649
5650        cx.executor()
5651            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5652        cx.run_until_parked();
5653        outline_panel.update(cx, |outline_panel, cx| {
5654            assert_eq!(
5655                display_entries(
5656                    &project,
5657                    &snapshot(outline_panel, cx),
5658                    &outline_panel.cached_entries,
5659                    None,
5660                    cx,
5661                ),
5662                all_matches,
5663            );
5664        });
5665
5666        let filter_text = "a";
5667        outline_panel.update_in(cx, |outline_panel, window, cx| {
5668            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5669                filter_editor.set_text(filter_text, window, cx);
5670            });
5671        });
5672        cx.executor()
5673            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5674        cx.run_until_parked();
5675
5676        outline_panel.update(cx, |outline_panel, cx| {
5677            assert_eq!(
5678                display_entries(
5679                    &project,
5680                    &snapshot(outline_panel, cx),
5681                    &outline_panel.cached_entries,
5682                    None,
5683                    cx,
5684                ),
5685                all_matches
5686                    .lines()
5687                    .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5688                    .filter(|item| item.contains(filter_text))
5689                    .collect::<Vec<_>>()
5690                    .join("\n"),
5691            );
5692        });
5693
5694        outline_panel.update_in(cx, |outline_panel, window, cx| {
5695            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5696                filter_editor.set_text("", window, cx);
5697            });
5698        });
5699        cx.executor()
5700            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5701        cx.run_until_parked();
5702        outline_panel.update(cx, |outline_panel, cx| {
5703            assert_eq!(
5704                display_entries(
5705                    &project,
5706                    &snapshot(outline_panel, cx),
5707                    &outline_panel.cached_entries,
5708                    None,
5709                    cx,
5710                ),
5711                all_matches,
5712            );
5713        });
5714    }
5715
5716    #[gpui::test(iterations = 10)]
5717    async fn test_item_opening(cx: &mut TestAppContext) {
5718        init_test(cx);
5719
5720        let fs = FakeFs::new(cx.background_executor.clone());
5721        let root = path!("/rust-analyzer");
5722        populate_with_test_ra_project(&fs, root).await;
5723        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5724        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5725        let workspace = add_outline_panel(&project, cx).await;
5726        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5727        let outline_panel = outline_panel(&workspace, cx);
5728        outline_panel.update_in(cx, |outline_panel, window, cx| {
5729            outline_panel.set_active(true, window, cx)
5730        });
5731
5732        workspace
5733            .update(cx, |workspace, window, cx| {
5734                ProjectSearchView::deploy_search(
5735                    workspace,
5736                    &workspace::DeploySearch::default(),
5737                    window,
5738                    cx,
5739                )
5740            })
5741            .unwrap();
5742        let search_view = workspace
5743            .update(cx, |workspace, _, cx| {
5744                workspace
5745                    .active_pane()
5746                    .read(cx)
5747                    .items()
5748                    .find_map(|item| item.downcast::<ProjectSearchView>())
5749                    .expect("Project search view expected to appear after new search event trigger")
5750            })
5751            .unwrap();
5752
5753        let query = "param_names_for_lifetime_elision_hints";
5754        perform_project_search(&search_view, query, cx);
5755        search_view.update(cx, |search_view, cx| {
5756            search_view
5757                .results_editor()
5758                .update(cx, |results_editor, cx| {
5759                    assert_eq!(
5760                        results_editor.display_text(cx).match_indices(query).count(),
5761                        9
5762                    );
5763                });
5764        });
5765        let all_matches = r#"rust-analyzer/
5766  crates/
5767    ide/src/
5768      inlay_hints/
5769        fn_lifetime_fn.rs
5770          search: match config.«param_names_for_lifetime_elision_hints» {
5771          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5772          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5773          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5774      inlay_hints.rs
5775        search: pub «param_names_for_lifetime_elision_hints»: bool,
5776        search: «param_names_for_lifetime_elision_hints»: self
5777      static_index.rs
5778        search: «param_names_for_lifetime_elision_hints»: false,
5779    rust-analyzer/src/
5780      cli/
5781        analysis_stats.rs
5782          search: «param_names_for_lifetime_elision_hints»: true,
5783      config.rs
5784        search: «param_names_for_lifetime_elision_hints»: self"#
5785            .to_string();
5786        let select_first_in_all_matches = |line_to_select: &str| {
5787            assert!(
5788                all_matches.contains(line_to_select),
5789                "`{line_to_select}` was not found in all matches `{all_matches}`"
5790            );
5791            all_matches.replacen(
5792                line_to_select,
5793                &format!("{line_to_select}{SELECTED_MARKER}"),
5794                1,
5795            )
5796        };
5797        let clear_outline_metadata = |input: &str| {
5798            input
5799                .replace("search: ", "")
5800                .replace("«", "")
5801                .replace("»", "")
5802        };
5803
5804        cx.executor()
5805            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5806        cx.run_until_parked();
5807
5808        let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5809            outline_panel
5810                .active_editor()
5811                .expect("should have an active editor open")
5812        });
5813        let initial_outline_selection =
5814            "search: match config.«param_names_for_lifetime_elision_hints» {";
5815        outline_panel.update_in(cx, |outline_panel, window, cx| {
5816            assert_eq!(
5817                display_entries(
5818                    &project,
5819                    &snapshot(outline_panel, cx),
5820                    &outline_panel.cached_entries,
5821                    outline_panel.selected_entry(),
5822                    cx,
5823                ),
5824                select_first_in_all_matches(initial_outline_selection)
5825            );
5826            assert_eq!(
5827                selected_row_text(&active_editor, cx),
5828                clear_outline_metadata(initial_outline_selection),
5829                "Should place the initial editor selection on the corresponding search result"
5830            );
5831
5832            outline_panel.select_next(&SelectNext, window, cx);
5833            outline_panel.select_next(&SelectNext, window, cx);
5834        });
5835
5836        let navigated_outline_selection =
5837            "search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {";
5838        outline_panel.update(cx, |outline_panel, cx| {
5839            assert_eq!(
5840                display_entries(
5841                    &project,
5842                    &snapshot(outline_panel, cx),
5843                    &outline_panel.cached_entries,
5844                    outline_panel.selected_entry(),
5845                    cx,
5846                ),
5847                select_first_in_all_matches(navigated_outline_selection)
5848            );
5849        });
5850        cx.executor()
5851            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5852        outline_panel.update(cx, |_, cx| {
5853            assert_eq!(
5854                selected_row_text(&active_editor, cx),
5855                clear_outline_metadata(navigated_outline_selection),
5856                "Should still have the initial caret position after SelectNext calls"
5857            );
5858        });
5859
5860        outline_panel.update_in(cx, |outline_panel, window, cx| {
5861            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5862        });
5863        outline_panel.update(cx, |_outline_panel, cx| {
5864            assert_eq!(
5865                selected_row_text(&active_editor, cx),
5866                clear_outline_metadata(navigated_outline_selection),
5867                "After opening, should move the caret to the opened outline entry's position"
5868            );
5869        });
5870
5871        outline_panel.update_in(cx, |outline_panel, window, cx| {
5872            outline_panel.select_next(&SelectNext, window, cx);
5873        });
5874        let next_navigated_outline_selection = "search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },";
5875        outline_panel.update(cx, |outline_panel, cx| {
5876            assert_eq!(
5877                display_entries(
5878                    &project,
5879                    &snapshot(outline_panel, cx),
5880                    &outline_panel.cached_entries,
5881                    outline_panel.selected_entry(),
5882                    cx,
5883                ),
5884                select_first_in_all_matches(next_navigated_outline_selection)
5885            );
5886        });
5887        cx.executor()
5888            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5889        outline_panel.update(cx, |_outline_panel, cx| {
5890            assert_eq!(
5891                selected_row_text(&active_editor, cx),
5892                clear_outline_metadata(next_navigated_outline_selection),
5893                "Should again preserve the selection after another SelectNext call"
5894            );
5895        });
5896
5897        outline_panel.update_in(cx, |outline_panel, window, cx| {
5898            outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx);
5899        });
5900        cx.executor()
5901            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5902        cx.run_until_parked();
5903        let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5904            outline_panel
5905                .active_editor()
5906                .expect("should have an active editor open")
5907        });
5908        outline_panel.update(cx, |outline_panel, cx| {
5909            assert_ne!(
5910                active_editor, new_active_editor,
5911                "After opening an excerpt, new editor should be open"
5912            );
5913            assert_eq!(
5914                display_entries(
5915                    &project,
5916                    &snapshot(outline_panel, cx),
5917                    &outline_panel.cached_entries,
5918                    outline_panel.selected_entry(),
5919                    cx,
5920                ),
5921                "outline: pub(super) fn hints
5922outline: fn hints_lifetimes_named  <==== selected"
5923            );
5924            assert_eq!(
5925                selected_row_text(&new_active_editor, cx),
5926                clear_outline_metadata(next_navigated_outline_selection),
5927                "When opening the excerpt, should navigate to the place corresponding the outline entry"
5928            );
5929        });
5930    }
5931
5932    #[gpui::test]
5933    async fn test_multiple_worktrees(cx: &mut TestAppContext) {
5934        init_test(cx);
5935
5936        let fs = FakeFs::new(cx.background_executor.clone());
5937        fs.insert_tree(
5938            path!("/root"),
5939            json!({
5940                "one": {
5941                    "a.txt": "aaa aaa"
5942                },
5943                "two": {
5944                    "b.txt": "a aaa"
5945                }
5946
5947            }),
5948        )
5949        .await;
5950        let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await;
5951        let workspace = add_outline_panel(&project, cx).await;
5952        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5953        let outline_panel = outline_panel(&workspace, cx);
5954        outline_panel.update_in(cx, |outline_panel, window, cx| {
5955            outline_panel.set_active(true, window, cx)
5956        });
5957
5958        let items = workspace
5959            .update(cx, |workspace, window, cx| {
5960                workspace.open_paths(
5961                    vec![PathBuf::from(path!("/root/two"))],
5962                    OpenOptions {
5963                        visible: Some(OpenVisible::OnlyDirectories),
5964                        ..Default::default()
5965                    },
5966                    None,
5967                    window,
5968                    cx,
5969                )
5970            })
5971            .unwrap()
5972            .await;
5973        assert_eq!(items.len(), 1, "Were opening another worktree directory");
5974        assert!(
5975            items[0].is_none(),
5976            "Directory should be opened successfully"
5977        );
5978
5979        workspace
5980            .update(cx, |workspace, window, cx| {
5981                ProjectSearchView::deploy_search(
5982                    workspace,
5983                    &workspace::DeploySearch::default(),
5984                    window,
5985                    cx,
5986                )
5987            })
5988            .unwrap();
5989        let search_view = workspace
5990            .update(cx, |workspace, _, cx| {
5991                workspace
5992                    .active_pane()
5993                    .read(cx)
5994                    .items()
5995                    .find_map(|item| item.downcast::<ProjectSearchView>())
5996                    .expect("Project search view expected to appear after new search event trigger")
5997            })
5998            .unwrap();
5999
6000        let query = "aaa";
6001        perform_project_search(&search_view, query, cx);
6002        search_view.update(cx, |search_view, cx| {
6003            search_view
6004                .results_editor()
6005                .update(cx, |results_editor, cx| {
6006                    assert_eq!(
6007                        results_editor.display_text(cx).match_indices(query).count(),
6008                        3
6009                    );
6010                });
6011        });
6012
6013        cx.executor()
6014            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6015        cx.run_until_parked();
6016        outline_panel.update(cx, |outline_panel, cx| {
6017            assert_eq!(
6018                display_entries(
6019                    &project,
6020                    &snapshot(outline_panel, cx),
6021                    &outline_panel.cached_entries,
6022                    outline_panel.selected_entry(),
6023                    cx,
6024                ),
6025                format!(
6026                    r#"one/
6027  a.txt
6028    search: «aaa» aaa  <==== selected
6029    search: aaa «aaa»
6030two/
6031  b.txt
6032    search: a «aaa»"#,
6033                ),
6034            );
6035        });
6036
6037        outline_panel.update_in(cx, |outline_panel, window, cx| {
6038            outline_panel.select_previous(&SelectPrevious, window, cx);
6039            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6040        });
6041        cx.executor()
6042            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6043        cx.run_until_parked();
6044        outline_panel.update(cx, |outline_panel, cx| {
6045            assert_eq!(
6046                display_entries(
6047                    &project,
6048                    &snapshot(outline_panel, cx),
6049                    &outline_panel.cached_entries,
6050                    outline_panel.selected_entry(),
6051                    cx,
6052                ),
6053                format!(
6054                    r#"one/
6055  a.txt  <==== selected
6056two/
6057  b.txt
6058    search: a «aaa»"#,
6059                ),
6060            );
6061        });
6062
6063        outline_panel.update_in(cx, |outline_panel, window, cx| {
6064            outline_panel.select_next(&SelectNext, window, cx);
6065            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6066        });
6067        cx.executor()
6068            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6069        cx.run_until_parked();
6070        outline_panel.update(cx, |outline_panel, cx| {
6071            assert_eq!(
6072                display_entries(
6073                    &project,
6074                    &snapshot(outline_panel, cx),
6075                    &outline_panel.cached_entries,
6076                    outline_panel.selected_entry(),
6077                    cx,
6078                ),
6079                format!(
6080                    r#"one/
6081  a.txt
6082two/  <==== selected"#,
6083                ),
6084            );
6085        });
6086
6087        outline_panel.update_in(cx, |outline_panel, window, cx| {
6088            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
6089        });
6090        cx.executor()
6091            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6092        cx.run_until_parked();
6093        outline_panel.update(cx, |outline_panel, cx| {
6094            assert_eq!(
6095                display_entries(
6096                    &project,
6097                    &snapshot(outline_panel, cx),
6098                    &outline_panel.cached_entries,
6099                    outline_panel.selected_entry(),
6100                    cx,
6101                ),
6102                format!(
6103                    r#"one/
6104  a.txt
6105two/  <==== selected
6106  b.txt
6107    search: a «aaa»"#,
6108                )
6109            );
6110        });
6111    }
6112
6113    #[gpui::test]
6114    async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
6115        init_test(cx);
6116
6117        let root = path!("/root");
6118        let fs = FakeFs::new(cx.background_executor.clone());
6119        fs.insert_tree(
6120            root,
6121            json!({
6122                "src": {
6123                    "lib.rs": indoc!("
6124#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6125struct OutlineEntryExcerpt {
6126    id: ExcerptId,
6127    buffer_id: BufferId,
6128    range: ExcerptRange<language::Anchor>,
6129}"),
6130                }
6131            }),
6132        )
6133        .await;
6134        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6135        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
6136        let workspace = add_outline_panel(&project, cx).await;
6137        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6138        let outline_panel = outline_panel(&workspace, cx);
6139        cx.update(|window, cx| {
6140            outline_panel.update(cx, |outline_panel, cx| {
6141                outline_panel.set_active(true, window, cx)
6142            });
6143        });
6144
6145        let _editor = workspace
6146            .update(cx, |workspace, window, cx| {
6147                workspace.open_abs_path(
6148                    PathBuf::from(path!("/root/src/lib.rs")),
6149                    OpenOptions {
6150                        visible: Some(OpenVisible::All),
6151                        ..Default::default()
6152                    },
6153                    window,
6154                    cx,
6155                )
6156            })
6157            .unwrap()
6158            .await
6159            .expect("Failed to open Rust source file")
6160            .downcast::<Editor>()
6161            .expect("Should open an editor for Rust source file");
6162
6163        cx.executor()
6164            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6165        cx.run_until_parked();
6166        outline_panel.update(cx, |outline_panel, cx| {
6167            assert_eq!(
6168                display_entries(
6169                    &project,
6170                    &snapshot(outline_panel, cx),
6171                    &outline_panel.cached_entries,
6172                    outline_panel.selected_entry(),
6173                    cx,
6174                ),
6175                indoc!(
6176                    "
6177outline: struct OutlineEntryExcerpt
6178  outline: id
6179  outline: buffer_id
6180  outline: range"
6181                )
6182            );
6183        });
6184
6185        cx.update(|window, cx| {
6186            outline_panel.update(cx, |outline_panel, cx| {
6187                outline_panel.select_next(&SelectNext, window, cx);
6188            });
6189        });
6190        cx.executor()
6191            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6192        cx.run_until_parked();
6193        outline_panel.update(cx, |outline_panel, cx| {
6194            assert_eq!(
6195                display_entries(
6196                    &project,
6197                    &snapshot(outline_panel, cx),
6198                    &outline_panel.cached_entries,
6199                    outline_panel.selected_entry(),
6200                    cx,
6201                ),
6202                indoc!(
6203                    "
6204outline: struct OutlineEntryExcerpt  <==== selected
6205  outline: id
6206  outline: buffer_id
6207  outline: range"
6208                )
6209            );
6210        });
6211
6212        cx.update(|window, cx| {
6213            outline_panel.update(cx, |outline_panel, cx| {
6214                outline_panel.select_next(&SelectNext, window, cx);
6215            });
6216        });
6217        cx.executor()
6218            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6219        cx.run_until_parked();
6220        outline_panel.update(cx, |outline_panel, cx| {
6221            assert_eq!(
6222                display_entries(
6223                    &project,
6224                    &snapshot(outline_panel, cx),
6225                    &outline_panel.cached_entries,
6226                    outline_panel.selected_entry(),
6227                    cx,
6228                ),
6229                indoc!(
6230                    "
6231outline: struct OutlineEntryExcerpt
6232  outline: id  <==== selected
6233  outline: buffer_id
6234  outline: range"
6235                )
6236            );
6237        });
6238
6239        cx.update(|window, cx| {
6240            outline_panel.update(cx, |outline_panel, cx| {
6241                outline_panel.select_next(&SelectNext, window, cx);
6242            });
6243        });
6244        cx.executor()
6245            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6246        cx.run_until_parked();
6247        outline_panel.update(cx, |outline_panel, cx| {
6248            assert_eq!(
6249                display_entries(
6250                    &project,
6251                    &snapshot(outline_panel, cx),
6252                    &outline_panel.cached_entries,
6253                    outline_panel.selected_entry(),
6254                    cx,
6255                ),
6256                indoc!(
6257                    "
6258outline: struct OutlineEntryExcerpt
6259  outline: id
6260  outline: buffer_id  <==== selected
6261  outline: range"
6262                )
6263            );
6264        });
6265
6266        cx.update(|window, cx| {
6267            outline_panel.update(cx, |outline_panel, cx| {
6268                outline_panel.select_next(&SelectNext, window, cx);
6269            });
6270        });
6271        cx.executor()
6272            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6273        cx.run_until_parked();
6274        outline_panel.update(cx, |outline_panel, cx| {
6275            assert_eq!(
6276                display_entries(
6277                    &project,
6278                    &snapshot(outline_panel, cx),
6279                    &outline_panel.cached_entries,
6280                    outline_panel.selected_entry(),
6281                    cx,
6282                ),
6283                indoc!(
6284                    "
6285outline: struct OutlineEntryExcerpt
6286  outline: id
6287  outline: buffer_id
6288  outline: range  <==== selected"
6289                )
6290            );
6291        });
6292
6293        cx.update(|window, cx| {
6294            outline_panel.update(cx, |outline_panel, cx| {
6295                outline_panel.select_next(&SelectNext, window, cx);
6296            });
6297        });
6298        cx.executor()
6299            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6300        cx.run_until_parked();
6301        outline_panel.update(cx, |outline_panel, cx| {
6302            assert_eq!(
6303                display_entries(
6304                    &project,
6305                    &snapshot(outline_panel, cx),
6306                    &outline_panel.cached_entries,
6307                    outline_panel.selected_entry(),
6308                    cx,
6309                ),
6310                indoc!(
6311                    "
6312outline: struct OutlineEntryExcerpt  <==== selected
6313  outline: id
6314  outline: buffer_id
6315  outline: range"
6316                )
6317            );
6318        });
6319
6320        cx.update(|window, cx| {
6321            outline_panel.update(cx, |outline_panel, cx| {
6322                outline_panel.select_previous(&SelectPrevious, window, cx);
6323            });
6324        });
6325        cx.executor()
6326            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6327        cx.run_until_parked();
6328        outline_panel.update(cx, |outline_panel, cx| {
6329            assert_eq!(
6330                display_entries(
6331                    &project,
6332                    &snapshot(outline_panel, cx),
6333                    &outline_panel.cached_entries,
6334                    outline_panel.selected_entry(),
6335                    cx,
6336                ),
6337                indoc!(
6338                    "
6339outline: struct OutlineEntryExcerpt
6340  outline: id
6341  outline: buffer_id
6342  outline: range  <==== selected"
6343                )
6344            );
6345        });
6346
6347        cx.update(|window, cx| {
6348            outline_panel.update(cx, |outline_panel, cx| {
6349                outline_panel.select_previous(&SelectPrevious, window, cx);
6350            });
6351        });
6352        cx.executor()
6353            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6354        cx.run_until_parked();
6355        outline_panel.update(cx, |outline_panel, cx| {
6356            assert_eq!(
6357                display_entries(
6358                    &project,
6359                    &snapshot(outline_panel, cx),
6360                    &outline_panel.cached_entries,
6361                    outline_panel.selected_entry(),
6362                    cx,
6363                ),
6364                indoc!(
6365                    "
6366outline: struct OutlineEntryExcerpt
6367  outline: id
6368  outline: buffer_id  <==== selected
6369  outline: range"
6370                )
6371            );
6372        });
6373
6374        cx.update(|window, cx| {
6375            outline_panel.update(cx, |outline_panel, cx| {
6376                outline_panel.select_previous(&SelectPrevious, window, cx);
6377            });
6378        });
6379        cx.executor()
6380            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6381        cx.run_until_parked();
6382        outline_panel.update(cx, |outline_panel, cx| {
6383            assert_eq!(
6384                display_entries(
6385                    &project,
6386                    &snapshot(outline_panel, cx),
6387                    &outline_panel.cached_entries,
6388                    outline_panel.selected_entry(),
6389                    cx,
6390                ),
6391                indoc!(
6392                    "
6393outline: struct OutlineEntryExcerpt
6394  outline: id  <==== selected
6395  outline: buffer_id
6396  outline: range"
6397                )
6398            );
6399        });
6400
6401        cx.update(|window, cx| {
6402            outline_panel.update(cx, |outline_panel, cx| {
6403                outline_panel.select_previous(&SelectPrevious, window, cx);
6404            });
6405        });
6406        cx.executor()
6407            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6408        cx.run_until_parked();
6409        outline_panel.update(cx, |outline_panel, cx| {
6410            assert_eq!(
6411                display_entries(
6412                    &project,
6413                    &snapshot(outline_panel, cx),
6414                    &outline_panel.cached_entries,
6415                    outline_panel.selected_entry(),
6416                    cx,
6417                ),
6418                indoc!(
6419                    "
6420outline: struct OutlineEntryExcerpt  <==== selected
6421  outline: id
6422  outline: buffer_id
6423  outline: range"
6424                )
6425            );
6426        });
6427
6428        cx.update(|window, cx| {
6429            outline_panel.update(cx, |outline_panel, cx| {
6430                outline_panel.select_previous(&SelectPrevious, window, cx);
6431            });
6432        });
6433        cx.executor()
6434            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6435        cx.run_until_parked();
6436        outline_panel.update(cx, |outline_panel, cx| {
6437            assert_eq!(
6438                display_entries(
6439                    &project,
6440                    &snapshot(outline_panel, cx),
6441                    &outline_panel.cached_entries,
6442                    outline_panel.selected_entry(),
6443                    cx,
6444                ),
6445                indoc!(
6446                    "
6447outline: struct OutlineEntryExcerpt
6448  outline: id
6449  outline: buffer_id
6450  outline: range  <==== selected"
6451                )
6452            );
6453        });
6454    }
6455
6456    #[gpui::test(iterations = 10)]
6457    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6458        init_test(cx);
6459
6460        let root = path!("/frontend-project");
6461        let fs = FakeFs::new(cx.background_executor.clone());
6462        fs.insert_tree(
6463            root,
6464            json!({
6465                "public": {
6466                    "lottie": {
6467                        "syntax-tree.json": r#"{ "something": "static" }"#
6468                    }
6469                },
6470                "src": {
6471                    "app": {
6472                        "(site)": {
6473                            "(about)": {
6474                                "jobs": {
6475                                    "[slug]": {
6476                                        "page.tsx": r#"static"#
6477                                    }
6478                                }
6479                            },
6480                            "(blog)": {
6481                                "post": {
6482                                    "[slug]": {
6483                                        "page.tsx": r#"static"#
6484                                    }
6485                                }
6486                            },
6487                        }
6488                    },
6489                    "components": {
6490                        "ErrorBoundary.tsx": r#"static"#,
6491                    }
6492                }
6493
6494            }),
6495        )
6496        .await;
6497        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6498        let workspace = add_outline_panel(&project, cx).await;
6499        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6500        let outline_panel = outline_panel(&workspace, cx);
6501        outline_panel.update_in(cx, |outline_panel, window, cx| {
6502            outline_panel.set_active(true, window, cx)
6503        });
6504
6505        workspace
6506            .update(cx, |workspace, window, cx| {
6507                ProjectSearchView::deploy_search(
6508                    workspace,
6509                    &workspace::DeploySearch::default(),
6510                    window,
6511                    cx,
6512                )
6513            })
6514            .unwrap();
6515        let search_view = workspace
6516            .update(cx, |workspace, _, cx| {
6517                workspace
6518                    .active_pane()
6519                    .read(cx)
6520                    .items()
6521                    .find_map(|item| item.downcast::<ProjectSearchView>())
6522                    .expect("Project search view expected to appear after new search event trigger")
6523            })
6524            .unwrap();
6525
6526        let query = "static";
6527        perform_project_search(&search_view, query, cx);
6528        search_view.update(cx, |search_view, cx| {
6529            search_view
6530                .results_editor()
6531                .update(cx, |results_editor, cx| {
6532                    assert_eq!(
6533                        results_editor.display_text(cx).match_indices(query).count(),
6534                        4
6535                    );
6536                });
6537        });
6538
6539        cx.executor()
6540            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6541        cx.run_until_parked();
6542        outline_panel.update(cx, |outline_panel, cx| {
6543            assert_eq!(
6544                display_entries(
6545                    &project,
6546                    &snapshot(outline_panel, cx),
6547                    &outline_panel.cached_entries,
6548                    outline_panel.selected_entry(),
6549                    cx,
6550                ),
6551                format!(
6552                    r#"frontend-project/
6553  public/lottie/
6554    syntax-tree.json
6555      search: {{ "something": "«static»" }}  <==== selected
6556  src/
6557    app/(site)/
6558      (about)/jobs/[slug]/
6559        page.tsx
6560          search: «static»
6561      (blog)/post/[slug]/
6562        page.tsx
6563          search: «static»
6564    components/
6565      ErrorBoundary.tsx
6566        search: «static»"#
6567                )
6568            );
6569        });
6570
6571        outline_panel.update_in(cx, |outline_panel, window, cx| {
6572            // Move to 5th element in the list, 3 items down.
6573            for _ in 0..2 {
6574                outline_panel.select_next(&SelectNext, window, cx);
6575            }
6576            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6577        });
6578        cx.executor()
6579            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6580        cx.run_until_parked();
6581        outline_panel.update(cx, |outline_panel, cx| {
6582            assert_eq!(
6583                display_entries(
6584                    &project,
6585                    &snapshot(outline_panel, cx),
6586                    &outline_panel.cached_entries,
6587                    outline_panel.selected_entry(),
6588                    cx,
6589                ),
6590                format!(
6591                    r#"frontend-project/
6592  public/lottie/
6593    syntax-tree.json
6594      search: {{ "something": "«static»" }}
6595  src/
6596    app/(site)/  <==== selected
6597    components/
6598      ErrorBoundary.tsx
6599        search: «static»"#
6600                )
6601            );
6602        });
6603
6604        outline_panel.update_in(cx, |outline_panel, window, cx| {
6605            // Move to the next visible non-FS entry
6606            for _ in 0..3 {
6607                outline_panel.select_next(&SelectNext, window, cx);
6608            }
6609        });
6610        cx.run_until_parked();
6611        outline_panel.update(cx, |outline_panel, cx| {
6612            assert_eq!(
6613                display_entries(
6614                    &project,
6615                    &snapshot(outline_panel, cx),
6616                    &outline_panel.cached_entries,
6617                    outline_panel.selected_entry(),
6618                    cx,
6619                ),
6620                format!(
6621                    r#"frontend-project/
6622  public/lottie/
6623    syntax-tree.json
6624      search: {{ "something": "«static»" }}
6625  src/
6626    app/(site)/
6627    components/
6628      ErrorBoundary.tsx
6629        search: «static»  <==== selected"#
6630                )
6631            );
6632        });
6633
6634        outline_panel.update_in(cx, |outline_panel, window, cx| {
6635            outline_panel
6636                .active_editor()
6637                .expect("Should have an active editor")
6638                .update(cx, |editor, cx| {
6639                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6640                });
6641        });
6642        cx.executor()
6643            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6644        cx.run_until_parked();
6645        outline_panel.update(cx, |outline_panel, cx| {
6646            assert_eq!(
6647                display_entries(
6648                    &project,
6649                    &snapshot(outline_panel, cx),
6650                    &outline_panel.cached_entries,
6651                    outline_panel.selected_entry(),
6652                    cx,
6653                ),
6654                format!(
6655                    r#"frontend-project/
6656  public/lottie/
6657    syntax-tree.json
6658      search: {{ "something": "«static»" }}
6659  src/
6660    app/(site)/
6661    components/
6662      ErrorBoundary.tsx  <==== selected"#
6663                )
6664            );
6665        });
6666
6667        outline_panel.update_in(cx, |outline_panel, window, cx| {
6668            outline_panel
6669                .active_editor()
6670                .expect("Should have an active editor")
6671                .update(cx, |editor, cx| {
6672                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6673                });
6674        });
6675        cx.executor()
6676            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6677        cx.run_until_parked();
6678        outline_panel.update(cx, |outline_panel, cx| {
6679            assert_eq!(
6680                display_entries(
6681                    &project,
6682                    &snapshot(outline_panel, cx),
6683                    &outline_panel.cached_entries,
6684                    outline_panel.selected_entry(),
6685                    cx,
6686                ),
6687                format!(
6688                    r#"frontend-project/
6689  public/lottie/
6690    syntax-tree.json
6691      search: {{ "something": "«static»" }}
6692  src/
6693    app/(site)/
6694    components/
6695      ErrorBoundary.tsx  <==== selected
6696        search: «static»"#
6697                )
6698            );
6699        });
6700
6701        outline_panel.update_in(cx, |outline_panel, window, cx| {
6702            outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx);
6703        });
6704        cx.executor()
6705            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6706        cx.run_until_parked();
6707        outline_panel.update(cx, |outline_panel, cx| {
6708            assert_eq!(
6709                display_entries(
6710                    &project,
6711                    &snapshot(outline_panel, cx),
6712                    &outline_panel.cached_entries,
6713                    outline_panel.selected_entry(),
6714                    cx,
6715                ),
6716                format!(r#"frontend-project/"#)
6717            );
6718        });
6719
6720        outline_panel.update_in(cx, |outline_panel, window, cx| {
6721            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
6722        });
6723        cx.executor()
6724            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6725        cx.run_until_parked();
6726        outline_panel.update(cx, |outline_panel, cx| {
6727            assert_eq!(
6728                display_entries(
6729                    &project,
6730                    &snapshot(outline_panel, cx),
6731                    &outline_panel.cached_entries,
6732                    outline_panel.selected_entry(),
6733                    cx,
6734                ),
6735                format!(
6736                    r#"frontend-project/
6737  public/lottie/
6738    syntax-tree.json
6739      search: {{ "something": "«static»" }}
6740  src/
6741    app/(site)/
6742      (about)/jobs/[slug]/
6743        page.tsx
6744          search: «static»
6745      (blog)/post/[slug]/
6746        page.tsx
6747          search: «static»
6748    components/
6749      ErrorBoundary.tsx  <==== selected
6750        search: «static»"#
6751                )
6752            );
6753        });
6754    }
6755
6756    async fn add_outline_panel(
6757        project: &Entity<Project>,
6758        cx: &mut TestAppContext,
6759    ) -> WindowHandle<Workspace> {
6760        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6761
6762        let outline_panel = window
6763            .update(cx, |_, window, cx| {
6764                cx.spawn_in(window, async |this, cx| {
6765                    OutlinePanel::load(this, cx.clone()).await
6766                })
6767            })
6768            .unwrap()
6769            .await
6770            .expect("Failed to load outline panel");
6771
6772        window
6773            .update(cx, |workspace, window, cx| {
6774                workspace.add_panel(outline_panel, window, cx);
6775            })
6776            .unwrap();
6777        window
6778    }
6779
6780    fn outline_panel(
6781        workspace: &WindowHandle<Workspace>,
6782        cx: &mut TestAppContext,
6783    ) -> Entity<OutlinePanel> {
6784        workspace
6785            .update(cx, |workspace, _, cx| {
6786                workspace
6787                    .panel::<OutlinePanel>(cx)
6788                    .expect("no outline panel")
6789            })
6790            .unwrap()
6791    }
6792
6793    fn display_entries(
6794        project: &Entity<Project>,
6795        multi_buffer_snapshot: &MultiBufferSnapshot,
6796        cached_entries: &[CachedEntry],
6797        selected_entry: Option<&PanelEntry>,
6798        cx: &mut App,
6799    ) -> String {
6800        let project = project.read(cx);
6801        let mut display_string = String::new();
6802        for entry in cached_entries {
6803            if !display_string.is_empty() {
6804                display_string += "\n";
6805            }
6806            for _ in 0..entry.depth {
6807                display_string += "  ";
6808            }
6809            display_string += &match &entry.entry {
6810                PanelEntry::Fs(entry) => match entry {
6811                    FsEntry::ExternalFile(_) => {
6812                        panic!("Did not cover external files with tests")
6813                    }
6814                    FsEntry::Directory(directory) => {
6815                        let path = if let Some(worktree) = project
6816                            .worktree_for_id(directory.worktree_id, cx)
6817                            .filter(|worktree| {
6818                                worktree.read(cx).root_entry() == Some(&directory.entry.entry)
6819                            }) {
6820                            worktree
6821                                .read(cx)
6822                                .root_name()
6823                                .join(&directory.entry.path)
6824                                .as_unix_str()
6825                                .to_string()
6826                        } else {
6827                            directory
6828                                .entry
6829                                .path
6830                                .file_name()
6831                                .unwrap_or_default()
6832                                .to_string()
6833                        };
6834                        format!("{path}/")
6835                    }
6836                    FsEntry::File(file) => file
6837                        .entry
6838                        .path
6839                        .file_name()
6840                        .map(|name| name.to_string())
6841                        .unwrap_or_default(),
6842                },
6843                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6844                    .entries
6845                    .iter()
6846                    .filter_map(|dir| dir.path.file_name())
6847                    .map(|name| name.to_string() + "/")
6848                    .collect(),
6849                PanelEntry::Outline(outline_entry) => match outline_entry {
6850                    OutlineEntry::Excerpt(_) => continue,
6851                    OutlineEntry::Outline(outline_entry) => {
6852                        format!("outline: {}", outline_entry.outline.text)
6853                    }
6854                },
6855                PanelEntry::Search(search_entry) => {
6856                    let search_data = search_entry.render_data.get_or_init(|| {
6857                        SearchData::new(&search_entry.match_range, multi_buffer_snapshot)
6858                    });
6859                    let mut search_result = String::new();
6860                    let mut last_end = 0;
6861                    for range in &search_data.search_match_indices {
6862                        search_result.push_str(&search_data.context_text[last_end..range.start]);
6863                        search_result.push('«');
6864                        search_result.push_str(&search_data.context_text[range.start..range.end]);
6865                        search_result.push('»');
6866                        last_end = range.end;
6867                    }
6868                    search_result.push_str(&search_data.context_text[last_end..]);
6869
6870                    format!("search: {search_result}")
6871                }
6872            };
6873
6874            if Some(&entry.entry) == selected_entry {
6875                display_string += SELECTED_MARKER;
6876            }
6877        }
6878        display_string
6879    }
6880
6881    fn init_test(cx: &mut TestAppContext) {
6882        cx.update(|cx| {
6883            let settings = SettingsStore::test(cx);
6884            cx.set_global(settings);
6885
6886            theme::init(theme::LoadThemes::JustBase, cx);
6887
6888            editor::init(cx);
6889            project_search::init(cx);
6890            buffer_search::init(cx);
6891            super::init(cx);
6892        });
6893    }
6894
6895    // Based on https://github.com/rust-lang/rust-analyzer/
6896    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6897        fs.insert_tree(
6898            root,
6899            json!({
6900                    "crates": {
6901                        "ide": {
6902                            "src": {
6903                                "inlay_hints": {
6904                                    "fn_lifetime_fn.rs": r##"
6905        pub(super) fn hints(
6906            acc: &mut Vec<InlayHint>,
6907            config: &InlayHintsConfig,
6908            func: ast::Fn,
6909        ) -> Option<()> {
6910            // ... snip
6911
6912            let mut used_names: FxHashMap<SmolStr, usize> =
6913                match config.param_names_for_lifetime_elision_hints {
6914                    true => generic_param_list
6915                        .iter()
6916                        .flat_map(|gpl| gpl.lifetime_params())
6917                        .filter_map(|param| param.lifetime())
6918                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6919                        .collect(),
6920                    false => Default::default(),
6921                };
6922            {
6923                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6924                if self_param.is_some() && potential_lt_refs.next().is_some() {
6925                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6926                        // self can't be used as a lifetime, so no need to check for collisions
6927                        "'self".into()
6928                    } else {
6929                        gen_idx_name()
6930                    });
6931                }
6932                potential_lt_refs.for_each(|(name, ..)| {
6933                    let name = match name {
6934                        Some(it) if config.param_names_for_lifetime_elision_hints => {
6935                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
6936                                *c += 1;
6937                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6938                            } else {
6939                                used_names.insert(it.text().as_str().into(), 0);
6940                                SmolStr::from_iter(["\'", it.text().as_str()])
6941                            }
6942                        }
6943                        _ => gen_idx_name(),
6944                    };
6945                    allocated_lifetimes.push(name);
6946                });
6947            }
6948
6949            // ... snip
6950        }
6951
6952        // ... snip
6953
6954            #[test]
6955            fn hints_lifetimes_named() {
6956                check_with_config(
6957                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6958                    r#"
6959        fn nested_in<'named>(named: &        &X<      &()>) {}
6960        //          ^'named1, 'named2, 'named3, $
6961                                  //^'named1 ^'named2 ^'named3
6962        "#,
6963                );
6964            }
6965
6966        // ... snip
6967        "##,
6968                                },
6969                        "inlay_hints.rs": r#"
6970    #[derive(Clone, Debug, PartialEq, Eq)]
6971    pub struct InlayHintsConfig {
6972        // ... snip
6973        pub param_names_for_lifetime_elision_hints: bool,
6974        pub max_length: Option<usize>,
6975        // ... snip
6976    }
6977
6978    impl Config {
6979        pub fn inlay_hints(&self) -> InlayHintsConfig {
6980            InlayHintsConfig {
6981                // ... snip
6982                param_names_for_lifetime_elision_hints: self
6983                    .inlayHints_lifetimeElisionHints_useParameterNames()
6984                    .to_owned(),
6985                max_length: self.inlayHints_maxLength().to_owned(),
6986                // ... snip
6987            }
6988        }
6989    }
6990    "#,
6991                        "static_index.rs": r#"
6992// ... snip
6993        fn add_file(&mut self, file_id: FileId) {
6994            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6995            let folds = self.analysis.folding_ranges(file_id).unwrap();
6996            let inlay_hints = self
6997                .analysis
6998                .inlay_hints(
6999                    &InlayHintsConfig {
7000                        // ... snip
7001                        closure_style: hir::ClosureStyle::ImplFn,
7002                        param_names_for_lifetime_elision_hints: false,
7003                        binding_mode_hints: false,
7004                        max_length: Some(25),
7005                        closure_capture_hints: false,
7006                        // ... snip
7007                    },
7008                    file_id,
7009                    None,
7010                )
7011                .unwrap();
7012            // ... snip
7013    }
7014// ... snip
7015    "#
7016                            }
7017                        },
7018                        "rust-analyzer": {
7019                            "src": {
7020                                "cli": {
7021                                    "analysis_stats.rs": r#"
7022        // ... snip
7023                for &file_id in &file_ids {
7024                    _ = analysis.inlay_hints(
7025                        &InlayHintsConfig {
7026                            // ... snip
7027                            implicit_drop_hints: true,
7028                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
7029                            param_names_for_lifetime_elision_hints: true,
7030                            hide_named_constructor_hints: false,
7031                            hide_closure_initialization_hints: false,
7032                            closure_style: hir::ClosureStyle::ImplFn,
7033                            max_length: Some(25),
7034                            closing_brace_hints_min_lines: Some(20),
7035                            fields_to_resolve: InlayFieldsToResolve::empty(),
7036                            range_exclusive_hints: true,
7037                        },
7038                        file_id.into(),
7039                        None,
7040                    );
7041                }
7042        // ... snip
7043                                    "#,
7044                                },
7045                                "config.rs": r#"
7046                config_data! {
7047                    /// Configs that only make sense when they are set by a client. As such they can only be defined
7048                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
7049                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
7050                        // ... snip
7051                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
7052                        inlayHints_maxLength: Option<usize>                        = Some(25),
7053                        // ... snip
7054                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
7055                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
7056                        // ... snip
7057                    }
7058                }
7059
7060                impl Config {
7061                    // ... snip
7062                    pub fn inlay_hints(&self) -> InlayHintsConfig {
7063                        InlayHintsConfig {
7064                            // ... snip
7065                            param_names_for_lifetime_elision_hints: self
7066                                .inlayHints_lifetimeElisionHints_useParameterNames()
7067                                .to_owned(),
7068                            max_length: self.inlayHints_maxLength().to_owned(),
7069                            // ... snip
7070                        }
7071                    }
7072                    // ... snip
7073                }
7074                "#
7075                                }
7076                        }
7077                    }
7078            }),
7079        )
7080        .await;
7081    }
7082
7083    fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
7084        outline_panel
7085            .active_editor()
7086            .unwrap()
7087            .read(cx)
7088            .buffer()
7089            .read(cx)
7090            .snapshot(cx)
7091    }
7092
7093    fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
7094        editor.update(cx, |editor, cx| {
7095            let selections = editor.selections.all::<language::Point>(&editor.display_snapshot(cx));
7096            assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
7097            let selection = selections.first().unwrap();
7098            let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
7099            let line_start = language::Point::new(selection.start.row, 0);
7100            let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
7101            multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
7102        })
7103    }
7104
7105    #[gpui::test]
7106    async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) {
7107        init_test(cx);
7108
7109        let fs = FakeFs::new(cx.background_executor.clone());
7110        fs.insert_tree(
7111            "/test",
7112            json!({
7113                "src": {
7114                    "lib.rs": indoc!("
7115                            mod outer {
7116                                pub struct OuterStruct {
7117                                    field: String,
7118                                }
7119                                impl OuterStruct {
7120                                    pub fn new() -> Self {
7121                                        Self { field: String::new() }
7122                                    }
7123                                    pub fn method(&self) {
7124                                        println!(\"{}\", self.field);
7125                                    }
7126                                }
7127                                mod inner {
7128                                    pub fn inner_function() {
7129                                        let x = 42;
7130                                        println!(\"{}\", x);
7131                                    }
7132                                    pub struct InnerStruct {
7133                                        value: i32,
7134                                    }
7135                                }
7136                            }
7137                            fn main() {
7138                                let s = outer::OuterStruct::new();
7139                                s.method();
7140                            }
7141                        "),
7142                }
7143            }),
7144        )
7145        .await;
7146
7147        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7148        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7149        let workspace = add_outline_panel(&project, cx).await;
7150        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7151        let outline_panel = outline_panel(&workspace, cx);
7152
7153        outline_panel.update_in(cx, |outline_panel, window, cx| {
7154            outline_panel.set_active(true, window, cx)
7155        });
7156
7157        workspace
7158            .update(cx, |workspace, window, cx| {
7159                workspace.open_abs_path(
7160                    PathBuf::from("/test/src/lib.rs"),
7161                    OpenOptions {
7162                        visible: Some(OpenVisible::All),
7163                        ..Default::default()
7164                    },
7165                    window,
7166                    cx,
7167                )
7168            })
7169            .unwrap()
7170            .await
7171            .unwrap();
7172
7173        cx.executor()
7174            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7175        cx.run_until_parked();
7176
7177        // Force another update cycle to ensure outlines are fetched
7178        outline_panel.update_in(cx, |panel, window, cx| {
7179            panel.update_non_fs_items(window, cx);
7180            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7181        });
7182        cx.executor()
7183            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7184        cx.run_until_parked();
7185
7186        outline_panel.update(cx, |outline_panel, cx| {
7187            assert_eq!(
7188                display_entries(
7189                    &project,
7190                    &snapshot(outline_panel, cx),
7191                    &outline_panel.cached_entries,
7192                    outline_panel.selected_entry(),
7193                    cx,
7194                ),
7195                indoc!(
7196                    "
7197outline: mod outer  <==== selected
7198  outline: pub struct OuterStruct
7199    outline: field
7200  outline: impl OuterStruct
7201    outline: pub fn new
7202    outline: pub fn method
7203  outline: mod inner
7204    outline: pub fn inner_function
7205    outline: pub struct InnerStruct
7206      outline: value
7207outline: fn main"
7208                )
7209            );
7210        });
7211
7212        let parent_outline = outline_panel
7213            .read_with(cx, |panel, _cx| {
7214                panel
7215                    .cached_entries
7216                    .iter()
7217                    .find_map(|entry| match &entry.entry {
7218                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7219                            if panel
7220                                .outline_children_cache
7221                                .get(&outline.buffer_id)
7222                                .and_then(|children_map| {
7223                                    let key =
7224                                        (outline.outline.range.clone(), outline.outline.depth);
7225                                    children_map.get(&key)
7226                                })
7227                                .copied()
7228                                .unwrap_or(false) =>
7229                        {
7230                            Some(entry.entry.clone())
7231                        }
7232                        _ => None,
7233                    })
7234            })
7235            .expect("Should find an outline with children");
7236
7237        outline_panel.update_in(cx, |panel, window, cx| {
7238            panel.select_entry(parent_outline.clone(), true, window, cx);
7239            panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7240        });
7241        cx.executor()
7242            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7243        cx.run_until_parked();
7244
7245        outline_panel.update(cx, |outline_panel, cx| {
7246            assert_eq!(
7247                display_entries(
7248                    &project,
7249                    &snapshot(outline_panel, cx),
7250                    &outline_panel.cached_entries,
7251                    outline_panel.selected_entry(),
7252                    cx,
7253                ),
7254                indoc!(
7255                    "
7256outline: mod outer  <==== selected
7257outline: fn main"
7258                )
7259            );
7260        });
7261
7262        outline_panel.update_in(cx, |panel, window, cx| {
7263            panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7264        });
7265        cx.executor()
7266            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7267        cx.run_until_parked();
7268
7269        outline_panel.update(cx, |outline_panel, cx| {
7270            assert_eq!(
7271                display_entries(
7272                    &project,
7273                    &snapshot(outline_panel, cx),
7274                    &outline_panel.cached_entries,
7275                    outline_panel.selected_entry(),
7276                    cx,
7277                ),
7278                indoc!(
7279                    "
7280outline: mod outer  <==== selected
7281  outline: pub struct OuterStruct
7282    outline: field
7283  outline: impl OuterStruct
7284    outline: pub fn new
7285    outline: pub fn method
7286  outline: mod inner
7287    outline: pub fn inner_function
7288    outline: pub struct InnerStruct
7289      outline: value
7290outline: fn main"
7291                )
7292            );
7293        });
7294
7295        outline_panel.update_in(cx, |panel, window, cx| {
7296            panel.collapsed_entries.clear();
7297            panel.update_cached_entries(None, window, cx);
7298        });
7299        cx.executor()
7300            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7301        cx.run_until_parked();
7302
7303        outline_panel.update_in(cx, |panel, window, cx| {
7304            let outlines_with_children: Vec<_> = panel
7305                .cached_entries
7306                .iter()
7307                .filter_map(|entry| match &entry.entry {
7308                    PanelEntry::Outline(OutlineEntry::Outline(outline))
7309                        if panel
7310                            .outline_children_cache
7311                            .get(&outline.buffer_id)
7312                            .and_then(|children_map| {
7313                                let key = (outline.outline.range.clone(), outline.outline.depth);
7314                                children_map.get(&key)
7315                            })
7316                            .copied()
7317                            .unwrap_or(false) =>
7318                    {
7319                        Some(entry.entry.clone())
7320                    }
7321                    _ => None,
7322                })
7323                .collect();
7324
7325            for outline in outlines_with_children {
7326                panel.select_entry(outline, false, window, cx);
7327                panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7328            }
7329        });
7330        cx.executor()
7331            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7332        cx.run_until_parked();
7333
7334        outline_panel.update(cx, |outline_panel, cx| {
7335            assert_eq!(
7336                display_entries(
7337                    &project,
7338                    &snapshot(outline_panel, cx),
7339                    &outline_panel.cached_entries,
7340                    outline_panel.selected_entry(),
7341                    cx,
7342                ),
7343                indoc!(
7344                    "
7345outline: mod outer
7346outline: fn main"
7347                )
7348            );
7349        });
7350
7351        let collapsed_entries_count =
7352            outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len());
7353        assert!(
7354            collapsed_entries_count > 0,
7355            "Should have collapsed entries tracked"
7356        );
7357    }
7358
7359    #[gpui::test]
7360    async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) {
7361        init_test(cx);
7362
7363        let fs = FakeFs::new(cx.background_executor.clone());
7364        fs.insert_tree(
7365            "/test",
7366            json!({
7367                "src": {
7368                    "main.rs": indoc!("
7369                            struct Config {
7370                                name: String,
7371                                value: i32,
7372                            }
7373                            impl Config {
7374                                fn new(name: String) -> Self {
7375                                    Self { name, value: 0 }
7376                                }
7377                                fn get_value(&self) -> i32 {
7378                                    self.value
7379                                }
7380                            }
7381                            enum Status {
7382                                Active,
7383                                Inactive,
7384                            }
7385                            fn process_config(config: Config) -> Status {
7386                                if config.get_value() > 0 {
7387                                    Status::Active
7388                                } else {
7389                                    Status::Inactive
7390                                }
7391                            }
7392                            fn main() {
7393                                let config = Config::new(\"test\".to_string());
7394                                let status = process_config(config);
7395                            }
7396                        "),
7397                }
7398            }),
7399        )
7400        .await;
7401
7402        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7403        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7404
7405        let workspace = add_outline_panel(&project, cx).await;
7406        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7407        let outline_panel = outline_panel(&workspace, cx);
7408
7409        outline_panel.update_in(cx, |outline_panel, window, cx| {
7410            outline_panel.set_active(true, window, cx)
7411        });
7412
7413        let _editor = workspace
7414            .update(cx, |workspace, window, cx| {
7415                workspace.open_abs_path(
7416                    PathBuf::from("/test/src/main.rs"),
7417                    OpenOptions {
7418                        visible: Some(OpenVisible::All),
7419                        ..Default::default()
7420                    },
7421                    window,
7422                    cx,
7423                )
7424            })
7425            .unwrap()
7426            .await
7427            .unwrap();
7428
7429        cx.executor()
7430            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7431        cx.run_until_parked();
7432
7433        outline_panel.update(cx, |outline_panel, _cx| {
7434            outline_panel.selected_entry = SelectedEntry::None;
7435        });
7436
7437        // Check initial state - all entries should be expanded by default
7438        outline_panel.update(cx, |outline_panel, cx| {
7439            assert_eq!(
7440                display_entries(
7441                    &project,
7442                    &snapshot(outline_panel, cx),
7443                    &outline_panel.cached_entries,
7444                    outline_panel.selected_entry(),
7445                    cx,
7446                ),
7447                indoc!(
7448                    "
7449outline: struct Config
7450  outline: name
7451  outline: value
7452outline: impl Config
7453  outline: fn new
7454  outline: fn get_value
7455outline: enum Status
7456  outline: Active
7457  outline: Inactive
7458outline: fn process_config
7459outline: fn main"
7460                )
7461            );
7462        });
7463
7464        outline_panel.update(cx, |outline_panel, _cx| {
7465            outline_panel.selected_entry = SelectedEntry::None;
7466        });
7467
7468        cx.update(|window, cx| {
7469            outline_panel.update(cx, |outline_panel, cx| {
7470                outline_panel.select_first(&SelectFirst, window, cx);
7471            });
7472        });
7473
7474        cx.executor()
7475            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7476        cx.run_until_parked();
7477
7478        outline_panel.update(cx, |outline_panel, cx| {
7479            assert_eq!(
7480                display_entries(
7481                    &project,
7482                    &snapshot(outline_panel, cx),
7483                    &outline_panel.cached_entries,
7484                    outline_panel.selected_entry(),
7485                    cx,
7486                ),
7487                indoc!(
7488                    "
7489outline: struct Config  <==== selected
7490  outline: name
7491  outline: value
7492outline: impl Config
7493  outline: fn new
7494  outline: fn get_value
7495outline: enum Status
7496  outline: Active
7497  outline: Inactive
7498outline: fn process_config
7499outline: fn main"
7500                )
7501            );
7502        });
7503
7504        cx.update(|window, cx| {
7505            outline_panel.update(cx, |outline_panel, cx| {
7506                outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7507            });
7508        });
7509
7510        cx.executor()
7511            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7512        cx.run_until_parked();
7513
7514        outline_panel.update(cx, |outline_panel, cx| {
7515            assert_eq!(
7516                display_entries(
7517                    &project,
7518                    &snapshot(outline_panel, cx),
7519                    &outline_panel.cached_entries,
7520                    outline_panel.selected_entry(),
7521                    cx,
7522                ),
7523                indoc!(
7524                    "
7525outline: struct Config  <==== selected
7526outline: impl Config
7527  outline: fn new
7528  outline: fn get_value
7529outline: enum Status
7530  outline: Active
7531  outline: Inactive
7532outline: fn process_config
7533outline: fn main"
7534                )
7535            );
7536        });
7537
7538        cx.update(|window, cx| {
7539            outline_panel.update(cx, |outline_panel, cx| {
7540                outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7541            });
7542        });
7543
7544        cx.executor()
7545            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7546        cx.run_until_parked();
7547
7548        outline_panel.update(cx, |outline_panel, cx| {
7549            assert_eq!(
7550                display_entries(
7551                    &project,
7552                    &snapshot(outline_panel, cx),
7553                    &outline_panel.cached_entries,
7554                    outline_panel.selected_entry(),
7555                    cx,
7556                ),
7557                indoc!(
7558                    "
7559outline: struct Config  <==== selected
7560  outline: name
7561  outline: value
7562outline: impl Config
7563  outline: fn new
7564  outline: fn get_value
7565outline: enum Status
7566  outline: Active
7567  outline: Inactive
7568outline: fn process_config
7569outline: fn main"
7570                )
7571            );
7572        });
7573    }
7574
7575    #[gpui::test]
7576    async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) {
7577        init_test(cx);
7578
7579        let fs = FakeFs::new(cx.background_executor.clone());
7580        fs.insert_tree(
7581            "/test",
7582            json!({
7583                "src": {
7584                    "lib.rs": indoc!("
7585                            mod outer {
7586                                pub struct OuterStruct {
7587                                    field: String,
7588                                }
7589                                impl OuterStruct {
7590                                    pub fn new() -> Self {
7591                                        Self { field: String::new() }
7592                                    }
7593                                    pub fn method(&self) {
7594                                        println!(\"{}\", self.field);
7595                                    }
7596                                }
7597                                mod inner {
7598                                    pub fn inner_function() {
7599                                        let x = 42;
7600                                        println!(\"{}\", x);
7601                                    }
7602                                    pub struct InnerStruct {
7603                                        value: i32,
7604                                    }
7605                                }
7606                            }
7607                            fn main() {
7608                                let s = outer::OuterStruct::new();
7609                                s.method();
7610                            }
7611                        "),
7612                }
7613            }),
7614        )
7615        .await;
7616
7617        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7618        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7619        let workspace = add_outline_panel(&project, cx).await;
7620        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7621        let outline_panel = outline_panel(&workspace, cx);
7622
7623        outline_panel.update_in(cx, |outline_panel, window, cx| {
7624            outline_panel.set_active(true, window, cx)
7625        });
7626
7627        workspace
7628            .update(cx, |workspace, window, cx| {
7629                workspace.open_abs_path(
7630                    PathBuf::from("/test/src/lib.rs"),
7631                    OpenOptions {
7632                        visible: Some(OpenVisible::All),
7633                        ..Default::default()
7634                    },
7635                    window,
7636                    cx,
7637                )
7638            })
7639            .unwrap()
7640            .await
7641            .unwrap();
7642
7643        cx.executor()
7644            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7645        cx.run_until_parked();
7646
7647        // Force another update cycle to ensure outlines are fetched
7648        outline_panel.update_in(cx, |panel, window, cx| {
7649            panel.update_non_fs_items(window, cx);
7650            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7651        });
7652        cx.executor()
7653            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7654        cx.run_until_parked();
7655
7656        outline_panel.update(cx, |outline_panel, cx| {
7657            assert_eq!(
7658                display_entries(
7659                    &project,
7660                    &snapshot(outline_panel, cx),
7661                    &outline_panel.cached_entries,
7662                    outline_panel.selected_entry(),
7663                    cx,
7664                ),
7665                indoc!(
7666                    "
7667outline: mod outer  <==== selected
7668  outline: pub struct OuterStruct
7669    outline: field
7670  outline: impl OuterStruct
7671    outline: pub fn new
7672    outline: pub fn method
7673  outline: mod inner
7674    outline: pub fn inner_function
7675    outline: pub struct InnerStruct
7676      outline: value
7677outline: fn main"
7678                )
7679            );
7680        });
7681
7682        let _parent_outline = outline_panel
7683            .read_with(cx, |panel, _cx| {
7684                panel
7685                    .cached_entries
7686                    .iter()
7687                    .find_map(|entry| match &entry.entry {
7688                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7689                            if panel
7690                                .outline_children_cache
7691                                .get(&outline.buffer_id)
7692                                .and_then(|children_map| {
7693                                    let key =
7694                                        (outline.outline.range.clone(), outline.outline.depth);
7695                                    children_map.get(&key)
7696                                })
7697                                .copied()
7698                                .unwrap_or(false) =>
7699                        {
7700                            Some(entry.entry.clone())
7701                        }
7702                        _ => None,
7703                    })
7704            })
7705            .expect("Should find an outline with children");
7706
7707        // Collapse all entries
7708        outline_panel.update_in(cx, |panel, window, cx| {
7709            panel.collapse_all_entries(&CollapseAllEntries, window, cx);
7710        });
7711        cx.executor()
7712            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7713        cx.run_until_parked();
7714
7715        let expected_collapsed_output = indoc!(
7716            "
7717        outline: mod outer  <==== selected
7718        outline: fn main"
7719        );
7720
7721        outline_panel.update(cx, |panel, cx| {
7722            assert_eq! {
7723                display_entries(
7724                    &project,
7725                    &snapshot(panel, cx),
7726                    &panel.cached_entries,
7727                    panel.selected_entry(),
7728                    cx,
7729                ),
7730                expected_collapsed_output
7731            };
7732        });
7733
7734        // Expand all entries
7735        outline_panel.update_in(cx, |panel, window, cx| {
7736            panel.expand_all_entries(&ExpandAllEntries, window, cx);
7737        });
7738        cx.executor()
7739            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7740        cx.run_until_parked();
7741
7742        let expected_expanded_output = indoc!(
7743            "
7744        outline: mod outer  <==== selected
7745          outline: pub struct OuterStruct
7746            outline: field
7747          outline: impl OuterStruct
7748            outline: pub fn new
7749            outline: pub fn method
7750          outline: mod inner
7751            outline: pub fn inner_function
7752            outline: pub struct InnerStruct
7753              outline: value
7754        outline: fn main"
7755        );
7756
7757        outline_panel.update(cx, |panel, cx| {
7758            assert_eq! {
7759                display_entries(
7760                    &project,
7761                    &snapshot(panel, cx),
7762                    &panel.cached_entries,
7763                    panel.selected_entry(),
7764                    cx,
7765                ),
7766                expected_expanded_output
7767            };
7768        });
7769    }
7770
7771    #[gpui::test]
7772    async fn test_buffer_search(cx: &mut TestAppContext) {
7773        init_test(cx);
7774
7775        let fs = FakeFs::new(cx.background_executor.clone());
7776        fs.insert_tree(
7777            "/test",
7778            json!({
7779                "foo.txt": r#"<_constitution>
7780
7781</_constitution>
7782
7783
7784
7785## 📊 Output
7786
7787| Field          | Meaning                |
7788"#
7789            }),
7790        )
7791        .await;
7792
7793        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7794        let workspace = add_outline_panel(&project, cx).await;
7795        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7796
7797        let editor = workspace
7798            .update(cx, |workspace, window, cx| {
7799                workspace.open_abs_path(
7800                    PathBuf::from("/test/foo.txt"),
7801                    OpenOptions {
7802                        visible: Some(OpenVisible::All),
7803                        ..OpenOptions::default()
7804                    },
7805                    window,
7806                    cx,
7807                )
7808            })
7809            .unwrap()
7810            .await
7811            .unwrap()
7812            .downcast::<Editor>()
7813            .unwrap();
7814
7815        let search_bar = workspace
7816            .update(cx, |_, window, cx| {
7817                cx.new(|cx| {
7818                    let mut search_bar = BufferSearchBar::new(None, window, cx);
7819                    search_bar.set_active_pane_item(Some(&editor), window, cx);
7820                    search_bar.show(window, cx);
7821                    search_bar
7822                })
7823            })
7824            .unwrap();
7825
7826        let outline_panel = outline_panel(&workspace, cx);
7827
7828        outline_panel.update_in(cx, |outline_panel, window, cx| {
7829            outline_panel.set_active(true, window, cx)
7830        });
7831
7832        search_bar
7833            .update_in(cx, |search_bar, window, cx| {
7834                search_bar.search("  ", None, true, window, cx)
7835            })
7836            .await
7837            .unwrap();
7838
7839        cx.executor()
7840            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7841        cx.run_until_parked();
7842
7843        outline_panel.update(cx, |outline_panel, cx| {
7844            assert_eq!(
7845                display_entries(
7846                    &project,
7847                    &snapshot(outline_panel, cx),
7848                    &outline_panel.cached_entries,
7849                    outline_panel.selected_entry(),
7850                    cx,
7851                ),
7852                "search: | Field«  »        | Meaning                |  <==== selected
7853search: | Field  «  »      | Meaning                |
7854search: | Field    «  »    | Meaning                |
7855search: | Field      «  »  | Meaning                |
7856search: | Field        «  »| Meaning                |
7857search: | Field          | Meaning«  »              |
7858search: | Field          | Meaning  «  »            |
7859search: | Field          | Meaning    «  »          |
7860search: | Field          | Meaning      «  »        |
7861search: | Field          | Meaning        «  »      |
7862search: | Field          | Meaning          «  »    |
7863search: | Field          | Meaning            «  »  |
7864search: | Field          | Meaning              «  »|"
7865            );
7866        });
7867    }
7868}