outline_panel.rs

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