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, IconName, IconSize, IndentGuideColors, IndentGuideLayout,
  51    Label, LabelCommon, ListItem, ScrollAxes, Scrollbars, StyledExt, StyledTypography, Toggleable,
  52    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());
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                        .on_click(cx.listener(
4737                            |outline_panel, _, window, cx| {
4738                                outline_panel.toggle_active_editor_pin(
4739                                    &ToggleActiveEditorPin,
4740                                    window,
4741                                    cx,
4742                                );
4743                            },
4744                        )),
4745                    ),
4746                ),
4747        )
4748    }
4749
4750    fn buffers_inside_directory(
4751        &self,
4752        dir_worktree: WorktreeId,
4753        dir_entry: &GitEntry,
4754    ) -> HashSet<BufferId> {
4755        if !dir_entry.is_dir() {
4756            debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4757            return HashSet::default();
4758        }
4759
4760        self.fs_entries
4761            .iter()
4762            .skip_while(|fs_entry| match fs_entry {
4763                FsEntry::Directory(directory) => {
4764                    directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4765                }
4766                _ => true,
4767            })
4768            .skip(1)
4769            .take_while(|fs_entry| match fs_entry {
4770                FsEntry::ExternalFile(..) => false,
4771                FsEntry::Directory(directory) => {
4772                    directory.worktree_id == dir_worktree
4773                        && directory.entry.path.starts_with(&dir_entry.path)
4774                }
4775                FsEntry::File(file) => {
4776                    file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4777                }
4778            })
4779            .filter_map(|fs_entry| match fs_entry {
4780                FsEntry::File(file) => Some(file.buffer_id),
4781                _ => None,
4782            })
4783            .collect()
4784    }
4785}
4786
4787fn workspace_active_editor(
4788    workspace: &Workspace,
4789    cx: &App,
4790) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4791    let active_item = workspace.active_item(cx)?;
4792    let active_editor = active_item
4793        .act_as::<Editor>(cx)
4794        .filter(|editor| editor.read(cx).mode().is_full())?;
4795    Some((active_item, active_editor))
4796}
4797
4798fn back_to_common_visited_parent(
4799    visited_dirs: &mut Vec<(ProjectEntryId, Arc<RelPath>)>,
4800    worktree_id: &WorktreeId,
4801    new_entry: &Entry,
4802) -> Option<(WorktreeId, ProjectEntryId)> {
4803    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4804        match new_entry.path.parent() {
4805            Some(parent_path) => {
4806                if parent_path == visited_path.as_ref() {
4807                    return Some((*worktree_id, *visited_dir_id));
4808                }
4809            }
4810            None => {
4811                break;
4812            }
4813        }
4814        visited_dirs.pop();
4815    }
4816    None
4817}
4818
4819fn file_name(path: &Path) -> String {
4820    let mut current_path = path;
4821    loop {
4822        if let Some(file_name) = current_path.file_name() {
4823            return file_name.to_string_lossy().into_owned();
4824        }
4825        match current_path.parent() {
4826            Some(parent) => current_path = parent,
4827            None => return path.to_string_lossy().into_owned(),
4828        }
4829    }
4830}
4831
4832impl Panel for OutlinePanel {
4833    fn persistent_name() -> &'static str {
4834        "Outline Panel"
4835    }
4836
4837    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4838        match OutlinePanelSettings::get_global(cx).dock {
4839            DockSide::Left => DockPosition::Left,
4840            DockSide::Right => DockPosition::Right,
4841        }
4842    }
4843
4844    fn position_is_valid(&self, position: DockPosition) -> bool {
4845        matches!(position, DockPosition::Left | DockPosition::Right)
4846    }
4847
4848    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4849        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
4850            let dock = match position {
4851                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
4852                DockPosition::Right => DockSide::Right,
4853            };
4854            settings.outline_panel.get_or_insert_default().dock = Some(dock);
4855        });
4856    }
4857
4858    fn size(&self, _: &Window, cx: &App) -> Pixels {
4859        self.width
4860            .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
4861    }
4862
4863    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
4864        self.width = size;
4865        cx.notify();
4866        cx.defer_in(window, |this, _, cx| {
4867            this.serialize(cx);
4868        });
4869    }
4870
4871    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4872        OutlinePanelSettings::get_global(cx)
4873            .button
4874            .then_some(IconName::ListTree)
4875    }
4876
4877    fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4878        Some("Outline Panel")
4879    }
4880
4881    fn toggle_action(&self) -> Box<dyn Action> {
4882        Box::new(ToggleFocus)
4883    }
4884
4885    fn starts_open(&self, _window: &Window, _: &App) -> bool {
4886        self.active
4887    }
4888
4889    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
4890        cx.spawn_in(window, async move |outline_panel, cx| {
4891            outline_panel
4892                .update_in(cx, |outline_panel, window, cx| {
4893                    let old_active = outline_panel.active;
4894                    outline_panel.active = active;
4895                    if old_active != active {
4896                        if active
4897                            && let Some((active_item, active_editor)) =
4898                                outline_panel.workspace.upgrade().and_then(|workspace| {
4899                                    workspace_active_editor(workspace.read(cx), cx)
4900                                })
4901                        {
4902                            if outline_panel.should_replace_active_item(active_item.as_ref()) {
4903                                outline_panel.replace_active_editor(
4904                                    active_item,
4905                                    active_editor,
4906                                    window,
4907                                    cx,
4908                                );
4909                            } else {
4910                                outline_panel.update_fs_entries(active_editor, None, window, cx)
4911                            }
4912                            return;
4913                        }
4914
4915                        if !outline_panel.pinned {
4916                            outline_panel.clear_previous(window, cx);
4917                        }
4918                    }
4919                    outline_panel.serialize(cx);
4920                })
4921                .ok();
4922        })
4923        .detach()
4924    }
4925
4926    fn activation_priority(&self) -> u32 {
4927        5
4928    }
4929}
4930
4931impl Focusable for OutlinePanel {
4932    fn focus_handle(&self, cx: &App) -> FocusHandle {
4933        self.filter_editor.focus_handle(cx)
4934    }
4935}
4936
4937impl EventEmitter<Event> for OutlinePanel {}
4938
4939impl EventEmitter<PanelEvent> for OutlinePanel {}
4940
4941impl Render for OutlinePanel {
4942    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4943        let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
4944            (project.is_local(), project.is_via_remote_server())
4945        });
4946        let query = self.query(cx);
4947        let pinned = self.pinned;
4948        let settings = OutlinePanelSettings::get_global(cx);
4949        let indent_size = settings.indent_size;
4950        let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
4951
4952        let search_query = match &self.mode {
4953            ItemsDisplayMode::Search(search_query) => Some(search_query),
4954            _ => None,
4955        };
4956
4957        v_flex()
4958            .id("outline-panel")
4959            .size_full()
4960            .overflow_hidden()
4961            .relative()
4962            .key_context(self.dispatch_context(window, cx))
4963            .on_action(cx.listener(Self::open_selected_entry))
4964            .on_action(cx.listener(Self::cancel))
4965            .on_action(cx.listener(Self::select_next))
4966            .on_action(cx.listener(Self::select_previous))
4967            .on_action(cx.listener(Self::select_first))
4968            .on_action(cx.listener(Self::select_last))
4969            .on_action(cx.listener(Self::select_parent))
4970            .on_action(cx.listener(Self::expand_selected_entry))
4971            .on_action(cx.listener(Self::collapse_selected_entry))
4972            .on_action(cx.listener(Self::expand_all_entries))
4973            .on_action(cx.listener(Self::collapse_all_entries))
4974            .on_action(cx.listener(Self::copy_path))
4975            .on_action(cx.listener(Self::copy_relative_path))
4976            .on_action(cx.listener(Self::toggle_active_editor_pin))
4977            .on_action(cx.listener(Self::unfold_directory))
4978            .on_action(cx.listener(Self::fold_directory))
4979            .on_action(cx.listener(Self::open_excerpts))
4980            .on_action(cx.listener(Self::open_excerpts_split))
4981            .when(is_local, |el| {
4982                el.on_action(cx.listener(Self::reveal_in_finder))
4983            })
4984            .when(is_local || is_via_ssh, |el| {
4985                el.on_action(cx.listener(Self::open_in_terminal))
4986            })
4987            .on_mouse_down(
4988                MouseButton::Right,
4989                cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
4990                    if let Some(entry) = outline_panel.selected_entry().cloned() {
4991                        outline_panel.deploy_context_menu(event.position, entry, window, cx)
4992                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
4993                        outline_panel.deploy_context_menu(
4994                            event.position,
4995                            PanelEntry::Fs(entry),
4996                            window,
4997                            cx,
4998                        )
4999                    }
5000                }),
5001            )
5002            .track_focus(&self.focus_handle)
5003            .when_some(search_query, |outline_panel, search_state| {
5004                outline_panel.child(
5005                    h_flex()
5006                        .py_1p5()
5007                        .px_2()
5008                        .h(DynamicSpacing::Base32.px(cx))
5009                        .flex_shrink_0()
5010                        .border_b_1()
5011                        .border_color(cx.theme().colors().border)
5012                        .gap_0p5()
5013                        .child(Label::new("Searching:").color(Color::Muted))
5014                        .child(Label::new(search_state.query.to_string())),
5015                )
5016            })
5017            .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
5018            .child(self.render_filter_footer(pinned, cx))
5019    }
5020}
5021
5022fn find_active_indent_guide_ix(
5023    outline_panel: &OutlinePanel,
5024    candidates: &[IndentGuideLayout],
5025) -> Option<usize> {
5026    let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
5027        return None;
5028    };
5029    let target_depth = outline_panel
5030        .cached_entries
5031        .get(*target_ix)
5032        .map(|cached_entry| cached_entry.depth)?;
5033
5034    let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
5035        .cached_entries
5036        .get(target_ix + 1)
5037        .filter(|cached_entry| cached_entry.depth > target_depth)
5038        .map(|entry| entry.depth)
5039    {
5040        (target_ix + 1, target_depth.saturating_sub(1))
5041    } else {
5042        (*target_ix, target_depth.saturating_sub(1))
5043    };
5044
5045    candidates
5046        .iter()
5047        .enumerate()
5048        .find(|(_, guide)| {
5049            guide.offset.y <= target_ix
5050                && target_ix < guide.offset.y + guide.length
5051                && guide.offset.x == target_depth
5052        })
5053        .map(|(ix, _)| ix)
5054}
5055
5056fn subscribe_for_editor_events(
5057    editor: &Entity<Editor>,
5058    window: &mut Window,
5059    cx: &mut Context<OutlinePanel>,
5060) -> Subscription {
5061    let debounce = Some(UPDATE_DEBOUNCE);
5062    cx.subscribe_in(
5063        editor,
5064        window,
5065        move |outline_panel, editor, e: &EditorEvent, window, cx| {
5066            if !outline_panel.active {
5067                return;
5068            }
5069            match e {
5070                EditorEvent::SelectionsChanged { local: true } => {
5071                    outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5072                    cx.notify();
5073                }
5074                EditorEvent::ExcerptsAdded { excerpts, .. } => {
5075                    outline_panel
5076                        .new_entries_for_fs_update
5077                        .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5078                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5079                }
5080                EditorEvent::ExcerptsRemoved { ids, .. } => {
5081                    let mut ids = ids.iter().collect::<HashSet<_>>();
5082                    for excerpts in outline_panel.excerpts.values_mut() {
5083                        excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5084                        if ids.is_empty() {
5085                            break;
5086                        }
5087                    }
5088                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5089                }
5090                EditorEvent::ExcerptsExpanded { ids } => {
5091                    outline_panel.invalidate_outlines(ids);
5092                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5093                    if update_cached_items {
5094                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5095                    }
5096                }
5097                EditorEvent::ExcerptsEdited { ids } => {
5098                    outline_panel.invalidate_outlines(ids);
5099                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5100                    if update_cached_items {
5101                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5102                    }
5103                }
5104                EditorEvent::BufferFoldToggled { ids, .. } => {
5105                    outline_panel.invalidate_outlines(ids);
5106                    let mut latest_unfolded_buffer_id = None;
5107                    let mut latest_folded_buffer_id = None;
5108                    let mut ignore_selections_change = false;
5109                    outline_panel.new_entries_for_fs_update.extend(
5110                        ids.iter()
5111                            .filter(|id| {
5112                                outline_panel
5113                                    .excerpts
5114                                    .iter()
5115                                    .find_map(|(buffer_id, excerpts)| {
5116                                        if excerpts.contains_key(id) {
5117                                            ignore_selections_change |= outline_panel
5118                                                .preserve_selection_on_buffer_fold_toggles
5119                                                .remove(buffer_id);
5120                                            Some(buffer_id)
5121                                        } else {
5122                                            None
5123                                        }
5124                                    })
5125                                    .map(|buffer_id| {
5126                                        if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5127                                            latest_folded_buffer_id = Some(*buffer_id);
5128                                            false
5129                                        } else {
5130                                            latest_unfolded_buffer_id = Some(*buffer_id);
5131                                            true
5132                                        }
5133                                    })
5134                                    .unwrap_or(true)
5135                            })
5136                            .copied(),
5137                    );
5138                    if !ignore_selections_change
5139                        && let Some(entry_to_select) = latest_unfolded_buffer_id
5140                            .or(latest_folded_buffer_id)
5141                            .and_then(|toggled_buffer_id| {
5142                                outline_panel.fs_entries.iter().find_map(
5143                                    |fs_entry| match fs_entry {
5144                                        FsEntry::ExternalFile(external) => {
5145                                            if external.buffer_id == toggled_buffer_id {
5146                                                Some(fs_entry.clone())
5147                                            } else {
5148                                                None
5149                                            }
5150                                        }
5151                                        FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5152                                            if *buffer_id == toggled_buffer_id {
5153                                                Some(fs_entry.clone())
5154                                            } else {
5155                                                None
5156                                            }
5157                                        }
5158                                        FsEntry::Directory(..) => None,
5159                                    },
5160                                )
5161                            })
5162                            .map(PanelEntry::Fs)
5163                    {
5164                        outline_panel.select_entry(entry_to_select, true, window, cx);
5165                    }
5166
5167                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5168                }
5169                EditorEvent::Reparsed(buffer_id) => {
5170                    if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5171                        for excerpt in excerpts.values_mut() {
5172                            excerpt.invalidate_outlines();
5173                        }
5174                    }
5175                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5176                    if update_cached_items {
5177                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5178                    }
5179                }
5180                _ => {}
5181            }
5182        },
5183    )
5184}
5185
5186fn empty_icon() -> AnyElement {
5187    h_flex()
5188        .size(IconSize::default().rems())
5189        .invisible()
5190        .flex_none()
5191        .into_any_element()
5192}
5193
5194fn horizontal_separator(cx: &mut App) -> Div {
5195    div().mx_2().border_primary(cx).border_t_1()
5196}
5197
5198#[derive(Debug, Default)]
5199struct GenerationState {
5200    entries: Vec<CachedEntry>,
5201    match_candidates: Vec<StringMatchCandidate>,
5202    max_width_estimate_and_index: Option<(u64, usize)>,
5203}
5204
5205impl GenerationState {
5206    fn clear(&mut self) {
5207        self.entries.clear();
5208        self.match_candidates.clear();
5209        self.max_width_estimate_and_index = None;
5210    }
5211}
5212
5213#[cfg(test)]
5214mod tests {
5215    use db::indoc;
5216    use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5217    use language::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
5218    use pretty_assertions::assert_eq;
5219    use project::FakeFs;
5220    use search::project_search::{self, perform_project_search};
5221    use serde_json::json;
5222    use util::path;
5223    use workspace::{OpenOptions, OpenVisible};
5224
5225    use super::*;
5226
5227    const SELECTED_MARKER: &str = "  <==== selected";
5228
5229    #[gpui::test(iterations = 10)]
5230    async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5231        init_test(cx);
5232
5233        let fs = FakeFs::new(cx.background_executor.clone());
5234        let root = path!("/rust-analyzer");
5235        populate_with_test_ra_project(&fs, root).await;
5236        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5237        project.read_with(cx, |project, _| {
5238            project.languages().add(Arc::new(rust_lang()))
5239        });
5240        let workspace = add_outline_panel(&project, cx).await;
5241        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5242        let outline_panel = outline_panel(&workspace, cx);
5243        outline_panel.update_in(cx, |outline_panel, window, cx| {
5244            outline_panel.set_active(true, window, cx)
5245        });
5246
5247        workspace
5248            .update(cx, |workspace, window, cx| {
5249                ProjectSearchView::deploy_search(
5250                    workspace,
5251                    &workspace::DeploySearch::default(),
5252                    window,
5253                    cx,
5254                )
5255            })
5256            .unwrap();
5257        let search_view = workspace
5258            .update(cx, |workspace, _, cx| {
5259                workspace
5260                    .active_pane()
5261                    .read(cx)
5262                    .items()
5263                    .find_map(|item| item.downcast::<ProjectSearchView>())
5264                    .expect("Project search view expected to appear after new search event trigger")
5265            })
5266            .unwrap();
5267
5268        let query = "param_names_for_lifetime_elision_hints";
5269        perform_project_search(&search_view, query, cx);
5270        search_view.update(cx, |search_view, cx| {
5271            search_view
5272                .results_editor()
5273                .update(cx, |results_editor, cx| {
5274                    assert_eq!(
5275                        results_editor.display_text(cx).match_indices(query).count(),
5276                        9
5277                    );
5278                });
5279        });
5280
5281        let all_matches = r#"rust-analyzer/
5282  crates/
5283    ide/src/
5284      inlay_hints/
5285        fn_lifetime_fn.rs
5286          search: match config.param_names_for_lifetime_elision_hints {
5287          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5288          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5289          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5290      inlay_hints.rs
5291        search: pub param_names_for_lifetime_elision_hints: bool,
5292        search: param_names_for_lifetime_elision_hints: self
5293      static_index.rs
5294        search: param_names_for_lifetime_elision_hints: false,
5295    rust-analyzer/src/
5296      cli/
5297        analysis_stats.rs
5298          search: param_names_for_lifetime_elision_hints: true,
5299      config.rs
5300        search: param_names_for_lifetime_elision_hints: self"#
5301            .to_string();
5302
5303        let select_first_in_all_matches = |line_to_select: &str| {
5304            assert!(all_matches.contains(line_to_select));
5305            all_matches.replacen(
5306                line_to_select,
5307                &format!("{line_to_select}{SELECTED_MARKER}"),
5308                1,
5309            )
5310        };
5311
5312        cx.executor()
5313            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5314        cx.run_until_parked();
5315        outline_panel.update(cx, |outline_panel, cx| {
5316            assert_eq!(
5317                display_entries(
5318                    &project,
5319                    &snapshot(outline_panel, cx),
5320                    &outline_panel.cached_entries,
5321                    outline_panel.selected_entry(),
5322                    cx,
5323                ),
5324                select_first_in_all_matches(
5325                    "search: match config.param_names_for_lifetime_elision_hints {"
5326                )
5327            );
5328        });
5329
5330        outline_panel.update_in(cx, |outline_panel, window, cx| {
5331            outline_panel.select_parent(&SelectParent, window, cx);
5332            assert_eq!(
5333                display_entries(
5334                    &project,
5335                    &snapshot(outline_panel, cx),
5336                    &outline_panel.cached_entries,
5337                    outline_panel.selected_entry(),
5338                    cx,
5339                ),
5340                select_first_in_all_matches("fn_lifetime_fn.rs")
5341            );
5342        });
5343        outline_panel.update_in(cx, |outline_panel, window, cx| {
5344            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5345        });
5346        cx.executor()
5347            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5348        cx.run_until_parked();
5349        outline_panel.update(cx, |outline_panel, cx| {
5350            assert_eq!(
5351                display_entries(
5352                    &project,
5353                    &snapshot(outline_panel, cx),
5354                    &outline_panel.cached_entries,
5355                    outline_panel.selected_entry(),
5356                    cx,
5357                ),
5358                format!(
5359                    r#"rust-analyzer/
5360  crates/
5361    ide/src/
5362      inlay_hints/
5363        fn_lifetime_fn.rs{SELECTED_MARKER}
5364      inlay_hints.rs
5365        search: pub param_names_for_lifetime_elision_hints: bool,
5366        search: param_names_for_lifetime_elision_hints: self
5367      static_index.rs
5368        search: param_names_for_lifetime_elision_hints: false,
5369    rust-analyzer/src/
5370      cli/
5371        analysis_stats.rs
5372          search: param_names_for_lifetime_elision_hints: true,
5373      config.rs
5374        search: param_names_for_lifetime_elision_hints: self"#,
5375                )
5376            );
5377        });
5378
5379        outline_panel.update_in(cx, |outline_panel, window, cx| {
5380            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5381        });
5382        cx.executor()
5383            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5384        cx.run_until_parked();
5385        outline_panel.update_in(cx, |outline_panel, window, cx| {
5386            outline_panel.select_parent(&SelectParent, window, cx);
5387            assert_eq!(
5388                display_entries(
5389                    &project,
5390                    &snapshot(outline_panel, cx),
5391                    &outline_panel.cached_entries,
5392                    outline_panel.selected_entry(),
5393                    cx,
5394                ),
5395                select_first_in_all_matches("inlay_hints/")
5396            );
5397        });
5398
5399        outline_panel.update_in(cx, |outline_panel, window, cx| {
5400            outline_panel.select_parent(&SelectParent, window, cx);
5401            assert_eq!(
5402                display_entries(
5403                    &project,
5404                    &snapshot(outline_panel, cx),
5405                    &outline_panel.cached_entries,
5406                    outline_panel.selected_entry(),
5407                    cx,
5408                ),
5409                select_first_in_all_matches("ide/src/")
5410            );
5411        });
5412
5413        outline_panel.update_in(cx, |outline_panel, window, cx| {
5414            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5415        });
5416        cx.executor()
5417            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5418        cx.run_until_parked();
5419        outline_panel.update(cx, |outline_panel, cx| {
5420            assert_eq!(
5421                display_entries(
5422                    &project,
5423                    &snapshot(outline_panel, cx),
5424                    &outline_panel.cached_entries,
5425                    outline_panel.selected_entry(),
5426                    cx,
5427                ),
5428                format!(
5429                    r#"rust-analyzer/
5430  crates/
5431    ide/src/{SELECTED_MARKER}
5432    rust-analyzer/src/
5433      cli/
5434        analysis_stats.rs
5435          search: param_names_for_lifetime_elision_hints: true,
5436      config.rs
5437        search: param_names_for_lifetime_elision_hints: self"#,
5438                )
5439            );
5440        });
5441        outline_panel.update_in(cx, |outline_panel, window, cx| {
5442            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5443        });
5444        cx.executor()
5445            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5446        cx.run_until_parked();
5447        outline_panel.update(cx, |outline_panel, cx| {
5448            assert_eq!(
5449                display_entries(
5450                    &project,
5451                    &snapshot(outline_panel, cx),
5452                    &outline_panel.cached_entries,
5453                    outline_panel.selected_entry(),
5454                    cx,
5455                ),
5456                select_first_in_all_matches("ide/src/")
5457            );
5458        });
5459    }
5460
5461    #[gpui::test(iterations = 10)]
5462    async fn test_item_filtering(cx: &mut TestAppContext) {
5463        init_test(cx);
5464
5465        let fs = FakeFs::new(cx.background_executor.clone());
5466        let root = path!("/rust-analyzer");
5467        populate_with_test_ra_project(&fs, root).await;
5468        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5469        project.read_with(cx, |project, _| {
5470            project.languages().add(Arc::new(rust_lang()))
5471        });
5472        let workspace = add_outline_panel(&project, cx).await;
5473        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5474        let outline_panel = outline_panel(&workspace, cx);
5475        outline_panel.update_in(cx, |outline_panel, window, cx| {
5476            outline_panel.set_active(true, window, cx)
5477        });
5478
5479        workspace
5480            .update(cx, |workspace, window, cx| {
5481                ProjectSearchView::deploy_search(
5482                    workspace,
5483                    &workspace::DeploySearch::default(),
5484                    window,
5485                    cx,
5486                )
5487            })
5488            .unwrap();
5489        let search_view = workspace
5490            .update(cx, |workspace, _, cx| {
5491                workspace
5492                    .active_pane()
5493                    .read(cx)
5494                    .items()
5495                    .find_map(|item| item.downcast::<ProjectSearchView>())
5496                    .expect("Project search view expected to appear after new search event trigger")
5497            })
5498            .unwrap();
5499
5500        let query = "param_names_for_lifetime_elision_hints";
5501        perform_project_search(&search_view, query, cx);
5502        search_view.update(cx, |search_view, cx| {
5503            search_view
5504                .results_editor()
5505                .update(cx, |results_editor, cx| {
5506                    assert_eq!(
5507                        results_editor.display_text(cx).match_indices(query).count(),
5508                        9
5509                    );
5510                });
5511        });
5512        let all_matches = r#"rust-analyzer/
5513  crates/
5514    ide/src/
5515      inlay_hints/
5516        fn_lifetime_fn.rs
5517          search: match config.param_names_for_lifetime_elision_hints {
5518          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5519          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5520          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5521      inlay_hints.rs
5522        search: pub param_names_for_lifetime_elision_hints: bool,
5523        search: param_names_for_lifetime_elision_hints: self
5524      static_index.rs
5525        search: param_names_for_lifetime_elision_hints: false,
5526    rust-analyzer/src/
5527      cli/
5528        analysis_stats.rs
5529          search: param_names_for_lifetime_elision_hints: true,
5530      config.rs
5531        search: param_names_for_lifetime_elision_hints: self"#
5532            .to_string();
5533
5534        cx.executor()
5535            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5536        cx.run_until_parked();
5537        outline_panel.update(cx, |outline_panel, cx| {
5538            assert_eq!(
5539                display_entries(
5540                    &project,
5541                    &snapshot(outline_panel, cx),
5542                    &outline_panel.cached_entries,
5543                    None,
5544                    cx,
5545                ),
5546                all_matches,
5547            );
5548        });
5549
5550        let filter_text = "a";
5551        outline_panel.update_in(cx, |outline_panel, window, cx| {
5552            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5553                filter_editor.set_text(filter_text, window, cx);
5554            });
5555        });
5556        cx.executor()
5557            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5558        cx.run_until_parked();
5559
5560        outline_panel.update(cx, |outline_panel, cx| {
5561            assert_eq!(
5562                display_entries(
5563                    &project,
5564                    &snapshot(outline_panel, cx),
5565                    &outline_panel.cached_entries,
5566                    None,
5567                    cx,
5568                ),
5569                all_matches
5570                    .lines()
5571                    .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5572                    .filter(|item| item.contains(filter_text))
5573                    .collect::<Vec<_>>()
5574                    .join("\n"),
5575            );
5576        });
5577
5578        outline_panel.update_in(cx, |outline_panel, window, cx| {
5579            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5580                filter_editor.set_text("", window, cx);
5581            });
5582        });
5583        cx.executor()
5584            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5585        cx.run_until_parked();
5586        outline_panel.update(cx, |outline_panel, cx| {
5587            assert_eq!(
5588                display_entries(
5589                    &project,
5590                    &snapshot(outline_panel, cx),
5591                    &outline_panel.cached_entries,
5592                    None,
5593                    cx,
5594                ),
5595                all_matches,
5596            );
5597        });
5598    }
5599
5600    #[gpui::test(iterations = 10)]
5601    async fn test_item_opening(cx: &mut TestAppContext) {
5602        init_test(cx);
5603
5604        let fs = FakeFs::new(cx.background_executor.clone());
5605        let root = path!("/rust-analyzer");
5606        populate_with_test_ra_project(&fs, root).await;
5607        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5608        project.read_with(cx, |project, _| {
5609            project.languages().add(Arc::new(rust_lang()))
5610        });
5611        let workspace = add_outline_panel(&project, cx).await;
5612        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5613        let outline_panel = outline_panel(&workspace, cx);
5614        outline_panel.update_in(cx, |outline_panel, window, cx| {
5615            outline_panel.set_active(true, window, cx)
5616        });
5617
5618        workspace
5619            .update(cx, |workspace, window, cx| {
5620                ProjectSearchView::deploy_search(
5621                    workspace,
5622                    &workspace::DeploySearch::default(),
5623                    window,
5624                    cx,
5625                )
5626            })
5627            .unwrap();
5628        let search_view = workspace
5629            .update(cx, |workspace, _, cx| {
5630                workspace
5631                    .active_pane()
5632                    .read(cx)
5633                    .items()
5634                    .find_map(|item| item.downcast::<ProjectSearchView>())
5635                    .expect("Project search view expected to appear after new search event trigger")
5636            })
5637            .unwrap();
5638
5639        let query = "param_names_for_lifetime_elision_hints";
5640        perform_project_search(&search_view, query, cx);
5641        search_view.update(cx, |search_view, cx| {
5642            search_view
5643                .results_editor()
5644                .update(cx, |results_editor, cx| {
5645                    assert_eq!(
5646                        results_editor.display_text(cx).match_indices(query).count(),
5647                        9
5648                    );
5649                });
5650        });
5651        let all_matches = r#"rust-analyzer/
5652  crates/
5653    ide/src/
5654      inlay_hints/
5655        fn_lifetime_fn.rs
5656          search: match config.param_names_for_lifetime_elision_hints {
5657          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5658          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5659          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5660      inlay_hints.rs
5661        search: pub param_names_for_lifetime_elision_hints: bool,
5662        search: param_names_for_lifetime_elision_hints: self
5663      static_index.rs
5664        search: param_names_for_lifetime_elision_hints: false,
5665    rust-analyzer/src/
5666      cli/
5667        analysis_stats.rs
5668          search: param_names_for_lifetime_elision_hints: true,
5669      config.rs
5670        search: param_names_for_lifetime_elision_hints: self"#
5671            .to_string();
5672        let select_first_in_all_matches = |line_to_select: &str| {
5673            assert!(all_matches.contains(line_to_select));
5674            all_matches.replacen(
5675                line_to_select,
5676                &format!("{line_to_select}{SELECTED_MARKER}"),
5677                1,
5678            )
5679        };
5680        cx.executor()
5681            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5682        cx.run_until_parked();
5683
5684        let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5685            outline_panel
5686                .active_editor()
5687                .expect("should have an active editor open")
5688        });
5689        let initial_outline_selection =
5690            "search: match config.param_names_for_lifetime_elision_hints {";
5691        outline_panel.update_in(cx, |outline_panel, window, cx| {
5692            assert_eq!(
5693                display_entries(
5694                    &project,
5695                    &snapshot(outline_panel, cx),
5696                    &outline_panel.cached_entries,
5697                    outline_panel.selected_entry(),
5698                    cx,
5699                ),
5700                select_first_in_all_matches(initial_outline_selection)
5701            );
5702            assert_eq!(
5703                selected_row_text(&active_editor, cx),
5704                initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5705                "Should place the initial editor selection on the corresponding search result"
5706            );
5707
5708            outline_panel.select_next(&SelectNext, window, cx);
5709            outline_panel.select_next(&SelectNext, window, cx);
5710        });
5711
5712        let navigated_outline_selection =
5713            "search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
5714        outline_panel.update(cx, |outline_panel, cx| {
5715            assert_eq!(
5716                display_entries(
5717                    &project,
5718                    &snapshot(outline_panel, cx),
5719                    &outline_panel.cached_entries,
5720                    outline_panel.selected_entry(),
5721                    cx,
5722                ),
5723                select_first_in_all_matches(navigated_outline_selection)
5724            );
5725        });
5726        cx.executor()
5727            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5728        outline_panel.update(cx, |_, cx| {
5729            assert_eq!(
5730                selected_row_text(&active_editor, cx),
5731                navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5732                "Should still have the initial caret position after SelectNext calls"
5733            );
5734        });
5735
5736        outline_panel.update_in(cx, |outline_panel, window, cx| {
5737            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5738        });
5739        outline_panel.update(cx, |_outline_panel, cx| {
5740            assert_eq!(
5741                selected_row_text(&active_editor, cx),
5742                navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5743                "After opening, should move the caret to the opened outline entry's position"
5744            );
5745        });
5746
5747        outline_panel.update_in(cx, |outline_panel, window, cx| {
5748            outline_panel.select_next(&SelectNext, window, cx);
5749        });
5750        let next_navigated_outline_selection = "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5751        outline_panel.update(cx, |outline_panel, cx| {
5752            assert_eq!(
5753                display_entries(
5754                    &project,
5755                    &snapshot(outline_panel, cx),
5756                    &outline_panel.cached_entries,
5757                    outline_panel.selected_entry(),
5758                    cx,
5759                ),
5760                select_first_in_all_matches(next_navigated_outline_selection)
5761            );
5762        });
5763        cx.executor()
5764            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5765        outline_panel.update(cx, |_outline_panel, cx| {
5766            assert_eq!(
5767                selected_row_text(&active_editor, cx),
5768                next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5769                "Should again preserve the selection after another SelectNext call"
5770            );
5771        });
5772
5773        outline_panel.update_in(cx, |outline_panel, window, cx| {
5774            outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx);
5775        });
5776        cx.executor()
5777            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5778        cx.run_until_parked();
5779        let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5780            outline_panel
5781                .active_editor()
5782                .expect("should have an active editor open")
5783        });
5784        outline_panel.update(cx, |outline_panel, cx| {
5785            assert_ne!(
5786                active_editor, new_active_editor,
5787                "After opening an excerpt, new editor should be open"
5788            );
5789            assert_eq!(
5790                display_entries(
5791                    &project,
5792                    &snapshot(outline_panel, cx),
5793                    &outline_panel.cached_entries,
5794                    outline_panel.selected_entry(),
5795                    cx,
5796                ),
5797                "fn_lifetime_fn.rs  <==== selected"
5798            );
5799            assert_eq!(
5800                selected_row_text(&new_active_editor, cx),
5801                next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5802                "When opening the excerpt, should navigate to the place corresponding the outline entry"
5803            );
5804        });
5805    }
5806
5807    #[gpui::test]
5808    async fn test_multiple_workrees(cx: &mut TestAppContext) {
5809        init_test(cx);
5810
5811        let fs = FakeFs::new(cx.background_executor.clone());
5812        fs.insert_tree(
5813            path!("/root"),
5814            json!({
5815                "one": {
5816                    "a.txt": "aaa aaa"
5817                },
5818                "two": {
5819                    "b.txt": "a aaa"
5820                }
5821
5822            }),
5823        )
5824        .await;
5825        let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await;
5826        let workspace = add_outline_panel(&project, cx).await;
5827        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5828        let outline_panel = outline_panel(&workspace, cx);
5829        outline_panel.update_in(cx, |outline_panel, window, cx| {
5830            outline_panel.set_active(true, window, cx)
5831        });
5832
5833        let items = workspace
5834            .update(cx, |workspace, window, cx| {
5835                workspace.open_paths(
5836                    vec![PathBuf::from(path!("/root/two"))],
5837                    OpenOptions {
5838                        visible: Some(OpenVisible::OnlyDirectories),
5839                        ..Default::default()
5840                    },
5841                    None,
5842                    window,
5843                    cx,
5844                )
5845            })
5846            .unwrap()
5847            .await;
5848        assert_eq!(items.len(), 1, "Were opening another worktree directory");
5849        assert!(
5850            items[0].is_none(),
5851            "Directory should be opened successfully"
5852        );
5853
5854        workspace
5855            .update(cx, |workspace, window, cx| {
5856                ProjectSearchView::deploy_search(
5857                    workspace,
5858                    &workspace::DeploySearch::default(),
5859                    window,
5860                    cx,
5861                )
5862            })
5863            .unwrap();
5864        let search_view = workspace
5865            .update(cx, |workspace, _, cx| {
5866                workspace
5867                    .active_pane()
5868                    .read(cx)
5869                    .items()
5870                    .find_map(|item| item.downcast::<ProjectSearchView>())
5871                    .expect("Project search view expected to appear after new search event trigger")
5872            })
5873            .unwrap();
5874
5875        let query = "aaa";
5876        perform_project_search(&search_view, query, cx);
5877        search_view.update(cx, |search_view, cx| {
5878            search_view
5879                .results_editor()
5880                .update(cx, |results_editor, cx| {
5881                    assert_eq!(
5882                        results_editor.display_text(cx).match_indices(query).count(),
5883                        3
5884                    );
5885                });
5886        });
5887
5888        cx.executor()
5889            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5890        cx.run_until_parked();
5891        outline_panel.update(cx, |outline_panel, cx| {
5892            assert_eq!(
5893                display_entries(
5894                    &project,
5895                    &snapshot(outline_panel, cx),
5896                    &outline_panel.cached_entries,
5897                    outline_panel.selected_entry(),
5898                    cx,
5899                ),
5900                format!(
5901                    r#"one/
5902  a.txt
5903    search: aaa aaa  <==== selected
5904    search: aaa aaa
5905two/
5906  b.txt
5907    search: a aaa"#,
5908                ),
5909            );
5910        });
5911
5912        outline_panel.update_in(cx, |outline_panel, window, cx| {
5913            outline_panel.select_previous(&SelectPrevious, window, cx);
5914            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5915        });
5916        cx.executor()
5917            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5918        cx.run_until_parked();
5919        outline_panel.update(cx, |outline_panel, cx| {
5920            assert_eq!(
5921                display_entries(
5922                    &project,
5923                    &snapshot(outline_panel, cx),
5924                    &outline_panel.cached_entries,
5925                    outline_panel.selected_entry(),
5926                    cx,
5927                ),
5928                format!(
5929                    r#"one/
5930  a.txt  <==== selected
5931two/
5932  b.txt
5933    search: a aaa"#,
5934                ),
5935            );
5936        });
5937
5938        outline_panel.update_in(cx, |outline_panel, window, cx| {
5939            outline_panel.select_next(&SelectNext, window, cx);
5940            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5941        });
5942        cx.executor()
5943            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5944        cx.run_until_parked();
5945        outline_panel.update(cx, |outline_panel, cx| {
5946            assert_eq!(
5947                display_entries(
5948                    &project,
5949                    &snapshot(outline_panel, cx),
5950                    &outline_panel.cached_entries,
5951                    outline_panel.selected_entry(),
5952                    cx,
5953                ),
5954                format!(
5955                    r#"one/
5956  a.txt
5957two/  <==== selected"#,
5958                ),
5959            );
5960        });
5961
5962        outline_panel.update_in(cx, |outline_panel, window, cx| {
5963            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5964        });
5965        cx.executor()
5966            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5967        cx.run_until_parked();
5968        outline_panel.update(cx, |outline_panel, cx| {
5969            assert_eq!(
5970                display_entries(
5971                    &project,
5972                    &snapshot(outline_panel, cx),
5973                    &outline_panel.cached_entries,
5974                    outline_panel.selected_entry(),
5975                    cx,
5976                ),
5977                format!(
5978                    r#"one/
5979  a.txt
5980two/  <==== selected
5981  b.txt
5982    search: a aaa"#,
5983                )
5984            );
5985        });
5986    }
5987
5988    #[gpui::test]
5989    async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
5990        init_test(cx);
5991
5992        let root = path!("/root");
5993        let fs = FakeFs::new(cx.background_executor.clone());
5994        fs.insert_tree(
5995            root,
5996            json!({
5997                "src": {
5998                    "lib.rs": indoc!("
5999#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6000struct OutlineEntryExcerpt {
6001    id: ExcerptId,
6002    buffer_id: BufferId,
6003    range: ExcerptRange<language::Anchor>,
6004}"),
6005                }
6006            }),
6007        )
6008        .await;
6009        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6010        project.read_with(cx, |project, _| {
6011            project.languages().add(Arc::new(
6012                rust_lang()
6013                    .with_outline_query(
6014                        r#"
6015                (struct_item
6016                    (visibility_modifier)? @context
6017                    "struct" @context
6018                    name: (_) @name) @item
6019
6020                (field_declaration
6021                    (visibility_modifier)? @context
6022                    name: (_) @name) @item
6023"#,
6024                    )
6025                    .unwrap(),
6026            ))
6027        });
6028        let workspace = add_outline_panel(&project, cx).await;
6029        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6030        let outline_panel = outline_panel(&workspace, cx);
6031        cx.update(|window, cx| {
6032            outline_panel.update(cx, |outline_panel, cx| {
6033                outline_panel.set_active(true, window, cx)
6034            });
6035        });
6036
6037        let _editor = workspace
6038            .update(cx, |workspace, window, cx| {
6039                workspace.open_abs_path(
6040                    PathBuf::from(path!("/root/src/lib.rs")),
6041                    OpenOptions {
6042                        visible: Some(OpenVisible::All),
6043                        ..Default::default()
6044                    },
6045                    window,
6046                    cx,
6047                )
6048            })
6049            .unwrap()
6050            .await
6051            .expect("Failed to open Rust source file")
6052            .downcast::<Editor>()
6053            .expect("Should open an editor for Rust source file");
6054
6055        cx.executor()
6056            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6057        cx.run_until_parked();
6058        outline_panel.update(cx, |outline_panel, cx| {
6059            assert_eq!(
6060                display_entries(
6061                    &project,
6062                    &snapshot(outline_panel, cx),
6063                    &outline_panel.cached_entries,
6064                    outline_panel.selected_entry(),
6065                    cx,
6066                ),
6067                indoc!(
6068                    "
6069outline: struct OutlineEntryExcerpt
6070  outline: id
6071  outline: buffer_id
6072  outline: range"
6073                )
6074            );
6075        });
6076
6077        cx.update(|window, cx| {
6078            outline_panel.update(cx, |outline_panel, cx| {
6079                outline_panel.select_next(&SelectNext, window, cx);
6080            });
6081        });
6082        cx.executor()
6083            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6084        cx.run_until_parked();
6085        outline_panel.update(cx, |outline_panel, cx| {
6086            assert_eq!(
6087                display_entries(
6088                    &project,
6089                    &snapshot(outline_panel, cx),
6090                    &outline_panel.cached_entries,
6091                    outline_panel.selected_entry(),
6092                    cx,
6093                ),
6094                indoc!(
6095                    "
6096outline: struct OutlineEntryExcerpt  <==== selected
6097  outline: id
6098  outline: buffer_id
6099  outline: range"
6100                )
6101            );
6102        });
6103
6104        cx.update(|window, cx| {
6105            outline_panel.update(cx, |outline_panel, cx| {
6106                outline_panel.select_next(&SelectNext, window, cx);
6107            });
6108        });
6109        cx.executor()
6110            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6111        cx.run_until_parked();
6112        outline_panel.update(cx, |outline_panel, cx| {
6113            assert_eq!(
6114                display_entries(
6115                    &project,
6116                    &snapshot(outline_panel, cx),
6117                    &outline_panel.cached_entries,
6118                    outline_panel.selected_entry(),
6119                    cx,
6120                ),
6121                indoc!(
6122                    "
6123outline: struct OutlineEntryExcerpt
6124  outline: id  <==== selected
6125  outline: buffer_id
6126  outline: range"
6127                )
6128            );
6129        });
6130
6131        cx.update(|window, cx| {
6132            outline_panel.update(cx, |outline_panel, cx| {
6133                outline_panel.select_next(&SelectNext, window, cx);
6134            });
6135        });
6136        cx.executor()
6137            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6138        cx.run_until_parked();
6139        outline_panel.update(cx, |outline_panel, cx| {
6140            assert_eq!(
6141                display_entries(
6142                    &project,
6143                    &snapshot(outline_panel, cx),
6144                    &outline_panel.cached_entries,
6145                    outline_panel.selected_entry(),
6146                    cx,
6147                ),
6148                indoc!(
6149                    "
6150outline: struct OutlineEntryExcerpt
6151  outline: id
6152  outline: buffer_id  <==== selected
6153  outline: range"
6154                )
6155            );
6156        });
6157
6158        cx.update(|window, cx| {
6159            outline_panel.update(cx, |outline_panel, cx| {
6160                outline_panel.select_next(&SelectNext, window, cx);
6161            });
6162        });
6163        cx.executor()
6164            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6165        cx.run_until_parked();
6166        outline_panel.update(cx, |outline_panel, cx| {
6167            assert_eq!(
6168                display_entries(
6169                    &project,
6170                    &snapshot(outline_panel, cx),
6171                    &outline_panel.cached_entries,
6172                    outline_panel.selected_entry(),
6173                    cx,
6174                ),
6175                indoc!(
6176                    "
6177outline: struct OutlineEntryExcerpt
6178  outline: id
6179  outline: buffer_id
6180  outline: range  <==== selected"
6181                )
6182            );
6183        });
6184
6185        cx.update(|window, cx| {
6186            outline_panel.update(cx, |outline_panel, cx| {
6187                outline_panel.select_next(&SelectNext, window, cx);
6188            });
6189        });
6190        cx.executor()
6191            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6192        cx.run_until_parked();
6193        outline_panel.update(cx, |outline_panel, cx| {
6194            assert_eq!(
6195                display_entries(
6196                    &project,
6197                    &snapshot(outline_panel, cx),
6198                    &outline_panel.cached_entries,
6199                    outline_panel.selected_entry(),
6200                    cx,
6201                ),
6202                indoc!(
6203                    "
6204outline: struct OutlineEntryExcerpt  <==== selected
6205  outline: id
6206  outline: buffer_id
6207  outline: range"
6208                )
6209            );
6210        });
6211
6212        cx.update(|window, cx| {
6213            outline_panel.update(cx, |outline_panel, cx| {
6214                outline_panel.select_previous(&SelectPrevious, window, cx);
6215            });
6216        });
6217        cx.executor()
6218            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6219        cx.run_until_parked();
6220        outline_panel.update(cx, |outline_panel, cx| {
6221            assert_eq!(
6222                display_entries(
6223                    &project,
6224                    &snapshot(outline_panel, cx),
6225                    &outline_panel.cached_entries,
6226                    outline_panel.selected_entry(),
6227                    cx,
6228                ),
6229                indoc!(
6230                    "
6231outline: struct OutlineEntryExcerpt
6232  outline: id
6233  outline: buffer_id
6234  outline: range  <==== selected"
6235                )
6236            );
6237        });
6238
6239        cx.update(|window, cx| {
6240            outline_panel.update(cx, |outline_panel, cx| {
6241                outline_panel.select_previous(&SelectPrevious, window, cx);
6242            });
6243        });
6244        cx.executor()
6245            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6246        cx.run_until_parked();
6247        outline_panel.update(cx, |outline_panel, cx| {
6248            assert_eq!(
6249                display_entries(
6250                    &project,
6251                    &snapshot(outline_panel, cx),
6252                    &outline_panel.cached_entries,
6253                    outline_panel.selected_entry(),
6254                    cx,
6255                ),
6256                indoc!(
6257                    "
6258outline: struct OutlineEntryExcerpt
6259  outline: id
6260  outline: buffer_id  <==== selected
6261  outline: range"
6262                )
6263            );
6264        });
6265
6266        cx.update(|window, cx| {
6267            outline_panel.update(cx, |outline_panel, cx| {
6268                outline_panel.select_previous(&SelectPrevious, window, cx);
6269            });
6270        });
6271        cx.executor()
6272            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6273        cx.run_until_parked();
6274        outline_panel.update(cx, |outline_panel, cx| {
6275            assert_eq!(
6276                display_entries(
6277                    &project,
6278                    &snapshot(outline_panel, cx),
6279                    &outline_panel.cached_entries,
6280                    outline_panel.selected_entry(),
6281                    cx,
6282                ),
6283                indoc!(
6284                    "
6285outline: struct OutlineEntryExcerpt
6286  outline: id  <==== selected
6287  outline: buffer_id
6288  outline: range"
6289                )
6290            );
6291        });
6292
6293        cx.update(|window, cx| {
6294            outline_panel.update(cx, |outline_panel, cx| {
6295                outline_panel.select_previous(&SelectPrevious, window, cx);
6296            });
6297        });
6298        cx.executor()
6299            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6300        cx.run_until_parked();
6301        outline_panel.update(cx, |outline_panel, cx| {
6302            assert_eq!(
6303                display_entries(
6304                    &project,
6305                    &snapshot(outline_panel, cx),
6306                    &outline_panel.cached_entries,
6307                    outline_panel.selected_entry(),
6308                    cx,
6309                ),
6310                indoc!(
6311                    "
6312outline: struct OutlineEntryExcerpt  <==== selected
6313  outline: id
6314  outline: buffer_id
6315  outline: range"
6316                )
6317            );
6318        });
6319
6320        cx.update(|window, cx| {
6321            outline_panel.update(cx, |outline_panel, cx| {
6322                outline_panel.select_previous(&SelectPrevious, window, cx);
6323            });
6324        });
6325        cx.executor()
6326            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6327        cx.run_until_parked();
6328        outline_panel.update(cx, |outline_panel, cx| {
6329            assert_eq!(
6330                display_entries(
6331                    &project,
6332                    &snapshot(outline_panel, cx),
6333                    &outline_panel.cached_entries,
6334                    outline_panel.selected_entry(),
6335                    cx,
6336                ),
6337                indoc!(
6338                    "
6339outline: struct OutlineEntryExcerpt
6340  outline: id
6341  outline: buffer_id
6342  outline: range  <==== selected"
6343                )
6344            );
6345        });
6346    }
6347
6348    #[gpui::test(iterations = 10)]
6349    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6350        init_test(cx);
6351
6352        let root = path!("/frontend-project");
6353        let fs = FakeFs::new(cx.background_executor.clone());
6354        fs.insert_tree(
6355            root,
6356            json!({
6357                "public": {
6358                    "lottie": {
6359                        "syntax-tree.json": r#"{ "something": "static" }"#
6360                    }
6361                },
6362                "src": {
6363                    "app": {
6364                        "(site)": {
6365                            "(about)": {
6366                                "jobs": {
6367                                    "[slug]": {
6368                                        "page.tsx": r#"static"#
6369                                    }
6370                                }
6371                            },
6372                            "(blog)": {
6373                                "post": {
6374                                    "[slug]": {
6375                                        "page.tsx": r#"static"#
6376                                    }
6377                                }
6378                            },
6379                        }
6380                    },
6381                    "components": {
6382                        "ErrorBoundary.tsx": r#"static"#,
6383                    }
6384                }
6385
6386            }),
6387        )
6388        .await;
6389        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6390        let workspace = add_outline_panel(&project, cx).await;
6391        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6392        let outline_panel = outline_panel(&workspace, cx);
6393        outline_panel.update_in(cx, |outline_panel, window, cx| {
6394            outline_panel.set_active(true, window, cx)
6395        });
6396
6397        workspace
6398            .update(cx, |workspace, window, cx| {
6399                ProjectSearchView::deploy_search(
6400                    workspace,
6401                    &workspace::DeploySearch::default(),
6402                    window,
6403                    cx,
6404                )
6405            })
6406            .unwrap();
6407        let search_view = workspace
6408            .update(cx, |workspace, _, cx| {
6409                workspace
6410                    .active_pane()
6411                    .read(cx)
6412                    .items()
6413                    .find_map(|item| item.downcast::<ProjectSearchView>())
6414                    .expect("Project search view expected to appear after new search event trigger")
6415            })
6416            .unwrap();
6417
6418        let query = "static";
6419        perform_project_search(&search_view, query, cx);
6420        search_view.update(cx, |search_view, cx| {
6421            search_view
6422                .results_editor()
6423                .update(cx, |results_editor, cx| {
6424                    assert_eq!(
6425                        results_editor.display_text(cx).match_indices(query).count(),
6426                        4
6427                    );
6428                });
6429        });
6430
6431        cx.executor()
6432            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6433        cx.run_until_parked();
6434        outline_panel.update(cx, |outline_panel, cx| {
6435            assert_eq!(
6436                display_entries(
6437                    &project,
6438                    &snapshot(outline_panel, cx),
6439                    &outline_panel.cached_entries,
6440                    outline_panel.selected_entry(),
6441                    cx,
6442                ),
6443                format!(
6444                    r#"frontend-project/
6445  public/lottie/
6446    syntax-tree.json
6447      search: {{ "something": "static" }}  <==== selected
6448  src/
6449    app/(site)/
6450      (about)/jobs/[slug]/
6451        page.tsx
6452          search: static
6453      (blog)/post/[slug]/
6454        page.tsx
6455          search: static
6456    components/
6457      ErrorBoundary.tsx
6458        search: static"#
6459                )
6460            );
6461        });
6462
6463        outline_panel.update_in(cx, |outline_panel, window, cx| {
6464            // Move to 5th element in the list, 3 items down.
6465            for _ in 0..2 {
6466                outline_panel.select_next(&SelectNext, window, cx);
6467            }
6468            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6469        });
6470        cx.executor()
6471            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6472        cx.run_until_parked();
6473        outline_panel.update(cx, |outline_panel, cx| {
6474            assert_eq!(
6475                display_entries(
6476                    &project,
6477                    &snapshot(outline_panel, cx),
6478                    &outline_panel.cached_entries,
6479                    outline_panel.selected_entry(),
6480                    cx,
6481                ),
6482                format!(
6483                    r#"frontend-project/
6484  public/lottie/
6485    syntax-tree.json
6486      search: {{ "something": "static" }}
6487  src/
6488    app/(site)/  <==== selected
6489    components/
6490      ErrorBoundary.tsx
6491        search: static"#
6492                )
6493            );
6494        });
6495
6496        outline_panel.update_in(cx, |outline_panel, window, cx| {
6497            // Move to the next visible non-FS entry
6498            for _ in 0..3 {
6499                outline_panel.select_next(&SelectNext, window, cx);
6500            }
6501        });
6502        cx.run_until_parked();
6503        outline_panel.update(cx, |outline_panel, cx| {
6504            assert_eq!(
6505                display_entries(
6506                    &project,
6507                    &snapshot(outline_panel, cx),
6508                    &outline_panel.cached_entries,
6509                    outline_panel.selected_entry(),
6510                    cx,
6511                ),
6512                format!(
6513                    r#"frontend-project/
6514  public/lottie/
6515    syntax-tree.json
6516      search: {{ "something": "static" }}
6517  src/
6518    app/(site)/
6519    components/
6520      ErrorBoundary.tsx
6521        search: static  <==== selected"#
6522                )
6523            );
6524        });
6525
6526        outline_panel.update_in(cx, |outline_panel, window, cx| {
6527            outline_panel
6528                .active_editor()
6529                .expect("Should have an active editor")
6530                .update(cx, |editor, cx| {
6531                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6532                });
6533        });
6534        cx.executor()
6535            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6536        cx.run_until_parked();
6537        outline_panel.update(cx, |outline_panel, cx| {
6538            assert_eq!(
6539                display_entries(
6540                    &project,
6541                    &snapshot(outline_panel, cx),
6542                    &outline_panel.cached_entries,
6543                    outline_panel.selected_entry(),
6544                    cx,
6545                ),
6546                format!(
6547                    r#"frontend-project/
6548  public/lottie/
6549    syntax-tree.json
6550      search: {{ "something": "static" }}
6551  src/
6552    app/(site)/
6553    components/
6554      ErrorBoundary.tsx  <==== selected"#
6555                )
6556            );
6557        });
6558
6559        outline_panel.update_in(cx, |outline_panel, window, cx| {
6560            outline_panel
6561                .active_editor()
6562                .expect("Should have an active editor")
6563                .update(cx, |editor, cx| {
6564                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6565                });
6566        });
6567        cx.executor()
6568            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6569        cx.run_until_parked();
6570        outline_panel.update(cx, |outline_panel, cx| {
6571            assert_eq!(
6572                display_entries(
6573                    &project,
6574                    &snapshot(outline_panel, cx),
6575                    &outline_panel.cached_entries,
6576                    outline_panel.selected_entry(),
6577                    cx,
6578                ),
6579                format!(
6580                    r#"frontend-project/
6581  public/lottie/
6582    syntax-tree.json
6583      search: {{ "something": "static" }}
6584  src/
6585    app/(site)/
6586    components/
6587      ErrorBoundary.tsx  <==== selected
6588        search: static"#
6589                )
6590            );
6591        });
6592    }
6593
6594    async fn add_outline_panel(
6595        project: &Entity<Project>,
6596        cx: &mut TestAppContext,
6597    ) -> WindowHandle<Workspace> {
6598        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6599
6600        let outline_panel = window
6601            .update(cx, |_, window, cx| {
6602                cx.spawn_in(window, async |this, cx| {
6603                    OutlinePanel::load(this, cx.clone()).await
6604                })
6605            })
6606            .unwrap()
6607            .await
6608            .expect("Failed to load outline panel");
6609
6610        window
6611            .update(cx, |workspace, window, cx| {
6612                workspace.add_panel(outline_panel, window, cx);
6613            })
6614            .unwrap();
6615        window
6616    }
6617
6618    fn outline_panel(
6619        workspace: &WindowHandle<Workspace>,
6620        cx: &mut TestAppContext,
6621    ) -> Entity<OutlinePanel> {
6622        workspace
6623            .update(cx, |workspace, _, cx| {
6624                workspace
6625                    .panel::<OutlinePanel>(cx)
6626                    .expect("no outline panel")
6627            })
6628            .unwrap()
6629    }
6630
6631    fn display_entries(
6632        project: &Entity<Project>,
6633        multi_buffer_snapshot: &MultiBufferSnapshot,
6634        cached_entries: &[CachedEntry],
6635        selected_entry: Option<&PanelEntry>,
6636        cx: &mut App,
6637    ) -> String {
6638        let project = project.read(cx);
6639        let mut display_string = String::new();
6640        for entry in cached_entries {
6641            if !display_string.is_empty() {
6642                display_string += "\n";
6643            }
6644            for _ in 0..entry.depth {
6645                display_string += "  ";
6646            }
6647            display_string += &match &entry.entry {
6648                PanelEntry::Fs(entry) => match entry {
6649                    FsEntry::ExternalFile(_) => {
6650                        panic!("Did not cover external files with tests")
6651                    }
6652                    FsEntry::Directory(directory) => {
6653                        let path = if let Some(worktree) = project
6654                            .worktree_for_id(directory.worktree_id, cx)
6655                            .filter(|worktree| {
6656                                worktree.read(cx).root_entry() == Some(&directory.entry.entry)
6657                            }) {
6658                            worktree
6659                                .read(cx)
6660                                .root_name()
6661                                .join(&directory.entry.path)
6662                                .as_unix_str()
6663                                .to_string()
6664                        } else {
6665                            directory
6666                                .entry
6667                                .path
6668                                .file_name()
6669                                .unwrap_or_default()
6670                                .to_string()
6671                        };
6672                        format!("{path}/")
6673                    }
6674                    FsEntry::File(file) => file
6675                        .entry
6676                        .path
6677                        .file_name()
6678                        .map(|name| name.to_string())
6679                        .unwrap_or_default(),
6680                },
6681                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6682                    .entries
6683                    .iter()
6684                    .filter_map(|dir| dir.path.file_name())
6685                    .map(|name| name.to_string() + "/")
6686                    .collect(),
6687                PanelEntry::Outline(outline_entry) => match outline_entry {
6688                    OutlineEntry::Excerpt(_) => continue,
6689                    OutlineEntry::Outline(outline_entry) => {
6690                        format!("outline: {}", outline_entry.outline.text)
6691                    }
6692                },
6693                PanelEntry::Search(search_entry) => {
6694                    format!(
6695                        "search: {}",
6696                        search_entry
6697                            .render_data
6698                            .get_or_init(|| SearchData::new(
6699                                &search_entry.match_range,
6700                                multi_buffer_snapshot
6701                            ))
6702                            .context_text
6703                    )
6704                }
6705            };
6706
6707            if Some(&entry.entry) == selected_entry {
6708                display_string += SELECTED_MARKER;
6709            }
6710        }
6711        display_string
6712    }
6713
6714    fn init_test(cx: &mut TestAppContext) {
6715        cx.update(|cx| {
6716            let settings = SettingsStore::test(cx);
6717            cx.set_global(settings);
6718
6719            theme::init(theme::LoadThemes::JustBase, cx);
6720
6721            language::init(cx);
6722            editor::init(cx);
6723            workspace::init_settings(cx);
6724            Project::init_settings(cx);
6725            project_search::init(cx);
6726            super::init(cx);
6727        });
6728    }
6729
6730    // Based on https://github.com/rust-lang/rust-analyzer/
6731    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6732        fs.insert_tree(
6733            root,
6734            json!({
6735                    "crates": {
6736                        "ide": {
6737                            "src": {
6738                                "inlay_hints": {
6739                                    "fn_lifetime_fn.rs": r##"
6740        pub(super) fn hints(
6741            acc: &mut Vec<InlayHint>,
6742            config: &InlayHintsConfig,
6743            func: ast::Fn,
6744        ) -> Option<()> {
6745            // ... snip
6746
6747            let mut used_names: FxHashMap<SmolStr, usize> =
6748                match config.param_names_for_lifetime_elision_hints {
6749                    true => generic_param_list
6750                        .iter()
6751                        .flat_map(|gpl| gpl.lifetime_params())
6752                        .filter_map(|param| param.lifetime())
6753                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6754                        .collect(),
6755                    false => Default::default(),
6756                };
6757            {
6758                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6759                if self_param.is_some() && potential_lt_refs.next().is_some() {
6760                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6761                        // self can't be used as a lifetime, so no need to check for collisions
6762                        "'self".into()
6763                    } else {
6764                        gen_idx_name()
6765                    });
6766                }
6767                potential_lt_refs.for_each(|(name, ..)| {
6768                    let name = match name {
6769                        Some(it) if config.param_names_for_lifetime_elision_hints => {
6770                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
6771                                *c += 1;
6772                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6773                            } else {
6774                                used_names.insert(it.text().as_str().into(), 0);
6775                                SmolStr::from_iter(["\'", it.text().as_str()])
6776                            }
6777                        }
6778                        _ => gen_idx_name(),
6779                    };
6780                    allocated_lifetimes.push(name);
6781                });
6782            }
6783
6784            // ... snip
6785        }
6786
6787        // ... snip
6788
6789            #[test]
6790            fn hints_lifetimes_named() {
6791                check_with_config(
6792                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6793                    r#"
6794        fn nested_in<'named>(named: &        &X<      &()>) {}
6795        //          ^'named1, 'named2, 'named3, $
6796                                  //^'named1 ^'named2 ^'named3
6797        "#,
6798                );
6799            }
6800
6801        // ... snip
6802        "##,
6803                                },
6804                        "inlay_hints.rs": r#"
6805    #[derive(Clone, Debug, PartialEq, Eq)]
6806    pub struct InlayHintsConfig {
6807        // ... snip
6808        pub param_names_for_lifetime_elision_hints: bool,
6809        pub max_length: Option<usize>,
6810        // ... snip
6811    }
6812
6813    impl Config {
6814        pub fn inlay_hints(&self) -> InlayHintsConfig {
6815            InlayHintsConfig {
6816                // ... snip
6817                param_names_for_lifetime_elision_hints: self
6818                    .inlayHints_lifetimeElisionHints_useParameterNames()
6819                    .to_owned(),
6820                max_length: self.inlayHints_maxLength().to_owned(),
6821                // ... snip
6822            }
6823        }
6824    }
6825    "#,
6826                        "static_index.rs": r#"
6827// ... snip
6828        fn add_file(&mut self, file_id: FileId) {
6829            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6830            let folds = self.analysis.folding_ranges(file_id).unwrap();
6831            let inlay_hints = self
6832                .analysis
6833                .inlay_hints(
6834                    &InlayHintsConfig {
6835                        // ... snip
6836                        closure_style: hir::ClosureStyle::ImplFn,
6837                        param_names_for_lifetime_elision_hints: false,
6838                        binding_mode_hints: false,
6839                        max_length: Some(25),
6840                        closure_capture_hints: false,
6841                        // ... snip
6842                    },
6843                    file_id,
6844                    None,
6845                )
6846                .unwrap();
6847            // ... snip
6848    }
6849// ... snip
6850    "#
6851                            }
6852                        },
6853                        "rust-analyzer": {
6854                            "src": {
6855                                "cli": {
6856                                    "analysis_stats.rs": r#"
6857        // ... snip
6858                for &file_id in &file_ids {
6859                    _ = analysis.inlay_hints(
6860                        &InlayHintsConfig {
6861                            // ... snip
6862                            implicit_drop_hints: true,
6863                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6864                            param_names_for_lifetime_elision_hints: true,
6865                            hide_named_constructor_hints: false,
6866                            hide_closure_initialization_hints: false,
6867                            closure_style: hir::ClosureStyle::ImplFn,
6868                            max_length: Some(25),
6869                            closing_brace_hints_min_lines: Some(20),
6870                            fields_to_resolve: InlayFieldsToResolve::empty(),
6871                            range_exclusive_hints: true,
6872                        },
6873                        file_id.into(),
6874                        None,
6875                    );
6876                }
6877        // ... snip
6878                                    "#,
6879                                },
6880                                "config.rs": r#"
6881                config_data! {
6882                    /// Configs that only make sense when they are set by a client. As such they can only be defined
6883                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
6884                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6885                        // ... snip
6886                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
6887                        inlayHints_maxLength: Option<usize>                        = Some(25),
6888                        // ... snip
6889                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6890                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
6891                        // ... snip
6892                    }
6893                }
6894
6895                impl Config {
6896                    // ... snip
6897                    pub fn inlay_hints(&self) -> InlayHintsConfig {
6898                        InlayHintsConfig {
6899                            // ... snip
6900                            param_names_for_lifetime_elision_hints: self
6901                                .inlayHints_lifetimeElisionHints_useParameterNames()
6902                                .to_owned(),
6903                            max_length: self.inlayHints_maxLength().to_owned(),
6904                            // ... snip
6905                        }
6906                    }
6907                    // ... snip
6908                }
6909                "#
6910                                }
6911                        }
6912                    }
6913            }),
6914        )
6915        .await;
6916    }
6917
6918    fn rust_lang() -> Language {
6919        Language::new(
6920            LanguageConfig {
6921                name: "Rust".into(),
6922                matcher: LanguageMatcher {
6923                    path_suffixes: vec!["rs".to_string()],
6924                    ..Default::default()
6925                },
6926                ..Default::default()
6927            },
6928            Some(tree_sitter_rust::LANGUAGE.into()),
6929        )
6930        .with_highlights_query(
6931            r#"
6932                (field_identifier) @field
6933                (struct_expression) @struct
6934            "#,
6935        )
6936        .unwrap()
6937        .with_injection_query(
6938            r#"
6939                (macro_invocation
6940                    (token_tree) @injection.content
6941                    (#set! injection.language "rust"))
6942            "#,
6943        )
6944        .unwrap()
6945    }
6946
6947    fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
6948        outline_panel
6949            .active_editor()
6950            .unwrap()
6951            .read(cx)
6952            .buffer()
6953            .read(cx)
6954            .snapshot(cx)
6955    }
6956
6957    fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
6958        editor.update(cx, |editor, cx| {
6959                let selections = editor.selections.all::<language::Point>(cx);
6960                assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
6961                let selection = selections.first().unwrap();
6962                let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
6963                let line_start = language::Point::new(selection.start.row, 0);
6964                let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
6965                multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
6966        })
6967    }
6968
6969    #[gpui::test]
6970    async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) {
6971        init_test(cx);
6972
6973        let fs = FakeFs::new(cx.background_executor.clone());
6974        fs.insert_tree(
6975            "/test",
6976            json!({
6977                "src": {
6978                    "lib.rs": indoc!("
6979                            mod outer {
6980                                pub struct OuterStruct {
6981                                    field: String,
6982                                }
6983                                impl OuterStruct {
6984                                    pub fn new() -> Self {
6985                                        Self { field: String::new() }
6986                                    }
6987                                    pub fn method(&self) {
6988                                        println!(\"{}\", self.field);
6989                                    }
6990                                }
6991                                mod inner {
6992                                    pub fn inner_function() {
6993                                        let x = 42;
6994                                        println!(\"{}\", x);
6995                                    }
6996                                    pub struct InnerStruct {
6997                                        value: i32,
6998                                    }
6999                                }
7000                            }
7001                            fn main() {
7002                                let s = outer::OuterStruct::new();
7003                                s.method();
7004                            }
7005                        "),
7006                }
7007            }),
7008        )
7009        .await;
7010
7011        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7012        project.read_with(cx, |project, _| {
7013            project.languages().add(Arc::new(
7014                rust_lang()
7015                    .with_outline_query(
7016                        r#"
7017                            (struct_item
7018                                (visibility_modifier)? @context
7019                                "struct" @context
7020                                name: (_) @name) @item
7021                            (impl_item
7022                                "impl" @context
7023                                trait: (_)? @context
7024                                "for"? @context
7025                                type: (_) @context
7026                                body: (_)) @item
7027                            (function_item
7028                                (visibility_modifier)? @context
7029                                "fn" @context
7030                                name: (_) @name
7031                                parameters: (_) @context) @item
7032                            (mod_item
7033                                (visibility_modifier)? @context
7034                                "mod" @context
7035                                name: (_) @name) @item
7036                            (enum_item
7037                                (visibility_modifier)? @context
7038                                "enum" @context
7039                                name: (_) @name) @item
7040                            (field_declaration
7041                                (visibility_modifier)? @context
7042                                name: (_) @name
7043                                ":" @context
7044                                type: (_) @context) @item
7045                            "#,
7046                    )
7047                    .unwrap(),
7048            ))
7049        });
7050        let workspace = add_outline_panel(&project, cx).await;
7051        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7052        let outline_panel = outline_panel(&workspace, cx);
7053
7054        outline_panel.update_in(cx, |outline_panel, window, cx| {
7055            outline_panel.set_active(true, window, cx)
7056        });
7057
7058        workspace
7059            .update(cx, |workspace, window, cx| {
7060                workspace.open_abs_path(
7061                    PathBuf::from("/test/src/lib.rs"),
7062                    OpenOptions {
7063                        visible: Some(OpenVisible::All),
7064                        ..Default::default()
7065                    },
7066                    window,
7067                    cx,
7068                )
7069            })
7070            .unwrap()
7071            .await
7072            .unwrap();
7073
7074        cx.executor()
7075            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7076        cx.run_until_parked();
7077
7078        // Force another update cycle to ensure outlines are fetched
7079        outline_panel.update_in(cx, |panel, window, cx| {
7080            panel.update_non_fs_items(window, cx);
7081            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7082        });
7083        cx.executor()
7084            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7085        cx.run_until_parked();
7086
7087        outline_panel.update(cx, |outline_panel, cx| {
7088            assert_eq!(
7089                display_entries(
7090                    &project,
7091                    &snapshot(outline_panel, cx),
7092                    &outline_panel.cached_entries,
7093                    outline_panel.selected_entry(),
7094                    cx,
7095                ),
7096                indoc!(
7097                    "
7098outline: mod outer  <==== selected
7099  outline: pub struct OuterStruct
7100    outline: field: String
7101  outline: impl OuterStruct
7102    outline: pub fn new()
7103    outline: pub fn method(&self)
7104  outline: mod inner
7105    outline: pub fn inner_function()
7106    outline: pub struct InnerStruct
7107      outline: value: i32
7108outline: fn main()"
7109                )
7110            );
7111        });
7112
7113        let parent_outline = outline_panel
7114            .read_with(cx, |panel, _cx| {
7115                panel
7116                    .cached_entries
7117                    .iter()
7118                    .find_map(|entry| match &entry.entry {
7119                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7120                            if panel
7121                                .outline_children_cache
7122                                .get(&outline.buffer_id)
7123                                .and_then(|children_map| {
7124                                    let key =
7125                                        (outline.outline.range.clone(), outline.outline.depth);
7126                                    children_map.get(&key)
7127                                })
7128                                .copied()
7129                                .unwrap_or(false) =>
7130                        {
7131                            Some(entry.entry.clone())
7132                        }
7133                        _ => None,
7134                    })
7135            })
7136            .expect("Should find an outline with children");
7137
7138        outline_panel.update_in(cx, |panel, window, cx| {
7139            panel.select_entry(parent_outline.clone(), true, window, cx);
7140            panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7141        });
7142        cx.executor()
7143            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7144        cx.run_until_parked();
7145
7146        outline_panel.update(cx, |outline_panel, cx| {
7147            assert_eq!(
7148                display_entries(
7149                    &project,
7150                    &snapshot(outline_panel, cx),
7151                    &outline_panel.cached_entries,
7152                    outline_panel.selected_entry(),
7153                    cx,
7154                ),
7155                indoc!(
7156                    "
7157outline: mod outer  <==== selected
7158outline: fn main()"
7159                )
7160            );
7161        });
7162
7163        outline_panel.update_in(cx, |panel, window, cx| {
7164            panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7165        });
7166        cx.executor()
7167            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7168        cx.run_until_parked();
7169
7170        outline_panel.update(cx, |outline_panel, cx| {
7171            assert_eq!(
7172                display_entries(
7173                    &project,
7174                    &snapshot(outline_panel, cx),
7175                    &outline_panel.cached_entries,
7176                    outline_panel.selected_entry(),
7177                    cx,
7178                ),
7179                indoc!(
7180                    "
7181outline: mod outer  <==== selected
7182  outline: pub struct OuterStruct
7183    outline: field: String
7184  outline: impl OuterStruct
7185    outline: pub fn new()
7186    outline: pub fn method(&self)
7187  outline: mod inner
7188    outline: pub fn inner_function()
7189    outline: pub struct InnerStruct
7190      outline: value: i32
7191outline: fn main()"
7192                )
7193            );
7194        });
7195
7196        outline_panel.update_in(cx, |panel, window, cx| {
7197            panel.collapsed_entries.clear();
7198            panel.update_cached_entries(None, window, cx);
7199        });
7200        cx.executor()
7201            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7202        cx.run_until_parked();
7203
7204        outline_panel.update_in(cx, |panel, window, cx| {
7205            let outlines_with_children: Vec<_> = panel
7206                .cached_entries
7207                .iter()
7208                .filter_map(|entry| match &entry.entry {
7209                    PanelEntry::Outline(OutlineEntry::Outline(outline))
7210                        if panel
7211                            .outline_children_cache
7212                            .get(&outline.buffer_id)
7213                            .and_then(|children_map| {
7214                                let key = (outline.outline.range.clone(), outline.outline.depth);
7215                                children_map.get(&key)
7216                            })
7217                            .copied()
7218                            .unwrap_or(false) =>
7219                    {
7220                        Some(entry.entry.clone())
7221                    }
7222                    _ => None,
7223                })
7224                .collect();
7225
7226            for outline in outlines_with_children {
7227                panel.select_entry(outline, false, window, cx);
7228                panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7229            }
7230        });
7231        cx.executor()
7232            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7233        cx.run_until_parked();
7234
7235        outline_panel.update(cx, |outline_panel, cx| {
7236            assert_eq!(
7237                display_entries(
7238                    &project,
7239                    &snapshot(outline_panel, cx),
7240                    &outline_panel.cached_entries,
7241                    outline_panel.selected_entry(),
7242                    cx,
7243                ),
7244                indoc!(
7245                    "
7246outline: mod outer
7247outline: fn main()"
7248                )
7249            );
7250        });
7251
7252        let collapsed_entries_count =
7253            outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len());
7254        assert!(
7255            collapsed_entries_count > 0,
7256            "Should have collapsed entries tracked"
7257        );
7258    }
7259
7260    #[gpui::test]
7261    async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) {
7262        init_test(cx);
7263
7264        let fs = FakeFs::new(cx.background_executor.clone());
7265        fs.insert_tree(
7266            "/test",
7267            json!({
7268                "src": {
7269                    "main.rs": indoc!("
7270                            struct Config {
7271                                name: String,
7272                                value: i32,
7273                            }
7274                            impl Config {
7275                                fn new(name: String) -> Self {
7276                                    Self { name, value: 0 }
7277                                }
7278                                fn get_value(&self) -> i32 {
7279                                    self.value
7280                                }
7281                            }
7282                            enum Status {
7283                                Active,
7284                                Inactive,
7285                            }
7286                            fn process_config(config: Config) -> Status {
7287                                if config.get_value() > 0 {
7288                                    Status::Active
7289                                } else {
7290                                    Status::Inactive
7291                                }
7292                            }
7293                            fn main() {
7294                                let config = Config::new(\"test\".to_string());
7295                                let status = process_config(config);
7296                            }
7297                        "),
7298                }
7299            }),
7300        )
7301        .await;
7302
7303        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7304        project.read_with(cx, |project, _| {
7305            project.languages().add(Arc::new(
7306                rust_lang()
7307                    .with_outline_query(
7308                        r#"
7309                            (struct_item
7310                                (visibility_modifier)? @context
7311                                "struct" @context
7312                                name: (_) @name) @item
7313                            (impl_item
7314                                "impl" @context
7315                                trait: (_)? @context
7316                                "for"? @context
7317                                type: (_) @context
7318                                body: (_)) @item
7319                            (function_item
7320                                (visibility_modifier)? @context
7321                                "fn" @context
7322                                name: (_) @name
7323                                parameters: (_) @context) @item
7324                            (mod_item
7325                                (visibility_modifier)? @context
7326                                "mod" @context
7327                                name: (_) @name) @item
7328                            (enum_item
7329                                (visibility_modifier)? @context
7330                                "enum" @context
7331                                name: (_) @name) @item
7332                            (field_declaration
7333                                (visibility_modifier)? @context
7334                                name: (_) @name
7335                                ":" @context
7336                                type: (_) @context) @item
7337                            "#,
7338                    )
7339                    .unwrap(),
7340            ))
7341        });
7342
7343        let workspace = add_outline_panel(&project, cx).await;
7344        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7345        let outline_panel = outline_panel(&workspace, cx);
7346
7347        outline_panel.update_in(cx, |outline_panel, window, cx| {
7348            outline_panel.set_active(true, window, cx)
7349        });
7350
7351        let _editor = workspace
7352            .update(cx, |workspace, window, cx| {
7353                workspace.open_abs_path(
7354                    PathBuf::from("/test/src/main.rs"),
7355                    OpenOptions {
7356                        visible: Some(OpenVisible::All),
7357                        ..Default::default()
7358                    },
7359                    window,
7360                    cx,
7361                )
7362            })
7363            .unwrap()
7364            .await
7365            .unwrap();
7366
7367        cx.executor()
7368            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7369        cx.run_until_parked();
7370
7371        outline_panel.update(cx, |outline_panel, _cx| {
7372            outline_panel.selected_entry = SelectedEntry::None;
7373        });
7374
7375        // Check initial state - all entries should be expanded by default
7376        outline_panel.update(cx, |outline_panel, cx| {
7377            assert_eq!(
7378                display_entries(
7379                    &project,
7380                    &snapshot(outline_panel, cx),
7381                    &outline_panel.cached_entries,
7382                    outline_panel.selected_entry(),
7383                    cx,
7384                ),
7385                indoc!(
7386                    "
7387outline: struct Config
7388  outline: name: String
7389  outline: value: i32
7390outline: impl Config
7391  outline: fn new(name: String)
7392  outline: fn get_value(&self)
7393outline: enum Status
7394outline: fn process_config(config: Config)
7395outline: fn main()"
7396                )
7397            );
7398        });
7399
7400        outline_panel.update(cx, |outline_panel, _cx| {
7401            outline_panel.selected_entry = SelectedEntry::None;
7402        });
7403
7404        cx.update(|window, cx| {
7405            outline_panel.update(cx, |outline_panel, cx| {
7406                outline_panel.select_first(&SelectFirst, window, cx);
7407            });
7408        });
7409
7410        cx.executor()
7411            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7412        cx.run_until_parked();
7413
7414        outline_panel.update(cx, |outline_panel, cx| {
7415            assert_eq!(
7416                display_entries(
7417                    &project,
7418                    &snapshot(outline_panel, cx),
7419                    &outline_panel.cached_entries,
7420                    outline_panel.selected_entry(),
7421                    cx,
7422                ),
7423                indoc!(
7424                    "
7425outline: struct Config  <==== selected
7426  outline: name: String
7427  outline: value: i32
7428outline: impl Config
7429  outline: fn new(name: String)
7430  outline: fn get_value(&self)
7431outline: enum Status
7432outline: fn process_config(config: Config)
7433outline: fn main()"
7434                )
7435            );
7436        });
7437
7438        cx.update(|window, cx| {
7439            outline_panel.update(cx, |outline_panel, cx| {
7440                outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
7441            });
7442        });
7443
7444        cx.executor()
7445            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7446        cx.run_until_parked();
7447
7448        outline_panel.update(cx, |outline_panel, cx| {
7449            assert_eq!(
7450                display_entries(
7451                    &project,
7452                    &snapshot(outline_panel, cx),
7453                    &outline_panel.cached_entries,
7454                    outline_panel.selected_entry(),
7455                    cx,
7456                ),
7457                indoc!(
7458                    "
7459outline: struct Config  <==== selected
7460outline: impl Config
7461  outline: fn new(name: String)
7462  outline: fn get_value(&self)
7463outline: enum Status
7464outline: fn process_config(config: Config)
7465outline: fn main()"
7466                )
7467            );
7468        });
7469
7470        cx.update(|window, cx| {
7471            outline_panel.update(cx, |outline_panel, cx| {
7472                outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
7473            });
7474        });
7475
7476        cx.executor()
7477            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7478        cx.run_until_parked();
7479
7480        outline_panel.update(cx, |outline_panel, cx| {
7481            assert_eq!(
7482                display_entries(
7483                    &project,
7484                    &snapshot(outline_panel, cx),
7485                    &outline_panel.cached_entries,
7486                    outline_panel.selected_entry(),
7487                    cx,
7488                ),
7489                indoc!(
7490                    "
7491outline: struct Config  <==== selected
7492  outline: name: String
7493  outline: value: i32
7494outline: impl Config
7495  outline: fn new(name: String)
7496  outline: fn get_value(&self)
7497outline: enum Status
7498outline: fn process_config(config: Config)
7499outline: fn main()"
7500                )
7501            );
7502        });
7503    }
7504}