outline_panel.rs

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