outline_panel.rs

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