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