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