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