outline_panel.rs

   1mod outline_panel_settings;
   2
   3use std::{
   4    cmp,
   5    collections::BTreeMap,
   6    hash::Hash,
   7    ops::Range,
   8    path::{Path, PathBuf, MAIN_SEPARATOR_STR},
   9    sync::{
  10        atomic::{self, AtomicBool},
  11        Arc, OnceLock,
  12    },
  13    time::Duration,
  14    u32,
  15};
  16
  17use anyhow::Context as _;
  18use collections::{hash_map, BTreeSet, HashMap, HashSet};
  19use db::kvp::KEY_VALUE_STORE;
  20use editor::{
  21    display_map::ToDisplayPoint,
  22    items::{entry_git_aware_label_color, entry_label_color},
  23    scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor, ScrollbarAutoHide},
  24    AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, EditorMode, EditorSettings, ExcerptId,
  25    ExcerptRange, MultiBufferSnapshot, RangeToAnchorExt, ShowScrollbar,
  26};
  27use file_icons::FileIcons;
  28use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
  29use gpui::{
  30    actions, anchored, deferred, div, point, px, size, uniform_list, Action, AnyElement, App,
  31    AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context, DismissEvent, Div,
  32    ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, InteractiveElement,
  33    IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton,
  34    MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy, SharedString, Stateful,
  35    StatefulInteractiveElement as _, Styled, Subscription, Task, UniformListScrollHandle,
  36    WeakEntity, Window,
  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, 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::{debug_panic, RangeExt, ResultExt, TryFutureExt};
  51use workspace::{
  52    dock::{DockPosition, Panel, PanelEvent},
  53    item::ItemHandle,
  54    searchable::{SearchEvent, SearchableItem},
  55    ui::{
  56        h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, ContextMenu, FluentBuilder,
  57        HighlightedLabel, Icon, IconButton, IconButtonShape, IconName, IconSize, Label,
  58        LabelCommon, ListItem, Scrollbar, ScrollbarState, StyledExt, StyledTypography, Toggleable,
  59        Tooltip,
  60    },
  61    OpenInTerminal, WeakItemHandle, Workspace,
  62};
  63use worktree::{Entry, GitEntry, 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, |outline_panel, mut cx| async move {
 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(&mut 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)),
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 {
1771                    !folded
1772                } else {
1773                    folded
1774                }
1775            });
1776        });
1777
1778        self.select_entry(entry.clone(), true, window, cx);
1779        if buffers_to_toggle.is_empty() {
1780            self.update_cached_entries(None, window, cx);
1781        } else {
1782            self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx)
1783                .detach();
1784        }
1785    }
1786
1787    fn toggle_buffers_fold(
1788        &self,
1789        buffers: HashSet<BufferId>,
1790        fold: bool,
1791        window: &mut Window,
1792        cx: &mut Context<Self>,
1793    ) -> Task<()> {
1794        let Some(active_editor) = self.active_editor() else {
1795            return Task::ready(());
1796        };
1797        cx.spawn_in(window, |outline_panel, mut cx| async move {
1798            outline_panel
1799                .update_in(&mut cx, |outline_panel, window, cx| {
1800                    active_editor.update(cx, |editor, cx| {
1801                        for buffer_id in buffers {
1802                            outline_panel
1803                                .preserve_selection_on_buffer_fold_toggles
1804                                .insert(buffer_id);
1805                            if fold {
1806                                editor.fold_buffer(buffer_id, cx);
1807                            } else {
1808                                editor.unfold_buffer(buffer_id, cx);
1809                            }
1810                        }
1811                    });
1812                    if let Some(selection) = outline_panel.selected_entry().cloned() {
1813                        outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx);
1814                    }
1815                })
1816                .ok();
1817        })
1818    }
1819
1820    fn copy_path(
1821        &mut self,
1822        _: &zed_actions::workspace::CopyPath,
1823        _: &mut Window,
1824        cx: &mut Context<Self>,
1825    ) {
1826        if let Some(clipboard_text) = self
1827            .selected_entry()
1828            .and_then(|entry| self.abs_path(entry, cx))
1829            .map(|p| p.to_string_lossy().to_string())
1830        {
1831            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1832        }
1833    }
1834
1835    fn copy_relative_path(
1836        &mut self,
1837        _: &zed_actions::workspace::CopyRelativePath,
1838        _: &mut Window,
1839        cx: &mut Context<Self>,
1840    ) {
1841        if let Some(clipboard_text) = self
1842            .selected_entry()
1843            .and_then(|entry| match entry {
1844                PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1845                PanelEntry::FoldedDirs(folded_dirs) => {
1846                    folded_dirs.entries.last().map(|entry| entry.path.clone())
1847                }
1848                PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1849            })
1850            .map(|p| p.to_string_lossy().to_string())
1851        {
1852            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1853        }
1854    }
1855
1856    fn reveal_in_finder(
1857        &mut self,
1858        _: &RevealInFileManager,
1859        _: &mut Window,
1860        cx: &mut Context<Self>,
1861    ) {
1862        if let Some(abs_path) = self
1863            .selected_entry()
1864            .and_then(|entry| self.abs_path(entry, cx))
1865        {
1866            cx.reveal_path(&abs_path);
1867        }
1868    }
1869
1870    fn open_in_terminal(
1871        &mut self,
1872        _: &OpenInTerminal,
1873        window: &mut Window,
1874        cx: &mut Context<Self>,
1875    ) {
1876        let selected_entry = self.selected_entry();
1877        let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
1878        let working_directory = if let (
1879            Some(abs_path),
1880            Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1881        ) = (&abs_path, selected_entry)
1882        {
1883            abs_path.parent().map(|p| p.to_owned())
1884        } else {
1885            abs_path
1886        };
1887
1888        if let Some(working_directory) = working_directory {
1889            window.dispatch_action(
1890                workspace::OpenTerminal { working_directory }.boxed_clone(),
1891                cx,
1892            )
1893        }
1894    }
1895
1896    fn reveal_entry_for_selection(
1897        &mut self,
1898        editor: Entity<Editor>,
1899        window: &mut Window,
1900        cx: &mut Context<Self>,
1901    ) {
1902        if !self.active
1903            || !OutlinePanelSettings::get_global(cx).auto_reveal_entries
1904            || self.focus_handle.contains_focused(window, cx)
1905        {
1906            return;
1907        }
1908        let project = self.project.clone();
1909        self.reveal_selection_task = cx.spawn_in(window, |outline_panel, mut cx| async move {
1910            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1911            let entry_with_selection =
1912                outline_panel.update_in(&mut cx, |outline_panel, window, cx| {
1913                    outline_panel.location_for_editor_selection(&editor, window, cx)
1914                })?;
1915            let Some(entry_with_selection) = entry_with_selection else {
1916                outline_panel.update(&mut cx, |outline_panel, cx| {
1917                    outline_panel.selected_entry = SelectedEntry::None;
1918                    cx.notify();
1919                })?;
1920                return Ok(());
1921            };
1922            let related_buffer_entry = match &entry_with_selection {
1923                PanelEntry::Fs(FsEntry::File(FsEntryFile {
1924                    worktree_id,
1925                    buffer_id,
1926                    ..
1927                })) => project.update(&mut cx, |project, cx| {
1928                    let entry_id = project
1929                        .buffer_for_id(*buffer_id, cx)
1930                        .and_then(|buffer| buffer.read(cx).entry_id(cx));
1931                    project
1932                        .worktree_for_id(*worktree_id, cx)
1933                        .zip(entry_id)
1934                        .and_then(|(worktree, entry_id)| {
1935                            let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1936                            Some((worktree, entry))
1937                        })
1938                })?,
1939                PanelEntry::Outline(outline_entry) => {
1940                    let (buffer_id, excerpt_id) = outline_entry.ids();
1941                    outline_panel.update(&mut cx, |outline_panel, cx| {
1942                        outline_panel
1943                            .collapsed_entries
1944                            .remove(&CollapsedEntry::ExternalFile(buffer_id));
1945                        outline_panel
1946                            .collapsed_entries
1947                            .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1948                        let project = outline_panel.project.read(cx);
1949                        let entry_id = project
1950                            .buffer_for_id(buffer_id, cx)
1951                            .and_then(|buffer| buffer.read(cx).entry_id(cx));
1952
1953                        entry_id.and_then(|entry_id| {
1954                            project
1955                                .worktree_for_entry(entry_id, cx)
1956                                .and_then(|worktree| {
1957                                    let worktree_id = worktree.read(cx).id();
1958                                    outline_panel
1959                                        .collapsed_entries
1960                                        .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1961                                    let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1962                                    Some((worktree, entry))
1963                                })
1964                        })
1965                    })?
1966                }
1967                PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
1968                PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
1969                    .start
1970                    .buffer_id
1971                    .or(match_range.end.buffer_id)
1972                    .map(|buffer_id| {
1973                        outline_panel.update(&mut cx, |outline_panel, cx| {
1974                            outline_panel
1975                                .collapsed_entries
1976                                .remove(&CollapsedEntry::ExternalFile(buffer_id));
1977                            let project = project.read(cx);
1978                            let entry_id = project
1979                                .buffer_for_id(buffer_id, cx)
1980                                .and_then(|buffer| buffer.read(cx).entry_id(cx));
1981
1982                            entry_id.and_then(|entry_id| {
1983                                project
1984                                    .worktree_for_entry(entry_id, cx)
1985                                    .and_then(|worktree| {
1986                                        let worktree_id = worktree.read(cx).id();
1987                                        outline_panel
1988                                            .collapsed_entries
1989                                            .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1990                                        let entry =
1991                                            worktree.read(cx).entry_for_id(entry_id)?.clone();
1992                                        Some((worktree, entry))
1993                                    })
1994                            })
1995                        })
1996                    })
1997                    .transpose()?
1998                    .flatten(),
1999                _ => return anyhow::Ok(()),
2000            };
2001            if let Some((worktree, buffer_entry)) = related_buffer_entry {
2002                outline_panel.update(&mut cx, |outline_panel, cx| {
2003                    let worktree_id = worktree.read(cx).id();
2004                    let mut dirs_to_expand = Vec::new();
2005                    {
2006                        let mut traversal = worktree.read(cx).traverse_from_path(
2007                            true,
2008                            true,
2009                            true,
2010                            buffer_entry.path.as_ref(),
2011                        );
2012                        let mut current_entry = buffer_entry;
2013                        loop {
2014                            if current_entry.is_dir()
2015                                && outline_panel
2016                                    .collapsed_entries
2017                                    .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
2018                            {
2019                                dirs_to_expand.push(current_entry.id);
2020                            }
2021
2022                            if traversal.back_to_parent() {
2023                                if let Some(parent_entry) = traversal.entry() {
2024                                    current_entry = parent_entry.clone();
2025                                    continue;
2026                                }
2027                            }
2028                            break;
2029                        }
2030                    }
2031                    for dir_to_expand in dirs_to_expand {
2032                        project
2033                            .update(cx, |project, cx| {
2034                                project.expand_entry(worktree_id, dir_to_expand, cx)
2035                            })
2036                            .unwrap_or_else(|| Task::ready(Ok(())))
2037                            .detach_and_log_err(cx)
2038                    }
2039                })?
2040            }
2041
2042            outline_panel.update_in(&mut cx, |outline_panel, window, cx| {
2043                outline_panel.select_entry(entry_with_selection, false, window, cx);
2044                outline_panel.update_cached_entries(None, window, cx);
2045            })?;
2046
2047            anyhow::Ok(())
2048        });
2049    }
2050
2051    fn render_excerpt(
2052        &self,
2053        excerpt: &OutlineEntryExcerpt,
2054        depth: usize,
2055        window: &mut Window,
2056        cx: &mut Context<OutlinePanel>,
2057    ) -> Option<Stateful<Div>> {
2058        let item_id = ElementId::from(excerpt.id.to_proto() as usize);
2059        let is_active = match self.selected_entry() {
2060            Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => {
2061                selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id
2062            }
2063            _ => false,
2064        };
2065        let has_outlines = self
2066            .excerpts
2067            .get(&excerpt.buffer_id)
2068            .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines {
2069                ExcerptOutlines::Outlines(outlines) => Some(outlines),
2070                ExcerptOutlines::Invalidated(outlines) => Some(outlines),
2071                ExcerptOutlines::NotFetched => None,
2072            })
2073            .map_or(false, |outlines| !outlines.is_empty());
2074        let is_expanded = !self
2075            .collapsed_entries
2076            .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id));
2077        let color = entry_label_color(is_active);
2078        let icon = if has_outlines {
2079            FileIcons::get_chevron_icon(is_expanded, cx)
2080                .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2081        } else {
2082            None
2083        }
2084        .unwrap_or_else(empty_icon);
2085
2086        let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?;
2087        let label_element = Label::new(label)
2088            .single_line()
2089            .color(color)
2090            .into_any_element();
2091
2092        Some(self.entry_element(
2093            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())),
2094            item_id,
2095            depth,
2096            Some(icon),
2097            is_active,
2098            label_element,
2099            window,
2100            cx,
2101        ))
2102    }
2103
2104    fn excerpt_label(
2105        &self,
2106        buffer_id: BufferId,
2107        range: &ExcerptRange<language::Anchor>,
2108        cx: &App,
2109    ) -> Option<String> {
2110        let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
2111        let excerpt_range = range.context.to_point(&buffer_snapshot);
2112        Some(format!(
2113            "Lines {}- {}",
2114            excerpt_range.start.row + 1,
2115            excerpt_range.end.row + 1,
2116        ))
2117    }
2118
2119    fn render_outline(
2120        &self,
2121        outline: &OutlineEntryOutline,
2122        depth: usize,
2123        string_match: Option<&StringMatch>,
2124        window: &mut Window,
2125        cx: &mut Context<Self>,
2126    ) -> Stateful<Div> {
2127        let item_id = ElementId::from(SharedString::from(format!(
2128            "{:?}|{:?}{:?}|{:?}",
2129            outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text,
2130        )));
2131
2132        let label_element = outline::render_item(
2133            &outline.outline,
2134            string_match
2135                .map(|string_match| string_match.ranges().collect::<Vec<_>>())
2136                .unwrap_or_default(),
2137            cx,
2138        )
2139        .into_any_element();
2140
2141        let is_active = match self.selected_entry() {
2142            Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => {
2143                outline == selected && outline.outline == selected.outline
2144            }
2145            _ => false,
2146        };
2147
2148        let icon = if self.is_singleton_active(cx) {
2149            None
2150        } else {
2151            Some(empty_icon())
2152        };
2153
2154        self.entry_element(
2155            PanelEntry::Outline(OutlineEntry::Outline(outline.clone())),
2156            item_id,
2157            depth,
2158            icon,
2159            is_active,
2160            label_element,
2161            window,
2162            cx,
2163        )
2164    }
2165
2166    fn render_entry(
2167        &self,
2168        rendered_entry: &FsEntry,
2169        depth: usize,
2170        string_match: Option<&StringMatch>,
2171        window: &mut Window,
2172        cx: &mut Context<Self>,
2173    ) -> Stateful<Div> {
2174        let settings = OutlinePanelSettings::get_global(cx);
2175        let is_active = match self.selected_entry() {
2176            Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
2177            _ => false,
2178        };
2179        let (item_id, label_element, icon) = match rendered_entry {
2180            FsEntry::File(FsEntryFile {
2181                worktree_id, entry, ..
2182            }) => {
2183                let name = self.entry_name(worktree_id, entry, cx);
2184                let color =
2185                    entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active);
2186                let icon = if settings.file_icons {
2187                    FileIcons::get_icon(&entry.path, cx)
2188                        .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
2189                } else {
2190                    None
2191                };
2192                (
2193                    ElementId::from(entry.id.to_proto() as usize),
2194                    HighlightedLabel::new(
2195                        name,
2196                        string_match
2197                            .map(|string_match| string_match.positions.clone())
2198                            .unwrap_or_default(),
2199                    )
2200                    .color(color)
2201                    .into_any_element(),
2202                    icon.unwrap_or_else(empty_icon),
2203                )
2204            }
2205            FsEntry::Directory(directory) => {
2206                let name = self.entry_name(&directory.worktree_id, &directory.entry, cx);
2207
2208                let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir(
2209                    directory.worktree_id,
2210                    directory.entry.id,
2211                ));
2212                let color = entry_git_aware_label_color(
2213                    directory.entry.git_summary,
2214                    directory.entry.is_ignored,
2215                    is_active,
2216                );
2217                let icon = if settings.folder_icons {
2218                    FileIcons::get_folder_icon(is_expanded, cx)
2219                } else {
2220                    FileIcons::get_chevron_icon(is_expanded, cx)
2221                }
2222                .map(Icon::from_path)
2223                .map(|icon| icon.color(color).into_any_element());
2224                (
2225                    ElementId::from(directory.entry.id.to_proto() as usize),
2226                    HighlightedLabel::new(
2227                        name,
2228                        string_match
2229                            .map(|string_match| string_match.positions.clone())
2230                            .unwrap_or_default(),
2231                    )
2232                    .color(color)
2233                    .into_any_element(),
2234                    icon.unwrap_or_else(empty_icon),
2235                )
2236            }
2237            FsEntry::ExternalFile(external_file) => {
2238                let color = entry_label_color(is_active);
2239                let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) {
2240                    Some(buffer_snapshot) => match buffer_snapshot.file() {
2241                        Some(file) => {
2242                            let path = file.path();
2243                            let icon = if settings.file_icons {
2244                                FileIcons::get_icon(path.as_ref(), cx)
2245                            } else {
2246                                None
2247                            }
2248                            .map(Icon::from_path)
2249                            .map(|icon| icon.color(color).into_any_element());
2250                            (icon, file_name(path.as_ref()))
2251                        }
2252                        None => (None, "Untitled".to_string()),
2253                    },
2254                    None => (None, "Unknown buffer".to_string()),
2255                };
2256                (
2257                    ElementId::from(external_file.buffer_id.to_proto() as usize),
2258                    HighlightedLabel::new(
2259                        name,
2260                        string_match
2261                            .map(|string_match| string_match.positions.clone())
2262                            .unwrap_or_default(),
2263                    )
2264                    .color(color)
2265                    .into_any_element(),
2266                    icon.unwrap_or_else(empty_icon),
2267                )
2268            }
2269        };
2270
2271        self.entry_element(
2272            PanelEntry::Fs(rendered_entry.clone()),
2273            item_id,
2274            depth,
2275            Some(icon),
2276            is_active,
2277            label_element,
2278            window,
2279            cx,
2280        )
2281    }
2282
2283    fn render_folded_dirs(
2284        &self,
2285        folded_dir: &FoldedDirsEntry,
2286        depth: usize,
2287        string_match: Option<&StringMatch>,
2288        window: &mut Window,
2289        cx: &mut Context<OutlinePanel>,
2290    ) -> Stateful<Div> {
2291        let settings = OutlinePanelSettings::get_global(cx);
2292        let is_active = match self.selected_entry() {
2293            Some(PanelEntry::FoldedDirs(selected_dirs)) => {
2294                selected_dirs.worktree_id == folded_dir.worktree_id
2295                    && selected_dirs.entries == folded_dir.entries
2296            }
2297            _ => false,
2298        };
2299        let (item_id, label_element, icon) = {
2300            let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx);
2301
2302            let is_expanded = folded_dir.entries.iter().all(|dir| {
2303                !self
2304                    .collapsed_entries
2305                    .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id))
2306            });
2307            let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored);
2308            let git_status = folded_dir
2309                .entries
2310                .first()
2311                .map(|entry| entry.git_summary)
2312                .unwrap_or_default();
2313            let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
2314            let icon = if settings.folder_icons {
2315                FileIcons::get_folder_icon(is_expanded, cx)
2316            } else {
2317                FileIcons::get_chevron_icon(is_expanded, cx)
2318            }
2319            .map(Icon::from_path)
2320            .map(|icon| icon.color(color).into_any_element());
2321            (
2322                ElementId::from(
2323                    folded_dir
2324                        .entries
2325                        .last()
2326                        .map(|entry| entry.id.to_proto())
2327                        .unwrap_or_else(|| folded_dir.worktree_id.to_proto())
2328                        as usize,
2329                ),
2330                HighlightedLabel::new(
2331                    name,
2332                    string_match
2333                        .map(|string_match| string_match.positions.clone())
2334                        .unwrap_or_default(),
2335                )
2336                .color(color)
2337                .into_any_element(),
2338                icon.unwrap_or_else(empty_icon),
2339            )
2340        };
2341
2342        self.entry_element(
2343            PanelEntry::FoldedDirs(folded_dir.clone()),
2344            item_id,
2345            depth,
2346            Some(icon),
2347            is_active,
2348            label_element,
2349            window,
2350            cx,
2351        )
2352    }
2353
2354    fn render_search_match(
2355        &mut self,
2356        multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
2357        match_range: &Range<editor::Anchor>,
2358        render_data: &Arc<OnceLock<SearchData>>,
2359        kind: SearchKind,
2360        depth: usize,
2361        string_match: Option<&StringMatch>,
2362        window: &mut Window,
2363        cx: &mut Context<Self>,
2364    ) -> Option<Stateful<Div>> {
2365        let search_data = match render_data.get() {
2366            Some(search_data) => search_data,
2367            None => {
2368                if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
2369                    if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
2370                        search_state
2371                            .highlight_search_match_tx
2372                            .try_send(HighlightArguments {
2373                                multi_buffer_snapshot: multi_buffer_snapshot.clone(),
2374                                match_range: match_range.clone(),
2375                                search_data: Arc::clone(render_data),
2376                            })
2377                            .ok();
2378                    }
2379                }
2380                return None;
2381            }
2382        };
2383        let search_matches = string_match
2384            .iter()
2385            .flat_map(|string_match| string_match.ranges())
2386            .collect::<Vec<_>>();
2387        let match_ranges = if search_matches.is_empty() {
2388            &search_data.search_match_indices
2389        } else {
2390            &search_matches
2391        };
2392        let label_element = outline::render_item(
2393            &OutlineItem {
2394                depth,
2395                annotation_range: None,
2396                range: search_data.context_range.clone(),
2397                text: search_data.context_text.clone(),
2398                highlight_ranges: search_data
2399                    .highlights_data
2400                    .get()
2401                    .cloned()
2402                    .unwrap_or_default(),
2403                name_ranges: search_data.search_match_indices.clone(),
2404                body_range: Some(search_data.context_range.clone()),
2405            },
2406            match_ranges.iter().cloned(),
2407            cx,
2408        );
2409        let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
2410        let entire_label = h_flex()
2411            .justify_center()
2412            .p_0()
2413            .when(search_data.truncated_left, |parent| {
2414                parent.child(truncated_contents_label())
2415            })
2416            .child(label_element)
2417            .when(search_data.truncated_right, |parent| {
2418                parent.child(truncated_contents_label())
2419            })
2420            .into_any_element();
2421
2422        let is_active = match self.selected_entry() {
2423            Some(PanelEntry::Search(SearchEntry {
2424                match_range: selected_match_range,
2425                ..
2426            })) => match_range == selected_match_range,
2427            _ => false,
2428        };
2429        Some(self.entry_element(
2430            PanelEntry::Search(SearchEntry {
2431                kind,
2432                match_range: match_range.clone(),
2433                render_data: render_data.clone(),
2434            }),
2435            ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2436            depth,
2437            None,
2438            is_active,
2439            entire_label,
2440            window,
2441            cx,
2442        ))
2443    }
2444
2445    fn entry_element(
2446        &self,
2447        rendered_entry: PanelEntry,
2448        item_id: ElementId,
2449        depth: usize,
2450        icon_element: Option<AnyElement>,
2451        is_active: bool,
2452        label_element: gpui::AnyElement,
2453        window: &mut Window,
2454        cx: &mut Context<OutlinePanel>,
2455    ) -> Stateful<Div> {
2456        let settings = OutlinePanelSettings::get_global(cx);
2457        div()
2458            .text_ui(cx)
2459            .id(item_id.clone())
2460            .on_click({
2461                let clicked_entry = rendered_entry.clone();
2462                cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| {
2463                    if event.down.button == MouseButton::Right || event.down.first_mouse {
2464                        return;
2465                    }
2466                    let change_focus = event.down.click_count > 1;
2467                    outline_panel.toggle_expanded(&clicked_entry, window, cx);
2468                    outline_panel.scroll_editor_to_entry(
2469                        &clicked_entry,
2470                        true,
2471                        change_focus,
2472                        window,
2473                        cx,
2474                    );
2475                })
2476            })
2477            .cursor_pointer()
2478            .child(
2479                ListItem::new(item_id)
2480                    .indent_level(depth)
2481                    .indent_step_size(px(settings.indent_size))
2482                    .toggle_state(is_active)
2483                    .when_some(icon_element, |list_item, icon_element| {
2484                        list_item.child(h_flex().child(icon_element))
2485                    })
2486                    .child(h_flex().h_6().child(label_element).ml_1())
2487                    .on_secondary_mouse_down(cx.listener(
2488                        move |outline_panel, event: &MouseDownEvent, window, cx| {
2489                            // Stop propagation to prevent the catch-all context menu for the project
2490                            // panel from being deployed.
2491                            cx.stop_propagation();
2492                            outline_panel.deploy_context_menu(
2493                                event.position,
2494                                rendered_entry.clone(),
2495                                window,
2496                                cx,
2497                            )
2498                        },
2499                    )),
2500            )
2501            .border_1()
2502            .border_r_2()
2503            .rounded_none()
2504            .hover(|style| {
2505                if is_active {
2506                    style
2507                } else {
2508                    let hover_color = cx.theme().colors().ghost_element_hover;
2509                    style.bg(hover_color).border_color(hover_color)
2510                }
2511            })
2512            .when(
2513                is_active && self.focus_handle.contains_focused(window, cx),
2514                |div| div.border_color(Color::Selected.color(cx)),
2515            )
2516    }
2517
2518    fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String {
2519        let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2520            Some(worktree) => {
2521                let worktree = worktree.read(cx);
2522                match worktree.snapshot().root_entry() {
2523                    Some(root_entry) => {
2524                        if root_entry.id == entry.id {
2525                            file_name(worktree.abs_path().as_ref())
2526                        } else {
2527                            let path = worktree.absolutize(entry.path.as_ref()).ok();
2528                            let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2529                            file_name(path)
2530                        }
2531                    }
2532                    None => {
2533                        let path = worktree.absolutize(entry.path.as_ref()).ok();
2534                        let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2535                        file_name(path)
2536                    }
2537                }
2538            }
2539            None => file_name(entry.path.as_ref()),
2540        };
2541        name
2542    }
2543
2544    fn update_fs_entries(
2545        &mut self,
2546        active_editor: Entity<Editor>,
2547        debounce: Option<Duration>,
2548        window: &mut Window,
2549        cx: &mut Context<Self>,
2550    ) {
2551        if !self.active {
2552            return;
2553        }
2554
2555        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2556        let active_multi_buffer = active_editor.read(cx).buffer().clone();
2557        let new_entries = self.new_entries_for_fs_update.clone();
2558        self.updating_fs_entries = true;
2559        self.fs_entries_update_task = cx.spawn_in(window, |outline_panel, mut cx| async move {
2560            if let Some(debounce) = debounce {
2561                cx.background_executor().timer(debounce).await;
2562            }
2563
2564            let mut new_collapsed_entries = HashSet::default();
2565            let mut new_unfolded_dirs = HashMap::default();
2566            let mut root_entries = HashSet::default();
2567            let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2568            let Ok(buffer_excerpts) = outline_panel.update(&mut cx, |outline_panel, cx| {
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                        buffer_excerpts
2583                            .entry(buffer_id)
2584                            .or_insert_with(|| (is_new, is_folded, Vec::new(), entry_id, worktree))
2585                            .2
2586                            .push(excerpt_id);
2587
2588                        let outlines = match outline_panel
2589                            .excerpts
2590                            .get(&buffer_id)
2591                            .and_then(|excerpts| excerpts.get(&excerpt_id))
2592                        {
2593                            Some(old_excerpt) => match &old_excerpt.outlines {
2594                                ExcerptOutlines::Outlines(outlines) => {
2595                                    ExcerptOutlines::Outlines(outlines.clone())
2596                                }
2597                                ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2598                                ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2599                            },
2600                            None => ExcerptOutlines::NotFetched,
2601                        };
2602                        new_excerpts.entry(buffer_id).or_default().insert(
2603                            excerpt_id,
2604                            Excerpt {
2605                                range: excerpt_range,
2606                                outlines,
2607                            },
2608                        );
2609                        buffer_excerpts
2610                    },
2611                );
2612                buffer_excerpts
2613            }) else {
2614                return;
2615            };
2616
2617            let Some((
2618                new_collapsed_entries,
2619                new_unfolded_dirs,
2620                new_fs_entries,
2621                new_depth_map,
2622                new_children_count,
2623            )) = cx
2624                .background_spawn(async move {
2625                    let mut processed_external_buffers = HashSet::default();
2626                    let mut new_worktree_entries =
2627                        BTreeMap::<WorktreeId, HashMap<ProjectEntryId, GitEntry>>::default();
2628                    let mut worktree_excerpts = HashMap::<
2629                        WorktreeId,
2630                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2631                    >::default();
2632                    let mut external_excerpts = HashMap::default();
2633
2634                    for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree)) in
2635                        buffer_excerpts
2636                    {
2637                        if is_folded {
2638                            match &worktree {
2639                                Some(worktree) => {
2640                                    new_collapsed_entries
2641                                        .insert(CollapsedEntry::File(worktree.id(), buffer_id));
2642                                }
2643                                None => {
2644                                    new_collapsed_entries
2645                                        .insert(CollapsedEntry::ExternalFile(buffer_id));
2646                                }
2647                            }
2648                        } else if is_new {
2649                            match &worktree {
2650                                Some(worktree) => {
2651                                    new_collapsed_entries
2652                                        .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2653                                }
2654                                None => {
2655                                    new_collapsed_entries
2656                                        .remove(&CollapsedEntry::ExternalFile(buffer_id));
2657                                }
2658                            }
2659                        }
2660
2661                        if let Some(worktree) = worktree {
2662                            let worktree_id = worktree.id();
2663                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2664
2665                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2666                                Some(entry) => {
2667                                    let entry = GitEntry {
2668                                        git_summary: worktree
2669                                            .status_for_file(&entry.path)
2670                                            .map(|status| status.summary())
2671                                            .unwrap_or_default(),
2672                                        entry,
2673                                    };
2674                                    let mut traversal = worktree
2675                                        .traverse_from_path(true, true, true, entry.path.as_ref())
2676                                        .with_git_statuses();
2677
2678                                    let mut entries_to_add = HashMap::default();
2679                                    worktree_excerpts
2680                                        .entry(worktree_id)
2681                                        .or_default()
2682                                        .insert(entry.id, (buffer_id, excerpts));
2683                                    let mut current_entry = entry;
2684                                    loop {
2685                                        if current_entry.is_dir() {
2686                                            let is_root =
2687                                                worktree.root_entry().map(|entry| entry.id)
2688                                                    == Some(current_entry.id);
2689                                            if is_root {
2690                                                root_entries.insert(current_entry.id);
2691                                                if auto_fold_dirs {
2692                                                    unfolded_dirs.insert(current_entry.id);
2693                                                }
2694                                            }
2695                                            if is_new {
2696                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
2697                                                    worktree_id,
2698                                                    current_entry.id,
2699                                                ));
2700                                            }
2701                                        }
2702
2703                                        let new_entry_added = entries_to_add
2704                                            .insert(current_entry.id, current_entry)
2705                                            .is_none();
2706                                        if new_entry_added && traversal.back_to_parent() {
2707                                            if let Some(parent_entry) = traversal.entry() {
2708                                                current_entry = parent_entry.to_owned();
2709                                                continue;
2710                                            }
2711                                        }
2712                                        break;
2713                                    }
2714                                    new_worktree_entries
2715                                        .entry(worktree_id)
2716                                        .or_insert_with(HashMap::default)
2717                                        .extend(entries_to_add);
2718                                }
2719                                None => {
2720                                    if processed_external_buffers.insert(buffer_id) {
2721                                        external_excerpts
2722                                            .entry(buffer_id)
2723                                            .or_insert_with(Vec::new)
2724                                            .extend(excerpts);
2725                                    }
2726                                }
2727                            }
2728                        } else if processed_external_buffers.insert(buffer_id) {
2729                            external_excerpts
2730                                .entry(buffer_id)
2731                                .or_insert_with(Vec::new)
2732                                .extend(excerpts);
2733                        }
2734                    }
2735
2736                    let mut new_children_count =
2737                        HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
2738
2739                    let worktree_entries = new_worktree_entries
2740                        .into_iter()
2741                        .map(|(worktree_id, entries)| {
2742                            let mut entries = entries.into_values().collect::<Vec<_>>();
2743                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2744                            (worktree_id, entries)
2745                        })
2746                        .flat_map(|(worktree_id, entries)| {
2747                            {
2748                                entries
2749                                    .into_iter()
2750                                    .filter_map(|entry| {
2751                                        if auto_fold_dirs {
2752                                            if let Some(parent) = entry.path.parent() {
2753                                                let children = new_children_count
2754                                                    .entry(worktree_id)
2755                                                    .or_default()
2756                                                    .entry(Arc::from(parent))
2757                                                    .or_default();
2758                                                if entry.is_dir() {
2759                                                    children.dirs += 1;
2760                                                } else {
2761                                                    children.files += 1;
2762                                                }
2763                                            }
2764                                        }
2765
2766                                        if entry.is_dir() {
2767                                            Some(FsEntry::Directory(FsEntryDirectory {
2768                                                worktree_id,
2769                                                entry,
2770                                            }))
2771                                        } else {
2772                                            let (buffer_id, excerpts) = worktree_excerpts
2773                                                .get_mut(&worktree_id)
2774                                                .and_then(|worktree_excerpts| {
2775                                                    worktree_excerpts.remove(&entry.id)
2776                                                })?;
2777                                            Some(FsEntry::File(FsEntryFile {
2778                                                worktree_id,
2779                                                buffer_id,
2780                                                entry,
2781                                                excerpts,
2782                                            }))
2783                                        }
2784                                    })
2785                                    .collect::<Vec<_>>()
2786                            }
2787                        })
2788                        .collect::<Vec<_>>();
2789
2790                    let mut visited_dirs = Vec::new();
2791                    let mut new_depth_map = HashMap::default();
2792                    let new_visible_entries = external_excerpts
2793                        .into_iter()
2794                        .sorted_by_key(|(id, _)| *id)
2795                        .map(|(buffer_id, excerpts)| {
2796                            FsEntry::ExternalFile(FsEntryExternalFile {
2797                                buffer_id,
2798                                excerpts,
2799                            })
2800                        })
2801                        .chain(worktree_entries)
2802                        .filter(|visible_item| {
2803                            match visible_item {
2804                                FsEntry::Directory(directory) => {
2805                                    let parent_id = back_to_common_visited_parent(
2806                                        &mut visited_dirs,
2807                                        &directory.worktree_id,
2808                                        &directory.entry,
2809                                    );
2810
2811                                    let mut depth = 0;
2812                                    if !root_entries.contains(&directory.entry.id) {
2813                                        if auto_fold_dirs {
2814                                            let children = new_children_count
2815                                                .get(&directory.worktree_id)
2816                                                .and_then(|children_count| {
2817                                                    children_count.get(&directory.entry.path)
2818                                                })
2819                                                .copied()
2820                                                .unwrap_or_default();
2821
2822                                            if !children.may_be_fold_part()
2823                                                || (children.dirs == 0
2824                                                    && visited_dirs
2825                                                        .last()
2826                                                        .map(|(parent_dir_id, _)| {
2827                                                            new_unfolded_dirs
2828                                                                .get(&directory.worktree_id)
2829                                                                .map_or(true, |unfolded_dirs| {
2830                                                                    unfolded_dirs
2831                                                                        .contains(parent_dir_id)
2832                                                                })
2833                                                        })
2834                                                        .unwrap_or(true))
2835                                            {
2836                                                new_unfolded_dirs
2837                                                    .entry(directory.worktree_id)
2838                                                    .or_default()
2839                                                    .insert(directory.entry.id);
2840                                            }
2841                                        }
2842
2843                                        depth = parent_id
2844                                            .and_then(|(worktree_id, id)| {
2845                                                new_depth_map.get(&(worktree_id, id)).copied()
2846                                            })
2847                                            .unwrap_or(0)
2848                                            + 1;
2849                                    };
2850                                    visited_dirs
2851                                        .push((directory.entry.id, directory.entry.path.clone()));
2852                                    new_depth_map
2853                                        .insert((directory.worktree_id, directory.entry.id), depth);
2854                                }
2855                                FsEntry::File(FsEntryFile {
2856                                    worktree_id,
2857                                    entry: file_entry,
2858                                    ..
2859                                }) => {
2860                                    let parent_id = back_to_common_visited_parent(
2861                                        &mut visited_dirs,
2862                                        worktree_id,
2863                                        file_entry,
2864                                    );
2865                                    let depth = if root_entries.contains(&file_entry.id) {
2866                                        0
2867                                    } else {
2868                                        parent_id
2869                                            .and_then(|(worktree_id, id)| {
2870                                                new_depth_map.get(&(worktree_id, id)).copied()
2871                                            })
2872                                            .unwrap_or(0)
2873                                            + 1
2874                                    };
2875                                    new_depth_map.insert((*worktree_id, file_entry.id), depth);
2876                                }
2877                                FsEntry::ExternalFile(..) => {
2878                                    visited_dirs.clear();
2879                                }
2880                            }
2881
2882                            true
2883                        })
2884                        .collect::<Vec<_>>();
2885
2886                    anyhow::Ok((
2887                        new_collapsed_entries,
2888                        new_unfolded_dirs,
2889                        new_visible_entries,
2890                        new_depth_map,
2891                        new_children_count,
2892                    ))
2893                })
2894                .await
2895                .log_err()
2896            else {
2897                return;
2898            };
2899
2900            outline_panel
2901                .update_in(&mut cx, |outline_panel, window, cx| {
2902                    outline_panel.updating_fs_entries = false;
2903                    outline_panel.new_entries_for_fs_update.clear();
2904                    outline_panel.excerpts = new_excerpts;
2905                    outline_panel.collapsed_entries = new_collapsed_entries;
2906                    outline_panel.unfolded_dirs = new_unfolded_dirs;
2907                    outline_panel.fs_entries = new_fs_entries;
2908                    outline_panel.fs_entries_depth = new_depth_map;
2909                    outline_panel.fs_children_count = new_children_count;
2910                    outline_panel.update_non_fs_items(window, cx);
2911                    outline_panel.update_cached_entries(debounce, window, cx);
2912
2913                    cx.notify();
2914                })
2915                .ok();
2916        });
2917    }
2918
2919    fn replace_active_editor(
2920        &mut self,
2921        new_active_item: Box<dyn ItemHandle>,
2922        new_active_editor: Entity<Editor>,
2923        window: &mut Window,
2924        cx: &mut Context<Self>,
2925    ) {
2926        self.clear_previous(window, cx);
2927        let buffer_search_subscription = cx.subscribe_in(
2928            &new_active_editor,
2929            window,
2930            |outline_panel: &mut Self,
2931             _,
2932             e: &SearchEvent,
2933             window: &mut Window,
2934             cx: &mut Context<Self>| {
2935                if matches!(e, SearchEvent::MatchesInvalidated) {
2936                    let update_cached_items = outline_panel.update_search_matches(window, cx);
2937                    if update_cached_items {
2938                        outline_panel.selected_entry.invalidate();
2939                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
2940                    }
2941                };
2942                outline_panel.autoscroll(cx);
2943            },
2944        );
2945        self.active_item = Some(ActiveItem {
2946            _buffer_search_subscription: buffer_search_subscription,
2947            _editor_subscrpiption: subscribe_for_editor_events(&new_active_editor, window, cx),
2948            item_handle: new_active_item.downgrade_item(),
2949            active_editor: new_active_editor.downgrade(),
2950        });
2951        self.new_entries_for_fs_update
2952            .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids());
2953        self.selected_entry.invalidate();
2954        self.update_fs_entries(new_active_editor, None, window, cx);
2955    }
2956
2957    fn clear_previous(&mut self, window: &mut Window, cx: &mut App) {
2958        self.fs_entries_update_task = Task::ready(());
2959        self.outline_fetch_tasks.clear();
2960        self.cached_entries_update_task = Task::ready(());
2961        self.reveal_selection_task = Task::ready(Ok(()));
2962        self.filter_editor
2963            .update(cx, |editor, cx| editor.clear(window, cx));
2964        self.collapsed_entries.clear();
2965        self.unfolded_dirs.clear();
2966        self.active_item = None;
2967        self.fs_entries.clear();
2968        self.fs_entries_depth.clear();
2969        self.fs_children_count.clear();
2970        self.excerpts.clear();
2971        self.cached_entries = Vec::new();
2972        self.selected_entry = SelectedEntry::None;
2973        self.pinned = false;
2974        self.mode = ItemsDisplayMode::Outline;
2975    }
2976
2977    fn location_for_editor_selection(
2978        &self,
2979        editor: &Entity<Editor>,
2980        window: &mut Window,
2981        cx: &mut Context<Self>,
2982    ) -> Option<PanelEntry> {
2983        let selection = editor.update(cx, |editor, cx| {
2984            editor.selections.newest::<language::Point>(cx).head()
2985        });
2986        let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
2987        let multi_buffer = editor.read(cx).buffer();
2988        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
2989        let (excerpt_id, buffer, _) = editor
2990            .read(cx)
2991            .buffer()
2992            .read(cx)
2993            .excerpt_containing(selection, cx)?;
2994        let buffer_id = buffer.read(cx).remote_id();
2995
2996        if editor.read(cx).is_buffer_folded(buffer_id, cx) {
2997            return self
2998                .fs_entries
2999                .iter()
3000                .find(|fs_entry| match fs_entry {
3001                    FsEntry::Directory(..) => false,
3002                    FsEntry::File(FsEntryFile {
3003                        buffer_id: other_buffer_id,
3004                        ..
3005                    })
3006                    | FsEntry::ExternalFile(FsEntryExternalFile {
3007                        buffer_id: other_buffer_id,
3008                        ..
3009                    }) => buffer_id == *other_buffer_id,
3010                })
3011                .cloned()
3012                .map(PanelEntry::Fs);
3013        }
3014
3015        let selection_display_point = selection.to_display_point(&editor_snapshot);
3016
3017        match &self.mode {
3018            ItemsDisplayMode::Search(search_state) => search_state
3019                .matches
3020                .iter()
3021                .rev()
3022                .min_by_key(|&(match_range, _)| {
3023                    let match_display_range =
3024                        match_range.clone().to_display_points(&editor_snapshot);
3025                    let start_distance = if selection_display_point < match_display_range.start {
3026                        match_display_range.start - selection_display_point
3027                    } else {
3028                        selection_display_point - match_display_range.start
3029                    };
3030                    let end_distance = if selection_display_point < match_display_range.end {
3031                        match_display_range.end - selection_display_point
3032                    } else {
3033                        selection_display_point - match_display_range.end
3034                    };
3035                    start_distance + end_distance
3036                })
3037                .and_then(|(closest_range, _)| {
3038                    self.cached_entries.iter().find_map(|cached_entry| {
3039                        if let PanelEntry::Search(SearchEntry { match_range, .. }) =
3040                            &cached_entry.entry
3041                        {
3042                            if match_range == closest_range {
3043                                Some(cached_entry.entry.clone())
3044                            } else {
3045                                None
3046                            }
3047                        } else {
3048                            None
3049                        }
3050                    })
3051                }),
3052            ItemsDisplayMode::Outline => self.outline_location(
3053                buffer_id,
3054                excerpt_id,
3055                multi_buffer_snapshot,
3056                editor_snapshot,
3057                selection_display_point,
3058            ),
3059        }
3060    }
3061
3062    fn outline_location(
3063        &self,
3064        buffer_id: BufferId,
3065        excerpt_id: ExcerptId,
3066        multi_buffer_snapshot: editor::MultiBufferSnapshot,
3067        editor_snapshot: editor::EditorSnapshot,
3068        selection_display_point: DisplayPoint,
3069    ) -> Option<PanelEntry> {
3070        let excerpt_outlines = self
3071            .excerpts
3072            .get(&buffer_id)
3073            .and_then(|excerpts| excerpts.get(&excerpt_id))
3074            .into_iter()
3075            .flat_map(|excerpt| excerpt.iter_outlines())
3076            .flat_map(|outline| {
3077                let start = multi_buffer_snapshot
3078                    .anchor_in_excerpt(excerpt_id, outline.range.start)?
3079                    .to_display_point(&editor_snapshot);
3080                let end = multi_buffer_snapshot
3081                    .anchor_in_excerpt(excerpt_id, outline.range.end)?
3082                    .to_display_point(&editor_snapshot);
3083                Some((start..end, outline))
3084            })
3085            .collect::<Vec<_>>();
3086
3087        let mut matching_outline_indices = Vec::new();
3088        let mut children = HashMap::default();
3089        let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
3090
3091        for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
3092            if outline_range
3093                .to_inclusive()
3094                .contains(&selection_display_point)
3095            {
3096                matching_outline_indices.push(i);
3097            } else if (outline_range.start.row()..outline_range.end.row())
3098                .to_inclusive()
3099                .contains(&selection_display_point.row())
3100            {
3101                matching_outline_indices.push(i);
3102            }
3103
3104            while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
3105                if parent_outline.depth >= outline.depth
3106                    || !parent_range.contains(&outline_range.start)
3107                {
3108                    parents_stack.pop();
3109                } else {
3110                    break;
3111                }
3112            }
3113            if let Some((_, _, parent_index)) = parents_stack.last_mut() {
3114                children
3115                    .entry(*parent_index)
3116                    .or_insert_with(Vec::new)
3117                    .push(i);
3118            }
3119            parents_stack.push((outline_range, outline, i));
3120        }
3121
3122        let outline_item = matching_outline_indices
3123            .into_iter()
3124            .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
3125            .filter(|(i, _)| {
3126                children
3127                    .get(i)
3128                    .map(|children| {
3129                        children.iter().all(|child_index| {
3130                            excerpt_outlines
3131                                .get(*child_index)
3132                                .map(|(child_range, _)| child_range.start > selection_display_point)
3133                                .unwrap_or(false)
3134                        })
3135                    })
3136                    .unwrap_or(true)
3137            })
3138            .min_by_key(|(_, (outline_range, outline))| {
3139                let distance_from_start = if outline_range.start > selection_display_point {
3140                    outline_range.start - selection_display_point
3141                } else {
3142                    selection_display_point - outline_range.start
3143                };
3144                let distance_from_end = if outline_range.end > selection_display_point {
3145                    outline_range.end - selection_display_point
3146                } else {
3147                    selection_display_point - outline_range.end
3148                };
3149
3150                (
3151                    cmp::Reverse(outline.depth),
3152                    distance_from_start + distance_from_end,
3153                )
3154            })
3155            .map(|(_, (_, outline))| *outline)
3156            .cloned();
3157
3158        let closest_container = match outline_item {
3159            Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
3160                buffer_id,
3161                excerpt_id,
3162                outline,
3163            })),
3164            None => {
3165                self.cached_entries.iter().rev().find_map(|cached_entry| {
3166                    match &cached_entry.entry {
3167                        PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
3168                            if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id {
3169                                Some(cached_entry.entry.clone())
3170                            } else {
3171                                None
3172                            }
3173                        }
3174                        PanelEntry::Fs(
3175                            FsEntry::ExternalFile(FsEntryExternalFile {
3176                                buffer_id: file_buffer_id,
3177                                excerpts: file_excerpts,
3178                            })
3179                            | FsEntry::File(FsEntryFile {
3180                                buffer_id: file_buffer_id,
3181                                excerpts: file_excerpts,
3182                                ..
3183                            }),
3184                        ) => {
3185                            if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
3186                                Some(cached_entry.entry.clone())
3187                            } else {
3188                                None
3189                            }
3190                        }
3191                        _ => None,
3192                    }
3193                })?
3194            }
3195        };
3196        Some(closest_container)
3197    }
3198
3199    fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context<Self>) {
3200        let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
3201        if excerpt_fetch_ranges.is_empty() {
3202            return;
3203        }
3204
3205        let syntax_theme = cx.theme().syntax().clone();
3206        let first_update = Arc::new(AtomicBool::new(true));
3207        for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
3208            for (excerpt_id, excerpt_range) in excerpt_ranges {
3209                let syntax_theme = syntax_theme.clone();
3210                let buffer_snapshot = buffer_snapshot.clone();
3211                let first_update = first_update.clone();
3212                self.outline_fetch_tasks.insert(
3213                    (buffer_id, excerpt_id),
3214                    cx.spawn_in(window, |outline_panel, mut cx| async move {
3215                        let fetched_outlines = cx
3216                            .background_spawn(async move {
3217                                buffer_snapshot
3218                                    .outline_items_containing(
3219                                        excerpt_range.context,
3220                                        false,
3221                                        Some(&syntax_theme),
3222                                    )
3223                                    .unwrap_or_default()
3224                            })
3225                            .await;
3226                        outline_panel
3227                            .update_in(&mut cx, |outline_panel, window, cx| {
3228                                if let Some(excerpt) = outline_panel
3229                                    .excerpts
3230                                    .entry(buffer_id)
3231                                    .or_default()
3232                                    .get_mut(&excerpt_id)
3233                                {
3234                                    let debounce = if first_update
3235                                        .fetch_and(false, atomic::Ordering::AcqRel)
3236                                    {
3237                                        None
3238                                    } else {
3239                                        Some(UPDATE_DEBOUNCE)
3240                                    };
3241                                    excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
3242                                    outline_panel.update_cached_entries(debounce, window, cx);
3243                                }
3244                            })
3245                            .ok();
3246                    }),
3247                );
3248            }
3249        }
3250    }
3251
3252    fn is_singleton_active(&self, cx: &App) -> bool {
3253        self.active_editor().map_or(false, |active_editor| {
3254            active_editor.read(cx).buffer().read(cx).is_singleton()
3255        })
3256    }
3257
3258    fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
3259        self.outline_fetch_tasks.clear();
3260        let mut ids = ids.iter().collect::<HashSet<_>>();
3261        for excerpts in self.excerpts.values_mut() {
3262            ids.retain(|id| {
3263                if let Some(excerpt) = excerpts.get_mut(id) {
3264                    excerpt.invalidate_outlines();
3265                    false
3266                } else {
3267                    true
3268                }
3269            });
3270            if ids.is_empty() {
3271                break;
3272            }
3273        }
3274    }
3275
3276    fn excerpt_fetch_ranges(
3277        &self,
3278        cx: &App,
3279    ) -> HashMap<
3280        BufferId,
3281        (
3282            BufferSnapshot,
3283            HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
3284        ),
3285    > {
3286        self.fs_entries
3287            .iter()
3288            .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
3289                match fs_entry {
3290                    FsEntry::File(FsEntryFile {
3291                        buffer_id,
3292                        excerpts: file_excerpts,
3293                        ..
3294                    })
3295                    | FsEntry::ExternalFile(FsEntryExternalFile {
3296                        buffer_id,
3297                        excerpts: file_excerpts,
3298                    }) => {
3299                        let excerpts = self.excerpts.get(buffer_id);
3300                        for &file_excerpt in file_excerpts {
3301                            if let Some(excerpt) = excerpts
3302                                .and_then(|excerpts| excerpts.get(&file_excerpt))
3303                                .filter(|excerpt| excerpt.should_fetch_outlines())
3304                            {
3305                                match excerpts_to_fetch.entry(*buffer_id) {
3306                                    hash_map::Entry::Occupied(mut o) => {
3307                                        o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
3308                                    }
3309                                    hash_map::Entry::Vacant(v) => {
3310                                        if let Some(buffer_snapshot) =
3311                                            self.buffer_snapshot_for_id(*buffer_id, cx)
3312                                        {
3313                                            v.insert((buffer_snapshot, HashMap::default()))
3314                                                .1
3315                                                .insert(file_excerpt, excerpt.range.clone());
3316                                        }
3317                                    }
3318                                }
3319                            }
3320                        }
3321                    }
3322                    FsEntry::Directory(..) => {}
3323                }
3324                excerpts_to_fetch
3325            })
3326    }
3327
3328    fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option<BufferSnapshot> {
3329        let editor = self.active_editor()?;
3330        Some(
3331            editor
3332                .read(cx)
3333                .buffer()
3334                .read(cx)
3335                .buffer(buffer_id)?
3336                .read(cx)
3337                .snapshot(),
3338        )
3339    }
3340
3341    fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option<PathBuf> {
3342        match entry {
3343            PanelEntry::Fs(
3344                FsEntry::File(FsEntryFile { buffer_id, .. })
3345                | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }),
3346            ) => self
3347                .buffer_snapshot_for_id(*buffer_id, cx)
3348                .and_then(|buffer_snapshot| {
3349                    let file = File::from_dyn(buffer_snapshot.file())?;
3350                    file.worktree.read(cx).absolutize(&file.path).ok()
3351                }),
3352            PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3353                worktree_id, entry, ..
3354            })) => self
3355                .project
3356                .read(cx)
3357                .worktree_for_id(*worktree_id, cx)?
3358                .read(cx)
3359                .absolutize(&entry.path)
3360                .ok(),
3361            PanelEntry::FoldedDirs(FoldedDirsEntry {
3362                worktree_id,
3363                entries: dirs,
3364                ..
3365            }) => dirs.last().and_then(|entry| {
3366                self.project
3367                    .read(cx)
3368                    .worktree_for_id(*worktree_id, cx)
3369                    .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
3370            }),
3371            PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
3372        }
3373    }
3374
3375    fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option<Arc<Path>> {
3376        match entry {
3377            FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3378                let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
3379                Some(buffer_snapshot.file()?.path().clone())
3380            }
3381            FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()),
3382            FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()),
3383        }
3384    }
3385
3386    fn update_cached_entries(
3387        &mut self,
3388        debounce: Option<Duration>,
3389        window: &mut Window,
3390        cx: &mut Context<OutlinePanel>,
3391    ) {
3392        if !self.active {
3393            return;
3394        }
3395
3396        let is_singleton = self.is_singleton_active(cx);
3397        let query = self.query(cx);
3398        self.updating_cached_entries = true;
3399        self.cached_entries_update_task = cx.spawn_in(window, |outline_panel, mut cx| async move {
3400            if let Some(debounce) = debounce {
3401                cx.background_executor().timer(debounce).await;
3402            }
3403            let Some(new_cached_entries) = outline_panel
3404                .update_in(&mut cx, |outline_panel, window, cx| {
3405                    outline_panel.generate_cached_entries(is_singleton, query, window, cx)
3406                })
3407                .ok()
3408            else {
3409                return;
3410            };
3411            let (new_cached_entries, max_width_item_index) = new_cached_entries.await;
3412            outline_panel
3413                .update_in(&mut cx, |outline_panel, window, cx| {
3414                    outline_panel.cached_entries = new_cached_entries;
3415                    outline_panel.max_width_item_index = max_width_item_index;
3416                    if outline_panel.selected_entry.is_invalidated()
3417                        || matches!(outline_panel.selected_entry, SelectedEntry::None)
3418                    {
3419                        if let Some(new_selected_entry) =
3420                            outline_panel.active_editor().and_then(|active_editor| {
3421                                outline_panel.location_for_editor_selection(
3422                                    &active_editor,
3423                                    window,
3424                                    cx,
3425                                )
3426                            })
3427                        {
3428                            outline_panel.select_entry(new_selected_entry, false, window, cx);
3429                        }
3430                    }
3431
3432                    outline_panel.autoscroll(cx);
3433                    outline_panel.updating_cached_entries = false;
3434                    cx.notify();
3435                })
3436                .ok();
3437        });
3438    }
3439
3440    fn generate_cached_entries(
3441        &self,
3442        is_singleton: bool,
3443        query: Option<String>,
3444        window: &mut Window,
3445        cx: &mut Context<Self>,
3446    ) -> Task<(Vec<CachedEntry>, Option<usize>)> {
3447        let project = self.project.clone();
3448        let Some(active_editor) = self.active_editor() else {
3449            return Task::ready((Vec::new(), None));
3450        };
3451        cx.spawn_in(window, |outline_panel, mut cx| async move {
3452            let mut generation_state = GenerationState::default();
3453
3454            let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
3455                let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
3456                let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>;
3457                let track_matches = query.is_some();
3458
3459                #[derive(Debug)]
3460                struct ParentStats {
3461                    path: Arc<Path>,
3462                    folded: bool,
3463                    expanded: bool,
3464                    depth: usize,
3465                }
3466                let mut parent_dirs = Vec::<ParentStats>::new();
3467                for entry in outline_panel.fs_entries.clone() {
3468                    let is_expanded = outline_panel.is_expanded(&entry);
3469                    let (depth, should_add) = match &entry {
3470                        FsEntry::Directory(directory_entry) => {
3471                            let mut should_add = true;
3472                            let is_root = project
3473                                .read(cx)
3474                                .worktree_for_id(directory_entry.worktree_id, cx)
3475                                .map_or(false, |worktree| {
3476                                    worktree.read(cx).root_entry() == Some(&directory_entry.entry)
3477                                });
3478                            let folded = auto_fold_dirs
3479                                && !is_root
3480                                && outline_panel
3481                                    .unfolded_dirs
3482                                    .get(&directory_entry.worktree_id)
3483                                    .map_or(true, |unfolded_dirs| {
3484                                        !unfolded_dirs.contains(&directory_entry.entry.id)
3485                                    });
3486                            let fs_depth = outline_panel
3487                                .fs_entries_depth
3488                                .get(&(directory_entry.worktree_id, directory_entry.entry.id))
3489                                .copied()
3490                                .unwrap_or(0);
3491                            while let Some(parent) = parent_dirs.last() {
3492                                if !is_root && directory_entry.entry.path.starts_with(&parent.path)
3493                                {
3494                                    break;
3495                                }
3496                                parent_dirs.pop();
3497                            }
3498                            let auto_fold = match parent_dirs.last() {
3499                                Some(parent) => {
3500                                    parent.folded
3501                                        && Some(parent.path.as_ref())
3502                                            == directory_entry.entry.path.parent()
3503                                        && outline_panel
3504                                            .fs_children_count
3505                                            .get(&directory_entry.worktree_id)
3506                                            .and_then(|entries| {
3507                                                entries.get(&directory_entry.entry.path)
3508                                            })
3509                                            .copied()
3510                                            .unwrap_or_default()
3511                                            .may_be_fold_part()
3512                                }
3513                                None => false,
3514                            };
3515                            let folded = folded || auto_fold;
3516                            let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
3517                                Some(parent) => {
3518                                    let parent_folded = parent.folded;
3519                                    let parent_expanded = parent.expanded;
3520                                    let new_depth = if parent_folded {
3521                                        parent.depth
3522                                    } else {
3523                                        parent.depth + 1
3524                                    };
3525                                    parent_dirs.push(ParentStats {
3526                                        path: directory_entry.entry.path.clone(),
3527                                        folded,
3528                                        expanded: parent_expanded && is_expanded,
3529                                        depth: new_depth,
3530                                    });
3531                                    (new_depth, parent_expanded, parent_folded)
3532                                }
3533                                None => {
3534                                    parent_dirs.push(ParentStats {
3535                                        path: directory_entry.entry.path.clone(),
3536                                        folded,
3537                                        expanded: is_expanded,
3538                                        depth: fs_depth,
3539                                    });
3540                                    (fs_depth, true, false)
3541                                }
3542                            };
3543
3544                            if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take()
3545                            {
3546                                if folded
3547                                    && directory_entry.worktree_id == folded_dirs.worktree_id
3548                                    && directory_entry.entry.path.parent()
3549                                        == folded_dirs
3550                                            .entries
3551                                            .last()
3552                                            .map(|entry| entry.path.as_ref())
3553                                {
3554                                    folded_dirs.entries.push(directory_entry.entry.clone());
3555                                    folded_dirs_entry = Some((folded_depth, folded_dirs))
3556                                } else {
3557                                    if !is_singleton {
3558                                        let start_of_collapsed_dir_sequence = !parent_expanded
3559                                            && parent_dirs
3560                                                .iter()
3561                                                .rev()
3562                                                .nth(folded_dirs.entries.len() + 1)
3563                                                .map_or(true, |parent| parent.expanded);
3564                                        if start_of_collapsed_dir_sequence
3565                                            || parent_expanded
3566                                            || query.is_some()
3567                                        {
3568                                            if parent_folded {
3569                                                folded_dirs
3570                                                    .entries
3571                                                    .push(directory_entry.entry.clone());
3572                                                should_add = false;
3573                                            }
3574                                            let new_folded_dirs =
3575                                                PanelEntry::FoldedDirs(folded_dirs.clone());
3576                                            outline_panel.push_entry(
3577                                                &mut generation_state,
3578                                                track_matches,
3579                                                new_folded_dirs,
3580                                                folded_depth,
3581                                                cx,
3582                                            );
3583                                        }
3584                                    }
3585
3586                                    folded_dirs_entry = if parent_folded {
3587                                        None
3588                                    } else {
3589                                        Some((
3590                                            depth,
3591                                            FoldedDirsEntry {
3592                                                worktree_id: directory_entry.worktree_id,
3593                                                entries: vec![directory_entry.entry.clone()],
3594                                            },
3595                                        ))
3596                                    };
3597                                }
3598                            } else if folded {
3599                                folded_dirs_entry = Some((
3600                                    depth,
3601                                    FoldedDirsEntry {
3602                                        worktree_id: directory_entry.worktree_id,
3603                                        entries: vec![directory_entry.entry.clone()],
3604                                    },
3605                                ));
3606                            }
3607
3608                            let should_add =
3609                                should_add && parent_expanded && folded_dirs_entry.is_none();
3610                            (depth, should_add)
3611                        }
3612                        FsEntry::ExternalFile(..) => {
3613                            if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() {
3614                                let parent_expanded = parent_dirs
3615                                    .iter()
3616                                    .rev()
3617                                    .find(|parent| {
3618                                        folded_dir
3619                                            .entries
3620                                            .iter()
3621                                            .all(|entry| entry.path != parent.path)
3622                                    })
3623                                    .map_or(true, |parent| parent.expanded);
3624                                if !is_singleton && (parent_expanded || query.is_some()) {
3625                                    outline_panel.push_entry(
3626                                        &mut generation_state,
3627                                        track_matches,
3628                                        PanelEntry::FoldedDirs(folded_dir),
3629                                        folded_depth,
3630                                        cx,
3631                                    );
3632                                }
3633                            }
3634                            parent_dirs.clear();
3635                            (0, true)
3636                        }
3637                        FsEntry::File(file) => {
3638                            if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3639                                let parent_expanded = parent_dirs
3640                                    .iter()
3641                                    .rev()
3642                                    .find(|parent| {
3643                                        folded_dirs
3644                                            .entries
3645                                            .iter()
3646                                            .all(|entry| entry.path != parent.path)
3647                                    })
3648                                    .map_or(true, |parent| parent.expanded);
3649                                if !is_singleton && (parent_expanded || query.is_some()) {
3650                                    outline_panel.push_entry(
3651                                        &mut generation_state,
3652                                        track_matches,
3653                                        PanelEntry::FoldedDirs(folded_dirs),
3654                                        folded_depth,
3655                                        cx,
3656                                    );
3657                                }
3658                            }
3659
3660                            let fs_depth = outline_panel
3661                                .fs_entries_depth
3662                                .get(&(file.worktree_id, file.entry.id))
3663                                .copied()
3664                                .unwrap_or(0);
3665                            while let Some(parent) = parent_dirs.last() {
3666                                if file.entry.path.starts_with(&parent.path) {
3667                                    break;
3668                                }
3669                                parent_dirs.pop();
3670                            }
3671                            match parent_dirs.last() {
3672                                Some(parent) => {
3673                                    let new_depth = parent.depth + 1;
3674                                    (new_depth, parent.expanded)
3675                                }
3676                                None => (fs_depth, true),
3677                            }
3678                        }
3679                    };
3680
3681                    if !is_singleton
3682                        && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3683                    {
3684                        outline_panel.push_entry(
3685                            &mut generation_state,
3686                            track_matches,
3687                            PanelEntry::Fs(entry.clone()),
3688                            depth,
3689                            cx,
3690                        );
3691                    }
3692
3693                    match outline_panel.mode {
3694                        ItemsDisplayMode::Search(_) => {
3695                            if is_singleton || query.is_some() || (should_add && is_expanded) {
3696                                outline_panel.add_search_entries(
3697                                    &mut generation_state,
3698                                    &active_editor,
3699                                    entry.clone(),
3700                                    depth,
3701                                    query.clone(),
3702                                    is_singleton,
3703                                    cx,
3704                                );
3705                            }
3706                        }
3707                        ItemsDisplayMode::Outline => {
3708                            let excerpts_to_consider =
3709                                if is_singleton || query.is_some() || (should_add && is_expanded) {
3710                                    match &entry {
3711                                        FsEntry::File(FsEntryFile {
3712                                            buffer_id,
3713                                            excerpts,
3714                                            ..
3715                                        })
3716                                        | FsEntry::ExternalFile(FsEntryExternalFile {
3717                                            buffer_id,
3718                                            excerpts,
3719                                            ..
3720                                        }) => Some((*buffer_id, excerpts)),
3721                                        _ => None,
3722                                    }
3723                                } else {
3724                                    None
3725                                };
3726                            if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
3727                                if !active_editor.read(cx).is_buffer_folded(buffer_id, cx) {
3728                                    outline_panel.add_excerpt_entries(
3729                                        &mut generation_state,
3730                                        buffer_id,
3731                                        entry_excerpts,
3732                                        depth,
3733                                        track_matches,
3734                                        is_singleton,
3735                                        query.as_deref(),
3736                                        cx,
3737                                    );
3738                                }
3739                            }
3740                        }
3741                    }
3742
3743                    if is_singleton
3744                        && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3745                        && !generation_state.entries.iter().any(|item| {
3746                            matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3747                        })
3748                    {
3749                        outline_panel.push_entry(
3750                            &mut generation_state,
3751                            track_matches,
3752                            PanelEntry::Fs(entry.clone()),
3753                            0,
3754                            cx,
3755                        );
3756                    }
3757                }
3758
3759                if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() {
3760                    let parent_expanded = parent_dirs
3761                        .iter()
3762                        .rev()
3763                        .find(|parent| {
3764                            folded_dirs
3765                                .entries
3766                                .iter()
3767                                .all(|entry| entry.path != parent.path)
3768                        })
3769                        .map_or(true, |parent| parent.expanded);
3770                    if parent_expanded || query.is_some() {
3771                        outline_panel.push_entry(
3772                            &mut generation_state,
3773                            track_matches,
3774                            PanelEntry::FoldedDirs(folded_dirs),
3775                            folded_depth,
3776                            cx,
3777                        );
3778                    }
3779                }
3780            }) else {
3781                return (Vec::new(), None);
3782            };
3783
3784            let Some(query) = query else {
3785                return (
3786                    generation_state.entries,
3787                    generation_state
3788                        .max_width_estimate_and_index
3789                        .map(|(_, index)| index),
3790                );
3791            };
3792
3793            let mut matched_ids = match_strings(
3794                &generation_state.match_candidates,
3795                &query,
3796                true,
3797                usize::MAX,
3798                &AtomicBool::default(),
3799                cx.background_executor().clone(),
3800            )
3801            .await
3802            .into_iter()
3803            .map(|string_match| (string_match.candidate_id, string_match))
3804            .collect::<HashMap<_, _>>();
3805
3806            let mut id = 0;
3807            generation_state.entries.retain_mut(|cached_entry| {
3808                let retain = match matched_ids.remove(&id) {
3809                    Some(string_match) => {
3810                        cached_entry.string_match = Some(string_match);
3811                        true
3812                    }
3813                    None => false,
3814                };
3815                id += 1;
3816                retain
3817            });
3818
3819            (
3820                generation_state.entries,
3821                generation_state
3822                    .max_width_estimate_and_index
3823                    .map(|(_, index)| index),
3824            )
3825        })
3826    }
3827
3828    fn push_entry(
3829        &self,
3830        state: &mut GenerationState,
3831        track_matches: bool,
3832        entry: PanelEntry,
3833        depth: usize,
3834        cx: &mut App,
3835    ) {
3836        let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry {
3837            match folded_dirs_entry.entries.len() {
3838                0 => {
3839                    debug_panic!("Empty folded dirs receiver");
3840                    return;
3841                }
3842                1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory {
3843                    worktree_id: folded_dirs_entry.worktree_id,
3844                    entry: folded_dirs_entry.entries[0].clone(),
3845                })),
3846                _ => entry,
3847            }
3848        } else {
3849            entry
3850        };
3851
3852        if track_matches {
3853            let id = state.entries.len();
3854            match &entry {
3855                PanelEntry::Fs(fs_entry) => {
3856                    if let Some(file_name) =
3857                        self.relative_path(fs_entry, cx).as_deref().map(file_name)
3858                    {
3859                        state
3860                            .match_candidates
3861                            .push(StringMatchCandidate::new(id, &file_name));
3862                    }
3863                }
3864                PanelEntry::FoldedDirs(folded_dir_entry) => {
3865                    let dir_names = self.dir_names_string(
3866                        &folded_dir_entry.entries,
3867                        folded_dir_entry.worktree_id,
3868                        cx,
3869                    );
3870                    {
3871                        state
3872                            .match_candidates
3873                            .push(StringMatchCandidate::new(id, &dir_names));
3874                    }
3875                }
3876                PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state
3877                    .match_candidates
3878                    .push(StringMatchCandidate::new(id, &outline_entry.outline.text)),
3879                PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {}
3880                PanelEntry::Search(new_search_entry) => {
3881                    if let Some(search_data) = new_search_entry.render_data.get() {
3882                        state
3883                            .match_candidates
3884                            .push(StringMatchCandidate::new(id, &search_data.context_text));
3885                    }
3886                }
3887            }
3888        }
3889
3890        let width_estimate = self.width_estimate(depth, &entry, cx);
3891        if Some(width_estimate)
3892            > state
3893                .max_width_estimate_and_index
3894                .map(|(estimate, _)| estimate)
3895        {
3896            state.max_width_estimate_and_index = Some((width_estimate, state.entries.len()));
3897        }
3898        state.entries.push(CachedEntry {
3899            depth,
3900            entry,
3901            string_match: None,
3902        });
3903    }
3904
3905    fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String {
3906        let dir_names_segment = entries
3907            .iter()
3908            .map(|entry| self.entry_name(&worktree_id, entry, cx))
3909            .collect::<PathBuf>();
3910        dir_names_segment.to_string_lossy().to_string()
3911    }
3912
3913    fn query(&self, cx: &App) -> Option<String> {
3914        let query = self.filter_editor.read(cx).text(cx);
3915        if query.trim().is_empty() {
3916            None
3917        } else {
3918            Some(query)
3919        }
3920    }
3921
3922    fn is_expanded(&self, entry: &FsEntry) -> bool {
3923        let entry_to_check = match entry {
3924            FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => {
3925                CollapsedEntry::ExternalFile(*buffer_id)
3926            }
3927            FsEntry::File(FsEntryFile {
3928                worktree_id,
3929                buffer_id,
3930                ..
3931            }) => CollapsedEntry::File(*worktree_id, *buffer_id),
3932            FsEntry::Directory(FsEntryDirectory {
3933                worktree_id, entry, ..
3934            }) => CollapsedEntry::Dir(*worktree_id, entry.id),
3935        };
3936        !self.collapsed_entries.contains(&entry_to_check)
3937    }
3938
3939    fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context<OutlinePanel>) -> bool {
3940        if !self.active {
3941            return false;
3942        }
3943
3944        let mut update_cached_items = false;
3945        update_cached_items |= self.update_search_matches(window, cx);
3946        self.fetch_outdated_outlines(window, cx);
3947        if update_cached_items {
3948            self.selected_entry.invalidate();
3949        }
3950        update_cached_items
3951    }
3952
3953    fn update_search_matches(
3954        &mut self,
3955        window: &mut Window,
3956        cx: &mut Context<OutlinePanel>,
3957    ) -> bool {
3958        if !self.active {
3959            return false;
3960        }
3961
3962        let project_search = self
3963            .active_item()
3964            .and_then(|item| item.downcast::<ProjectSearchView>());
3965        let project_search_matches = project_search
3966            .as_ref()
3967            .map(|project_search| project_search.read(cx).get_matches(cx))
3968            .unwrap_or_default();
3969
3970        let buffer_search = self
3971            .active_item()
3972            .as_deref()
3973            .and_then(|active_item| {
3974                self.workspace
3975                    .upgrade()
3976                    .and_then(|workspace| workspace.read(cx).pane_for(active_item))
3977            })
3978            .and_then(|pane| {
3979                pane.read(cx)
3980                    .toolbar()
3981                    .read(cx)
3982                    .item_of_type::<BufferSearchBar>()
3983            });
3984        let buffer_search_matches = self
3985            .active_editor()
3986            .map(|active_editor| {
3987                active_editor.update(cx, |editor, cx| editor.get_matches(window, cx))
3988            })
3989            .unwrap_or_default();
3990
3991        let mut update_cached_entries = false;
3992        if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
3993            if matches!(self.mode, ItemsDisplayMode::Search(_)) {
3994                self.mode = ItemsDisplayMode::Outline;
3995                update_cached_entries = true;
3996            }
3997        } else {
3998            let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
3999                (
4000                    SearchKind::Project,
4001                    project_search_matches,
4002                    project_search
4003                        .map(|project_search| project_search.read(cx).search_query_text(cx))
4004                        .unwrap_or_default(),
4005                )
4006            } else {
4007                (
4008                    SearchKind::Buffer,
4009                    buffer_search_matches,
4010                    buffer_search
4011                        .map(|buffer_search| buffer_search.read(cx).query(cx))
4012                        .unwrap_or_default(),
4013                )
4014            };
4015
4016            let mut previous_matches = HashMap::default();
4017            update_cached_entries = match &mut self.mode {
4018                ItemsDisplayMode::Search(current_search_state) => {
4019                    let update = current_search_state.query != new_search_query
4020                        || current_search_state.kind != kind
4021                        || current_search_state.matches.is_empty()
4022                        || current_search_state.matches.iter().enumerate().any(
4023                            |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
4024                        );
4025                    if current_search_state.kind == kind {
4026                        previous_matches.extend(current_search_state.matches.drain(..));
4027                    }
4028                    update
4029                }
4030                ItemsDisplayMode::Outline => true,
4031            };
4032            self.mode = ItemsDisplayMode::Search(SearchState::new(
4033                kind,
4034                new_search_query,
4035                previous_matches,
4036                new_search_matches,
4037                cx.theme().syntax().clone(),
4038                window,
4039                cx,
4040            ));
4041        }
4042        update_cached_entries
4043    }
4044
4045    fn add_excerpt_entries(
4046        &self,
4047        state: &mut GenerationState,
4048        buffer_id: BufferId,
4049        entries_to_add: &[ExcerptId],
4050        parent_depth: usize,
4051        track_matches: bool,
4052        is_singleton: bool,
4053        query: Option<&str>,
4054        cx: &mut Context<Self>,
4055    ) {
4056        if let Some(excerpts) = self.excerpts.get(&buffer_id) {
4057            for &excerpt_id in entries_to_add {
4058                let Some(excerpt) = excerpts.get(&excerpt_id) else {
4059                    continue;
4060                };
4061                let excerpt_depth = parent_depth + 1;
4062                self.push_entry(
4063                    state,
4064                    track_matches,
4065                    PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt {
4066                        buffer_id,
4067                        id: excerpt_id,
4068                        range: excerpt.range.clone(),
4069                    })),
4070                    excerpt_depth,
4071                    cx,
4072                );
4073
4074                let mut outline_base_depth = excerpt_depth + 1;
4075                if is_singleton {
4076                    outline_base_depth = 0;
4077                    state.clear();
4078                } else if query.is_none()
4079                    && self
4080                        .collapsed_entries
4081                        .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
4082                {
4083                    continue;
4084                }
4085
4086                for outline in excerpt.iter_outlines() {
4087                    self.push_entry(
4088                        state,
4089                        track_matches,
4090                        PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline {
4091                            buffer_id,
4092                            excerpt_id,
4093                            outline: outline.clone(),
4094                        })),
4095                        outline_base_depth + outline.depth,
4096                        cx,
4097                    );
4098                }
4099            }
4100        }
4101    }
4102
4103    fn add_search_entries(
4104        &mut self,
4105        state: &mut GenerationState,
4106        active_editor: &Entity<Editor>,
4107        parent_entry: FsEntry,
4108        parent_depth: usize,
4109        filter_query: Option<String>,
4110        is_singleton: bool,
4111        cx: &mut Context<Self>,
4112    ) {
4113        let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
4114            return;
4115        };
4116
4117        let kind = search_state.kind;
4118        let related_excerpts = match &parent_entry {
4119            FsEntry::Directory(_) => return,
4120            FsEntry::ExternalFile(external) => &external.excerpts,
4121            FsEntry::File(file) => &file.excerpts,
4122        }
4123        .iter()
4124        .copied()
4125        .collect::<HashSet<_>>();
4126
4127        let depth = if is_singleton { 0 } else { parent_depth + 1 };
4128        let new_search_matches = search_state
4129            .matches
4130            .iter()
4131            .filter(|(match_range, _)| {
4132                related_excerpts.contains(&match_range.start.excerpt_id)
4133                    || related_excerpts.contains(&match_range.end.excerpt_id)
4134            })
4135            .filter(|(match_range, _)| {
4136                let editor = active_editor.read(cx);
4137                if let Some(buffer_id) = match_range.start.buffer_id {
4138                    if editor.is_buffer_folded(buffer_id, cx) {
4139                        return false;
4140                    }
4141                }
4142                if let Some(buffer_id) = match_range.start.buffer_id {
4143                    if editor.is_buffer_folded(buffer_id, cx) {
4144                        return false;
4145                    }
4146                }
4147                true
4148            });
4149
4150        let new_search_entries = new_search_matches
4151            .map(|(match_range, search_data)| SearchEntry {
4152                match_range: match_range.clone(),
4153                kind,
4154                render_data: Arc::clone(search_data),
4155            })
4156            .collect::<Vec<_>>();
4157        for new_search_entry in new_search_entries {
4158            self.push_entry(
4159                state,
4160                filter_query.is_some(),
4161                PanelEntry::Search(new_search_entry),
4162                depth,
4163                cx,
4164            );
4165        }
4166    }
4167
4168    fn active_editor(&self) -> Option<Entity<Editor>> {
4169        self.active_item.as_ref()?.active_editor.upgrade()
4170    }
4171
4172    fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
4173        self.active_item.as_ref()?.item_handle.upgrade()
4174    }
4175
4176    fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
4177        self.active_item().map_or(true, |active_item| {
4178            !self.pinned && active_item.item_id() != new_active_item.item_id()
4179        })
4180    }
4181
4182    pub fn toggle_active_editor_pin(
4183        &mut self,
4184        _: &ToggleActiveEditorPin,
4185        window: &mut Window,
4186        cx: &mut Context<Self>,
4187    ) {
4188        self.pinned = !self.pinned;
4189        if !self.pinned {
4190            if let Some((active_item, active_editor)) = self
4191                .workspace
4192                .upgrade()
4193                .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
4194            {
4195                if self.should_replace_active_item(active_item.as_ref()) {
4196                    self.replace_active_editor(active_item, active_editor, window, cx);
4197                }
4198            }
4199        }
4200
4201        cx.notify();
4202    }
4203
4204    fn selected_entry(&self) -> Option<&PanelEntry> {
4205        match &self.selected_entry {
4206            SelectedEntry::Invalidated(entry) => entry.as_ref(),
4207            SelectedEntry::Valid(entry, _) => Some(entry),
4208            SelectedEntry::None => None,
4209        }
4210    }
4211
4212    fn select_entry(
4213        &mut self,
4214        entry: PanelEntry,
4215        focus: bool,
4216        window: &mut Window,
4217        cx: &mut Context<Self>,
4218    ) {
4219        if focus {
4220            self.focus_handle.focus(window);
4221        }
4222        let ix = self
4223            .cached_entries
4224            .iter()
4225            .enumerate()
4226            .find(|(_, cached_entry)| &cached_entry.entry == &entry)
4227            .map(|(i, _)| i)
4228            .unwrap_or_default();
4229
4230        self.selected_entry = SelectedEntry::Valid(entry, ix);
4231
4232        self.autoscroll(cx);
4233        cx.notify();
4234    }
4235
4236    fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
4237        if !Self::should_show_scrollbar(cx)
4238            || !(self.show_scrollbar || self.vertical_scrollbar_state.is_dragging())
4239        {
4240            return None;
4241        }
4242        Some(
4243            div()
4244                .occlude()
4245                .id("project-panel-vertical-scroll")
4246                .on_mouse_move(cx.listener(|_, _, _, cx| {
4247                    cx.notify();
4248                    cx.stop_propagation()
4249                }))
4250                .on_hover(|_, _, cx| {
4251                    cx.stop_propagation();
4252                })
4253                .on_any_mouse_down(|_, _, cx| {
4254                    cx.stop_propagation();
4255                })
4256                .on_mouse_up(
4257                    MouseButton::Left,
4258                    cx.listener(|outline_panel, _, window, cx| {
4259                        if !outline_panel.vertical_scrollbar_state.is_dragging()
4260                            && !outline_panel.focus_handle.contains_focused(window, cx)
4261                        {
4262                            outline_panel.hide_scrollbar(window, cx);
4263                            cx.notify();
4264                        }
4265
4266                        cx.stop_propagation();
4267                    }),
4268                )
4269                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4270                    cx.notify();
4271                }))
4272                .h_full()
4273                .absolute()
4274                .right_1()
4275                .top_1()
4276                .bottom_0()
4277                .w(px(12.))
4278                .cursor_default()
4279                .children(Scrollbar::vertical(self.vertical_scrollbar_state.clone())),
4280        )
4281    }
4282
4283    fn render_horizontal_scrollbar(
4284        &self,
4285        _: &mut Window,
4286        cx: &mut Context<Self>,
4287    ) -> Option<Stateful<Div>> {
4288        if !Self::should_show_scrollbar(cx)
4289            || !(self.show_scrollbar || self.horizontal_scrollbar_state.is_dragging())
4290        {
4291            return None;
4292        }
4293
4294        let scroll_handle = self.scroll_handle.0.borrow();
4295        let longest_item_width = scroll_handle
4296            .last_item_size
4297            .filter(|size| size.contents.width > size.item.width)?
4298            .contents
4299            .width
4300            .0 as f64;
4301        if longest_item_width < scroll_handle.base_handle.bounds().size.width.0 as f64 {
4302            return None;
4303        }
4304
4305        Some(
4306            div()
4307                .occlude()
4308                .id("project-panel-horizontal-scroll")
4309                .on_mouse_move(cx.listener(|_, _, _, cx| {
4310                    cx.notify();
4311                    cx.stop_propagation()
4312                }))
4313                .on_hover(|_, _, cx| {
4314                    cx.stop_propagation();
4315                })
4316                .on_any_mouse_down(|_, _, cx| {
4317                    cx.stop_propagation();
4318                })
4319                .on_mouse_up(
4320                    MouseButton::Left,
4321                    cx.listener(|outline_panel, _, window, cx| {
4322                        if !outline_panel.horizontal_scrollbar_state.is_dragging()
4323                            && !outline_panel.focus_handle.contains_focused(window, cx)
4324                        {
4325                            outline_panel.hide_scrollbar(window, cx);
4326                            cx.notify();
4327                        }
4328
4329                        cx.stop_propagation();
4330                    }),
4331                )
4332                .on_scroll_wheel(cx.listener(|_, _, _, cx| {
4333                    cx.notify();
4334                }))
4335                .w_full()
4336                .absolute()
4337                .right_1()
4338                .left_1()
4339                .bottom_0()
4340                .h(px(12.))
4341                .cursor_default()
4342                .when(self.width.is_some(), |this| {
4343                    this.children(Scrollbar::horizontal(
4344                        self.horizontal_scrollbar_state.clone(),
4345                    ))
4346                }),
4347        )
4348    }
4349
4350    fn should_show_scrollbar(cx: &App) -> bool {
4351        let show = OutlinePanelSettings::get_global(cx)
4352            .scrollbar
4353            .show
4354            .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show);
4355        match show {
4356            ShowScrollbar::Auto => true,
4357            ShowScrollbar::System => true,
4358            ShowScrollbar::Always => true,
4359            ShowScrollbar::Never => false,
4360        }
4361    }
4362
4363    fn should_autohide_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 => cx
4371                .try_global::<ScrollbarAutoHide>()
4372                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
4373            ShowScrollbar::Always => false,
4374            ShowScrollbar::Never => true,
4375        }
4376    }
4377
4378    fn hide_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4379        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
4380        if !Self::should_autohide_scrollbar(cx) {
4381            return;
4382        }
4383        self.hide_scrollbar_task = Some(cx.spawn_in(window, |panel, mut cx| async move {
4384            cx.background_executor()
4385                .timer(SCROLLBAR_SHOW_INTERVAL)
4386                .await;
4387            panel
4388                .update(&mut cx, |panel, cx| {
4389                    panel.show_scrollbar = false;
4390                    cx.notify();
4391                })
4392                .log_err();
4393        }))
4394    }
4395
4396    fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 {
4397        let item_text_chars = match entry {
4398            PanelEntry::Fs(FsEntry::ExternalFile(external)) => self
4399                .buffer_snapshot_for_id(external.buffer_id, cx)
4400                .and_then(|snapshot| {
4401                    Some(snapshot.file()?.path().file_name()?.to_string_lossy().len())
4402                })
4403                .unwrap_or_default(),
4404            PanelEntry::Fs(FsEntry::Directory(directory)) => directory
4405                .entry
4406                .path
4407                .file_name()
4408                .map(|name| name.to_string_lossy().len())
4409                .unwrap_or_default(),
4410            PanelEntry::Fs(FsEntry::File(file)) => file
4411                .entry
4412                .path
4413                .file_name()
4414                .map(|name| name.to_string_lossy().len())
4415                .unwrap_or_default(),
4416            PanelEntry::FoldedDirs(folded_dirs) => {
4417                folded_dirs
4418                    .entries
4419                    .iter()
4420                    .map(|dir| {
4421                        dir.path
4422                            .file_name()
4423                            .map(|name| name.to_string_lossy().len())
4424                            .unwrap_or_default()
4425                    })
4426                    .sum::<usize>()
4427                    + folded_dirs.entries.len().saturating_sub(1) * MAIN_SEPARATOR_STR.len()
4428            }
4429            PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self
4430                .excerpt_label(excerpt.buffer_id, &excerpt.range, cx)
4431                .map(|label| label.len())
4432                .unwrap_or_default(),
4433            PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(),
4434            PanelEntry::Search(search) => search
4435                .render_data
4436                .get()
4437                .map(|data| data.context_text.len())
4438                .unwrap_or_default(),
4439        };
4440
4441        (item_text_chars + depth) as u64
4442    }
4443
4444    fn render_main_contents(
4445        &mut self,
4446        query: Option<String>,
4447        show_indent_guides: bool,
4448        indent_size: f32,
4449        window: &mut Window,
4450        cx: &mut Context<Self>,
4451    ) -> Div {
4452        let contents = if self.cached_entries.is_empty() {
4453            let header = if self.updating_fs_entries || self.updating_cached_entries {
4454                None
4455            } else if query.is_some() {
4456                Some("No matches for query")
4457            } else {
4458                Some("No outlines available")
4459            };
4460
4461            v_flex()
4462                .flex_1()
4463                .justify_center()
4464                .size_full()
4465                .when_some(header, |panel, header| {
4466                    panel
4467                        .child(h_flex().justify_center().child(Label::new(header)))
4468                        .when_some(query.clone(), |panel, query| {
4469                            panel.child(h_flex().justify_center().child(Label::new(query)))
4470                        })
4471                        .child(
4472                            h_flex()
4473                                .pt(DynamicSpacing::Base04.rems(cx))
4474                                .justify_center()
4475                                .child({
4476                                    let keystroke =
4477                                        match self.position(window, cx) {
4478                                            DockPosition::Left => window
4479                                                .keystroke_text_for(&workspace::ToggleLeftDock),
4480                                            DockPosition::Bottom => window
4481                                                .keystroke_text_for(&workspace::ToggleBottomDock),
4482                                            DockPosition::Right => window
4483                                                .keystroke_text_for(&workspace::ToggleRightDock),
4484                                        };
4485                                    Label::new(format!("Toggle this panel with {keystroke}"))
4486                                }),
4487                        )
4488                })
4489        } else {
4490            let list_contents = {
4491                let items_len = self.cached_entries.len();
4492                let multi_buffer_snapshot = self
4493                    .active_editor()
4494                    .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
4495                uniform_list(cx.entity().clone(), "entries", items_len, {
4496                    move |outline_panel, range, window, cx| {
4497                        let entries = outline_panel.cached_entries.get(range);
4498                        entries
4499                            .map(|entries| entries.to_vec())
4500                            .unwrap_or_default()
4501                            .into_iter()
4502                            .filter_map(|cached_entry| match cached_entry.entry {
4503                                PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
4504                                    &entry,
4505                                    cached_entry.depth,
4506                                    cached_entry.string_match.as_ref(),
4507                                    window,
4508                                    cx,
4509                                )),
4510                                PanelEntry::FoldedDirs(folded_dirs_entry) => {
4511                                    Some(outline_panel.render_folded_dirs(
4512                                        &folded_dirs_entry,
4513                                        cached_entry.depth,
4514                                        cached_entry.string_match.as_ref(),
4515                                        window,
4516                                        cx,
4517                                    ))
4518                                }
4519                                PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => {
4520                                    outline_panel.render_excerpt(
4521                                        &excerpt,
4522                                        cached_entry.depth,
4523                                        window,
4524                                        cx,
4525                                    )
4526                                }
4527                                PanelEntry::Outline(OutlineEntry::Outline(entry)) => {
4528                                    Some(outline_panel.render_outline(
4529                                        &entry,
4530                                        cached_entry.depth,
4531                                        cached_entry.string_match.as_ref(),
4532                                        window,
4533                                        cx,
4534                                    ))
4535                                }
4536                                PanelEntry::Search(SearchEntry {
4537                                    match_range,
4538                                    render_data,
4539                                    kind,
4540                                    ..
4541                                }) => outline_panel.render_search_match(
4542                                    multi_buffer_snapshot.as_ref(),
4543                                    &match_range,
4544                                    &render_data,
4545                                    kind,
4546                                    cached_entry.depth,
4547                                    cached_entry.string_match.as_ref(),
4548                                    window,
4549                                    cx,
4550                                ),
4551                            })
4552                            .collect()
4553                    }
4554                })
4555                .with_sizing_behavior(ListSizingBehavior::Infer)
4556                .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained)
4557                .with_width_from_item(self.max_width_item_index)
4558                .track_scroll(self.scroll_handle.clone())
4559                .when(show_indent_guides, |list| {
4560                    list.with_decoration(
4561                        ui::indent_guides(
4562                            cx.entity().clone(),
4563                            px(indent_size),
4564                            IndentGuideColors::panel(cx),
4565                            |outline_panel, range, _, _| {
4566                                let entries = outline_panel.cached_entries.get(range);
4567                                if let Some(entries) = entries {
4568                                    entries.into_iter().map(|item| item.depth).collect()
4569                                } else {
4570                                    smallvec::SmallVec::new()
4571                                }
4572                            },
4573                        )
4574                        .with_render_fn(
4575                            cx.entity().clone(),
4576                            move |outline_panel, params, _, _| {
4577                                const LEFT_OFFSET: f32 = 14.;
4578
4579                                let indent_size = params.indent_size;
4580                                let item_height = params.item_height;
4581                                let active_indent_guide_ix = find_active_indent_guide_ix(
4582                                    outline_panel,
4583                                    &params.indent_guides,
4584                                );
4585
4586                                params
4587                                    .indent_guides
4588                                    .into_iter()
4589                                    .enumerate()
4590                                    .map(|(ix, layout)| {
4591                                        let bounds = Bounds::new(
4592                                            point(
4593                                                px(layout.offset.x as f32) * indent_size
4594                                                    + px(LEFT_OFFSET),
4595                                                px(layout.offset.y as f32) * item_height,
4596                                            ),
4597                                            size(px(1.), px(layout.length as f32) * item_height),
4598                                        );
4599                                        ui::RenderedIndentGuide {
4600                                            bounds,
4601                                            layout,
4602                                            is_active: active_indent_guide_ix == Some(ix),
4603                                            hitbox: None,
4604                                        }
4605                                    })
4606                                    .collect()
4607                            },
4608                        ),
4609                    )
4610                })
4611            };
4612
4613            v_flex()
4614                .flex_shrink()
4615                .size_full()
4616                .child(list_contents.size_full().flex_shrink())
4617                .children(self.render_vertical_scrollbar(cx))
4618                .when_some(
4619                    self.render_horizontal_scrollbar(window, cx),
4620                    |this, scrollbar| this.pb_4().child(scrollbar),
4621                )
4622        }
4623        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
4624            deferred(
4625                anchored()
4626                    .position(*position)
4627                    .anchor(gpui::Corner::TopLeft)
4628                    .child(menu.clone()),
4629            )
4630            .with_priority(1)
4631        }));
4632
4633        v_flex().w_full().flex_1().overflow_hidden().child(contents)
4634    }
4635
4636    fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context<Self>) -> Div {
4637        v_flex().flex_none().child(horizontal_separator(cx)).child(
4638            h_flex()
4639                .p_2()
4640                .w_full()
4641                .child(self.filter_editor.clone())
4642                .child(
4643                    div().child(
4644                        IconButton::new(
4645                            "outline-panel-menu",
4646                            if pinned {
4647                                IconName::Unpin
4648                            } else {
4649                                IconName::Pin
4650                            },
4651                        )
4652                        .tooltip(Tooltip::text(if pinned {
4653                            "Unpin Outline"
4654                        } else {
4655                            "Pin Active Outline"
4656                        }))
4657                        .shape(IconButtonShape::Square)
4658                        .on_click(cx.listener(
4659                            |outline_panel, _, window, cx| {
4660                                outline_panel.toggle_active_editor_pin(
4661                                    &ToggleActiveEditorPin,
4662                                    window,
4663                                    cx,
4664                                );
4665                            },
4666                        )),
4667                    ),
4668                ),
4669        )
4670    }
4671
4672    fn buffers_inside_directory(
4673        &self,
4674        dir_worktree: WorktreeId,
4675        dir_entry: &GitEntry,
4676    ) -> HashSet<BufferId> {
4677        if !dir_entry.is_dir() {
4678            debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}");
4679            return HashSet::default();
4680        }
4681
4682        self.fs_entries
4683            .iter()
4684            .skip_while(|fs_entry| match fs_entry {
4685                FsEntry::Directory(directory) => {
4686                    directory.worktree_id != dir_worktree || &directory.entry != dir_entry
4687                }
4688                _ => true,
4689            })
4690            .skip(1)
4691            .take_while(|fs_entry| match fs_entry {
4692                FsEntry::ExternalFile(..) => false,
4693                FsEntry::Directory(directory) => {
4694                    directory.worktree_id == dir_worktree
4695                        && directory.entry.path.starts_with(&dir_entry.path)
4696                }
4697                FsEntry::File(file) => {
4698                    file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path)
4699                }
4700            })
4701            .filter_map(|fs_entry| match fs_entry {
4702                FsEntry::File(file) => Some(file.buffer_id),
4703                _ => None,
4704            })
4705            .collect()
4706    }
4707}
4708
4709fn workspace_active_editor(
4710    workspace: &Workspace,
4711    cx: &App,
4712) -> Option<(Box<dyn ItemHandle>, Entity<Editor>)> {
4713    let active_item = workspace.active_item(cx)?;
4714    let active_editor = active_item
4715        .act_as::<Editor>(cx)
4716        .filter(|editor| editor.read(cx).mode() == EditorMode::Full)?;
4717    Some((active_item, active_editor))
4718}
4719
4720fn back_to_common_visited_parent(
4721    visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
4722    worktree_id: &WorktreeId,
4723    new_entry: &Entry,
4724) -> Option<(WorktreeId, ProjectEntryId)> {
4725    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
4726        match new_entry.path.parent() {
4727            Some(parent_path) => {
4728                if parent_path == visited_path.as_ref() {
4729                    return Some((*worktree_id, *visited_dir_id));
4730                }
4731            }
4732            None => {
4733                break;
4734            }
4735        }
4736        visited_dirs.pop();
4737    }
4738    None
4739}
4740
4741fn file_name(path: &Path) -> String {
4742    let mut current_path = path;
4743    loop {
4744        if let Some(file_name) = current_path.file_name() {
4745            return file_name.to_string_lossy().into_owned();
4746        }
4747        match current_path.parent() {
4748            Some(parent) => current_path = parent,
4749            None => return path.to_string_lossy().into_owned(),
4750        }
4751    }
4752}
4753
4754impl Panel for OutlinePanel {
4755    fn persistent_name() -> &'static str {
4756        "Outline Panel"
4757    }
4758
4759    fn position(&self, _: &Window, cx: &App) -> DockPosition {
4760        match OutlinePanelSettings::get_global(cx).dock {
4761            OutlinePanelDockPosition::Left => DockPosition::Left,
4762            OutlinePanelDockPosition::Right => DockPosition::Right,
4763        }
4764    }
4765
4766    fn position_is_valid(&self, position: DockPosition) -> bool {
4767        matches!(position, DockPosition::Left | DockPosition::Right)
4768    }
4769
4770    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
4771        settings::update_settings_file::<OutlinePanelSettings>(
4772            self.fs.clone(),
4773            cx,
4774            move |settings, _| {
4775                let dock = match position {
4776                    DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
4777                    DockPosition::Right => OutlinePanelDockPosition::Right,
4778                };
4779                settings.dock = Some(dock);
4780            },
4781        );
4782    }
4783
4784    fn size(&self, _: &Window, cx: &App) -> Pixels {
4785        self.width
4786            .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
4787    }
4788
4789    fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
4790        self.width = size;
4791        self.serialize(cx);
4792        cx.notify();
4793    }
4794
4795    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
4796        OutlinePanelSettings::get_global(cx)
4797            .button
4798            .then_some(IconName::ListTree)
4799    }
4800
4801    fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> {
4802        Some("Outline Panel")
4803    }
4804
4805    fn toggle_action(&self) -> Box<dyn Action> {
4806        Box::new(ToggleFocus)
4807    }
4808
4809    fn starts_open(&self, _window: &Window, _: &App) -> bool {
4810        self.active
4811    }
4812
4813    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
4814        cx.spawn_in(window, |outline_panel, mut cx| async move {
4815            outline_panel
4816                .update_in(&mut cx, |outline_panel, window, cx| {
4817                    let old_active = outline_panel.active;
4818                    outline_panel.active = active;
4819                    if old_active != active {
4820                        if active {
4821                            if let Some((active_item, active_editor)) =
4822                                outline_panel.workspace.upgrade().and_then(|workspace| {
4823                                    workspace_active_editor(workspace.read(cx), cx)
4824                                })
4825                            {
4826                                if outline_panel.should_replace_active_item(active_item.as_ref()) {
4827                                    outline_panel.replace_active_editor(
4828                                        active_item,
4829                                        active_editor,
4830                                        window,
4831                                        cx,
4832                                    );
4833                                } else {
4834                                    outline_panel.update_fs_entries(active_editor, None, window, cx)
4835                                }
4836                                return;
4837                            }
4838                        }
4839
4840                        if !outline_panel.pinned {
4841                            outline_panel.clear_previous(window, cx);
4842                        }
4843                    }
4844                    outline_panel.serialize(cx);
4845                })
4846                .ok();
4847        })
4848        .detach()
4849    }
4850
4851    fn activation_priority(&self) -> u32 {
4852        5
4853    }
4854}
4855
4856impl Focusable for OutlinePanel {
4857    fn focus_handle(&self, cx: &App) -> FocusHandle {
4858        self.filter_editor.focus_handle(cx).clone()
4859    }
4860}
4861
4862impl EventEmitter<Event> for OutlinePanel {}
4863
4864impl EventEmitter<PanelEvent> for OutlinePanel {}
4865
4866impl Render for OutlinePanel {
4867    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4868        let (is_local, is_via_ssh) = self
4869            .project
4870            .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
4871        let query = self.query(cx);
4872        let pinned = self.pinned;
4873        let settings = OutlinePanelSettings::get_global(cx);
4874        let indent_size = settings.indent_size;
4875        let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
4876
4877        let search_query = match &self.mode {
4878            ItemsDisplayMode::Search(search_query) => Some(search_query),
4879            _ => None,
4880        };
4881
4882        v_flex()
4883            .id("outline-panel")
4884            .size_full()
4885            .overflow_hidden()
4886            .relative()
4887            .on_hover(cx.listener(|this, hovered, window, cx| {
4888                if *hovered {
4889                    this.show_scrollbar = true;
4890                    this.hide_scrollbar_task.take();
4891                    cx.notify();
4892                } else if !this.focus_handle.contains_focused(window, cx) {
4893                    this.hide_scrollbar(window, cx);
4894                }
4895            }))
4896            .key_context(self.dispatch_context(window, cx))
4897            .on_action(cx.listener(Self::open))
4898            .on_action(cx.listener(Self::cancel))
4899            .on_action(cx.listener(Self::select_next))
4900            .on_action(cx.listener(Self::select_previous))
4901            .on_action(cx.listener(Self::select_first))
4902            .on_action(cx.listener(Self::select_last))
4903            .on_action(cx.listener(Self::select_parent))
4904            .on_action(cx.listener(Self::expand_selected_entry))
4905            .on_action(cx.listener(Self::collapse_selected_entry))
4906            .on_action(cx.listener(Self::expand_all_entries))
4907            .on_action(cx.listener(Self::collapse_all_entries))
4908            .on_action(cx.listener(Self::copy_path))
4909            .on_action(cx.listener(Self::copy_relative_path))
4910            .on_action(cx.listener(Self::toggle_active_editor_pin))
4911            .on_action(cx.listener(Self::unfold_directory))
4912            .on_action(cx.listener(Self::fold_directory))
4913            .on_action(cx.listener(Self::open_excerpts))
4914            .on_action(cx.listener(Self::open_excerpts_split))
4915            .when(is_local, |el| {
4916                el.on_action(cx.listener(Self::reveal_in_finder))
4917            })
4918            .when(is_local || is_via_ssh, |el| {
4919                el.on_action(cx.listener(Self::open_in_terminal))
4920            })
4921            .on_mouse_down(
4922                MouseButton::Right,
4923                cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| {
4924                    if let Some(entry) = outline_panel.selected_entry().cloned() {
4925                        outline_panel.deploy_context_menu(event.position, entry, window, cx)
4926                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
4927                        outline_panel.deploy_context_menu(
4928                            event.position,
4929                            PanelEntry::Fs(entry),
4930                            window,
4931                            cx,
4932                        )
4933                    }
4934                }),
4935            )
4936            .track_focus(&self.focus_handle)
4937            .when_some(search_query, |outline_panel, search_state| {
4938                outline_panel.child(
4939                    h_flex()
4940                        .py_1p5()
4941                        .px_2()
4942                        .h(DynamicSpacing::Base32.px(cx))
4943                        .flex_shrink_0()
4944                        .border_b_1()
4945                        .border_color(cx.theme().colors().border)
4946                        .gap_0p5()
4947                        .child(Label::new("Searching:").color(Color::Muted))
4948                        .child(Label::new(format!("'{}'", search_state.query))),
4949                )
4950            })
4951            .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx))
4952            .child(self.render_filter_footer(pinned, cx))
4953    }
4954}
4955
4956fn find_active_indent_guide_ix(
4957    outline_panel: &OutlinePanel,
4958    candidates: &[IndentGuideLayout],
4959) -> Option<usize> {
4960    let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
4961        return None;
4962    };
4963    let target_depth = outline_panel
4964        .cached_entries
4965        .get(*target_ix)
4966        .map(|cached_entry| cached_entry.depth)?;
4967
4968    let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
4969        .cached_entries
4970        .get(target_ix + 1)
4971        .filter(|cached_entry| cached_entry.depth > target_depth)
4972        .map(|entry| entry.depth)
4973    {
4974        (target_ix + 1, target_depth.saturating_sub(1))
4975    } else {
4976        (*target_ix, target_depth.saturating_sub(1))
4977    };
4978
4979    candidates
4980        .iter()
4981        .enumerate()
4982        .find(|(_, guide)| {
4983            guide.offset.y <= target_ix
4984                && target_ix < guide.offset.y + guide.length
4985                && guide.offset.x == target_depth
4986        })
4987        .map(|(ix, _)| ix)
4988}
4989
4990fn subscribe_for_editor_events(
4991    editor: &Entity<Editor>,
4992    window: &mut Window,
4993    cx: &mut Context<OutlinePanel>,
4994) -> Subscription {
4995    let debounce = Some(UPDATE_DEBOUNCE);
4996    cx.subscribe_in(
4997        editor,
4998        window,
4999        move |outline_panel, editor, e: &EditorEvent, window, cx| {
5000            if !outline_panel.active {
5001                return;
5002            }
5003            match e {
5004                EditorEvent::SelectionsChanged { local: true } => {
5005                    outline_panel.reveal_entry_for_selection(editor.clone(), window, cx);
5006                    cx.notify();
5007                }
5008                EditorEvent::ExcerptsAdded { excerpts, .. } => {
5009                    outline_panel
5010                        .new_entries_for_fs_update
5011                        .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
5012                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5013                }
5014                EditorEvent::ExcerptsRemoved { ids } => {
5015                    let mut ids = ids.iter().collect::<HashSet<_>>();
5016                    for excerpts in outline_panel.excerpts.values_mut() {
5017                        excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
5018                        if ids.is_empty() {
5019                            break;
5020                        }
5021                    }
5022                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5023                }
5024                EditorEvent::ExcerptsExpanded { ids } => {
5025                    outline_panel.invalidate_outlines(ids);
5026                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5027                    if update_cached_items {
5028                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5029                    }
5030                }
5031                EditorEvent::ExcerptsEdited { ids } => {
5032                    outline_panel.invalidate_outlines(ids);
5033                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5034                    if update_cached_items {
5035                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5036                    }
5037                }
5038                EditorEvent::BufferFoldToggled { ids, .. } => {
5039                    outline_panel.invalidate_outlines(ids);
5040                    let mut latest_unfolded_buffer_id = None;
5041                    let mut latest_folded_buffer_id = None;
5042                    let mut ignore_selections_change = false;
5043                    outline_panel.new_entries_for_fs_update.extend(
5044                        ids.iter()
5045                            .filter(|id| {
5046                                outline_panel
5047                                    .excerpts
5048                                    .iter()
5049                                    .find_map(|(buffer_id, excerpts)| {
5050                                        if excerpts.contains_key(id) {
5051                                            ignore_selections_change |= outline_panel
5052                                                .preserve_selection_on_buffer_fold_toggles
5053                                                .remove(buffer_id);
5054                                            Some(buffer_id)
5055                                        } else {
5056                                            None
5057                                        }
5058                                    })
5059                                    .map(|buffer_id| {
5060                                        if editor.read(cx).is_buffer_folded(*buffer_id, cx) {
5061                                            latest_folded_buffer_id = Some(*buffer_id);
5062                                            false
5063                                        } else {
5064                                            latest_unfolded_buffer_id = Some(*buffer_id);
5065                                            true
5066                                        }
5067                                    })
5068                                    .unwrap_or(true)
5069                            })
5070                            .copied(),
5071                    );
5072                    if !ignore_selections_change {
5073                        if let Some(entry_to_select) = latest_unfolded_buffer_id
5074                            .or(latest_folded_buffer_id)
5075                            .and_then(|toggled_buffer_id| {
5076                                outline_panel.fs_entries.iter().find_map(
5077                                    |fs_entry| match fs_entry {
5078                                        FsEntry::ExternalFile(external) => {
5079                                            if external.buffer_id == toggled_buffer_id {
5080                                                Some(fs_entry.clone())
5081                                            } else {
5082                                                None
5083                                            }
5084                                        }
5085                                        FsEntry::File(FsEntryFile { buffer_id, .. }) => {
5086                                            if *buffer_id == toggled_buffer_id {
5087                                                Some(fs_entry.clone())
5088                                            } else {
5089                                                None
5090                                            }
5091                                        }
5092                                        FsEntry::Directory(..) => None,
5093                                    },
5094                                )
5095                            })
5096                            .map(PanelEntry::Fs)
5097                        {
5098                            outline_panel.select_entry(entry_to_select, true, window, cx);
5099                        }
5100                    }
5101
5102                    outline_panel.update_fs_entries(editor.clone(), debounce, window, cx);
5103                }
5104                EditorEvent::Reparsed(buffer_id) => {
5105                    if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
5106                        for (_, excerpt) in excerpts {
5107                            excerpt.invalidate_outlines();
5108                        }
5109                    }
5110                    let update_cached_items = outline_panel.update_non_fs_items(window, cx);
5111                    if update_cached_items {
5112                        outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx);
5113                    }
5114                }
5115                _ => {}
5116            }
5117        },
5118    )
5119}
5120
5121fn empty_icon() -> AnyElement {
5122    h_flex()
5123        .size(IconSize::default().rems())
5124        .invisible()
5125        .flex_none()
5126        .into_any_element()
5127}
5128
5129fn horizontal_separator(cx: &mut App) -> Div {
5130    div().mx_2().border_primary(cx).border_t_1()
5131}
5132
5133#[derive(Debug, Default)]
5134struct GenerationState {
5135    entries: Vec<CachedEntry>,
5136    match_candidates: Vec<StringMatchCandidate>,
5137    max_width_estimate_and_index: Option<(u64, usize)>,
5138}
5139
5140impl GenerationState {
5141    fn clear(&mut self) {
5142        self.entries.clear();
5143        self.match_candidates.clear();
5144        self.max_width_estimate_and_index = None;
5145    }
5146}
5147
5148#[cfg(test)]
5149mod tests {
5150    use db::indoc;
5151    use gpui::{TestAppContext, VisualTestContext, WindowHandle};
5152    use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
5153    use pretty_assertions::assert_eq;
5154    use project::FakeFs;
5155    use search::project_search::{self, perform_project_search};
5156    use serde_json::json;
5157    use util::path;
5158    use workspace::{OpenOptions, OpenVisible};
5159
5160    use super::*;
5161
5162    const SELECTED_MARKER: &str = "  <==== selected";
5163
5164    #[gpui::test(iterations = 10)]
5165    async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
5166        init_test(cx);
5167
5168        let fs = FakeFs::new(cx.background_executor.clone());
5169        populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5170        let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5171        project.read_with(cx, |project, _| {
5172            project.languages().add(Arc::new(rust_lang()))
5173        });
5174        let workspace = add_outline_panel(&project, cx).await;
5175        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5176        let outline_panel = outline_panel(&workspace, cx);
5177        outline_panel.update_in(cx, |outline_panel, window, cx| {
5178            outline_panel.set_active(true, window, cx)
5179        });
5180
5181        workspace
5182            .update(cx, |workspace, window, cx| {
5183                ProjectSearchView::deploy_search(
5184                    workspace,
5185                    &workspace::DeploySearch::default(),
5186                    window,
5187                    cx,
5188                )
5189            })
5190            .unwrap();
5191        let search_view = workspace
5192            .update(cx, |workspace, _, cx| {
5193                workspace
5194                    .active_pane()
5195                    .read(cx)
5196                    .items()
5197                    .find_map(|item| item.downcast::<ProjectSearchView>())
5198                    .expect("Project search view expected to appear after new search event trigger")
5199            })
5200            .unwrap();
5201
5202        let query = "param_names_for_lifetime_elision_hints";
5203        perform_project_search(&search_view, query, cx);
5204        search_view.update(cx, |search_view, cx| {
5205            search_view
5206                .results_editor()
5207                .update(cx, |results_editor, cx| {
5208                    assert_eq!(
5209                        results_editor.display_text(cx).match_indices(query).count(),
5210                        9
5211                    );
5212                });
5213        });
5214
5215        let all_matches = r#"/rust-analyzer/
5216  crates/
5217    ide/src/
5218      inlay_hints/
5219        fn_lifetime_fn.rs
5220          search: match config.param_names_for_lifetime_elision_hints {
5221          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5222          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5223          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5224      inlay_hints.rs
5225        search: pub param_names_for_lifetime_elision_hints: bool,
5226        search: param_names_for_lifetime_elision_hints: self
5227      static_index.rs
5228        search: param_names_for_lifetime_elision_hints: false,
5229    rust-analyzer/src/
5230      cli/
5231        analysis_stats.rs
5232          search: param_names_for_lifetime_elision_hints: true,
5233      config.rs
5234        search: param_names_for_lifetime_elision_hints: self"#;
5235        let select_first_in_all_matches = |line_to_select: &str| {
5236            assert!(all_matches.contains(line_to_select));
5237            all_matches.replacen(
5238                line_to_select,
5239                &format!("{line_to_select}{SELECTED_MARKER}"),
5240                1,
5241            )
5242        };
5243
5244        cx.executor()
5245            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5246        cx.run_until_parked();
5247        outline_panel.update(cx, |outline_panel, cx| {
5248            assert_eq!(
5249                display_entries(
5250                    &project,
5251                    &snapshot(&outline_panel, cx),
5252                    &outline_panel.cached_entries,
5253                    outline_panel.selected_entry(),
5254                    cx,
5255                ),
5256                select_first_in_all_matches(
5257                    "search: match config.param_names_for_lifetime_elision_hints {"
5258                )
5259            );
5260        });
5261
5262        outline_panel.update_in(cx, |outline_panel, window, cx| {
5263            outline_panel.select_parent(&SelectParent, window, cx);
5264            assert_eq!(
5265                display_entries(
5266                    &project,
5267                    &snapshot(&outline_panel, cx),
5268                    &outline_panel.cached_entries,
5269                    outline_panel.selected_entry(),
5270                    cx,
5271                ),
5272                select_first_in_all_matches("fn_lifetime_fn.rs")
5273            );
5274        });
5275        outline_panel.update_in(cx, |outline_panel, window, cx| {
5276            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5277        });
5278        cx.executor()
5279            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5280        cx.run_until_parked();
5281        outline_panel.update(cx, |outline_panel, cx| {
5282            assert_eq!(
5283                display_entries(
5284                    &project,
5285                    &snapshot(&outline_panel, cx),
5286                    &outline_panel.cached_entries,
5287                    outline_panel.selected_entry(),
5288                    cx,
5289                ),
5290                format!(
5291                    r#"/rust-analyzer/
5292  crates/
5293    ide/src/
5294      inlay_hints/
5295        fn_lifetime_fn.rs{SELECTED_MARKER}
5296      inlay_hints.rs
5297        search: pub param_names_for_lifetime_elision_hints: bool,
5298        search: param_names_for_lifetime_elision_hints: self
5299      static_index.rs
5300        search: param_names_for_lifetime_elision_hints: false,
5301    rust-analyzer/src/
5302      cli/
5303        analysis_stats.rs
5304          search: param_names_for_lifetime_elision_hints: true,
5305      config.rs
5306        search: param_names_for_lifetime_elision_hints: self"#,
5307                )
5308            );
5309        });
5310
5311        outline_panel.update_in(cx, |outline_panel, window, cx| {
5312            outline_panel.expand_all_entries(&ExpandAllEntries, window, cx);
5313        });
5314        cx.executor()
5315            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5316        cx.run_until_parked();
5317        outline_panel.update_in(cx, |outline_panel, window, cx| {
5318            outline_panel.select_parent(&SelectParent, window, cx);
5319            assert_eq!(
5320                display_entries(
5321                    &project,
5322                    &snapshot(&outline_panel, cx),
5323                    &outline_panel.cached_entries,
5324                    outline_panel.selected_entry(),
5325                    cx,
5326                ),
5327                select_first_in_all_matches("inlay_hints/")
5328            );
5329        });
5330
5331        outline_panel.update_in(cx, |outline_panel, window, cx| {
5332            outline_panel.select_parent(&SelectParent, window, cx);
5333            assert_eq!(
5334                display_entries(
5335                    &project,
5336                    &snapshot(&outline_panel, cx),
5337                    &outline_panel.cached_entries,
5338                    outline_panel.selected_entry(),
5339                    cx,
5340                ),
5341                select_first_in_all_matches("ide/src/")
5342            );
5343        });
5344
5345        outline_panel.update_in(cx, |outline_panel, window, cx| {
5346            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
5347        });
5348        cx.executor()
5349            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5350        cx.run_until_parked();
5351        outline_panel.update(cx, |outline_panel, cx| {
5352            assert_eq!(
5353                display_entries(
5354                    &project,
5355                    &snapshot(&outline_panel, cx),
5356                    &outline_panel.cached_entries,
5357                    outline_panel.selected_entry(),
5358                    cx,
5359                ),
5360                format!(
5361                    r#"/rust-analyzer/
5362  crates/
5363    ide/src/{SELECTED_MARKER}
5364    rust-analyzer/src/
5365      cli/
5366        analysis_stats.rs
5367          search: param_names_for_lifetime_elision_hints: true,
5368      config.rs
5369        search: param_names_for_lifetime_elision_hints: self"#,
5370                )
5371            );
5372        });
5373        outline_panel.update_in(cx, |outline_panel, window, cx| {
5374            outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx);
5375        });
5376        cx.executor()
5377            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5378        cx.run_until_parked();
5379        outline_panel.update(cx, |outline_panel, cx| {
5380            assert_eq!(
5381                display_entries(
5382                    &project,
5383                    &snapshot(&outline_panel, cx),
5384                    &outline_panel.cached_entries,
5385                    outline_panel.selected_entry(),
5386                    cx,
5387                ),
5388                select_first_in_all_matches("ide/src/")
5389            );
5390        });
5391    }
5392
5393    #[gpui::test(iterations = 10)]
5394    async fn test_item_filtering(cx: &mut TestAppContext) {
5395        init_test(cx);
5396
5397        let fs = FakeFs::new(cx.background_executor.clone());
5398        populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5399        let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5400        project.read_with(cx, |project, _| {
5401            project.languages().add(Arc::new(rust_lang()))
5402        });
5403        let workspace = add_outline_panel(&project, cx).await;
5404        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5405        let outline_panel = outline_panel(&workspace, cx);
5406        outline_panel.update_in(cx, |outline_panel, window, cx| {
5407            outline_panel.set_active(true, window, cx)
5408        });
5409
5410        workspace
5411            .update(cx, |workspace, window, cx| {
5412                ProjectSearchView::deploy_search(
5413                    workspace,
5414                    &workspace::DeploySearch::default(),
5415                    window,
5416                    cx,
5417                )
5418            })
5419            .unwrap();
5420        let search_view = workspace
5421            .update(cx, |workspace, _, cx| {
5422                workspace
5423                    .active_pane()
5424                    .read(cx)
5425                    .items()
5426                    .find_map(|item| item.downcast::<ProjectSearchView>())
5427                    .expect("Project search view expected to appear after new search event trigger")
5428            })
5429            .unwrap();
5430
5431        let query = "param_names_for_lifetime_elision_hints";
5432        perform_project_search(&search_view, query, cx);
5433        search_view.update(cx, |search_view, cx| {
5434            search_view
5435                .results_editor()
5436                .update(cx, |results_editor, cx| {
5437                    assert_eq!(
5438                        results_editor.display_text(cx).match_indices(query).count(),
5439                        9
5440                    );
5441                });
5442        });
5443        let all_matches = r#"/rust-analyzer/
5444  crates/
5445    ide/src/
5446      inlay_hints/
5447        fn_lifetime_fn.rs
5448          search: match config.param_names_for_lifetime_elision_hints {
5449          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5450          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5451          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5452      inlay_hints.rs
5453        search: pub param_names_for_lifetime_elision_hints: bool,
5454        search: param_names_for_lifetime_elision_hints: self
5455      static_index.rs
5456        search: param_names_for_lifetime_elision_hints: false,
5457    rust-analyzer/src/
5458      cli/
5459        analysis_stats.rs
5460          search: param_names_for_lifetime_elision_hints: true,
5461      config.rs
5462        search: param_names_for_lifetime_elision_hints: self"#;
5463
5464        cx.executor()
5465            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5466        cx.run_until_parked();
5467        outline_panel.update(cx, |outline_panel, cx| {
5468            assert_eq!(
5469                display_entries(
5470                    &project,
5471                    &snapshot(&outline_panel, cx),
5472                    &outline_panel.cached_entries,
5473                    None,
5474                    cx,
5475                ),
5476                all_matches,
5477            );
5478        });
5479
5480        let filter_text = "a";
5481        outline_panel.update_in(cx, |outline_panel, window, cx| {
5482            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5483                filter_editor.set_text(filter_text, window, cx);
5484            });
5485        });
5486        cx.executor()
5487            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5488        cx.run_until_parked();
5489
5490        outline_panel.update(cx, |outline_panel, cx| {
5491            assert_eq!(
5492                display_entries(
5493                    &project,
5494                    &snapshot(&outline_panel, cx),
5495                    &outline_panel.cached_entries,
5496                    None,
5497                    cx,
5498                ),
5499                all_matches
5500                    .lines()
5501                    .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out
5502                    .filter(|item| item.contains(filter_text))
5503                    .collect::<Vec<_>>()
5504                    .join("\n"),
5505            );
5506        });
5507
5508        outline_panel.update_in(cx, |outline_panel, window, cx| {
5509            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5510                filter_editor.set_text("", window, cx);
5511            });
5512        });
5513        cx.executor()
5514            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5515        cx.run_until_parked();
5516        outline_panel.update(cx, |outline_panel, cx| {
5517            assert_eq!(
5518                display_entries(
5519                    &project,
5520                    &snapshot(&outline_panel, cx),
5521                    &outline_panel.cached_entries,
5522                    None,
5523                    cx,
5524                ),
5525                all_matches,
5526            );
5527        });
5528    }
5529
5530    #[gpui::test(iterations = 10)]
5531    async fn test_item_opening(cx: &mut TestAppContext) {
5532        init_test(cx);
5533
5534        let fs = FakeFs::new(cx.background_executor.clone());
5535        populate_with_test_ra_project(&fs, path!("/rust-analyzer")).await;
5536        let project = Project::test(fs.clone(), [path!("/rust-analyzer").as_ref()], cx).await;
5537        project.read_with(cx, |project, _| {
5538            project.languages().add(Arc::new(rust_lang()))
5539        });
5540        let workspace = add_outline_panel(&project, cx).await;
5541        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5542        let outline_panel = outline_panel(&workspace, cx);
5543        outline_panel.update_in(cx, |outline_panel, window, cx| {
5544            outline_panel.set_active(true, window, cx)
5545        });
5546
5547        workspace
5548            .update(cx, |workspace, window, cx| {
5549                ProjectSearchView::deploy_search(
5550                    workspace,
5551                    &workspace::DeploySearch::default(),
5552                    window,
5553                    cx,
5554                )
5555            })
5556            .unwrap();
5557        let search_view = workspace
5558            .update(cx, |workspace, _, cx| {
5559                workspace
5560                    .active_pane()
5561                    .read(cx)
5562                    .items()
5563                    .find_map(|item| item.downcast::<ProjectSearchView>())
5564                    .expect("Project search view expected to appear after new search event trigger")
5565            })
5566            .unwrap();
5567
5568        let query = "param_names_for_lifetime_elision_hints";
5569        perform_project_search(&search_view, query, cx);
5570        search_view.update(cx, |search_view, cx| {
5571            search_view
5572                .results_editor()
5573                .update(cx, |results_editor, cx| {
5574                    assert_eq!(
5575                        results_editor.display_text(cx).match_indices(query).count(),
5576                        9
5577                    );
5578                });
5579        });
5580        let root_path = format!("{}/", path!("/rust-analyzer"));
5581        let all_matches = format!(
5582            r#"{root_path}
5583  crates/
5584    ide/src/
5585      inlay_hints/
5586        fn_lifetime_fn.rs
5587          search: match config.param_names_for_lifetime_elision_hints {{
5588          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {{
5589          search: Some(it) if config.param_names_for_lifetime_elision_hints => {{
5590          search: InlayHintsConfig {{ param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }},
5591      inlay_hints.rs
5592        search: pub param_names_for_lifetime_elision_hints: bool,
5593        search: param_names_for_lifetime_elision_hints: self
5594      static_index.rs
5595        search: param_names_for_lifetime_elision_hints: false,
5596    rust-analyzer/src/
5597      cli/
5598        analysis_stats.rs
5599          search: param_names_for_lifetime_elision_hints: true,
5600      config.rs
5601        search: param_names_for_lifetime_elision_hints: self"#
5602        );
5603        let select_first_in_all_matches = |line_to_select: &str| {
5604            assert!(all_matches.contains(line_to_select));
5605            all_matches.replacen(
5606                line_to_select,
5607                &format!("{line_to_select}{SELECTED_MARKER}"),
5608                1,
5609            )
5610        };
5611        cx.executor()
5612            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5613        cx.run_until_parked();
5614
5615        let active_editor = outline_panel.update(cx, |outline_panel, _| {
5616            outline_panel
5617                .active_editor()
5618                .expect("should have an active editor open")
5619        });
5620        let initial_outline_selection =
5621            "search: match config.param_names_for_lifetime_elision_hints {";
5622        outline_panel.update_in(cx, |outline_panel, window, cx| {
5623            assert_eq!(
5624                display_entries(
5625                    &project,
5626                    &snapshot(&outline_panel, cx),
5627                    &outline_panel.cached_entries,
5628                    outline_panel.selected_entry(),
5629                    cx,
5630                ),
5631                select_first_in_all_matches(initial_outline_selection)
5632            );
5633            assert_eq!(
5634                selected_row_text(&active_editor, cx),
5635                initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5636                "Should place the initial editor selection on the corresponding search result"
5637            );
5638
5639            outline_panel.select_next(&SelectNext, window, cx);
5640            outline_panel.select_next(&SelectNext, window, cx);
5641        });
5642
5643        let navigated_outline_selection =
5644            "search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
5645        outline_panel.update(cx, |outline_panel, cx| {
5646            assert_eq!(
5647                display_entries(
5648                    &project,
5649                    &snapshot(&outline_panel, cx),
5650                    &outline_panel.cached_entries,
5651                    outline_panel.selected_entry(),
5652                    cx,
5653                ),
5654                select_first_in_all_matches(navigated_outline_selection)
5655            );
5656        });
5657        cx.executor()
5658            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5659        outline_panel.update(cx, |_, cx| {
5660            assert_eq!(
5661                selected_row_text(&active_editor, cx),
5662                navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5663                "Should still have the initial caret position after SelectNext calls"
5664            );
5665        });
5666
5667        outline_panel.update_in(cx, |outline_panel, window, cx| {
5668            outline_panel.open(&Open, window, cx);
5669        });
5670        outline_panel.update(cx, |_outline_panel, cx| {
5671            assert_eq!(
5672                selected_row_text(&active_editor, cx),
5673                navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5674                "After opening, should move the caret to the opened outline entry's position"
5675            );
5676        });
5677
5678        outline_panel.update_in(cx, |outline_panel, window, cx| {
5679            outline_panel.select_next(&SelectNext, window, cx);
5680        });
5681        let next_navigated_outline_selection =
5682            "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5683        outline_panel.update(cx, |outline_panel, cx| {
5684            assert_eq!(
5685                display_entries(
5686                    &project,
5687                    &snapshot(&outline_panel, cx),
5688                    &outline_panel.cached_entries,
5689                    outline_panel.selected_entry(),
5690                    cx,
5691                ),
5692                select_first_in_all_matches(next_navigated_outline_selection)
5693            );
5694        });
5695        cx.executor()
5696            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5697        outline_panel.update(cx, |_outline_panel, cx| {
5698            assert_eq!(
5699                selected_row_text(&active_editor, cx),
5700                next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5701                "Should again preserve the selection after another SelectNext call"
5702            );
5703        });
5704
5705        outline_panel.update_in(cx, |outline_panel, window, cx| {
5706            outline_panel.open_excerpts(&editor::OpenExcerpts, window, cx);
5707        });
5708        cx.executor()
5709            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5710        cx.run_until_parked();
5711        let new_active_editor = outline_panel.update(cx, |outline_panel, _| {
5712            outline_panel
5713                .active_editor()
5714                .expect("should have an active editor open")
5715        });
5716        outline_panel.update(cx, |outline_panel, cx| {
5717            assert_ne!(
5718                active_editor, new_active_editor,
5719                "After opening an excerpt, new editor should be open"
5720            );
5721            assert_eq!(
5722                display_entries(
5723                    &project,
5724                    &snapshot(&outline_panel, cx),
5725                    &outline_panel.cached_entries,
5726                    outline_panel.selected_entry(),
5727                    cx,
5728                ),
5729                "fn_lifetime_fn.rs  <==== selected"
5730            );
5731            assert_eq!(
5732                selected_row_text(&new_active_editor, cx),
5733                next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5734                "When opening the excerpt, should navigate to the place corresponding the outline entry"
5735            );
5736        });
5737    }
5738
5739    #[gpui::test]
5740    async fn test_multiple_workrees(cx: &mut TestAppContext) {
5741        init_test(cx);
5742
5743        let fs = FakeFs::new(cx.background_executor.clone());
5744        fs.insert_tree(
5745            "/root",
5746            json!({
5747                "one": {
5748                    "a.txt": "aaa aaa"
5749                },
5750                "two": {
5751                    "b.txt": "a aaa"
5752                }
5753
5754            }),
5755        )
5756        .await;
5757        let project = Project::test(fs.clone(), [Path::new("/root/one")], cx).await;
5758        let workspace = add_outline_panel(&project, cx).await;
5759        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5760        let outline_panel = outline_panel(&workspace, cx);
5761        outline_panel.update_in(cx, |outline_panel, window, cx| {
5762            outline_panel.set_active(true, window, cx)
5763        });
5764
5765        let items = workspace
5766            .update(cx, |workspace, window, cx| {
5767                workspace.open_paths(
5768                    vec![PathBuf::from("/root/two")],
5769                    OpenOptions {
5770                        visible: Some(OpenVisible::OnlyDirectories),
5771                        ..Default::default()
5772                    },
5773                    None,
5774                    window,
5775                    cx,
5776                )
5777            })
5778            .unwrap()
5779            .await;
5780        assert_eq!(items.len(), 1, "Were opening another worktree directory");
5781        assert!(
5782            items[0].is_none(),
5783            "Directory should be opened successfully"
5784        );
5785
5786        workspace
5787            .update(cx, |workspace, window, cx| {
5788                ProjectSearchView::deploy_search(
5789                    workspace,
5790                    &workspace::DeploySearch::default(),
5791                    window,
5792                    cx,
5793                )
5794            })
5795            .unwrap();
5796        let search_view = workspace
5797            .update(cx, |workspace, _, cx| {
5798                workspace
5799                    .active_pane()
5800                    .read(cx)
5801                    .items()
5802                    .find_map(|item| item.downcast::<ProjectSearchView>())
5803                    .expect("Project search view expected to appear after new search event trigger")
5804            })
5805            .unwrap();
5806
5807        let query = "aaa";
5808        perform_project_search(&search_view, query, cx);
5809        search_view.update(cx, |search_view, cx| {
5810            search_view
5811                .results_editor()
5812                .update(cx, |results_editor, cx| {
5813                    assert_eq!(
5814                        results_editor.display_text(cx).match_indices(query).count(),
5815                        3
5816                    );
5817                });
5818        });
5819
5820        cx.executor()
5821            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5822        cx.run_until_parked();
5823        outline_panel.update(cx, |outline_panel, cx| {
5824            assert_eq!(
5825                display_entries(
5826                    &project,
5827                    &snapshot(&outline_panel, cx),
5828                    &outline_panel.cached_entries,
5829                    outline_panel.selected_entry(),
5830                    cx,
5831                ),
5832                r#"/root/one/
5833  a.txt
5834    search: aaa aaa  <==== selected
5835    search: aaa aaa
5836/root/two/
5837  b.txt
5838    search: a aaa"#
5839            );
5840        });
5841
5842        outline_panel.update_in(cx, |outline_panel, window, cx| {
5843            outline_panel.select_previous(&SelectPrevious, window, cx);
5844            outline_panel.open(&Open, window, cx);
5845        });
5846        cx.executor()
5847            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5848        cx.run_until_parked();
5849        outline_panel.update(cx, |outline_panel, cx| {
5850            assert_eq!(
5851                display_entries(
5852                    &project,
5853                    &snapshot(&outline_panel, cx),
5854                    &outline_panel.cached_entries,
5855                    outline_panel.selected_entry(),
5856                    cx,
5857                ),
5858                r#"/root/one/
5859  a.txt  <==== selected
5860/root/two/
5861  b.txt
5862    search: a aaa"#
5863            );
5864        });
5865
5866        outline_panel.update_in(cx, |outline_panel, window, cx| {
5867            outline_panel.select_next(&SelectNext, window, cx);
5868            outline_panel.open(&Open, window, cx);
5869        });
5870        cx.executor()
5871            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5872        cx.run_until_parked();
5873        outline_panel.update(cx, |outline_panel, cx| {
5874            assert_eq!(
5875                display_entries(
5876                    &project,
5877                    &snapshot(&outline_panel, cx),
5878                    &outline_panel.cached_entries,
5879                    outline_panel.selected_entry(),
5880                    cx,
5881                ),
5882                r#"/root/one/
5883  a.txt
5884/root/two/  <==== selected"#
5885            );
5886        });
5887
5888        outline_panel.update_in(cx, |outline_panel, window, cx| {
5889            outline_panel.open(&Open, window, cx);
5890        });
5891        cx.executor()
5892            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5893        cx.run_until_parked();
5894        outline_panel.update(cx, |outline_panel, cx| {
5895            assert_eq!(
5896                display_entries(
5897                    &project,
5898                    &snapshot(&outline_panel, cx),
5899                    &outline_panel.cached_entries,
5900                    outline_panel.selected_entry(),
5901                    cx,
5902                ),
5903                r#"/root/one/
5904  a.txt
5905/root/two/  <==== selected
5906  b.txt
5907    search: a aaa"#
5908            );
5909        });
5910    }
5911
5912    #[gpui::test]
5913    async fn test_navigating_in_singleton(cx: &mut TestAppContext) {
5914        init_test(cx);
5915
5916        let root = path!("/root");
5917        let fs = FakeFs::new(cx.background_executor.clone());
5918        fs.insert_tree(
5919            root,
5920            json!({
5921                "src": {
5922                    "lib.rs": indoc!("
5923#[derive(Clone, Debug, PartialEq, Eq, Hash)]
5924struct OutlineEntryExcerpt {
5925    id: ExcerptId,
5926    buffer_id: BufferId,
5927    range: ExcerptRange<language::Anchor>,
5928}"),
5929                }
5930            }),
5931        )
5932        .await;
5933        let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
5934        project.read_with(cx, |project, _| {
5935            project.languages().add(Arc::new(
5936                rust_lang()
5937                    .with_outline_query(
5938                        r#"
5939                (struct_item
5940                    (visibility_modifier)? @context
5941                    "struct" @context
5942                    name: (_) @name) @item
5943
5944                (field_declaration
5945                    (visibility_modifier)? @context
5946                    name: (_) @name) @item
5947"#,
5948                    )
5949                    .unwrap(),
5950            ))
5951        });
5952        let workspace = add_outline_panel(&project, cx).await;
5953        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5954        let outline_panel = outline_panel(&workspace, cx);
5955        cx.update(|window, cx| {
5956            outline_panel.update(cx, |outline_panel, cx| {
5957                outline_panel.set_active(true, window, cx)
5958            });
5959        });
5960
5961        let _editor = workspace
5962            .update(cx, |workspace, window, cx| {
5963                workspace.open_abs_path(
5964                    PathBuf::from(path!("/root/src/lib.rs")),
5965                    OpenOptions {
5966                        visible: Some(OpenVisible::All),
5967                        ..Default::default()
5968                    },
5969                    window,
5970                    cx,
5971                )
5972            })
5973            .unwrap()
5974            .await
5975            .expect("Failed to open Rust source file")
5976            .downcast::<Editor>()
5977            .expect("Should open an editor for Rust source file");
5978
5979        cx.executor()
5980            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5981        cx.run_until_parked();
5982        outline_panel.update(cx, |outline_panel, cx| {
5983            assert_eq!(
5984                display_entries(
5985                    &project,
5986                    &snapshot(&outline_panel, cx),
5987                    &outline_panel.cached_entries,
5988                    outline_panel.selected_entry(),
5989                    cx,
5990                ),
5991                indoc!(
5992                    "
5993outline: struct OutlineEntryExcerpt
5994  outline: id
5995  outline: buffer_id
5996  outline: range"
5997                )
5998            );
5999        });
6000
6001        cx.update(|window, cx| {
6002            outline_panel.update(cx, |outline_panel, cx| {
6003                outline_panel.select_next(&SelectNext, window, cx);
6004            });
6005        });
6006        cx.executor()
6007            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6008        cx.run_until_parked();
6009        outline_panel.update(cx, |outline_panel, cx| {
6010            assert_eq!(
6011                display_entries(
6012                    &project,
6013                    &snapshot(&outline_panel, cx),
6014                    &outline_panel.cached_entries,
6015                    outline_panel.selected_entry(),
6016                    cx,
6017                ),
6018                indoc!(
6019                    "
6020outline: struct OutlineEntryExcerpt  <==== selected
6021  outline: id
6022  outline: buffer_id
6023  outline: range"
6024                )
6025            );
6026        });
6027
6028        cx.update(|window, cx| {
6029            outline_panel.update(cx, |outline_panel, cx| {
6030                outline_panel.select_next(&SelectNext, window, cx);
6031            });
6032        });
6033        cx.executor()
6034            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6035        cx.run_until_parked();
6036        outline_panel.update(cx, |outline_panel, cx| {
6037            assert_eq!(
6038                display_entries(
6039                    &project,
6040                    &snapshot(&outline_panel, cx),
6041                    &outline_panel.cached_entries,
6042                    outline_panel.selected_entry(),
6043                    cx,
6044                ),
6045                indoc!(
6046                    "
6047outline: struct OutlineEntryExcerpt
6048  outline: id  <==== selected
6049  outline: buffer_id
6050  outline: range"
6051                )
6052            );
6053        });
6054
6055        cx.update(|window, cx| {
6056            outline_panel.update(cx, |outline_panel, cx| {
6057                outline_panel.select_next(&SelectNext, window, cx);
6058            });
6059        });
6060        cx.executor()
6061            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6062        cx.run_until_parked();
6063        outline_panel.update(cx, |outline_panel, cx| {
6064            assert_eq!(
6065                display_entries(
6066                    &project,
6067                    &snapshot(&outline_panel, cx),
6068                    &outline_panel.cached_entries,
6069                    outline_panel.selected_entry(),
6070                    cx,
6071                ),
6072                indoc!(
6073                    "
6074outline: struct OutlineEntryExcerpt
6075  outline: id
6076  outline: buffer_id  <==== selected
6077  outline: range"
6078                )
6079            );
6080        });
6081
6082        cx.update(|window, cx| {
6083            outline_panel.update(cx, |outline_panel, cx| {
6084                outline_panel.select_next(&SelectNext, window, cx);
6085            });
6086        });
6087        cx.executor()
6088            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6089        cx.run_until_parked();
6090        outline_panel.update(cx, |outline_panel, cx| {
6091            assert_eq!(
6092                display_entries(
6093                    &project,
6094                    &snapshot(&outline_panel, cx),
6095                    &outline_panel.cached_entries,
6096                    outline_panel.selected_entry(),
6097                    cx,
6098                ),
6099                indoc!(
6100                    "
6101outline: struct OutlineEntryExcerpt
6102  outline: id
6103  outline: buffer_id
6104  outline: range  <==== selected"
6105                )
6106            );
6107        });
6108
6109        cx.update(|window, cx| {
6110            outline_panel.update(cx, |outline_panel, cx| {
6111                outline_panel.select_next(&SelectNext, window, cx);
6112            });
6113        });
6114        cx.executor()
6115            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6116        cx.run_until_parked();
6117        outline_panel.update(cx, |outline_panel, cx| {
6118            assert_eq!(
6119                display_entries(
6120                    &project,
6121                    &snapshot(&outline_panel, cx),
6122                    &outline_panel.cached_entries,
6123                    outline_panel.selected_entry(),
6124                    cx,
6125                ),
6126                indoc!(
6127                    "
6128outline: struct OutlineEntryExcerpt  <==== selected
6129  outline: id
6130  outline: buffer_id
6131  outline: range"
6132                )
6133            );
6134        });
6135
6136        cx.update(|window, cx| {
6137            outline_panel.update(cx, |outline_panel, cx| {
6138                outline_panel.select_previous(&SelectPrevious, window, cx);
6139            });
6140        });
6141        cx.executor()
6142            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6143        cx.run_until_parked();
6144        outline_panel.update(cx, |outline_panel, cx| {
6145            assert_eq!(
6146                display_entries(
6147                    &project,
6148                    &snapshot(&outline_panel, cx),
6149                    &outline_panel.cached_entries,
6150                    outline_panel.selected_entry(),
6151                    cx,
6152                ),
6153                indoc!(
6154                    "
6155outline: struct OutlineEntryExcerpt
6156  outline: id
6157  outline: buffer_id
6158  outline: range  <==== selected"
6159                )
6160            );
6161        });
6162
6163        cx.update(|window, cx| {
6164            outline_panel.update(cx, |outline_panel, cx| {
6165                outline_panel.select_previous(&SelectPrevious, window, cx);
6166            });
6167        });
6168        cx.executor()
6169            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6170        cx.run_until_parked();
6171        outline_panel.update(cx, |outline_panel, cx| {
6172            assert_eq!(
6173                display_entries(
6174                    &project,
6175                    &snapshot(&outline_panel, cx),
6176                    &outline_panel.cached_entries,
6177                    outline_panel.selected_entry(),
6178                    cx,
6179                ),
6180                indoc!(
6181                    "
6182outline: struct OutlineEntryExcerpt
6183  outline: id
6184  outline: buffer_id  <==== selected
6185  outline: range"
6186                )
6187            );
6188        });
6189
6190        cx.update(|window, cx| {
6191            outline_panel.update(cx, |outline_panel, cx| {
6192                outline_panel.select_previous(&SelectPrevious, window, cx);
6193            });
6194        });
6195        cx.executor()
6196            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6197        cx.run_until_parked();
6198        outline_panel.update(cx, |outline_panel, cx| {
6199            assert_eq!(
6200                display_entries(
6201                    &project,
6202                    &snapshot(&outline_panel, cx),
6203                    &outline_panel.cached_entries,
6204                    outline_panel.selected_entry(),
6205                    cx,
6206                ),
6207                indoc!(
6208                    "
6209outline: struct OutlineEntryExcerpt
6210  outline: id  <==== selected
6211  outline: buffer_id
6212  outline: range"
6213                )
6214            );
6215        });
6216
6217        cx.update(|window, cx| {
6218            outline_panel.update(cx, |outline_panel, cx| {
6219                outline_panel.select_previous(&SelectPrevious, window, cx);
6220            });
6221        });
6222        cx.executor()
6223            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6224        cx.run_until_parked();
6225        outline_panel.update(cx, |outline_panel, cx| {
6226            assert_eq!(
6227                display_entries(
6228                    &project,
6229                    &snapshot(&outline_panel, cx),
6230                    &outline_panel.cached_entries,
6231                    outline_panel.selected_entry(),
6232                    cx,
6233                ),
6234                indoc!(
6235                    "
6236outline: struct OutlineEntryExcerpt  <==== selected
6237  outline: id
6238  outline: buffer_id
6239  outline: range"
6240                )
6241            );
6242        });
6243
6244        cx.update(|window, cx| {
6245            outline_panel.update(cx, |outline_panel, cx| {
6246                outline_panel.select_previous(&SelectPrevious, window, cx);
6247            });
6248        });
6249        cx.executor()
6250            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6251        cx.run_until_parked();
6252        outline_panel.update(cx, |outline_panel, cx| {
6253            assert_eq!(
6254                display_entries(
6255                    &project,
6256                    &snapshot(&outline_panel, cx),
6257                    &outline_panel.cached_entries,
6258                    outline_panel.selected_entry(),
6259                    cx,
6260                ),
6261                indoc!(
6262                    "
6263outline: struct OutlineEntryExcerpt
6264  outline: id
6265  outline: buffer_id
6266  outline: range  <==== selected"
6267                )
6268            );
6269        });
6270    }
6271
6272    #[gpui::test(iterations = 10)]
6273    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
6274        init_test(cx);
6275
6276        let root = "/frontend-project";
6277        let fs = FakeFs::new(cx.background_executor.clone());
6278        fs.insert_tree(
6279            root,
6280            json!({
6281                "public": {
6282                    "lottie": {
6283                        "syntax-tree.json": r#"{ "something": "static" }"#
6284                    }
6285                },
6286                "src": {
6287                    "app": {
6288                        "(site)": {
6289                            "(about)": {
6290                                "jobs": {
6291                                    "[slug]": {
6292                                        "page.tsx": r#"static"#
6293                                    }
6294                                }
6295                            },
6296                            "(blog)": {
6297                                "post": {
6298                                    "[slug]": {
6299                                        "page.tsx": r#"static"#
6300                                    }
6301                                }
6302                            },
6303                        }
6304                    },
6305                    "components": {
6306                        "ErrorBoundary.tsx": r#"static"#,
6307                    }
6308                }
6309
6310            }),
6311        )
6312        .await;
6313        let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
6314        let workspace = add_outline_panel(&project, cx).await;
6315        let cx = &mut VisualTestContext::from_window(*workspace, cx);
6316        let outline_panel = outline_panel(&workspace, cx);
6317        outline_panel.update_in(cx, |outline_panel, window, cx| {
6318            outline_panel.set_active(true, window, cx)
6319        });
6320
6321        workspace
6322            .update(cx, |workspace, window, cx| {
6323                ProjectSearchView::deploy_search(
6324                    workspace,
6325                    &workspace::DeploySearch::default(),
6326                    window,
6327                    cx,
6328                )
6329            })
6330            .unwrap();
6331        let search_view = workspace
6332            .update(cx, |workspace, _, cx| {
6333                workspace
6334                    .active_pane()
6335                    .read(cx)
6336                    .items()
6337                    .find_map(|item| item.downcast::<ProjectSearchView>())
6338                    .expect("Project search view expected to appear after new search event trigger")
6339            })
6340            .unwrap();
6341
6342        let query = "static";
6343        perform_project_search(&search_view, query, cx);
6344        search_view.update(cx, |search_view, cx| {
6345            search_view
6346                .results_editor()
6347                .update(cx, |results_editor, cx| {
6348                    assert_eq!(
6349                        results_editor.display_text(cx).match_indices(query).count(),
6350                        4
6351                    );
6352                });
6353        });
6354
6355        cx.executor()
6356            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6357        cx.run_until_parked();
6358        outline_panel.update(cx, |outline_panel, cx| {
6359            assert_eq!(
6360                display_entries(
6361                    &project,
6362                    &snapshot(&outline_panel, cx),
6363                    &outline_panel.cached_entries,
6364                    outline_panel.selected_entry(),
6365                    cx,
6366                ),
6367                r#"/frontend-project/
6368  public/lottie/
6369    syntax-tree.json
6370      search: { "something": "static" }  <==== selected
6371  src/
6372    app/(site)/
6373      (about)/jobs/[slug]/
6374        page.tsx
6375          search: static
6376      (blog)/post/[slug]/
6377        page.tsx
6378          search: static
6379    components/
6380      ErrorBoundary.tsx
6381        search: static"#
6382            );
6383        });
6384
6385        outline_panel.update_in(cx, |outline_panel, window, cx| {
6386            // Move to 5th element in the list, 3 items down.
6387            for _ in 0..2 {
6388                outline_panel.select_next(&SelectNext, window, cx);
6389            }
6390            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx);
6391        });
6392        cx.executor()
6393            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6394        cx.run_until_parked();
6395        outline_panel.update(cx, |outline_panel, cx| {
6396            assert_eq!(
6397                display_entries(
6398                    &project,
6399                    &snapshot(&outline_panel, cx),
6400                    &outline_panel.cached_entries,
6401                    outline_panel.selected_entry(),
6402                    cx,
6403                ),
6404                r#"/frontend-project/
6405  public/lottie/
6406    syntax-tree.json
6407      search: { "something": "static" }
6408  src/
6409    app/(site)/  <==== selected
6410    components/
6411      ErrorBoundary.tsx
6412        search: static"#
6413            );
6414        });
6415
6416        outline_panel.update_in(cx, |outline_panel, window, cx| {
6417            // Move to the next visible non-FS entry
6418            for _ in 0..3 {
6419                outline_panel.select_next(&SelectNext, window, cx);
6420            }
6421        });
6422        cx.run_until_parked();
6423        outline_panel.update(cx, |outline_panel, cx| {
6424            assert_eq!(
6425                display_entries(
6426                    &project,
6427                    &snapshot(&outline_panel, cx),
6428                    &outline_panel.cached_entries,
6429                    outline_panel.selected_entry(),
6430                    cx,
6431                ),
6432                r#"/frontend-project/
6433  public/lottie/
6434    syntax-tree.json
6435      search: { "something": "static" }
6436  src/
6437    app/(site)/
6438    components/
6439      ErrorBoundary.tsx
6440        search: static  <==== selected"#
6441            );
6442        });
6443
6444        outline_panel.update_in(cx, |outline_panel, window, cx| {
6445            outline_panel
6446                .active_editor()
6447                .expect("Should have an active editor")
6448                .update(cx, |editor, cx| {
6449                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6450                });
6451        });
6452        cx.executor()
6453            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6454        cx.run_until_parked();
6455        outline_panel.update(cx, |outline_panel, cx| {
6456            assert_eq!(
6457                display_entries(
6458                    &project,
6459                    &snapshot(&outline_panel, cx),
6460                    &outline_panel.cached_entries,
6461                    outline_panel.selected_entry(),
6462                    cx,
6463                ),
6464                r#"/frontend-project/
6465  public/lottie/
6466    syntax-tree.json
6467      search: { "something": "static" }
6468  src/
6469    app/(site)/
6470    components/
6471      ErrorBoundary.tsx  <==== selected"#
6472            );
6473        });
6474
6475        outline_panel.update_in(cx, |outline_panel, window, cx| {
6476            outline_panel
6477                .active_editor()
6478                .expect("Should have an active editor")
6479                .update(cx, |editor, cx| {
6480                    editor.toggle_fold(&editor::actions::ToggleFold, window, cx)
6481                });
6482        });
6483        cx.executor()
6484            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
6485        cx.run_until_parked();
6486        outline_panel.update(cx, |outline_panel, cx| {
6487            assert_eq!(
6488                display_entries(
6489                    &project,
6490                    &snapshot(&outline_panel, cx),
6491                    &outline_panel.cached_entries,
6492                    outline_panel.selected_entry(),
6493                    cx,
6494                ),
6495                r#"/frontend-project/
6496  public/lottie/
6497    syntax-tree.json
6498      search: { "something": "static" }
6499  src/
6500    app/(site)/
6501    components/
6502      ErrorBoundary.tsx  <==== selected
6503        search: static"#
6504            );
6505        });
6506    }
6507
6508    async fn add_outline_panel(
6509        project: &Entity<Project>,
6510        cx: &mut TestAppContext,
6511    ) -> WindowHandle<Workspace> {
6512        let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
6513
6514        let outline_panel = window
6515            .update(cx, |_, window, cx| cx.spawn_in(window, OutlinePanel::load))
6516            .unwrap()
6517            .await
6518            .expect("Failed to load outline panel");
6519
6520        window
6521            .update(cx, |workspace, window, cx| {
6522                workspace.add_panel(outline_panel, window, cx);
6523            })
6524            .unwrap();
6525        window
6526    }
6527
6528    fn outline_panel(
6529        workspace: &WindowHandle<Workspace>,
6530        cx: &mut TestAppContext,
6531    ) -> Entity<OutlinePanel> {
6532        workspace
6533            .update(cx, |workspace, _, cx| {
6534                workspace
6535                    .panel::<OutlinePanel>(cx)
6536                    .expect("no outline panel")
6537            })
6538            .unwrap()
6539    }
6540
6541    fn display_entries(
6542        project: &Entity<Project>,
6543        multi_buffer_snapshot: &MultiBufferSnapshot,
6544        cached_entries: &[CachedEntry],
6545        selected_entry: Option<&PanelEntry>,
6546        cx: &mut App,
6547    ) -> String {
6548        let mut display_string = String::new();
6549        for entry in cached_entries {
6550            if !display_string.is_empty() {
6551                display_string += "\n";
6552            }
6553            for _ in 0..entry.depth {
6554                display_string += "  ";
6555            }
6556            display_string += &match &entry.entry {
6557                PanelEntry::Fs(entry) => match entry {
6558                    FsEntry::ExternalFile(_) => {
6559                        panic!("Did not cover external files with tests")
6560                    }
6561                    FsEntry::Directory(directory) => {
6562                        match project
6563                            .read(cx)
6564                            .worktree_for_id(directory.worktree_id, cx)
6565                            .and_then(|worktree| {
6566                                if worktree.read(cx).root_entry() == Some(&directory.entry.entry) {
6567                                    Some(worktree.read(cx).abs_path())
6568                                } else {
6569                                    None
6570                                }
6571                            }) {
6572                            Some(root_path) => format!(
6573                                "{}/{}",
6574                                root_path.display(),
6575                                directory.entry.path.display(),
6576                            ),
6577                            None => format!(
6578                                "{}/",
6579                                directory
6580                                    .entry
6581                                    .path
6582                                    .file_name()
6583                                    .unwrap_or_default()
6584                                    .to_string_lossy()
6585                            ),
6586                        }
6587                    }
6588                    FsEntry::File(file) => file
6589                        .entry
6590                        .path
6591                        .file_name()
6592                        .map(|name| name.to_string_lossy().to_string())
6593                        .unwrap_or_default(),
6594                },
6595                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
6596                    .entries
6597                    .iter()
6598                    .filter_map(|dir| dir.path.file_name())
6599                    .map(|name| name.to_string_lossy().to_string() + "/")
6600                    .collect(),
6601                PanelEntry::Outline(outline_entry) => match outline_entry {
6602                    OutlineEntry::Excerpt(_) => continue,
6603                    OutlineEntry::Outline(outline_entry) => {
6604                        format!("outline: {}", outline_entry.outline.text)
6605                    }
6606                },
6607                PanelEntry::Search(search_entry) => {
6608                    format!(
6609                        "search: {}",
6610                        search_entry
6611                            .render_data
6612                            .get_or_init(|| SearchData::new(
6613                                &search_entry.match_range,
6614                                &multi_buffer_snapshot
6615                            ))
6616                            .context_text
6617                    )
6618                }
6619            };
6620
6621            if Some(&entry.entry) == selected_entry {
6622                display_string += SELECTED_MARKER;
6623            }
6624        }
6625        display_string
6626    }
6627
6628    fn init_test(cx: &mut TestAppContext) {
6629        cx.update(|cx| {
6630            let settings = SettingsStore::test(cx);
6631            cx.set_global(settings);
6632
6633            theme::init(theme::LoadThemes::JustBase, cx);
6634
6635            language::init(cx);
6636            editor::init(cx);
6637            workspace::init_settings(cx);
6638            Project::init_settings(cx);
6639            project_search::init(cx);
6640            super::init(cx);
6641        });
6642    }
6643
6644    // Based on https://github.com/rust-lang/rust-analyzer/
6645    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
6646        fs.insert_tree(
6647            root,
6648            json!({
6649                    "crates": {
6650                        "ide": {
6651                            "src": {
6652                                "inlay_hints": {
6653                                    "fn_lifetime_fn.rs": r##"
6654        pub(super) fn hints(
6655            acc: &mut Vec<InlayHint>,
6656            config: &InlayHintsConfig,
6657            func: ast::Fn,
6658        ) -> Option<()> {
6659            // ... snip
6660
6661            let mut used_names: FxHashMap<SmolStr, usize> =
6662                match config.param_names_for_lifetime_elision_hints {
6663                    true => generic_param_list
6664                        .iter()
6665                        .flat_map(|gpl| gpl.lifetime_params())
6666                        .filter_map(|param| param.lifetime())
6667                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
6668                        .collect(),
6669                    false => Default::default(),
6670                };
6671            {
6672                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
6673                if self_param.is_some() && potential_lt_refs.next().is_some() {
6674                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
6675                        // self can't be used as a lifetime, so no need to check for collisions
6676                        "'self".into()
6677                    } else {
6678                        gen_idx_name()
6679                    });
6680                }
6681                potential_lt_refs.for_each(|(name, ..)| {
6682                    let name = match name {
6683                        Some(it) if config.param_names_for_lifetime_elision_hints => {
6684                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
6685                                *c += 1;
6686                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
6687                            } else {
6688                                used_names.insert(it.text().as_str().into(), 0);
6689                                SmolStr::from_iter(["\'", it.text().as_str()])
6690                            }
6691                        }
6692                        _ => gen_idx_name(),
6693                    };
6694                    allocated_lifetimes.push(name);
6695                });
6696            }
6697
6698            // ... snip
6699        }
6700
6701        // ... snip
6702
6703            #[test]
6704            fn hints_lifetimes_named() {
6705                check_with_config(
6706                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
6707                    r#"
6708        fn nested_in<'named>(named: &        &X<      &()>) {}
6709        //          ^'named1, 'named2, 'named3, $
6710                                  //^'named1 ^'named2 ^'named3
6711        "#,
6712                );
6713            }
6714
6715        // ... snip
6716        "##,
6717                                },
6718                        "inlay_hints.rs": r#"
6719    #[derive(Clone, Debug, PartialEq, Eq)]
6720    pub struct InlayHintsConfig {
6721        // ... snip
6722        pub param_names_for_lifetime_elision_hints: bool,
6723        pub max_length: Option<usize>,
6724        // ... snip
6725    }
6726
6727    impl Config {
6728        pub fn inlay_hints(&self) -> InlayHintsConfig {
6729            InlayHintsConfig {
6730                // ... snip
6731                param_names_for_lifetime_elision_hints: self
6732                    .inlayHints_lifetimeElisionHints_useParameterNames()
6733                    .to_owned(),
6734                max_length: self.inlayHints_maxLength().to_owned(),
6735                // ... snip
6736            }
6737        }
6738    }
6739    "#,
6740                        "static_index.rs": r#"
6741// ... snip
6742        fn add_file(&mut self, file_id: FileId) {
6743            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
6744            let folds = self.analysis.folding_ranges(file_id).unwrap();
6745            let inlay_hints = self
6746                .analysis
6747                .inlay_hints(
6748                    &InlayHintsConfig {
6749                        // ... snip
6750                        closure_style: hir::ClosureStyle::ImplFn,
6751                        param_names_for_lifetime_elision_hints: false,
6752                        binding_mode_hints: false,
6753                        max_length: Some(25),
6754                        closure_capture_hints: false,
6755                        // ... snip
6756                    },
6757                    file_id,
6758                    None,
6759                )
6760                .unwrap();
6761            // ... snip
6762    }
6763// ... snip
6764    "#
6765                            }
6766                        },
6767                        "rust-analyzer": {
6768                            "src": {
6769                                "cli": {
6770                                    "analysis_stats.rs": r#"
6771        // ... snip
6772                for &file_id in &file_ids {
6773                    _ = analysis.inlay_hints(
6774                        &InlayHintsConfig {
6775                            // ... snip
6776                            implicit_drop_hints: true,
6777                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
6778                            param_names_for_lifetime_elision_hints: true,
6779                            hide_named_constructor_hints: false,
6780                            hide_closure_initialization_hints: false,
6781                            closure_style: hir::ClosureStyle::ImplFn,
6782                            max_length: Some(25),
6783                            closing_brace_hints_min_lines: Some(20),
6784                            fields_to_resolve: InlayFieldsToResolve::empty(),
6785                            range_exclusive_hints: true,
6786                        },
6787                        file_id.into(),
6788                        None,
6789                    );
6790                }
6791        // ... snip
6792                                    "#,
6793                                },
6794                                "config.rs": r#"
6795                config_data! {
6796                    /// Configs that only make sense when they are set by a client. As such they can only be defined
6797                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
6798                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
6799                        // ... snip
6800                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
6801                        inlayHints_maxLength: Option<usize>                        = Some(25),
6802                        // ... snip
6803                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
6804                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
6805                        // ... snip
6806                    }
6807                }
6808
6809                impl Config {
6810                    // ... snip
6811                    pub fn inlay_hints(&self) -> InlayHintsConfig {
6812                        InlayHintsConfig {
6813                            // ... snip
6814                            param_names_for_lifetime_elision_hints: self
6815                                .inlayHints_lifetimeElisionHints_useParameterNames()
6816                                .to_owned(),
6817                            max_length: self.inlayHints_maxLength().to_owned(),
6818                            // ... snip
6819                        }
6820                    }
6821                    // ... snip
6822                }
6823                "#
6824                                }
6825                        }
6826                    }
6827            }),
6828        )
6829        .await;
6830    }
6831
6832    fn rust_lang() -> Language {
6833        Language::new(
6834            LanguageConfig {
6835                name: "Rust".into(),
6836                matcher: LanguageMatcher {
6837                    path_suffixes: vec!["rs".to_string()],
6838                    ..Default::default()
6839                },
6840                ..Default::default()
6841            },
6842            Some(tree_sitter_rust::LANGUAGE.into()),
6843        )
6844        .with_highlights_query(
6845            r#"
6846                (field_identifier) @field
6847                (struct_expression) @struct
6848            "#,
6849        )
6850        .unwrap()
6851        .with_injection_query(
6852            r#"
6853                (macro_invocation
6854                    (token_tree) @injection.content
6855                    (#set! injection.language "rust"))
6856            "#,
6857        )
6858        .unwrap()
6859    }
6860
6861    fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot {
6862        outline_panel
6863            .active_editor()
6864            .unwrap()
6865            .read(cx)
6866            .buffer()
6867            .read(cx)
6868            .snapshot(cx)
6869    }
6870
6871    fn selected_row_text(editor: &Entity<Editor>, cx: &mut App) -> String {
6872        editor.update(cx, |editor, cx| {
6873                let selections = editor.selections.all::<language::Point>(cx);
6874                assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
6875                let selection = selections.first().unwrap();
6876                let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
6877                let line_start = language::Point::new(selection.start.row, 0);
6878                let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
6879                multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
6880        })
6881    }
6882}