outline_panel.rs

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