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