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.update(cx, |editor, cx| {
2414            editor.selections.newest::<language::Point>(cx).head()
2415        });
2416        let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(cx));
2417        let multi_buffer = editor.read(cx).buffer();
2418        let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx);
2419        let (excerpt_id, buffer, _) = editor
2420            .read(cx)
2421            .buffer()
2422            .read(cx)
2423            .excerpt_containing(selection, cx)?;
2424        let buffer_id = buffer.read(cx).remote_id();
2425        let selection_display_point = selection.to_display_point(&editor_snapshot);
2426
2427        match &self.mode {
2428            ItemsDisplayMode::Search(search_state) => search_state
2429                .matches
2430                .iter()
2431                .rev()
2432                .min_by_key(|&(match_range, _)| {
2433                    let match_display_range =
2434                        match_range.clone().to_display_points(&editor_snapshot);
2435                    let start_distance = if selection_display_point < match_display_range.start {
2436                        match_display_range.start - selection_display_point
2437                    } else {
2438                        selection_display_point - match_display_range.start
2439                    };
2440                    let end_distance = if selection_display_point < match_display_range.end {
2441                        match_display_range.end - selection_display_point
2442                    } else {
2443                        selection_display_point - match_display_range.end
2444                    };
2445                    start_distance + end_distance
2446                })
2447                .and_then(|(closest_range, _)| {
2448                    self.cached_entries.iter().find_map(|cached_entry| {
2449                        if let PanelEntry::Search(SearchEntry { match_range, .. }) =
2450                            &cached_entry.entry
2451                        {
2452                            if match_range == closest_range {
2453                                Some(cached_entry.entry.clone())
2454                            } else {
2455                                None
2456                            }
2457                        } else {
2458                            None
2459                        }
2460                    })
2461                }),
2462            ItemsDisplayMode::Outline => self.outline_location(
2463                buffer_id,
2464                excerpt_id,
2465                multi_buffer_snapshot,
2466                editor_snapshot,
2467                selection_display_point,
2468            ),
2469        }
2470    }
2471
2472    fn outline_location(
2473        &mut self,
2474        buffer_id: BufferId,
2475        excerpt_id: ExcerptId,
2476        multi_buffer_snapshot: editor::MultiBufferSnapshot,
2477        editor_snapshot: editor::EditorSnapshot,
2478        selection_display_point: DisplayPoint,
2479    ) -> Option<PanelEntry> {
2480        let excerpt_outlines = self
2481            .excerpts
2482            .get(&buffer_id)
2483            .and_then(|excerpts| excerpts.get(&excerpt_id))
2484            .into_iter()
2485            .flat_map(|excerpt| excerpt.iter_outlines())
2486            .flat_map(|outline| {
2487                let start = multi_buffer_snapshot
2488                    .anchor_in_excerpt(excerpt_id, outline.range.start)?
2489                    .to_display_point(&editor_snapshot);
2490                let end = multi_buffer_snapshot
2491                    .anchor_in_excerpt(excerpt_id, outline.range.end)?
2492                    .to_display_point(&editor_snapshot);
2493                Some((start..end, outline))
2494            })
2495            .collect::<Vec<_>>();
2496
2497        let mut matching_outline_indices = Vec::new();
2498        let mut children = HashMap::default();
2499        let mut parents_stack = Vec::<(&Range<DisplayPoint>, &&Outline, usize)>::new();
2500
2501        for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() {
2502            if outline_range
2503                .to_inclusive()
2504                .contains(&selection_display_point)
2505            {
2506                matching_outline_indices.push(i);
2507            } else if (outline_range.start.row()..outline_range.end.row())
2508                .to_inclusive()
2509                .contains(&selection_display_point.row())
2510            {
2511                matching_outline_indices.push(i);
2512            }
2513
2514            while let Some((parent_range, parent_outline, _)) = parents_stack.last() {
2515                if parent_outline.depth >= outline.depth
2516                    || !parent_range.contains(&outline_range.start)
2517                {
2518                    parents_stack.pop();
2519                } else {
2520                    break;
2521                }
2522            }
2523            if let Some((_, _, parent_index)) = parents_stack.last_mut() {
2524                children
2525                    .entry(*parent_index)
2526                    .or_insert_with(Vec::new)
2527                    .push(i);
2528            }
2529            parents_stack.push((outline_range, outline, i));
2530        }
2531
2532        let outline_item = matching_outline_indices
2533            .into_iter()
2534            .flat_map(|i| Some((i, excerpt_outlines.get(i)?)))
2535            .filter(|(i, _)| {
2536                children
2537                    .get(i)
2538                    .map(|children| {
2539                        children.iter().all(|child_index| {
2540                            excerpt_outlines
2541                                .get(*child_index)
2542                                .map(|(child_range, _)| child_range.start > selection_display_point)
2543                                .unwrap_or(false)
2544                        })
2545                    })
2546                    .unwrap_or(true)
2547            })
2548            .min_by_key(|(_, (outline_range, outline))| {
2549                let distance_from_start = if outline_range.start > selection_display_point {
2550                    outline_range.start - selection_display_point
2551                } else {
2552                    selection_display_point - outline_range.start
2553                };
2554                let distance_from_end = if outline_range.end > selection_display_point {
2555                    outline_range.end - selection_display_point
2556                } else {
2557                    selection_display_point - outline_range.end
2558                };
2559
2560                (
2561                    cmp::Reverse(outline.depth),
2562                    distance_from_start + distance_from_end,
2563                )
2564            })
2565            .map(|(_, (_, outline))| *outline)
2566            .cloned();
2567
2568        let closest_container = match outline_item {
2569            Some(outline) => {
2570                PanelEntry::Outline(OutlineEntry::Outline(buffer_id, excerpt_id, outline))
2571            }
2572            None => {
2573                self.cached_entries.iter().rev().find_map(|cached_entry| {
2574                    match &cached_entry.entry {
2575                        PanelEntry::Outline(OutlineEntry::Excerpt(
2576                            entry_buffer_id,
2577                            entry_excerpt_id,
2578                            _,
2579                        )) => {
2580                            if entry_buffer_id == &buffer_id && entry_excerpt_id == &excerpt_id {
2581                                Some(cached_entry.entry.clone())
2582                            } else {
2583                                None
2584                            }
2585                        }
2586                        PanelEntry::Fs(
2587                            FsEntry::ExternalFile(file_buffer_id, file_excerpts)
2588                            | FsEntry::File(_, _, file_buffer_id, file_excerpts),
2589                        ) => {
2590                            if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) {
2591                                Some(cached_entry.entry.clone())
2592                            } else {
2593                                None
2594                            }
2595                        }
2596                        _ => None,
2597                    }
2598                })?
2599            }
2600        };
2601        Some(closest_container)
2602    }
2603
2604    fn fetch_outdated_outlines(&mut self, cx: &mut ViewContext<Self>) {
2605        let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx);
2606        if excerpt_fetch_ranges.is_empty() {
2607            return;
2608        }
2609
2610        let syntax_theme = cx.theme().syntax().clone();
2611        for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges {
2612            for (excerpt_id, excerpt_range) in excerpt_ranges {
2613                let syntax_theme = syntax_theme.clone();
2614                let buffer_snapshot = buffer_snapshot.clone();
2615                self.outline_fetch_tasks.insert(
2616                    (buffer_id, excerpt_id),
2617                    cx.spawn(|outline_panel, mut cx| async move {
2618                        let fetched_outlines = cx
2619                            .background_executor()
2620                            .spawn(async move {
2621                                buffer_snapshot
2622                                    .outline_items_containing(
2623                                        excerpt_range.context,
2624                                        false,
2625                                        Some(&syntax_theme),
2626                                    )
2627                                    .unwrap_or_default()
2628                            })
2629                            .await;
2630                        outline_panel
2631                            .update(&mut cx, |outline_panel, cx| {
2632                                if let Some(excerpt) = outline_panel
2633                                    .excerpts
2634                                    .entry(buffer_id)
2635                                    .or_default()
2636                                    .get_mut(&excerpt_id)
2637                                {
2638                                    excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines);
2639                                }
2640                                outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
2641                            })
2642                            .ok();
2643                    }),
2644                );
2645            }
2646        }
2647    }
2648
2649    fn is_singleton_active(&self, cx: &AppContext) -> bool {
2650        self.active_editor().map_or(false, |active_editor| {
2651            active_editor.read(cx).buffer().read(cx).is_singleton()
2652        })
2653    }
2654
2655    fn invalidate_outlines(&mut self, ids: &[ExcerptId]) {
2656        self.outline_fetch_tasks.clear();
2657        let mut ids = ids.iter().collect::<HashSet<_>>();
2658        for excerpts in self.excerpts.values_mut() {
2659            ids.retain(|id| {
2660                if let Some(excerpt) = excerpts.get_mut(id) {
2661                    excerpt.invalidate_outlines();
2662                    false
2663                } else {
2664                    true
2665                }
2666            });
2667            if ids.is_empty() {
2668                break;
2669            }
2670        }
2671    }
2672
2673    fn excerpt_fetch_ranges(
2674        &self,
2675        cx: &AppContext,
2676    ) -> HashMap<
2677        BufferId,
2678        (
2679            BufferSnapshot,
2680            HashMap<ExcerptId, ExcerptRange<language::Anchor>>,
2681        ),
2682    > {
2683        self.fs_entries
2684            .iter()
2685            .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| {
2686                match fs_entry {
2687                    FsEntry::File(_, _, buffer_id, file_excerpts)
2688                    | FsEntry::ExternalFile(buffer_id, file_excerpts) => {
2689                        let excerpts = self.excerpts.get(buffer_id);
2690                        for &file_excerpt in file_excerpts {
2691                            if let Some(excerpt) = excerpts
2692                                .and_then(|excerpts| excerpts.get(&file_excerpt))
2693                                .filter(|excerpt| excerpt.should_fetch_outlines())
2694                            {
2695                                match excerpts_to_fetch.entry(*buffer_id) {
2696                                    hash_map::Entry::Occupied(mut o) => {
2697                                        o.get_mut().1.insert(file_excerpt, excerpt.range.clone());
2698                                    }
2699                                    hash_map::Entry::Vacant(v) => {
2700                                        if let Some(buffer_snapshot) =
2701                                            self.buffer_snapshot_for_id(*buffer_id, cx)
2702                                        {
2703                                            v.insert((buffer_snapshot, HashMap::default()))
2704                                                .1
2705                                                .insert(file_excerpt, excerpt.range.clone());
2706                                        }
2707                                    }
2708                                }
2709                            }
2710                        }
2711                    }
2712                    FsEntry::Directory(..) => {}
2713                }
2714                excerpts_to_fetch
2715            })
2716    }
2717
2718    fn buffer_snapshot_for_id(
2719        &self,
2720        buffer_id: BufferId,
2721        cx: &AppContext,
2722    ) -> Option<BufferSnapshot> {
2723        let editor = self.active_editor()?;
2724        Some(
2725            editor
2726                .read(cx)
2727                .buffer()
2728                .read(cx)
2729                .buffer(buffer_id)?
2730                .read(cx)
2731                .snapshot(),
2732        )
2733    }
2734
2735    fn abs_path(&self, entry: &PanelEntry, cx: &AppContext) -> Option<PathBuf> {
2736        match entry {
2737            PanelEntry::Fs(
2738                FsEntry::File(_, _, buffer_id, _) | FsEntry::ExternalFile(buffer_id, _),
2739            ) => self
2740                .buffer_snapshot_for_id(*buffer_id, cx)
2741                .and_then(|buffer_snapshot| {
2742                    let file = File::from_dyn(buffer_snapshot.file())?;
2743                    file.worktree.read(cx).absolutize(&file.path).ok()
2744                }),
2745            PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self
2746                .project
2747                .read(cx)
2748                .worktree_for_id(*worktree_id, cx)?
2749                .read(cx)
2750                .absolutize(&entry.path)
2751                .ok(),
2752            PanelEntry::FoldedDirs(worktree_id, dirs) => dirs.last().and_then(|entry| {
2753                self.project
2754                    .read(cx)
2755                    .worktree_for_id(*worktree_id, cx)
2756                    .and_then(|worktree| worktree.read(cx).absolutize(&entry.path).ok())
2757            }),
2758            PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
2759        }
2760    }
2761
2762    fn relative_path(&self, entry: &FsEntry, cx: &AppContext) -> Option<Arc<Path>> {
2763        match entry {
2764            FsEntry::ExternalFile(buffer_id, _) => {
2765                let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?;
2766                Some(buffer_snapshot.file()?.path().clone())
2767            }
2768            FsEntry::Directory(_, entry) => Some(entry.path.clone()),
2769            FsEntry::File(_, entry, ..) => Some(entry.path.clone()),
2770        }
2771    }
2772
2773    fn update_cached_entries(
2774        &mut self,
2775        debounce: Option<Duration>,
2776        cx: &mut ViewContext<OutlinePanel>,
2777    ) {
2778        if !self.active {
2779            return;
2780        }
2781
2782        let is_singleton = self.is_singleton_active(cx);
2783        let query = self.query(cx);
2784        self.cached_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2785            if let Some(debounce) = debounce {
2786                cx.background_executor().timer(debounce).await;
2787            }
2788            let Some(new_cached_entries) = outline_panel
2789                .update(&mut cx, |outline_panel, cx| {
2790                    outline_panel.generate_cached_entries(is_singleton, query, cx)
2791                })
2792                .ok()
2793            else {
2794                return;
2795            };
2796            let new_cached_entries = new_cached_entries.await;
2797            outline_panel
2798                .update(&mut cx, |outline_panel, cx| {
2799                    outline_panel.cached_entries = new_cached_entries;
2800                    if outline_panel.selected_entry.is_invalidated() {
2801                        if let Some(new_selected_entry) =
2802                            outline_panel.active_editor().and_then(|active_editor| {
2803                                outline_panel.location_for_editor_selection(&active_editor, cx)
2804                            })
2805                        {
2806                            outline_panel.select_entry(new_selected_entry, false, cx);
2807                        }
2808                    }
2809
2810                    outline_panel.autoscroll(cx);
2811                    cx.notify();
2812                })
2813                .ok();
2814        });
2815    }
2816
2817    fn generate_cached_entries(
2818        &self,
2819        is_singleton: bool,
2820        query: Option<String>,
2821        cx: &mut ViewContext<'_, Self>,
2822    ) -> Task<Vec<CachedEntry>> {
2823        let project = self.project.clone();
2824        cx.spawn(|outline_panel, mut cx| async move {
2825            let mut entries = Vec::new();
2826            let mut match_candidates = Vec::new();
2827
2828            let Ok(()) = outline_panel.update(&mut cx, |outline_panel, cx| {
2829                let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2830                let mut folded_dirs_entry = None::<(usize, WorktreeId, Vec<Entry>)>;
2831                let track_matches = query.is_some();
2832
2833                #[derive(Debug)]
2834                struct ParentStats {
2835                    path: Arc<Path>,
2836                    folded: bool,
2837                    expanded: bool,
2838                    depth: usize,
2839                }
2840                let mut parent_dirs = Vec::<ParentStats>::new();
2841                for entry in outline_panel.fs_entries.clone() {
2842                    let is_expanded = outline_panel.is_expanded(&entry);
2843                    let (depth, should_add) = match &entry {
2844                        FsEntry::Directory(worktree_id, dir_entry) => {
2845                            let mut should_add = true;
2846                            let is_root = project
2847                                .read(cx)
2848                                .worktree_for_id(*worktree_id, cx)
2849                                .map_or(false, |worktree| {
2850                                    worktree.read(cx).root_entry() == Some(dir_entry)
2851                                });
2852                            let folded = auto_fold_dirs
2853                                && !is_root
2854                                && outline_panel
2855                                    .unfolded_dirs
2856                                    .get(worktree_id)
2857                                    .map_or(true, |unfolded_dirs| {
2858                                        !unfolded_dirs.contains(&dir_entry.id)
2859                                    });
2860                            let fs_depth = outline_panel
2861                                .fs_entries_depth
2862                                .get(&(*worktree_id, dir_entry.id))
2863                                .copied()
2864                                .unwrap_or(0);
2865                            while let Some(parent) = parent_dirs.last() {
2866                                if dir_entry.path.starts_with(&parent.path) {
2867                                    break;
2868                                }
2869                                parent_dirs.pop();
2870                            }
2871                            let auto_fold = match parent_dirs.last() {
2872                                Some(parent) => {
2873                                    parent.folded
2874                                        && Some(parent.path.as_ref()) == dir_entry.path.parent()
2875                                        && outline_panel
2876                                            .fs_children_count
2877                                            .get(worktree_id)
2878                                            .and_then(|entries| entries.get(&dir_entry.path))
2879                                            .copied()
2880                                            .unwrap_or_default()
2881                                            .may_be_fold_part()
2882                                }
2883                                None => false,
2884                            };
2885                            let folded = folded || auto_fold;
2886                            let (depth, parent_expanded, parent_folded) = match parent_dirs.last() {
2887                                Some(parent) => {
2888                                    let parent_folded = parent.folded;
2889                                    let parent_expanded = parent.expanded;
2890                                    let new_depth = if parent_folded {
2891                                        parent.depth
2892                                    } else {
2893                                        parent.depth + 1
2894                                    };
2895                                    parent_dirs.push(ParentStats {
2896                                        path: dir_entry.path.clone(),
2897                                        folded,
2898                                        expanded: parent_expanded && is_expanded,
2899                                        depth: new_depth,
2900                                    });
2901                                    (new_depth, parent_expanded, parent_folded)
2902                                }
2903                                None => {
2904                                    parent_dirs.push(ParentStats {
2905                                        path: dir_entry.path.clone(),
2906                                        folded,
2907                                        expanded: is_expanded,
2908                                        depth: fs_depth,
2909                                    });
2910                                    (fs_depth, true, false)
2911                                }
2912                            };
2913
2914                            if let Some((folded_depth, folded_worktree_id, mut folded_dirs)) =
2915                                folded_dirs_entry.take()
2916                            {
2917                                if folded
2918                                    && worktree_id == &folded_worktree_id
2919                                    && dir_entry.path.parent()
2920                                        == folded_dirs.last().map(|entry| entry.path.as_ref())
2921                                {
2922                                    folded_dirs.push(dir_entry.clone());
2923                                    folded_dirs_entry =
2924                                        Some((folded_depth, folded_worktree_id, folded_dirs))
2925                                } else {
2926                                    if !is_singleton {
2927                                        let start_of_collapsed_dir_sequence = !parent_expanded
2928                                            && parent_dirs
2929                                                .iter()
2930                                                .rev()
2931                                                .nth(folded_dirs.len() + 1)
2932                                                .map_or(true, |parent| parent.expanded);
2933                                        if start_of_collapsed_dir_sequence
2934                                            || parent_expanded
2935                                            || query.is_some()
2936                                        {
2937                                            if parent_folded {
2938                                                folded_dirs.push(dir_entry.clone());
2939                                                should_add = false;
2940                                            }
2941                                            let new_folded_dirs = PanelEntry::FoldedDirs(
2942                                                folded_worktree_id,
2943                                                folded_dirs,
2944                                            );
2945                                            outline_panel.push_entry(
2946                                                &mut entries,
2947                                                &mut match_candidates,
2948                                                track_matches,
2949                                                new_folded_dirs,
2950                                                folded_depth,
2951                                                cx,
2952                                            );
2953                                        }
2954                                    }
2955
2956                                    folded_dirs_entry = if parent_folded {
2957                                        None
2958                                    } else {
2959                                        Some((depth, *worktree_id, vec![dir_entry.clone()]))
2960                                    };
2961                                }
2962                            } else if folded {
2963                                folded_dirs_entry =
2964                                    Some((depth, *worktree_id, vec![dir_entry.clone()]));
2965                            }
2966
2967                            let should_add =
2968                                should_add && parent_expanded && folded_dirs_entry.is_none();
2969                            (depth, should_add)
2970                        }
2971                        FsEntry::ExternalFile(..) => {
2972                            if let Some((folded_depth, worktree_id, folded_dirs)) =
2973                                folded_dirs_entry.take()
2974                            {
2975                                let parent_expanded = parent_dirs
2976                                    .iter()
2977                                    .rev()
2978                                    .find(|parent| {
2979                                        folded_dirs.iter().all(|entry| entry.path != parent.path)
2980                                    })
2981                                    .map_or(true, |parent| parent.expanded);
2982                                if !is_singleton && (parent_expanded || query.is_some()) {
2983                                    outline_panel.push_entry(
2984                                        &mut entries,
2985                                        &mut match_candidates,
2986                                        track_matches,
2987                                        PanelEntry::FoldedDirs(worktree_id, folded_dirs),
2988                                        folded_depth,
2989                                        cx,
2990                                    );
2991                                }
2992                            }
2993                            parent_dirs.clear();
2994                            (0, true)
2995                        }
2996                        FsEntry::File(worktree_id, file_entry, ..) => {
2997                            if let Some((folded_depth, worktree_id, folded_dirs)) =
2998                                folded_dirs_entry.take()
2999                            {
3000                                let parent_expanded = parent_dirs
3001                                    .iter()
3002                                    .rev()
3003                                    .find(|parent| {
3004                                        folded_dirs.iter().all(|entry| entry.path != parent.path)
3005                                    })
3006                                    .map_or(true, |parent| parent.expanded);
3007                                if !is_singleton && (parent_expanded || query.is_some()) {
3008                                    outline_panel.push_entry(
3009                                        &mut entries,
3010                                        &mut match_candidates,
3011                                        track_matches,
3012                                        PanelEntry::FoldedDirs(worktree_id, folded_dirs),
3013                                        folded_depth,
3014                                        cx,
3015                                    );
3016                                }
3017                            }
3018
3019                            let fs_depth = outline_panel
3020                                .fs_entries_depth
3021                                .get(&(*worktree_id, file_entry.id))
3022                                .copied()
3023                                .unwrap_or(0);
3024                            while let Some(parent) = parent_dirs.last() {
3025                                if file_entry.path.starts_with(&parent.path) {
3026                                    break;
3027                                }
3028                                parent_dirs.pop();
3029                            }
3030                            let (depth, should_add) = match parent_dirs.last() {
3031                                Some(parent) => {
3032                                    let new_depth = parent.depth + 1;
3033                                    (new_depth, parent.expanded)
3034                                }
3035                                None => (fs_depth, true),
3036                            };
3037                            (depth, should_add)
3038                        }
3039                    };
3040
3041                    if !is_singleton
3042                        && (should_add || (query.is_some() && folded_dirs_entry.is_none()))
3043                    {
3044                        outline_panel.push_entry(
3045                            &mut entries,
3046                            &mut match_candidates,
3047                            track_matches,
3048                            PanelEntry::Fs(entry.clone()),
3049                            depth,
3050                            cx,
3051                        );
3052                    }
3053
3054                    match outline_panel.mode {
3055                        ItemsDisplayMode::Search(_) => {
3056                            if is_singleton || query.is_some() || (should_add && is_expanded) {
3057                                outline_panel.add_search_entries(
3058                                    &mut entries,
3059                                    &mut match_candidates,
3060                                    entry.clone(),
3061                                    depth,
3062                                    query.clone(),
3063                                    is_singleton,
3064                                    cx,
3065                                );
3066                            }
3067                        }
3068                        ItemsDisplayMode::Outline => {
3069                            let excerpts_to_consider =
3070                                if is_singleton || query.is_some() || (should_add && is_expanded) {
3071                                    match &entry {
3072                                        FsEntry::File(_, _, buffer_id, entry_excerpts) => {
3073                                            Some((*buffer_id, entry_excerpts))
3074                                        }
3075                                        FsEntry::ExternalFile(buffer_id, entry_excerpts) => {
3076                                            Some((*buffer_id, entry_excerpts))
3077                                        }
3078                                        _ => None,
3079                                    }
3080                                } else {
3081                                    None
3082                                };
3083                            if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider {
3084                                outline_panel.add_excerpt_entries(
3085                                    buffer_id,
3086                                    entry_excerpts,
3087                                    depth,
3088                                    track_matches,
3089                                    is_singleton,
3090                                    query.as_deref(),
3091                                    &mut entries,
3092                                    &mut match_candidates,
3093                                    cx,
3094                                );
3095                            }
3096                        }
3097                    }
3098
3099                    if is_singleton
3100                        && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..))
3101                        && !entries.iter().any(|item| {
3102                            matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_))
3103                        })
3104                    {
3105                        outline_panel.push_entry(
3106                            &mut entries,
3107                            &mut match_candidates,
3108                            track_matches,
3109                            PanelEntry::Fs(entry.clone()),
3110                            0,
3111                            cx,
3112                        );
3113                    }
3114                }
3115
3116                if let Some((folded_depth, worktree_id, folded_dirs)) = folded_dirs_entry.take() {
3117                    let parent_expanded = parent_dirs
3118                        .iter()
3119                        .rev()
3120                        .find(|parent| folded_dirs.iter().all(|entry| entry.path != parent.path))
3121                        .map_or(true, |parent| parent.expanded);
3122                    if parent_expanded || query.is_some() {
3123                        outline_panel.push_entry(
3124                            &mut entries,
3125                            &mut match_candidates,
3126                            track_matches,
3127                            PanelEntry::FoldedDirs(worktree_id, folded_dirs),
3128                            folded_depth,
3129                            cx,
3130                        );
3131                    }
3132                }
3133            }) else {
3134                return Vec::new();
3135            };
3136
3137            let Some(query) = query else {
3138                return entries;
3139            };
3140
3141            let mut matched_ids = match_strings(
3142                &match_candidates,
3143                &query,
3144                true,
3145                usize::MAX,
3146                &AtomicBool::default(),
3147                cx.background_executor().clone(),
3148            )
3149            .await
3150            .into_iter()
3151            .map(|string_match| (string_match.candidate_id, string_match))
3152            .collect::<HashMap<_, _>>();
3153
3154            let mut id = 0;
3155            entries.retain_mut(|cached_entry| {
3156                let retain = match matched_ids.remove(&id) {
3157                    Some(string_match) => {
3158                        cached_entry.string_match = Some(string_match);
3159                        true
3160                    }
3161                    None => false,
3162                };
3163                id += 1;
3164                retain
3165            });
3166
3167            entries
3168        })
3169    }
3170
3171    #[allow(clippy::too_many_arguments)]
3172    fn push_entry(
3173        &self,
3174        entries: &mut Vec<CachedEntry>,
3175        match_candidates: &mut Vec<StringMatchCandidate>,
3176        track_matches: bool,
3177        entry: PanelEntry,
3178        depth: usize,
3179        cx: &mut WindowContext,
3180    ) {
3181        let entry = if let PanelEntry::FoldedDirs(worktree_id, entries) = &entry {
3182            match entries.len() {
3183                0 => {
3184                    debug_panic!("Empty folded dirs receiver");
3185                    return;
3186                }
3187                1 => PanelEntry::Fs(FsEntry::Directory(*worktree_id, entries[0].clone())),
3188                _ => entry,
3189            }
3190        } else {
3191            entry
3192        };
3193
3194        if track_matches {
3195            let id = entries.len();
3196            match &entry {
3197                PanelEntry::Fs(fs_entry) => {
3198                    if let Some(file_name) =
3199                        self.relative_path(fs_entry, cx).as_deref().map(file_name)
3200                    {
3201                        match_candidates.push(StringMatchCandidate {
3202                            id,
3203                            string: file_name.to_string(),
3204                            char_bag: file_name.chars().collect(),
3205                        });
3206                    }
3207                }
3208                PanelEntry::FoldedDirs(worktree_id, entries) => {
3209                    let dir_names = self.dir_names_string(entries, *worktree_id, cx);
3210                    {
3211                        match_candidates.push(StringMatchCandidate {
3212                            id,
3213                            string: dir_names.clone(),
3214                            char_bag: dir_names.chars().collect(),
3215                        });
3216                    }
3217                }
3218                PanelEntry::Outline(outline_entry) => match outline_entry {
3219                    OutlineEntry::Outline(_, _, outline) => {
3220                        match_candidates.push(StringMatchCandidate {
3221                            id,
3222                            string: outline.text.clone(),
3223                            char_bag: outline.text.chars().collect(),
3224                        });
3225                    }
3226                    OutlineEntry::Excerpt(..) => {}
3227                },
3228                PanelEntry::Search(new_search_entry) => {
3229                    match_candidates.push(StringMatchCandidate {
3230                        id,
3231                        char_bag: new_search_entry.render_data.context_text.chars().collect(),
3232                        string: new_search_entry.render_data.context_text.clone(),
3233                    });
3234                }
3235            }
3236        }
3237        entries.push(CachedEntry {
3238            depth,
3239            entry,
3240            string_match: None,
3241        });
3242    }
3243
3244    fn dir_names_string(
3245        &self,
3246        entries: &[Entry],
3247        worktree_id: WorktreeId,
3248        cx: &AppContext,
3249    ) -> String {
3250        let dir_names_segment = entries
3251            .iter()
3252            .map(|entry| self.entry_name(&worktree_id, entry, cx))
3253            .collect::<PathBuf>();
3254        dir_names_segment.to_string_lossy().to_string()
3255    }
3256
3257    fn query(&self, cx: &AppContext) -> Option<String> {
3258        let query = self.filter_editor.read(cx).text(cx);
3259        if query.trim().is_empty() {
3260            None
3261        } else {
3262            Some(query)
3263        }
3264    }
3265
3266    fn is_expanded(&self, entry: &FsEntry) -> bool {
3267        let entry_to_check = match entry {
3268            FsEntry::ExternalFile(buffer_id, _) => CollapsedEntry::ExternalFile(*buffer_id),
3269            FsEntry::File(worktree_id, _, buffer_id, _) => {
3270                CollapsedEntry::File(*worktree_id, *buffer_id)
3271            }
3272            FsEntry::Directory(worktree_id, entry) => CollapsedEntry::Dir(*worktree_id, entry.id),
3273        };
3274        !self.collapsed_entries.contains(&entry_to_check)
3275    }
3276
3277    fn update_non_fs_items(&mut self, cx: &mut ViewContext<OutlinePanel>) {
3278        if !self.active {
3279            return;
3280        }
3281
3282        self.update_search_matches(cx);
3283        self.fetch_outdated_outlines(cx);
3284        self.autoscroll(cx);
3285    }
3286
3287    fn update_search_matches(&mut self, cx: &mut ViewContext<OutlinePanel>) {
3288        if !self.active {
3289            return;
3290        }
3291
3292        let project_search = self
3293            .active_item()
3294            .and_then(|item| item.downcast::<ProjectSearchView>());
3295        let project_search_matches = project_search
3296            .as_ref()
3297            .map(|project_search| project_search.read(cx).get_matches(cx))
3298            .unwrap_or_default();
3299
3300        let buffer_search = self
3301            .active_item()
3302            .as_deref()
3303            .and_then(|active_item| {
3304                self.workspace
3305                    .upgrade()
3306                    .and_then(|workspace| workspace.read(cx).pane_for(active_item))
3307            })
3308            .and_then(|pane| {
3309                pane.read(cx)
3310                    .toolbar()
3311                    .read(cx)
3312                    .item_of_type::<BufferSearchBar>()
3313            });
3314        let buffer_search_matches = self
3315            .active_editor()
3316            .map(|active_editor| active_editor.update(cx, |editor, cx| editor.get_matches(cx)))
3317            .unwrap_or_default();
3318
3319        let mut update_cached_entries = false;
3320        if buffer_search_matches.is_empty() && project_search_matches.is_empty() {
3321            if matches!(self.mode, ItemsDisplayMode::Search(_)) {
3322                self.mode = ItemsDisplayMode::Outline;
3323                update_cached_entries = true;
3324            }
3325        } else {
3326            let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() {
3327                (
3328                    SearchKind::Project,
3329                    project_search_matches,
3330                    project_search
3331                        .map(|project_search| project_search.read(cx).search_query_text(cx))
3332                        .unwrap_or_default(),
3333                )
3334            } else {
3335                (
3336                    SearchKind::Buffer,
3337                    buffer_search_matches,
3338                    buffer_search
3339                        .map(|buffer_search| buffer_search.read(cx).query(cx))
3340                        .unwrap_or_default(),
3341                )
3342            };
3343
3344            update_cached_entries = match &self.mode {
3345                ItemsDisplayMode::Search(current_search_state) => {
3346                    current_search_state.query != new_search_query
3347                        || current_search_state.kind != kind
3348                        || current_search_state.matches.is_empty()
3349                        || current_search_state.matches.iter().enumerate().any(
3350                            |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range),
3351                        )
3352                }
3353                ItemsDisplayMode::Outline => true,
3354            };
3355            self.mode = ItemsDisplayMode::Search(SearchState::new(
3356                kind,
3357                new_search_query,
3358                new_search_matches,
3359                cx.theme().syntax().clone(),
3360                cx,
3361            ));
3362        }
3363        if update_cached_entries {
3364            self.selected_entry.invalidate();
3365            self.update_cached_entries(Some(UPDATE_DEBOUNCE), cx);
3366        }
3367    }
3368
3369    #[allow(clippy::too_many_arguments)]
3370    fn add_excerpt_entries(
3371        &self,
3372        buffer_id: BufferId,
3373        entries_to_add: &[ExcerptId],
3374        parent_depth: usize,
3375        track_matches: bool,
3376        is_singleton: bool,
3377        query: Option<&str>,
3378        entries: &mut Vec<CachedEntry>,
3379        match_candidates: &mut Vec<StringMatchCandidate>,
3380        cx: &mut ViewContext<Self>,
3381    ) {
3382        if let Some(excerpts) = self.excerpts.get(&buffer_id) {
3383            for &excerpt_id in entries_to_add {
3384                let Some(excerpt) = excerpts.get(&excerpt_id) else {
3385                    continue;
3386                };
3387                let excerpt_depth = parent_depth + 1;
3388                self.push_entry(
3389                    entries,
3390                    match_candidates,
3391                    track_matches,
3392                    PanelEntry::Outline(OutlineEntry::Excerpt(
3393                        buffer_id,
3394                        excerpt_id,
3395                        excerpt.range.clone(),
3396                    )),
3397                    excerpt_depth,
3398                    cx,
3399                );
3400
3401                let mut outline_base_depth = excerpt_depth + 1;
3402                if is_singleton {
3403                    outline_base_depth = 0;
3404                    entries.clear();
3405                    match_candidates.clear();
3406                } else if query.is_none()
3407                    && self
3408                        .collapsed_entries
3409                        .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id))
3410                {
3411                    continue;
3412                }
3413
3414                for outline in excerpt.iter_outlines() {
3415                    self.push_entry(
3416                        entries,
3417                        match_candidates,
3418                        track_matches,
3419                        PanelEntry::Outline(OutlineEntry::Outline(
3420                            buffer_id,
3421                            excerpt_id,
3422                            outline.clone(),
3423                        )),
3424                        outline_base_depth + outline.depth,
3425                        cx,
3426                    );
3427                }
3428            }
3429        }
3430    }
3431
3432    #[allow(clippy::too_many_arguments)]
3433    fn add_search_entries(
3434        &mut self,
3435        entries: &mut Vec<CachedEntry>,
3436        match_candidates: &mut Vec<StringMatchCandidate>,
3437        parent_entry: FsEntry,
3438        parent_depth: usize,
3439        filter_query: Option<String>,
3440        is_singleton: bool,
3441        cx: &mut ViewContext<Self>,
3442    ) {
3443        let Some(active_editor) = self.active_editor() else {
3444            return;
3445        };
3446        let ItemsDisplayMode::Search(search_state) = &mut self.mode else {
3447            return;
3448        };
3449
3450        let kind = search_state.kind;
3451        let related_excerpts = match &parent_entry {
3452            FsEntry::Directory(_, _) => return,
3453            FsEntry::ExternalFile(_, excerpts) => excerpts,
3454            FsEntry::File(_, _, _, excerpts) => excerpts,
3455        }
3456        .iter()
3457        .copied()
3458        .collect::<HashSet<_>>();
3459
3460        let depth = if is_singleton { 0 } else { parent_depth + 1 };
3461        let multi_buffer_snapshot = active_editor.read(cx).buffer().read(cx).snapshot(cx);
3462        let new_search_matches = search_state.matches.iter().filter(|(match_range, _)| {
3463            related_excerpts.contains(&match_range.start.excerpt_id)
3464                || related_excerpts.contains(&match_range.end.excerpt_id)
3465        });
3466
3467        let previous_search_matches = entries
3468            .iter()
3469            .skip_while(|entry| {
3470                if let PanelEntry::Fs(entry) = &entry.entry {
3471                    entry == &parent_entry
3472                } else {
3473                    true
3474                }
3475            })
3476            .take_while(|entry| matches!(entry.entry, PanelEntry::Search(_)))
3477            .fold(
3478                HashMap::default(),
3479                |mut previous_matches, previous_entry| match &previous_entry.entry {
3480                    PanelEntry::Search(search_entry) => {
3481                        previous_matches.insert(
3482                            (search_entry.kind, &search_entry.match_range),
3483                            &search_entry.render_data,
3484                        );
3485                        previous_matches
3486                    }
3487                    _ => previous_matches,
3488                },
3489            );
3490
3491        let new_search_entries = new_search_matches
3492            .map(|(match_range, search_data)| {
3493                let previous_search_data =
3494                    previous_search_matches.get(&(kind, match_range)).copied();
3495                let render_data = search_data
3496                    .get()
3497                    .or(previous_search_data)
3498                    .unwrap_or_else(|| {
3499                        search_data.get_or_init(|| {
3500                            Arc::new(SearchData::new(match_range, &multi_buffer_snapshot))
3501                        })
3502                    });
3503                if let (Some(previous_highlights), None) = (
3504                    previous_search_data.and_then(|data| data.highlights_data.get()),
3505                    render_data.highlights_data.get(),
3506                ) {
3507                    render_data
3508                        .highlights_data
3509                        .set(previous_highlights.clone())
3510                        .ok();
3511                }
3512
3513                SearchEntry {
3514                    match_range: match_range.clone(),
3515                    kind,
3516                    render_data: Arc::clone(render_data),
3517                }
3518            })
3519            .collect::<Vec<_>>();
3520        for new_search_entry in new_search_entries {
3521            self.push_entry(
3522                entries,
3523                match_candidates,
3524                filter_query.is_some(),
3525                PanelEntry::Search(new_search_entry),
3526                depth,
3527                cx,
3528            );
3529        }
3530    }
3531
3532    fn active_editor(&self) -> Option<View<Editor>> {
3533        self.active_item.as_ref()?.active_editor.upgrade()
3534    }
3535
3536    fn active_item(&self) -> Option<Box<dyn ItemHandle>> {
3537        self.active_item.as_ref()?.item_handle.upgrade()
3538    }
3539
3540    fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool {
3541        self.active_item().map_or(true, |active_item| {
3542            !self.pinned && active_item.item_id() != new_active_item.item_id()
3543        })
3544    }
3545
3546    pub fn toggle_active_editor_pin(
3547        &mut self,
3548        _: &ToggleActiveEditorPin,
3549        cx: &mut ViewContext<Self>,
3550    ) {
3551        self.pinned = !self.pinned;
3552        if !self.pinned {
3553            if let Some((active_item, active_editor)) = self
3554                .workspace
3555                .upgrade()
3556                .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
3557            {
3558                if self.should_replace_active_item(active_item.as_ref()) {
3559                    self.replace_active_editor(active_item, active_editor, cx);
3560                }
3561            }
3562        }
3563
3564        cx.notify();
3565    }
3566
3567    fn selected_entry(&self) -> Option<&PanelEntry> {
3568        match &self.selected_entry {
3569            SelectedEntry::Invalidated(entry) => entry.as_ref(),
3570            SelectedEntry::Valid(entry, _) => Some(entry),
3571            SelectedEntry::None => None,
3572        }
3573    }
3574
3575    fn select_entry(&mut self, entry: PanelEntry, focus: bool, cx: &mut ViewContext<Self>) {
3576        if focus {
3577            self.focus_handle.focus(cx);
3578        }
3579        let ix = self
3580            .cached_entries
3581            .iter()
3582            .enumerate()
3583            .find(|(_, cached_entry)| &cached_entry.entry == &entry)
3584            .map(|(i, _)| i)
3585            .unwrap_or_default();
3586
3587        self.selected_entry = SelectedEntry::Valid(entry, ix);
3588
3589        self.autoscroll(cx);
3590        cx.notify();
3591    }
3592}
3593
3594fn workspace_active_editor(
3595    workspace: &Workspace,
3596    cx: &AppContext,
3597) -> Option<(Box<dyn ItemHandle>, View<Editor>)> {
3598    let active_item = workspace.active_item(cx)?;
3599    let active_editor = active_item
3600        .act_as::<Editor>(cx)
3601        .filter(|editor| editor.read(cx).mode() == EditorMode::Full)?;
3602    Some((active_item, active_editor))
3603}
3604
3605fn back_to_common_visited_parent(
3606    visited_dirs: &mut Vec<(ProjectEntryId, Arc<Path>)>,
3607    worktree_id: &WorktreeId,
3608    new_entry: &Entry,
3609) -> Option<(WorktreeId, ProjectEntryId)> {
3610    while let Some((visited_dir_id, visited_path)) = visited_dirs.last() {
3611        match new_entry.path.parent() {
3612            Some(parent_path) => {
3613                if parent_path == visited_path.as_ref() {
3614                    return Some((*worktree_id, *visited_dir_id));
3615                }
3616            }
3617            None => {
3618                break;
3619            }
3620        }
3621        visited_dirs.pop();
3622    }
3623    None
3624}
3625
3626fn file_name(path: &Path) -> String {
3627    let mut current_path = path;
3628    loop {
3629        if let Some(file_name) = current_path.file_name() {
3630            return file_name.to_string_lossy().into_owned();
3631        }
3632        match current_path.parent() {
3633            Some(parent) => current_path = parent,
3634            None => return path.to_string_lossy().into_owned(),
3635        }
3636    }
3637}
3638
3639impl Panel for OutlinePanel {
3640    fn persistent_name() -> &'static str {
3641        "Outline Panel"
3642    }
3643
3644    fn position(&self, cx: &WindowContext) -> DockPosition {
3645        match OutlinePanelSettings::get_global(cx).dock {
3646            OutlinePanelDockPosition::Left => DockPosition::Left,
3647            OutlinePanelDockPosition::Right => DockPosition::Right,
3648        }
3649    }
3650
3651    fn position_is_valid(&self, position: DockPosition) -> bool {
3652        matches!(position, DockPosition::Left | DockPosition::Right)
3653    }
3654
3655    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
3656        settings::update_settings_file::<OutlinePanelSettings>(
3657            self.fs.clone(),
3658            cx,
3659            move |settings, _| {
3660                let dock = match position {
3661                    DockPosition::Left | DockPosition::Bottom => OutlinePanelDockPosition::Left,
3662                    DockPosition::Right => OutlinePanelDockPosition::Right,
3663                };
3664                settings.dock = Some(dock);
3665            },
3666        );
3667    }
3668
3669    fn size(&self, cx: &WindowContext) -> Pixels {
3670        self.width
3671            .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
3672    }
3673
3674    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
3675        self.width = size;
3676        self.serialize(cx);
3677        cx.notify();
3678    }
3679
3680    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
3681        OutlinePanelSettings::get_global(cx)
3682            .button
3683            .then_some(IconName::ListTree)
3684    }
3685
3686    fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
3687        Some("Outline Panel")
3688    }
3689
3690    fn toggle_action(&self) -> Box<dyn Action> {
3691        Box::new(ToggleFocus)
3692    }
3693
3694    fn starts_open(&self, _: &WindowContext) -> bool {
3695        self.active
3696    }
3697
3698    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
3699        cx.spawn(|outline_panel, mut cx| async move {
3700            outline_panel
3701                .update(&mut cx, |outline_panel, cx| {
3702                    let old_active = outline_panel.active;
3703                    outline_panel.active = active;
3704                    if active && old_active != active {
3705                        if let Some((active_item, active_editor)) = outline_panel
3706                            .workspace
3707                            .upgrade()
3708                            .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx))
3709                        {
3710                            if outline_panel.should_replace_active_item(active_item.as_ref()) {
3711                                outline_panel.replace_active_editor(active_item, active_editor, cx);
3712                            } else {
3713                                outline_panel.update_fs_entries(
3714                                    &active_editor,
3715                                    HashSet::default(),
3716                                    None,
3717                                    cx,
3718                                )
3719                            }
3720                        } else if !outline_panel.pinned {
3721                            outline_panel.clear_previous(cx);
3722                        }
3723                    }
3724                    outline_panel.serialize(cx);
3725                })
3726                .ok();
3727        })
3728        .detach()
3729    }
3730}
3731
3732impl FocusableView for OutlinePanel {
3733    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3734        self.filter_editor.focus_handle(cx).clone()
3735    }
3736}
3737
3738impl EventEmitter<Event> for OutlinePanel {}
3739
3740impl EventEmitter<PanelEvent> for OutlinePanel {}
3741
3742impl Render for OutlinePanel {
3743    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3744        let project = self.project.read(cx);
3745        let query = self.query(cx);
3746        let pinned = self.pinned;
3747        let settings = OutlinePanelSettings::get_global(cx);
3748        let indent_size = settings.indent_size;
3749        let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always;
3750
3751        let outline_panel = v_flex()
3752            .id("outline-panel")
3753            .size_full()
3754            .relative()
3755            .key_context(self.dispatch_context(cx))
3756            .on_action(cx.listener(Self::open))
3757            .on_action(cx.listener(Self::cancel))
3758            .on_action(cx.listener(Self::select_next))
3759            .on_action(cx.listener(Self::select_prev))
3760            .on_action(cx.listener(Self::select_first))
3761            .on_action(cx.listener(Self::select_last))
3762            .on_action(cx.listener(Self::select_parent))
3763            .on_action(cx.listener(Self::expand_selected_entry))
3764            .on_action(cx.listener(Self::collapse_selected_entry))
3765            .on_action(cx.listener(Self::expand_all_entries))
3766            .on_action(cx.listener(Self::collapse_all_entries))
3767            .on_action(cx.listener(Self::copy_path))
3768            .on_action(cx.listener(Self::copy_relative_path))
3769            .on_action(cx.listener(Self::toggle_active_editor_pin))
3770            .on_action(cx.listener(Self::unfold_directory))
3771            .on_action(cx.listener(Self::fold_directory))
3772            .when(project.is_local(), |el| {
3773                el.on_action(cx.listener(Self::reveal_in_finder))
3774            })
3775            .when(project.is_local() || project.is_via_ssh(), |el| {
3776                el.on_action(cx.listener(Self::open_in_terminal))
3777            })
3778            .on_mouse_down(
3779                MouseButton::Right,
3780                cx.listener(move |outline_panel, event: &MouseDownEvent, cx| {
3781                    if let Some(entry) = outline_panel.selected_entry().cloned() {
3782                        outline_panel.deploy_context_menu(event.position, entry, cx)
3783                    } else if let Some(entry) = outline_panel.fs_entries.first().cloned() {
3784                        outline_panel.deploy_context_menu(event.position, PanelEntry::Fs(entry), cx)
3785                    }
3786                }),
3787            )
3788            .track_focus(&self.focus_handle(cx));
3789
3790        if self.cached_entries.is_empty() {
3791            let header = if self.updating_fs_entries {
3792                "Loading outlines"
3793            } else if query.is_some() {
3794                "No matches for query"
3795            } else {
3796                "No outlines available"
3797            };
3798
3799            outline_panel.child(
3800                v_flex()
3801                    .justify_center()
3802                    .size_full()
3803                    .child(h_flex().justify_center().child(Label::new(header)))
3804                    .when_some(query.clone(), |panel, query| {
3805                        panel.child(h_flex().justify_center().child(Label::new(query)))
3806                    })
3807                    .child(
3808                        h_flex()
3809                            .pt(Spacing::Small.rems(cx))
3810                            .justify_center()
3811                            .child({
3812                                let keystroke = match self.position(cx) {
3813                                    DockPosition::Left => {
3814                                        cx.keystroke_text_for(&workspace::ToggleLeftDock)
3815                                    }
3816                                    DockPosition::Bottom => {
3817                                        cx.keystroke_text_for(&workspace::ToggleBottomDock)
3818                                    }
3819                                    DockPosition::Right => {
3820                                        cx.keystroke_text_for(&workspace::ToggleRightDock)
3821                                    }
3822                                };
3823                                Label::new(format!("Toggle this panel with {keystroke}"))
3824                            }),
3825                    ),
3826            )
3827        } else {
3828            let search_query = match &self.mode {
3829                ItemsDisplayMode::Search(search_query) => Some(search_query),
3830                _ => None,
3831            };
3832            outline_panel
3833                .when_some(search_query, |outline_panel, search_state| {
3834                    outline_panel.child(
3835                        div()
3836                            .mx_2()
3837                            .child(
3838                                Label::new(format!("Searching: '{}'", search_state.query))
3839                                    .color(Color::Muted),
3840                            )
3841                            .child(horizontal_separator(cx)),
3842                    )
3843                })
3844                .child({
3845                    let items_len = self.cached_entries.len();
3846                    let multi_buffer_snapshot = self
3847                        .active_editor()
3848                        .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx));
3849                    uniform_list(cx.view().clone(), "entries", items_len, {
3850                        move |outline_panel, range, cx| {
3851                            let entries = outline_panel.cached_entries.get(range);
3852                            entries
3853                                .map(|entries| entries.to_vec())
3854                                .unwrap_or_default()
3855                                .into_iter()
3856                                .filter_map(|cached_entry| match cached_entry.entry {
3857                                    PanelEntry::Fs(entry) => Some(outline_panel.render_entry(
3858                                        &entry,
3859                                        cached_entry.depth,
3860                                        cached_entry.string_match.as_ref(),
3861                                        cx,
3862                                    )),
3863                                    PanelEntry::FoldedDirs(worktree_id, entries) => {
3864                                        Some(outline_panel.render_folded_dirs(
3865                                            worktree_id,
3866                                            &entries,
3867                                            cached_entry.depth,
3868                                            cached_entry.string_match.as_ref(),
3869                                            cx,
3870                                        ))
3871                                    }
3872                                    PanelEntry::Outline(OutlineEntry::Excerpt(
3873                                        buffer_id,
3874                                        excerpt_id,
3875                                        excerpt,
3876                                    )) => outline_panel.render_excerpt(
3877                                        buffer_id,
3878                                        excerpt_id,
3879                                        &excerpt,
3880                                        cached_entry.depth,
3881                                        cx,
3882                                    ),
3883                                    PanelEntry::Outline(OutlineEntry::Outline(
3884                                        buffer_id,
3885                                        excerpt_id,
3886                                        outline,
3887                                    )) => Some(outline_panel.render_outline(
3888                                        buffer_id,
3889                                        excerpt_id,
3890                                        &outline,
3891                                        cached_entry.depth,
3892                                        cached_entry.string_match.as_ref(),
3893                                        cx,
3894                                    )),
3895                                    PanelEntry::Search(SearchEntry {
3896                                        match_range,
3897                                        render_data,
3898                                        kind,
3899                                        ..
3900                                    }) => Some(outline_panel.render_search_match(
3901                                        multi_buffer_snapshot.as_ref(),
3902                                        &match_range,
3903                                        &render_data,
3904                                        kind,
3905                                        cached_entry.depth,
3906                                        cached_entry.string_match.as_ref(),
3907                                        cx,
3908                                    )),
3909                                })
3910                                .collect()
3911                        }
3912                    })
3913                    .size_full()
3914                    .track_scroll(self.scroll_handle.clone())
3915                    .when(show_indent_guides, |list| {
3916                        list.with_decoration(
3917                            ui::indent_guides(
3918                                cx.view().clone(),
3919                                px(indent_size),
3920                                IndentGuideColors::panel(cx),
3921                                |outline_panel, range, _| {
3922                                    let entries = outline_panel.cached_entries.get(range);
3923                                    if let Some(entries) = entries {
3924                                        entries.into_iter().map(|item| item.depth).collect()
3925                                    } else {
3926                                        smallvec::SmallVec::new()
3927                                    }
3928                                },
3929                            )
3930                            .with_render_fn(
3931                                cx.view().clone(),
3932                                move |outline_panel, params, _| {
3933                                    const LEFT_OFFSET: f32 = 14.;
3934
3935                                    let indent_size = params.indent_size;
3936                                    let item_height = params.item_height;
3937                                    let active_indent_guide_ix = find_active_indent_guide_ix(
3938                                        outline_panel,
3939                                        &params.indent_guides,
3940                                    );
3941
3942                                    params
3943                                        .indent_guides
3944                                        .into_iter()
3945                                        .enumerate()
3946                                        .map(|(ix, layout)| {
3947                                            let bounds = Bounds::new(
3948                                                point(
3949                                                    px(layout.offset.x as f32) * indent_size
3950                                                        + px(LEFT_OFFSET),
3951                                                    px(layout.offset.y as f32) * item_height,
3952                                                ),
3953                                                size(
3954                                                    px(1.),
3955                                                    px(layout.length as f32) * item_height,
3956                                                ),
3957                                            );
3958                                            ui::RenderedIndentGuide {
3959                                                bounds,
3960                                                layout,
3961                                                is_active: active_indent_guide_ix == Some(ix),
3962                                                hitbox: None,
3963                                            }
3964                                        })
3965                                        .collect()
3966                                },
3967                            ),
3968                        )
3969                    })
3970                })
3971        }
3972        .children(self.context_menu.as_ref().map(|(menu, position, _)| {
3973            deferred(
3974                anchored()
3975                    .position(*position)
3976                    .anchor(gpui::AnchorCorner::TopLeft)
3977                    .child(menu.clone()),
3978            )
3979            .with_priority(1)
3980        }))
3981        .child(
3982            v_flex().child(horizontal_separator(cx)).child(
3983                h_flex().p_2().child(self.filter_editor.clone()).child(
3984                    div().child(
3985                        IconButton::new(
3986                            "outline-panel-menu",
3987                            if pinned {
3988                                IconName::Unpin
3989                            } else {
3990                                IconName::Pin
3991                            },
3992                        )
3993                        .tooltip(move |cx| {
3994                            Tooltip::text(
3995                                if pinned {
3996                                    "Unpin Outline"
3997                                } else {
3998                                    "Pin Active Outline"
3999                                },
4000                                cx,
4001                            )
4002                        })
4003                        .shape(IconButtonShape::Square)
4004                        .on_click(cx.listener(|outline_panel, _, cx| {
4005                            outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, cx);
4006                        })),
4007                    ),
4008                ),
4009            ),
4010        )
4011    }
4012}
4013
4014fn find_active_indent_guide_ix(
4015    outline_panel: &OutlinePanel,
4016    candidates: &[IndentGuideLayout],
4017) -> Option<usize> {
4018    let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else {
4019        return None;
4020    };
4021    let target_depth = outline_panel
4022        .cached_entries
4023        .get(*target_ix)
4024        .map(|cached_entry| cached_entry.depth)?;
4025
4026    let (target_ix, target_depth) = if let Some(target_depth) = outline_panel
4027        .cached_entries
4028        .get(target_ix + 1)
4029        .filter(|cached_entry| cached_entry.depth > target_depth)
4030        .map(|entry| entry.depth)
4031    {
4032        (target_ix + 1, target_depth.saturating_sub(1))
4033    } else {
4034        (*target_ix, target_depth.saturating_sub(1))
4035    };
4036
4037    candidates
4038        .iter()
4039        .enumerate()
4040        .find(|(_, guide)| {
4041            guide.offset.y <= target_ix
4042                && target_ix < guide.offset.y + guide.length
4043                && guide.offset.x == target_depth
4044        })
4045        .map(|(ix, _)| ix)
4046}
4047
4048fn subscribe_for_editor_events(
4049    editor: &View<Editor>,
4050    cx: &mut ViewContext<OutlinePanel>,
4051) -> Subscription {
4052    let debounce = Some(UPDATE_DEBOUNCE);
4053    cx.subscribe(
4054        editor,
4055        move |outline_panel, editor, e: &EditorEvent, cx| match e {
4056            EditorEvent::SelectionsChanged { local: true } => {
4057                outline_panel.reveal_entry_for_selection(&editor, cx);
4058                cx.notify();
4059            }
4060            EditorEvent::ExcerptsAdded { excerpts, .. } => {
4061                outline_panel.update_fs_entries(
4062                    &editor,
4063                    excerpts.iter().map(|&(excerpt_id, _)| excerpt_id).collect(),
4064                    debounce,
4065                    cx,
4066                );
4067            }
4068            EditorEvent::ExcerptsRemoved { ids } => {
4069                let mut ids = ids.iter().collect::<HashSet<_>>();
4070                for excerpts in outline_panel.excerpts.values_mut() {
4071                    excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id));
4072                    if ids.is_empty() {
4073                        break;
4074                    }
4075                }
4076                outline_panel.update_fs_entries(&editor, HashSet::default(), debounce, cx);
4077            }
4078            EditorEvent::ExcerptsExpanded { ids } => {
4079                outline_panel.invalidate_outlines(ids);
4080                outline_panel.update_non_fs_items(cx);
4081            }
4082            EditorEvent::ExcerptsEdited { ids } => {
4083                outline_panel.invalidate_outlines(ids);
4084                outline_panel.update_non_fs_items(cx);
4085            }
4086            EditorEvent::Reparsed(buffer_id) => {
4087                if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) {
4088                    for (_, excerpt) in excerpts {
4089                        excerpt.invalidate_outlines();
4090                    }
4091                }
4092                outline_panel.update_non_fs_items(cx);
4093            }
4094            _ => {}
4095        },
4096    )
4097}
4098
4099fn empty_icon() -> AnyElement {
4100    h_flex()
4101        .size(IconSize::default().rems())
4102        .invisible()
4103        .flex_none()
4104        .into_any_element()
4105}
4106
4107fn horizontal_separator(cx: &mut WindowContext) -> Div {
4108    div().mx_2().border_primary(cx).border_t_1()
4109}
4110
4111#[cfg(test)]
4112mod tests {
4113    use gpui::{TestAppContext, VisualTestContext, WindowHandle};
4114    use language::{tree_sitter_rust, Language, LanguageConfig, LanguageMatcher};
4115    use pretty_assertions::assert_eq;
4116    use project::FakeFs;
4117    use search::project_search::{self, perform_project_search};
4118    use serde_json::json;
4119
4120    use super::*;
4121
4122    const SELECTED_MARKER: &str = "  <==== selected";
4123
4124    #[gpui::test]
4125    async fn test_project_search_results_toggling(cx: &mut TestAppContext) {
4126        init_test(cx);
4127
4128        let fs = FakeFs::new(cx.background_executor.clone());
4129        populate_with_test_ra_project(&fs, "/rust-analyzer").await;
4130        let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
4131        project.read_with(cx, |project, _| {
4132            project.languages().add(Arc::new(rust_lang()))
4133        });
4134        let workspace = add_outline_panel(&project, cx).await;
4135        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4136        let outline_panel = outline_panel(&workspace, cx);
4137        outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4138
4139        workspace
4140            .update(cx, |workspace, cx| {
4141                ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4142            })
4143            .unwrap();
4144        let search_view = workspace
4145            .update(cx, |workspace, cx| {
4146                workspace
4147                    .active_pane()
4148                    .read(cx)
4149                    .items()
4150                    .find_map(|item| item.downcast::<ProjectSearchView>())
4151                    .expect("Project search view expected to appear after new search event trigger")
4152            })
4153            .unwrap();
4154
4155        let query = "param_names_for_lifetime_elision_hints";
4156        perform_project_search(&search_view, query, cx);
4157        search_view.update(cx, |search_view, cx| {
4158            search_view
4159                .results_editor()
4160                .update(cx, |results_editor, cx| {
4161                    assert_eq!(
4162                        results_editor.display_text(cx).match_indices(query).count(),
4163                        9
4164                    );
4165                });
4166        });
4167
4168        let all_matches = r#"/
4169  crates/
4170    ide/src/
4171      inlay_hints/
4172        fn_lifetime_fn.rs
4173          search: match config.param_names_for_lifetime_elision_hints {
4174          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
4175          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
4176          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
4177      inlay_hints.rs
4178        search: pub param_names_for_lifetime_elision_hints: bool,
4179        search: param_names_for_lifetime_elision_hints: self
4180      static_index.rs
4181        search: param_names_for_lifetime_elision_hints: false,
4182    rust-analyzer/src/
4183      cli/
4184        analysis_stats.rs
4185          search: param_names_for_lifetime_elision_hints: true,
4186      config.rs
4187        search: param_names_for_lifetime_elision_hints: self"#;
4188        let select_first_in_all_matches = |line_to_select: &str| {
4189            assert!(all_matches.contains(line_to_select));
4190            all_matches.replacen(
4191                line_to_select,
4192                &format!("{line_to_select}{SELECTED_MARKER}"),
4193                1,
4194            )
4195        };
4196
4197        cx.executor()
4198            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4199        cx.run_until_parked();
4200        outline_panel.update(cx, |outline_panel, _| {
4201            assert_eq!(
4202                display_entries(
4203                    &outline_panel.cached_entries,
4204                    outline_panel.selected_entry()
4205                ),
4206                select_first_in_all_matches(
4207                    "search: match config.param_names_for_lifetime_elision_hints {"
4208                )
4209            );
4210        });
4211
4212        outline_panel.update(cx, |outline_panel, cx| {
4213            outline_panel.select_parent(&SelectParent, cx);
4214            assert_eq!(
4215                display_entries(
4216                    &outline_panel.cached_entries,
4217                    outline_panel.selected_entry()
4218                ),
4219                select_first_in_all_matches("fn_lifetime_fn.rs")
4220            );
4221        });
4222        outline_panel.update(cx, |outline_panel, cx| {
4223            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4224        });
4225        cx.run_until_parked();
4226        outline_panel.update(cx, |outline_panel, _| {
4227            assert_eq!(
4228                display_entries(
4229                    &outline_panel.cached_entries,
4230                    outline_panel.selected_entry()
4231                ),
4232                format!(
4233                    r#"/
4234  crates/
4235    ide/src/
4236      inlay_hints/
4237        fn_lifetime_fn.rs{SELECTED_MARKER}
4238      inlay_hints.rs
4239        search: pub param_names_for_lifetime_elision_hints: bool,
4240        search: param_names_for_lifetime_elision_hints: self
4241      static_index.rs
4242        search: param_names_for_lifetime_elision_hints: false,
4243    rust-analyzer/src/
4244      cli/
4245        analysis_stats.rs
4246          search: param_names_for_lifetime_elision_hints: true,
4247      config.rs
4248        search: param_names_for_lifetime_elision_hints: self"#,
4249                )
4250            );
4251        });
4252
4253        outline_panel.update(cx, |outline_panel, cx| {
4254            outline_panel.expand_all_entries(&ExpandAllEntries, cx);
4255        });
4256        cx.run_until_parked();
4257        outline_panel.update(cx, |outline_panel, cx| {
4258            outline_panel.select_parent(&SelectParent, cx);
4259            assert_eq!(
4260                display_entries(
4261                    &outline_panel.cached_entries,
4262                    outline_panel.selected_entry()
4263                ),
4264                select_first_in_all_matches("inlay_hints/")
4265            );
4266        });
4267
4268        outline_panel.update(cx, |outline_panel, cx| {
4269            outline_panel.select_parent(&SelectParent, cx);
4270            assert_eq!(
4271                display_entries(
4272                    &outline_panel.cached_entries,
4273                    outline_panel.selected_entry()
4274                ),
4275                select_first_in_all_matches("ide/src/")
4276            );
4277        });
4278
4279        outline_panel.update(cx, |outline_panel, cx| {
4280            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4281        });
4282        cx.run_until_parked();
4283        outline_panel.update(cx, |outline_panel, _| {
4284            assert_eq!(
4285                display_entries(
4286                    &outline_panel.cached_entries,
4287                    outline_panel.selected_entry()
4288                ),
4289                format!(
4290                    r#"/
4291  crates/
4292    ide/src/{SELECTED_MARKER}
4293    rust-analyzer/src/
4294      cli/
4295        analysis_stats.rs
4296          search: param_names_for_lifetime_elision_hints: true,
4297      config.rs
4298        search: param_names_for_lifetime_elision_hints: self"#,
4299                )
4300            );
4301        });
4302        outline_panel.update(cx, |outline_panel, cx| {
4303            outline_panel.expand_selected_entry(&ExpandSelectedEntry, cx);
4304        });
4305        cx.run_until_parked();
4306        outline_panel.update(cx, |outline_panel, _| {
4307            assert_eq!(
4308                display_entries(
4309                    &outline_panel.cached_entries,
4310                    outline_panel.selected_entry()
4311                ),
4312                select_first_in_all_matches("ide/src/")
4313            );
4314        });
4315    }
4316
4317    #[gpui::test]
4318    async fn test_item_filtering(cx: &mut TestAppContext) {
4319        init_test(cx);
4320
4321        let fs = FakeFs::new(cx.background_executor.clone());
4322        populate_with_test_ra_project(&fs, "/rust-analyzer").await;
4323        let project = Project::test(fs.clone(), ["/rust-analyzer".as_ref()], cx).await;
4324        project.read_with(cx, |project, _| {
4325            project.languages().add(Arc::new(rust_lang()))
4326        });
4327        let workspace = add_outline_panel(&project, cx).await;
4328        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4329        let outline_panel = outline_panel(&workspace, cx);
4330        outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4331
4332        workspace
4333            .update(cx, |workspace, cx| {
4334                ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4335            })
4336            .unwrap();
4337        let search_view = workspace
4338            .update(cx, |workspace, cx| {
4339                workspace
4340                    .active_pane()
4341                    .read(cx)
4342                    .items()
4343                    .find_map(|item| item.downcast::<ProjectSearchView>())
4344                    .expect("Project search view expected to appear after new search event trigger")
4345            })
4346            .unwrap();
4347
4348        let query = "param_names_for_lifetime_elision_hints";
4349        perform_project_search(&search_view, query, cx);
4350        search_view.update(cx, |search_view, cx| {
4351            search_view
4352                .results_editor()
4353                .update(cx, |results_editor, cx| {
4354                    assert_eq!(
4355                        results_editor.display_text(cx).match_indices(query).count(),
4356                        9
4357                    );
4358                });
4359        });
4360        let all_matches = r#"/
4361  crates/
4362    ide/src/
4363      inlay_hints/
4364        fn_lifetime_fn.rs
4365          search: match config.param_names_for_lifetime_elision_hints {
4366          search: allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
4367          search: Some(it) if config.param_names_for_lifetime_elision_hints => {
4368          search: InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
4369      inlay_hints.rs
4370        search: pub param_names_for_lifetime_elision_hints: bool,
4371        search: param_names_for_lifetime_elision_hints: self
4372      static_index.rs
4373        search: param_names_for_lifetime_elision_hints: false,
4374    rust-analyzer/src/
4375      cli/
4376        analysis_stats.rs
4377          search: param_names_for_lifetime_elision_hints: true,
4378      config.rs
4379        search: param_names_for_lifetime_elision_hints: self"#;
4380
4381        cx.executor()
4382            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4383        cx.run_until_parked();
4384        outline_panel.update(cx, |outline_panel, _| {
4385            assert_eq!(
4386                display_entries(&outline_panel.cached_entries, None,),
4387                all_matches,
4388            );
4389        });
4390
4391        let filter_text = "a";
4392        outline_panel.update(cx, |outline_panel, cx| {
4393            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
4394                filter_editor.set_text(filter_text, cx);
4395            });
4396        });
4397        cx.executor()
4398            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4399        cx.run_until_parked();
4400
4401        outline_panel.update(cx, |outline_panel, _| {
4402            assert_eq!(
4403                display_entries(&outline_panel.cached_entries, None),
4404                all_matches
4405                    .lines()
4406                    .filter(|item| item.contains(filter_text))
4407                    .collect::<Vec<_>>()
4408                    .join("\n"),
4409            );
4410        });
4411
4412        outline_panel.update(cx, |outline_panel, cx| {
4413            outline_panel.filter_editor.update(cx, |filter_editor, cx| {
4414                filter_editor.set_text("", cx);
4415            });
4416        });
4417        cx.executor()
4418            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4419        cx.run_until_parked();
4420        outline_panel.update(cx, |outline_panel, _| {
4421            assert_eq!(
4422                display_entries(&outline_panel.cached_entries, None,),
4423                all_matches,
4424            );
4425        });
4426    }
4427
4428    #[gpui::test]
4429    async fn test_frontend_repo_structure(cx: &mut TestAppContext) {
4430        init_test(cx);
4431
4432        let root = "/frontend-project";
4433        let fs = FakeFs::new(cx.background_executor.clone());
4434        fs.insert_tree(
4435            root,
4436            json!({
4437                "public": {
4438                    "lottie": {
4439                        "syntax-tree.json": r#"{ "something": "static" }"#
4440                    }
4441                },
4442                "src": {
4443                    "app": {
4444                        "(site)": {
4445                            "(about)": {
4446                                "jobs": {
4447                                    "[slug]": {
4448                                        "page.tsx": r#"static"#
4449                                    }
4450                                }
4451                            },
4452                            "(blog)": {
4453                                "post": {
4454                                    "[slug]": {
4455                                        "page.tsx": r#"static"#
4456                                    }
4457                                }
4458                            },
4459                        }
4460                    },
4461                    "components": {
4462                        "ErrorBoundary.tsx": r#"static"#,
4463                    }
4464                }
4465
4466            }),
4467        )
4468        .await;
4469        let project = Project::test(fs.clone(), [root.as_ref()], cx).await;
4470        let workspace = add_outline_panel(&project, cx).await;
4471        let cx = &mut VisualTestContext::from_window(*workspace, cx);
4472        let outline_panel = outline_panel(&workspace, cx);
4473        outline_panel.update(cx, |outline_panel, cx| outline_panel.set_active(true, cx));
4474
4475        workspace
4476            .update(cx, |workspace, cx| {
4477                ProjectSearchView::deploy_search(workspace, &workspace::DeploySearch::default(), cx)
4478            })
4479            .unwrap();
4480        let search_view = workspace
4481            .update(cx, |workspace, cx| {
4482                workspace
4483                    .active_pane()
4484                    .read(cx)
4485                    .items()
4486                    .find_map(|item| item.downcast::<ProjectSearchView>())
4487                    .expect("Project search view expected to appear after new search event trigger")
4488            })
4489            .unwrap();
4490
4491        let query = "static";
4492        perform_project_search(&search_view, query, cx);
4493        search_view.update(cx, |search_view, cx| {
4494            search_view
4495                .results_editor()
4496                .update(cx, |results_editor, cx| {
4497                    assert_eq!(
4498                        results_editor.display_text(cx).match_indices(query).count(),
4499                        4
4500                    );
4501                });
4502        });
4503
4504        cx.executor()
4505            .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100));
4506        cx.run_until_parked();
4507        outline_panel.update(cx, |outline_panel, _| {
4508            assert_eq!(
4509                display_entries(
4510                    &outline_panel.cached_entries,
4511                    outline_panel.selected_entry()
4512                ),
4513                r#"/
4514  public/lottie/
4515    syntax-tree.json
4516      search: { "something": "static" }  <==== selected
4517  src/
4518    app/(site)/
4519      (about)/jobs/[slug]/
4520        page.tsx
4521          search: static
4522      (blog)/post/[slug]/
4523        page.tsx
4524          search: static
4525    components/
4526      ErrorBoundary.tsx
4527        search: static"#
4528            );
4529        });
4530
4531        outline_panel.update(cx, |outline_panel, cx| {
4532            outline_panel.select_next(&SelectNext, cx);
4533            outline_panel.select_next(&SelectNext, cx);
4534            outline_panel.collapse_selected_entry(&CollapseSelectedEntry, cx);
4535        });
4536        cx.run_until_parked();
4537        outline_panel.update(cx, |outline_panel, _| {
4538            assert_eq!(
4539                display_entries(
4540                    &outline_panel.cached_entries,
4541                    outline_panel.selected_entry()
4542                ),
4543                r#"/
4544  public/lottie/
4545    syntax-tree.json
4546      search: { "something": "static" }
4547  src/
4548    app/(site)/  <==== selected
4549    components/
4550      ErrorBoundary.tsx
4551        search: static"#
4552            );
4553        });
4554    }
4555
4556    async fn add_outline_panel(
4557        project: &Model<Project>,
4558        cx: &mut TestAppContext,
4559    ) -> WindowHandle<Workspace> {
4560        let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
4561
4562        let outline_panel = window
4563            .update(cx, |_, cx| cx.spawn(OutlinePanel::load))
4564            .unwrap()
4565            .await
4566            .expect("Failed to load outline panel");
4567
4568        window
4569            .update(cx, |workspace, cx| {
4570                workspace.add_panel(outline_panel, cx);
4571            })
4572            .unwrap();
4573        window
4574    }
4575
4576    fn outline_panel(
4577        workspace: &WindowHandle<Workspace>,
4578        cx: &mut TestAppContext,
4579    ) -> View<OutlinePanel> {
4580        workspace
4581            .update(cx, |workspace, cx| {
4582                workspace
4583                    .panel::<OutlinePanel>(cx)
4584                    .expect("no outline panel")
4585            })
4586            .unwrap()
4587    }
4588
4589    fn display_entries(
4590        cached_entries: &[CachedEntry],
4591        selected_entry: Option<&PanelEntry>,
4592    ) -> String {
4593        let mut display_string = String::new();
4594        for entry in cached_entries {
4595            if !display_string.is_empty() {
4596                display_string += "\n";
4597            }
4598            for _ in 0..entry.depth {
4599                display_string += "  ";
4600            }
4601            display_string += &match &entry.entry {
4602                PanelEntry::Fs(entry) => match entry {
4603                    FsEntry::ExternalFile(_, _) => {
4604                        panic!("Did not cover external files with tests")
4605                    }
4606                    FsEntry::Directory(_, dir_entry) => format!(
4607                        "{}/",
4608                        dir_entry
4609                            .path
4610                            .file_name()
4611                            .map(|name| name.to_string_lossy().to_string())
4612                            .unwrap_or_default()
4613                    ),
4614                    FsEntry::File(_, file_entry, ..) => file_entry
4615                        .path
4616                        .file_name()
4617                        .map(|name| name.to_string_lossy().to_string())
4618                        .unwrap_or_default(),
4619                },
4620                PanelEntry::FoldedDirs(_, dirs) => dirs
4621                    .iter()
4622                    .filter_map(|dir| dir.path.file_name())
4623                    .map(|name| name.to_string_lossy().to_string() + "/")
4624                    .collect(),
4625                PanelEntry::Outline(outline_entry) => match outline_entry {
4626                    OutlineEntry::Excerpt(_, _, _) => continue,
4627                    OutlineEntry::Outline(_, _, outline) => format!("outline: {}", outline.text),
4628                },
4629                PanelEntry::Search(SearchEntry { render_data, .. }) => {
4630                    format!("search: {}", render_data.context_text)
4631                }
4632            };
4633
4634            if Some(&entry.entry) == selected_entry {
4635                display_string += SELECTED_MARKER;
4636            }
4637        }
4638        display_string
4639    }
4640
4641    fn init_test(cx: &mut TestAppContext) {
4642        cx.update(|cx| {
4643            let settings = SettingsStore::test(cx);
4644            cx.set_global(settings);
4645
4646            theme::init(theme::LoadThemes::JustBase, cx);
4647
4648            language::init(cx);
4649            editor::init(cx);
4650            workspace::init_settings(cx);
4651            Project::init_settings(cx);
4652            project_search::init(cx);
4653            super::init((), cx);
4654        });
4655    }
4656
4657    // Based on https://github.com/rust-lang/rust-analyzer/
4658    async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) {
4659        fs.insert_tree(
4660            root,
4661            json!({
4662                    "crates": {
4663                        "ide": {
4664                            "src": {
4665                                "inlay_hints": {
4666                                    "fn_lifetime_fn.rs": r##"
4667        pub(super) fn hints(
4668            acc: &mut Vec<InlayHint>,
4669            config: &InlayHintsConfig,
4670            func: ast::Fn,
4671        ) -> Option<()> {
4672            // ... snip
4673
4674            let mut used_names: FxHashMap<SmolStr, usize> =
4675                match config.param_names_for_lifetime_elision_hints {
4676                    true => generic_param_list
4677                        .iter()
4678                        .flat_map(|gpl| gpl.lifetime_params())
4679                        .filter_map(|param| param.lifetime())
4680                        .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0)))
4681                        .collect(),
4682                    false => Default::default(),
4683                };
4684            {
4685                let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided);
4686                if self_param.is_some() && potential_lt_refs.next().is_some() {
4687                    allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints {
4688                        // self can't be used as a lifetime, so no need to check for collisions
4689                        "'self".into()
4690                    } else {
4691                        gen_idx_name()
4692                    });
4693                }
4694                potential_lt_refs.for_each(|(name, ..)| {
4695                    let name = match name {
4696                        Some(it) if config.param_names_for_lifetime_elision_hints => {
4697                            if let Some(c) = used_names.get_mut(it.text().as_str()) {
4698                                *c += 1;
4699                                SmolStr::from(format!("'{text}{c}", text = it.text().as_str()))
4700                            } else {
4701                                used_names.insert(it.text().as_str().into(), 0);
4702                                SmolStr::from_iter(["\'", it.text().as_str()])
4703                            }
4704                        }
4705                        _ => gen_idx_name(),
4706                    };
4707                    allocated_lifetimes.push(name);
4708                });
4709            }
4710
4711            // ... snip
4712        }
4713
4714        // ... snip
4715
4716            #[test]
4717            fn hints_lifetimes_named() {
4718                check_with_config(
4719                    InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG },
4720                    r#"
4721        fn nested_in<'named>(named: &        &X<      &()>) {}
4722        //          ^'named1, 'named2, 'named3, $
4723                                  //^'named1 ^'named2 ^'named3
4724        "#,
4725                );
4726            }
4727
4728        // ... snip
4729        "##,
4730                                },
4731                        "inlay_hints.rs": r#"
4732    #[derive(Clone, Debug, PartialEq, Eq)]
4733    pub struct InlayHintsConfig {
4734        // ... snip
4735        pub param_names_for_lifetime_elision_hints: bool,
4736        pub max_length: Option<usize>,
4737        // ... snip
4738    }
4739
4740    impl Config {
4741        pub fn inlay_hints(&self) -> InlayHintsConfig {
4742            InlayHintsConfig {
4743                // ... snip
4744                param_names_for_lifetime_elision_hints: self
4745                    .inlayHints_lifetimeElisionHints_useParameterNames()
4746                    .to_owned(),
4747                max_length: self.inlayHints_maxLength().to_owned(),
4748                // ... snip
4749            }
4750        }
4751    }
4752    "#,
4753                        "static_index.rs": r#"
4754// ... snip
4755        fn add_file(&mut self, file_id: FileId) {
4756            let current_crate = crates_for(self.db, file_id).pop().map(Into::into);
4757            let folds = self.analysis.folding_ranges(file_id).unwrap();
4758            let inlay_hints = self
4759                .analysis
4760                .inlay_hints(
4761                    &InlayHintsConfig {
4762                        // ... snip
4763                        closure_style: hir::ClosureStyle::ImplFn,
4764                        param_names_for_lifetime_elision_hints: false,
4765                        binding_mode_hints: false,
4766                        max_length: Some(25),
4767                        closure_capture_hints: false,
4768                        // ... snip
4769                    },
4770                    file_id,
4771                    None,
4772                )
4773                .unwrap();
4774            // ... snip
4775    }
4776// ... snip
4777    "#
4778                            }
4779                        },
4780                        "rust-analyzer": {
4781                            "src": {
4782                                "cli": {
4783                                    "analysis_stats.rs": r#"
4784        // ... snip
4785                for &file_id in &file_ids {
4786                    _ = analysis.inlay_hints(
4787                        &InlayHintsConfig {
4788                            // ... snip
4789                            implicit_drop_hints: true,
4790                            lifetime_elision_hints: ide::LifetimeElisionHints::Always,
4791                            param_names_for_lifetime_elision_hints: true,
4792                            hide_named_constructor_hints: false,
4793                            hide_closure_initialization_hints: false,
4794                            closure_style: hir::ClosureStyle::ImplFn,
4795                            max_length: Some(25),
4796                            closing_brace_hints_min_lines: Some(20),
4797                            fields_to_resolve: InlayFieldsToResolve::empty(),
4798                            range_exclusive_hints: true,
4799                        },
4800                        file_id.into(),
4801                        None,
4802                    );
4803                }
4804        // ... snip
4805                                    "#,
4806                                },
4807                                "config.rs": r#"
4808                config_data! {
4809                    /// Configs that only make sense when they are set by a client. As such they can only be defined
4810                    /// by setting them using client's settings (e.g `settings.json` on VS Code).
4811                    client: struct ClientDefaultConfigData <- ClientConfigInput -> {
4812                        // ... snip
4813                        /// Maximum length for inlay hints. Set to null to have an unlimited length.
4814                        inlayHints_maxLength: Option<usize>                        = Some(25),
4815                        // ... snip
4816                        /// Whether to prefer using parameter names as the name for elided lifetime hints if possible.
4817                        inlayHints_lifetimeElisionHints_useParameterNames: bool    = false,
4818                        // ... snip
4819                    }
4820                }
4821
4822                impl Config {
4823                    // ... snip
4824                    pub fn inlay_hints(&self) -> InlayHintsConfig {
4825                        InlayHintsConfig {
4826                            // ... snip
4827                            param_names_for_lifetime_elision_hints: self
4828                                .inlayHints_lifetimeElisionHints_useParameterNames()
4829                                .to_owned(),
4830                            max_length: self.inlayHints_maxLength().to_owned(),
4831                            // ... snip
4832                        }
4833                    }
4834                    // ... snip
4835                }
4836                "#
4837                                }
4838                        }
4839                    }
4840            }),
4841        )
4842        .await;
4843    }
4844
4845    fn rust_lang() -> Language {
4846        Language::new(
4847            LanguageConfig {
4848                name: "Rust".into(),
4849                matcher: LanguageMatcher {
4850                    path_suffixes: vec!["rs".to_string()],
4851                    ..Default::default()
4852                },
4853                ..Default::default()
4854            },
4855            Some(tree_sitter_rust::LANGUAGE.into()),
4856        )
4857        .with_highlights_query(
4858            r#"
4859                (field_identifier) @field
4860                (struct_expression) @struct
4861            "#,
4862        )
4863        .unwrap()
4864        .with_injection_query(
4865            r#"
4866                (macro_invocation
4867                    (token_tree) @content
4868                    (#set! "language" "rust"))
4869            "#,
4870        )
4871        .unwrap()
4872    }
4873}