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