outline_panel.rs

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