outline_panel.rs

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