mirror of
https://github.com/denoland/deno.git
synced 2024-12-26 00:59:24 -05:00
feat(lsp): support linking to symbols in JSDoc on hover (#13631)
Closes #13198
This commit is contained in:
parent
773f882e5e
commit
2f2c778a07
3 changed files with 250 additions and 42 deletions
|
@ -1105,7 +1105,7 @@ impl Inner {
|
|||
error!("Unable to get quick info: {}", err);
|
||||
LspError::internal_error()
|
||||
})?;
|
||||
maybe_quick_info.map(|qi| qi.to_hover(line_index))
|
||||
maybe_quick_info.map(|qi| qi.to_hover(line_index, self))
|
||||
};
|
||||
self.performance.measure(mark);
|
||||
Ok(hover)
|
||||
|
@ -1724,7 +1724,7 @@ impl Inner {
|
|||
},
|
||||
)?;
|
||||
if let Some(completion_info) = maybe_completion_info {
|
||||
completion_info.as_completion_item(¶ms)
|
||||
completion_info.as_completion_item(¶ms, self)
|
||||
} else {
|
||||
error!(
|
||||
"Received an undefined response from tsc for completion details."
|
||||
|
@ -2265,7 +2265,7 @@ impl Inner {
|
|||
})?;
|
||||
|
||||
if let Some(signature_help_items) = maybe_signature_help_items {
|
||||
let signature_help = signature_help_items.into_signature_help();
|
||||
let signature_help = signature_help_items.into_signature_help(self);
|
||||
self.performance.measure(mark);
|
||||
Ok(Some(signature_help))
|
||||
} else {
|
||||
|
|
221
cli/lsp/tsc.rs
221
cli/lsp/tsc.rs
|
@ -72,6 +72,8 @@ static CODEBLOCK_RE: Lazy<Regex> =
|
|||
Lazy::new(|| Regex::new(r"^\s*[~`]{3}").unwrap());
|
||||
static EMAIL_MATCH_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"(.+)\s<([-.\w]+@[-.\w]+)>").unwrap());
|
||||
static HTTP_RE: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r#"(?i)^https?:"#).unwrap());
|
||||
static JSDOC_LINKS_RE: Lazy<Regex> = Lazy::new(|| {
|
||||
Regex::new(r"(?i)\{@(link|linkplain|linkcode) (https?://[^ |}]+?)(?:[| ]([^{}\n]+?))?\}").unwrap()
|
||||
});
|
||||
|
@ -346,19 +348,14 @@ async fn get_asset(
|
|||
}
|
||||
}
|
||||
|
||||
fn display_parts_to_string(parts: &[SymbolDisplayPart]) -> String {
|
||||
parts
|
||||
.iter()
|
||||
.map(|p| p.text.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
fn get_tag_body_text(tag: &JsDocTagInfo) -> Option<String> {
|
||||
fn get_tag_body_text(
|
||||
tag: &JsDocTagInfo,
|
||||
language_server: &language_server::Inner,
|
||||
) -> Option<String> {
|
||||
tag.text.as_ref().map(|display_parts| {
|
||||
// TODO(@kitsonk) check logic in vscode about handling this API change in
|
||||
// tsserver
|
||||
let text = display_parts_to_string(display_parts);
|
||||
let text = display_parts_to_string(display_parts, language_server);
|
||||
match tag.name.as_str() {
|
||||
"example" => {
|
||||
if CAPTION_RE.is_match(&text) {
|
||||
|
@ -380,13 +377,16 @@ fn get_tag_body_text(tag: &JsDocTagInfo) -> Option<String> {
|
|||
})
|
||||
}
|
||||
|
||||
fn get_tag_documentation(tag: &JsDocTagInfo) -> String {
|
||||
fn get_tag_documentation(
|
||||
tag: &JsDocTagInfo,
|
||||
language_server: &language_server::Inner,
|
||||
) -> String {
|
||||
match tag.name.as_str() {
|
||||
"augments" | "extends" | "param" | "template" => {
|
||||
if let Some(display_parts) = &tag.text {
|
||||
// TODO(@kitsonk) check logic in vscode about handling this API change
|
||||
// in tsserver
|
||||
let text = display_parts_to_string(display_parts);
|
||||
let text = display_parts_to_string(display_parts, language_server);
|
||||
let body: Vec<&str> = PART_RE.split(&text).collect();
|
||||
if body.len() == 3 {
|
||||
let param = body[1];
|
||||
|
@ -406,7 +406,7 @@ fn get_tag_documentation(tag: &JsDocTagInfo) -> String {
|
|||
_ => (),
|
||||
}
|
||||
let label = format!("*@{}*", tag.name);
|
||||
let maybe_text = get_tag_body_text(tag);
|
||||
let maybe_text = get_tag_body_text(tag, language_server);
|
||||
if let Some(text) = maybe_text {
|
||||
if text.contains('\n') {
|
||||
format!("{} \n{}", label, text)
|
||||
|
@ -427,9 +427,9 @@ fn make_codeblock(text: &str) -> String {
|
|||
}
|
||||
|
||||
/// Replace JSDoc like links (`{@link http://example.com}`) with markdown links
|
||||
fn replace_links(text: &str) -> String {
|
||||
fn replace_links<S: AsRef<str>>(text: S) -> String {
|
||||
JSDOC_LINKS_RE
|
||||
.replace_all(text, |c: &Captures| match &c[1] {
|
||||
.replace_all(text.as_ref(), |c: &Captures| match &c[1] {
|
||||
"linkcode" => format!(
|
||||
"[`{}`]({})",
|
||||
if c.get(3).is_none() {
|
||||
|
@ -656,6 +656,10 @@ impl TextSpan {
|
|||
pub struct SymbolDisplayPart {
|
||||
text: String,
|
||||
kind: String,
|
||||
// This is only on `JSDocLinkDisplayPart` which extends `SymbolDisplayPart`
|
||||
// but is only used as an upcast of a `SymbolDisplayPart` and not explicitly
|
||||
// returned by any API, so it is safe to add it as an optional value.
|
||||
target: Option<DocumentSpan>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
|
@ -676,15 +680,104 @@ pub struct QuickInfo {
|
|||
tags: Option<Vec<JsDocTagInfo>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Link {
|
||||
name: Option<String>,
|
||||
target: Option<DocumentSpan>,
|
||||
text: Option<String>,
|
||||
linkcode: bool,
|
||||
}
|
||||
|
||||
/// Takes `SymbolDisplayPart` items and converts them into a string, handling
|
||||
/// any `{@link Symbol}` and `{@linkcode Symbol}` JSDoc tags and linking them
|
||||
/// to the their source location.
|
||||
fn display_parts_to_string(
|
||||
parts: &[SymbolDisplayPart],
|
||||
language_server: &language_server::Inner,
|
||||
) -> String {
|
||||
let mut out = Vec::<String>::new();
|
||||
|
||||
let mut current_link: Option<Link> = None;
|
||||
for part in parts {
|
||||
match part.kind.as_str() {
|
||||
"link" => {
|
||||
if let Some(link) = current_link.as_mut() {
|
||||
if let Some(target) = &link.target {
|
||||
if let Some(specifier) = target.to_target(language_server) {
|
||||
let link_text = link.text.clone().unwrap_or_else(|| {
|
||||
link
|
||||
.name
|
||||
.clone()
|
||||
.map(|ref n| n.replace('`', "\\`"))
|
||||
.unwrap_or_else(|| "".to_string())
|
||||
});
|
||||
let link_str = if link.linkcode {
|
||||
format!("[`{}`]({})", link_text, specifier)
|
||||
} else {
|
||||
format!("[{}]({})", link_text, specifier)
|
||||
};
|
||||
out.push(link_str);
|
||||
}
|
||||
} else {
|
||||
let maybe_text = link.text.clone().or_else(|| link.name.clone());
|
||||
if let Some(text) = maybe_text {
|
||||
if HTTP_RE.is_match(&text) {
|
||||
let parts: Vec<&str> = text.split(' ').collect();
|
||||
if parts.len() == 1 {
|
||||
out.push(parts[0].to_string());
|
||||
} else {
|
||||
let link_text = parts[1..].join(" ").replace('`', "\\`");
|
||||
let link_str = if link.linkcode {
|
||||
format!("[`{}`]({})", link_text, parts[0])
|
||||
} else {
|
||||
format!("[{}]({})", link_text, parts[0])
|
||||
};
|
||||
out.push(link_str);
|
||||
}
|
||||
} else {
|
||||
out.push(text.replace('`', "\\`"));
|
||||
}
|
||||
}
|
||||
}
|
||||
current_link = None;
|
||||
} else {
|
||||
current_link = Some(Link {
|
||||
linkcode: part.text.as_str() == "{@linkcode ",
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
"linkName" => {
|
||||
if let Some(link) = current_link.as_mut() {
|
||||
link.name = Some(part.text.clone());
|
||||
link.target = part.target.clone();
|
||||
}
|
||||
}
|
||||
"linkText" => {
|
||||
if let Some(link) = current_link.as_mut() {
|
||||
link.name = Some(part.text.clone());
|
||||
}
|
||||
}
|
||||
_ => out.push(part.text.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
replace_links(out.join(""))
|
||||
}
|
||||
|
||||
impl QuickInfo {
|
||||
pub fn to_hover(&self, line_index: Arc<LineIndex>) -> lsp::Hover {
|
||||
let mut contents = Vec::<lsp::MarkedString>::new();
|
||||
pub(crate) fn to_hover(
|
||||
&self,
|
||||
line_index: Arc<LineIndex>,
|
||||
language_server: &language_server::Inner,
|
||||
) -> lsp::Hover {
|
||||
let mut parts = Vec::<lsp::MarkedString>::new();
|
||||
if let Some(display_string) = self
|
||||
.display_parts
|
||||
.clone()
|
||||
.map(|p| display_parts_to_string(&p))
|
||||
.map(|p| display_parts_to_string(&p, language_server))
|
||||
{
|
||||
contents.push(lsp::MarkedString::from_language_code(
|
||||
parts.push(lsp::MarkedString::from_language_code(
|
||||
"typescript".to_string(),
|
||||
display_string,
|
||||
));
|
||||
|
@ -692,31 +785,31 @@ impl QuickInfo {
|
|||
if let Some(documentation) = self
|
||||
.documentation
|
||||
.clone()
|
||||
.map(|p| display_parts_to_string(&p))
|
||||
.map(|p| display_parts_to_string(&p, language_server))
|
||||
{
|
||||
contents.push(lsp::MarkedString::from_markdown(documentation));
|
||||
parts.push(lsp::MarkedString::from_markdown(documentation));
|
||||
}
|
||||
if let Some(tags) = &self.tags {
|
||||
let tags_preview = tags
|
||||
.iter()
|
||||
.map(get_tag_documentation)
|
||||
.map(|tag_info| get_tag_documentation(tag_info, language_server))
|
||||
.collect::<Vec<String>>()
|
||||
.join(" \n\n");
|
||||
if !tags_preview.is_empty() {
|
||||
contents.push(lsp::MarkedString::from_markdown(format!(
|
||||
parts.push(lsp::MarkedString::from_markdown(format!(
|
||||
"\n\n{}",
|
||||
tags_preview
|
||||
)));
|
||||
}
|
||||
}
|
||||
lsp::Hover {
|
||||
contents: lsp::HoverContents::Array(contents),
|
||||
contents: lsp::HoverContents::Array(parts),
|
||||
range: Some(self.text_span.to_range(line_index)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DocumentSpan {
|
||||
text_span: TextSpan,
|
||||
|
@ -772,6 +865,36 @@ impl DocumentSpan {
|
|||
};
|
||||
Some(link)
|
||||
}
|
||||
|
||||
/// Convert the `DocumentSpan` into a specifier that can be sent to the client
|
||||
/// to link to the target document span. Used for converting JSDoc symbol
|
||||
/// links to markdown links.
|
||||
fn to_target(
|
||||
&self,
|
||||
language_server: &language_server::Inner,
|
||||
) -> Option<ModuleSpecifier> {
|
||||
let specifier = normalize_specifier(&self.file_name).ok()?;
|
||||
log::info!(
|
||||
"to_target file_name: {} specifier: {}",
|
||||
self.file_name,
|
||||
specifier
|
||||
);
|
||||
let asset_or_doc =
|
||||
language_server.get_maybe_cached_asset_or_document(&specifier)?;
|
||||
let line_index = asset_or_doc.line_index();
|
||||
let range = self.text_span.to_range(line_index);
|
||||
let mut target = language_server
|
||||
.url_map
|
||||
.normalize_specifier(&specifier)
|
||||
.ok()?;
|
||||
target.set_fragment(Some(&format!(
|
||||
"L{},{}",
|
||||
range.start.line + 1,
|
||||
range.start.character + 1
|
||||
)));
|
||||
|
||||
Some(target)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
@ -1750,23 +1873,27 @@ pub struct CompletionEntryDetails {
|
|||
}
|
||||
|
||||
impl CompletionEntryDetails {
|
||||
pub fn as_completion_item(
|
||||
pub(crate) fn as_completion_item(
|
||||
&self,
|
||||
original_item: &lsp::CompletionItem,
|
||||
language_server: &language_server::Inner,
|
||||
) -> lsp::CompletionItem {
|
||||
let detail = if original_item.detail.is_some() {
|
||||
original_item.detail.clone()
|
||||
} else if !self.display_parts.is_empty() {
|
||||
Some(replace_links(&display_parts_to_string(&self.display_parts)))
|
||||
Some(replace_links(&display_parts_to_string(
|
||||
&self.display_parts,
|
||||
language_server,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let documentation = if let Some(parts) = &self.documentation {
|
||||
let mut value = display_parts_to_string(parts);
|
||||
let mut value = display_parts_to_string(parts, language_server);
|
||||
if let Some(tags) = &self.tags {
|
||||
let tag_documentation = tags
|
||||
.iter()
|
||||
.map(get_tag_documentation)
|
||||
.map(|tag_info| get_tag_documentation(tag_info, language_server))
|
||||
.collect::<Vec<String>>()
|
||||
.join("");
|
||||
value = format!("{}\n\n{}", value, tag_documentation);
|
||||
|
@ -2155,12 +2282,15 @@ pub struct SignatureHelpItems {
|
|||
}
|
||||
|
||||
impl SignatureHelpItems {
|
||||
pub fn into_signature_help(self) -> lsp::SignatureHelp {
|
||||
pub(crate) fn into_signature_help(
|
||||
self,
|
||||
language_server: &language_server::Inner,
|
||||
) -> lsp::SignatureHelp {
|
||||
lsp::SignatureHelp {
|
||||
signatures: self
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|item| item.into_signature_information())
|
||||
.map(|item| item.into_signature_information(language_server))
|
||||
.collect(),
|
||||
active_parameter: Some(self.argument_index),
|
||||
active_signature: Some(self.selected_item_index),
|
||||
|
@ -2181,16 +2311,24 @@ pub struct SignatureHelpItem {
|
|||
}
|
||||
|
||||
impl SignatureHelpItem {
|
||||
pub fn into_signature_information(self) -> lsp::SignatureInformation {
|
||||
let prefix_text = display_parts_to_string(&self.prefix_display_parts);
|
||||
pub(crate) fn into_signature_information(
|
||||
self,
|
||||
language_server: &language_server::Inner,
|
||||
) -> lsp::SignatureInformation {
|
||||
let prefix_text =
|
||||
display_parts_to_string(&self.prefix_display_parts, language_server);
|
||||
let params_text = self
|
||||
.parameters
|
||||
.iter()
|
||||
.map(|param| display_parts_to_string(¶m.display_parts))
|
||||
.map(|param| {
|
||||
display_parts_to_string(¶m.display_parts, language_server)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ");
|
||||
let suffix_text = display_parts_to_string(&self.suffix_display_parts);
|
||||
let documentation = display_parts_to_string(&self.documentation);
|
||||
let suffix_text =
|
||||
display_parts_to_string(&self.suffix_display_parts, language_server);
|
||||
let documentation =
|
||||
display_parts_to_string(&self.documentation, language_server);
|
||||
lsp::SignatureInformation {
|
||||
label: format!("{}{}{}", prefix_text, params_text, suffix_text),
|
||||
documentation: Some(lsp::Documentation::MarkupContent(
|
||||
|
@ -2203,7 +2341,7 @@ impl SignatureHelpItem {
|
|||
self
|
||||
.parameters
|
||||
.into_iter()
|
||||
.map(|param| param.into_parameter_information())
|
||||
.map(|param| param.into_parameter_information(language_server))
|
||||
.collect(),
|
||||
),
|
||||
active_parameter: None,
|
||||
|
@ -2221,11 +2359,16 @@ pub struct SignatureHelpParameter {
|
|||
}
|
||||
|
||||
impl SignatureHelpParameter {
|
||||
pub fn into_parameter_information(self) -> lsp::ParameterInformation {
|
||||
let documentation = display_parts_to_string(&self.documentation);
|
||||
pub(crate) fn into_parameter_information(
|
||||
self,
|
||||
language_server: &language_server::Inner,
|
||||
) -> lsp::ParameterInformation {
|
||||
let documentation =
|
||||
display_parts_to_string(&self.documentation, language_server);
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple(display_parts_to_string(
|
||||
&self.display_parts,
|
||||
language_server,
|
||||
)),
|
||||
documentation: Some(lsp::Documentation::MarkupContent(
|
||||
lsp::MarkupContent {
|
||||
|
|
|
@ -1660,6 +1660,71 @@ fn lsp_hover_typescript_types() {
|
|||
shutdown(&mut client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_hover_jsdoc_symbol_link() {
|
||||
let mut client = init("initialize_params.json");
|
||||
did_open(
|
||||
&mut client,
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/b.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "export function hello() {}\n"
|
||||
}
|
||||
}),
|
||||
);
|
||||
did_open(
|
||||
&mut client,
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts",
|
||||
"languageId": "typescript",
|
||||
"version": 1,
|
||||
"text": "import { hello } from \"./b.ts\";\n\nhello();\n\nconst b = \"b\";\n\n/** JSDoc {@link hello} and {@linkcode b} */\nfunction a() {}\n"
|
||||
}
|
||||
}),
|
||||
);
|
||||
let (maybe_res, maybe_err) = client
|
||||
.write_request::<_, _, Value>(
|
||||
"textDocument/hover",
|
||||
json!({
|
||||
"textDocument": {
|
||||
"uri": "file:///a/file.ts"
|
||||
},
|
||||
"position": {
|
||||
"line": 7,
|
||||
"character": 10
|
||||
}
|
||||
}),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(maybe_err.is_none());
|
||||
assert_eq!(
|
||||
maybe_res,
|
||||
Some(json!({
|
||||
"contents": [
|
||||
{
|
||||
"language": "typescript",
|
||||
"value": "function a(): void"
|
||||
},
|
||||
"JSDoc [hello](file:///a/b.ts#L1,1) and [`b`](file:///a/file.ts#L5,7)"
|
||||
],
|
||||
"range": {
|
||||
"start": {
|
||||
"line": 7,
|
||||
"character": 9
|
||||
},
|
||||
"end": {
|
||||
"line": 7,
|
||||
"character": 10
|
||||
}
|
||||
}
|
||||
}))
|
||||
);
|
||||
shutdown(&mut client);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lsp_goto_type_definition() {
|
||||
let mut client = init("initialize_params.json");
|
||||
|
|
Loading…
Reference in a new issue