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, 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::{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, Spacing, StyledExt,
  55        StyledTypography, 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.scroll_to_item(index);
1107                cx.notify();
1108            }
1109        }
1110    }
1111
1112    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
1113        if !self.focus_handle.contains_focused(cx) {
1114            cx.emit(Event::Focus);
1115        }
1116    }
1117
1118    fn deploy_context_menu(
1119        &mut self,
1120        position: Point<Pixels>,
1121        entry: PanelEntry,
1122        cx: &mut ViewContext<Self>,
1123    ) {
1124        self.select_entry(entry.clone(), true, cx);
1125        let is_root = match &entry {
1126            PanelEntry::Fs(FsEntry::File(worktree_id, entry, ..))
1127            | PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => self
1128                .project
1129                .read(cx)
1130                .worktree_for_id(*worktree_id, cx)
1131                .map(|worktree| {
1132                    worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1133                })
1134                .unwrap_or(false),
1135            PanelEntry::FoldedDirs(worktree_id, entries) => entries
1136                .first()
1137                .and_then(|entry| {
1138                    self.project
1139                        .read(cx)
1140                        .worktree_for_id(*worktree_id, cx)
1141                        .map(|worktree| {
1142                            worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id)
1143                        })
1144                })
1145                .unwrap_or(false),
1146            PanelEntry::Fs(FsEntry::ExternalFile(..)) => false,
1147            PanelEntry::Outline(..) => {
1148                cx.notify();
1149                return;
1150            }
1151            PanelEntry::Search(_) => {
1152                cx.notify();
1153                return;
1154            }
1155        };
1156        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
1157        let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry);
1158        let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry);
1159
1160        let context_menu = ContextMenu::build(cx, |menu, _| {
1161            menu.context(self.focus_handle.clone())
1162                .when(cfg!(target_os = "macos"), |menu| {
1163                    menu.action("Reveal in Finder", Box::new(RevealInFileManager))
1164                })
1165                .when(cfg!(not(target_os = "macos")), |menu| {
1166                    menu.action("Reveal in File Manager", Box::new(RevealInFileManager))
1167                })
1168                .action("Open in Terminal", Box::new(OpenInTerminal))
1169                .when(is_unfoldable, |menu| {
1170                    menu.action("Unfold Directory", Box::new(UnfoldDirectory))
1171                })
1172                .when(is_foldable, |menu| {
1173                    menu.action("Fold Directory", Box::new(FoldDirectory))
1174                })
1175                .separator()
1176                .action("Copy Path", Box::new(CopyPath))
1177                .action("Copy Relative Path", Box::new(CopyRelativePath))
1178        });
1179        cx.focus_view(&context_menu);
1180        let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| {
1181            outline_panel.context_menu.take();
1182            cx.notify();
1183        });
1184        self.context_menu = Some((context_menu, position, subscription));
1185        cx.notify();
1186    }
1187
1188    fn is_unfoldable(&self, entry: &PanelEntry) -> bool {
1189        matches!(entry, PanelEntry::FoldedDirs(..))
1190    }
1191
1192    fn is_foldable(&self, entry: &PanelEntry) -> bool {
1193        let (directory_worktree, directory_entry) = match entry {
1194            PanelEntry::Fs(FsEntry::Directory(directory_worktree, directory_entry)) => {
1195                (*directory_worktree, Some(directory_entry))
1196            }
1197            _ => return false,
1198        };
1199        let Some(directory_entry) = directory_entry else {
1200            return false;
1201        };
1202
1203        if self
1204            .unfolded_dirs
1205            .get(&directory_worktree)
1206            .map_or(true, |unfolded_dirs| {
1207                !unfolded_dirs.contains(&directory_entry.id)
1208            })
1209        {
1210            return false;
1211        }
1212
1213        let children = self
1214            .fs_children_count
1215            .get(&directory_worktree)
1216            .and_then(|entries| entries.get(&directory_entry.path))
1217            .copied()
1218            .unwrap_or_default();
1219
1220        children.may_be_fold_part() && children.dirs > 0
1221    }
1222
1223    fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext<Self>) {
1224        let entry_to_expand = match self.selected_entry() {
1225            Some(PanelEntry::FoldedDirs(worktree_id, dir_entries)) => dir_entries
1226                .last()
1227                .map(|entry| CollapsedEntry::Dir(*worktree_id, entry.id)),
1228            Some(PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry))) => {
1229                Some(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
1230            }
1231            Some(PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _))) => {
1232                Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1233            }
1234            Some(PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _))) => {
1235                Some(CollapsedEntry::ExternalFile(*buffer_id))
1236            }
1237            Some(PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _))) => {
1238                Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1239            }
1240            None | Some(PanelEntry::Search(_)) | Some(PanelEntry::Outline(..)) => None,
1241        };
1242        let Some(collapsed_entry) = entry_to_expand else {
1243            return;
1244        };
1245        let expanded = self.collapsed_entries.remove(&collapsed_entry);
1246        if expanded {
1247            if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry {
1248                self.project.update(cx, |project, cx| {
1249                    project.expand_entry(worktree_id, dir_entry_id, cx);
1250                });
1251            }
1252            self.update_cached_entries(None, cx);
1253        } else {
1254            self.select_next(&SelectNext, cx)
1255        }
1256    }
1257
1258    fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext<Self>) {
1259        let Some(selected_entry) = self.selected_entry().cloned() else {
1260            return;
1261        };
1262        match &selected_entry {
1263            PanelEntry::Fs(FsEntry::Directory(worktree_id, selected_dir_entry)) => {
1264                self.collapsed_entries
1265                    .insert(CollapsedEntry::Dir(*worktree_id, selected_dir_entry.id));
1266                self.select_entry(selected_entry, true, cx);
1267                self.update_cached_entries(None, cx);
1268            }
1269            PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1270                self.collapsed_entries
1271                    .insert(CollapsedEntry::File(*worktree_id, *buffer_id));
1272                self.select_entry(selected_entry, true, cx);
1273                self.update_cached_entries(None, cx);
1274            }
1275            PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1276                self.collapsed_entries
1277                    .insert(CollapsedEntry::ExternalFile(*buffer_id));
1278                self.select_entry(selected_entry, true, cx);
1279                self.update_cached_entries(None, cx);
1280            }
1281            PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
1282                if let Some(dir_entry) = dir_entries.last() {
1283                    if self
1284                        .collapsed_entries
1285                        .insert(CollapsedEntry::Dir(*worktree_id, dir_entry.id))
1286                    {
1287                        self.select_entry(selected_entry, true, cx);
1288                        self.update_cached_entries(None, cx);
1289                    }
1290                }
1291            }
1292            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1293                if self
1294                    .collapsed_entries
1295                    .insert(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1296                {
1297                    self.select_entry(selected_entry, true, cx);
1298                    self.update_cached_entries(None, cx);
1299                }
1300            }
1301            PanelEntry::Search(_) | PanelEntry::Outline(..) => {}
1302        }
1303    }
1304
1305    pub fn expand_all_entries(&mut self, _: &ExpandAllEntries, cx: &mut ViewContext<Self>) {
1306        let expanded_entries =
1307            self.fs_entries
1308                .iter()
1309                .fold(HashSet::default(), |mut entries, fs_entry| {
1310                    match fs_entry {
1311                        FsEntry::ExternalFile(buffer_id, _) => {
1312                            entries.insert(CollapsedEntry::ExternalFile(*buffer_id));
1313                            entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
1314                                |excerpts| {
1315                                    excerpts.iter().map(|(excerpt_id, _)| {
1316                                        CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
1317                                    })
1318                                },
1319                            ));
1320                        }
1321                        FsEntry::Directory(worktree_id, entry) => {
1322                            entries.insert(CollapsedEntry::Dir(*worktree_id, entry.id));
1323                        }
1324                        FsEntry::File(worktree_id, _, buffer_id, _) => {
1325                            entries.insert(CollapsedEntry::File(*worktree_id, *buffer_id));
1326                            entries.extend(self.excerpts.get(buffer_id).into_iter().flat_map(
1327                                |excerpts| {
1328                                    excerpts.iter().map(|(excerpt_id, _)| {
1329                                        CollapsedEntry::Excerpt(*buffer_id, *excerpt_id)
1330                                    })
1331                                },
1332                            ));
1333                        }
1334                    }
1335                    entries
1336                });
1337        self.collapsed_entries
1338            .retain(|entry| !expanded_entries.contains(entry));
1339        self.update_cached_entries(None, cx);
1340    }
1341
1342    pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext<Self>) {
1343        let new_entries = self
1344            .cached_entries
1345            .iter()
1346            .flat_map(|cached_entry| match &cached_entry.entry {
1347                PanelEntry::Fs(FsEntry::Directory(worktree_id, entry)) => {
1348                    Some(CollapsedEntry::Dir(*worktree_id, entry.id))
1349                }
1350                PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1351                    Some(CollapsedEntry::File(*worktree_id, *buffer_id))
1352                }
1353                PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1354                    Some(CollapsedEntry::ExternalFile(*buffer_id))
1355                }
1356                PanelEntry::FoldedDirs(worktree_id, entries) => {
1357                    Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id))
1358                }
1359                PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1360                    Some(CollapsedEntry::Excerpt(*buffer_id, *excerpt_id))
1361                }
1362                PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1363            })
1364            .collect::<Vec<_>>();
1365        self.collapsed_entries.extend(new_entries);
1366        self.update_cached_entries(None, cx);
1367    }
1368
1369    fn toggle_expanded(&mut self, entry: &PanelEntry, cx: &mut ViewContext<Self>) {
1370        match entry {
1371            PanelEntry::Fs(FsEntry::Directory(worktree_id, dir_entry)) => {
1372                let entry_id = dir_entry.id;
1373                let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1374                if self.collapsed_entries.remove(&collapsed_entry) {
1375                    self.project
1376                        .update(cx, |project, cx| {
1377                            project.expand_entry(*worktree_id, entry_id, cx)
1378                        })
1379                        .unwrap_or_else(|| Task::ready(Ok(())))
1380                        .detach_and_log_err(cx);
1381                } else {
1382                    self.collapsed_entries.insert(collapsed_entry);
1383                }
1384            }
1385            PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1386                let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id);
1387                if !self.collapsed_entries.remove(&collapsed_entry) {
1388                    self.collapsed_entries.insert(collapsed_entry);
1389                }
1390            }
1391            PanelEntry::Fs(FsEntry::ExternalFile(buffer_id, _)) => {
1392                let collapsed_entry = CollapsedEntry::ExternalFile(*buffer_id);
1393                if !self.collapsed_entries.remove(&collapsed_entry) {
1394                    self.collapsed_entries.insert(collapsed_entry);
1395                }
1396            }
1397            PanelEntry::FoldedDirs(worktree_id, dir_entries) => {
1398                if let Some(entry_id) = dir_entries.first().map(|entry| entry.id) {
1399                    let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id);
1400                    if self.collapsed_entries.remove(&collapsed_entry) {
1401                        self.project
1402                            .update(cx, |project, cx| {
1403                                project.expand_entry(*worktree_id, entry_id, cx)
1404                            })
1405                            .unwrap_or_else(|| Task::ready(Ok(())))
1406                            .detach_and_log_err(cx);
1407                    } else {
1408                        self.collapsed_entries.insert(collapsed_entry);
1409                    }
1410                }
1411            }
1412            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) => {
1413                let collapsed_entry = CollapsedEntry::Excerpt(*buffer_id, *excerpt_id);
1414                if !self.collapsed_entries.remove(&collapsed_entry) {
1415                    self.collapsed_entries.insert(collapsed_entry);
1416                }
1417            }
1418            PanelEntry::Search(_) | PanelEntry::Outline(..) => return,
1419        }
1420
1421        self.select_entry(entry.clone(), true, cx);
1422        self.update_cached_entries(None, cx);
1423    }
1424
1425    fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext<Self>) {
1426        if let Some(clipboard_text) = self
1427            .selected_entry()
1428            .and_then(|entry| self.abs_path(entry, cx))
1429            .map(|p| p.to_string_lossy().to_string())
1430        {
1431            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1432        }
1433    }
1434
1435    fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext<Self>) {
1436        if let Some(clipboard_text) = self
1437            .selected_entry()
1438            .and_then(|entry| match entry {
1439                PanelEntry::Fs(entry) => self.relative_path(entry, cx),
1440                PanelEntry::FoldedDirs(_, dirs) => dirs.last().map(|entry| entry.path.clone()),
1441                PanelEntry::Search(_) | PanelEntry::Outline(..) => None,
1442            })
1443            .map(|p| p.to_string_lossy().to_string())
1444        {
1445            cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text));
1446        }
1447    }
1448
1449    fn reveal_in_finder(&mut self, _: &RevealInFileManager, cx: &mut ViewContext<Self>) {
1450        if let Some(abs_path) = self
1451            .selected_entry()
1452            .and_then(|entry| self.abs_path(entry, cx))
1453        {
1454            cx.reveal_path(&abs_path);
1455        }
1456    }
1457
1458    fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
1459        let selected_entry = self.selected_entry();
1460        let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx));
1461        let working_directory = if let (
1462            Some(abs_path),
1463            Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))),
1464        ) = (&abs_path, selected_entry)
1465        {
1466            abs_path.parent().map(|p| p.to_owned())
1467        } else {
1468            abs_path
1469        };
1470
1471        if let Some(working_directory) = working_directory {
1472            cx.dispatch_action(workspace::OpenTerminal { working_directory }.boxed_clone())
1473        }
1474    }
1475
1476    fn reveal_entry_for_selection(&mut self, editor: View<Editor>, cx: &mut ViewContext<'_, Self>) {
1477        if !self.active || !OutlinePanelSettings::get_global(cx).auto_reveal_entries {
1478            return;
1479        }
1480        let project = self.project.clone();
1481        self.reveal_selection_task = cx.spawn(|outline_panel, mut cx| async move {
1482            cx.background_executor().timer(UPDATE_DEBOUNCE).await;
1483            let entry_with_selection = outline_panel.update(&mut cx, |outline_panel, cx| {
1484                outline_panel.location_for_editor_selection(&editor, cx)
1485            })?;
1486            let Some(entry_with_selection) = entry_with_selection else {
1487                outline_panel.update(&mut cx, |outline_panel, cx| {
1488                    outline_panel.selected_entry = SelectedEntry::None;
1489                    cx.notify();
1490                })?;
1491                return Ok(());
1492            };
1493            let related_buffer_entry = match &entry_with_selection {
1494                PanelEntry::Fs(FsEntry::File(worktree_id, _, buffer_id, _)) => {
1495                    project.update(&mut cx, |project, cx| {
1496                        let entry_id = project
1497                            .buffer_for_id(*buffer_id, cx)
1498                            .and_then(|buffer| buffer.read(cx).entry_id(cx));
1499                        project
1500                            .worktree_for_id(*worktree_id, cx)
1501                            .zip(entry_id)
1502                            .and_then(|(worktree, entry_id)| {
1503                                let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1504                                Some((worktree, entry))
1505                            })
1506                    })?
1507                }
1508                PanelEntry::Outline(outline_entry) => {
1509                    let &(OutlineEntry::Outline(buffer_id, excerpt_id, _)
1510                    | OutlineEntry::Excerpt(buffer_id, excerpt_id, _)) = outline_entry;
1511                    outline_panel.update(&mut cx, |outline_panel, cx| {
1512                        outline_panel
1513                            .collapsed_entries
1514                            .remove(&CollapsedEntry::ExternalFile(buffer_id));
1515                        outline_panel
1516                            .collapsed_entries
1517                            .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1518                        let project = outline_panel.project.read(cx);
1519                        let entry_id = project
1520                            .buffer_for_id(buffer_id, cx)
1521                            .and_then(|buffer| buffer.read(cx).entry_id(cx));
1522
1523                        entry_id.and_then(|entry_id| {
1524                            project
1525                                .worktree_for_entry(entry_id, cx)
1526                                .and_then(|worktree| {
1527                                    let worktree_id = worktree.read(cx).id();
1528                                    outline_panel
1529                                        .collapsed_entries
1530                                        .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1531                                    let entry = worktree.read(cx).entry_for_id(entry_id)?.clone();
1532                                    Some((worktree, entry))
1533                                })
1534                        })
1535                    })?
1536                }
1537                PanelEntry::Fs(FsEntry::ExternalFile(..)) => None,
1538                PanelEntry::Search(SearchEntry { match_range, .. }) => match_range
1539                    .start
1540                    .buffer_id
1541                    .or(match_range.end.buffer_id)
1542                    .map(|buffer_id| {
1543                        outline_panel.update(&mut cx, |outline_panel, cx| {
1544                            outline_panel
1545                                .collapsed_entries
1546                                .remove(&CollapsedEntry::ExternalFile(buffer_id));
1547                            let project = project.read(cx);
1548                            let entry_id = project
1549                                .buffer_for_id(buffer_id, cx)
1550                                .and_then(|buffer| buffer.read(cx).entry_id(cx));
1551
1552                            entry_id.and_then(|entry_id| {
1553                                project
1554                                    .worktree_for_entry(entry_id, cx)
1555                                    .and_then(|worktree| {
1556                                        let worktree_id = worktree.read(cx).id();
1557                                        outline_panel
1558                                            .collapsed_entries
1559                                            .remove(&CollapsedEntry::File(worktree_id, buffer_id));
1560                                        let entry =
1561                                            worktree.read(cx).entry_for_id(entry_id)?.clone();
1562                                        Some((worktree, entry))
1563                                    })
1564                            })
1565                        })
1566                    })
1567                    .transpose()?
1568                    .flatten(),
1569                _ => return anyhow::Ok(()),
1570            };
1571            if let Some((worktree, buffer_entry)) = related_buffer_entry {
1572                outline_panel.update(&mut cx, |outline_panel, cx| {
1573                    let worktree_id = worktree.read(cx).id();
1574                    let mut dirs_to_expand = Vec::new();
1575                    {
1576                        let mut traversal = worktree.read(cx).traverse_from_path(
1577                            true,
1578                            true,
1579                            true,
1580                            buffer_entry.path.as_ref(),
1581                        );
1582                        let mut current_entry = buffer_entry;
1583                        loop {
1584                            if current_entry.is_dir()
1585                                && outline_panel
1586                                    .collapsed_entries
1587                                    .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id))
1588                            {
1589                                dirs_to_expand.push(current_entry.id);
1590                            }
1591
1592                            if traversal.back_to_parent() {
1593                                if let Some(parent_entry) = traversal.entry() {
1594                                    current_entry = parent_entry.clone();
1595                                    continue;
1596                                }
1597                            }
1598                            break;
1599                        }
1600                    }
1601                    for dir_to_expand in dirs_to_expand {
1602                        project
1603                            .update(cx, |project, cx| {
1604                                project.expand_entry(worktree_id, dir_to_expand, cx)
1605                            })
1606                            .unwrap_or_else(|| Task::ready(Ok(())))
1607                            .detach_and_log_err(cx)
1608                    }
1609                })?
1610            }
1611
1612            outline_panel.update(&mut cx, |outline_panel, cx| {
1613                outline_panel.select_entry(entry_with_selection, false, cx);
1614                outline_panel.update_cached_entries(None, cx);
1615            })?;
1616
1617            anyhow::Ok(())
1618        });
1619    }
1620
1621    fn render_excerpt(
1622        &self,
1623        buffer_id: BufferId,
1624        excerpt_id: ExcerptId,
1625        range: &ExcerptRange<language::Anchor>,
1626        depth: usize,
1627        cx: &mut ViewContext<OutlinePanel>,
1628    ) -> Option<Stateful<Div>> {
1629        let item_id = ElementId::from(excerpt_id.to_proto() as usize);
1630        let is_active = match self.selected_entry() {
1631            Some(PanelEntry::Outline(OutlineEntry::Excerpt(
1632                selected_buffer_id,
1633                selected_excerpt_id,
1634                _,
1635            ))) => selected_buffer_id == &buffer_id && selected_excerpt_id == &excerpt_id,
1636            _ => false,
1637        };
1638        let has_outlines = self
1639            .excerpts
1640            .get(&buffer_id)
1641            .and_then(|excerpts| match &excerpts.get(&excerpt_id)?.outlines {
1642                ExcerptOutlines::Outlines(outlines) => Some(outlines),
1643                ExcerptOutlines::Invalidated(outlines) => Some(outlines),
1644                ExcerptOutlines::NotFetched => None,
1645            })
1646            .map_or(false, |outlines| !outlines.is_empty());
1647        let is_expanded = !self
1648            .collapsed_entries
1649            .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
1650        let color = entry_git_aware_label_color(None, false, is_active);
1651        let icon = if has_outlines {
1652            FileIcons::get_chevron_icon(is_expanded, cx)
1653                .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1654        } else {
1655            None
1656        }
1657        .unwrap_or_else(empty_icon);
1658
1659        let label = self.excerpt_label(buffer_id, range, cx)?;
1660        let label_element = Label::new(label)
1661            .single_line()
1662            .color(color)
1663            .into_any_element();
1664
1665        Some(self.entry_element(
1666            PanelEntry::Outline(OutlineEntry::Excerpt(buffer_id, excerpt_id, range.clone())),
1667            item_id,
1668            depth,
1669            Some(icon),
1670            is_active,
1671            label_element,
1672            cx,
1673        ))
1674    }
1675
1676    fn excerpt_label(
1677        &self,
1678        buffer_id: BufferId,
1679        range: &ExcerptRange<language::Anchor>,
1680        cx: &AppContext,
1681    ) -> Option<String> {
1682        let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?;
1683        let excerpt_range = range.context.to_point(&buffer_snapshot);
1684        Some(format!(
1685            "Lines {}- {}",
1686            excerpt_range.start.row + 1,
1687            excerpt_range.end.row + 1,
1688        ))
1689    }
1690
1691    fn render_outline(
1692        &self,
1693        buffer_id: BufferId,
1694        excerpt_id: ExcerptId,
1695        rendered_outline: &Outline,
1696        depth: usize,
1697        string_match: Option<&StringMatch>,
1698        cx: &mut ViewContext<Self>,
1699    ) -> Stateful<Div> {
1700        let (item_id, label_element) = (
1701            ElementId::from(SharedString::from(format!(
1702                "{buffer_id:?}|{excerpt_id:?}{:?}|{:?}",
1703                rendered_outline.range, &rendered_outline.text,
1704            ))),
1705            outline::render_item(
1706                rendered_outline,
1707                string_match
1708                    .map(|string_match| string_match.ranges().collect::<Vec<_>>())
1709                    .unwrap_or_default(),
1710                cx,
1711            )
1712            .into_any_element(),
1713        );
1714        let is_active = match self.selected_entry() {
1715            Some(PanelEntry::Outline(OutlineEntry::Outline(
1716                selected_buffer_id,
1717                selected_excerpt_id,
1718                selected_entry,
1719            ))) => {
1720                selected_buffer_id == &buffer_id
1721                    && selected_excerpt_id == &excerpt_id
1722                    && selected_entry == rendered_outline
1723            }
1724            _ => false,
1725        };
1726        let icon = if self.is_singleton_active(cx) {
1727            None
1728        } else {
1729            Some(empty_icon())
1730        };
1731        self.entry_element(
1732            PanelEntry::Outline(OutlineEntry::Outline(
1733                buffer_id,
1734                excerpt_id,
1735                rendered_outline.clone(),
1736            )),
1737            item_id,
1738            depth,
1739            icon,
1740            is_active,
1741            label_element,
1742            cx,
1743        )
1744    }
1745
1746    fn render_entry(
1747        &self,
1748        rendered_entry: &FsEntry,
1749        depth: usize,
1750        string_match: Option<&StringMatch>,
1751        cx: &mut ViewContext<Self>,
1752    ) -> Stateful<Div> {
1753        let settings = OutlinePanelSettings::get_global(cx);
1754        let is_active = match self.selected_entry() {
1755            Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry,
1756            _ => false,
1757        };
1758        let (item_id, label_element, icon) = match rendered_entry {
1759            FsEntry::File(worktree_id, entry, ..) => {
1760                let name = self.entry_name(worktree_id, entry, cx);
1761                let color =
1762                    entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1763                let icon = if settings.file_icons {
1764                    FileIcons::get_icon(&entry.path, cx)
1765                        .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element())
1766                } else {
1767                    None
1768                };
1769                (
1770                    ElementId::from(entry.id.to_proto() as usize),
1771                    HighlightedLabel::new(
1772                        name,
1773                        string_match
1774                            .map(|string_match| string_match.positions.clone())
1775                            .unwrap_or_default(),
1776                    )
1777                    .color(color)
1778                    .into_any_element(),
1779                    icon.unwrap_or_else(empty_icon),
1780                )
1781            }
1782            FsEntry::Directory(worktree_id, entry) => {
1783                let name = self.entry_name(worktree_id, entry, cx);
1784
1785                let is_expanded = !self
1786                    .collapsed_entries
1787                    .contains(&CollapsedEntry::Dir(*worktree_id, entry.id));
1788                let color =
1789                    entry_git_aware_label_color(entry.git_status, entry.is_ignored, is_active);
1790                let icon = if settings.folder_icons {
1791                    FileIcons::get_folder_icon(is_expanded, cx)
1792                } else {
1793                    FileIcons::get_chevron_icon(is_expanded, cx)
1794                }
1795                .map(Icon::from_path)
1796                .map(|icon| icon.color(color).into_any_element());
1797                (
1798                    ElementId::from(entry.id.to_proto() as usize),
1799                    HighlightedLabel::new(
1800                        name,
1801                        string_match
1802                            .map(|string_match| string_match.positions.clone())
1803                            .unwrap_or_default(),
1804                    )
1805                    .color(color)
1806                    .into_any_element(),
1807                    icon.unwrap_or_else(empty_icon),
1808                )
1809            }
1810            FsEntry::ExternalFile(buffer_id, ..) => {
1811                let color = entry_label_color(is_active);
1812                let (icon, name) = match self.buffer_snapshot_for_id(*buffer_id, cx) {
1813                    Some(buffer_snapshot) => match buffer_snapshot.file() {
1814                        Some(file) => {
1815                            let path = file.path();
1816                            let icon = if settings.file_icons {
1817                                FileIcons::get_icon(path.as_ref(), cx)
1818                            } else {
1819                                None
1820                            }
1821                            .map(Icon::from_path)
1822                            .map(|icon| icon.color(color).into_any_element());
1823                            (icon, file_name(path.as_ref()))
1824                        }
1825                        None => (None, "Untitled".to_string()),
1826                    },
1827                    None => (None, "Unknown buffer".to_string()),
1828                };
1829                (
1830                    ElementId::from(buffer_id.to_proto() as usize),
1831                    HighlightedLabel::new(
1832                        name,
1833                        string_match
1834                            .map(|string_match| string_match.positions.clone())
1835                            .unwrap_or_default(),
1836                    )
1837                    .color(color)
1838                    .into_any_element(),
1839                    icon.unwrap_or_else(empty_icon),
1840                )
1841            }
1842        };
1843
1844        self.entry_element(
1845            PanelEntry::Fs(rendered_entry.clone()),
1846            item_id,
1847            depth,
1848            Some(icon),
1849            is_active,
1850            label_element,
1851            cx,
1852        )
1853    }
1854
1855    fn render_folded_dirs(
1856        &self,
1857        worktree_id: WorktreeId,
1858        dir_entries: &[Entry],
1859        depth: usize,
1860        string_match: Option<&StringMatch>,
1861        cx: &mut ViewContext<OutlinePanel>,
1862    ) -> Stateful<Div> {
1863        let settings = OutlinePanelSettings::get_global(cx);
1864        let is_active = match self.selected_entry() {
1865            Some(PanelEntry::FoldedDirs(selected_worktree_id, selected_entries)) => {
1866                selected_worktree_id == &worktree_id && selected_entries == dir_entries
1867            }
1868            _ => false,
1869        };
1870        let (item_id, label_element, icon) = {
1871            let name = self.dir_names_string(dir_entries, worktree_id, cx);
1872
1873            let is_expanded = dir_entries.iter().all(|dir| {
1874                !self
1875                    .collapsed_entries
1876                    .contains(&CollapsedEntry::Dir(worktree_id, dir.id))
1877            });
1878            let is_ignored = dir_entries.iter().any(|entry| entry.is_ignored);
1879            let git_status = dir_entries.first().and_then(|entry| entry.git_status);
1880            let color = entry_git_aware_label_color(git_status, is_ignored, is_active);
1881            let icon = if settings.folder_icons {
1882                FileIcons::get_folder_icon(is_expanded, cx)
1883            } else {
1884                FileIcons::get_chevron_icon(is_expanded, cx)
1885            }
1886            .map(Icon::from_path)
1887            .map(|icon| icon.color(color).into_any_element());
1888            (
1889                ElementId::from(
1890                    dir_entries
1891                        .last()
1892                        .map(|entry| entry.id.to_proto())
1893                        .unwrap_or_else(|| worktree_id.to_proto()) as usize,
1894                ),
1895                HighlightedLabel::new(
1896                    name,
1897                    string_match
1898                        .map(|string_match| string_match.positions.clone())
1899                        .unwrap_or_default(),
1900                )
1901                .color(color)
1902                .into_any_element(),
1903                icon.unwrap_or_else(empty_icon),
1904            )
1905        };
1906
1907        self.entry_element(
1908            PanelEntry::FoldedDirs(worktree_id, dir_entries.to_vec()),
1909            item_id,
1910            depth,
1911            Some(icon),
1912            is_active,
1913            label_element,
1914            cx,
1915        )
1916    }
1917
1918    #[allow(clippy::too_many_arguments)]
1919    fn render_search_match(
1920        &mut self,
1921        multi_buffer_snapshot: Option<&MultiBufferSnapshot>,
1922        match_range: &Range<editor::Anchor>,
1923        render_data: &Arc<OnceLock<SearchData>>,
1924        kind: SearchKind,
1925        depth: usize,
1926        string_match: Option<&StringMatch>,
1927        cx: &mut ViewContext<Self>,
1928    ) -> Option<Stateful<Div>> {
1929        let search_data = match render_data.get() {
1930            Some(search_data) => search_data,
1931            None => {
1932                if let ItemsDisplayMode::Search(search_state) = &mut self.mode {
1933                    if let Some(multi_buffer_snapshot) = multi_buffer_snapshot {
1934                        search_state
1935                            .highlight_search_match_tx
1936                            .try_send(HighlightArguments {
1937                                multi_buffer_snapshot: multi_buffer_snapshot.clone(),
1938                                match_range: match_range.clone(),
1939                                search_data: Arc::clone(render_data),
1940                            })
1941                            .ok();
1942                    }
1943                }
1944                return None;
1945            }
1946        };
1947        let search_matches = string_match
1948            .iter()
1949            .flat_map(|string_match| string_match.ranges())
1950            .collect::<Vec<_>>();
1951        let match_ranges = if search_matches.is_empty() {
1952            &search_data.search_match_indices
1953        } else {
1954            &search_matches
1955        };
1956        let label_element = outline::render_item(
1957            &OutlineItem {
1958                depth,
1959                annotation_range: None,
1960                range: search_data.context_range.clone(),
1961                text: search_data.context_text.clone(),
1962                highlight_ranges: search_data
1963                    .highlights_data
1964                    .get()
1965                    .cloned()
1966                    .unwrap_or_default(),
1967                name_ranges: search_data.search_match_indices.clone(),
1968                body_range: Some(search_data.context_range.clone()),
1969            },
1970            match_ranges.iter().cloned(),
1971            cx,
1972        );
1973        let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK);
1974        let entire_label = h_flex()
1975            .justify_center()
1976            .p_0()
1977            .when(search_data.truncated_left, |parent| {
1978                parent.child(truncated_contents_label())
1979            })
1980            .child(label_element)
1981            .when(search_data.truncated_right, |parent| {
1982                parent.child(truncated_contents_label())
1983            })
1984            .into_any_element();
1985
1986        let is_active = match self.selected_entry() {
1987            Some(PanelEntry::Search(SearchEntry {
1988                match_range: selected_match_range,
1989                ..
1990            })) => match_range == selected_match_range,
1991            _ => false,
1992        };
1993        Some(self.entry_element(
1994            PanelEntry::Search(SearchEntry {
1995                kind,
1996                match_range: match_range.clone(),
1997                render_data: render_data.clone(),
1998            }),
1999            ElementId::from(SharedString::from(format!("search-{match_range:?}"))),
2000            depth,
2001            None,
2002            is_active,
2003            entire_label,
2004            cx,
2005        ))
2006    }
2007
2008    #[allow(clippy::too_many_arguments)]
2009    fn entry_element(
2010        &self,
2011        rendered_entry: PanelEntry,
2012        item_id: ElementId,
2013        depth: usize,
2014        icon_element: Option<AnyElement>,
2015        is_active: bool,
2016        label_element: gpui::AnyElement,
2017        cx: &mut ViewContext<OutlinePanel>,
2018    ) -> Stateful<Div> {
2019        let settings = OutlinePanelSettings::get_global(cx);
2020        div()
2021            .text_ui(cx)
2022            .id(item_id.clone())
2023            .on_click({
2024                let clicked_entry = rendered_entry.clone();
2025                cx.listener(move |outline_panel, event: &gpui::ClickEvent, cx| {
2026                    if event.down.button == MouseButton::Right || event.down.first_mouse {
2027                        return;
2028                    }
2029                    let change_selection = event.down.click_count > 1;
2030                    outline_panel.toggle_expanded(&clicked_entry, cx);
2031                    outline_panel.open_entry(&clicked_entry, change_selection, cx);
2032                })
2033            })
2034            .cursor_pointer()
2035            .child(
2036                ListItem::new(item_id)
2037                    .indent_level(depth)
2038                    .indent_step_size(px(settings.indent_size))
2039                    .selected(is_active)
2040                    .when_some(icon_element, |list_item, icon_element| {
2041                        list_item.child(h_flex().child(icon_element))
2042                    })
2043                    .child(h_flex().h_6().child(label_element).ml_1())
2044                    .on_secondary_mouse_down(cx.listener(
2045                        move |outline_panel, event: &MouseDownEvent, cx| {
2046                            // Stop propagation to prevent the catch-all context menu for the project
2047                            // panel from being deployed.
2048                            cx.stop_propagation();
2049                            outline_panel.deploy_context_menu(
2050                                event.position,
2051                                rendered_entry.clone(),
2052                                cx,
2053                            )
2054                        },
2055                    )),
2056            )
2057            .border_1()
2058            .border_r_2()
2059            .rounded_none()
2060            .hover(|style| {
2061                if is_active {
2062                    style
2063                } else {
2064                    let hover_color = cx.theme().colors().ghost_element_hover;
2065                    style.bg(hover_color).border_color(hover_color)
2066                }
2067            })
2068            .when(is_active && self.focus_handle.contains_focused(cx), |div| {
2069                div.border_color(Color::Selected.color(cx))
2070            })
2071    }
2072
2073    fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &AppContext) -> String {
2074        let name = match self.project.read(cx).worktree_for_id(*worktree_id, cx) {
2075            Some(worktree) => {
2076                let worktree = worktree.read(cx);
2077                match worktree.snapshot().root_entry() {
2078                    Some(root_entry) => {
2079                        if root_entry.id == entry.id {
2080                            file_name(worktree.abs_path().as_ref())
2081                        } else {
2082                            let path = worktree.absolutize(entry.path.as_ref()).ok();
2083                            let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2084                            file_name(path)
2085                        }
2086                    }
2087                    None => {
2088                        let path = worktree.absolutize(entry.path.as_ref()).ok();
2089                        let path = path.as_deref().unwrap_or_else(|| entry.path.as_ref());
2090                        file_name(path)
2091                    }
2092                }
2093            }
2094            None => file_name(entry.path.as_ref()),
2095        };
2096        name
2097    }
2098
2099    fn update_fs_entries(
2100        &mut self,
2101        active_editor: &View<Editor>,
2102        new_entries: HashSet<ExcerptId>,
2103        debounce: Option<Duration>,
2104        cx: &mut ViewContext<Self>,
2105    ) {
2106        if !self.active {
2107            return;
2108        }
2109
2110        let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
2111        let active_multi_buffer = active_editor.read(cx).buffer().clone();
2112        self.updating_fs_entries = true;
2113        self.fs_entries_update_task = cx.spawn(|outline_panel, mut cx| async move {
2114            if let Some(debounce) = debounce {
2115                cx.background_executor().timer(debounce).await;
2116            }
2117
2118            let mut new_collapsed_entries = HashSet::default();
2119            let mut new_unfolded_dirs = HashMap::default();
2120            let mut root_entries = HashSet::default();
2121            let mut new_excerpts = HashMap::<BufferId, HashMap<ExcerptId, Excerpt>>::default();
2122            let Ok(buffer_excerpts) = outline_panel.update(&mut cx, |outline_panel, cx| {
2123                new_collapsed_entries = outline_panel.collapsed_entries.clone();
2124                new_unfolded_dirs = outline_panel.unfolded_dirs.clone();
2125                let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx);
2126                let buffer_excerpts = multi_buffer_snapshot.excerpts().fold(
2127                    HashMap::default(),
2128                    |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| {
2129                        let buffer_id = buffer_snapshot.remote_id();
2130                        let file = File::from_dyn(buffer_snapshot.file());
2131                        let entry_id = file.and_then(|file| file.project_entry_id(cx));
2132                        let worktree = file.map(|file| file.worktree.read(cx).snapshot());
2133                        let is_new = new_entries.contains(&excerpt_id)
2134                            || !outline_panel.excerpts.contains_key(&buffer_id);
2135                        buffer_excerpts
2136                            .entry(buffer_id)
2137                            .or_insert_with(|| (is_new, Vec::new(), entry_id, worktree))
2138                            .1
2139                            .push(excerpt_id);
2140
2141                        let outlines = match outline_panel
2142                            .excerpts
2143                            .get(&buffer_id)
2144                            .and_then(|excerpts| excerpts.get(&excerpt_id))
2145                        {
2146                            Some(old_excerpt) => match &old_excerpt.outlines {
2147                                ExcerptOutlines::Outlines(outlines) => {
2148                                    ExcerptOutlines::Outlines(outlines.clone())
2149                                }
2150                                ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
2151                                ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
2152                            },
2153                            None => ExcerptOutlines::NotFetched,
2154                        };
2155                        new_excerpts.entry(buffer_id).or_default().insert(
2156                            excerpt_id,
2157                            Excerpt {
2158                                range: excerpt_range,
2159                                outlines,
2160                            },
2161                        );
2162                        buffer_excerpts
2163                    },
2164                );
2165                buffer_excerpts
2166            }) else {
2167                return;
2168            };
2169
2170            let Some((
2171                new_collapsed_entries,
2172                new_unfolded_dirs,
2173                new_fs_entries,
2174                new_depth_map,
2175                new_children_count,
2176            )) = cx
2177                .background_executor()
2178                .spawn(async move {
2179                    let mut processed_external_buffers = HashSet::default();
2180                    let mut new_worktree_entries =
2181                        HashMap::<WorktreeId, (worktree::Snapshot, HashSet<Entry>)>::default();
2182                    let mut worktree_excerpts = HashMap::<
2183                        WorktreeId,
2184                        HashMap<ProjectEntryId, (BufferId, Vec<ExcerptId>)>,
2185                    >::default();
2186                    let mut external_excerpts = HashMap::default();
2187
2188                    for (buffer_id, (is_new, excerpts, entry_id, worktree)) in buffer_excerpts {
2189                        if is_new {
2190                            match &worktree {
2191                                Some(worktree) => {
2192                                    new_collapsed_entries
2193                                        .remove(&CollapsedEntry::File(worktree.id(), buffer_id));
2194                                }
2195                                None => {
2196                                    new_collapsed_entries
2197                                        .remove(&CollapsedEntry::ExternalFile(buffer_id));
2198                                }
2199                            }
2200                        }
2201
2202                        if let Some(worktree) = worktree {
2203                            let worktree_id = worktree.id();
2204                            let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default();
2205
2206                            match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() {
2207                                Some(entry) => {
2208                                    let mut traversal = worktree.traverse_from_path(
2209                                        true,
2210                                        true,
2211                                        true,
2212                                        entry.path.as_ref(),
2213                                    );
2214
2215                                    let mut entries_to_add = HashSet::default();
2216                                    worktree_excerpts
2217                                        .entry(worktree_id)
2218                                        .or_default()
2219                                        .insert(entry.id, (buffer_id, excerpts));
2220                                    let mut current_entry = entry;
2221                                    loop {
2222                                        if current_entry.is_dir() {
2223                                            let is_root =
2224                                                worktree.root_entry().map(|entry| entry.id)
2225                                                    == Some(current_entry.id);
2226                                            if is_root {
2227                                                root_entries.insert(current_entry.id);
2228                                                if auto_fold_dirs {
2229                                                    unfolded_dirs.insert(current_entry.id);
2230                                                }
2231                                            }
2232                                            if is_new {
2233                                                new_collapsed_entries.remove(&CollapsedEntry::Dir(
2234                                                    worktree_id,
2235                                                    current_entry.id,
2236                                                ));
2237                                            }
2238                                        }
2239
2240                                        let new_entry_added = entries_to_add.insert(current_entry);
2241                                        if new_entry_added && traversal.back_to_parent() {
2242                                            if let Some(parent_entry) = traversal.entry() {
2243                                                current_entry = parent_entry.clone();
2244                                                continue;
2245                                            }
2246                                        }
2247                                        break;
2248                                    }
2249                                    new_worktree_entries
2250                                        .entry(worktree_id)
2251                                        .or_insert_with(|| (worktree.clone(), HashSet::default()))
2252                                        .1
2253                                        .extend(entries_to_add);
2254                                }
2255                                None => {
2256                                    if processed_external_buffers.insert(buffer_id) {
2257                                        external_excerpts
2258                                            .entry(buffer_id)
2259                                            .or_insert_with(Vec::new)
2260                                            .extend(excerpts);
2261                                    }
2262                                }
2263                            }
2264                        } else if processed_external_buffers.insert(buffer_id) {
2265                            external_excerpts
2266                                .entry(buffer_id)
2267                                .or_insert_with(Vec::new)
2268                                .extend(excerpts);
2269                        }
2270                    }
2271
2272                    let mut new_children_count =
2273                        HashMap::<WorktreeId, HashMap<Arc<Path>, FsChildren>>::default();
2274
2275                    let worktree_entries = new_worktree_entries
2276                        .into_iter()
2277                        .map(|(worktree_id, (worktree_snapshot, entries))| {
2278                            let mut entries = entries.into_iter().collect::<Vec<_>>();
2279                            // For a proper git status propagation, we have to keep the entries sorted lexicographically.
2280                            entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref()));
2281                            worktree_snapshot.propagate_git_statuses(&mut entries);
2282                            project::sort_worktree_entries(&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(Spacing::Small.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}