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