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(Color::Selected.color(cx)),
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::{Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
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, _| {
5247            project.languages().add(Arc::new(rust_lang()))
5248        });
5249        let workspace = add_outline_panel(&project, cx).await;
5250        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5251        let outline_panel = outline_panel(&workspace, cx);
5252        outline_panel.update_in(cx, |outline_panel, window, cx| {
5253            outline_panel.set_active(true, window, cx)
5254        });
5255
5256        workspace
5257            .update(cx, |workspace, window, cx| {
5258                ProjectSearchView::deploy_search(
5259                    workspace,
5260                    &workspace::DeploySearch::default(),
5261                    window,
5262                    cx,
5263                )
5264            })
5265            .unwrap();
5266        let search_view = workspace
5267            .update(cx, |workspace, _, cx| {
5268                workspace
5269                    .active_pane()
5270                    .read(cx)
5271                    .items()
5272                    .find_map(|item| item.downcast::<ProjectSearchView>())
5273                    .expect("Project search view expected to appear after new search event trigger")
5274            })
5275            .unwrap();
5276
5277        let query = "param_names_for_lifetime_elision_hints";
5278        perform_project_search(&search_view, query, cx);
5279        search_view.update(cx, |search_view, cx| {
5280            search_view
5281                .results_editor()
5282                .update(cx, |results_editor, cx| {
5283                    assert_eq!(
5284                        results_editor.display_text(cx).match_indices(query).count(),
5285                        9
5286                    );
5287                });
5288        });
5289
5290        let all_matches = r#"rust-analyzer/
5291  crates/
5292    ide/src/
5293      inlay_hints/
5294        fn_lifetime_fn.rs
5295          search: match config.«param_names_for_lifetime_elision_hints» {
5296          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5297          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5298          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5299      inlay_hints.rs
5300        search: pub «param_names_for_lifetime_elision_hints»: bool,
5301        search: «param_names_for_lifetime_elision_hints»: self
5302      static_index.rs
5303        search: «param_names_for_lifetime_elision_hints»: false,
5304    rust-analyzer/src/
5305      cli/
5306        analysis_stats.rs
5307          search: «param_names_for_lifetime_elision_hints»: true,
5308      config.rs
5309        search: «param_names_for_lifetime_elision_hints»: self"#
5310            .to_string();
5311
5312        let select_first_in_all_matches = |line_to_select: &str| {
5313            assert!(
5314                all_matches.contains(line_to_select),
5315                "`{line_to_select}` was not found in all matches `{all_matches}`"
5316            );
5317            all_matches.replacen(
5318                line_to_select,
5319                &format!("{line_to_select}{SELECTED_MARKER}"),
5320                1,
5321            )
5322        };
5323
5324        cx.executor()
5325            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5326        cx.run_until_parked();
5327        outline_panel.update(cx, |outline_panel, cx| {
5328            assert_eq!(
5329                display_entries(
5330                    &project,
5331                    &snapshot(outline_panel, cx),
5332                    &outline_panel.cached_entries,
5333                    outline_panel.selected_entry(),
5334                    cx,
5335                ),
5336                select_first_in_all_matches(
5337                    "search: match config.«param_names_for_lifetime_elision_hints» {"
5338                )
5339            );
5340        });
5341
5342        outline_panel.update_in(cx, |outline_panel, window, cx| {
5343            outline_panel.select_parent(&SelectParent, window, cx);
5344            assert_eq!(
5345                display_entries(
5346                    &project,
5347                    &snapshot(outline_panel, cx),
5348                    &outline_panel.cached_entries,
5349                    outline_panel.selected_entry(),
5350                    cx,
5351                ),
5352                select_first_in_all_matches("fn_lifetime_fn.rs")
5353            );
5354        });
5355        outline_panel.update_in(cx, |outline_panel, window, cx| {
5356            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5357        });
5358        cx.executor()
5359            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5360        cx.run_until_parked();
5361        outline_panel.update(cx, |outline_panel, cx| {
5362            assert_eq!(
5363                display_entries(
5364                    &project,
5365                    &snapshot(outline_panel, cx),
5366                    &outline_panel.cached_entries,
5367                    outline_panel.selected_entry(),
5368                    cx,
5369                ),
5370                format!(
5371                    r#"rust-analyzer/
5372  crates/
5373    ide/src/
5374      inlay_hints/
5375        fn_lifetime_fn.rs{SELECTED_MARKER}
5376      inlay_hints.rs
5377        search: pub «param_names_for_lifetime_elision_hints»: bool,
5378        search: «param_names_for_lifetime_elision_hints»: self
5379      static_index.rs
5380        search: «param_names_for_lifetime_elision_hints»: false,
5381    rust-analyzer/src/
5382      cli/
5383        analysis_stats.rs
5384          search: «param_names_for_lifetime_elision_hints»: true,
5385      config.rs
5386        search: «param_names_for_lifetime_elision_hints»: self"#,
5387                )
5388            );
5389        });
5390
5391        outline_panel.update_in(cx, |outline_panel, window, cx| {
5392            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5393        });
5394        cx.executor()
5395            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5396        cx.run_until_parked();
5397        outline_panel.update_in(cx, |outline_panel, window, cx| {
5398            outline_panel.select_parent(&SelectParent, window, cx);
5399            assert_eq!(
5400                display_entries(
5401                    &project,
5402                    &snapshot(outline_panel, cx),
5403                    &outline_panel.cached_entries,
5404                    outline_panel.selected_entry(),
5405                    cx,
5406                ),
5407                select_first_in_all_matches("inlay_hints/")
5408            );
5409        });
5410
5411        outline_panel.update_in(cx, |outline_panel, window, cx| {
5412            outline_panel.select_parent(&SelectParent, window, cx);
5413            assert_eq!(
5414                display_entries(
5415                    &project,
5416                    &snapshot(outline_panel, cx),
5417                    &outline_panel.cached_entries,
5418                    outline_panel.selected_entry(),
5419                    cx,
5420                ),
5421                select_first_in_all_matches("ide/src/")
5422            );
5423        });
5424
5425        outline_panel.update_in(cx, |outline_panel, window, cx| {
5426            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5427        });
5428        cx.executor()
5429            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5430        cx.run_until_parked();
5431        outline_panel.update(cx, |outline_panel, cx| {
5432            assert_eq!(
5433                display_entries(
5434                    &project,
5435                    &snapshot(outline_panel, cx),
5436                    &outline_panel.cached_entries,
5437                    outline_panel.selected_entry(),
5438                    cx,
5439                ),
5440                format!(
5441                    r#"rust-analyzer/
5442  crates/
5443    ide/src/{SELECTED_MARKER}
5444    rust-analyzer/src/
5445      cli/
5446        analysis_stats.rs
5447          search: «param_names_for_lifetime_elision_hints»: true,
5448      config.rs
5449        search: «param_names_for_lifetime_elision_hints»: self"#,
5450                )
5451            );
5452        });
5453        outline_panel.update_in(cx, |outline_panel, window, cx| {
5454            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5455        });
5456        cx.executor()
5457            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5458        cx.run_until_parked();
5459        outline_panel.update(cx, |outline_panel, cx| {
5460            assert_eq!(
5461                display_entries(
5462                    &project,
5463                    &snapshot(outline_panel, cx),
5464                    &outline_panel.cached_entries,
5465                    outline_panel.selected_entry(),
5466                    cx,
5467                ),
5468                select_first_in_all_matches("ide/src/")
5469            );
5470        });
5471    }
5472
5473    #[gpui::test(iterations = 10)]
5474    async fn test_item_filtering(cx: &mut TestAppContext) {
5475        init_test(cx);
5476
5477        let fs = FakeFs::new(cx.background_executor.clone());
5478        let root = path!("/rust-analyzer");
5479        populate_with_test_ra_project(&fs, root).await;
5480        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5481        project.read_with(cx, |project, _| {
5482            project.languages().add(Arc::new(rust_lang()))
5483        });
5484        let workspace = add_outline_panel(&project, cx).await;
5485        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5486        let outline_panel = outline_panel(&workspace, cx);
5487        outline_panel.update_in(cx, |outline_panel, window, cx| {
5488            outline_panel.set_active(true, window, cx)
5489        });
5490
5491        workspace
5492            .update(cx, |workspace, window, cx| {
5493                ProjectSearchView::deploy_search(
5494                    workspace,
5495                    &workspace::DeploySearch::default(),
5496                    window,
5497                    cx,
5498                )
5499            })
5500            .unwrap();
5501        let search_view = workspace
5502            .update(cx, |workspace, _, cx| {
5503                workspace
5504                    .active_pane()
5505                    .read(cx)
5506                    .items()
5507                    .find_map(|item| item.downcast::<ProjectSearchView>())
5508                    .expect("Project search view expected to appear after new search event trigger")
5509            })
5510            .unwrap();
5511
5512        let query = "param_names_for_lifetime_elision_hints";
5513        perform_project_search(&search_view, query, cx);
5514        search_view.update(cx, |search_view, cx| {
5515            search_view
5516                .results_editor()
5517                .update(cx, |results_editor, cx| {
5518                    assert_eq!(
5519                        results_editor.display_text(cx).match_indices(query).count(),
5520                        9
5521                    );
5522                });
5523        });
5524        let all_matches = r#"rust-analyzer/
5525  crates/
5526    ide/src/
5527      inlay_hints/
5528        fn_lifetime_fn.rs
5529          search: match config.«param_names_for_lifetime_elision_hints» {
5530          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5531          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5532          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5533      inlay_hints.rs
5534        search: pub «param_names_for_lifetime_elision_hints»: bool,
5535        search: «param_names_for_lifetime_elision_hints»: self
5536      static_index.rs
5537        search: «param_names_for_lifetime_elision_hints»: false,
5538    rust-analyzer/src/
5539      cli/
5540        analysis_stats.rs
5541          search: «param_names_for_lifetime_elision_hints»: true,
5542      config.rs
5543        search: «param_names_for_lifetime_elision_hints»: self"#
5544            .to_string();
5545
5546        cx.executor()
5547            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5548        cx.run_until_parked();
5549        outline_panel.update(cx, |outline_panel, cx| {
5550            assert_eq!(
5551                display_entries(
5552                    &project,
5553                    &snapshot(outline_panel, cx),
5554                    &outline_panel.cached_entries,
5555                    None,
5556                    cx,
5557                ),
5558                all_matches,
5559            );
5560        });
5561
5562        let filter_text = "a";
5563        outline_panel.update_in(cx, |outline_panel, window, cx| {
5564            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5565                filter_editor.set_text(filter_text, window, cx);
5566            });
5567        });
5568        cx.executor()
5569            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5570        cx.run_until_parked();
5571
5572        outline_panel.update(cx, |outline_panel, cx| {
5573            assert_eq!(
5574                display_entries(
5575                    &project,
5576                    &snapshot(outline_panel, cx),
5577                    &outline_panel.cached_entries,
5578                    None,
5579                    cx,
5580                ),
5581                all_matches
5582                    .lines()
5583                    .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5584                    .filter(|item| item.contains(filter_text))
5585                    .collect::<Vec<_>>()
5586                    .join("\n"),
5587            );
5588        });
5589
5590        outline_panel.update_in(cx, |outline_panel, window, cx| {
5591            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5592                filter_editor.set_text("", window, cx);
5593            });
5594        });
5595        cx.executor()
5596            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5597        cx.run_until_parked();
5598        outline_panel.update(cx, |outline_panel, cx| {
5599            assert_eq!(
5600                display_entries(
5601                    &project,
5602                    &snapshot(outline_panel, cx),
5603                    &outline_panel.cached_entries,
5604                    None,
5605                    cx,
5606                ),
5607                all_matches,
5608            );
5609        });
5610    }
5611
5612    #[gpui::test(iterations = 10)]
5613    async fn test_item_opening(cx: &mut TestAppContext) {
5614        init_test(cx);
5615
5616        let fs = FakeFs::new(cx.background_executor.clone());
5617        let root = path!("/rust-analyzer");
5618        populate_with_test_ra_project(&fs, root).await;
5619        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
5620        project.read_with(cx, |project, _| {
5621            project.languages().add(Arc::new(rust_lang()))
5622        });
5623        let workspace = add_outline_panel(&project, cx).await;
5624        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5625        let outline_panel = outline_panel(&workspace, cx);
5626        outline_panel.update_in(cx, |outline_panel, window, cx| {
5627            outline_panel.set_active(true, window, cx)
5628        });
5629
5630        workspace
5631            .update(cx, |workspace, window, cx| {
5632                ProjectSearchView::deploy_search(
5633                    workspace,
5634                    &workspace::DeploySearch::default(),
5635                    window,
5636                    cx,
5637                )
5638            })
5639            .unwrap();
5640        let search_view = workspace
5641            .update(cx, |workspace, _, cx| {
5642                workspace
5643                    .active_pane()
5644                    .read(cx)
5645                    .items()
5646                    .find_map(|item| item.downcast::<ProjectSearchView>())
5647                    .expect("Project search view expected to appear after new search event trigger")
5648            })
5649            .unwrap();
5650
5651        let query = "param_names_for_lifetime_elision_hints";
5652        perform_project_search(&search_view, query, cx);
5653        search_view.update(cx, |search_view, cx| {
5654            search_view
5655                .results_editor()
5656                .update(cx, |results_editor, cx| {
5657                    assert_eq!(
5658                        results_editor.display_text(cx).match_indices(query).count(),
5659                        9
5660                    );
5661                });
5662        });
5663        let all_matches = r#"rust-analyzer/
5664  crates/
5665    ide/src/
5666      inlay_hints/
5667        fn_lifetime_fn.rs
5668          search: match config.«param_names_for_lifetime_elision_hints» {
5669          search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» {
5670          search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {
5671          search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },
5672      inlay_hints.rs
5673        search: pub «param_names_for_lifetime_elision_hints»: bool,
5674        search: «param_names_for_lifetime_elision_hints»: self
5675      static_index.rs
5676        search: «param_names_for_lifetime_elision_hints»: false,
5677    rust-analyzer/src/
5678      cli/
5679        analysis_stats.rs
5680          search: «param_names_for_lifetime_elision_hints»: true,
5681      config.rs
5682        search: «param_names_for_lifetime_elision_hints»: self"#
5683            .to_string();
5684        let select_first_in_all_matches = |line_to_select: &str| {
5685            assert!(
5686                all_matches.contains(line_to_select),
5687                "`{line_to_select}` was not found in all matches `{all_matches}`"
5688            );
5689            all_matches.replacen(
5690                line_to_select,
5691                &format!("{line_to_select}{SELECTED_MARKER}"),
5692                1,
5693            )
5694        };
5695        let clear_outline_metadata = |input: &str| {
5696            input
5697                .replace("search: ", "")
5698                .replace("«", "")
5699                .replace("»", "")
5700        };
5701
5702        cx.executor()
5703            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5704        cx.run_until_parked();
5705
5706        let active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5707            outline_panel
5708                .active_editor()
5709                .expect("should have an active editor open")
5710        });
5711        let initial_outline_selection =
5712            "search: match config.«param_names_for_lifetime_elision_hints» {";
5713        outline_panel.update_in(cx, |outline_panel, window, cx| {
5714            assert_eq!(
5715                display_entries(
5716                    &project,
5717                    &snapshot(outline_panel, cx),
5718                    &outline_panel.cached_entries,
5719                    outline_panel.selected_entry(),
5720                    cx,
5721                ),
5722                select_first_in_all_matches(initial_outline_selection)
5723            );
5724            assert_eq!(
5725                selected_row_text(&active_editor, cx),
5726                clear_outline_metadata(initial_outline_selection),
5727                "Should place the initial editor selection on the corresponding search result"
5728            );
5729
5730            outline_panel.select_next(&SelectNext, window, cx);
5731            outline_panel.select_next(&SelectNext, window, cx);
5732        });
5733
5734        let navigated_outline_selection =
5735            "search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {";
5736        outline_panel.update(cx, |outline_panel, cx| {
5737            assert_eq!(
5738                display_entries(
5739                    &project,
5740                    &snapshot(outline_panel, cx),
5741                    &outline_panel.cached_entries,
5742                    outline_panel.selected_entry(),
5743                    cx,
5744                ),
5745                select_first_in_all_matches(navigated_outline_selection)
5746            );
5747        });
5748        cx.executor()
5749            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5750        outline_panel.update(cx, |_, cx| {
5751            assert_eq!(
5752                selected_row_text(&active_editor, cx),
5753                clear_outline_metadata(navigated_outline_selection),
5754                "Should still have the initial caret position after SelectNext calls"
5755            );
5756        });
5757
5758        outline_panel.update_in(cx, |outline_panel, window, cx| {
5759            outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx);
5760        });
5761        outline_panel.update(cx, |_outline_panel, cx| {
5762            assert_eq!(
5763                selected_row_text(&active_editor, cx),
5764                clear_outline_metadata(navigated_outline_selection),
5765                "After opening, should move the caret to the opened outline entry's position"
5766            );
5767        });
5768
5769        outline_panel.update_in(cx, |outline_panel, window, cx| {
5770            outline_panel.select_next(&SelectNext, window, cx);
5771        });
5772        let next_navigated_outline_selection = "search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },";
5773        outline_panel.update(cx, |outline_panel, cx| {
5774            assert_eq!(
5775                display_entries(
5776                    &project,
5777                    &snapshot(outline_panel, cx),
5778                    &outline_panel.cached_entries,
5779                    outline_panel.selected_entry(),
5780                    cx,
5781                ),
5782                select_first_in_all_matches(next_navigated_outline_selection)
5783            );
5784        });
5785        cx.executor()
5786            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5787        outline_panel.update(cx, |_outline_panel, cx| {
5788            assert_eq!(
5789                selected_row_text(&active_editor, cx),
5790                clear_outline_metadata(next_navigated_outline_selection),
5791                "Should again preserve the selection after another SelectNext call"
5792            );
5793        });
5794
5795        outline_panel.update_in(cx, |outline_panel, window, cx| {
5796            outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx);
5797        });
5798        cx.executor()
5799            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5800        cx.run_until_parked();
5801        let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| {
5802            outline_panel
5803                .active_editor()
5804                .expect("should have an active editor open")
5805        });
5806        outline_panel.update(cx, |outline_panel, cx| {
5807            assert_ne!(
5808                active_editor, new_active_editor,
5809                "After opening an excerpt, new editor should be open"
5810            );
5811            assert_eq!(
5812                display_entries(
5813                    &project,
5814                    &snapshot(outline_panel, cx),
5815                    &outline_panel.cached_entries,
5816                    outline_panel.selected_entry(),
5817                    cx,
5818                ),
5819                "fn_lifetime_fn.rs  <==== selected"
5820            );
5821            assert_eq!(
5822                selected_row_text(&new_active_editor, cx),
5823                clear_outline_metadata(next_navigated_outline_selection),
5824                "When opening the excerpt, should navigate to the place corresponding the outline entry"
5825            );
5826        });
5827    }
5828
5829    #[gpui::test]
5830    async fn test_multiple_worktrees(cx: &mut TestAppContext) {
5831        init_test(cx);
5832
5833        let fs = FakeFs::new(cx.background_executor.clone());
5834        fs.insert_tree(
5835            path!("/root"),
5836            json!({
5837                "one": {
5838                    "a.txt": "aaa aaa"
5839                },
5840                "two": {
5841                    "b.txt": "a aaa"
5842                }
5843
5844            }),
5845        )
5846        .await;
5847        let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await;
5848        let workspace = add_outline_panel(&project, cx).await;
5849        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5850        let outline_panel = outline_panel(&workspace, cx);
5851        outline_panel.update_in(cx, |outline_panel, window, cx| {
5852            outline_panel.set_active(true, window, cx)
5853        });
5854
5855        let items = workspace
5856            .update(cx, |workspace, window, cx| {
5857                workspace.open_paths(
5858                    vec![PathBuf::from(path!("/root/two"))],
5859                    OpenOptions {
5860                        visible: Some(OpenVisible::OnlyDirectories),
5861                        ..Default::default()
5862                    },
5863                    None,
5864                    window,
5865                    cx,
5866                )
5867            })
5868            .unwrap()
5869            .await;
5870        assert_eq!(items.len(), 1, "Were opening another worktree directory");
5871        assert!(
5872            items[0].is_none(),
5873            "Directory should be opened successfully"
5874        );
5875
5876        workspace
5877            .update(cx, |workspace, window, cx| {
5878                ProjectSearchView::deploy_search(
5879                    workspace,
5880                    &workspace::DeploySearch::default(),
5881                    window,
5882                    cx,
5883                )
5884            })
5885            .unwrap();
5886        let search_view = workspace
5887            .update(cx, |workspace, _, cx| {
5888                workspace
5889                    .active_pane()
5890                    .read(cx)
5891                    .items()
5892                    .find_map(|item| item.downcast::<ProjectSearchView>())
5893                    .expect("Project search view expected to appear after new search event trigger")
5894            })
5895            .unwrap();
5896
5897        let query = "aaa";
5898        perform_project_search(&search_view, query, cx);
5899        search_view.update(cx, |search_view, cx| {
5900            search_view
5901                .results_editor()
5902                .update(cx, |results_editor, cx| {
5903                    assert_eq!(
5904                        results_editor.display_text(cx).match_indices(query).count(),
5905                        3
5906                    );
5907                });
5908        });
5909
5910        cx.executor()
5911            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5912        cx.run_until_parked();
5913        outline_panel.update(cx, |outline_panel, cx| {
5914            assert_eq!(
5915                display_entries(
5916                    &project,
5917                    &snapshot(outline_panel, cx),
5918                    &outline_panel.cached_entries,
5919                    outline_panel.selected_entry(),
5920                    cx,
5921                ),
5922                format!(
5923                    r#"one/
5924  a.txt
5925    search: «aaa» aaa  <==== selected
5926    search: aaa «aaa»
5927two/
5928  b.txt
5929    search: a «aaa»"#,
5930                ),
5931            );
5932        });
5933
5934        outline_panel.update_in(cx, |outline_panel, window, cx| {
5935            outline_panel.select_previous(&SelectPrevious, window, cx);
5936            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5937        });
5938        cx.executor()
5939            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5940        cx.run_until_parked();
5941        outline_panel.update(cx, |outline_panel, cx| {
5942            assert_eq!(
5943                display_entries(
5944                    &project,
5945                    &snapshot(outline_panel, cx),
5946                    &outline_panel.cached_entries,
5947                    outline_panel.selected_entry(),
5948                    cx,
5949                ),
5950                format!(
5951                    r#"one/
5952  a.txt  <==== selected
5953two/
5954  b.txt
5955    search: a «aaa»"#,
5956                ),
5957            );
5958        });
5959
5960        outline_panel.update_in(cx, |outline_panel, window, cx| {
5961            outline_panel.select_next(&SelectNext, window, cx);
5962            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5963        });
5964        cx.executor()
5965            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5966        cx.run_until_parked();
5967        outline_panel.update(cx, |outline_panel, cx| {
5968            assert_eq!(
5969                display_entries(
5970                    &project,
5971                    &snapshot(outline_panel, cx),
5972                    &outline_panel.cached_entries,
5973                    outline_panel.selected_entry(),
5974                    cx,
5975                ),
5976                format!(
5977                    r#"one/
5978  a.txt
5979two/  <==== selected"#,
5980                ),
5981            );
5982        });
5983
5984        outline_panel.update_in(cx, |outline_panel, window, cx| {
5985            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5986        });
5987        cx.executor()
5988            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5989        cx.run_until_parked();
5990        outline_panel.update(cx, |outline_panel, cx| {
5991            assert_eq!(
5992                display_entries(
5993                    &project,
5994                    &snapshot(outline_panel, cx),
5995                    &outline_panel.cached_entries,
5996                    outline_panel.selected_entry(),
5997                    cx,
5998                ),
5999                format!(
6000                    r#"one/
6001  a.txt
6002two/  <==== selected
6003  b.txt
6004    search: a «aaa»"#,
6005                )
6006            );
6007        });
6008    }
6009
6010    #[gpui::test]
6011    async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
6012        init_test(cx);
6013
6014        let root = path!("/root");
6015        let fs = FakeFs::new(cx.background_executor.clone());
6016        fs.insert_tree(
6017            root,
6018            json!({
6019                "src": {
6020                    "lib.rs": indoc!("
6021#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6022struct OutlineEntryExcerpt {
6023    id: ExcerptId,
6024    buffer_id: BufferId,
6025    range: ExcerptRange<language::Anchor>,
6026}"),
6027                }
6028            }),
6029        )
6030        .await;
6031        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6032        project.read_with(cx, |project, _| {
6033            project.languages().add(Arc::new(
6034                rust_lang()
6035                    .with_outline_query(
6036                        r#"
6037                (struct_item
6038                    (visibility_modifier)? @context
6039                    "struct" @context
6040                    name: (_) @name) @item
6041
6042                (field_declaration
6043                    (visibility_modifier)? @context
6044                    name: (_) @name) @item
6045"#,
6046                    )
6047                    .unwrap(),
6048            ))
6049        });
6050        let workspace = add_outline_panel(&project, cx).await;
6051        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6052        let outline_panel = outline_panel(&workspace, cx);
6053        cx.update(|window, cx| {
6054            outline_panel.update(cx, |outline_panel, cx| {
6055                outline_panel.set_active(true, window, cx)
6056            });
6057        });
6058
6059        let _editor = workspace
6060            .update(cx, |workspace, window, cx| {
6061                workspace.open_abs_path(
6062                    PathBuf::from(path!("/root/src/lib.rs")),
6063                    OpenOptions {
6064                        visible: Some(OpenVisible::All),
6065                        ..Default::default()
6066                    },
6067                    window,
6068                    cx,
6069                )
6070            })
6071            .unwrap()
6072            .await
6073            .expect("Failed to open Rust source file")
6074            .downcast::<Editor>()
6075            .expect("Should open an editor for Rust source file");
6076
6077        cx.executor()
6078            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6079        cx.run_until_parked();
6080        outline_panel.update(cx, |outline_panel, cx| {
6081            assert_eq!(
6082                display_entries(
6083                    &project,
6084                    &snapshot(outline_panel, cx),
6085                    &outline_panel.cached_entries,
6086                    outline_panel.selected_entry(),
6087                    cx,
6088                ),
6089                indoc!(
6090                    "
6091outline: struct OutlineEntryExcerpt
6092  outline: id
6093  outline: buffer_id
6094  outline: range"
6095                )
6096            );
6097        });
6098
6099        cx.update(|window, cx| {
6100            outline_panel.update(cx, |outline_panel, cx| {
6101                outline_panel.select_next(&SelectNext, window, cx);
6102            });
6103        });
6104        cx.executor()
6105            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6106        cx.run_until_parked();
6107        outline_panel.update(cx, |outline_panel, cx| {
6108            assert_eq!(
6109                display_entries(
6110                    &project,
6111                    &snapshot(outline_panel, cx),
6112                    &outline_panel.cached_entries,
6113                    outline_panel.selected_entry(),
6114                    cx,
6115                ),
6116                indoc!(
6117                    "
6118outline: struct OutlineEntryExcerpt  <==== selected
6119  outline: id
6120  outline: buffer_id
6121  outline: range"
6122                )
6123            );
6124        });
6125
6126        cx.update(|window, cx| {
6127            outline_panel.update(cx, |outline_panel, cx| {
6128                outline_panel.select_next(&SelectNext, window, cx);
6129            });
6130        });
6131        cx.executor()
6132            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6133        cx.run_until_parked();
6134        outline_panel.update(cx, |outline_panel, cx| {
6135            assert_eq!(
6136                display_entries(
6137                    &project,
6138                    &snapshot(outline_panel, cx),
6139                    &outline_panel.cached_entries,
6140                    outline_panel.selected_entry(),
6141                    cx,
6142                ),
6143                indoc!(
6144                    "
6145outline: struct OutlineEntryExcerpt
6146  outline: id  <==== selected
6147  outline: buffer_id
6148  outline: range"
6149                )
6150            );
6151        });
6152
6153        cx.update(|window, cx| {
6154            outline_panel.update(cx, |outline_panel, cx| {
6155                outline_panel.select_next(&SelectNext, window, cx);
6156            });
6157        });
6158        cx.executor()
6159            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6160        cx.run_until_parked();
6161        outline_panel.update(cx, |outline_panel, cx| {
6162            assert_eq!(
6163                display_entries(
6164                    &project,
6165                    &snapshot(outline_panel, cx),
6166                    &outline_panel.cached_entries,
6167                    outline_panel.selected_entry(),
6168                    cx,
6169                ),
6170                indoc!(
6171                    "
6172outline: struct OutlineEntryExcerpt
6173  outline: id
6174  outline: buffer_id  <==== selected
6175  outline: range"
6176                )
6177            );
6178        });
6179
6180        cx.update(|window, cx| {
6181            outline_panel.update(cx, |outline_panel, cx| {
6182                outline_panel.select_next(&SelectNext, window, cx);
6183            });
6184        });
6185        cx.executor()
6186            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6187        cx.run_until_parked();
6188        outline_panel.update(cx, |outline_panel, cx| {
6189            assert_eq!(
6190                display_entries(
6191                    &project,
6192                    &snapshot(outline_panel, cx),
6193                    &outline_panel.cached_entries,
6194                    outline_panel.selected_entry(),
6195                    cx,
6196                ),
6197                indoc!(
6198                    "
6199outline: struct OutlineEntryExcerpt
6200  outline: id
6201  outline: buffer_id
6202  outline: range  <==== selected"
6203                )
6204            );
6205        });
6206
6207        cx.update(|window, cx| {
6208            outline_panel.update(cx, |outline_panel, cx| {
6209                outline_panel.select_next(&SelectNext, window, cx);
6210            });
6211        });
6212        cx.executor()
6213            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6214        cx.run_until_parked();
6215        outline_panel.update(cx, |outline_panel, cx| {
6216            assert_eq!(
6217                display_entries(
6218                    &project,
6219                    &snapshot(outline_panel, cx),
6220                    &outline_panel.cached_entries,
6221                    outline_panel.selected_entry(),
6222                    cx,
6223                ),
6224                indoc!(
6225                    "
6226outline: struct OutlineEntryExcerpt  <==== selected
6227  outline: id
6228  outline: buffer_id
6229  outline: range"
6230                )
6231            );
6232        });
6233
6234        cx.update(|window, cx| {
6235            outline_panel.update(cx, |outline_panel, cx| {
6236                outline_panel.select_previous(&SelectPrevious, window, cx);
6237            });
6238        });
6239        cx.executor()
6240            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6241        cx.run_until_parked();
6242        outline_panel.update(cx, |outline_panel, cx| {
6243            assert_eq!(
6244                display_entries(
6245                    &project,
6246                    &snapshot(outline_panel, cx),
6247                    &outline_panel.cached_entries,
6248                    outline_panel.selected_entry(),
6249                    cx,
6250                ),
6251                indoc!(
6252                    "
6253outline: struct OutlineEntryExcerpt
6254  outline: id
6255  outline: buffer_id
6256  outline: range  <==== selected"
6257                )
6258            );
6259        });
6260
6261        cx.update(|window, cx| {
6262            outline_panel.update(cx, |outline_panel, cx| {
6263                outline_panel.select_previous(&SelectPrevious, window, cx);
6264            });
6265        });
6266        cx.executor()
6267            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6268        cx.run_until_parked();
6269        outline_panel.update(cx, |outline_panel, cx| {
6270            assert_eq!(
6271                display_entries(
6272                    &project,
6273                    &snapshot(outline_panel, cx),
6274                    &outline_panel.cached_entries,
6275                    outline_panel.selected_entry(),
6276                    cx,
6277                ),
6278                indoc!(
6279                    "
6280outline: struct OutlineEntryExcerpt
6281  outline: id
6282  outline: buffer_id  <==== selected
6283  outline: range"
6284                )
6285            );
6286        });
6287
6288        cx.update(|window, cx| {
6289            outline_panel.update(cx, |outline_panel, cx| {
6290                outline_panel.select_previous(&SelectPrevious, window, cx);
6291            });
6292        });
6293        cx.executor()
6294            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6295        cx.run_until_parked();
6296        outline_panel.update(cx, |outline_panel, cx| {
6297            assert_eq!(
6298                display_entries(
6299                    &project,
6300                    &snapshot(outline_panel, cx),
6301                    &outline_panel.cached_entries,
6302                    outline_panel.selected_entry(),
6303                    cx,
6304                ),
6305                indoc!(
6306                    "
6307outline: struct OutlineEntryExcerpt
6308  outline: id  <==== selected
6309  outline: buffer_id
6310  outline: range"
6311                )
6312            );
6313        });
6314
6315        cx.update(|window, cx| {
6316            outline_panel.update(cx, |outline_panel, cx| {
6317                outline_panel.select_previous(&SelectPrevious, window, cx);
6318            });
6319        });
6320        cx.executor()
6321            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6322        cx.run_until_parked();
6323        outline_panel.update(cx, |outline_panel, cx| {
6324            assert_eq!(
6325                display_entries(
6326                    &project,
6327                    &snapshot(outline_panel, cx),
6328                    &outline_panel.cached_entries,
6329                    outline_panel.selected_entry(),
6330                    cx,
6331                ),
6332                indoc!(
6333                    "
6334outline: struct OutlineEntryExcerpt  <==== selected
6335  outline: id
6336  outline: buffer_id
6337  outline: range"
6338                )
6339            );
6340        });
6341
6342        cx.update(|window, cx| {
6343            outline_panel.update(cx, |outline_panel, cx| {
6344                outline_panel.select_previous(&SelectPrevious, window, cx);
6345            });
6346        });
6347        cx.executor()
6348            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6349        cx.run_until_parked();
6350        outline_panel.update(cx, |outline_panel, cx| {
6351            assert_eq!(
6352                display_entries(
6353                    &project,
6354                    &snapshot(outline_panel, cx),
6355                    &outline_panel.cached_entries,
6356                    outline_panel.selected_entry(),
6357                    cx,
6358                ),
6359                indoc!(
6360                    "
6361outline: struct OutlineEntryExcerpt
6362  outline: id
6363  outline: buffer_id
6364  outline: range  <==== selected"
6365                )
6366            );
6367        });
6368    }
6369
6370    #[gpui::test(iterations = 10)]
6371    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6372        init_test(cx);
6373
6374        let root = path!("/frontend-project");
6375        let fs = FakeFs::new(cx.background_executor.clone());
6376        fs.insert_tree(
6377            root,
6378            json!({
6379                "public": {
6380                    "lottie": {
6381                        "syntax-tree.json": r#"{ "something": "static" }"#
6382                    }
6383                },
6384                "src": {
6385                    "app": {
6386                        "(site)": {
6387                            "(about)": {
6388                                "jobs": {
6389                                    "[slug]": {
6390                                        "page.tsx": r#"static"#
6391                                    }
6392                                }
6393                            },
6394                            "(blog)": {
6395                                "post": {
6396                                    "[slug]": {
6397                                        "page.tsx": r#"static"#
6398                                    }
6399                                }
6400                            },
6401                        }
6402                    },
6403                    "components": {
6404                        "ErrorBoundary.tsx": r#"static"#,
6405                    }
6406                }
6407
6408            }),
6409        )
6410        .await;
6411        let project = Project::test(fs.clone(), [Path::new(root)], cx).await;
6412        let workspace = add_outline_panel(&project, cx).await;
6413        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6414        let outline_panel = outline_panel(&workspace, cx);
6415        outline_panel.update_in(cx, |outline_panel, window, cx| {
6416            outline_panel.set_active(true, window, cx)
6417        });
6418
6419        workspace
6420            .update(cx, |workspace, window, cx| {
6421                ProjectSearchView::deploy_search(
6422                    workspace,
6423                    &workspace::DeploySearch::default(),
6424                    window,
6425                    cx,
6426                )
6427            })
6428            .unwrap();
6429        let search_view = workspace
6430            .update(cx, |workspace, _, cx| {
6431                workspace
6432                    .active_pane()
6433                    .read(cx)
6434                    .items()
6435                    .find_map(|item| item.downcast::<ProjectSearchView>())
6436                    .expect("Project search view expected to appear after new search event trigger")
6437            })
6438            .unwrap();
6439
6440        let query = "static";
6441        perform_project_search(&search_view, query, cx);
6442        search_view.update(cx, |search_view, cx| {
6443            search_view
6444                .results_editor()
6445                .update(cx, |results_editor, cx| {
6446                    assert_eq!(
6447                        results_editor.display_text(cx).match_indices(query).count(),
6448                        4
6449                    );
6450                });
6451        });
6452
6453        cx.executor()
6454            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6455        cx.run_until_parked();
6456        outline_panel.update(cx, |outline_panel, cx| {
6457            assert_eq!(
6458                display_entries(
6459                    &project,
6460                    &snapshot(outline_panel, cx),
6461                    &outline_panel.cached_entries,
6462                    outline_panel.selected_entry(),
6463                    cx,
6464                ),
6465                format!(
6466                    r#"frontend-project/
6467  public/lottie/
6468    syntax-tree.json
6469      search: {{ "something": "«static»" }}  <==== selected
6470  src/
6471    app/(site)/
6472      (about)/jobs/[slug]/
6473        page.tsx
6474          search: «static»
6475      (blog)/post/[slug]/
6476        page.tsx
6477          search: «static»
6478    components/
6479      ErrorBoundary.tsx
6480        search: «static»"#
6481                )
6482            );
6483        });
6484
6485        outline_panel.update_in(cx, |outline_panel, window, cx| {
6486            // Move to 5th element in the list, 3 items down.
6487            for _ in 0..2 {
6488                outline_panel.select_next(&SelectNext, window, cx);
6489            }
6490            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6491        });
6492        cx.executor()
6493            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6494        cx.run_until_parked();
6495        outline_panel.update(cx, |outline_panel, cx| {
6496            assert_eq!(
6497                display_entries(
6498                    &project,
6499                    &snapshot(outline_panel, cx),
6500                    &outline_panel.cached_entries,
6501                    outline_panel.selected_entry(),
6502                    cx,
6503                ),
6504                format!(
6505                    r#"frontend-project/
6506  public/lottie/
6507    syntax-tree.json
6508      search: {{ "something": "«static»" }}
6509  src/
6510    app/(site)/  <==== selected
6511    components/
6512      ErrorBoundary.tsx
6513        search: «static»"#
6514                )
6515            );
6516        });
6517
6518        outline_panel.update_in(cx, |outline_panel, window, cx| {
6519            // Move to the next visible non-FS entry
6520            for _ in 0..3 {
6521                outline_panel.select_next(&SelectNext, window, cx);
6522            }
6523        });
6524        cx.run_until_parked();
6525        outline_panel.update(cx, |outline_panel, cx| {
6526            assert_eq!(
6527                display_entries(
6528                    &project,
6529                    &snapshot(outline_panel, cx),
6530                    &outline_panel.cached_entries,
6531                    outline_panel.selected_entry(),
6532                    cx,
6533                ),
6534                format!(
6535                    r#"frontend-project/
6536  public/lottie/
6537    syntax-tree.json
6538      search: {{ "something": "«static»" }}
6539  src/
6540    app/(site)/
6541    components/
6542      ErrorBoundary.tsx
6543        search: «static»  <==== selected"#
6544                )
6545            );
6546        });
6547
6548        outline_panel.update_in(cx, |outline_panel, window, cx| {
6549            outline_panel
6550                .active_editor()
6551                .expect("Should have an active editor")
6552                .update(cx, |editor, cx| {
6553                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6554                });
6555        });
6556        cx.executor()
6557            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6558        cx.run_until_parked();
6559        outline_panel.update(cx, |outline_panel, cx| {
6560            assert_eq!(
6561                display_entries(
6562                    &project,
6563                    &snapshot(outline_panel, cx),
6564                    &outline_panel.cached_entries,
6565                    outline_panel.selected_entry(),
6566                    cx,
6567                ),
6568                format!(
6569                    r#"frontend-project/
6570  public/lottie/
6571    syntax-tree.json
6572      search: {{ "something": "«static»" }}
6573  src/
6574    app/(site)/
6575    components/
6576      ErrorBoundary.tsx  <==== selected"#
6577                )
6578            );
6579        });
6580
6581        outline_panel.update_in(cx, |outline_panel, window, cx| {
6582            outline_panel
6583                .active_editor()
6584                .expect("Should have an active editor")
6585                .update(cx, |editor, cx| {
6586                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6587                });
6588        });
6589        cx.executor()
6590            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6591        cx.run_until_parked();
6592        outline_panel.update(cx, |outline_panel, cx| {
6593            assert_eq!(
6594                display_entries(
6595                    &project,
6596                    &snapshot(outline_panel, cx),
6597                    &outline_panel.cached_entries,
6598                    outline_panel.selected_entry(),
6599                    cx,
6600                ),
6601                format!(
6602                    r#"frontend-project/
6603  public/lottie/
6604    syntax-tree.json  <==== selected
6605  src/
6606    app/(site)/
6607    components/
6608      ErrorBoundary.tsx"#
6609                )
6610            );
6611        });
6612
6613        outline_panel.update_in(cx, |outline_panel, window, cx| {
6614            outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx);
6615        });
6616        cx.executor()
6617            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6618        cx.run_until_parked();
6619        outline_panel.update(cx, |outline_panel, cx| {
6620            assert_eq!(
6621                display_entries(
6622                    &project,
6623                    &snapshot(outline_panel, cx),
6624                    &outline_panel.cached_entries,
6625                    outline_panel.selected_entry(),
6626                    cx,
6627                ),
6628                format!(r#"frontend-project/"#)
6629            );
6630        });
6631
6632        outline_panel.update_in(cx, |outline_panel, window, cx| {
6633            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
6634        });
6635        cx.executor()
6636            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6637        cx.run_until_parked();
6638        outline_panel.update(cx, |outline_panel, cx| {
6639            assert_eq!(
6640                display_entries(
6641                    &project,
6642                    &snapshot(outline_panel, cx),
6643                    &outline_panel.cached_entries,
6644                    outline_panel.selected_entry(),
6645                    cx,
6646                ),
6647                format!(
6648                    r#"frontend-project/
6649  public/lottie/
6650    syntax-tree.json  <==== selected
6651      search: {{ "something": "«static»" }}
6652  src/
6653    app/(site)/
6654      (about)/jobs/[slug]/
6655        page.tsx
6656          search: «static»
6657      (blog)/post/[slug]/
6658        page.tsx
6659          search: «static»
6660    components/
6661      ErrorBoundary.tsx
6662        search: «static»"#
6663                )
6664            );
6665        });
6666    }
6667
6668    async fn add_outline_panel(
6669        project: &Entity<Project>,
6670        cx: &mut TestAppContext,
6671    ) -> WindowHandle<Workspace> {
6672        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6673
6674        let outline_panel = window
6675            .update(cx, |_, window, cx| {
6676                cx.spawn_in(window, async |this, cx| {
6677                    OutlinePanel::load(this, cx.clone()).await
6678                })
6679            })
6680            .unwrap()
6681            .await
6682            .expect("Failed to load outline panel");
6683
6684        window
6685            .update(cx, |workspace, window, cx| {
6686                workspace.add_panel(outline_panel, window, cx);
6687            })
6688            .unwrap();
6689        window
6690    }
6691
6692    fn outline_panel(
6693        workspace: &WindowHandle<Workspace>,
6694        cx: &mut TestAppContext,
6695    ) -> Entity<OutlinePanel> {
6696        workspace
6697            .update(cx, |workspace, _, cx| {
6698                workspace
6699                    .panel::<OutlinePanel>(cx)
6700                    .expect("no outline panel")
6701            })
6702            .unwrap()
6703    }
6704
6705    fn display_entries(
6706        project: &Entity<Project>,
6707        multi_buffer_snapshot: &MultiBufferSnapshot,
6708        cached_entries: &[CachedEntry],
6709        selected_entry: Option<&PanelEntry>,
6710        cx: &mut App,
6711    ) -> String {
6712        let project = project.read(cx);
6713        let mut display_string = String::new();
6714        for entry in cached_entries {
6715            if !display_string.is_empty() {
6716                display_string += "\n";
6717            }
6718            for _ in 0..entry.depth {
6719                display_string += "  ";
6720            }
6721            display_string += &match &entry.entry {
6722                PanelEntry::Fs(entry) => match entry {
6723                    FsEntry::ExternalFile(_) => {
6724                        panic!("Did not cover external files with tests")
6725                    }
6726                    FsEntry::Directory(directory) => {
6727                        let path = if let Some(worktree) = project
6728                            .worktree_for_id(directory.worktree_id, cx)
6729                            .filter(|worktree| {
6730                                worktree.read(cx).root_entry() == Some(&directory.entry.entry)
6731                            }) {
6732                            worktree
6733                                .read(cx)
6734                                .root_name()
6735                                .join(&directory.entry.path)
6736                                .as_unix_str()
6737                                .to_string()
6738                        } else {
6739                            directory
6740                                .entry
6741                                .path
6742                                .file_name()
6743                                .unwrap_or_default()
6744                                .to_string()
6745                        };
6746                        format!("{path}/")
6747                    }
6748                    FsEntry::File(file) => file
6749                        .entry
6750                        .path
6751                        .file_name()
6752                        .map(|name| name.to_string())
6753                        .unwrap_or_default(),
6754                },
6755                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6756                    .entries
6757                    .iter()
6758                    .filter_map(|dir| dir.path.file_name())
6759                    .map(|name| name.to_string() + "/")
6760                    .collect(),
6761                PanelEntry::Outline(outline_entry) => match outline_entry {
6762                    OutlineEntry::Excerpt(_) => continue,
6763                    OutlineEntry::Outline(outline_entry) => {
6764                        format!("outline: {}", outline_entry.outline.text)
6765                    }
6766                },
6767                PanelEntry::Search(search_entry) => {
6768                    let search_data = search_entry.render_data.get_or_init(|| {
6769                        SearchData::new(&search_entry.match_range, multi_buffer_snapshot)
6770                    });
6771                    let mut search_result = String::new();
6772                    let mut last_end = 0;
6773                    for range in &search_data.search_match_indices {
6774                        search_result.push_str(&search_data.context_text[last_end..range.start]);
6775                        search_result.push('«');
6776                        search_result.push_str(&search_data.context_text[range.start..range.end]);
6777                        search_result.push('»');
6778                        last_end = range.end;
6779                    }
6780                    search_result.push_str(&search_data.context_text[last_end..]);
6781
6782                    format!("search: {search_result}")
6783                }
6784            };
6785
6786            if Some(&entry.entry) == selected_entry {
6787                display_string += SELECTED_MARKER;
6788            }
6789        }
6790        display_string
6791    }
6792
6793    fn init_test(cx: &mut TestAppContext) {
6794        cx.update(|cx| {
6795            let settings = SettingsStore::test(cx);
6796            cx.set_global(settings);
6797
6798            theme::init(theme::LoadThemes::JustBase, cx);
6799
6800            editor::init(cx);
6801            project_search::init(cx);
6802            buffer_search::init(cx);
6803            super::init(cx);
6804        });
6805    }
6806
6807    // Based on https://github.com/rust-lang/rust-analyzer/
6808    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6809        fs.insert_tree(
6810            root,
6811            json!({
6812                    "crates": {
6813                        "ide": {
6814                            "src": {
6815                                "inlay_hints": {
6816                                    "fn_lifetime_fn.rs": r##"
6817        pub(super) fn hints(
6818            acc: &mut Vec<InlayHint>,
6819            config: &InlayHintsConfig,
6820            func: ast::Fn,
6821        ) -> Option<()> {
6822            // ... snip
6823
6824            let mut used_names: FxHashMap<SmolStr, usize> =
6825                match config.param_names_for_lifetime_elision_hints {
6826                    true => generic_param_list
6827                        .iter()
6828                        .flat_map(|gpl| gpl.lifetime_params())
6829                        .filter_map(|param| param.lifetime())
6830                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6831                        .collect(),
6832                    false => Default::default(),
6833                };
6834            {
6835                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6836                if self_param.is_some() && potential_lt_refs.next().is_some() {
6837                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6838                        // self can't be used as a lifetime, so no need to check for collisions
6839                        "'self".into()
6840                    } else {
6841                        gen_idx_name()
6842                    });
6843                }
6844                potential_lt_refs.for_each(|(name, ..)| {
6845                    let name = match name {
6846                        Some(it) if config.param_names_for_lifetime_elision_hints => {
6847                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
6848                                *c += 1;
6849                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6850                            } else {
6851                                used_names.insert(it.text().as_str().into(), 0);
6852                                SmolStr::from_iter(["\'", it.text().as_str()])
6853                            }
6854                        }
6855                        _ => gen_idx_name(),
6856                    };
6857                    allocated_lifetimes.push(name);
6858                });
6859            }
6860
6861            // ... snip
6862        }
6863
6864        // ... snip
6865
6866            #[test]
6867            fn hints_lifetimes_named() {
6868                check_with_config(
6869                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6870                    r#"
6871        fn nested_in<'named>(named: &        &X<      &()>) {}
6872        //          ^'named1, 'named2, 'named3, $
6873                                  //^'named1 ^'named2 ^'named3
6874        "#,
6875                );
6876            }
6877
6878        // ... snip
6879        "##,
6880                                },
6881                        "inlay_hints.rs": r#"
6882    #[derive(Clone, Debug, PartialEq, Eq)]
6883    pub struct InlayHintsConfig {
6884        // ... snip
6885        pub param_names_for_lifetime_elision_hints: bool,
6886        pub max_length: Option<usize>,
6887        // ... snip
6888    }
6889
6890    impl Config {
6891        pub fn inlay_hints(&self) -> InlayHintsConfig {
6892            InlayHintsConfig {
6893                // ... snip
6894                param_names_for_lifetime_elision_hints: self
6895                    .inlayHints_lifetimeElisionHints_useParameterNames()
6896                    .to_owned(),
6897                max_length: self.inlayHints_maxLength().to_owned(),
6898                // ... snip
6899            }
6900        }
6901    }
6902    "#,
6903                        "static_index.rs": r#"
6904// ... snip
6905        fn add_file(&mut self, file_id: FileId) {
6906            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6907            let folds = self.analysis.folding_ranges(file_id).unwrap();
6908            let inlay_hints = self
6909                .analysis
6910                .inlay_hints(
6911                    &InlayHintsConfig {
6912                        // ... snip
6913                        closure_style: hir::ClosureStyle::ImplFn,
6914                        param_names_for_lifetime_elision_hints: false,
6915                        binding_mode_hints: false,
6916                        max_length: Some(25),
6917                        closure_capture_hints: false,
6918                        // ... snip
6919                    },
6920                    file_id,
6921                    None,
6922                )
6923                .unwrap();
6924            // ... snip
6925    }
6926// ... snip
6927    "#
6928                            }
6929                        },
6930                        "rust-analyzer": {
6931                            "src": {
6932                                "cli": {
6933                                    "analysis_stats.rs": r#"
6934        // ... snip
6935                for &file_id in &file_ids {
6936                    _ = analysis.inlay_hints(
6937                        &InlayHintsConfig {
6938                            // ... snip
6939                            implicit_drop_hints: true,
6940                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6941                            param_names_for_lifetime_elision_hints: true,
6942                            hide_named_constructor_hints: false,
6943                            hide_closure_initialization_hints: false,
6944                            closure_style: hir::ClosureStyle::ImplFn,
6945                            max_length: Some(25),
6946                            closing_brace_hints_min_lines: Some(20),
6947                            fields_to_resolve: InlayFieldsToResolve::empty(),
6948                            range_exclusive_hints: true,
6949                        },
6950                        file_id.into(),
6951                        None,
6952                    );
6953                }
6954        // ... snip
6955                                    "#,
6956                                },
6957                                "config.rs": r#"
6958                config_data! {
6959                    /// Configs that only make sense when they are set by a client. As such they can only be defined
6960                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
6961                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6962                        // ... snip
6963                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
6964                        inlayHints_maxLength: Option<usize>                        = Some(25),
6965                        // ... snip
6966                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6967                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
6968                        // ... snip
6969                    }
6970                }
6971
6972                impl Config {
6973                    // ... snip
6974                    pub fn inlay_hints(&self) -> InlayHintsConfig {
6975                        InlayHintsConfig {
6976                            // ... snip
6977                            param_names_for_lifetime_elision_hints: self
6978                                .inlayHints_lifetimeElisionHints_useParameterNames()
6979                                .to_owned(),
6980                            max_length: self.inlayHints_maxLength().to_owned(),
6981                            // ... snip
6982                        }
6983                    }
6984                    // ... snip
6985                }
6986                "#
6987                                }
6988                        }
6989                    }
6990            }),
6991        )
6992        .await;
6993    }
6994
6995    fn rust_lang() -> Language {
6996        Language::new(
6997            LanguageConfig {
6998                name: "Rust".into(),
6999                matcher: LanguageMatcher {
7000                    path_suffixes: vec!["rs".to_string()],
7001                    ..Default::default()
7002                },
7003                ..Default::default()
7004            },
7005            Some(tree_sitter_rust::LANGUAGE.into()),
7006        )
7007        .with_highlights_query(
7008            r#"
7009                (field_identifier) @field
7010                (struct_expression) @struct
7011            "#,
7012        )
7013        .unwrap()
7014        .with_injection_query(
7015            r#"
7016                (macro_invocation
7017                    (token_tree) @injection.content
7018                    (#set! injection.language "rust"))
7019            "#,
7020        )
7021        .unwrap()
7022    }
7023
7024    fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
7025        outline_panel
7026            .active_editor()
7027            .unwrap()
7028            .read(cx)
7029            .buffer()
7030            .read(cx)
7031            .snapshot(cx)
7032    }
7033
7034    fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
7035        editor.update(cx, |editor, cx| {
7036            let selections = editor.selections.all::<language::Point>(&editor.display_snapshot(cx));
7037            assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
7038            let selection = selections.first().unwrap();
7039            let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
7040            let line_start = language::Point::new(selection.start.row, 0);
7041            let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
7042            multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
7043        })
7044    }
7045
7046    #[gpui::test]
7047    async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) {
7048        init_test(cx);
7049
7050        let fs = FakeFs::new(cx.background_executor.clone());
7051        fs.insert_tree(
7052            "/test",
7053            json!({
7054                "src": {
7055                    "lib.rs": indoc!("
7056                            mod outer {
7057                                pub struct OuterStruct {
7058                                    field: String,
7059                                }
7060                                impl OuterStruct {
7061                                    pub fn new() -> Self {
7062                                        Self { field: String::new() }
7063                                    }
7064                                    pub fn method(&self) {
7065                                        println!(\"{}\", self.field);
7066                                    }
7067                                }
7068                                mod inner {
7069                                    pub fn inner_function() {
7070                                        let x = 42;
7071                                        println!(\"{}\", x);
7072                                    }
7073                                    pub struct InnerStruct {
7074                                        value: i32,
7075                                    }
7076                                }
7077                            }
7078                            fn main() {
7079                                let s = outer::OuterStruct::new();
7080                                s.method();
7081                            }
7082                        "),
7083                }
7084            }),
7085        )
7086        .await;
7087
7088        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7089        project.read_with(cx, |project, _| {
7090            project.languages().add(Arc::new(
7091                rust_lang()
7092                    .with_outline_query(
7093                        r#"
7094                            (struct_item
7095                                (visibility_modifier)? @context
7096                                "struct" @context
7097                                name: (_) @name) @item
7098                            (impl_item
7099                                "impl" @context
7100                                trait: (_)? @context
7101                                "for"? @context
7102                                type: (_) @context
7103                                body: (_)) @item
7104                            (function_item
7105                                (visibility_modifier)? @context
7106                                "fn" @context
7107                                name: (_) @name
7108                                parameters: (_) @context) @item
7109                            (mod_item
7110                                (visibility_modifier)? @context
7111                                "mod" @context
7112                                name: (_) @name) @item
7113                            (enum_item
7114                                (visibility_modifier)? @context
7115                                "enum" @context
7116                                name: (_) @name) @item
7117                            (field_declaration
7118                                (visibility_modifier)? @context
7119                                name: (_) @name
7120                                ":" @context
7121                                type: (_) @context) @item
7122                            "#,
7123                    )
7124                    .unwrap(),
7125            ))
7126        });
7127        let workspace = add_outline_panel(&project, cx).await;
7128        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7129        let outline_panel = outline_panel(&workspace, cx);
7130
7131        outline_panel.update_in(cx, |outline_panel, window, cx| {
7132            outline_panel.set_active(true, window, cx)
7133        });
7134
7135        workspace
7136            .update(cx, |workspace, window, cx| {
7137                workspace.open_abs_path(
7138                    PathBuf::from("/test/src/lib.rs"),
7139                    OpenOptions {
7140                        visible: Some(OpenVisible::All),
7141                        ..Default::default()
7142                    },
7143                    window,
7144                    cx,
7145                )
7146            })
7147            .unwrap()
7148            .await
7149            .unwrap();
7150
7151        cx.executor()
7152            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7153        cx.run_until_parked();
7154
7155        // Force another update cycle to ensure outlines are fetched
7156        outline_panel.update_in(cx, |panel, window, cx| {
7157            panel.update_non_fs_items(window, cx);
7158            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7159        });
7160        cx.executor()
7161            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7162        cx.run_until_parked();
7163
7164        outline_panel.update(cx, |outline_panel, cx| {
7165            assert_eq!(
7166                display_entries(
7167                    &project,
7168                    &snapshot(outline_panel, cx),
7169                    &outline_panel.cached_entries,
7170                    outline_panel.selected_entry(),
7171                    cx,
7172                ),
7173                indoc!(
7174                    "
7175outline: mod outer  <==== selected
7176  outline: pub struct OuterStruct
7177    outline: field: String
7178  outline: impl OuterStruct
7179    outline: pub fn new()
7180    outline: pub fn method(&self)
7181  outline: mod inner
7182    outline: pub fn inner_function()
7183    outline: pub struct InnerStruct
7184      outline: value: i32
7185outline: fn main()"
7186                )
7187            );
7188        });
7189
7190        let parent_outline = outline_panel
7191            .read_with(cx, |panel, _cx| {
7192                panel
7193                    .cached_entries
7194                    .iter()
7195                    .find_map(|entry| match &entry.entry {
7196                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7197                            if panel
7198                                .outline_children_cache
7199                                .get(&outline.buffer_id)
7200                                .and_then(|children_map| {
7201                                    let key =
7202                                        (outline.outline.range.clone(), outline.outline.depth);
7203                                    children_map.get(&key)
7204                                })
7205                                .copied()
7206                                .unwrap_or(false) =>
7207                        {
7208                            Some(entry.entry.clone())
7209                        }
7210                        _ => None,
7211                    })
7212            })
7213            .expect("Should find an outline with children");
7214
7215        outline_panel.update_in(cx, |panel, window, cx| {
7216            panel.select_entry(parent_outline.clone(), true, window, cx);
7217            panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7218        });
7219        cx.executor()
7220            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7221        cx.run_until_parked();
7222
7223        outline_panel.update(cx, |outline_panel, cx| {
7224            assert_eq!(
7225                display_entries(
7226                    &project,
7227                    &snapshot(outline_panel, cx),
7228                    &outline_panel.cached_entries,
7229                    outline_panel.selected_entry(),
7230                    cx,
7231                ),
7232                indoc!(
7233                    "
7234outline: mod outer  <==== selected
7235outline: fn main()"
7236                )
7237            );
7238        });
7239
7240        outline_panel.update_in(cx, |panel, window, cx| {
7241            panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7242        });
7243        cx.executor()
7244            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7245        cx.run_until_parked();
7246
7247        outline_panel.update(cx, |outline_panel, cx| {
7248            assert_eq!(
7249                display_entries(
7250                    &project,
7251                    &snapshot(outline_panel, cx),
7252                    &outline_panel.cached_entries,
7253                    outline_panel.selected_entry(),
7254                    cx,
7255                ),
7256                indoc!(
7257                    "
7258outline: mod outer  <==== selected
7259  outline: pub struct OuterStruct
7260    outline: field: String
7261  outline: impl OuterStruct
7262    outline: pub fn new()
7263    outline: pub fn method(&self)
7264  outline: mod inner
7265    outline: pub fn inner_function()
7266    outline: pub struct InnerStruct
7267      outline: value: i32
7268outline: fn main()"
7269                )
7270            );
7271        });
7272
7273        outline_panel.update_in(cx, |panel, window, cx| {
7274            panel.collapsed_entries.clear();
7275            panel.update_cached_entries(None, window, cx);
7276        });
7277        cx.executor()
7278            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7279        cx.run_until_parked();
7280
7281        outline_panel.update_in(cx, |panel, window, cx| {
7282            let outlines_with_children: Vec<_> = panel
7283                .cached_entries
7284                .iter()
7285                .filter_map(|entry| match &entry.entry {
7286                    PanelEntry::Outline(OutlineEntry::Outline(outline))
7287                        if panel
7288                            .outline_children_cache
7289                            .get(&outline.buffer_id)
7290                            .and_then(|children_map| {
7291                                let key = (outline.outline.range.clone(), outline.outline.depth);
7292                                children_map.get(&key)
7293                            })
7294                            .copied()
7295                            .unwrap_or(false) =>
7296                    {
7297                        Some(entry.entry.clone())
7298                    }
7299                    _ => None,
7300                })
7301                .collect();
7302
7303            for outline in outlines_with_children {
7304                panel.select_entry(outline, false, window, cx);
7305                panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7306            }
7307        });
7308        cx.executor()
7309            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7310        cx.run_until_parked();
7311
7312        outline_panel.update(cx, |outline_panel, cx| {
7313            assert_eq!(
7314                display_entries(
7315                    &project,
7316                    &snapshot(outline_panel, cx),
7317                    &outline_panel.cached_entries,
7318                    outline_panel.selected_entry(),
7319                    cx,
7320                ),
7321                indoc!(
7322                    "
7323outline: mod outer
7324outline: fn main()"
7325                )
7326            );
7327        });
7328
7329        let collapsed_entries_count =
7330            outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len());
7331        assert!(
7332            collapsed_entries_count > 0,
7333            "Should have collapsed entries tracked"
7334        );
7335    }
7336
7337    #[gpui::test]
7338    async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) {
7339        init_test(cx);
7340
7341        let fs = FakeFs::new(cx.background_executor.clone());
7342        fs.insert_tree(
7343            "/test",
7344            json!({
7345                "src": {
7346                    "main.rs": indoc!("
7347                            struct Config {
7348                                name: String,
7349                                value: i32,
7350                            }
7351                            impl Config {
7352                                fn new(name: String) -> Self {
7353                                    Self { name, value: 0 }
7354                                }
7355                                fn get_value(&self) -> i32 {
7356                                    self.value
7357                                }
7358                            }
7359                            enum Status {
7360                                Active,
7361                                Inactive,
7362                            }
7363                            fn process_config(config: Config) -> Status {
7364                                if config.get_value() > 0 {
7365                                    Status::Active
7366                                } else {
7367                                    Status::Inactive
7368                                }
7369                            }
7370                            fn main() {
7371                                let config = Config::new(\"test\".to_string());
7372                                let status = process_config(config);
7373                            }
7374                        "),
7375                }
7376            }),
7377        )
7378        .await;
7379
7380        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7381        project.read_with(cx, |project, _| {
7382            project.languages().add(Arc::new(
7383                rust_lang()
7384                    .with_outline_query(
7385                        r#"
7386                            (struct_item
7387                                (visibility_modifier)? @context
7388                                "struct" @context
7389                                name: (_) @name) @item
7390                            (impl_item
7391                                "impl" @context
7392                                trait: (_)? @context
7393                                "for"? @context
7394                                type: (_) @context
7395                                body: (_)) @item
7396                            (function_item
7397                                (visibility_modifier)? @context
7398                                "fn" @context
7399                                name: (_) @name
7400                                parameters: (_) @context) @item
7401                            (mod_item
7402                                (visibility_modifier)? @context
7403                                "mod" @context
7404                                name: (_) @name) @item
7405                            (enum_item
7406                                (visibility_modifier)? @context
7407                                "enum" @context
7408                                name: (_) @name) @item
7409                            (field_declaration
7410                                (visibility_modifier)? @context
7411                                name: (_) @name
7412                                ":" @context
7413                                type: (_) @context) @item
7414                            "#,
7415                    )
7416                    .unwrap(),
7417            ))
7418        });
7419
7420        let workspace = add_outline_panel(&project, cx).await;
7421        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7422        let outline_panel = outline_panel(&workspace, cx);
7423
7424        outline_panel.update_in(cx, |outline_panel, window, cx| {
7425            outline_panel.set_active(true, window, cx)
7426        });
7427
7428        let _editor = workspace
7429            .update(cx, |workspace, window, cx| {
7430                workspace.open_abs_path(
7431                    PathBuf::from("/test/src/main.rs"),
7432                    OpenOptions {
7433                        visible: Some(OpenVisible::All),
7434                        ..Default::default()
7435                    },
7436                    window,
7437                    cx,
7438                )
7439            })
7440            .unwrap()
7441            .await
7442            .unwrap();
7443
7444        cx.executor()
7445            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7446        cx.run_until_parked();
7447
7448        outline_panel.update(cx, |outline_panel, _cx| {
7449            outline_panel.selected_entry = SelectedEntry::None;
7450        });
7451
7452        // Check initial state - all entries should be expanded by default
7453        outline_panel.update(cx, |outline_panel, cx| {
7454            assert_eq!(
7455                display_entries(
7456                    &project,
7457                    &snapshot(outline_panel, cx),
7458                    &outline_panel.cached_entries,
7459                    outline_panel.selected_entry(),
7460                    cx,
7461                ),
7462                indoc!(
7463                    "
7464outline: struct Config
7465  outline: name: String
7466  outline: value: i32
7467outline: impl Config
7468  outline: fn new(name: String)
7469  outline: fn get_value(&self)
7470outline: enum Status
7471outline: fn process_config(config: Config)
7472outline: fn main()"
7473                )
7474            );
7475        });
7476
7477        outline_panel.update(cx, |outline_panel, _cx| {
7478            outline_panel.selected_entry = SelectedEntry::None;
7479        });
7480
7481        cx.update(|window, cx| {
7482            outline_panel.update(cx, |outline_panel, cx| {
7483                outline_panel.select_first(&SelectFirst, window, cx);
7484            });
7485        });
7486
7487        cx.executor()
7488            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7489        cx.run_until_parked();
7490
7491        outline_panel.update(cx, |outline_panel, cx| {
7492            assert_eq!(
7493                display_entries(
7494                    &project,
7495                    &snapshot(outline_panel, cx),
7496                    &outline_panel.cached_entries,
7497                    outline_panel.selected_entry(),
7498                    cx,
7499                ),
7500                indoc!(
7501                    "
7502outline: struct Config  <==== selected
7503  outline: name: String
7504  outline: value: i32
7505outline: impl Config
7506  outline: fn new(name: String)
7507  outline: fn get_value(&self)
7508outline: enum Status
7509outline: fn process_config(config: Config)
7510outline: fn main()"
7511                )
7512            );
7513        });
7514
7515        cx.update(|window, cx| {
7516            outline_panel.update(cx, |outline_panel, cx| {
7517                outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
7518            });
7519        });
7520
7521        cx.executor()
7522            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7523        cx.run_until_parked();
7524
7525        outline_panel.update(cx, |outline_panel, cx| {
7526            assert_eq!(
7527                display_entries(
7528                    &project,
7529                    &snapshot(outline_panel, cx),
7530                    &outline_panel.cached_entries,
7531                    outline_panel.selected_entry(),
7532                    cx,
7533                ),
7534                indoc!(
7535                    "
7536outline: struct Config  <==== selected
7537outline: impl Config
7538  outline: fn new(name: String)
7539  outline: fn get_value(&self)
7540outline: enum Status
7541outline: fn process_config(config: Config)
7542outline: fn main()"
7543                )
7544            );
7545        });
7546
7547        cx.update(|window, cx| {
7548            outline_panel.update(cx, |outline_panel, cx| {
7549                outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
7550            });
7551        });
7552
7553        cx.executor()
7554            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7555        cx.run_until_parked();
7556
7557        outline_panel.update(cx, |outline_panel, cx| {
7558            assert_eq!(
7559                display_entries(
7560                    &project,
7561                    &snapshot(outline_panel, cx),
7562                    &outline_panel.cached_entries,
7563                    outline_panel.selected_entry(),
7564                    cx,
7565                ),
7566                indoc!(
7567                    "
7568outline: struct Config  <==== selected
7569  outline: name: String
7570  outline: value: i32
7571outline: impl Config
7572  outline: fn new(name: String)
7573  outline: fn get_value(&self)
7574outline: enum Status
7575outline: fn process_config(config: Config)
7576outline: fn main()"
7577                )
7578            );
7579        });
7580    }
7581
7582    #[gpui::test]
7583    async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) {
7584        init_test(cx);
7585
7586        let fs = FakeFs::new(cx.background_executor.clone());
7587        fs.insert_tree(
7588            "/test",
7589            json!({
7590                "src": {
7591                    "lib.rs": indoc!("
7592                            mod outer {
7593                                pub struct OuterStruct {
7594                                    field: String,
7595                                }
7596                                impl OuterStruct {
7597                                    pub fn new() -> Self {
7598                                        Self { field: String::new() }
7599                                    }
7600                                    pub fn method(&self) {
7601                                        println!(\"{}\", self.field);
7602                                    }
7603                                }
7604                                mod inner {
7605                                    pub fn inner_function() {
7606                                        let x = 42;
7607                                        println!(\"{}\", x);
7608                                    }
7609                                    pub struct InnerStruct {
7610                                        value: i32,
7611                                    }
7612                                }
7613                            }
7614                            fn main() {
7615                                let s = outer::OuterStruct::new();
7616                                s.method();
7617                            }
7618                        "),
7619                }
7620            }),
7621        )
7622        .await;
7623
7624        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7625        project.read_with(cx, |project, _| {
7626            project.languages().add(Arc::new(
7627                rust_lang()
7628                    .with_outline_query(
7629                        r#"
7630                            (struct_item
7631                                (visibility_modifier)? @context
7632                                "struct" @context
7633                                name: (_) @name) @item
7634                            (impl_item
7635                                "impl" @context
7636                                trait: (_)? @context
7637                                "for"? @context
7638                                type: (_) @context
7639                                body: (_)) @item
7640                            (function_item
7641                                (visibility_modifier)? @context
7642                                "fn" @context
7643                                name: (_) @name
7644                                parameters: (_) @context) @item
7645                            (mod_item
7646                                (visibility_modifier)? @context
7647                                "mod" @context
7648                                name: (_) @name) @item
7649                            (enum_item
7650                                (visibility_modifier)? @context
7651                                "enum" @context
7652                                name: (_) @name) @item
7653                            (field_declaration
7654                                (visibility_modifier)? @context
7655                                name: (_) @name
7656                                ":" @context
7657                                type: (_) @context) @item
7658                            "#,
7659                    )
7660                    .unwrap(),
7661            ))
7662        });
7663        let workspace = add_outline_panel(&project, cx).await;
7664        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7665        let outline_panel = outline_panel(&workspace, cx);
7666
7667        outline_panel.update_in(cx, |outline_panel, window, cx| {
7668            outline_panel.set_active(true, window, cx)
7669        });
7670
7671        workspace
7672            .update(cx, |workspace, window, cx| {
7673                workspace.open_abs_path(
7674                    PathBuf::from("/test/src/lib.rs"),
7675                    OpenOptions {
7676                        visible: Some(OpenVisible::All),
7677                        ..Default::default()
7678                    },
7679                    window,
7680                    cx,
7681                )
7682            })
7683            .unwrap()
7684            .await
7685            .unwrap();
7686
7687        cx.executor()
7688            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7689        cx.run_until_parked();
7690
7691        // Force another update cycle to ensure outlines are fetched
7692        outline_panel.update_in(cx, |panel, window, cx| {
7693            panel.update_non_fs_items(window, cx);
7694            panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
7695        });
7696        cx.executor()
7697            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7698        cx.run_until_parked();
7699
7700        outline_panel.update(cx, |outline_panel, cx| {
7701            assert_eq!(
7702                display_entries(
7703                    &project,
7704                    &snapshot(outline_panel, cx),
7705                    &outline_panel.cached_entries,
7706                    outline_panel.selected_entry(),
7707                    cx,
7708                ),
7709                indoc!(
7710                    "
7711outline: mod outer  <==== selected
7712  outline: pub struct OuterStruct
7713    outline: field: String
7714  outline: impl OuterStruct
7715    outline: pub fn new()
7716    outline: pub fn method(&self)
7717  outline: mod inner
7718    outline: pub fn inner_function()
7719    outline: pub struct InnerStruct
7720      outline: value: i32
7721outline: fn main()"
7722                )
7723            );
7724        });
7725
7726        let _parent_outline = outline_panel
7727            .read_with(cx, |panel, _cx| {
7728                panel
7729                    .cached_entries
7730                    .iter()
7731                    .find_map(|entry| match &entry.entry {
7732                        PanelEntry::Outline(OutlineEntry::Outline(outline))
7733                            if panel
7734                                .outline_children_cache
7735                                .get(&outline.buffer_id)
7736                                .and_then(|children_map| {
7737                                    let key =
7738                                        (outline.outline.range.clone(), outline.outline.depth);
7739                                    children_map.get(&key)
7740                                })
7741                                .copied()
7742                                .unwrap_or(false) =>
7743                        {
7744                            Some(entry.entry.clone())
7745                        }
7746                        _ => None,
7747                    })
7748            })
7749            .expect("Should find an outline with children");
7750
7751        // Collapse all entries
7752        outline_panel.update_in(cx, |panel, window, cx| {
7753            panel.collapse_all_entries(&CollapseAllEntries, window, cx);
7754        });
7755        cx.executor()
7756            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7757        cx.run_until_parked();
7758
7759        let expected_collapsed_output = indoc!(
7760            "
7761        outline: mod outer  <==== selected
7762        outline: fn main()"
7763        );
7764
7765        outline_panel.update(cx, |panel, cx| {
7766            assert_eq! {
7767                display_entries(
7768                    &project,
7769                    &snapshot(panel, cx),
7770                    &panel.cached_entries,
7771                    panel.selected_entry(),
7772                    cx,
7773                ),
7774                expected_collapsed_output
7775            };
7776        });
7777
7778        // Expand all entries
7779        outline_panel.update_in(cx, |panel, window, cx| {
7780            panel.expand_all_entries(&ExpandAllEntries, window, cx);
7781        });
7782        cx.executor()
7783            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
7784        cx.run_until_parked();
7785
7786        let expected_expanded_output = indoc!(
7787            "
7788        outline: mod outer  <==== selected
7789          outline: pub struct OuterStruct
7790            outline: field: String
7791          outline: impl OuterStruct
7792            outline: pub fn new()
7793            outline: pub fn method(&self)
7794          outline: mod inner
7795            outline: pub fn inner_function()
7796            outline: pub struct InnerStruct
7797              outline: value: i32
7798        outline: fn main()"
7799        );
7800
7801        outline_panel.update(cx, |panel, cx| {
7802            assert_eq! {
7803                display_entries(
7804                    &project,
7805                    &snapshot(panel, cx),
7806                    &panel.cached_entries,
7807                    panel.selected_entry(),
7808                    cx,
7809                ),
7810                expected_expanded_output
7811            };
7812        });
7813    }
7814
7815    #[gpui::test]
7816    async fn test_buffer_search(cx: &mut TestAppContext) {
7817        init_test(cx);
7818
7819        let fs = FakeFs::new(cx.background_executor.clone());
7820        fs.insert_tree(
7821            "/test",
7822            json!({
7823                "foo.txt": r#"<_constitution>
7824
7825</_constitution>
7826
7827
7828
7829## 📊 Output
7830
7831| Field          | Meaning                |
7832"#
7833            }),
7834        )
7835        .await;
7836
7837        let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
7838        let workspace = add_outline_panel(&project, cx).await;
7839        let cx = &mut VisualTestContext::from_window(*workspace, cx);
7840
7841        let editor = workspace
7842            .update(cx, |workspace, window, cx| {
7843                workspace.open_abs_path(
7844                    PathBuf::from("/test/foo.txt"),
7845                    OpenOptions {
7846                        visible: Some(OpenVisible::All),
7847                        ..OpenOptions::default()
7848                    },
7849                    window,
7850                    cx,
7851                )
7852            })
7853            .unwrap()
7854            .await
7855            .unwrap()
7856            .downcast::<Editor>()
7857            .unwrap();
7858
7859        let search_bar = workspace
7860            .update(cx, |_, window, cx| {
7861                cx.new(|cx| {
7862                    let mut search_bar = BufferSearchBar::new(None, window, cx);
7863                    search_bar.set_active_pane_item(Some(&editor), window, cx);
7864                    search_bar.show(window, cx);
7865                    search_bar
7866                })
7867            })
7868            .unwrap();
7869
7870        let outline_panel = outline_panel(&workspace, cx);
7871
7872        outline_panel.update_in(cx, |outline_panel, window, cx| {
7873            outline_panel.set_active(true, window, cx)
7874        });
7875
7876        search_bar
7877            .update_in(cx, |search_bar, window, cx| {
7878                search_bar.search("  ", None, true, window, cx)
7879            })
7880            .await
7881            .unwrap();
7882
7883        cx.executor()
7884            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500));
7885        cx.run_until_parked();
7886
7887        outline_panel.update(cx, |outline_panel, cx| {
7888            assert_eq!(
7889                display_entries(
7890                    &project,
7891                    &snapshot(outline_panel, cx),
7892                    &outline_panel.cached_entries,
7893                    outline_panel.selected_entry(),
7894                    cx,
7895                ),
7896                "search: | Field«  »        | Meaning                |  <==== selected
7897search: | Field  «  »      | Meaning                |
7898search: | Field    «  »    | Meaning                |
7899search: | Field      «  »  | Meaning                |
7900search: | Field        «  »| Meaning                |
7901search: | Field          | Meaning«  »              |
7902search: | Field          | Meaning  «  »            |
7903search: | Field          | Meaning    «  »          |
7904search: | Field          | Meaning      «  »        |
7905search: | Field          | Meaning        «  »      |
7906search: | Field          | Meaning          «  »    |
7907search: | Field          | Meaning            «  »  |
7908search: | Field          | Meaning              «  »|"
7909            );
7910        });
7911    }
7912}