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