outline_panel.rs

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