outline_panel.rs

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