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