outline_panel.rs

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