outline_panel.rs

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