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