outline_panel.rs

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