outline_panel.rs

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