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