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