outline_panel.rs

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