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