outline_panel.rs

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