outline_panel.rs

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