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 old_active != active {
4660                        if active {
4661                            if let Some((active_item, active_editor)) =
4662                                outline_panel.workspace.upgrade().and_then(|workspace| {
4663                                    workspace_active_editor(workspace.read(cx), cx)
4664                                })
4665                            {
4666                                if outline_panel.should_replace_active_item(active_item.as_ref()) {
4667                                    outline_panel.replace_active_editor(
4668                                        active_item,
4669                                        active_editor,
4670                                        cx,
4671                                    );
4672                                } else {
4673                                    outline_panel.update_fs_entries(active_editor, None, cx)
4674                                }
4675                                return;
4676                            }
4677                        }
4678
4679                        if !outline_panel.pinned {
4680                            outline_panel.clear_previous(cx);
4681                        }
4682                    }
4683                    outline_panel.serialize(cx);
4684                })
4685                .ok();
4686        })
4687        .detach()
4688    }
4689
4690    fn activation_priority(&self) -> u32 {
4691        5
4692    }
4693}
4694
4695impl FocusableView for OutlinePanel {
4696    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4697        self.filter_editor.focus_handle(cx).clone()
4698    }
4699}
4700
4701impl EventEmitter<Event> for OutlinePanel {}
4702
4703impl EventEmitter<PanelEvent> for OutlinePanel {}
4704
4705impl Render for OutlinePanel {
4706    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4707        let (is_local, is_via_ssh) = self
4708            .project
4709            .read_with(cx, |project, _| (project.is_local(), project.is_via_ssh()));
4710        let query = self.query(cx);
4711        let pinned = self.pinned;
4712        let settings = OutlinePanelSettings::get_global(cx);
4713        let indent_size = settings.indent_size;
4714        let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
4715
4716        let search_query = match &self.mode {
4717            ItemsDisplayMode::Search(search_query) => Some(search_query),
4718            _ => None,
4719        };
4720
4721        v_flex()
4722            .id("outline-panel")
4723            .size_full()
4724            .overflow_hidden()
4725            .relative()
4726            .on_hover(cx.listener(|this, hovered, cx| {
4727                if *hovered {
4728                    this.show_scrollbar = true;
4729                    this.hide_scrollbar_task.take();
4730                    cx.notify();
4731                } else if !this.focus_handle.contains_focused(cx) {
4732                    this.hide_scrollbar(cx);
4733                }
4734            }))
4735            .key_context(self.dispatch_context(cx))
4736            .on_action(cx.listener(Self::open))
4737            .on_action(cx.listener(Self::cancel))
4738            .on_action(cx.listener(Self::select_next))
4739            .on_action(cx.listener(Self::select_prev))
4740            .on_action(cx.listener(Self::select_first))
4741            .on_action(cx.listener(Self::select_last))
4742            .on_action(cx.listener(Self::select_parent))
4743            .on_action(cx.listener(Self::expand_selected_entry))
4744            .on_action(cx.listener(Self::collapse_selected_entry))
4745            .on_action(cx.listener(Self::expand_all_entries))
4746            .on_action(cx.listener(Self::collapse_all_entries))
4747            .on_action(cx.listener(Self::copy_path))
4748            .on_action(cx.listener(Self::copy_relative_path))
4749            .on_action(cx.listener(Self::toggle_active_editor_pin))
4750            .on_action(cx.listener(Self::unfold_directory))
4751            .on_action(cx.listener(Self::fold_directory))
4752            .on_action(cx.listener(Self::open_excerpts))
4753            .on_action(cx.listener(Self::open_excerpts_split))
4754            .when(is_local, |el| {
4755                el.on_action(cx.listener(Self::reveal_in_finder))
4756            })
4757            .when(is_local || is_via_ssh, |el| {
4758                el.on_action(cx.listener(Self::open_in_terminal))
4759            })
4760            .on_mouse_down(
4761                MouseButton::Right,
4762                cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
4763                    if let Some(entry) = outline_panel.selected_entry().cloned() {
4764                        outline_panel.deploy_context_menu(event.position, entry, cx)
4765                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
4766                        outline_panel.deploy_context_menu(event.position, PanelEntry::Fs(entry), cx)
4767                    }
4768                }),
4769            )
4770            .track_focus(&self.focus_handle)
4771            .when_some(search_query, |outline_panel, search_state| {
4772                outline_panel.child(
4773                    v_flex()
4774                        .child(
4775                            Label::new(format!("Searching: '{}'", search_state.query))
4776                                .color(Color::Muted)
4777                                .mx_2(),
4778                        )
4779                        .child(horizontal_separator(cx)),
4780                )
4781            })
4782            .child(self.render_main_contents(query, show_indent_guides, indent_size, cx))
4783            .child(self.render_filter_footer(pinned, cx))
4784    }
4785}
4786
4787fn find_active_indent_guide_ix(
4788    outline_panel: &OutlinePanel,
4789    candidates: &[IndentGuideLayout],
4790) -> Option<usize> {
4791    let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
4792        return None;
4793    };
4794    let target_depth = outline_panel
4795        .cached_entries
4796        .get(*target_ix)
4797        .map(|cached_entry| cached_entry.depth)?;
4798
4799    let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
4800        .cached_entries
4801        .get(target_ix + 1)
4802        .filter(|cached_entry| cached_entry.depth > target_depth)
4803        .map(|entry| entry.depth)
4804    {
4805        (target_ix + 1, target_depth.saturating_sub(1))
4806    } else {
4807        (*target_ix, target_depth.saturating_sub(1))
4808    };
4809
4810    candidates
4811        .iter()
4812        .enumerate()
4813        .find(|(_, guide)| {
4814            guide.offset.y <= target_ix
4815                && target_ix < guide.offset.y + guide.length
4816                && guide.offset.x == target_depth
4817        })
4818        .map(|(ix, _)| ix)
4819}
4820
4821fn subscribe_for_editor_events(
4822    editor: &View<Editor>,
4823    cx: &mut ViewContext<OutlinePanel>,
4824) -> Subscription {
4825    let debounce = Some(UPDATE_DEBOUNCE);
4826    cx.subscribe(editor, move |outline_panel, editor, e: &EditorEvent, cx| {
4827        if !outline_panel.active {
4828            return;
4829        }
4830        match e {
4831            EditorEvent::SelectionsChanged { local: true } => {
4832                outline_panel.reveal_entry_for_selection(editor, cx);
4833                cx.notify();
4834            }
4835            EditorEvent::ExcerptsAdded { excerpts, .. } => {
4836                outline_panel
4837                    .new_entries_for_fs_update
4838                    .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id));
4839                outline_panel.update_fs_entries(editor, debounce, cx);
4840            }
4841            EditorEvent::ExcerptsRemoved { ids } => {
4842                let mut ids = ids.iter().collect::<HashSet<_>>();
4843                for excerpts in outline_panel.excerpts.values_mut() {
4844                    excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
4845                    if ids.is_empty() {
4846                        break;
4847                    }
4848                }
4849                outline_panel.update_fs_entries(editor, debounce, cx);
4850            }
4851            EditorEvent::ExcerptsExpanded { ids } => {
4852                outline_panel.invalidate_outlines(ids);
4853                outline_panel.update_non_fs_items(cx);
4854            }
4855            EditorEvent::ExcerptsEdited { ids } => {
4856                outline_panel.invalidate_outlines(ids);
4857                outline_panel.update_non_fs_items(cx);
4858            }
4859            EditorEvent::BufferFoldToggled { ids, .. } => {
4860                outline_panel.invalidate_outlines(ids);
4861                let mut latest_unfolded_buffer_id = None;
4862                let mut latest_folded_buffer_id = None;
4863                let mut ignore_selections_change = false;
4864                outline_panel.new_entries_for_fs_update.extend(
4865                    ids.iter()
4866                        .filter(|id| {
4867                            outline_panel
4868                                .excerpts
4869                                .iter()
4870                                .find_map(|(buffer_id, excerpts)| {
4871                                    if excerpts.contains_key(id) {
4872                                        ignore_selections_change |= outline_panel
4873                                            .preserve_selection_on_buffer_fold_toggles
4874                                            .remove(buffer_id);
4875                                        Some(buffer_id)
4876                                    } else {
4877                                        None
4878                                    }
4879                                })
4880                                .map(|buffer_id| {
4881                                    if editor.read(cx).buffer_folded(*buffer_id, cx) {
4882                                        latest_folded_buffer_id = Some(*buffer_id);
4883                                        false
4884                                    } else {
4885                                        latest_unfolded_buffer_id = Some(*buffer_id);
4886                                        true
4887                                    }
4888                                })
4889                                .unwrap_or(true)
4890                        })
4891                        .copied(),
4892                );
4893                if !ignore_selections_change {
4894                    if let Some(entry_to_select) = latest_unfolded_buffer_id
4895                        .or(latest_folded_buffer_id)
4896                        .and_then(|toggled_buffer_id| {
4897                            outline_panel
4898                                .fs_entries
4899                                .iter()
4900                                .find_map(|fs_entry| match fs_entry {
4901                                    FsEntry::ExternalFile(external) => {
4902                                        if external.buffer_id == toggled_buffer_id {
4903                                            Some(fs_entry.clone())
4904                                        } else {
4905                                            None
4906                                        }
4907                                    }
4908                                    FsEntry::File(FsEntryFile { buffer_id, .. }) => {
4909                                        if *buffer_id == toggled_buffer_id {
4910                                            Some(fs_entry.clone())
4911                                        } else {
4912                                            None
4913                                        }
4914                                    }
4915                                    FsEntry::Directory(..) => None,
4916                                })
4917                        })
4918                        .map(PanelEntry::Fs)
4919                    {
4920                        outline_panel.select_entry(entry_to_select, true, cx);
4921                    }
4922                }
4923
4924                outline_panel.update_fs_entries(editor, debounce, cx);
4925            }
4926            EditorEvent::Reparsed(buffer_id) => {
4927                if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
4928                    for (_, excerpt) in excerpts {
4929                        excerpt.invalidate_outlines();
4930                    }
4931                }
4932                outline_panel.update_non_fs_items(cx);
4933            }
4934            _ => {}
4935        }
4936    })
4937}
4938
4939fn empty_icon() -> AnyElement {
4940    h_flex()
4941        .size(IconSize::default().rems())
4942        .invisible()
4943        .flex_none()
4944        .into_any_element()
4945}
4946
4947fn horizontal_separator(cx: &mut WindowContext) -> Div {
4948    div().mx_2().border_primary(cx).border_t_1()
4949}
4950
4951#[derive(Debug, Default)]
4952struct GenerationState {
4953    entries: Vec<CachedEntry>,
4954    match_candidates: Vec<StringMatchCandidate>,
4955    max_width_estimate_and_index: Option<(u64, usize)>,
4956}
4957
4958impl GenerationState {
4959    fn clear(&mut self) {
4960        self.entries.clear();
4961        self.match_candidates.clear();
4962        self.max_width_estimate_and_index = None;
4963    }
4964}
4965
4966#[cfg(test)]
4967mod tests {
4968    use gpui::{TestAppContext, VisualTestContext, WindowHandle};
4969    use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
4970    use pretty_assertions::assert_eq;
4971    use project::FakeFs;
4972    use search::project_search::{self, perform_project_search};
4973    use serde_json::json;
4974
4975    use super::*;
4976
4977    const SELECTED_MARKER: &str = "  <==== selected";
4978
4979    #[gpui::test(iterations = 10)]
4980    async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
4981        init_test(cx);
4982
4983        let fs = FakeFs::new(cx.background_executor.clone());
4984        populate_with_test_ra_project(&fs, "/rust-analyzer").await;
4985        let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
4986        project.read_with(cx, |project, _| {
4987            project.languages().add(Arc::new(rust_lang()))
4988        });
4989        let workspace = add_outline_panel(&project, cx).await;
4990        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4991        let outline_panel = outline_panel(&workspace, cx);
4992        outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4993
4994        workspace
4995            .update(cx, |workspace, cx| {
4996                ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4997            })
4998            .unwrap();
4999        let search_view = workspace
5000            .update(cx, |workspace, cx| {
5001                workspace
5002                    .active_pane()
5003                    .read(cx)
5004                    .items()
5005                    .find_map(|item| item.downcast::<ProjectSearchView>())
5006                    .expect("Project search view expected to appear after new search event trigger")
5007            })
5008            .unwrap();
5009
5010        let query = "param_names_for_lifetime_elision_hints";
5011        perform_project_search(&search_view, query, cx);
5012        search_view.update(cx, |search_view, cx| {
5013            search_view
5014                .results_editor()
5015                .update(cx, |results_editor, cx| {
5016                    assert_eq!(
5017                        results_editor.display_text(cx).match_indices(query).count(),
5018                        9
5019                    );
5020                });
5021        });
5022
5023        let all_matches = r#"/
5024  crates/
5025    ide/src/
5026      inlay_hints/
5027        fn_lifetime_fn.rs
5028          search: match config.param_names_for_lifetime_elision_hints {
5029          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5030          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5031          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5032      inlay_hints.rs
5033        search: pub param_names_for_lifetime_elision_hints: bool,
5034        search: param_names_for_lifetime_elision_hints: self
5035      static_index.rs
5036        search: param_names_for_lifetime_elision_hints: false,
5037    rust-analyzer/src/
5038      cli/
5039        analysis_stats.rs
5040          search: param_names_for_lifetime_elision_hints: true,
5041      config.rs
5042        search: param_names_for_lifetime_elision_hints: self"#;
5043        let select_first_in_all_matches = |line_to_select: &str| {
5044            assert!(all_matches.contains(line_to_select));
5045            all_matches.replacen(
5046                line_to_select,
5047                &format!("{line_to_select}{SELECTED_MARKER}"),
5048                1,
5049            )
5050        };
5051
5052        cx.executor()
5053            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5054        cx.run_until_parked();
5055        outline_panel.update(cx, |outline_panel, cx| {
5056            assert_eq!(
5057                display_entries(
5058                    &snapshot(&outline_panel, cx),
5059                    &outline_panel.cached_entries,
5060                    outline_panel.selected_entry()
5061                ),
5062                select_first_in_all_matches(
5063                    "search: match config.param_names_for_lifetime_elision_hints {"
5064                )
5065            );
5066        });
5067
5068        outline_panel.update(cx, |outline_panel, cx| {
5069            outline_panel.select_parent(&SelectParent, cx);
5070            assert_eq!(
5071                display_entries(
5072                    &snapshot(&outline_panel, cx),
5073                    &outline_panel.cached_entries,
5074                    outline_panel.selected_entry()
5075                ),
5076                select_first_in_all_matches("fn_lifetime_fn.rs")
5077            );
5078        });
5079        outline_panel.update(cx, |outline_panel, cx| {
5080            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
5081        });
5082        cx.executor()
5083            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5084        cx.run_until_parked();
5085        outline_panel.update(cx, |outline_panel, cx| {
5086            assert_eq!(
5087                display_entries(
5088                    &snapshot(&outline_panel, cx),
5089                    &outline_panel.cached_entries,
5090                    outline_panel.selected_entry()
5091                ),
5092                format!(
5093                    r#"/
5094  crates/
5095    ide/src/
5096      inlay_hints/
5097        fn_lifetime_fn.rs{SELECTED_MARKER}
5098      inlay_hints.rs
5099        search: pub param_names_for_lifetime_elision_hints: bool,
5100        search: param_names_for_lifetime_elision_hints: self
5101      static_index.rs
5102        search: param_names_for_lifetime_elision_hints: false,
5103    rust-analyzer/src/
5104      cli/
5105        analysis_stats.rs
5106          search: param_names_for_lifetime_elision_hints: true,
5107      config.rs
5108        search: param_names_for_lifetime_elision_hints: self"#,
5109                )
5110            );
5111        });
5112
5113        outline_panel.update(cx, |outline_panel, cx| {
5114            outline_panel.expand_all_entries(&ExpandAllEntries, cx);
5115        });
5116        cx.executor()
5117            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5118        cx.run_until_parked();
5119        outline_panel.update(cx, |outline_panel, cx| {
5120            outline_panel.select_parent(&SelectParent, cx);
5121            assert_eq!(
5122                display_entries(
5123                    &snapshot(&outline_panel, cx),
5124                    &outline_panel.cached_entries,
5125                    outline_panel.selected_entry()
5126                ),
5127                select_first_in_all_matches("inlay_hints/")
5128            );
5129        });
5130
5131        outline_panel.update(cx, |outline_panel, cx| {
5132            outline_panel.select_parent(&SelectParent, cx);
5133            assert_eq!(
5134                display_entries(
5135                    &snapshot(&outline_panel, cx),
5136                    &outline_panel.cached_entries,
5137                    outline_panel.selected_entry()
5138                ),
5139                select_first_in_all_matches("ide/src/")
5140            );
5141        });
5142
5143        outline_panel.update(cx, |outline_panel, cx| {
5144            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
5145        });
5146        cx.executor()
5147            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5148        cx.run_until_parked();
5149        outline_panel.update(cx, |outline_panel, cx| {
5150            assert_eq!(
5151                display_entries(
5152                    &snapshot(&outline_panel, cx),
5153                    &outline_panel.cached_entries,
5154                    outline_panel.selected_entry()
5155                ),
5156                format!(
5157                    r#"/
5158  crates/
5159    ide/src/{SELECTED_MARKER}
5160    rust-analyzer/src/
5161      cli/
5162        analysis_stats.rs
5163          search: param_names_for_lifetime_elision_hints: true,
5164      config.rs
5165        search: param_names_for_lifetime_elision_hints: self"#,
5166                )
5167            );
5168        });
5169        outline_panel.update(cx, |outline_panel, cx| {
5170            outline_panel.expand_selected_entry(&ExpandSelectedEntry, cx);
5171        });
5172        cx.executor()
5173            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5174        cx.run_until_parked();
5175        outline_panel.update(cx, |outline_panel, cx| {
5176            assert_eq!(
5177                display_entries(
5178                    &snapshot(&outline_panel, cx),
5179                    &outline_panel.cached_entries,
5180                    outline_panel.selected_entry()
5181                ),
5182                select_first_in_all_matches("ide/src/")
5183            );
5184        });
5185    }
5186
5187    #[gpui::test(iterations = 10)]
5188    async fn test_item_filtering(cx: &mut TestAppContext) {
5189        init_test(cx);
5190
5191        let fs = FakeFs::new(cx.background_executor.clone());
5192        populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5193        let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5194        project.read_with(cx, |project, _| {
5195            project.languages().add(Arc::new(rust_lang()))
5196        });
5197        let workspace = add_outline_panel(&project, cx).await;
5198        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5199        let outline_panel = outline_panel(&workspace, cx);
5200        outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
5201
5202        workspace
5203            .update(cx, |workspace, cx| {
5204                ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
5205            })
5206            .unwrap();
5207        let search_view = workspace
5208            .update(cx, |workspace, cx| {
5209                workspace
5210                    .active_pane()
5211                    .read(cx)
5212                    .items()
5213                    .find_map(|item| item.downcast::<ProjectSearchView>())
5214                    .expect("Project search view expected to appear after new search event trigger")
5215            })
5216            .unwrap();
5217
5218        let query = "param_names_for_lifetime_elision_hints";
5219        perform_project_search(&search_view, query, cx);
5220        search_view.update(cx, |search_view, cx| {
5221            search_view
5222                .results_editor()
5223                .update(cx, |results_editor, cx| {
5224                    assert_eq!(
5225                        results_editor.display_text(cx).match_indices(query).count(),
5226                        9
5227                    );
5228                });
5229        });
5230        let all_matches = r#"/
5231  crates/
5232    ide/src/
5233      inlay_hints/
5234        fn_lifetime_fn.rs
5235          search: match config.param_names_for_lifetime_elision_hints {
5236          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5237          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5238          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5239      inlay_hints.rs
5240        search: pub param_names_for_lifetime_elision_hints: bool,
5241        search: param_names_for_lifetime_elision_hints: self
5242      static_index.rs
5243        search: param_names_for_lifetime_elision_hints: false,
5244    rust-analyzer/src/
5245      cli/
5246        analysis_stats.rs
5247          search: param_names_for_lifetime_elision_hints: true,
5248      config.rs
5249        search: param_names_for_lifetime_elision_hints: self"#;
5250
5251        cx.executor()
5252            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5253        cx.run_until_parked();
5254        outline_panel.update(cx, |outline_panel, cx| {
5255            assert_eq!(
5256                display_entries(
5257                    &snapshot(&outline_panel, cx),
5258                    &outline_panel.cached_entries,
5259                    None,
5260                ),
5261                all_matches,
5262            );
5263        });
5264
5265        let filter_text = "a";
5266        outline_panel.update(cx, |outline_panel, cx| {
5267            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5268                filter_editor.set_text(filter_text, cx);
5269            });
5270        });
5271        cx.executor()
5272            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5273        cx.run_until_parked();
5274
5275        outline_panel.update(cx, |outline_panel, cx| {
5276            assert_eq!(
5277                display_entries(
5278                    &snapshot(&outline_panel, cx),
5279                    &outline_panel.cached_entries,
5280                    None,
5281                ),
5282                all_matches
5283                    .lines()
5284                    .filter(|item| item.contains(filter_text))
5285                    .collect::<Vec<_>>()
5286                    .join("\n"),
5287            );
5288        });
5289
5290        outline_panel.update(cx, |outline_panel, cx| {
5291            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
5292                filter_editor.set_text("", cx);
5293            });
5294        });
5295        cx.executor()
5296            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5297        cx.run_until_parked();
5298        outline_panel.update(cx, |outline_panel, cx| {
5299            assert_eq!(
5300                display_entries(
5301                    &snapshot(&outline_panel, cx),
5302                    &outline_panel.cached_entries,
5303                    None,
5304                ),
5305                all_matches,
5306            );
5307        });
5308    }
5309
5310    #[gpui::test(iterations = 10)]
5311    async fn test_item_opening(cx: &mut TestAppContext) {
5312        init_test(cx);
5313
5314        let fs = FakeFs::new(cx.background_executor.clone());
5315        populate_with_test_ra_project(&fs, "/rust-analyzer").await;
5316        let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
5317        project.read_with(cx, |project, _| {
5318            project.languages().add(Arc::new(rust_lang()))
5319        });
5320        let workspace = add_outline_panel(&project, cx).await;
5321        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5322        let outline_panel = outline_panel(&workspace, cx);
5323        outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
5324
5325        workspace
5326            .update(cx, |workspace, cx| {
5327                ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
5328            })
5329            .unwrap();
5330        let search_view = workspace
5331            .update(cx, |workspace, cx| {
5332                workspace
5333                    .active_pane()
5334                    .read(cx)
5335                    .items()
5336                    .find_map(|item| item.downcast::<ProjectSearchView>())
5337                    .expect("Project search view expected to appear after new search event trigger")
5338            })
5339            .unwrap();
5340
5341        let query = "param_names_for_lifetime_elision_hints";
5342        perform_project_search(&search_view, query, cx);
5343        search_view.update(cx, |search_view, cx| {
5344            search_view
5345                .results_editor()
5346                .update(cx, |results_editor, cx| {
5347                    assert_eq!(
5348                        results_editor.display_text(cx).match_indices(query).count(),
5349                        9
5350                    );
5351                });
5352        });
5353        let all_matches = r#"/
5354  crates/
5355    ide/src/
5356      inlay_hints/
5357        fn_lifetime_fn.rs
5358          search: match config.param_names_for_lifetime_elision_hints {
5359          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5360          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
5361          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5362      inlay_hints.rs
5363        search: pub param_names_for_lifetime_elision_hints: bool,
5364        search: param_names_for_lifetime_elision_hints: self
5365      static_index.rs
5366        search: param_names_for_lifetime_elision_hints: false,
5367    rust-analyzer/src/
5368      cli/
5369        analysis_stats.rs
5370          search: param_names_for_lifetime_elision_hints: true,
5371      config.rs
5372        search: param_names_for_lifetime_elision_hints: self"#;
5373        let select_first_in_all_matches = |line_to_select: &str| {
5374            assert!(all_matches.contains(line_to_select));
5375            all_matches.replacen(
5376                line_to_select,
5377                &format!("{line_to_select}{SELECTED_MARKER}"),
5378                1,
5379            )
5380        };
5381        cx.executor()
5382            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5383        cx.run_until_parked();
5384
5385        let active_editor = outline_panel.update(cx, |outline_panel, _| {
5386            outline_panel
5387                .active_editor()
5388                .expect("should have an active editor open")
5389        });
5390        let initial_outline_selection =
5391            "search: match config.param_names_for_lifetime_elision_hints {";
5392        outline_panel.update(cx, |outline_panel, cx| {
5393            assert_eq!(
5394                display_entries(
5395                    &snapshot(&outline_panel, cx),
5396                    &outline_panel.cached_entries,
5397                    outline_panel.selected_entry(),
5398                ),
5399                select_first_in_all_matches(initial_outline_selection)
5400            );
5401            assert_eq!(
5402                selected_row_text(&active_editor, cx),
5403                initial_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5404                "Should place the initial editor selection on the corresponding search result"
5405            );
5406
5407            outline_panel.select_next(&SelectNext, cx);
5408            outline_panel.select_next(&SelectNext, cx);
5409        });
5410
5411        let navigated_outline_selection =
5412            "search: Some(it) if config.param_names_for_lifetime_elision_hints => {";
5413        outline_panel.update(cx, |outline_panel, cx| {
5414            assert_eq!(
5415                display_entries(
5416                    &snapshot(&outline_panel, cx),
5417                    &outline_panel.cached_entries,
5418                    outline_panel.selected_entry(),
5419                ),
5420                select_first_in_all_matches(navigated_outline_selection)
5421            );
5422        });
5423        cx.executor()
5424            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
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                "Should still have the initial caret position after SelectNext calls"
5430            );
5431        });
5432
5433        outline_panel.update(cx, |outline_panel, cx| {
5434            outline_panel.open(&Open, cx);
5435        });
5436        outline_panel.update(cx, |_, cx| {
5437            assert_eq!(
5438                selected_row_text(&active_editor, cx),
5439                navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5440                "After opening, should move the caret to the opened outline entry's position"
5441            );
5442        });
5443
5444        outline_panel.update(cx, |outline_panel, cx| {
5445            outline_panel.select_next(&SelectNext, cx);
5446        });
5447        let next_navigated_outline_selection =
5448            "search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },";
5449        outline_panel.update(cx, |outline_panel, cx| {
5450            assert_eq!(
5451                display_entries(
5452                    &snapshot(&outline_panel, cx),
5453                    &outline_panel.cached_entries,
5454                    outline_panel.selected_entry(),
5455                ),
5456                select_first_in_all_matches(next_navigated_outline_selection)
5457            );
5458        });
5459        cx.executor()
5460            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5461        outline_panel.update(cx, |_, cx| {
5462            assert_eq!(
5463                selected_row_text(&active_editor, cx),
5464                next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5465                "Should again preserve the selection after another SelectNext call"
5466            );
5467        });
5468
5469        outline_panel.update(cx, |outline_panel, cx| {
5470            outline_panel.open_excerpts(&editor::OpenExcerpts, cx);
5471        });
5472        cx.executor()
5473            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5474        cx.run_until_parked();
5475        let new_active_editor = outline_panel.update(cx, |outline_panel, _| {
5476            outline_panel
5477                .active_editor()
5478                .expect("should have an active editor open")
5479        });
5480        outline_panel.update(cx, |outline_panel, cx| {
5481            assert_ne!(
5482                active_editor, new_active_editor,
5483                "After opening an excerpt, new editor should be open"
5484            );
5485            assert_eq!(
5486                display_entries(
5487                    &snapshot(&outline_panel, cx),
5488                    &outline_panel.cached_entries,
5489                    outline_panel.selected_entry(),
5490                ),
5491                "fn_lifetime_fn.rs  <==== selected"
5492            );
5493            assert_eq!(
5494                selected_row_text(&new_active_editor, cx),
5495                next_navigated_outline_selection.replace("search: ", ""), // Clear outline metadata prefixes
5496                "When opening the excerpt, should navigate to the place corresponding the outline entry"
5497            );
5498        });
5499    }
5500
5501    #[gpui::test(iterations = 10)]
5502    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
5503        init_test(cx);
5504
5505        let root = "/frontend-project";
5506        let fs = FakeFs::new(cx.background_executor.clone());
5507        fs.insert_tree(
5508            root,
5509            json!({
5510                "public": {
5511                    "lottie": {
5512                        "syntax-tree.json": r#"{ "something": "static" }"#
5513                    }
5514                },
5515                "src": {
5516                    "app": {
5517                        "(site)": {
5518                            "(about)": {
5519                                "jobs": {
5520                                    "[slug]": {
5521                                        "page.tsx": r#"static"#
5522                                    }
5523                                }
5524                            },
5525                            "(blog)": {
5526                                "post": {
5527                                    "[slug]": {
5528                                        "page.tsx": r#"static"#
5529                                    }
5530                                }
5531                            },
5532                        }
5533                    },
5534                    "components": {
5535                        "ErrorBoundary.tsx": r#"static"#,
5536                    }
5537                }
5538
5539            }),
5540        )
5541        .await;
5542        let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
5543        let workspace = add_outline_panel(&project, cx).await;
5544        let cx = &mut VisualTestContext::from_window(*workspace, cx);
5545        let outline_panel = outline_panel(&workspace, cx);
5546        outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
5547
5548        workspace
5549            .update(cx, |workspace, cx| {
5550                ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
5551            })
5552            .unwrap();
5553        let search_view = workspace
5554            .update(cx, |workspace, cx| {
5555                workspace
5556                    .active_pane()
5557                    .read(cx)
5558                    .items()
5559                    .find_map(|item| item.downcast::<ProjectSearchView>())
5560                    .expect("Project search view expected to appear after new search event trigger")
5561            })
5562            .unwrap();
5563
5564        let query = "static";
5565        perform_project_search(&search_view, query, cx);
5566        search_view.update(cx, |search_view, cx| {
5567            search_view
5568                .results_editor()
5569                .update(cx, |results_editor, cx| {
5570                    assert_eq!(
5571                        results_editor.display_text(cx).match_indices(query).count(),
5572                        4
5573                    );
5574                });
5575        });
5576
5577        cx.executor()
5578            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5579        cx.run_until_parked();
5580        outline_panel.update(cx, |outline_panel, cx| {
5581            assert_eq!(
5582                display_entries(
5583                    &snapshot(&outline_panel, cx),
5584                    &outline_panel.cached_entries,
5585                    outline_panel.selected_entry()
5586                ),
5587                r#"/
5588  public/lottie/
5589    syntax-tree.json
5590      search: { "something": "static" }  <==== selected
5591  src/
5592    app/(site)/
5593      (about)/jobs/[slug]/
5594        page.tsx
5595          search: static
5596      (blog)/post/[slug]/
5597        page.tsx
5598          search: static
5599    components/
5600      ErrorBoundary.tsx
5601        search: static"#
5602            );
5603        });
5604
5605        outline_panel.update(cx, |outline_panel, cx| {
5606            // Move to 5th element in the list, 3 items down.
5607            for _ in 0..2 {
5608                outline_panel.select_next(&SelectNext, cx);
5609            }
5610            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
5611        });
5612        cx.executor()
5613            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5614        cx.run_until_parked();
5615        outline_panel.update(cx, |outline_panel, cx| {
5616            assert_eq!(
5617                display_entries(
5618                    &snapshot(&outline_panel, cx),
5619                    &outline_panel.cached_entries,
5620                    outline_panel.selected_entry()
5621                ),
5622                r#"/
5623  public/lottie/
5624    syntax-tree.json
5625      search: { "something": "static" }
5626  src/
5627    app/(site)/  <==== selected
5628    components/
5629      ErrorBoundary.tsx
5630        search: static"#
5631            );
5632        });
5633
5634        outline_panel.update(cx, |outline_panel, cx| {
5635            // Move to the next visible non-FS entry
5636            for _ in 0..3 {
5637                outline_panel.select_next(&SelectNext, cx);
5638            }
5639        });
5640        cx.run_until_parked();
5641        outline_panel.update(cx, |outline_panel, cx| {
5642            assert_eq!(
5643                display_entries(
5644                    &snapshot(&outline_panel, cx),
5645                    &outline_panel.cached_entries,
5646                    outline_panel.selected_entry()
5647                ),
5648                r#"/
5649  public/lottie/
5650    syntax-tree.json
5651      search: { "something": "static" }
5652  src/
5653    app/(site)/
5654    components/
5655      ErrorBoundary.tsx
5656        search: static  <==== selected"#
5657            );
5658        });
5659
5660        outline_panel.update(cx, |outline_panel, cx| {
5661            outline_panel
5662                .active_editor()
5663                .expect("Should have an active editor")
5664                .update(cx, |editor, cx| {
5665                    editor.toggle_fold(&editor::actions::ToggleFold, cx)
5666                });
5667        });
5668        cx.executor()
5669            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5670        cx.run_until_parked();
5671        outline_panel.update(cx, |outline_panel, cx| {
5672            assert_eq!(
5673                display_entries(
5674                    &snapshot(&outline_panel, cx),
5675                    &outline_panel.cached_entries,
5676                    outline_panel.selected_entry()
5677                ),
5678                r#"/
5679  public/lottie/
5680    syntax-tree.json
5681      search: { "something": "static" }
5682  src/
5683    app/(site)/
5684    components/
5685      ErrorBoundary.tsx  <==== selected"#
5686            );
5687        });
5688
5689        outline_panel.update(cx, |outline_panel, cx| {
5690            outline_panel
5691                .active_editor()
5692                .expect("Should have an active editor")
5693                .update(cx, |editor, cx| {
5694                    editor.toggle_fold(&editor::actions::ToggleFold, cx)
5695                });
5696        });
5697        cx.executor()
5698            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
5699        cx.run_until_parked();
5700        outline_panel.update(cx, |outline_panel, cx| {
5701            assert_eq!(
5702                display_entries(
5703                    &snapshot(&outline_panel, cx),
5704                    &outline_panel.cached_entries,
5705                    outline_panel.selected_entry()
5706                ),
5707                r#"/
5708  public/lottie/
5709    syntax-tree.json
5710      search: { "something": "static" }
5711  src/
5712    app/(site)/
5713    components/
5714      ErrorBoundary.tsx  <==== selected
5715        search: static"#
5716            );
5717        });
5718    }
5719
5720    async fn add_outline_panel(
5721        project: &Model<Project>,
5722        cx: &mut TestAppContext,
5723    ) -> WindowHandle<Workspace> {
5724        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
5725
5726        let outline_panel = window
5727            .update(cx, |_, cx| cx.spawn(OutlinePanel::load))
5728            .unwrap()
5729            .await
5730            .expect("Failed to load outline panel");
5731
5732        window
5733            .update(cx, |workspace, cx| {
5734                workspace.add_panel(outline_panel, cx);
5735            })
5736            .unwrap();
5737        window
5738    }
5739
5740    fn outline_panel(
5741        workspace: &WindowHandle<Workspace>,
5742        cx: &mut TestAppContext,
5743    ) -> View<OutlinePanel> {
5744        workspace
5745            .update(cx, |workspace, cx| {
5746                workspace
5747                    .panel::<OutlinePanel>(cx)
5748                    .expect("no outline panel")
5749            })
5750            .unwrap()
5751    }
5752
5753    fn display_entries(
5754        multi_buffer_snapshot: &MultiBufferSnapshot,
5755        cached_entries: &[CachedEntry],
5756        selected_entry: Option<&PanelEntry>,
5757    ) -> String {
5758        let mut display_string = String::new();
5759        for entry in cached_entries {
5760            if !display_string.is_empty() {
5761                display_string += "\n";
5762            }
5763            for _ in 0..entry.depth {
5764                display_string += "  ";
5765            }
5766            display_string += &match &entry.entry {
5767                PanelEntry::Fs(entry) => match entry {
5768                    FsEntry::ExternalFile(_) => {
5769                        panic!("Did not cover external files with tests")
5770                    }
5771                    FsEntry::Directory(directory) => format!(
5772                        "{}/",
5773                        directory
5774                            .entry
5775                            .path
5776                            .file_name()
5777                            .map(|name| name.to_string_lossy().to_string())
5778                            .unwrap_or_default()
5779                    ),
5780                    FsEntry::File(file) => file
5781                        .entry
5782                        .path
5783                        .file_name()
5784                        .map(|name| name.to_string_lossy().to_string())
5785                        .unwrap_or_default(),
5786                },
5787                PanelEntry::FoldedDirs(folded_dirs) => folded_dirs
5788                    .entries
5789                    .iter()
5790                    .filter_map(|dir| dir.path.file_name())
5791                    .map(|name| name.to_string_lossy().to_string() + "/")
5792                    .collect(),
5793                PanelEntry::Outline(outline_entry) => match outline_entry {
5794                    OutlineEntry::Excerpt(_) => continue,
5795                    OutlineEntry::Outline(outline_entry) => {
5796                        format!("outline: {}", outline_entry.outline.text)
5797                    }
5798                },
5799                PanelEntry::Search(search_entry) => {
5800                    format!(
5801                        "search: {}",
5802                        search_entry
5803                            .render_data
5804                            .get_or_init(|| SearchData::new(
5805                                &search_entry.match_range,
5806                                &multi_buffer_snapshot
5807                            ))
5808                            .context_text
5809                    )
5810                }
5811            };
5812
5813            if Some(&entry.entry) == selected_entry {
5814                display_string += SELECTED_MARKER;
5815            }
5816        }
5817        display_string
5818    }
5819
5820    fn init_test(cx: &mut TestAppContext) {
5821        cx.update(|cx| {
5822            let settings = SettingsStore::test(cx);
5823            cx.set_global(settings);
5824
5825            theme::init(theme::LoadThemes::JustBase, cx);
5826
5827            language::init(cx);
5828            editor::init(cx);
5829            workspace::init_settings(cx);
5830            Project::init_settings(cx);
5831            project_search::init(cx);
5832            super::init((), cx);
5833        });
5834    }
5835
5836    // Based on https://github.com/rust-lang/rust-analyzer/
5837    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
5838        fs.insert_tree(
5839            root,
5840            json!({
5841                    "crates": {
5842                        "ide": {
5843                            "src": {
5844                                "inlay_hints": {
5845                                    "fn_lifetime_fn.rs": r##"
5846        pub(super) fn hints(
5847            acc: &mut Vec<InlayHint>,
5848            config: &InlayHintsConfig,
5849            func: ast::Fn,
5850        ) -> Option<()> {
5851            // ... snip
5852
5853            let mut used_names: FxHashMap<SmolStr, usize> =
5854                match config.param_names_for_lifetime_elision_hints {
5855                    true => generic_param_list
5856                        .iter()
5857                        .flat_map(|gpl| gpl.lifetime_params())
5858                        .filter_map(|param| param.lifetime())
5859                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
5860                        .collect(),
5861                    false => Default::default(),
5862                };
5863            {
5864                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
5865                if self_param.is_some() && potential_lt_refs.next().is_some() {
5866                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
5867                        // self can't be used as a lifetime, so no need to check for collisions
5868                        "'self".into()
5869                    } else {
5870                        gen_idx_name()
5871                    });
5872                }
5873                potential_lt_refs.for_each(|(name, ..)| {
5874                    let name = match name {
5875                        Some(it) if config.param_names_for_lifetime_elision_hints => {
5876                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
5877                                *c += 1;
5878                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
5879                            } else {
5880                                used_names.insert(it.text().as_str().into(), 0);
5881                                SmolStr::from_iter(["\'", it.text().as_str()])
5882                            }
5883                        }
5884                        _ => gen_idx_name(),
5885                    };
5886                    allocated_lifetimes.push(name);
5887                });
5888            }
5889
5890            // ... snip
5891        }
5892
5893        // ... snip
5894
5895            #[test]
5896            fn hints_lifetimes_named() {
5897                check_with_config(
5898                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
5899                    r#"
5900        fn nested_in<'named>(named: &        &X<      &()>) {}
5901        //          ^'named1, 'named2, 'named3, $
5902                                  //^'named1 ^'named2 ^'named3
5903        "#,
5904                );
5905            }
5906
5907        // ... snip
5908        "##,
5909                                },
5910                        "inlay_hints.rs": r#"
5911    #[derive(Clone, Debug, PartialEq, Eq)]
5912    pub struct InlayHintsConfig {
5913        // ... snip
5914        pub param_names_for_lifetime_elision_hints: bool,
5915        pub max_length: Option<usize>,
5916        // ... snip
5917    }
5918
5919    impl Config {
5920        pub fn inlay_hints(&self) -> InlayHintsConfig {
5921            InlayHintsConfig {
5922                // ... snip
5923                param_names_for_lifetime_elision_hints: self
5924                    .inlayHints_lifetimeElisionHints_useParameterNames()
5925                    .to_owned(),
5926                max_length: self.inlayHints_maxLength().to_owned(),
5927                // ... snip
5928            }
5929        }
5930    }
5931    "#,
5932                        "static_index.rs": r#"
5933// ... snip
5934        fn add_file(&mut self, file_id: FileId) {
5935            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
5936            let folds = self.analysis.folding_ranges(file_id).unwrap();
5937            let inlay_hints = self
5938                .analysis
5939                .inlay_hints(
5940                    &InlayHintsConfig {
5941                        // ... snip
5942                        closure_style: hir::ClosureStyle::ImplFn,
5943                        param_names_for_lifetime_elision_hints: false,
5944                        binding_mode_hints: false,
5945                        max_length: Some(25),
5946                        closure_capture_hints: false,
5947                        // ... snip
5948                    },
5949                    file_id,
5950                    None,
5951                )
5952                .unwrap();
5953            // ... snip
5954    }
5955// ... snip
5956    "#
5957                            }
5958                        },
5959                        "rust-analyzer": {
5960                            "src": {
5961                                "cli": {
5962                                    "analysis_stats.rs": r#"
5963        // ... snip
5964                for &file_id in &file_ids {
5965                    _ = analysis.inlay_hints(
5966                        &InlayHintsConfig {
5967                            // ... snip
5968                            implicit_drop_hints: true,
5969                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
5970                            param_names_for_lifetime_elision_hints: true,
5971                            hide_named_constructor_hints: false,
5972                            hide_closure_initialization_hints: false,
5973                            closure_style: hir::ClosureStyle::ImplFn,
5974                            max_length: Some(25),
5975                            closing_brace_hints_min_lines: Some(20),
5976                            fields_to_resolve: InlayFieldsToResolve::empty(),
5977                            range_exclusive_hints: true,
5978                        },
5979                        file_id.into(),
5980                        None,
5981                    );
5982                }
5983        // ... snip
5984                                    "#,
5985                                },
5986                                "config.rs": r#"
5987                config_data! {
5988                    /// Configs that only make sense when they are set by a client. As such they can only be defined
5989                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
5990                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
5991                        // ... snip
5992                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
5993                        inlayHints_maxLength: Option<usize>                        = Some(25),
5994                        // ... snip
5995                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
5996                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
5997                        // ... snip
5998                    }
5999                }
6000
6001                impl Config {
6002                    // ... snip
6003                    pub fn inlay_hints(&self) -> InlayHintsConfig {
6004                        InlayHintsConfig {
6005                            // ... snip
6006                            param_names_for_lifetime_elision_hints: self
6007                                .inlayHints_lifetimeElisionHints_useParameterNames()
6008                                .to_owned(),
6009                            max_length: self.inlayHints_maxLength().to_owned(),
6010                            // ... snip
6011                        }
6012                    }
6013                    // ... snip
6014                }
6015                "#
6016                                }
6017                        }
6018                    }
6019            }),
6020        )
6021        .await;
6022    }
6023
6024    fn rust_lang() -> Language {
6025        Language::new(
6026            LanguageConfig {
6027                name: "Rust".into(),
6028                matcher: LanguageMatcher {
6029                    path_suffixes: vec!["rs".to_string()],
6030                    ..Default::default()
6031                },
6032                ..Default::default()
6033            },
6034            Some(tree_sitter_rust::LANGUAGE.into()),
6035        )
6036        .with_highlights_query(
6037            r#"
6038                (field_identifier) @field
6039                (struct_expression) @struct
6040            "#,
6041        )
6042        .unwrap()
6043        .with_injection_query(
6044            r#"
6045                (macro_invocation
6046                    (token_tree) @injection.content
6047                    (#set! injection.language "rust"))
6048            "#,
6049        )
6050        .unwrap()
6051    }
6052
6053    fn snapshot(outline_panel: &OutlinePanel, cx: &AppContext) -> MultiBufferSnapshot {
6054        outline_panel
6055            .active_editor()
6056            .unwrap()
6057            .read(cx)
6058            .buffer()
6059            .read(cx)
6060            .snapshot(cx)
6061    }
6062
6063    fn selected_row_text(editor: &View<Editor>, cx: &mut WindowContext) -> String {
6064        editor.update(cx, |editor, cx| {
6065                let selections = editor.selections.all::<language::Point>(cx);
6066                assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions");
6067                let selection = selections.first().unwrap();
6068                let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
6069                let line_start = language::Point::new(selection.start.row, 0);
6070                let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right);
6071                multi_buffer_snapshot.text_for_range(line_start..line_end).collect::<String>().trim().to_owned()
6072        })
6073    }
6074}