outline_panel.rs

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