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