3262 lines · 117.1 KB · Rust
RawDownload
use crate::{    Anchor, Editor, EditorSettings, EditorSnapshot, FindAllReferences, GoToDefinition,    GoToDefinitionSplit, GoToTypeDefinition, GoToTypeDefinitionSplit, GotoDefinitionKind,    HighlightKey, Navigated, PointForPosition, SelectPhase,    editor_settings::GoToDefinitionFallback, scroll::ScrollAmount,};use gpui::{    App, AsyncWindowContext, Context, Entity, HighlightStyle, Modifiers, Pixels, Task,    UnderlineStyle, Window, px,};use language::{Bias, ToOffset};use linkify::{LinkFinder, LinkKind};use lsp::LanguageServerId;use project::{InlayId, LocationLink, Project, ResolvedPath};use regex::Regex;use settings::Settings;use std::{ops::Range, str::FromStr as _, sync::LazyLock};use text::OffsetRangeExt;use theme::ActiveTheme as _;use util::{    ResultExt, TryFutureExt as _, markdown::source_position_from_fragment, paths::PathWithPosition,}; #[derive(Debug)]pub struct HoveredLinkState {    pub last_trigger_point: TriggerPoint,    pub preferred_kind: GotoDefinitionKind,    pub symbol_range: Option<RangeInEditor>,    pub links: Vec<HoverLink>,    pub task: Option<Task<Option<()>>>,} #[derive(Debug, Eq, PartialEq, Clone)]pub enum RangeInEditor {    Text(Range<Anchor>),    Inlay(InlayHighlight),} impl RangeInEditor {    pub fn as_text_range(&self) -> Option<Range<Anchor>> {        match self {            Self::Text(range) => Some(range.clone()),            Self::Inlay(_) => None,        }    }     pub fn point_within_range(        &self,        trigger_point: &TriggerPoint,        snapshot: &EditorSnapshot,    ) -> bool {        match (self, trigger_point) {            (Self::Text(range), TriggerPoint::Text(point)) => {                let buffer_snapshot = snapshot.buffer_snapshot();                if !range.start.is_valid(&buffer_snapshot)                    || !range.end.is_valid(&buffer_snapshot)                    || !point.is_valid(&buffer_snapshot)                {                    return false;                }                let point_after_start = range.start.cmp(point, &buffer_snapshot).is_le();                let point_after_end = range.end.cmp(point, &buffer_snapshot).is_ge();                point_after_start && point_after_end            }            (Self::Inlay(highlight), TriggerPoint::InlayHint(point, _, _)) => {                highlight.inlay == point.inlay                    && highlight.range.contains(&point.range.start)                    && highlight.range.contains(&point.range.end)            }            (Self::Inlay(_), TriggerPoint::Text(_))            | (Self::Text(_), TriggerPoint::InlayHint(_, _, _)) => false,        }    }} #[derive(Debug, Clone)]pub enum HoverLink {    Url(String),    File(ResolvedFileTarget),    Text(LocationLink),    /// Navigate to an LSP-given location whose buffer may not be loaded yet.    /// Used by inlay-hint hover, code-lens references, and document-link    /// targets that point inside a workspace file (e.g. `file:///foo#9,16`).    LspLocation(lsp::Location, LanguageServerId),} /// Convert a `documentLink` target URI into a [`HoverLink`], reusing the/// existing navigation paths: `file://` URIs go through the LSP location/// pipeline (so an optional `#line[,column]` fragment is honored), while/// any other scheme is opened as a regular URL.pub fn document_link_target_to_hover_link(target: &str, server_id: LanguageServerId) -> HoverLink {    if let Ok(url) = url::Url::parse(target)        && url.scheme() == "file"        && let Ok(uri) = lsp::Uri::from_str(target)    {        let position = url            .fragment()            .and_then(source_position_from_fragment)            .map(|(line, character)| lsp::Position { line, character })            .unwrap_or_default();        return HoverLink::LspLocation(            lsp::Location {                uri,                range: lsp::Range::new(position, position),            },            server_id,        );    }    HoverLink::Url(target.to_string())} #[derive(Debug, Clone, PartialEq, Eq)]pub struct InlayHighlight {    pub inlay: InlayId,    pub inlay_position: Anchor,    pub range: Range<usize>,} #[derive(Debug, Clone, PartialEq)]pub enum TriggerPoint {    Text(Anchor),    InlayHint(InlayHighlight, lsp::Location, LanguageServerId),} impl TriggerPoint {    fn anchor(&self) -> &Anchor {        match self {            TriggerPoint::Text(anchor) => anchor,            TriggerPoint::InlayHint(inlay_range, _, _) => &inlay_range.inlay_position,        }    }} pub fn exclude_link_to_position(    buffer: &Entity<language::Buffer>,    current_position: &text::Anchor,    location: &LocationLink,    cx: &App,) -> bool {    // Exclude definition links that points back to cursor position.    // (i.e., currently cursor upon definition).    let snapshot = buffer.read(cx).snapshot();    !(buffer == &location.target.buffer        && current_position            .bias_right(&snapshot)            .cmp(&location.target.range.start, &snapshot)            .is_ge()        && current_position            .cmp(&location.target.range.end, &snapshot)            .is_le())} impl Editor {    pub(crate) fn update_hovered_link(        &mut self,        point_for_position: PointForPosition,        mouse_position: Option<gpui::Point<Pixels>>,        snapshot: &EditorSnapshot,        modifiers: Modifiers,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let hovered_link_modifier = Editor::is_cmd_or_ctrl_pressed(&modifiers, cx);        if !hovered_link_modifier || self.has_pending_selection() {            self.hide_hovered_link(cx);            return;        }         if !cx.is_cursor_visible() {            self.hide_hovered_link(cx);            return;        }         match point_for_position.as_valid() {            Some(point) => {                let trigger_point = TriggerPoint::Text(                    snapshot                        .buffer_snapshot()                        .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)),                );                 show_link_definition(modifiers.shift, self, trigger_point, snapshot, window, cx);            }            None => {                self.update_inlay_link_and_hover_points(                    snapshot,                    point_for_position,                    mouse_position,                    hovered_link_modifier,                    modifiers.shift,                    window,                    cx,                );            }        }    }     pub(crate) fn hide_hovered_link(&mut self, cx: &mut Context<Self>) {        self.hovered_link_state.take();        self.clear_highlights(HighlightKey::HoveredLinkState, cx);    }     pub(crate) fn handle_click_hovered_link(        &mut self,        point: PointForPosition,        modifiers: Modifiers,        window: &mut Window,        cx: &mut Context<Editor>,    ) {        let reveal_task = self.cmd_click_reveal_task(point, modifiers, window, cx);        cx.spawn_in(window, async move |editor, cx| {            let definition_revealed = reveal_task.await.log_err().unwrap_or(Navigated::No);            let find_references = editor                .update_in(cx, |editor, window, cx| {                    if definition_revealed == Navigated::Yes {                        return None;                    }                    match EditorSettings::get_global(cx).go_to_definition_fallback {                        GoToDefinitionFallback::None => None,                        GoToDefinitionFallback::FindAllReferences => {                            editor.find_all_references(&FindAllReferences::default(), window, cx)                        }                    }                })                .ok()                .flatten();            if let Some(find_references) = find_references {                find_references.await.log_err();            }        })        .detach();    }     pub fn scroll_hover(        &mut self,        amount: ScrollAmount,        window: &mut Window,        cx: &mut Context<Self>,    ) -> bool {        let selection = self.selections.newest_anchor().head();        let snapshot = self.snapshot(window, cx);         if let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {            popover                .symbol_range                .point_within_range(&TriggerPoint::Text(selection), &snapshot)        }) {            popover.scroll(amount, window, cx);            true        } else if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() {            context_menu.scroll_aside(amount, window, cx);            true        } else {            false        }    }     fn cmd_click_reveal_task(        &mut self,        point: PointForPosition,        modifiers: Modifiers,        window: &mut Window,        cx: &mut Context<Editor>,    ) -> Task<anyhow::Result<Navigated>> {        if let Some(hovered_link_state) = self.hovered_link_state.take() {            self.hide_hovered_link(cx);            if !hovered_link_state.links.is_empty() {                if !self.focus_handle.is_focused(window) {                    window.focus(&self.focus_handle, cx);                }                 // exclude links pointing back to the current anchor                let current_position = point                    .next_valid                    .to_point(&self.snapshot(window, cx).display_snapshot);                let Some((buffer, anchor)) = self                    .buffer()                    .read(cx)                    .text_anchor_for_position(current_position, cx)                else {                    return Task::ready(Ok(Navigated::No));                };                let Some(multi_buffer_anchor) = self                    .buffer()                    .read(cx)                    .snapshot(cx)                    .anchor_in_excerpt(anchor)                else {                    return Task::ready(Ok(Navigated::No));                };                let links = hovered_link_state                    .links                    .into_iter()                    .filter(|link| {                        if let HoverLink::Text(location) = link {                            exclude_link_to_position(&buffer, &anchor, location, cx)                        } else {                            true                        }                    })                    .collect();                let nav_entry = self.navigation_entry(multi_buffer_anchor, cx);                let split = Self::is_alt_pressed(&modifiers, cx);                let navigate_task =                    self.navigate_to_hover_links(None, links, nav_entry, split, window, cx);                self.select(SelectPhase::End, window, cx);                return navigate_task;            }        }         // We don't have the correct kind of link cached, set the selection on        // click and immediately trigger GoToDefinition.        self.select(            SelectPhase::Begin {                position: point.next_valid,                add: false,                click_count: 1,            },            window,            cx,        );         let navigate_task = if point.as_valid().is_some() {            let split = Self::is_alt_pressed(&modifiers, cx);            match (modifiers.shift, split) {                (true, true) => {                    self.go_to_type_definition_split(&GoToTypeDefinitionSplit, window, cx)                }                (true, false) => self.go_to_type_definition(&GoToTypeDefinition, window, cx),                (false, true) => self.go_to_definition_split(&GoToDefinitionSplit, window, cx),                (false, false) => self.go_to_definition(&GoToDefinition::default(), window, cx),            }        } else {            Task::ready(Ok(Navigated::No))        };        self.select(SelectPhase::End, window, cx);        navigate_task    }} pub fn show_link_definition(    shift_held: bool,    editor: &mut Editor,    trigger_point: TriggerPoint,    snapshot: &EditorSnapshot,    window: &mut Window,    cx: &mut Context<Editor>,) {    let preferred_kind = match trigger_point {        TriggerPoint::Text(_) if !shift_held => GotoDefinitionKind::Symbol,        _ => GotoDefinitionKind::Type,    };     let (mut hovered_link_state, is_cached) =        if let Some(existing) = editor.hovered_link_state.take() {            (existing, true)        } else {            (                HoveredLinkState {                    last_trigger_point: trigger_point.clone(),                    symbol_range: None,                    preferred_kind,                    links: vec![],                    task: None,                },                false,            )        };     if editor.pending_rename.is_some() {        return;    }     let anchor = trigger_point.anchor().bias_left(snapshot.buffer_snapshot());    let Some((anchor, _)) = snapshot.buffer_snapshot().anchor_to_buffer_anchor(anchor) else {        return;    };    let Some(buffer) = editor.buffer.read(cx).buffer(anchor.buffer_id) else {        return;    };    let same_kind = hovered_link_state.preferred_kind == preferred_kind        || hovered_link_state            .links            .first()            .is_some_and(|d| matches!(d, HoverLink::Url(_) | HoverLink::LspLocation(_, _)));     if same_kind {        if is_cached && (hovered_link_state.last_trigger_point == trigger_point)            || hovered_link_state                .symbol_range                .as_ref()                .is_some_and(|symbol_range| {                    symbol_range.point_within_range(&trigger_point, snapshot)                })        {            editor.hovered_link_state = Some(hovered_link_state);            return;        }    } else {        editor.hide_hovered_link(cx)    }    let project = editor.project.clone();    let provider = editor.semantics_provider.clone();     // Record the requested position so a mouse move on the same point short-circuits    // instead of re-querying, even when the server returns no `originSelectionRange`    // (which would otherwise leave `symbol_range` empty).    hovered_link_state.last_trigger_point = trigger_point.clone();     hovered_link_state.task = Some(cx.spawn_in(window, async move |this, cx| {        async move {            // LSP document links take priority: the server explicitly            // declares which ranges are clickable, so they are more            // accurate than the heuristic-based URL/file detection.            //            // Resolution is deduplicated by `LspStore`; awaiting here only            // blocks until either the cached resolved entry is returned or            // the in-flight `Shared` task completes.            let resolved_document_links = this                .update(cx, |editor, cx| {                    editor.document_links_at(buffer.clone(), anchor, cx)                })                .ok()                .flatten();            let resolved_document_links = match resolved_document_links {                Some(task) => task.await,                None => Vec::new(),            };            let snapshot = this.read_with(cx, |editor, cx| editor.buffer.read(cx).snapshot(cx))?;            let detected_document_link =                resolved_document_links                    .into_iter()                    .find_map(|(server_id, link)| {                        let multi_buffer_range =                            snapshot.buffer_anchor_range_to_anchor_range(link.range.clone())?;                        Some((link.range, multi_buffer_range, link.target, server_id))                    });            drop(snapshot);             let result = match &trigger_point {                TriggerPoint::Text(_) => {                    let mut links = Vec::new();                    let mut symbol_range = None;                     // LSP-provided document link wins over heuristic URL/file                    // detection at the same position: the server tells us the                    // exact range and target, while `find_url`/`find_file` are                    // best-effort text matches.                    if let Some((_, multi_buffer_range, Some(target), server_id)) =                        detected_document_link.clone()                    {                        symbol_range = Some(RangeInEditor::Text(multi_buffer_range));                        links.push(document_link_target_to_hover_link(&target, server_id));                    } else if let Some((url_range, url)) = find_url(&buffer, anchor, cx) {                        let snapshot =                            this.read_with(cx, |editor, cx| editor.buffer.read(cx).snapshot(cx))?;                        if let Some(range) = snapshot.buffer_anchor_range_to_anchor_range(url_range)                        {                            symbol_range = Some(RangeInEditor::Text(range));                        }                        links.push(HoverLink::Url(url));                    } else if let Some((filename_range, file_target)) =                        find_file(&buffer, project.clone(), anchor, cx).await                    {                        let snapshot =                            this.read_with(cx, |editor, cx| editor.buffer.read(cx).snapshot(cx))?;                        if let Some(range) =                            snapshot.buffer_anchor_range_to_anchor_range(filename_range)                        {                            symbol_range = Some(RangeInEditor::Text(range));                        }                        links.push(HoverLink::File(file_target));                    }                     // Always also collect LSP definitions so that cmd-click                    // reveals every applicable target (e.g. a position that                    // carries both a document link and a definition).                    if let Some(provider) = provider {                        let task = cx.update(|_, cx| {                            provider.definitions(&buffer, anchor, preferred_kind, cx)                        })?;                        if let Some(task) = task                            && let Some(definition_result) = task.await.ok().flatten()                        {                            if symbol_range.is_none() {                                let snapshot = this.read_with(cx, |editor, cx| {                                    editor.buffer.read(cx).snapshot(cx)                                })?;                                symbol_range = definition_result.iter().find_map(|link| {                                    link.origin.as_ref().and_then(|origin| {                                        let range = snapshot.buffer_anchor_range_to_anchor_range(                                            origin.range.clone(),                                        )?;                                        Some(RangeInEditor::Text(range))                                    })                                });                            }                            links.extend(definition_result.into_iter().map(HoverLink::Text));                        }                    }

Lines 1–500 of 3262.