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 smol::channel;
  50use theme::SyntaxTheme;
  51use theme_settings::ThemeSettings;
  52use ui::{
  53    ContextMenu, FluentBuilder, HighlightedLabel, IconButton, IconButtonShape, IndentGuideColors,
  54    IndentGuideLayout, ListItem, ScrollAxes, Scrollbars, Tab, Tooltip, WithScrollbar, 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: 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) = channel::unbounded();
 180        let (notify_tx, notify_rx) = 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(h_flex().justify_center().child({
4565                    let keystroke = match self.position(window, cx) {
4566                        DockPosition::Left => window.keystroke_text_for(&workspace::ToggleLeftDock),
4567                        DockPosition::Bottom => {
4568                            window.keystroke_text_for(&workspace::ToggleBottomDock)
4569                        }
4570                        DockPosition::Right => {
4571                            window.keystroke_text_for(&workspace::ToggleRightDock)
4572                        }
4573                    };
4574                    Label::new(format!("Toggle Panel With {keystroke}")).color(Color::Muted)
4575                }))
4576        } else {
4577            let list_contents = {
4578                let items_len = self.cached_entries.len();
4579                let multi_buffer_snapshot = self
4580                    .active_editor()
4581                    .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4582                uniform_list(
4583                    "entries",
4584                    items_len,
4585                    cx.processor(move |outline_panel, range: Range<usize>, window, cx| {
4586                        outline_panel.rendered_entries_len = range.end - range.start;
4587                        let entries = outline_panel.cached_entries.get(range);
4588                        entries
4589                            .map(|entries| entries.to_vec())
4590                            .unwrap_or_default()
4591                            .into_iter()
4592                            .filter_map(|cached_entry| match cached_entry.entry {
4593                                PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4594                                    &entry,
4595                                    cached_entry.depth,
4596                                    cached_entry.string_match.as_ref(),
4597                                    window,
4598                                    cx,
4599                                )),
4600                                PanelEntry::FoldedDirs(folded_dirs_entry) => {
4601                                    Some(outline_panel.render_folded_dirs(
4602                                        &folded_dirs_entry,
4603                                        cached_entry.depth,
4604                                        cached_entry.string_match.as_ref(),
4605                                        window,
4606                                        cx,
4607                                    ))
4608                                }
4609                                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4610                                    outline_panel.render_excerpt(
4611                                        &excerpt,
4612                                        cached_entry.depth,
4613                                        window,
4614                                        cx,
4615                                    )
4616                                }
4617                                PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4618                                    Some(outline_panel.render_outline(
4619                                        &entry,
4620                                        cached_entry.depth,
4621                                        cached_entry.string_match.as_ref(),
4622                                        window,
4623                                        cx,
4624                                    ))
4625                                }
4626                                PanelEntry::Search(SearchEntry {
4627                                    match_range,
4628                                    render_data,
4629                                    kind,
4630                                    ..
4631                                }) => outline_panel.render_search_match(
4632                                    multi_buffer_snapshot.as_ref(),
4633                                    &match_range,
4634                                    &render_data,
4635                                    kind,
4636                                    cached_entry.depth,
4637                                    cached_entry.string_match.as_ref(),
4638                                    window,
4639                                    cx,
4640                                ),
4641                            })
4642                            .collect()
4643                    }),
4644                )
4645                .with_sizing_behavior(ListSizingBehavior::Infer)
4646                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4647                .with_width_from_item(self.max_width_item_index)
4648                .track_scroll(&self.scroll_handle)
4649                .when(show_indent_guides, |list| {
4650                    list.with_decoration(
4651                        ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
4652                            .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| {
4653                                let entries = outline_panel.cached_entries.get(range);
4654                                if let Some(entries) = entries {
4655                                    entries.iter().map(|item| item.depth).collect()
4656                                } else {
4657                                    smallvec::SmallVec::new()
4658                                }
4659                            })
4660                            .with_render_fn(cx.entity(), move |outline_panel, params, _, _| {
4661                                const LEFT_OFFSET: Pixels = px(14.);
4662
4663                                let indent_size = params.indent_size;
4664                                let item_height = params.item_height;
4665                                let active_indent_guide_ix = find_active_indent_guide_ix(
4666                                    outline_panel,
4667                                    &params.indent_guides,
4668                                );
4669
4670                                params
4671                                    .indent_guides
4672                                    .into_iter()
4673                                    .enumerate()
4674                                    .map(|(ix, layout)| {
4675                                        let bounds = Bounds::new(
4676                                            point(
4677                                                layout.offset.x * indent_size + LEFT_OFFSET,
4678                                                layout.offset.y * item_height,
4679                                            ),
4680                                            size(px(1.), layout.length * item_height),
4681                                        );
4682                                        ui::RenderedIndentGuide {
4683                                            bounds,
4684                                            layout,
4685                                            is_active: active_indent_guide_ix == Some(ix),
4686                                            hitbox: None,
4687                                        }
4688                                    })
4689                                    .collect()
4690                            }),
4691                    )
4692                })
4693            };
4694
4695            v_flex()
4696                .flex_shrink()
4697                .size_full()
4698                .child(list_contents.size_full().flex_shrink())
4699                .custom_scrollbars(
4700                    Scrollbars::for_settings::<OutlinePanelSettingsScrollbarProxy>()
4701                        .tracked_scroll_handle(&self.scroll_handle.clone())
4702                        .with_track_along(
4703                            ScrollAxes::Horizontal,
4704                            cx.theme().colors().panel_background,
4705                        )
4706                        .tracked_entity(cx.entity_id()),
4707                    window,
4708                    cx,
4709                )
4710        }
4711        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4712            deferred(
4713                anchored()
4714                    .position(*position)
4715                    .anchor(gpui::Corner::TopLeft)
4716                    .child(menu.clone()),
4717            )
4718            .with_priority(1)
4719        }));
4720
4721        v_flex().w_full().flex_1().overflow_hidden().child(contents)
4722    }
4723
4724    fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4725        let (icon, icon_tooltip) = if pinned {
4726            (IconName::Unpin, "Unpin Outline")
4727        } else {
4728            (IconName::Pin, "Pin Active Outline")
4729        };
4730
4731        let has_query = self.query(cx).is_some();
4732
4733        h_flex()
4734            .p_2()
4735            .h(Tab::container_height(cx))
4736            .justify_between()
4737            .border_b_1()
4738            .border_color(cx.theme().colors().border)
4739            .child(
4740                h_flex()
4741                    .w_full()
4742                    .gap_1p5()
4743                    .child(
4744                        Icon::new(IconName::MagnifyingGlass)
4745                            .size(IconSize::Small)
4746                            .color(Color::Muted),
4747                    )
4748                    .child(self.filter_editor.clone()),
4749            )
4750            .child(
4751                h_flex()
4752                    .when(has_query, |this| {
4753                        this.child(
4754                            IconButton::new("clear_filter", IconName::Close)
4755                                .shape(IconButtonShape::Square)
4756                                .tooltip(Tooltip::text("Clear Filter"))
4757                                .on_click(cx.listener(|outline_panel, _, window, cx| {
4758                                    outline_panel.filter_editor.update(cx, |editor, cx| {
4759                                        editor.set_text("", window, cx);
4760                                    });
4761                                    cx.notify();
4762                                })),
4763                        )
4764                    })
4765                    .child(
4766                        IconButton::new("pin_button", icon)
4767                            .tooltip(Tooltip::text(icon_tooltip))
4768                            .shape(IconButtonShape::Square)
4769                            .on_click(cx.listener(|outline_panel, _, window, cx| {
4770                                outline_panel.toggle_active_editor_pin(
4771                                    &ToggleActiveEditorPin,
4772                                    window,
4773                                    cx,
4774                                );
4775                            })),
4776                    ),
4777            )
4778    }
4779
4780    fn buffers_inside_directory(
4781        &self,
4782        dir_worktree: WorktreeId,
4783        dir_entry: &GitEntry,
4784    ) -> HashSet<BufferId> {
4785        if !dir_entry.is_dir() {
4786            debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4787            return HashSet::default();
4788        }
4789
4790        self.fs_entries
4791            .iter()
4792            .skip_while(|fs_entry| match fs_entry {
4793                FsEntry::Directory(directory) => {
4794                    directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4795                }
4796                _ => true,
4797            })
4798            .skip(1)
4799            .take_while(|fs_entry| match fs_entry {
4800                FsEntry::ExternalFile(..) => false,
4801                FsEntry::Directory(directory) => {
4802                    directory.worktree_id == dir_worktree
4803                        && directory.entry.path.starts_with(&dir_entry.path)
4804                }
4805                FsEntry::File(file) => {
4806                    file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4807                }
4808            })
4809            .filter_map(|fs_entry| match fs_entry {
4810                FsEntry::File(file) => Some(file.buffer_id),
4811                _ => None,
4812            })
4813            .collect()
4814    }
4815}
4816
4817fn workspace_active_editor(
4818    workspace: &Workspace,
4819    cx: &App,
4820) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4821    let active_item = workspace.active_item(cx)?;
4822    let active_editor = active_item
4823        .act_as::<Editor>(cx)
4824        .filter(|editor| editor.read(cx).mode().is_full())?;
4825    Some((active_item, active_editor))
4826}
4827
4828fn back_to_common_visited_parent(
4829    visited_dirs: &mut Vec<(ProjectEntryId, Arc<RelPath>)>,
4830    worktree_id: &WorktreeId,
4831    new_entry: &Entry,
4832) -> Option<(WorktreeId, ProjectEntryId)> {
4833    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4834        match new_entry.path.parent() {
4835            Some(parent_path) => {
4836                if parent_path == visited_path.as_ref() {
4837                    return Some((*worktree_id, *visited_dir_id));
4838                }
4839            }
4840            None => {
4841                break;
4842            }
4843        }
4844        visited_dirs.pop();
4845    }
4846    None
4847}
4848
4849fn file_name(path: &Path) -> String {
4850    let mut current_path = path;
4851    loop {
4852        if let Some(file_name) = current_path.file_name() {
4853            return file_name.to_string_lossy().into_owned();
4854        }
4855        match current_path.parent() {
4856            Some(parent) => current_path = parent,
4857            None => return path.to_string_lossy().into_owned(),
4858        }
4859    }
4860}
4861
4862impl Panel for OutlinePanel {
4863    fn persistent_name() -> &'static str {
4864        "Outline Panel"
4865    }
4866
4867    fn panel_key() -> &'static str {
4868        OUTLINE_PANEL_KEY
4869    }
4870
4871    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4872        match OutlinePanelSettings::get_global(cx).dock {
4873            DockSide::Left => DockPosition::Left,
4874            DockSide::Right => DockPosition::Right,
4875        }
4876    }
4877
4878    fn position_is_valid(&self, position: DockPosition) -> bool {
4879        matches!(position, DockPosition::Left | DockPosition::Right)
4880    }
4881
4882    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4883        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
4884            let dock = match position {
4885                DockPosition::Left | DockPosition::Bottom => DockSide::Left,
4886                DockPosition::Right => DockSide::Right,
4887            };
4888            settings.outline_panel.get_or_insert_default().dock = Some(dock);
4889        });
4890    }
4891
4892    fn default_size(&self, _: &Window, cx: &App) -> Pixels {
4893        OutlinePanelSettings::get_global(cx).default_width
4894    }
4895
4896    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4897        OutlinePanelSettings::get_global(cx)
4898            .button
4899            .then_some(IconName::ListTree)
4900    }
4901
4902    fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4903        Some("Outline Panel")
4904    }
4905
4906    fn toggle_action(&self) -> Box<dyn Action> {
4907        Box::new(ToggleFocus)
4908    }
4909
4910    fn starts_open(&self, _window: &Window, _: &App) -> bool {
4911        self.active
4912    }
4913
4914    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
4915        cx.spawn_in(window, async move |outline_panel, cx| {
4916            outline_panel
4917                .update_in(cx, |outline_panel, window, cx| {
4918                    let old_active = outline_panel.active;
4919                    outline_panel.active = active;
4920                    if old_active != active {
4921                        if active
4922                            && let Some((active_item, active_editor)) =
4923                                outline_panel.workspace.upgrade().and_then(|workspace| {
4924                                    workspace_active_editor(workspace.read(cx), cx)
4925                                })
4926                        {
4927                            if outline_panel.should_replace_active_item(active_item.as_ref()) {
4928                                outline_panel.replace_active_editor(
4929                                    active_item,
4930                                    active_editor,
4931                                    window,
4932                                    cx,
4933                                );
4934                            } else {
4935                                outline_panel.update_fs_entries(active_editor, None, window, cx)
4936                            }
4937                            return;
4938                        }
4939
4940                        if !outline_panel.pinned {
4941                            outline_panel.clear_previous(window, cx);
4942                        }
4943                    }
4944                    outline_panel.serialize(cx);
4945                })
4946                .ok();
4947        })
4948        .detach()
4949    }
4950
4951    fn activation_priority(&self) -> u32 {
4952        6
4953    }
4954}
4955
4956impl Focusable for OutlinePanel {
4957    fn focus_handle(&self, cx: &App) -> FocusHandle {
4958        self.filter_editor.focus_handle(cx)
4959    }
4960}
4961
4962impl EventEmitter<Event> for OutlinePanel {}
4963
4964impl EventEmitter<PanelEvent> for OutlinePanel {}
4965
4966impl Render for OutlinePanel {
4967    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4968        let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| {
4969            (project.is_local(), project.is_via_remote_server())
4970        });
4971        let query = self.query(cx);
4972        let pinned = self.pinned;
4973        let settings = OutlinePanelSettings::get_global(cx);
4974        let indent_size = settings.indent_size;
4975        let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
4976
4977        let search_query = match &self.mode {
4978            ItemsDisplayMode::Search(search_query) => Some(search_query),
4979            _ => None,
4980        };
4981
4982        let search_query_text = search_query.map(|sq| sq.query.to_string());
4983
4984        v_flex()
4985            .id("outline-panel")
4986            .size_full()
4987            .overflow_hidden()
4988            .relative()
4989            .key_context(self.dispatch_context(window, cx))
4990            .on_action(cx.listener(Self::open_selected_entry))
4991            .on_action(cx.listener(Self::cancel))
4992            .on_action(cx.listener(Self::scroll_up))
4993            .on_action(cx.listener(Self::scroll_down))
4994            .on_action(cx.listener(Self::select_next))
4995            .on_action(cx.listener(Self::scroll_cursor_center))
4996            .on_action(cx.listener(Self::scroll_cursor_top))
4997            .on_action(cx.listener(Self::scroll_cursor_bottom))
4998            .on_action(cx.listener(Self::select_previous))
4999            .on_action(cx.listener(Self::select_first))
5000            .on_action(cx.listener(Self::select_last))
5001            .on_action(cx.listener(Self::select_parent))
5002            .on_action(cx.listener(Self::expand_selected_entry))
5003            .on_action(cx.listener(Self::collapse_selected_entry))
5004            .on_action(cx.listener(Self::expand_all_entries))
5005            .on_action(cx.listener(Self::collapse_all_entries))
5006            .on_action(cx.listener(Self::copy_path))
5007            .on_action(cx.listener(Self::copy_relative_path))
5008            .on_action(cx.listener(Self::toggle_active_editor_pin))
5009            .on_action(cx.listener(Self::unfold_directory))
5010            .on_action(cx.listener(Self::fold_directory))
5011            .on_action(cx.listener(Self::open_excerpts))
5012            .on_action(cx.listener(Self::open_excerpts_split))
5013            .when(is_local, |el| {
5014                el.on_action(cx.listener(Self::reveal_in_finder))
5015            })
5016            .when(is_local || is_via_ssh, |el| {
5017                el.on_action(cx.listener(Self::open_in_terminal))
5018            })
5019            .on_mouse_down(
5020                MouseButton::Right,
5021                cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
5022                    if let Some(entry) = outline_panel.selected_entry().cloned() {
5023                        outline_panel.deploy_context_menu(event.position, entry, window, cx)
5024                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
5025                        outline_panel.deploy_context_menu(
5026                            event.position,
5027                            PanelEntry::Fs(entry),
5028                            window,
5029                            cx,
5030                        )
5031                    }
5032                }),
5033            )
5034            .track_focus(&self.focus_handle)
5035            .child(self.render_filter_footer(pinned, cx))
5036            .when_some(search_query_text, |outline_panel, query_text| {
5037                outline_panel.child(
5038                    h_flex()
5039                        .py_1p5()
5040                        .px_2()
5041                        .h(Tab::container_height(cx))
5042                        .gap_0p5()
5043                        .border_b_1()
5044                        .border_color(cx.theme().colors().border_variant)
5045                        .child(Label::new("Searching:").color(Color::Muted))
5046                        .child(Label::new(query_text)),
5047                )
5048            })
5049            .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
5050    }
5051}
5052
5053fn find_active_indent_guide_ix(
5054    outline_panel: &OutlinePanel,
5055    candidates: &[IndentGuideLayout],
5056) -> Option<usize> {
5057    let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
5058        return None;
5059    };
5060    let target_depth = outline_panel
5061        .cached_entries
5062        .get(*target_ix)
5063        .map(|cached_entry| cached_entry.depth)?;
5064
5065    let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
5066        .cached_entries
5067        .get(target_ix + 1)
5068        .filter(|cached_entry| cached_entry.depth > target_depth)
5069        .map(|entry| entry.depth)
5070    {
5071        (target_ix + 1, target_depth.saturating_sub(1))
5072    } else {
5073        (*target_ix, target_depth.saturating_sub(1))
5074    };
5075
5076    candidates
5077        .iter()
5078        .enumerate()
5079        .find(|(_, guide)| {
5080            guide.offset.y <= target_ix
5081                && target_ix < guide.offset.y + guide.length
5082                && guide.offset.x == target_depth
5083        })
5084        .map(|(ix, _)| ix)
5085}
5086
5087fn subscribe_for_editor_events(
5088    editor: &Entity<Editor>,
5089    window: &mut Window,
5090    cx: &mut Context<OutlinePanel>,
5091) -> Subscription {
5092    let debounce = Some(UPDATE_DEBOUNCE);
5093    cx.subscribe_in(
5094        editor,
5095        window,
5096        move |outline_panel, editor, e: &EditorEvent, window, cx| {
5097            if !outline_panel.active {
5098                return;
5099            }
5100            match e {
5101                EditorEvent::SelectionsChanged { local: true } => {
5102                    outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5103                    cx.notify();
5104                }
5105                EditorEvent::BuffersRemoved { removed_buffer_ids } => {
5106                    outline_panel
5107                        .buffers
5108                        .retain(|buffer_id, _| !removed_buffer_ids.contains(buffer_id));
5109                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5110                }
5111                EditorEvent::BufferRangesUpdated { buffer, .. } => {
5112                    outline_panel
5113                        .new_entries_for_fs_update
5114                        .insert(buffer.read(cx).remote_id());
5115                    outline_panel.invalidate_outlines(&[buffer.read(cx).remote_id()]);
5116                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5117                }
5118                EditorEvent::BuffersEdited { buffer_ids } => {
5119                    outline_panel.invalidate_outlines(buffer_ids);
5120                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5121                    if update_cached_items {
5122                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5123                    }
5124                }
5125                EditorEvent::BufferFoldToggled { ids, .. } => {
5126                    outline_panel.invalidate_outlines(ids);
5127                    let mut latest_unfolded_buffer_id = None;
5128                    let mut latest_folded_buffer_id = None;
5129                    let mut ignore_selections_change = false;
5130                    outline_panel.new_entries_for_fs_update.extend(
5131                        ids.iter()
5132                            .filter(|id| {
5133                                if outline_panel.buffers.contains_key(&id) {
5134                                    ignore_selections_change |= outline_panel
5135                                        .preserve_selection_on_buffer_fold_toggles
5136                                        .remove(&id);
5137                                    if editor.read(cx).is_buffer_folded(**id, cx) {
5138                                        latest_folded_buffer_id = Some(**id);
5139                                        false
5140                                    } else {
5141                                        latest_unfolded_buffer_id = Some(**id);
5142                                        true
5143                                    }
5144                                } else {
5145                                    false
5146                                }
5147                            })
5148                            .copied(),
5149                    );
5150                    if !ignore_selections_change
5151                        && let Some(entry_to_select) = latest_unfolded_buffer_id
5152                            .or(latest_folded_buffer_id)
5153                            .and_then(|toggled_buffer_id| {
5154                                outline_panel.fs_entries.iter().find_map(
5155                                    |fs_entry| match fs_entry {
5156                                        FsEntry::ExternalFile(external) => {
5157                                            if external.buffer_id == toggled_buffer_id {
5158                                                Some(fs_entry.clone())
5159                                            } else {
5160                                                None
5161                                            }
5162                                        }
5163                                        FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5164                                            if *buffer_id == toggled_buffer_id {
5165                                                Some(fs_entry.clone())
5166                                            } else {
5167                                                None
5168                                            }
5169                                        }
5170                                        FsEntry::Directory(..) => None,
5171                                    },
5172                                )
5173                            })
5174                            .map(PanelEntry::Fs)
5175                    {
5176                        outline_panel.select_entry(entry_to_select, true, window, cx);
5177                    }
5178
5179                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5180                }
5181                EditorEvent::Reparsed(buffer_id) => {
5182                    if let Some(buffer) = outline_panel.buffers.get_mut(buffer_id) {
5183                        buffer.invalidate_outlines();
5184                    }
5185                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5186                    if update_cached_items {
5187                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5188                    }
5189                }
5190                EditorEvent::OutlineSymbolsChanged => {
5191                    for buffer in outline_panel.buffers.values_mut() {
5192                        buffer.invalidate_outlines();
5193                    }
5194                    if matches!(
5195                        outline_panel.selected_entry(),
5196                        Some(PanelEntry::Outline(..)),
5197                    ) {
5198                        outline_panel.selected_entry.invalidate();
5199                    }
5200                    if outline_panel.update_non_fs_items(window, cx) {
5201                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5202                    }
5203                }
5204                EditorEvent::TitleChanged => {
5205                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5206                }
5207                _ => {}
5208            }
5209        },
5210    )
5211}
5212
5213fn empty_icon() -> AnyElement {
5214    h_flex()
5215        .size(IconSize::default().rems())
5216        .invisible()
5217        .flex_none()
5218        .into_any_element()
5219}
5220
5221#[derive(Debug, Default)]
5222struct GenerationState {
5223    entries: Vec<CachedEntry>,
5224    match_candidates: Vec<StringMatchCandidate>,
5225    max_width_estimate_and_index: Option<(u64, usize)>,
5226}
5227
5228impl GenerationState {
5229    fn clear(&mut self) {
5230        self.entries.clear();
5231        self.match_candidates.clear();
5232        self.max_width_estimate_and_index = None;
5233    }
5234}
5235
5236#[cfg(test)]
5237mod tests {
5238    use db::indoc;
5239    use gpui::{TestAppContext, UpdateGlobal, VisualTestContext, WindowHandle};
5240    use language::{self, FakeLspAdapter, markdown_lang, rust_lang};
5241    use pretty_assertions::assert_eq;
5242    use project::FakeFs;
5243    use search::{
5244        buffer_search,
5245        project_search::{self, perform_project_search},
5246    };
5247    use serde_json::json;
5248    use smol::stream::StreamExt as _;
5249    use util::path;
5250    use workspace::{MultiWorkspace, OpenOptions, OpenVisible, ToolbarItemView};
5251
5252    use super::*;
5253
5254    const SELECTED_MARKER: &str = "  <==== selected";
5255
5256    #[gpui::test(iterations = 10)]
5257    async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5258        init_test(cx);
5259
5260        let fs = FakeFs::new(cx.background_executor.clone());
5261        let root = path!("/rust-analyzer");
5262        populate_with_test_ra_project(&fs, root).await;
5263        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5264        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5265        let (window, workspace) = add_outline_panel(&project, cx).await;
5266        let cx = &mut VisualTestContext::from_window(window.into(), cx);
5267        let outline_panel = outline_panel(&workspace, cx);
5268        outline_panel.update_in(cx, |outline_panel, window, cx| {
5269            outline_panel.set_active(true, window, cx)
5270        });
5271
5272        workspace.update_in(cx, |workspace, window, cx| {
5273            ProjectSearchView::deploy_search(
5274                workspace,
5275                &workspace::DeploySearch::default(),
5276                window,
5277                cx,
5278            )
5279        });
5280        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
5281            workspace
5282                .active_pane()
5283                .read(cx)
5284                .items()
5285                .find_map(|item| item.downcast::<ProjectSearchView>())
5286                .expect("Project search view expected to appear after new search event trigger")
5287        });
5288
5289        let query = "param_names_for_lifetime_elision_hints";
5290        perform_project_search(&search_view, query, cx);
5291        search_view.update(cx, |search_view, cx| {
5292            search_view
5293                .results_editor()
5294                .update(cx, |results_editor, cx| {
5295                    assert_eq!(
5296                        results_editor.display_text(cx).match_indices(query).count(),
5297                        9
5298                    );
5299                });
5300        });
5301
5302        let all_matches = r#"rust-analyzer/
5303  crates/
5304    ide/src/
5305      inlay_hints/
5306        fn_lifetime_fn.rs
5307          search: match config.«param_names_for_lifetime_elision_hints» {
5308          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5309          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5310          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5311      inlay_hints.rs
5312        search: pub «param_names_for_lifetime_elision_hints»: bool,
5313        search: «param_names_for_lifetime_elision_hints»: self
5314      static_index.rs
5315        search: «param_names_for_lifetime_elision_hints»: false,
5316    rust-analyzer/src/
5317      cli/
5318        analysis_stats.rs
5319          search: «param_names_for_lifetime_elision_hints»: true,
5320      config.rs
5321        search: «param_names_for_lifetime_elision_hints»: self"#
5322            .to_string();
5323
5324        let select_first_in_all_matches = |line_to_select: &str| {
5325            assert!(
5326                all_matches.contains(line_to_select),
5327                "`{line_to_select}` was not found in all matches `{all_matches}`"
5328            );
5329            all_matches.replacen(
5330                line_to_select,
5331                &format!("{line_to_select}{SELECTED_MARKER}"),
5332                1,
5333            )
5334        };
5335
5336        cx.executor()
5337            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5338        cx.run_until_parked();
5339        outline_panel.update(cx, |outline_panel, cx| {
5340            assert_eq!(
5341                display_entries(
5342                    &project,
5343                    &snapshot(outline_panel, cx),
5344                    &outline_panel.cached_entries,
5345                    outline_panel.selected_entry(),
5346                    cx,
5347                ),
5348                select_first_in_all_matches(
5349                    "search: match config.«param_names_for_lifetime_elision_hints» {"
5350                )
5351            );
5352        });
5353
5354        outline_panel.update_in(cx, |outline_panel, window, cx| {
5355            outline_panel.select_parent(&SelectParent, window, cx);
5356            assert_eq!(
5357                display_entries(
5358                    &project,
5359                    &snapshot(outline_panel, cx),
5360                    &outline_panel.cached_entries,
5361                    outline_panel.selected_entry(),
5362                    cx,
5363                ),
5364                select_first_in_all_matches("fn_lifetime_fn.rs")
5365            );
5366        });
5367        outline_panel.update_in(cx, |outline_panel, window, cx| {
5368            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5369        });
5370        cx.executor()
5371            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5372        cx.run_until_parked();
5373        outline_panel.update(cx, |outline_panel, cx| {
5374            assert_eq!(
5375                display_entries(
5376                    &project,
5377                    &snapshot(outline_panel, cx),
5378                    &outline_panel.cached_entries,
5379                    outline_panel.selected_entry(),
5380                    cx,
5381                ),
5382                format!(
5383                    r#"rust-analyzer/
5384  crates/
5385    ide/src/
5386      inlay_hints/
5387        fn_lifetime_fn.rs{SELECTED_MARKER}
5388      inlay_hints.rs
5389        search: pub «param_names_for_lifetime_elision_hints»: bool,
5390        search: «param_names_for_lifetime_elision_hints»: self
5391      static_index.rs
5392        search: «param_names_for_lifetime_elision_hints»: false,
5393    rust-analyzer/src/
5394      cli/
5395        analysis_stats.rs
5396          search: «param_names_for_lifetime_elision_hints»: true,
5397      config.rs
5398        search: «param_names_for_lifetime_elision_hints»: self"#,
5399                )
5400            );
5401        });
5402
5403        outline_panel.update_in(cx, |outline_panel, window, cx| {
5404            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5405        });
5406        cx.executor()
5407            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5408        cx.run_until_parked();
5409        outline_panel.update_in(cx, |outline_panel, window, cx| {
5410            outline_panel.select_parent(&SelectParent, window, cx);
5411            assert_eq!(
5412                display_entries(
5413                    &project,
5414                    &snapshot(outline_panel, cx),
5415                    &outline_panel.cached_entries,
5416                    outline_panel.selected_entry(),
5417                    cx,
5418                ),
5419                select_first_in_all_matches("inlay_hints/")
5420            );
5421        });
5422
5423        outline_panel.update_in(cx, |outline_panel, window, cx| {
5424            outline_panel.select_parent(&SelectParent, window, cx);
5425            assert_eq!(
5426                display_entries(
5427                    &project,
5428                    &snapshot(outline_panel, cx),
5429                    &outline_panel.cached_entries,
5430                    outline_panel.selected_entry(),
5431                    cx,
5432                ),
5433                select_first_in_all_matches("ide/src/")
5434            );
5435        });
5436
5437        outline_panel.update_in(cx, |outline_panel, window, cx| {
5438            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5439        });
5440        cx.executor()
5441            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5442        cx.run_until_parked();
5443        outline_panel.update(cx, |outline_panel, cx| {
5444            assert_eq!(
5445                display_entries(
5446                    &project,
5447                    &snapshot(outline_panel, cx),
5448                    &outline_panel.cached_entries,
5449                    outline_panel.selected_entry(),
5450                    cx,
5451                ),
5452                format!(
5453                    r#"rust-analyzer/
5454  crates/
5455    ide/src/{SELECTED_MARKER}
5456    rust-analyzer/src/
5457      cli/
5458        analysis_stats.rs
5459          search: «param_names_for_lifetime_elision_hints»: true,
5460      config.rs
5461        search: «param_names_for_lifetime_elision_hints»: self"#,
5462                )
5463            );
5464        });
5465        outline_panel.update_in(cx, |outline_panel, window, cx| {
5466            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5467        });
5468        cx.executor()
5469            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5470        cx.run_until_parked();
5471        outline_panel.update(cx, |outline_panel, cx| {
5472            assert_eq!(
5473                display_entries(
5474                    &project,
5475                    &snapshot(outline_panel, cx),
5476                    &outline_panel.cached_entries,
5477                    outline_panel.selected_entry(),
5478                    cx,
5479                ),
5480                select_first_in_all_matches("ide/src/")
5481            );
5482        });
5483    }
5484
5485    #[gpui::test(iterations = 10)]
5486    async fn test_item_filtering(cx: &mut TestAppContext) {
5487        init_test(cx);
5488
5489        let fs = FakeFs::new(cx.background_executor.clone());
5490        let root = path!("/rust-analyzer");
5491        populate_with_test_ra_project(&fs, root).await;
5492        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5493        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5494        let (window, workspace) = add_outline_panel(&project, cx).await;
5495        let cx = &mut VisualTestContext::from_window(window.into(), cx);
5496        let outline_panel = outline_panel(&workspace, cx);
5497        outline_panel.update_in(cx, |outline_panel, window, cx| {
5498            outline_panel.set_active(true, window, cx)
5499        });
5500
5501        workspace.update_in(cx, |workspace, window, cx| {
5502            ProjectSearchView::deploy_search(
5503                workspace,
5504                &workspace::DeploySearch::default(),
5505                window,
5506                cx,
5507            )
5508        });
5509        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
5510            workspace
5511                .active_pane()
5512                .read(cx)
5513                .items()
5514                .find_map(|item| item.downcast::<ProjectSearchView>())
5515                .expect("Project search view expected to appear after new search event trigger")
5516        });
5517
5518        let query = "param_names_for_lifetime_elision_hints";
5519        perform_project_search(&search_view, query, cx);
5520        search_view.update(cx, |search_view, cx| {
5521            search_view
5522                .results_editor()
5523                .update(cx, |results_editor, cx| {
5524                    assert_eq!(
5525                        results_editor.display_text(cx).match_indices(query).count(),
5526                        9
5527                    );
5528                });
5529        });
5530        let all_matches = r#"rust-analyzer/
5531  crates/
5532    ide/src/
5533      inlay_hints/
5534        fn_lifetime_fn.rs
5535          search: match config.«param_names_for_lifetime_elision_hints» {
5536          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5537          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5538          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5539      inlay_hints.rs
5540        search: pub «param_names_for_lifetime_elision_hints»: bool,
5541        search: «param_names_for_lifetime_elision_hints»: self
5542      static_index.rs
5543        search: «param_names_for_lifetime_elision_hints»: false,
5544    rust-analyzer/src/
5545      cli/
5546        analysis_stats.rs
5547          search: «param_names_for_lifetime_elision_hints»: true,
5548      config.rs
5549        search: «param_names_for_lifetime_elision_hints»: self"#
5550            .to_string();
5551
5552        cx.executor()
5553            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5554        cx.run_until_parked();
5555        outline_panel.update(cx, |outline_panel, cx| {
5556            assert_eq!(
5557                display_entries(
5558                    &project,
5559                    &snapshot(outline_panel, cx),
5560                    &outline_panel.cached_entries,
5561                    None,
5562                    cx,
5563                ),
5564                all_matches,
5565            );
5566        });
5567
5568        let filter_text = "a";
5569        outline_panel.update_in(cx, |outline_panel, window, cx| {
5570            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5571                filter_editor.set_text(filter_text, window, cx);
5572            });
5573        });
5574        cx.executor()
5575            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5576        cx.run_until_parked();
5577
5578        outline_panel.update(cx, |outline_panel, cx| {
5579            assert_eq!(
5580                display_entries(
5581                    &project,
5582                    &snapshot(outline_panel, cx),
5583                    &outline_panel.cached_entries,
5584                    None,
5585                    cx,
5586                ),
5587                all_matches
5588                    .lines()
5589                    .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5590                    .filter(|item| item.contains(filter_text))
5591                    .collect::<Vec<_>>()
5592                    .join("\n"),
5593            );
5594        });
5595
5596        outline_panel.update_in(cx, |outline_panel, window, cx| {
5597            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5598                filter_editor.set_text("", window, cx);
5599            });
5600        });
5601        cx.executor()
5602            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5603        cx.run_until_parked();
5604        outline_panel.update(cx, |outline_panel, cx| {
5605            assert_eq!(
5606                display_entries(
5607                    &project,
5608                    &snapshot(outline_panel, cx),
5609                    &outline_panel.cached_entries,
5610                    None,
5611                    cx,
5612                ),
5613                all_matches,
5614            );
5615        });
5616    }
5617
5618    #[gpui::test(iterations = 10)]
5619    async fn test_item_opening(cx: &mut TestAppContext) {
5620        init_test(cx);
5621
5622        let fs = FakeFs::new(cx.background_executor.clone());
5623        let root = path!("/rust-analyzer");
5624        populate_with_test_ra_project(&fs, root).await;
5625        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5626        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
5627        let (window, workspace) = add_outline_panel(&project, cx).await;
5628        let cx = &mut VisualTestContext::from_window(window.into(), cx);
5629        let outline_panel = outline_panel(&workspace, cx);
5630        outline_panel.update_in(cx, |outline_panel, window, cx| {
5631            outline_panel.set_active(true, window, cx)
5632        });
5633
5634        workspace.update_in(cx, |workspace, window, cx| {
5635            ProjectSearchView::deploy_search(
5636                workspace,
5637                &workspace::DeploySearch::default(),
5638                window,
5639                cx,
5640            )
5641        });
5642        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
5643            workspace
5644                .active_pane()
5645                .read(cx)
5646                .items()
5647                .find_map(|item| item.downcast::<ProjectSearchView>())
5648                .expect("Project search view expected to appear after new search event trigger")
5649        });
5650
5651        let query = "param_names_for_lifetime_elision_hints";
5652        perform_project_search(&search_view, query, cx);
5653        search_view.update(cx, |search_view, cx| {
5654            search_view
5655                .results_editor()
5656                .update(cx, |results_editor, cx| {
5657                    assert_eq!(
5658                        results_editor.display_text(cx).match_indices(query).count(),
5659                        9
5660                    );
5661                });
5662        });
5663        let all_matches = r#"rust-analyzer/
5664  crates/
5665    ide/src/
5666      inlay_hints/
5667        fn_lifetime_fn.rs
5668          search: match config.«param_names_for_lifetime_elision_hints» {
5669          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5670          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5671          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5672      inlay_hints.rs
5673        search: pub «param_names_for_lifetime_elision_hints»: bool,
5674        search: «param_names_for_lifetime_elision_hints»: self
5675      static_index.rs
5676        search: «param_names_for_lifetime_elision_hints»: false,
5677    rust-analyzer/src/
5678      cli/
5679        analysis_stats.rs
5680          search: «param_names_for_lifetime_elision_hints»: true,
5681      config.rs
5682        search: «param_names_for_lifetime_elision_hints»: self"#
5683            .to_string();
5684        let select_first_in_all_matches = |line_to_select: &str| {
5685            assert!(
5686                all_matches.contains(line_to_select),
5687                "`{line_to_select}` was not found in all matches `{all_matches}`"
5688            );
5689            all_matches.replacen(
5690                line_to_select,
5691                &format!("{line_to_select}{SELECTED_MARKER}"),
5692                1,
5693            )
5694        };
5695        let clear_outline_metadata = |input: &str| {
5696            input
5697                .replace("search: ", "")
5698                .replace("«", "")
5699                .replace("»", "")
5700        };
5701
5702        cx.executor()
5703            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5704        cx.run_until_parked();
5705
5706        let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5707            outline_panel
5708                .active_editor()
5709                .expect("should have an active editor open")
5710        });
5711        let initial_outline_selection =
5712            "search: match config.«param_names_for_lifetime_elision_hints» {";
5713        outline_panel.update_in(cx, |outline_panel, window, cx| {
5714            assert_eq!(
5715                display_entries(
5716                    &project,
5717                    &snapshot(outline_panel, cx),
5718                    &outline_panel.cached_entries,
5719                    outline_panel.selected_entry(),
5720                    cx,
5721                ),
5722                select_first_in_all_matches(initial_outline_selection)
5723            );
5724            assert_eq!(
5725                selected_row_text(&active_editor, cx),
5726                clear_outline_metadata(initial_outline_selection),
5727                "Should place the initial editor selection on the corresponding search result"
5728            );
5729
5730            outline_panel.select_next(&SelectNext, window, cx);
5731            outline_panel.select_next(&SelectNext, window, cx);
5732        });
5733
5734        let navigated_outline_selection =
5735            "search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {";
5736        outline_panel.update(cx, |outline_panel, cx| {
5737            assert_eq!(
5738                display_entries(
5739                    &project,
5740                    &snapshot(outline_panel, cx),
5741                    &outline_panel.cached_entries,
5742                    outline_panel.selected_entry(),
5743                    cx,
5744                ),
5745                select_first_in_all_matches(navigated_outline_selection)
5746            );
5747        });
5748        cx.executor()
5749            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5750        outline_panel.update(cx, |_, cx| {
5751            assert_eq!(
5752                selected_row_text(&active_editor, cx),
5753                clear_outline_metadata(navigated_outline_selection),
5754                "Should still have the initial caret position after SelectNext calls"
5755            );
5756        });
5757
5758        outline_panel.update_in(cx, |outline_panel, window, cx| {
5759            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5760        });
5761        outline_panel.update(cx, |_outline_panel, cx| {
5762            assert_eq!(
5763                selected_row_text(&active_editor, cx),
5764                clear_outline_metadata(navigated_outline_selection),
5765                "After opening, should move the caret to the opened outline entry's position"
5766            );
5767        });
5768
5769        outline_panel.update_in(cx, |outline_panel, window, cx| {
5770            outline_panel.select_next(&SelectNext, window, cx);
5771        });
5772        let next_navigated_outline_selection = "search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },";
5773        outline_panel.update(cx, |outline_panel, cx| {
5774            assert_eq!(
5775                display_entries(
5776                    &project,
5777                    &snapshot(outline_panel, cx),
5778                    &outline_panel.cached_entries,
5779                    outline_panel.selected_entry(),
5780                    cx,
5781                ),
5782                select_first_in_all_matches(next_navigated_outline_selection)
5783            );
5784        });
5785        cx.executor()
5786            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5787        outline_panel.update(cx, |_outline_panel, cx| {
5788            assert_eq!(
5789                selected_row_text(&active_editor, cx),
5790                clear_outline_metadata(next_navigated_outline_selection),
5791                "Should again preserve the selection after another SelectNext call"
5792            );
5793        });
5794
5795        outline_panel.update_in(cx, |outline_panel, window, cx| {
5796            outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx);
5797        });
5798        cx.executor()
5799            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5800        cx.run_until_parked();
5801        let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5802            outline_panel
5803                .active_editor()
5804                .expect("should have an active editor open")
5805        });
5806        outline_panel.update(cx, |outline_panel, cx| {
5807            assert_ne!(
5808                active_editor, new_active_editor,
5809                "After opening an excerpt, new editor should be open"
5810            );
5811            assert_eq!(
5812                display_entries(
5813                    &project,
5814                    &snapshot(outline_panel, cx),
5815                    &outline_panel.cached_entries,
5816                    outline_panel.selected_entry(),
5817                    cx,
5818                ),
5819                "outline: pub(super) fn hints
5820outline: fn hints_lifetimes_named  <==== selected"
5821            );
5822            assert_eq!(
5823                selected_row_text(&new_active_editor, cx),
5824                clear_outline_metadata(next_navigated_outline_selection),
5825                "When opening the excerpt, should navigate to the place corresponding the outline entry"
5826            );
5827        });
5828    }
5829
5830    #[gpui::test]
5831    async fn test_multiple_worktrees(cx: &mut TestAppContext) {
5832        init_test(cx);
5833
5834        let fs = FakeFs::new(cx.background_executor.clone());
5835        fs.insert_tree(
5836            path!("/root"),
5837            json!({
5838                "one": {
5839                    "a.txt": "aaa aaa"
5840                },
5841                "two": {
5842                    "b.txt": "a aaa"
5843                }
5844
5845            }),
5846        )
5847        .await;
5848        let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await;
5849        let (window, workspace) = add_outline_panel(&project, cx).await;
5850        let cx = &mut VisualTestContext::from_window(window.into(), cx);
5851        let outline_panel = outline_panel(&workspace, cx);
5852        outline_panel.update_in(cx, |outline_panel, window, cx| {
5853            outline_panel.set_active(true, window, cx)
5854        });
5855
5856        let items = workspace
5857            .update_in(cx, |workspace, window, cx| {
5858                workspace.open_paths(
5859                    vec![PathBuf::from(path!("/root/two"))],
5860                    OpenOptions {
5861                        visible: Some(OpenVisible::OnlyDirectories),
5862                        ..Default::default()
5863                    },
5864                    None,
5865                    window,
5866                    cx,
5867                )
5868            })
5869            .await;
5870        assert_eq!(items.len(), 1, "Were opening another worktree directory");
5871        assert!(
5872            items[0].is_none(),
5873            "Directory should be opened successfully"
5874        );
5875
5876        workspace.update_in(cx, |workspace, window, cx| {
5877            ProjectSearchView::deploy_search(
5878                workspace,
5879                &workspace::DeploySearch::default(),
5880                window,
5881                cx,
5882            )
5883        });
5884        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
5885            workspace
5886                .active_pane()
5887                .read(cx)
5888                .items()
5889                .find_map(|item| item.downcast::<ProjectSearchView>())
5890                .expect("Project search view expected to appear after new search event trigger")
5891        });
5892
5893        let query = "aaa";
5894        perform_project_search(&search_view, query, cx);
5895        search_view.update(cx, |search_view, cx| {
5896            search_view
5897                .results_editor()
5898                .update(cx, |results_editor, cx| {
5899                    assert_eq!(
5900                        results_editor.display_text(cx).match_indices(query).count(),
5901                        3
5902                    );
5903                });
5904        });
5905
5906        cx.executor()
5907            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5908        cx.run_until_parked();
5909        outline_panel.update(cx, |outline_panel, cx| {
5910            assert_eq!(
5911                display_entries(
5912                    &project,
5913                    &snapshot(outline_panel, cx),
5914                    &outline_panel.cached_entries,
5915                    outline_panel.selected_entry(),
5916                    cx,
5917                ),
5918                format!(
5919                    r#"one/
5920  a.txt
5921    search: «aaa» aaa  <==== selected
5922    search: aaa «aaa»
5923two/
5924  b.txt
5925    search: a «aaa»"#,
5926                ),
5927            );
5928        });
5929
5930        outline_panel.update_in(cx, |outline_panel, window, cx| {
5931            outline_panel.select_previous(&SelectPrevious, window, cx);
5932            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5933        });
5934        cx.executor()
5935            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5936        cx.run_until_parked();
5937        outline_panel.update(cx, |outline_panel, cx| {
5938            assert_eq!(
5939                display_entries(
5940                    &project,
5941                    &snapshot(outline_panel, cx),
5942                    &outline_panel.cached_entries,
5943                    outline_panel.selected_entry(),
5944                    cx,
5945                ),
5946                format!(
5947                    r#"one/
5948  a.txt  <==== selected
5949two/
5950  b.txt
5951    search: a «aaa»"#,
5952                ),
5953            );
5954        });
5955
5956        outline_panel.update_in(cx, |outline_panel, window, cx| {
5957            outline_panel.select_next(&SelectNext, window, cx);
5958            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5959        });
5960        cx.executor()
5961            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5962        cx.run_until_parked();
5963        outline_panel.update(cx, |outline_panel, cx| {
5964            assert_eq!(
5965                display_entries(
5966                    &project,
5967                    &snapshot(outline_panel, cx),
5968                    &outline_panel.cached_entries,
5969                    outline_panel.selected_entry(),
5970                    cx,
5971                ),
5972                format!(
5973                    r#"one/
5974  a.txt
5975two/  <==== selected"#,
5976                ),
5977            );
5978        });
5979
5980        outline_panel.update_in(cx, |outline_panel, window, cx| {
5981            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5982        });
5983        cx.executor()
5984            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5985        cx.run_until_parked();
5986        outline_panel.update(cx, |outline_panel, cx| {
5987            assert_eq!(
5988                display_entries(
5989                    &project,
5990                    &snapshot(outline_panel, cx),
5991                    &outline_panel.cached_entries,
5992                    outline_panel.selected_entry(),
5993                    cx,
5994                ),
5995                format!(
5996                    r#"one/
5997  a.txt
5998two/  <==== selected
5999  b.txt
6000    search: a «aaa»"#,
6001                )
6002            );
6003        });
6004    }
6005
6006    #[gpui::test]
6007    async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
6008        init_test(cx);
6009
6010        let root = path!("/root");
6011        let fs = FakeFs::new(cx.background_executor.clone());
6012        fs.insert_tree(
6013            root,
6014            json!({
6015                "src": {
6016                    "lib.rs": indoc!("
6017#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6018struct OutlineEntryExcerpt {
6019    id: ExcerptId,
6020    buffer_id: BufferId,
6021    range: ExcerptRange<language::Anchor>,
6022}"),
6023                }
6024            }),
6025        )
6026        .await;
6027        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6028        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
6029        let (window, workspace) = add_outline_panel(&project, cx).await;
6030        let cx = &mut VisualTestContext::from_window(window.into(), cx);
6031        let outline_panel = outline_panel(&workspace, cx);
6032        cx.update(|window, cx| {
6033            outline_panel.update(cx, |outline_panel, cx| {
6034                outline_panel.set_active(true, window, cx)
6035            });
6036        });
6037
6038        let _editor = workspace
6039            .update_in(cx, |workspace, window, cx| {
6040                workspace.open_abs_path(
6041                    PathBuf::from(path!("/root/src/lib.rs")),
6042                    OpenOptions {
6043                        visible: Some(OpenVisible::All),
6044                        ..Default::default()
6045                    },
6046                    window,
6047                    cx,
6048                )
6049            })
6050            .await
6051            .expect("Failed to open Rust source file")
6052            .downcast::<Editor>()
6053            .expect("Should open an editor for Rust source file");
6054
6055        cx.executor()
6056            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6057        cx.run_until_parked();
6058        outline_panel.update(cx, |outline_panel, cx| {
6059            assert_eq!(
6060                display_entries(
6061                    &project,
6062                    &snapshot(outline_panel, cx),
6063                    &outline_panel.cached_entries,
6064                    outline_panel.selected_entry(),
6065                    cx,
6066                ),
6067                indoc!(
6068                    "
6069outline: struct OutlineEntryExcerpt
6070  outline: id
6071  outline: buffer_id
6072  outline: range"
6073                )
6074            );
6075        });
6076
6077        cx.update(|window, cx| {
6078            outline_panel.update(cx, |outline_panel, cx| {
6079                outline_panel.select_next(&SelectNext, window, cx);
6080            });
6081        });
6082        cx.executor()
6083            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6084        cx.run_until_parked();
6085        outline_panel.update(cx, |outline_panel, cx| {
6086            assert_eq!(
6087                display_entries(
6088                    &project,
6089                    &snapshot(outline_panel, cx),
6090                    &outline_panel.cached_entries,
6091                    outline_panel.selected_entry(),
6092                    cx,
6093                ),
6094                indoc!(
6095                    "
6096outline: struct OutlineEntryExcerpt  <==== selected
6097  outline: id
6098  outline: buffer_id
6099  outline: range"
6100                )
6101            );
6102        });
6103
6104        cx.update(|window, cx| {
6105            outline_panel.update(cx, |outline_panel, cx| {
6106                outline_panel.select_next(&SelectNext, window, cx);
6107            });
6108        });
6109        cx.executor()
6110            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6111        cx.run_until_parked();
6112        outline_panel.update(cx, |outline_panel, cx| {
6113            assert_eq!(
6114                display_entries(
6115                    &project,
6116                    &snapshot(outline_panel, cx),
6117                    &outline_panel.cached_entries,
6118                    outline_panel.selected_entry(),
6119                    cx,
6120                ),
6121                indoc!(
6122                    "
6123outline: struct OutlineEntryExcerpt
6124  outline: id  <==== selected
6125  outline: buffer_id
6126  outline: range"
6127                )
6128            );
6129        });
6130
6131        cx.update(|window, cx| {
6132            outline_panel.update(cx, |outline_panel, cx| {
6133                outline_panel.select_next(&SelectNext, window, cx);
6134            });
6135        });
6136        cx.executor()
6137            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6138        cx.run_until_parked();
6139        outline_panel.update(cx, |outline_panel, cx| {
6140            assert_eq!(
6141                display_entries(
6142                    &project,
6143                    &snapshot(outline_panel, cx),
6144                    &outline_panel.cached_entries,
6145                    outline_panel.selected_entry(),
6146                    cx,
6147                ),
6148                indoc!(
6149                    "
6150outline: struct OutlineEntryExcerpt
6151  outline: id
6152  outline: buffer_id  <==== selected
6153  outline: range"
6154                )
6155            );
6156        });
6157
6158        cx.update(|window, cx| {
6159            outline_panel.update(cx, |outline_panel, cx| {
6160                outline_panel.select_next(&SelectNext, window, cx);
6161            });
6162        });
6163        cx.executor()
6164            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6165        cx.run_until_parked();
6166        outline_panel.update(cx, |outline_panel, cx| {
6167            assert_eq!(
6168                display_entries(
6169                    &project,
6170                    &snapshot(outline_panel, cx),
6171                    &outline_panel.cached_entries,
6172                    outline_panel.selected_entry(),
6173                    cx,
6174                ),
6175                indoc!(
6176                    "
6177outline: struct OutlineEntryExcerpt
6178  outline: id
6179  outline: buffer_id
6180  outline: range  <==== selected"
6181                )
6182            );
6183        });
6184
6185        cx.update(|window, cx| {
6186            outline_panel.update(cx, |outline_panel, cx| {
6187                outline_panel.select_next(&SelectNext, window, cx);
6188            });
6189        });
6190        cx.executor()
6191            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6192        cx.run_until_parked();
6193        outline_panel.update(cx, |outline_panel, cx| {
6194            assert_eq!(
6195                display_entries(
6196                    &project,
6197                    &snapshot(outline_panel, cx),
6198                    &outline_panel.cached_entries,
6199                    outline_panel.selected_entry(),
6200                    cx,
6201                ),
6202                indoc!(
6203                    "
6204outline: struct OutlineEntryExcerpt  <==== selected
6205  outline: id
6206  outline: buffer_id
6207  outline: range"
6208                )
6209            );
6210        });
6211
6212        cx.update(|window, cx| {
6213            outline_panel.update(cx, |outline_panel, cx| {
6214                outline_panel.select_previous(&SelectPrevious, window, cx);
6215            });
6216        });
6217        cx.executor()
6218            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6219        cx.run_until_parked();
6220        outline_panel.update(cx, |outline_panel, cx| {
6221            assert_eq!(
6222                display_entries(
6223                    &project,
6224                    &snapshot(outline_panel, cx),
6225                    &outline_panel.cached_entries,
6226                    outline_panel.selected_entry(),
6227                    cx,
6228                ),
6229                indoc!(
6230                    "
6231outline: struct OutlineEntryExcerpt
6232  outline: id
6233  outline: buffer_id
6234  outline: range  <==== selected"
6235                )
6236            );
6237        });
6238
6239        cx.update(|window, cx| {
6240            outline_panel.update(cx, |outline_panel, cx| {
6241                outline_panel.select_previous(&SelectPrevious, window, cx);
6242            });
6243        });
6244        cx.executor()
6245            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6246        cx.run_until_parked();
6247        outline_panel.update(cx, |outline_panel, cx| {
6248            assert_eq!(
6249                display_entries(
6250                    &project,
6251                    &snapshot(outline_panel, cx),
6252                    &outline_panel.cached_entries,
6253                    outline_panel.selected_entry(),
6254                    cx,
6255                ),
6256                indoc!(
6257                    "
6258outline: struct OutlineEntryExcerpt
6259  outline: id
6260  outline: buffer_id  <==== selected
6261  outline: range"
6262                )
6263            );
6264        });
6265
6266        cx.update(|window, cx| {
6267            outline_panel.update(cx, |outline_panel, cx| {
6268                outline_panel.select_previous(&SelectPrevious, window, cx);
6269            });
6270        });
6271        cx.executor()
6272            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6273        cx.run_until_parked();
6274        outline_panel.update(cx, |outline_panel, cx| {
6275            assert_eq!(
6276                display_entries(
6277                    &project,
6278                    &snapshot(outline_panel, cx),
6279                    &outline_panel.cached_entries,
6280                    outline_panel.selected_entry(),
6281                    cx,
6282                ),
6283                indoc!(
6284                    "
6285outline: struct OutlineEntryExcerpt
6286  outline: id  <==== selected
6287  outline: buffer_id
6288  outline: range"
6289                )
6290            );
6291        });
6292
6293        cx.update(|window, cx| {
6294            outline_panel.update(cx, |outline_panel, cx| {
6295                outline_panel.select_previous(&SelectPrevious, window, cx);
6296            });
6297        });
6298        cx.executor()
6299            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6300        cx.run_until_parked();
6301        outline_panel.update(cx, |outline_panel, cx| {
6302            assert_eq!(
6303                display_entries(
6304                    &project,
6305                    &snapshot(outline_panel, cx),
6306                    &outline_panel.cached_entries,
6307                    outline_panel.selected_entry(),
6308                    cx,
6309                ),
6310                indoc!(
6311                    "
6312outline: struct OutlineEntryExcerpt  <==== selected
6313  outline: id
6314  outline: buffer_id
6315  outline: range"
6316                )
6317            );
6318        });
6319
6320        cx.update(|window, cx| {
6321            outline_panel.update(cx, |outline_panel, cx| {
6322                outline_panel.select_previous(&SelectPrevious, window, cx);
6323            });
6324        });
6325        cx.executor()
6326            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6327        cx.run_until_parked();
6328        outline_panel.update(cx, |outline_panel, cx| {
6329            assert_eq!(
6330                display_entries(
6331                    &project,
6332                    &snapshot(outline_panel, cx),
6333                    &outline_panel.cached_entries,
6334                    outline_panel.selected_entry(),
6335                    cx,
6336                ),
6337                indoc!(
6338                    "
6339outline: struct OutlineEntryExcerpt
6340  outline: id
6341  outline: buffer_id
6342  outline: range  <==== selected"
6343                )
6344            );
6345        });
6346    }
6347
6348    #[gpui::test(iterations = 10)]
6349    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6350        init_test(cx);
6351
6352        let root = path!("/frontend-project");
6353        let fs = FakeFs::new(cx.background_executor.clone());
6354        fs.insert_tree(
6355            root,
6356            json!({
6357                "public": {
6358                    "lottie": {
6359                        "syntax-tree.json": r#"{ "something": "static" }"#
6360                    }
6361                },
6362                "src": {
6363                    "app": {
6364                        "(site)": {
6365                            "(about)": {
6366                                "jobs": {
6367                                    "[slug]": {
6368                                        "page.tsx": r#"static"#
6369                                    }
6370                                }
6371                            },
6372                            "(blog)": {
6373                                "post": {
6374                                    "[slug]": {
6375                                        "page.tsx": r#"static"#
6376                                    }
6377                                }
6378                            },
6379                        }
6380                    },
6381                    "components": {
6382                        "ErrorBoundary.tsx": r#"static"#,
6383                    }
6384                }
6385
6386            }),
6387        )
6388        .await;
6389        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6390        let (window, workspace) = add_outline_panel(&project, cx).await;
6391        let cx = &mut VisualTestContext::from_window(window.into(), cx);
6392        let outline_panel = outline_panel(&workspace, cx);
6393        outline_panel.update_in(cx, |outline_panel, window, cx| {
6394            outline_panel.set_active(true, window, cx)
6395        });
6396
6397        workspace.update_in(cx, |workspace, window, cx| {
6398            ProjectSearchView::deploy_search(
6399                workspace,
6400                &workspace::DeploySearch::default(),
6401                window,
6402                cx,
6403            )
6404        });
6405        let search_view = workspace.update_in(cx, |workspace, _window, cx| {
6406            workspace
6407                .active_pane()
6408                .read(cx)
6409                .items()
6410                .find_map(|item| item.downcast::<ProjectSearchView>())
6411                .expect("Project search view expected to appear after new search event trigger")
6412        });
6413
6414        let query = "static";
6415        perform_project_search(&search_view, query, cx);
6416        search_view.update(cx, |search_view, cx| {
6417            search_view
6418                .results_editor()
6419                .update(cx, |results_editor, cx| {
6420                    assert_eq!(
6421                        results_editor.display_text(cx).match_indices(query).count(),
6422                        4
6423                    );
6424                });
6425        });
6426
6427        cx.executor()
6428            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6429        cx.run_until_parked();
6430        outline_panel.update(cx, |outline_panel, cx| {
6431            assert_eq!(
6432                display_entries(
6433                    &project,
6434                    &snapshot(outline_panel, cx),
6435                    &outline_panel.cached_entries,
6436                    outline_panel.selected_entry(),
6437                    cx,
6438                ),
6439                format!(
6440                    r#"frontend-project/
6441  public/lottie/
6442    syntax-tree.json
6443      search: {{ "something": "«static»" }}  <==== selected
6444  src/
6445    app/(site)/
6446      (about)/jobs/[slug]/
6447        page.tsx
6448          search: «static»
6449      (blog)/post/[slug]/
6450        page.tsx
6451          search: «static»
6452    components/
6453      ErrorBoundary.tsx
6454        search: «static»"#
6455                )
6456            );
6457        });
6458
6459        outline_panel.update_in(cx, |outline_panel, window, cx| {
6460            // Move to 5th element in the list, 3 items down.
6461            for _ in 0..2 {
6462                outline_panel.select_next(&SelectNext, window, cx);
6463            }
6464            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6465        });
6466        cx.executor()
6467            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6468        cx.run_until_parked();
6469        outline_panel.update(cx, |outline_panel, cx| {
6470            assert_eq!(
6471                display_entries(
6472                    &project,
6473                    &snapshot(outline_panel, cx),
6474                    &outline_panel.cached_entries,
6475                    outline_panel.selected_entry(),
6476                    cx,
6477                ),
6478                format!(
6479                    r#"frontend-project/
6480  public/lottie/
6481    syntax-tree.json
6482      search: {{ "something": "«static»" }}
6483  src/
6484    app/(site)/  <==== selected
6485    components/
6486      ErrorBoundary.tsx
6487        search: «static»"#
6488                )
6489            );
6490        });
6491
6492        outline_panel.update_in(cx, |outline_panel, window, cx| {
6493            // Move to the next visible non-FS entry
6494            for _ in 0..3 {
6495                outline_panel.select_next(&SelectNext, window, cx);
6496            }
6497        });
6498        cx.run_until_parked();
6499        outline_panel.update(cx, |outline_panel, cx| {
6500            assert_eq!(
6501                display_entries(
6502                    &project,
6503                    &snapshot(outline_panel, cx),
6504                    &outline_panel.cached_entries,
6505                    outline_panel.selected_entry(),
6506                    cx,
6507                ),
6508                format!(
6509                    r#"frontend-project/
6510  public/lottie/
6511    syntax-tree.json
6512      search: {{ "something": "«static»" }}
6513  src/
6514    app/(site)/
6515    components/
6516      ErrorBoundary.tsx
6517        search: «static»  <==== selected"#
6518                )
6519            );
6520        });
6521
6522        outline_panel.update_in(cx, |outline_panel, window, cx| {
6523            outline_panel
6524                .active_editor()
6525                .expect("Should have an active editor")
6526                .update(cx, |editor, cx| {
6527                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6528                });
6529        });
6530        cx.executor()
6531            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6532        cx.run_until_parked();
6533        outline_panel.update(cx, |outline_panel, cx| {
6534            assert_eq!(
6535                display_entries(
6536                    &project,
6537                    &snapshot(outline_panel, cx),
6538                    &outline_panel.cached_entries,
6539                    outline_panel.selected_entry(),
6540                    cx,
6541                ),
6542                format!(
6543                    r#"frontend-project/
6544  public/lottie/
6545    syntax-tree.json
6546      search: {{ "something": "«static»" }}
6547  src/
6548    app/(site)/
6549    components/
6550      ErrorBoundary.tsx  <==== selected"#
6551                )
6552            );
6553        });
6554
6555        outline_panel.update_in(cx, |outline_panel, window, cx| {
6556            outline_panel
6557                .active_editor()
6558                .expect("Should have an active editor")
6559                .update(cx, |editor, cx| {
6560                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6561                });
6562        });
6563        cx.executor()
6564            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6565        cx.run_until_parked();
6566        outline_panel.update(cx, |outline_panel, cx| {
6567            assert_eq!(
6568                display_entries(
6569                    &project,
6570                    &snapshot(outline_panel, cx),
6571                    &outline_panel.cached_entries,
6572                    outline_panel.selected_entry(),
6573                    cx,
6574                ),
6575                format!(
6576                    r#"frontend-project/
6577  public/lottie/
6578    syntax-tree.json
6579      search: {{ "something": "«static»" }}
6580  src/
6581    app/(site)/
6582    components/
6583      ErrorBoundary.tsx  <==== selected
6584        search: «static»"#
6585                )
6586            );
6587        });
6588
6589        outline_panel.update_in(cx, |outline_panel, window, cx| {
6590            outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx);
6591        });
6592        cx.executor()
6593            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6594        cx.run_until_parked();
6595        outline_panel.update(cx, |outline_panel, cx| {
6596            assert_eq!(
6597                display_entries(
6598                    &project,
6599                    &snapshot(outline_panel, cx),
6600                    &outline_panel.cached_entries,
6601                    outline_panel.selected_entry(),
6602                    cx,
6603                ),
6604                format!(r#"frontend-project/"#)
6605            );
6606        });
6607
6608        outline_panel.update_in(cx, |outline_panel, window, cx| {
6609            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
6610        });
6611        cx.executor()
6612            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6613        cx.run_until_parked();
6614        outline_panel.update(cx, |outline_panel, cx| {
6615            assert_eq!(
6616                display_entries(
6617                    &project,
6618                    &snapshot(outline_panel, cx),
6619                    &outline_panel.cached_entries,
6620                    outline_panel.selected_entry(),
6621                    cx,
6622                ),
6623                format!(
6624                    r#"frontend-project/
6625  public/lottie/
6626    syntax-tree.json
6627      search: {{ "something": "«static»" }}
6628  src/
6629    app/(site)/
6630      (about)/jobs/[slug]/
6631        page.tsx
6632          search: «static»
6633      (blog)/post/[slug]/
6634        page.tsx
6635          search: «static»
6636    components/
6637      ErrorBoundary.tsx  <==== selected
6638        search: «static»"#
6639                )
6640            );
6641        });
6642    }
6643
6644    async fn add_outline_panel(
6645        project: &Entity<Project>,
6646        cx: &mut TestAppContext,
6647    ) -> (WindowHandle<MultiWorkspace>, Entity<Workspace>) {
6648        let window =
6649            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6650        let workspace = window
6651            .read_with(cx, |mw, _| mw.workspace().clone())
6652            .unwrap();
6653
6654        let workspace_weak = workspace.downgrade();
6655        let outline_panel = window
6656            .update(cx, |_, window, cx| {
6657                cx.spawn_in(window, async move |_this, cx| {
6658                    OutlinePanel::load(workspace_weak, cx.clone()).await
6659                })
6660            })
6661            .unwrap()
6662            .await
6663            .expect("Failed to load outline panel");
6664
6665        window
6666            .update(cx, |multi_workspace, window, cx| {
6667                multi_workspace.workspace().update(cx, |workspace, cx| {
6668                    workspace.add_panel(outline_panel, window, cx);
6669                });
6670            })
6671            .unwrap();
6672        (window, workspace)
6673    }
6674
6675    fn outline_panel(
6676        workspace: &Entity<Workspace>,
6677        cx: &mut VisualTestContext,
6678    ) -> Entity<OutlinePanel> {
6679        workspace.update_in(cx, |workspace, _window, cx| {
6680            workspace
6681                .panel::<OutlinePanel>(cx)
6682                .expect("no outline panel")
6683        })
6684    }
6685
6686    fn display_entries(
6687        project: &Entity<Project>,
6688        multi_buffer_snapshot: &MultiBufferSnapshot,
6689        cached_entries: &[CachedEntry],
6690        selected_entry: Option<&PanelEntry>,
6691        cx: &mut App,
6692    ) -> String {
6693        let project = project.read(cx);
6694        let mut display_string = String::new();
6695        for entry in cached_entries {
6696            if !display_string.is_empty() {
6697                display_string += "\n";
6698            }
6699            for _ in 0..entry.depth {
6700                display_string += "  ";
6701            }
6702            display_string += &match &entry.entry {
6703                PanelEntry::Fs(entry) => match entry {
6704                    FsEntry::ExternalFile(_) => {
6705                        panic!("Did not cover external files with tests")
6706                    }
6707                    FsEntry::Directory(directory) => {
6708                        let path = if let Some(worktree) = project
6709                            .worktree_for_id(directory.worktree_id, cx)
6710                            .filter(|worktree| {
6711                                worktree.read(cx).root_entry() == Some(&directory.entry.entry)
6712                            }) {
6713                            worktree
6714                                .read(cx)
6715                                .root_name()
6716                                .join(&directory.entry.path)
6717                                .as_unix_str()
6718                                .to_string()
6719                        } else {
6720                            directory
6721                                .entry
6722                                .path
6723                                .file_name()
6724                                .unwrap_or_default()
6725                                .to_string()
6726                        };
6727                        format!("{path}/")
6728                    }
6729                    FsEntry::File(file) => file
6730                        .entry
6731                        .path
6732                        .file_name()
6733                        .map(|name| name.to_string())
6734                        .unwrap_or_default(),
6735                },
6736                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6737                    .entries
6738                    .iter()
6739                    .filter_map(|dir| dir.path.file_name())
6740                    .map(|name| name.to_string() + "/")
6741                    .collect(),
6742                PanelEntry::Outline(outline_entry) => match outline_entry {
6743                    OutlineEntry::Excerpt(_) => continue,
6744                    OutlineEntry::Outline(outline_entry) => {
6745                        format!("outline: {}", outline_entry.text)
6746                    }
6747                },
6748                PanelEntry::Search(search_entry) => {
6749                    let search_data = search_entry.render_data.get_or_init(|| {
6750                        SearchData::new(&search_entry.match_range, multi_buffer_snapshot)
6751                    });
6752                    let mut search_result = String::new();
6753                    let mut last_end = 0;
6754                    for range in &search_data.search_match_indices {
6755                        search_result.push_str(&search_data.context_text[last_end..range.start]);
6756                        search_result.push('«');
6757                        search_result.push_str(&search_data.context_text[range.start..range.end]);
6758                        search_result.push('»');
6759                        last_end = range.end;
6760                    }
6761                    search_result.push_str(&search_data.context_text[last_end..]);
6762
6763                    format!("search: {search_result}")
6764                }
6765            };
6766
6767            if Some(&entry.entry) == selected_entry {
6768                display_string += SELECTED_MARKER;
6769            }
6770        }
6771        display_string
6772    }
6773
6774    fn init_test(cx: &mut TestAppContext) {
6775        cx.update(|cx| {
6776            let settings = SettingsStore::test(cx);
6777            cx.set_global(settings);
6778
6779            theme_settings::init(theme::LoadThemes::JustBase, cx);
6780
6781            editor::init(cx);
6782            project_search::init(cx);
6783            buffer_search::init(cx);
6784            super::init(cx);
6785        });
6786    }
6787
6788    // Based on https://github.com/rust-lang/rust-analyzer/
6789    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6790        fs.insert_tree(
6791            root,
6792            json!({
6793                    "crates": {
6794                        "ide": {
6795                            "src": {
6796                                "inlay_hints": {
6797                                    "fn_lifetime_fn.rs": r##"
6798        pub(super) fn hints(
6799            acc: &mut Vec<InlayHint>,
6800            config: &InlayHintsConfig,
6801            func: ast::Fn,
6802        ) -> Option<()> {
6803            // ... snip
6804
6805            let mut used_names: FxHashMap<SmolStr, usize> =
6806                match config.param_names_for_lifetime_elision_hints {
6807                    true => generic_param_list
6808                        .iter()
6809                        .flat_map(|gpl| gpl.lifetime_params())
6810                        .filter_map(|param| param.lifetime())
6811                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6812                        .collect(),
6813                    false => Default::default(),
6814                };
6815            {
6816                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6817                if self_param.is_some() && potential_lt_refs.next().is_some() {
6818                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6819                        // self can't be used as a lifetime, so no need to check for collisions
6820                        "'self".into()
6821                    } else {
6822                        gen_idx_name()
6823                    });
6824                }
6825                potential_lt_refs.for_each(|(name, ..)| {
6826                    let name = match name {
6827                        Some(it) if config.param_names_for_lifetime_elision_hints => {
6828                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
6829                                *c += 1;
6830                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6831                            } else {
6832                                used_names.insert(it.text().as_str().into(), 0);
6833                                SmolStr::from_iter(["\'", it.text().as_str()])
6834                            }
6835                        }
6836                        _ => gen_idx_name(),
6837                    };
6838                    allocated_lifetimes.push(name);
6839                });
6840            }
6841
6842            // ... snip
6843        }
6844
6845        // ... snip
6846
6847            #[test]
6848            fn hints_lifetimes_named() {
6849                check_with_config(
6850                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6851                    r#"
6852        fn nested_in<'named>(named: &        &X<      &()>) {}
6853        //          ^'named1, 'named2, 'named3, $
6854                                  //^'named1 ^'named2 ^'named3
6855        "#,
6856                );
6857            }
6858
6859        // ... snip
6860        "##,
6861                                },
6862                        "inlay_hints.rs": r#"
6863    #[derive(Clone, Debug, PartialEq, Eq)]
6864    pub struct InlayHintsConfig {
6865        // ... snip
6866        pub param_names_for_lifetime_elision_hints: bool,
6867        pub max_length: Option<usize>,
6868        // ... snip
6869    }
6870
6871    impl Config {
6872        pub fn inlay_hints(&self) -> InlayHintsConfig {
6873            InlayHintsConfig {
6874                // ... snip
6875                param_names_for_lifetime_elision_hints: self
6876                    .inlayHints_lifetimeElisionHints_useParameterNames()
6877                    .to_owned(),
6878                max_length: self.inlayHints_maxLength().to_owned(),
6879                // ... snip
6880            }
6881        }
6882    }
6883    "#,
6884                        "static_index.rs": r#"
6885// ... snip
6886        fn add_file(&mut self, file_id: FileId) {
6887            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6888            let folds = self.analysis.folding_ranges(file_id).unwrap();
6889            let inlay_hints = self
6890                .analysis
6891                .inlay_hints(
6892                    &InlayHintsConfig {
6893                        // ... snip
6894                        closure_style: hir::ClosureStyle::ImplFn,
6895                        param_names_for_lifetime_elision_hints: false,
6896                        binding_mode_hints: false,
6897                        max_length: Some(25),
6898                        closure_capture_hints: false,
6899                        // ... snip
6900                    },
6901                    file_id,
6902                    None,
6903                )
6904                .unwrap();
6905            // ... snip
6906    }
6907// ... snip
6908    "#
6909                            }
6910                        },
6911                        "rust-analyzer": {
6912                            "src": {
6913                                "cli": {
6914                                    "analysis_stats.rs": r#"
6915        // ... snip
6916                for &file_id in &file_ids {
6917                    _ = analysis.inlay_hints(
6918                        &InlayHintsConfig {
6919                            // ... snip
6920                            implicit_drop_hints: true,
6921                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6922                            param_names_for_lifetime_elision_hints: true,
6923                            hide_named_constructor_hints: false,
6924                            hide_closure_initialization_hints: false,
6925                            closure_style: hir::ClosureStyle::ImplFn,
6926                            max_length: Some(25),
6927                            closing_brace_hints_min_lines: Some(20),
6928                            fields_to_resolve: InlayFieldsToResolve::empty(),
6929                            range_exclusive_hints: true,
6930                        },
6931                        file_id.into(),
6932                        None,
6933                    );
6934                }
6935        // ... snip
6936                                    "#,
6937                                },
6938                                "config.rs": r#"
6939                config_data! {
6940                    /// Configs that only make sense when they are set by a client. As such they can only be defined
6941                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
6942                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6943                        // ... snip
6944                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
6945                        inlayHints_maxLength: Option<usize>                        = Some(25),
6946                        // ... snip
6947                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6948                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
6949                        // ... snip
6950                    }
6951                }
6952
6953                impl Config {
6954                    // ... snip
6955                    pub fn inlay_hints(&self) -> InlayHintsConfig {
6956                        InlayHintsConfig {
6957                            // ... snip
6958                            param_names_for_lifetime_elision_hints: self
6959                                .inlayHints_lifetimeElisionHints_useParameterNames()
6960                                .to_owned(),
6961                            max_length: self.inlayHints_maxLength().to_owned(),
6962                            // ... snip
6963                        }
6964                    }
6965                    // ... snip
6966                }
6967                "#
6968                                }
6969                        }
6970                    }
6971            }),
6972        )
6973        .await;
6974    }
6975
6976    fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
6977        outline_panel
6978            .active_editor()
6979            .unwrap()
6980            .read(cx)
6981            .buffer()
6982            .read(cx)
6983            .snapshot(cx)
6984    }
6985
6986    fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
6987        editor.update(cx, |editor, cx| {
6988            let selections = editor.selections.all::<language::Point>(&editor.display_snapshot(cx));
6989            assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
6990            let selection = selections.first().unwrap();
6991            let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
6992            let line_start = language::Point::new(selection.start.row, 0);
6993            let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
6994            multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
6995        })
6996    }
6997
6998    #[gpui::test]
6999    async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) {
7000        init_test(cx);
7001
7002        let fs = FakeFs::new(cx.background_executor.clone());
7003        fs.insert_tree(
7004            "/test",
7005            json!({
7006                "src": {
7007                    "lib.rs": indoc!("
7008                            mod outer {
7009                                pub struct OuterStruct {
7010                                    field: String,
7011                                }
7012                                impl OuterStruct {
7013                                    pub fn new() -> Self {
7014                                        Self { field: String::new() }
7015                                    }
7016                                    pub fn method(&self) {
7017                                        println!(\"{}\", self.field);
7018                                    }
7019                                }
7020                                mod inner {
7021                                    pub fn inner_function() {
7022                                        let x = 42;
7023                                        println!(\"{}\", x);
7024                                    }
7025                                    pub struct InnerStruct {
7026                                        value: i32,
7027                                    }
7028                                }
7029                            }
7030                            fn main() {
7031                                let s = outer::OuterStruct::new();
7032                                s.method();
7033                            }
7034                        "),
7035                }
7036            }),
7037        )
7038        .await;
7039
7040        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7041        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7042        let (window, workspace) = add_outline_panel(&project, cx).await;
7043        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7044        let outline_panel = outline_panel(&workspace, cx);
7045
7046        outline_panel.update_in(cx, |outline_panel, window, cx| {
7047            outline_panel.set_active(true, window, cx)
7048        });
7049
7050        workspace
7051            .update_in(cx, |workspace, window, cx| {
7052                workspace.open_abs_path(
7053                    PathBuf::from("/test/src/lib.rs"),
7054                    OpenOptions {
7055                        visible: Some(OpenVisible::All),
7056                        ..Default::default()
7057                    },
7058                    window,
7059                    cx,
7060                )
7061            })
7062            .await
7063            .unwrap();
7064
7065        cx.executor()
7066            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7067        cx.run_until_parked();
7068
7069        // Force another update cycle to ensure outlines are fetched
7070        outline_panel.update_in(cx, |panel, window, cx| {
7071            panel.update_non_fs_items(window, cx);
7072            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7073        });
7074        cx.executor()
7075            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7076        cx.run_until_parked();
7077
7078        outline_panel.update(cx, |outline_panel, cx| {
7079            assert_eq!(
7080                display_entries(
7081                    &project,
7082                    &snapshot(outline_panel, cx),
7083                    &outline_panel.cached_entries,
7084                    outline_panel.selected_entry(),
7085                    cx,
7086                ),
7087                indoc!(
7088                    "
7089outline: mod outer  <==== selected
7090  outline: pub struct OuterStruct
7091    outline: field
7092  outline: impl OuterStruct
7093    outline: pub fn new
7094    outline: pub fn method
7095  outline: mod inner
7096    outline: pub fn inner_function
7097    outline: pub struct InnerStruct
7098      outline: value
7099outline: fn main"
7100                )
7101            );
7102        });
7103
7104        let parent_outline = outline_panel
7105            .read_with(cx, |panel, _cx| {
7106                panel
7107                    .cached_entries
7108                    .iter()
7109                    .find_map(|entry| match &entry.entry {
7110                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7111                            if panel
7112                                .outline_children_cache
7113                                .get(&outline.range.start.buffer_id)
7114                                .and_then(|children_map| {
7115                                    let key = (outline.range.clone(), outline.depth);
7116                                    children_map.get(&key)
7117                                })
7118                                .copied()
7119                                .unwrap_or(false) =>
7120                        {
7121                            Some(entry.entry.clone())
7122                        }
7123                        _ => None,
7124                    })
7125            })
7126            .expect("Should find an outline with children");
7127
7128        outline_panel.update_in(cx, |panel, window, cx| {
7129            panel.select_entry(parent_outline.clone(), true, window, cx);
7130            panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7131        });
7132        cx.executor()
7133            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7134        cx.run_until_parked();
7135
7136        outline_panel.update(cx, |outline_panel, cx| {
7137            assert_eq!(
7138                display_entries(
7139                    &project,
7140                    &snapshot(outline_panel, cx),
7141                    &outline_panel.cached_entries,
7142                    outline_panel.selected_entry(),
7143                    cx,
7144                ),
7145                indoc!(
7146                    "
7147outline: mod outer  <==== selected
7148outline: fn main"
7149                )
7150            );
7151        });
7152
7153        outline_panel.update_in(cx, |panel, window, cx| {
7154            panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7155        });
7156        cx.executor()
7157            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7158        cx.run_until_parked();
7159
7160        outline_panel.update(cx, |outline_panel, cx| {
7161            assert_eq!(
7162                display_entries(
7163                    &project,
7164                    &snapshot(outline_panel, cx),
7165                    &outline_panel.cached_entries,
7166                    outline_panel.selected_entry(),
7167                    cx,
7168                ),
7169                indoc!(
7170                    "
7171outline: mod outer  <==== selected
7172  outline: pub struct OuterStruct
7173    outline: field
7174  outline: impl OuterStruct
7175    outline: pub fn new
7176    outline: pub fn method
7177  outline: mod inner
7178    outline: pub fn inner_function
7179    outline: pub struct InnerStruct
7180      outline: value
7181outline: fn main"
7182                )
7183            );
7184        });
7185
7186        outline_panel.update_in(cx, |panel, window, cx| {
7187            panel.collapsed_entries.clear();
7188            panel.update_cached_entries(None, window, cx);
7189        });
7190        cx.executor()
7191            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7192        cx.run_until_parked();
7193
7194        outline_panel.update_in(cx, |panel, window, cx| {
7195            let outlines_with_children: Vec<_> = panel
7196                .cached_entries
7197                .iter()
7198                .filter_map(|entry| match &entry.entry {
7199                    PanelEntry::Outline(OutlineEntry::Outline(outline))
7200                        if panel
7201                            .outline_children_cache
7202                            .get(&outline.range.start.buffer_id)
7203                            .and_then(|children_map| {
7204                                let key = (outline.range.clone(), outline.depth);
7205                                children_map.get(&key)
7206                            })
7207                            .copied()
7208                            .unwrap_or(false) =>
7209                    {
7210                        Some(entry.entry.clone())
7211                    }
7212                    _ => None,
7213                })
7214                .collect();
7215
7216            for outline in outlines_with_children {
7217                panel.select_entry(outline, false, window, cx);
7218                panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7219            }
7220        });
7221        cx.executor()
7222            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7223        cx.run_until_parked();
7224
7225        outline_panel.update(cx, |outline_panel, cx| {
7226            assert_eq!(
7227                display_entries(
7228                    &project,
7229                    &snapshot(outline_panel, cx),
7230                    &outline_panel.cached_entries,
7231                    outline_panel.selected_entry(),
7232                    cx,
7233                ),
7234                indoc!(
7235                    "
7236outline: mod outer
7237outline: fn main"
7238                )
7239            );
7240        });
7241
7242        let collapsed_entries_count =
7243            outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len());
7244        assert!(
7245            collapsed_entries_count > 0,
7246            "Should have collapsed entries tracked"
7247        );
7248    }
7249
7250    #[gpui::test]
7251    async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) {
7252        init_test(cx);
7253
7254        let fs = FakeFs::new(cx.background_executor.clone());
7255        fs.insert_tree(
7256            "/test",
7257            json!({
7258                "src": {
7259                    "main.rs": indoc!("
7260                            struct Config {
7261                                name: String,
7262                                value: i32,
7263                            }
7264                            impl Config {
7265                                fn new(name: String) -> Self {
7266                                    Self { name, value: 0 }
7267                                }
7268                                fn get_value(&self) -> i32 {
7269                                    self.value
7270                                }
7271                            }
7272                            enum Status {
7273                                Active,
7274                                Inactive,
7275                            }
7276                            fn process_config(config: Config) -> Status {
7277                                if config.get_value() > 0 {
7278                                    Status::Active
7279                                } else {
7280                                    Status::Inactive
7281                                }
7282                            }
7283                            fn main() {
7284                                let config = Config::new(\"test\".to_string());
7285                                let status = process_config(config);
7286                            }
7287                        "),
7288                }
7289            }),
7290        )
7291        .await;
7292
7293        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7294        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7295
7296        let (window, workspace) = add_outline_panel(&project, cx).await;
7297        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7298        let outline_panel = outline_panel(&workspace, cx);
7299
7300        outline_panel.update_in(cx, |outline_panel, window, cx| {
7301            outline_panel.set_active(true, window, cx)
7302        });
7303
7304        let _editor = workspace
7305            .update_in(cx, |workspace, window, cx| {
7306                workspace.open_abs_path(
7307                    PathBuf::from("/test/src/main.rs"),
7308                    OpenOptions {
7309                        visible: Some(OpenVisible::All),
7310                        ..Default::default()
7311                    },
7312                    window,
7313                    cx,
7314                )
7315            })
7316            .await
7317            .unwrap();
7318
7319        cx.executor()
7320            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7321        cx.run_until_parked();
7322
7323        outline_panel.update(cx, |outline_panel, _cx| {
7324            outline_panel.selected_entry = SelectedEntry::None;
7325        });
7326
7327        // Check initial state - all entries should be expanded by default
7328        outline_panel.update(cx, |outline_panel, cx| {
7329            assert_eq!(
7330                display_entries(
7331                    &project,
7332                    &snapshot(outline_panel, cx),
7333                    &outline_panel.cached_entries,
7334                    outline_panel.selected_entry(),
7335                    cx,
7336                ),
7337                indoc!(
7338                    "
7339outline: struct Config
7340  outline: name
7341  outline: value
7342outline: impl Config
7343  outline: fn new
7344  outline: fn get_value
7345outline: enum Status
7346  outline: Active
7347  outline: Inactive
7348outline: fn process_config
7349outline: fn main"
7350                )
7351            );
7352        });
7353
7354        outline_panel.update(cx, |outline_panel, _cx| {
7355            outline_panel.selected_entry = SelectedEntry::None;
7356        });
7357
7358        cx.update(|window, cx| {
7359            outline_panel.update(cx, |outline_panel, cx| {
7360                outline_panel.select_first(&SelectFirst, window, cx);
7361            });
7362        });
7363
7364        cx.executor()
7365            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7366        cx.run_until_parked();
7367
7368        outline_panel.update(cx, |outline_panel, cx| {
7369            assert_eq!(
7370                display_entries(
7371                    &project,
7372                    &snapshot(outline_panel, cx),
7373                    &outline_panel.cached_entries,
7374                    outline_panel.selected_entry(),
7375                    cx,
7376                ),
7377                indoc!(
7378                    "
7379outline: struct Config  <==== selected
7380  outline: name
7381  outline: value
7382outline: impl Config
7383  outline: fn new
7384  outline: fn get_value
7385outline: enum Status
7386  outline: Active
7387  outline: Inactive
7388outline: fn process_config
7389outline: fn main"
7390                )
7391            );
7392        });
7393
7394        cx.update(|window, cx| {
7395            outline_panel.update(cx, |outline_panel, cx| {
7396                outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7397            });
7398        });
7399
7400        cx.executor()
7401            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7402        cx.run_until_parked();
7403
7404        outline_panel.update(cx, |outline_panel, cx| {
7405            assert_eq!(
7406                display_entries(
7407                    &project,
7408                    &snapshot(outline_panel, cx),
7409                    &outline_panel.cached_entries,
7410                    outline_panel.selected_entry(),
7411                    cx,
7412                ),
7413                indoc!(
7414                    "
7415outline: struct Config  <==== selected
7416outline: impl Config
7417  outline: fn new
7418  outline: fn get_value
7419outline: enum Status
7420  outline: Active
7421  outline: Inactive
7422outline: fn process_config
7423outline: fn main"
7424                )
7425            );
7426        });
7427
7428        cx.update(|window, cx| {
7429            outline_panel.update(cx, |outline_panel, cx| {
7430                outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7431            });
7432        });
7433
7434        cx.executor()
7435            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7436        cx.run_until_parked();
7437
7438        outline_panel.update(cx, |outline_panel, cx| {
7439            assert_eq!(
7440                display_entries(
7441                    &project,
7442                    &snapshot(outline_panel, cx),
7443                    &outline_panel.cached_entries,
7444                    outline_panel.selected_entry(),
7445                    cx,
7446                ),
7447                indoc!(
7448                    "
7449outline: struct Config  <==== selected
7450  outline: name
7451  outline: value
7452outline: impl Config
7453  outline: fn new
7454  outline: fn get_value
7455outline: enum Status
7456  outline: Active
7457  outline: Inactive
7458outline: fn process_config
7459outline: fn main"
7460                )
7461            );
7462        });
7463    }
7464
7465    #[gpui::test]
7466    async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) {
7467        init_test(cx);
7468
7469        let fs = FakeFs::new(cx.background_executor.clone());
7470        fs.insert_tree(
7471            "/test",
7472            json!({
7473                "src": {
7474                    "lib.rs": indoc!("
7475                            mod outer {
7476                                pub struct OuterStruct {
7477                                    field: String,
7478                                }
7479                                impl OuterStruct {
7480                                    pub fn new() -> Self {
7481                                        Self { field: String::new() }
7482                                    }
7483                                    pub fn method(&self) {
7484                                        println!(\"{}\", self.field);
7485                                    }
7486                                }
7487                                mod inner {
7488                                    pub fn inner_function() {
7489                                        let x = 42;
7490                                        println!(\"{}\", x);
7491                                    }
7492                                    pub struct InnerStruct {
7493                                        value: i32,
7494                                    }
7495                                }
7496                            }
7497                            fn main() {
7498                                let s = outer::OuterStruct::new();
7499                                s.method();
7500                            }
7501                        "),
7502                }
7503            }),
7504        )
7505        .await;
7506
7507        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7508        project.read_with(cx, |project, _| project.languages().add(rust_lang()));
7509        let (window, workspace) = add_outline_panel(&project, cx).await;
7510        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7511        let outline_panel = outline_panel(&workspace, cx);
7512
7513        outline_panel.update_in(cx, |outline_panel, window, cx| {
7514            outline_panel.set_active(true, window, cx)
7515        });
7516
7517        workspace
7518            .update_in(cx, |workspace, window, cx| {
7519                workspace.open_abs_path(
7520                    PathBuf::from("/test/src/lib.rs"),
7521                    OpenOptions {
7522                        visible: Some(OpenVisible::All),
7523                        ..Default::default()
7524                    },
7525                    window,
7526                    cx,
7527                )
7528            })
7529            .await
7530            .unwrap();
7531
7532        cx.executor()
7533            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7534        cx.run_until_parked();
7535
7536        // Force another update cycle to ensure outlines are fetched
7537        outline_panel.update_in(cx, |panel, window, cx| {
7538            panel.update_non_fs_items(window, cx);
7539            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7540        });
7541        cx.executor()
7542            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7543        cx.run_until_parked();
7544
7545        outline_panel.update(cx, |outline_panel, cx| {
7546            assert_eq!(
7547                display_entries(
7548                    &project,
7549                    &snapshot(outline_panel, cx),
7550                    &outline_panel.cached_entries,
7551                    outline_panel.selected_entry(),
7552                    cx,
7553                ),
7554                indoc!(
7555                    "
7556outline: mod outer  <==== selected
7557  outline: pub struct OuterStruct
7558    outline: field
7559  outline: impl OuterStruct
7560    outline: pub fn new
7561    outline: pub fn method
7562  outline: mod inner
7563    outline: pub fn inner_function
7564    outline: pub struct InnerStruct
7565      outline: value
7566outline: fn main"
7567                )
7568            );
7569        });
7570
7571        let _parent_outline = outline_panel
7572            .read_with(cx, |panel, _cx| {
7573                panel
7574                    .cached_entries
7575                    .iter()
7576                    .find_map(|entry| match &entry.entry {
7577                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7578                            if panel
7579                                .outline_children_cache
7580                                .get(&outline.range.start.buffer_id)
7581                                .and_then(|children_map| {
7582                                    let key = (outline.range.clone(), outline.depth);
7583                                    children_map.get(&key)
7584                                })
7585                                .copied()
7586                                .unwrap_or(false) =>
7587                        {
7588                            Some(entry.entry.clone())
7589                        }
7590                        _ => None,
7591                    })
7592            })
7593            .expect("Should find an outline with children");
7594
7595        // Collapse all entries
7596        outline_panel.update_in(cx, |panel, window, cx| {
7597            panel.collapse_all_entries(&CollapseAllEntries, window, cx);
7598        });
7599        cx.executor()
7600            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7601        cx.run_until_parked();
7602
7603        let expected_collapsed_output = indoc!(
7604            "
7605        outline: mod outer  <==== selected
7606        outline: fn main"
7607        );
7608
7609        outline_panel.update(cx, |panel, cx| {
7610            assert_eq! {
7611                display_entries(
7612                    &project,
7613                    &snapshot(panel, cx),
7614                    &panel.cached_entries,
7615                    panel.selected_entry(),
7616                    cx,
7617                ),
7618                expected_collapsed_output
7619            };
7620        });
7621
7622        // Expand all entries
7623        outline_panel.update_in(cx, |panel, window, cx| {
7624            panel.expand_all_entries(&ExpandAllEntries, window, cx);
7625        });
7626        cx.executor()
7627            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7628        cx.run_until_parked();
7629
7630        let expected_expanded_output = indoc!(
7631            "
7632        outline: mod outer  <==== selected
7633          outline: pub struct OuterStruct
7634            outline: field
7635          outline: impl OuterStruct
7636            outline: pub fn new
7637            outline: pub fn method
7638          outline: mod inner
7639            outline: pub fn inner_function
7640            outline: pub struct InnerStruct
7641              outline: value
7642        outline: fn main"
7643        );
7644
7645        outline_panel.update(cx, |panel, cx| {
7646            assert_eq! {
7647                display_entries(
7648                    &project,
7649                    &snapshot(panel, cx),
7650                    &panel.cached_entries,
7651                    panel.selected_entry(),
7652                    cx,
7653                ),
7654                expected_expanded_output
7655            };
7656        });
7657    }
7658
7659    #[gpui::test]
7660    async fn test_buffer_search(cx: &mut TestAppContext) {
7661        init_test(cx);
7662
7663        let fs = FakeFs::new(cx.background_executor.clone());
7664        fs.insert_tree(
7665            "/test",
7666            json!({
7667                "foo.txt": r#"<_constitution>
7668
7669</_constitution>
7670
7671
7672
7673## 📊 Output
7674
7675| Field          | Meaning                |
7676"#
7677            }),
7678        )
7679        .await;
7680
7681        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7682        let (window, workspace) = add_outline_panel(&project, cx).await;
7683        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7684
7685        let editor = workspace
7686            .update_in(cx, |workspace, window, cx| {
7687                workspace.open_abs_path(
7688                    PathBuf::from("/test/foo.txt"),
7689                    OpenOptions {
7690                        visible: Some(OpenVisible::All),
7691                        ..OpenOptions::default()
7692                    },
7693                    window,
7694                    cx,
7695                )
7696            })
7697            .await
7698            .unwrap()
7699            .downcast::<Editor>()
7700            .unwrap();
7701
7702        let search_bar = workspace.update_in(cx, |_, window, cx| {
7703            cx.new(|cx| {
7704                let mut search_bar = BufferSearchBar::new(None, window, cx);
7705                search_bar.set_active_pane_item(Some(&editor), window, cx);
7706                search_bar.show(window, cx);
7707                search_bar
7708            })
7709        });
7710
7711        let outline_panel = outline_panel(&workspace, cx);
7712
7713        outline_panel.update_in(cx, |outline_panel, window, cx| {
7714            outline_panel.set_active(true, window, cx)
7715        });
7716
7717        search_bar
7718            .update_in(cx, |search_bar, window, cx| {
7719                search_bar.search("  ", None, true, window, cx)
7720            })
7721            .await
7722            .unwrap();
7723
7724        cx.executor()
7725            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7726        cx.run_until_parked();
7727
7728        outline_panel.update(cx, |outline_panel, cx| {
7729            assert_eq!(
7730                display_entries(
7731                    &project,
7732                    &snapshot(outline_panel, cx),
7733                    &outline_panel.cached_entries,
7734                    outline_panel.selected_entry(),
7735                    cx,
7736                ),
7737                "search: | Field«  »        | Meaning                |  <==== selected
7738search: | Field  «  »      | Meaning                |
7739search: | Field    «  »    | Meaning                |
7740search: | Field      «  »  | Meaning                |
7741search: | Field        «  »| Meaning                |
7742search: | Field          | Meaning«  »              |
7743search: | Field          | Meaning  «  »            |
7744search: | Field          | Meaning    «  »          |
7745search: | Field          | Meaning      «  »        |
7746search: | Field          | Meaning        «  »      |
7747search: | Field          | Meaning          «  »    |
7748search: | Field          | Meaning            «  »  |
7749search: | Field          | Meaning              «  »|"
7750            );
7751        });
7752    }
7753
7754    #[gpui::test]
7755    async fn test_outline_panel_lsp_document_symbols(cx: &mut TestAppContext) {
7756        init_test(cx);
7757
7758        let root = path!("/root");
7759        let fs = FakeFs::new(cx.background_executor.clone());
7760        fs.insert_tree(
7761            root,
7762            json!({
7763                "src": {
7764                    "lib.rs": "struct Foo {\n    bar: u32,\n    baz: String,\n}\n",
7765                }
7766            }),
7767        )
7768        .await;
7769
7770        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
7771        let language_registry = project.read_with(cx, |project, _| {
7772            project.languages().add(rust_lang());
7773            project.languages().clone()
7774        });
7775
7776        let mut fake_language_servers = language_registry.register_fake_lsp(
7777            "Rust",
7778            FakeLspAdapter {
7779                capabilities: lsp::ServerCapabilities {
7780                    document_symbol_provider: Some(lsp::OneOf::Left(true)),
7781                    ..lsp::ServerCapabilities::default()
7782                },
7783                initializer: Some(Box::new(|fake_language_server| {
7784                    fake_language_server
7785                        .set_request_handler::<lsp::request::DocumentSymbolRequest, _, _>(
7786                            move |_, _| async move {
7787                                #[allow(deprecated)]
7788                                Ok(Some(lsp::DocumentSymbolResponse::Nested(vec![
7789                                    lsp::DocumentSymbol {
7790                                        name: "Foo".to_string(),
7791                                        detail: None,
7792                                        kind: lsp::SymbolKind::STRUCT,
7793                                        tags: None,
7794                                        deprecated: None,
7795                                        range: lsp::Range::new(
7796                                            lsp::Position::new(0, 0),
7797                                            lsp::Position::new(3, 1),
7798                                        ),
7799                                        selection_range: lsp::Range::new(
7800                                            lsp::Position::new(0, 7),
7801                                            lsp::Position::new(0, 10),
7802                                        ),
7803                                        children: Some(vec![
7804                                            lsp::DocumentSymbol {
7805                                                name: "bar".to_string(),
7806                                                detail: None,
7807                                                kind: lsp::SymbolKind::FIELD,
7808                                                tags: None,
7809                                                deprecated: None,
7810                                                range: lsp::Range::new(
7811                                                    lsp::Position::new(1, 4),
7812                                                    lsp::Position::new(1, 13),
7813                                                ),
7814                                                selection_range: lsp::Range::new(
7815                                                    lsp::Position::new(1, 4),
7816                                                    lsp::Position::new(1, 7),
7817                                                ),
7818                                                children: None,
7819                                            },
7820                                            lsp::DocumentSymbol {
7821                                                name: "lsp_only_field".to_string(),
7822                                                detail: None,
7823                                                kind: lsp::SymbolKind::FIELD,
7824                                                tags: None,
7825                                                deprecated: None,
7826                                                range: lsp::Range::new(
7827                                                    lsp::Position::new(2, 4),
7828                                                    lsp::Position::new(2, 15),
7829                                                ),
7830                                                selection_range: lsp::Range::new(
7831                                                    lsp::Position::new(2, 4),
7832                                                    lsp::Position::new(2, 7),
7833                                                ),
7834                                                children: None,
7835                                            },
7836                                        ]),
7837                                    },
7838                                ])))
7839                            },
7840                        );
7841                })),
7842                ..FakeLspAdapter::default()
7843            },
7844        );
7845
7846        let (window, workspace) = add_outline_panel(&project, cx).await;
7847        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7848        let outline_panel = outline_panel(&workspace, cx);
7849        cx.update(|window, cx| {
7850            outline_panel.update(cx, |outline_panel, cx| {
7851                outline_panel.set_active(true, window, cx)
7852            });
7853        });
7854
7855        let _editor = workspace
7856            .update_in(cx, |workspace, window, cx| {
7857                workspace.open_abs_path(
7858                    PathBuf::from(path!("/root/src/lib.rs")),
7859                    OpenOptions {
7860                        visible: Some(OpenVisible::All),
7861                        ..OpenOptions::default()
7862                    },
7863                    window,
7864                    cx,
7865                )
7866            })
7867            .await
7868            .expect("Failed to open Rust source file")
7869            .downcast::<Editor>()
7870            .expect("Should open an editor for Rust source file");
7871        let _fake_language_server = fake_language_servers.next().await.unwrap();
7872        cx.executor()
7873            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7874        cx.run_until_parked();
7875
7876        // Step 1: tree-sitter outlines by default
7877        outline_panel.update(cx, |outline_panel, cx| {
7878            assert_eq!(
7879                display_entries(
7880                    &project,
7881                    &snapshot(outline_panel, cx),
7882                    &outline_panel.cached_entries,
7883                    outline_panel.selected_entry(),
7884                    cx,
7885                ),
7886                indoc!(
7887                    "
7888outline: struct Foo  <==== selected
7889  outline: bar
7890  outline: baz"
7891                ),
7892                "Step 1: tree-sitter outlines should be displayed by default"
7893            );
7894        });
7895
7896        // Step 2: Switch to LSP document symbols
7897        cx.update(|_, cx| {
7898            settings::SettingsStore::update_global(
7899                cx,
7900                |store: &mut settings::SettingsStore, cx| {
7901                    store.update_user_settings(cx, |settings| {
7902                        settings.project.all_languages.defaults.document_symbols =
7903                            Some(settings::DocumentSymbols::On);
7904                    });
7905                },
7906            );
7907        });
7908        cx.executor()
7909            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7910        cx.run_until_parked();
7911
7912        outline_panel.update(cx, |outline_panel, cx| {
7913            assert_eq!(
7914                display_entries(
7915                    &project,
7916                    &snapshot(outline_panel, cx),
7917                    &outline_panel.cached_entries,
7918                    outline_panel.selected_entry(),
7919                    cx,
7920                ),
7921                indoc!(
7922                    "
7923outline: struct Foo  <==== selected
7924  outline: bar
7925  outline: lsp_only_field"
7926                ),
7927                "Step 2: After switching to LSP, should see LSP-provided symbols"
7928            );
7929        });
7930
7931        // Step 3: Switch back to tree-sitter
7932        cx.update(|_, cx| {
7933            settings::SettingsStore::update_global(
7934                cx,
7935                |store: &mut settings::SettingsStore, cx| {
7936                    store.update_user_settings(cx, |settings| {
7937                        settings.project.all_languages.defaults.document_symbols =
7938                            Some(settings::DocumentSymbols::Off);
7939                    });
7940                },
7941            );
7942        });
7943        cx.executor()
7944            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7945        cx.run_until_parked();
7946
7947        outline_panel.update(cx, |outline_panel, cx| {
7948            assert_eq!(
7949                display_entries(
7950                    &project,
7951                    &snapshot(outline_panel, cx),
7952                    &outline_panel.cached_entries,
7953                    outline_panel.selected_entry(),
7954                    cx,
7955                ),
7956                indoc!(
7957                    "
7958outline: struct Foo  <==== selected
7959  outline: bar
7960  outline: baz"
7961                ),
7962                "Step 3: tree-sitter outlines should be restored"
7963            );
7964        });
7965    }
7966
7967    #[gpui::test]
7968    async fn test_markdown_outline_selection_at_heading_boundaries(cx: &mut TestAppContext) {
7969        init_test(cx);
7970
7971        let fs = FakeFs::new(cx.background_executor.clone());
7972        fs.insert_tree(
7973            "/test",
7974            json!({
7975                "doc.md": indoc!("
7976                    # Section A
7977
7978                    ## Sub Section A
7979
7980                    ## Sub Section B
7981
7982                    # Section B
7983
7984                ")
7985            }),
7986        )
7987        .await;
7988
7989        let project = Project::test(fs.clone(), [Path::new("/test")], cx).await;
7990        project.read_with(cx, |project, _| project.languages().add(markdown_lang()));
7991        let (window, workspace) = add_outline_panel(&project, cx).await;
7992        let cx = &mut VisualTestContext::from_window(window.into(), cx);
7993        let outline_panel = outline_panel(&workspace, cx);
7994        outline_panel.update_in(cx, |outline_panel, window, cx| {
7995            outline_panel.set_active(true, window, cx)
7996        });
7997
7998        let editor = workspace
7999            .update_in(cx, |workspace, window, cx| {
8000                workspace.open_abs_path(
8001                    PathBuf::from("/test/doc.md"),
8002                    OpenOptions {
8003                        visible: Some(OpenVisible::All),
8004                        ..Default::default()
8005                    },
8006                    window,
8007                    cx,
8008                )
8009            })
8010            .await
8011            .unwrap()
8012            .downcast::<Editor>()
8013            .unwrap();
8014
8015        cx.run_until_parked();
8016
8017        outline_panel.update_in(cx, |panel, window, cx| {
8018            panel.update_non_fs_items(window, cx);
8019            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
8020        });
8021
8022        // Helper function to move the cursor to the first column of a given row
8023        // and return the selected outline entry's text.
8024        let move_cursor_and_get_selection =
8025            |row: u32, cx: &mut VisualTestContext| -> Option<String> {
8026                cx.update(|window, cx| {
8027                    editor.update(cx, |editor, cx| {
8028                        editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8029                            s.select_ranges(Some(
8030                                language::Point::new(row, 0)..language::Point::new(row, 0),
8031                            ))
8032                        });
8033                    });
8034                });
8035
8036                cx.run_until_parked();
8037
8038                outline_panel.read_with(cx, |panel, _cx| {
8039                    panel.selected_entry().and_then(|entry| match entry {
8040                        PanelEntry::Outline(OutlineEntry::Outline(outline)) => {
8041                            Some(outline.text.clone())
8042                        }
8043                        _ => None,
8044                    })
8045                })
8046            };
8047
8048        assert_eq!(
8049            move_cursor_and_get_selection(0, cx).as_deref(),
8050            Some("# Section A"),
8051            "Cursor at row 0 should select '# Section A'"
8052        );
8053
8054        assert_eq!(
8055            move_cursor_and_get_selection(2, cx).as_deref(),
8056            Some("## Sub Section A"),
8057            "Cursor at row 2 should select '## Sub Section A'"
8058        );
8059
8060        assert_eq!(
8061            move_cursor_and_get_selection(4, cx).as_deref(),
8062            Some("## Sub Section B"),
8063            "Cursor at row 4 should select '## Sub Section B'"
8064        );
8065
8066        assert_eq!(
8067            move_cursor_and_get_selection(6, cx).as_deref(),
8068            Some("# Section B"),
8069            "Cursor at row 6 should select '# Section B'"
8070        );
8071    }
8072}