outline_panel.rs

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