696 lines · 25.9 KB · Rust
RawDownload
//! Find References UI component for displaying LSP textDocument/references results.//!//! This module provides a hover card that shows all references to a symbol//! as a flat list with file info, line numbers, and syntax-highlighted code snippets. use std::collections::HashMap;use std::path::PathBuf; use lsp::ReferenceLocation;use pathfinder_geometry::vector::Vector2F;use string_offset::CharOffset;use warp_core::ui::appearance::Appearance;use warp_core::ui::icons::Icon as WarpIcon;use warp_core::ui::theme::color::internal_colors;use warp_editor::content::buffer::InitialBufferState;use warp_editor::render::element::VerticalExpansionBehavior;use warp_files::FileModel;use warpui::elements::{    Border, ChildAnchor, ChildView, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox,    Container, CornerRadius, CrossAxisAlignment, Fill, Flex, Hoverable, MouseStateHandle,    OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Radius, ScrollbarWidth,    Shrinkable, Stack, Text,};use warpui::keymap::FixedBinding;use warpui::platform::Cursor;use warpui::prelude::Align;use warpui::ui_components::components::UiComponent;use warpui::{    AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,}; use super::editor::view::{CodeEditorRenderOptions, CodeEditorView};use super::global_buffer_model::GlobalBufferModel;use crate::editor::InteractionState;use crate::search::result_renderer::ItemHighlightState; /// Maximum height for the find references card.pub const FIND_REFERENCES_CARD_MAX_HEIGHT: f32 = 300.; const HAS_REFERENCES: &str = "HasReferences"; pub fn init(app: &mut AppContext) {    use warpui::keymap::macros::*;     app.register_fixed_bindings([        FixedBinding::new(            "escape",            FindReferencesViewAction::Close,            id!(FindReferencesView::ui_name()),        ),        FixedBinding::new(            "enter",            FindReferencesViewAction::SelectReference,            id!(FindReferencesView::ui_name()) & id!(HAS_REFERENCES),        ),        FixedBinding::new(            "numpadenter",            FindReferencesViewAction::SelectReference,            id!(FindReferencesView::ui_name()) & id!(HAS_REFERENCES),        ),        FixedBinding::new(            "up",            FindReferencesViewAction::ArrowUp,            id!(FindReferencesView::ui_name()) & id!(HAS_REFERENCES),        ),        FixedBinding::new(            "down",            FindReferencesViewAction::ArrowDown,            id!(FindReferencesView::ui_name()) & id!(HAS_REFERENCES),        ),    ]);} /// Actions that FindReferencesView can handle.#[derive(Clone, Debug)]pub enum FindReferencesViewAction {    /// Navigate to the reference at the given index.    GotoReference(usize),    /// Close the references card.    Close,    /// Move selection up (arrow up).    ArrowUp,    /// Move selection down (arrow down).    ArrowDown,    /// Select the currently highlighted reference (enter).    SelectReference,} /// Events emitted by FindReferencesView.pub enum FindReferencesViewEvent {    /// User requested navigation to a reference.    GotoReference(usize),    /// User requested to close the card.    CloseRequested,} /// View for displaying find references results with async line loading.pub struct FindReferencesView {    /// Flat list of all references with their UI state.    references: Vec<ReferenceEntryWithUi>,    /// The offset where references were requested (used for positioning).    request_offset: CharOffset,    /// Scroll state for the card content.    scroll_state: ClippedScrollStateHandle,    back_mouse_state: MouseStateHandle,    /// The index of the currently selected reference for keyboard navigation.    selected_reference_index: usize,} /// A reference entry bundled with its UI state.pub struct ReferenceEntryWithUi {    /// The reference data.    pub entry: FlatReferenceEntry,    /// Mouse state for the entry row hover.    pub entry_mouse_state: MouseStateHandle,    /// Mouse state for the file name tooltip.    pub file_name_mouse_state: MouseStateHandle,    /// Editor view for syntax highlighting.    pub editor_view: ViewHandle<CodeEditorView>,} impl ReferenceEntryWithUi {    /// Updates the line content and refreshes the editor view.    /// Trims whitespace from the start of the line and adjusts the column accordingly.    pub fn update_line_content(&mut self, line: &str, ctx: &mut ViewContext<FindReferencesView>) {        let trimmed = line.trim_start();        let trim_offset = line.len() - trimmed.len();        self.entry.line_content = Some(trimmed.to_string());        self.entry.column = self.entry.column.saturating_sub(trim_offset);         // Update the editor view with the loaded content        let content = trimmed.to_string();        let file_path = self.entry.file_path.clone();        self.editor_view.update(ctx, |view, ctx| {            view.set_language_with_local_path(&file_path, ctx);            let state = InitialBufferState::plain_text(&content);            view.reset(state, ctx);        });    }} impl FindReferencesView {    /// Creates a new FindReferencesView with async line loading.    /// Lines are loaded asynchronously from GlobalBufferModel (fast) or disk (slow).    ///    /// Converts raw ReferenceLocation to FlatReferenceEntry and loads line content.    pub fn new(        raw_references: Vec<ReferenceLocation>,        workspace_root: Option<PathBuf>,        request_offset: CharOffset,        ctx: &mut ViewContext<Self>,    ) -> Self {        // Convert ReferenceLocation to ReferenceEntryWithUi (without line content - will load async)        let mut references: Vec<ReferenceEntryWithUi> = raw_references            .into_iter()            .map(|r| {                let file_path = r.file_path.clone();                 // Get just the file name (leaf) for display                let file_name = file_path                    .file_name()                    .map(|n| n.to_string_lossy().to_string())                    .unwrap_or_else(|| file_path.to_string_lossy().to_string());                 // Guard against empty file names from malformed LSP URIs                let file_name = if file_name.is_empty() {                    "[unknown]".to_string()                } else {                    file_name                };                 // Get relative path from workspace root for tooltip                let display_path = workspace_root                    .as_ref()                    .and_then(|root| file_path.strip_prefix(root).ok())                    .map(|p| p.to_string_lossy().to_string())                    .unwrap_or_else(|| file_path.to_string_lossy().to_string());                 let line_number = r.range.start.line + 1; // Convert 0-based to 1-based                 let entry = FlatReferenceEntry {                    file_path,                    file_name,                    display_path,                    line_number,                    column: r.range.start.column,                    line_content: None, // Will be loaded async                };                 // Create editor view for this reference                let editor_view = Self::create_editor_view_for_reference(&entry, ctx);                 ReferenceEntryWithUi {                    entry,                    entry_mouse_state: MouseStateHandle::default(),                    file_name_mouse_state: MouseStateHandle::default(),                    editor_view,                }            })            .collect();         // Batch references by file for efficient loading        let mut file_to_refs: HashMap<PathBuf, Vec<usize>> = HashMap::new();        for (idx, reference) in references.iter().enumerate() {            if reference.entry.line_content.is_none() {                file_to_refs                    .entry(reference.entry.file_path.clone())                    .or_default()                    .push(idx);            }        }         // Try to load lines from GlobalBufferModel first (fast, in-memory)        let global_buffer = GlobalBufferModel::handle(ctx);        for (file_path, ref_indices) in file_to_refs.iter() {            // Collect all line numbers we need from this file (0-based)            let line_numbers: Vec<usize> = ref_indices                .iter()                .map(|&idx| references[idx].entry.line_number - 1)                .collect();             if line_numbers.is_empty() {                continue;            }             if let Some(lines) = global_buffer.update(ctx, |model, ctx| {                model.get_lines_for_file(file_path, line_numbers.clone(), ctx)            }) {                // Build a map from line number to content for quick lookup                let line_map: HashMap<usize, &String> =                    lines.iter().map(|(ln, content)| (*ln, content)).collect();                 // Successfully loaded from buffer - update references                for &ref_idx in ref_indices {                    let reference = &mut references[ref_idx];                    let line_num = reference.entry.line_number - 1; // 0-based                    if let Some(line) = line_map.get(&line_num) {                        reference.update_line_content(line, ctx);                    }                }            }        }         // For any references still without line_content, spawn async file reads        for (file_path, ref_indices) in file_to_refs {            // Check if any refs for this file still need loading            let needs_loading: Vec<usize> = ref_indices                .iter()                .copied()                .filter(|&idx| references[idx].entry.line_content.is_none())                .collect();             if needs_loading.is_empty() {                continue;            }             // Collect line numbers for this file (0-based)            let line_numbers: Vec<usize> = needs_loading                .iter()                .map(|&idx| references[idx].entry.line_number - 1)                .collect();             let file_path_clone = file_path.clone();            ctx.spawn(                async move { FileModel::read_lines_async(&file_path_clone, line_numbers).await },                move |me, result, ctx| {                    if let Ok(lines) = result {                        // Build a map from line number to content for quick lookup                        let line_map: HashMap<usize, &String> =                            lines.iter().map(|(ln, content)| (*ln, content)).collect();                         for &ref_idx in &needs_loading {                            if ref_idx >= me.references.len() {                                continue;                            }                            let reference = &mut me.references[ref_idx];                            let line_num = reference.entry.line_number - 1; // 0-based                            if let Some(line) = line_map.get(&line_num) {                                reference.update_line_content(line, ctx);                            }                        }                        ctx.notify(); // Trigger re-render                    }                },            );        }         Self {            references,            request_offset,            scroll_state: ClippedScrollStateHandle::default(),            back_mouse_state: MouseStateHandle::default(),            selected_reference_index: 0,        }    }     /// Returns the request offset for positioning the card.    pub fn request_offset(&self) -> CharOffset {        self.request_offset    }     /// Gets a reference by index.    pub fn get_reference(&self, index: usize) -> Option<&FlatReferenceEntry> {        self.references.get(index).map(|r| &r.entry)    }     /// Creates a read-only code editor view for a single reference line.    fn create_editor_view_for_reference(        reference: &FlatReferenceEntry,        ctx: &mut ViewContext<Self>,    ) -> ViewHandle<CodeEditorView> {        let view = ctx.add_typed_action_view(|ctx| {            let mut editor_view = CodeEditorView::new(                None,                None,                CodeEditorRenderOptions::new(VerticalExpansionBehavior::InfiniteHeight),                ctx,            )            .with_can_show_diff_ui(false)            .with_show_line_numbers(false)            .with_horizontal_scrollbar_appearance(                warpui::elements::new_scrollable::ScrollableAppearance::new(                    warpui::elements::ScrollbarWidth::None,                    false,                ),            );             editor_view.set_vertical_scrollbar_appearance(                warpui::elements::new_scrollable::ScrollableAppearance::new(                    warpui::elements::ScrollbarWidth::None,                    false,                ),            );             editor_view        });         // Initialize the editor with the reference line content (or empty if still loading)        let content = reference.line_content.clone().unwrap_or_default();        let file_path = reference.file_path.clone();        view.update(ctx, |view, ctx| {            view.set_show_current_line_highlights(false, ctx);            view.set_interaction_state(InteractionState::Disabled, ctx);             // Set up syntax highlighting based on file extension            view.set_language_with_local_path(&file_path, ctx);             // Reset with the reference line content            let state = InitialBufferState::plain_text(&content);            view.reset(state, ctx);        });         view    }} impl Entity for FindReferencesView {    type Event = FindReferencesViewEvent;} impl TypedActionView for FindReferencesView {    type Action = FindReferencesViewAction;     fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {        match action {            FindReferencesViewAction::GotoReference(index) => {                ctx.emit(FindReferencesViewEvent::GotoReference(*index));            }            FindReferencesViewAction::Close => {                ctx.emit(FindReferencesViewEvent::CloseRequested);            }            FindReferencesViewAction::ArrowUp => {                if !self.references.is_empty() {                    self.selected_reference_index =                        (self.selected_reference_index + self.references.len() - 1)                            % self.references.len();                    ctx.notify();                }            }            FindReferencesViewAction::ArrowDown => {                if !self.references.is_empty() {                    self.selected_reference_index =                        (self.selected_reference_index + 1) % self.references.len();                    ctx.notify();                }            }            FindReferencesViewAction::SelectReference => {                if self.selected_reference_index < self.references.len() {                    ctx.emit(FindReferencesViewEvent::GotoReference(                        self.selected_reference_index,                    ));                }            }        }    }} impl View for FindReferencesView {    fn ui_name() -> &'static str {        "FindReferencesView"    }     fn keymap_context(&self, _app: &AppContext) -> warpui::keymap::Context {        let mut context = Self::default_keymap_context();        if !self.references.is_empty() {            context.set.insert(HAS_REFERENCES);        }        context    }     fn render(&self, app: &AppContext) -> Box<dyn Element> {        if self.references.is_empty() {            return warpui::elements::Empty::new().finish();        }         let appearance = Appearance::handle(app).as_ref(app);        let theme = appearance.theme();         // Header with back arrow and "Showing X references"        let header = render_header(self.back_mouse_state.clone(), self.references.len(), app);         // Content: flat list of reference entries        let mut content_column =            Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);         for (index, reference) in self.references.iter().enumerate() {            let is_selected = index == self.selected_reference_index;            let entry = render_reference_entry(                &reference.entry,                reference.entry_mouse_state.clone(),                reference.file_name_mouse_state.clone(),                &reference.editor_view,                index,                is_selected,                app,            );            content_column.add_child(entry);        }         // Make content scrollable        let scrollable_content = ClippedScrollable::vertical(            self.scroll_state.clone(),            content_column.finish(),            ScrollbarWidth::None,            theme.disabled_ui_text_color().into(),            theme.active_ui_text_color().into(),            Fill::None,        )        .finish();         // Combine header and content        let card_content = Flex::column()            .with_cross_axis_alignment(CrossAxisAlignment::Stretch)            .with_child(header)            .with_child(Shrinkable::new(1., scrollable_content).finish())            .finish();         ConstrainedBox::new(            Container::new(card_content)                .with_background_color(internal_colors::neutral_1(theme))                .with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))                .with_border(Border::all(1.).with_border_fill(internal_colors::neutral_4(theme)))                .finish(),        )        .with_max_height(FIND_REFERENCES_CARD_MAX_HEIGHT)        .finish()    }} /// A single reference entry with file information for flat list display.#[derive(Debug, Clone)]pub struct FlatReferenceEntry {    /// The absolute file path.    pub file_path: PathBuf,    /// The display file name (just the file name, not full path).    pub file_name: String,    /// The relative path from workspace root (for tooltip display).    pub display_path: String,    /// 1-based line number where the reference appears.    pub line_number: usize,    /// 0-based column number where the reference starts (adjusted for trimmed whitespace).    pub column: usize,    /// The content of the line containing the reference (trimmed).    /// None indicates the line is still loading from disk.    pub line_content: Option<String>,} /// Renders the card header with back arrow and "Showing X references" text.fn render_header(    back_mouse_state: MouseStateHandle,    total_refs: usize,    app: &AppContext,) -> Box<dyn Element> {    let appearance = Appearance::handle(app).as_ref(app);    let theme = appearance.theme();     // "Showing X references" title    let title_text = if total_refs == 1 {        "Showing 1 reference".to_string()    } else {

Lines 1–500 of 696.

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t