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