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