diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index 2960b9da83..fa20b8a1af 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -1,12 +1,12 @@ use crate::docstring::Docstring; use crate::goto::{GotoTarget, find_goto_target}; -use crate::{Db, MarkupKind, RangedValue}; +use crate::{Db, HasNavigationTargets, MarkupKind, NavigationTarget, RangedValue}; use ruff_db::files::{File, FileRange}; use ruff_db::parsed::parsed_module; use ruff_text_size::{Ranged, TextSize}; use std::fmt; use std::fmt::Formatter; -use ty_python_semantic::types::{KnownInstanceType, Type, TypeVarVariance}; +use ty_python_semantic::types::{KnownInstanceType, Type, TypeDetail, TypeVarVariance}; use ty_python_semantic::{DisplaySettings, SemanticModel}; pub fn hover(db: &dyn Db, file: File, offset: TextSize) -> Option>> { @@ -59,13 +59,21 @@ pub struct Hover<'db> { contents: Vec>, } +type Linkify<'a> = &'a dyn Fn(&NavigationTarget) -> Option; + impl<'db> Hover<'db> { /// Renders the hover to a string using the specified markup kind. - pub const fn display<'a>(&'a self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHover<'db, 'a> { + pub const fn display<'a>( + &'a self, + db: &'db dyn Db, + kind: MarkupKind, + linkify: Linkify<'a>, + ) -> DisplayHover<'db, 'a> { DisplayHover { db, hover: self, kind, + linkify, } } @@ -96,6 +104,7 @@ pub struct DisplayHover<'db, 'a> { db: &'db dyn Db, hover: &'a Hover<'db>, kind: MarkupKind, + linkify: Linkify<'a>, } impl fmt::Display for DisplayHover<'_, '_> { @@ -106,7 +115,7 @@ impl fmt::Display for DisplayHover<'_, '_> { self.kind.horizontal_line().fmt(f)?; } - content.display(self.db, self.kind).fmt(f)?; + content.display(self.db, self.kind, self.linkify).fmt(f)?; first = false; } @@ -122,11 +131,17 @@ pub enum HoverContent<'db> { } impl<'db> HoverContent<'db> { - fn display(&self, db: &'db dyn Db, kind: MarkupKind) -> DisplayHoverContent<'_, 'db> { + fn display<'a>( + &'a self, + db: &'db dyn Db, + kind: MarkupKind, + linkify: Linkify<'a>, + ) -> DisplayHoverContent<'a, 'db> { DisplayHoverContent { db, content: self, kind, + linkify, } } } @@ -135,6 +150,7 @@ pub(crate) struct DisplayHoverContent<'a, 'db> { db: &'db dyn Db, content: &'a HoverContent<'db>, kind: MarkupKind, + linkify: Linkify<'a>, } impl fmt::Display for DisplayHoverContent<'_, '_> { @@ -151,21 +167,84 @@ impl fmt::Display for DisplayHoverContent<'_, '_> { Some(TypeVarVariance::Bivariant) => " (bivariant)", None => "", }; - self.kind - .fenced_code_block( - format!( - "{}{variance}", - ty.display_with(self.db, DisplaySettings::default().multiline()) - ), - "python", - ) - .fmt(f) + + if self.kind == MarkupKind::Markdown { + let details = ty.display(self.db).to_string_parts(); + + // Ok so the idea here is that we potentially have a random soup of spans here, + // and each byte of the string can have at most one target associate with it. + // Thankfully, they were generally pushed in print order, with the inner smaller types + // appearing before the outer bigger ones. + // + // So we record where we are in the string, and every time we find a type, we + // check if it's further along in the string. If it is, great, we give it the + // span for its range, and then advance where we are. + let mut offset = 0; + for (target, detail) in details.targets.iter().zip(&details.details) { + match detail { + TypeDetail::Type(ty) => { + let start = target.start().to_usize(); + let end = target.end().to_usize(); + // If we skipped over some bytes, push them with no target + if start > offset { + write!(f, "{}", escape(&details.label[offset..start]))?; + } + // Ok, this is the first type that claimed these bytes, give it the target + if start >= offset { + if let Some(target) = + ty.navigation_targets(self.db).into_iter().next() + && let Some(uri) = (self.linkify)(&target) + { + write!( + f, + "[{}]({})", + escape(&details.label[start..end]), + uri + )?; + } else { + write!(f, "{}", escape(&details.label[start..end]))?; + } + offset = end; + } + } + TypeDetail::SignatureStart + | TypeDetail::SignatureEnd + | TypeDetail::Parameter(_) => { + // Don't care about these + } + } + } + // "flush" the rest of the label without any target + if offset < details.label.len() { + write!(f, "{}", escape(&details.label[offset..details.label.len()]))?; + } + write!(f, "{}", escape(variance)) + } else { + self.kind + .fenced_code_block( + format!( + "{}{variance}", + ty.display_with(self.db, DisplaySettings::default().multiline()) + ), + "python", + ) + .fmt(f) + } } HoverContent::Docstring(docstring) => docstring.render(self.kind).fmt(f), } } } +fn escape(x: &str) -> String { + x.replace('[', "\\[") + .replace(']', "\\]") + .replace('(', "\\(") + .replace(')', "\\)") + .replace('`', "\\`") + .replace('#', "\\#") +} + #[cfg(test)] mod tests { use crate::tests::{CursorTest, cursor_test}; @@ -3670,9 +3749,9 @@ def function(): write!( &mut buf, "{plaintext}{line}{markdown}{line}", - plaintext = hover.display(&self.db, MarkupKind::PlainText), + plaintext = hover.display(&self.db, MarkupKind::PlainText, &|_| None), line = MarkupKind::PlainText.horizontal_line(), - markdown = hover.display(&self.db, MarkupKind::Markdown), + markdown = hover.display(&self.db, MarkupKind::Markdown, &|_| None), ) .unwrap(); diff --git a/crates/ty_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs index 3c37b3d581..bee8383139 100644 --- a/crates/ty_server/src/server/api/requests/hover.rs +++ b/crates/ty_server/src/server/api/requests/hover.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; -use crate::document::{FileRangeExt, PositionExt}; +use crate::document::{FileRangeExt, PositionExt, ToRangeExt}; use crate::server::api::traits::{ BackgroundDocumentRequestHandler, RequestHandler, RetriableRequestHandler, }; @@ -8,7 +8,7 @@ use crate::session::DocumentSnapshot; use crate::session::client::Client; use lsp_types::request::HoverRequest; use lsp_types::{HoverContents, HoverParams, MarkupContent, Url}; -use ty_ide::{MarkupKind, hover}; +use ty_ide::{MarkupKind, NavigationTarget, hover}; use ty_project::ProjectDatabase; pub(crate) struct HoverRequestHandler; @@ -61,7 +61,23 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler { (MarkupKind::PlainText, lsp_types::MarkupKind::PlainText) }; - let contents = range_info.display(db, markup_kind).to_string(); + let to_lsp_link = |target: &NavigationTarget| -> Option { + let file = target.file(); + let location = target + .focus_range() + .to_lsp_range(db, file, snapshot.encoding())? + .to_location()?; + let uri = location.uri; + let line1 = location.range.start.line; + let char1 = location.range.start.character; + let line2 = location.range.end.line; + let char2 = location.range.end.character; + Some(format!("{uri}#L{line1}C{char1}-L{line2}C{char2}")) + }; + + let contents = range_info + .display(db, markup_kind, &to_lsp_link) + .to_string(); Ok(Some(lsp_types::Hover { contents: HoverContents::Markup(MarkupContent { diff --git a/crates/ty_wasm/src/lib.rs b/crates/ty_wasm/src/lib.rs index 1ae085581c..1d11e1c6d6 100644 --- a/crates/ty_wasm/src/lib.rs +++ b/crates/ty_wasm/src/lib.rs @@ -399,7 +399,7 @@ impl Workspace { Ok(Some(Hover { markdown: range_info - .display(&self.db, MarkupKind::Markdown) + .display(&self.db, MarkupKind::Markdown, &|_| None) .to_string(), range: source_range, }))