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