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