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