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