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