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 _, 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 point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot()).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot()).is_ge() } (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(parse_uri_fragment_position) .unwrap_or_default(); return HoverLink::LspLocation( lsp::Location { uri, range: lsp::Range::new(position, position), }, server_id, ); } HoverLink::Url(target.to_string())} /// Parse a URI fragment such as `9,16`, `9:16`, `L9`, or `L9:16` into an/// LSP position (1-based input, 0-based output). Servers like the JSON/// language server attach this fragment to `file://` document link/// targets to point at a specific row/column inside the file.fn parse_uri_fragment_position(fragment: &str) -> Option<lsp::Position> { let stripped = fragment.strip_prefix('L').unwrap_or(fragment); let (line_str, column_str) = match stripped.split_once([',', ':']) { Some((line, column)) => (line, Some(column)), None => (stripped, None), }; let line = line_str.parse::<u32>().ok()?.checked_sub(1)?; let character = column_str .and_then(|column| column.parse::<u32>().ok()) .and_then(|column| column.checked_sub(1)) .unwrap_or(0); Some(lsp::Position { line, character })} #[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(), )?;Lines 1–500 of 3252. Load more
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 _, 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 point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot()).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot()).is_ge() } (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(parse_uri_fragment_position) .unwrap_or_default(); return HoverLink::LspLocation( lsp::Location { uri, range: lsp::Range::new(position, position), }, server_id, ); } HoverLink::Url(target.to_string()) } /// Parse a URI fragment such as `9,16`, `9:16`, `L9`, or `L9:16` into an /// LSP position (1-based input, 0-based output). Servers like the JSON /// language server attach this fragment to `file://` document link /// targets to point at a specific row/column inside the file. fn parse_uri_fragment_position(fragment: &str) -> Option<lsp::Position> { let stripped = fragment.strip_prefix('L').unwrap_or(fragment); let (line_str, column_str) = match stripped.split_once([',', ':']) { Some((line, column)) => (line, Some(column)), None => (stripped, None), }; let line = line_str.parse::<u32>().ok()?.checked_sub(1)?; let character = column_str .and_then(|column| column.parse::<u32>().ok()) .and_then(|column| column.checked_sub(1)) .unwrap_or(0); Some(lsp::Position { line, character }) } #[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(), )?;