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