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