diff --git a/README.md b/README.md index 8ac4671a..5f248459 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ It contains: Language service (LSP) features: - [Semantic highlighting](https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide) - - Also known as "syntax highlighting". + - The "semantic highlighting" is supplementary to ["syntax highlighting"](https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide). - [Diagnostics](https://code.visualstudio.com/api/language-extensions/programmatic-language-features#provide-diagnostics) - Also known as "error checking" or "error reporting". - [Document highlight](https://code.visualstudio.com/api/language-extensions/programmatic-language-features#highlight-all-occurrences-of-a-symbol-in-a-document) @@ -26,6 +26,8 @@ Language service (LSP) features: - (Todo) Highlight all exit points in a function context. - (Todo) Highlight all captures in a closure context. - (Todo) Highlight all occurrences of a symbol in a document. +- [Document links](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentLink) + - Renders path or link references in the document, such as `image("path.png")` or `bibliography(style: "path.csl")`. - [Document symbols](https://code.visualstudio.com/docs/getstarted/userinterface#_outline-view) - Also known as "document outline" or "table of contents" _in Typst_. - [Folding ranges](https://burkeholland.gitbook.io/vs-code-can-do-that/exercise-3-navigation-and-refactoring/folding-sections) diff --git a/crates/tinymist-query/src/analysis.rs b/crates/tinymist-query/src/analysis.rs index 106a9f83..33ac8ea4 100644 --- a/crates/tinymist-query/src/analysis.rs +++ b/crates/tinymist-query/src/analysis.rs @@ -6,6 +6,8 @@ pub mod call; pub use call::*; pub mod color_exprs; pub use color_exprs::*; +pub mod link_exprs; +pub use link_exprs::*; pub mod def_use; pub use def_use::*; pub mod import; diff --git a/crates/tinymist-query/src/analysis/link_exprs.rs b/crates/tinymist-query/src/analysis/link_exprs.rs new file mode 100644 index 00000000..915f4bf0 --- /dev/null +++ b/crates/tinymist-query/src/analysis/link_exprs.rs @@ -0,0 +1,162 @@ +//! Analyze color expressions in a source file. +use std::ops::Range; + +use typst::syntax::{ + ast::{self, AstNode}, + LinkedNode, Source, SyntaxKind, +}; + +use crate::prelude::*; + +/// Get link expressions from a source. +pub fn get_link_exprs(ctx: &mut AnalysisContext, src: &Source) -> Option, Url)>> { + let root = LinkedNode::new(src.root()); + get_link_exprs_in(ctx, &root) +} + +/// Get link expressions in a source node. +pub fn get_link_exprs_in( + ctx: &mut AnalysisContext, + node: &LinkedNode, +) -> Option, Url)>> { + let mut worker = LinkStrWorker { ctx, links: vec![] }; + worker.collect_links(node)?; + Some(worker.links) +} + +struct LinkStrWorker<'a, 'w> { + ctx: &'a mut AnalysisContext<'w>, + links: Vec<(Range, Url)>, +} + +impl<'a, 'w> LinkStrWorker<'a, 'w> { + fn collect_links(&mut self, node: &LinkedNode) -> Option<()> { + match node.kind() { + // SyntaxKind::Link => { } + SyntaxKind::FuncCall => { + let fc = self.analyze_call(node); + if fc.is_some() { + return Some(()); + } + } + // early exit + k if k.is_trivia() || k.is_keyword() || k.is_error() => return Some(()), + _ => {} + }; + + for child in node.children() { + self.collect_links(&child); + } + + Some(()) + } + + fn analyze_call(&mut self, node: &LinkedNode) -> Option<()> { + let call = node.cast::()?; + let mut callee = call.callee(); + 'check_link_fn: loop { + match callee { + ast::Expr::FieldAccess(fa) => { + let target = fa.target(); + let ast::Expr::Ident(ident) = target else { + return None; + }; + if ident.get().as_str() != "std" { + return None; + } + callee = ast::Expr::Ident(fa.field()); + continue 'check_link_fn; + } + ast::Expr::Ident(ident) => match ident.get().as_str() { + "raw" => { + self.analyze_reader(node, call, "theme", false); + self.analyze_reader(node, call, "syntaxes", false); + } + "bibliography" => { + self.analyze_reader(node, call, "cite", false); + self.analyze_reader(node, call, "style", false); + self.analyze_reader(node, call, "path", true); + } + "cbor" | "csv" | "image" | "read" | "json" | "yaml" | "xml" => { + self.analyze_reader(node, call, "path", true); + } + _ => return None, + }, + _ => return None, + } + return None; + } + } + + fn analyze_reader( + &mut self, + node: &LinkedNode, + call: ast::FuncCall, + key: &str, + pos: bool, + ) -> Option<()> { + let arg = call.args().items().next()?; + match arg { + ast::Arg::Pos(s) if pos => { + self.analyze_path_exp(node, s); + } + _ => {} + } + for item in call.args().items() { + match item { + ast::Arg::Named(named) if named.name().get().as_str() == key => { + self.analyze_path_exp(node, named.expr()); + } + _ => {} + } + } + Some(()) + } + + fn analyze_path_exp(&mut self, node: &LinkedNode, expr: ast::Expr) -> Option<()> { + match expr { + ast::Expr::Str(s) => self.analyze_path_str(node, s), + ast::Expr::Array(a) => { + for item in a.items() { + if let ast::ArrayItem::Pos(ast::Expr::Str(s)) = item { + self.analyze_path_str(node, s); + } + } + Some(()) + } + _ => None, + } + } + + fn analyze_path_str(&mut self, node: &LinkedNode, s: ast::Str<'_>) -> Option<()> { + let str_node = node.find(s.span())?; + let str_range = str_node.range(); + let content_range = str_range.start + 1..str_range.end - 1; + if content_range.is_empty() { + return None; + } + + // Avoid creating new ids here. + let id = node.span().id()?; + let base = id.vpath().join(s.get().as_str()); + let root = self.ctx.path_for_id(id.join("/")).ok()?; + let path = base.resolve(&root)?; + if !path.exists() { + return None; + } + + self.push_path(content_range, path.as_path()) + } + + fn push_path(&mut self, range: Range, path: &Path) -> Option<()> { + self.push_link(range, path_to_url(path).ok()?) + } + + fn push_link(&mut self, range: Range, target: Url) -> Option<()> { + // let rng = self.ctx.to_lsp_range(range, &self.source); + + self.links.push((range, target)); + + Some(()) + } +} diff --git a/crates/tinymist-query/src/document_link.rs b/crates/tinymist-query/src/document_link.rs new file mode 100644 index 00000000..650bf2e7 --- /dev/null +++ b/crates/tinymist-query/src/document_link.rs @@ -0,0 +1,39 @@ +use crate::{analysis::get_link_exprs, prelude::*, SemanticRequest}; + +/// The [`textDocument/documentLink`] request is sent from the client to the +/// server to request the location of links in a document. +/// +/// A document link is a range in a text document that links to an internal or +/// external resource, like another text document or a web site. +/// +/// [`textDocument/documentLink`]: https://microsoft.github.io/language-server-protocol/specification#textDocument_documentLink +/// +/// # Compatibility +/// +/// The [`DocumentLink::tooltip`] field was introduced in specification version +/// 3.15.0 and requires client-side support in order to be used. +#[derive(Debug, Clone)] +pub struct DocumentLinkRequest { + /// The path of the document to request color for. + pub path: PathBuf, +} + +impl SemanticRequest for DocumentLinkRequest { + type Response = Vec; + + fn request(self, ctx: &mut AnalysisContext) -> Option { + let source = ctx.source_by_path(&self.path).ok()?; + let links = get_link_exprs(ctx, &source); + links.map(|links| { + links + .into_iter() + .map(|(range, target)| DocumentLink { + range: ctx.to_lsp_range(range, &source), + target: Some(target), + tooltip: None, + data: None, + }) + .collect() + }) + } +} diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin.typ.snap index 5c0af606..15c3308a 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin.typ.snap @@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" input_file: crates/tinymist-query/src/fixtures/hover/builtin.typ --- { - "contents": "```typc\nlet table(children, align: alignment | auto | array | function, column-gutter: auto | relative | fraction | int | array, columns: auto | relative | fraction | int | array, fill: color | gradient | pattern | none | array | function, gutter: auto | relative | fraction | int | array, inset: relative | dictionary | array | function, row-gutter: auto | relative | fraction | int | array, rows: auto | relative | fraction | int | array, stroke: length | color | gradient | pattern | dictionary | stroke | none | array | function);\n```\n---\n\n\nA table of items.\n\nTables are used to arrange content in cells. Cells can contain arbitrary\ncontent, including multiple paragraphs and are specified in row-major order.\nFor a hands-on explanation of all the ways you can use and customize tables\nin Typst, check out the [table guide](https://typst.app/docs/guides/table-guide/).\n\nBecause tables are just grids with different defaults for some cell\nproperties (notably `stroke` and `inset`), refer to the [grid\ndocumentation](https://typst.app/docs/reference/layout/grid/) for more information on how to size the table tracks\nand specify the cell appearance properties.\n\nIf you are unsure whether you should be using a table or a grid, consider\nwhether the content you are arranging semantically belongs together as a set\nof related data points or similar or whether you are just want to enhance\nyour presentation by arranging unrelated content in a grid. In the former\ncase, a table is the right choice, while in the latter case, a grid is more\nappropriate. Furthermore, Typst will annotate its output in the future such\nthat screenreaders will annouce content in `table` as tabular while a grid's\ncontent will be announced no different than multiple content blocks in the\ndocument flow.\n\nNote that, to override a particular cell's properties or apply show rules on\ntable cells, you can use the [`table.cell`](https://typst.app/docs/reference/model/table/#definitions-cell) element. See its\ndocumentation for more information.\n\nAlthough the `table` and the `grid` share most properties, set and show\nrules on one of them do not affect the other.\n\nTo give a table a caption and make it [referenceable](https://typst.app/docs/reference/model/ref/), put it into a\n[figure].\n\n# Example\n\nThe example below demonstrates some of the most common table options.\n```typ\n#table(\n columns: (1fr, auto, auto),\n inset: 10pt,\n align: horizon,\n table.header(\n [], [*Area*], [*Parameters*],\n ),\n image(\"cylinder.svg\"),\n $ pi h (D^2 - d^2) / 4 $,\n [\n $h$: height \\\n $D$: outer radius \\\n $d$: inner radius\n ],\n image(\"tetrahedron.svg\"),\n $ sqrt(2) / 12 a^3 $,\n [$a$: edge length]\n)\n```\n\nMuch like with grids, you can use [`table.cell`](https://typst.app/docs/reference/model/table/#definitions-cell) to customize\nthe appearance and the position of each cell.\n\n```typ\n>>> #set page(width: auto)\n>>> #set text(font: \"IBM Plex Sans\")\n>>> #let gray = rgb(\"#565565\")\n>>>\n#set table(\n stroke: none,\n gutter: 0.2em,\n fill: (x, y) =>\n if x == 0 or y == 0 { gray },\n inset: (right: 1.5em),\n)\n\n#show table.cell: it => {\n if it.x == 0 or it.y == 0 {\n set text(white)\n strong(it)\n } else if it.body == [] {\n // Replace empty cells with 'N/A'\n pad(..it.inset)[_N/A_]\n } else {\n it\n }\n}\n\n#let a = table.cell(\n fill: green.lighten(60%),\n)[A]\n#let b = table.cell(\n fill: aqua.lighten(60%),\n)[B]\n\n#table(\n columns: 4,\n [], [Exam 1], [Exam 2], [Exam 3],\n\n [John], [], a, [],\n [Mary], [], a, a,\n [Robert], b, a, b,\n)\n```\n---\n[Open docs](https://typst.app/docs/reference/model/table/)", + "contents": "```typc\nlet table(children, align: alignment | auto | array | function, column-gutter: auto | relative | fraction | int | array, columns: auto | relative | fraction | int | array, fill: color | gradient | pattern | none | array | function, gutter: auto | relative | fraction | int | array, inset: relative | dictionary | array | function, row-gutter: auto | relative | fraction | int | array, rows: auto | relative | fraction | int | array, stroke: length | color | gradient | pattern | dictionary | stroke | none | array | function);\n```\n\n---\n\n\nA table of items.\n\nTables are used to arrange content in cells. Cells can contain arbitrary\ncontent, including multiple paragraphs and are specified in row-major order.\nFor a hands-on explanation of all the ways you can use and customize tables\nin Typst, check out the [table guide](https://typst.app/docs/guides/table-guide/).\n\nBecause tables are just grids with different defaults for some cell\nproperties (notably `stroke` and `inset`), refer to the [grid\ndocumentation](https://typst.app/docs/reference/layout/grid/) for more information on how to size the table tracks\nand specify the cell appearance properties.\n\nIf you are unsure whether you should be using a table or a grid, consider\nwhether the content you are arranging semantically belongs together as a set\nof related data points or similar or whether you are just want to enhance\nyour presentation by arranging unrelated content in a grid. In the former\ncase, a table is the right choice, while in the latter case, a grid is more\nappropriate. Furthermore, Typst will annotate its output in the future such\nthat screenreaders will annouce content in `table` as tabular while a grid's\ncontent will be announced no different than multiple content blocks in the\ndocument flow.\n\nNote that, to override a particular cell's properties or apply show rules on\ntable cells, you can use the [`table.cell`](https://typst.app/docs/reference/model/table/#definitions-cell) element. See its\ndocumentation for more information.\n\nAlthough the `table` and the `grid` share most properties, set and show\nrules on one of them do not affect the other.\n\nTo give a table a caption and make it [referenceable](https://typst.app/docs/reference/model/ref/), put it into a\n[figure].\n\n# Example\n\nThe example below demonstrates some of the most common table options.\n```typ\n#table(\n columns: (1fr, auto, auto),\n inset: 10pt,\n align: horizon,\n table.header(\n [], [*Area*], [*Parameters*],\n ),\n image(\"cylinder.svg\"),\n $ pi h (D^2 - d^2) / 4 $,\n [\n $h$: height \\\n $D$: outer radius \\\n $d$: inner radius\n ],\n image(\"tetrahedron.svg\"),\n $ sqrt(2) / 12 a^3 $,\n [$a$: edge length]\n)\n```\n\nMuch like with grids, you can use [`table.cell`](https://typst.app/docs/reference/model/table/#definitions-cell) to customize\nthe appearance and the position of each cell.\n\n```typ\n>>> #set page(width: auto)\n>>> #set text(font: \"IBM Plex Sans\")\n>>> #let gray = rgb(\"#565565\")\n>>>\n#set table(\n stroke: none,\n gutter: 0.2em,\n fill: (x, y) =>\n if x == 0 or y == 0 { gray },\n inset: (right: 1.5em),\n)\n\n#show table.cell: it => {\n if it.x == 0 or it.y == 0 {\n set text(white)\n strong(it)\n } else if it.body == [] {\n // Replace empty cells with 'N/A'\n pad(..it.inset)[_N/A_]\n } else {\n it\n }\n}\n\n#let a = table.cell(\n fill: green.lighten(60%),\n)[A]\n#let b = table.cell(\n fill: aqua.lighten(60%),\n)[B]\n\n#table(\n columns: 4,\n [], [Exam 1], [Exam 2], [Exam 3],\n\n [John], [], a, [],\n [Mary], [], a, a,\n [Robert], b, a, b,\n)\n```\n\n---\n[Open docs](https://typst.app/docs/reference/model/table/)", "range": "0:20:0:25" } diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_module.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_module.typ.snap index be3c00f2..db882c99 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_module.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_module.typ.snap @@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" input_file: crates/tinymist-query/src/fixtures/hover/builtin_module.typ --- { - "contents": "```typc\n\n```\n---\n```typc\nlet sys;\n```", + "contents": "```typc\n\n```\n\n---\n```typc\nlet sys;\n```", "range": "0:20:0:23" } diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var2.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var2.typ.snap index 0c55b482..13f9509e 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var2.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var2.typ.snap @@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" input_file: crates/tinymist-query/src/fixtures/hover/builtin_var2.typ --- { - "contents": "```typc\n\n```\n---\n```typc\nlet sys;\n```", + "contents": "```typc\n\n```\n\n---\n```typc\nlet sys;\n```", "range": "0:20:0:23" } diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var3.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var3.typ.snap index 4dbae615..d070755d 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var3.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@builtin_var3.typ.snap @@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" input_file: crates/tinymist-query/src/fixtures/hover/builtin_var3.typ --- { - "contents": "```typc\n\n```\n---\n```typc\nlet sys;\n```", + "contents": "```typc\n\n```\n\n---\n```typc\nlet sys;\n```", "range": "0:2:0:5" } diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@pagebreak.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@pagebreak.typ.snap index b36eefe1..6985fe50 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@pagebreak.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@pagebreak.typ.snap @@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" input_file: crates/tinymist-query/src/fixtures/hover/pagebreak.typ --- { - "contents": "```typc\nlet pagebreak(to: \"even\" | \"odd\" | none, weak: bool);\n```\n---\n\n\nA manual page break.\n\nMust not be used inside any containers.\n\n# Example\n```typ\nThe next page contains\nmore details on compound theory.\n#pagebreak()\n\n== Compound Theory\nIn 1984, the first ...\n```\n---\n[Open docs](https://typst.app/docs/reference/layout/pagebreak/)", + "contents": "```typc\nlet pagebreak(to: \"even\" | \"odd\" | none, weak: bool);\n```\n\n---\n\n\nA manual page break.\n\nMust not be used inside any containers.\n\n# Example\n```typ\nThe next page contains\nmore details on compound theory.\n#pagebreak()\n\n== Compound Theory\nIn 1984, the first ...\n```\n\n---\n[Open docs](https://typst.app/docs/reference/layout/pagebreak/)", "range": "0:20:0:29" } diff --git a/crates/tinymist-query/src/fixtures/hover/snaps/test@user.typ.snap b/crates/tinymist-query/src/fixtures/hover/snaps/test@user.typ.snap index 2c52aebe..32532d1f 100644 --- a/crates/tinymist-query/src/fixtures/hover/snaps/test@user.typ.snap +++ b/crates/tinymist-query/src/fixtures/hover/snaps/test@user.typ.snap @@ -4,6 +4,6 @@ expression: "JsonRepr::new_redacted(result, &REDACT_LOC)" input_file: crates/tinymist-query/src/fixtures/hover/user.typ --- { - "contents": "```typc\nlet f();\n```\n---\n\n\nTest", + "contents": "```typc\nlet f();\n```\n\n---\n\n\nTest", "range": "3:20:3:21" } diff --git a/crates/tinymist-query/src/hover.rs b/crates/tinymist-query/src/hover.rs index f92a8be0..cda0170c 100644 --- a/crates/tinymist-query/src/hover.rs +++ b/crates/tinymist-query/src/hover.rs @@ -3,10 +3,13 @@ use core::fmt; use typst_shim::syntax::LinkedNodeExt; use crate::{ - analysis::{analyze_dyn_signature, find_definition, DefinitionLink, Signature}, + analysis::{ + analyze_dyn_signature, find_definition, get_link_exprs_in, DefinitionLink, Signature, + }, jump_from_cursor, prelude::*, syntax::{find_docs_before, get_deref_target, LexicalKind, LexicalVarKind}, + ty::PathPreference, upstream::{expr_tooltip, plain_docs_sentence, route_of_value, truncated_repr, Tooltip}, LspHoverContents, StatefulRequest, }; @@ -42,7 +45,8 @@ impl StatefulRequest for HoverRequest { let cursor = offset + 1; let contents = def_tooltip(ctx, &source, doc.as_ref(), cursor) - .or_else(|| star_tooltip(ctx, &source, cursor)); + .or_else(|| star_tooltip(ctx, &source, cursor)) + .or_else(|| link_tooltip(ctx, &source, cursor)); let contents = contents.or_else(|| { Some(typst_to_lsp::tooltip( @@ -64,7 +68,7 @@ impl StatefulRequest for HoverRequest { } MarkedString::String(e) => e, }) - .join("\n---\n"), + .join("\n\n---\n"), LspHoverContents::Scalar(MarkedString::String(contents)) => contents, LspHoverContents::Scalar(MarkedString::LanguageString(contents)) => { format!("```{}\n{}\n```", contents.language, contents.value) @@ -117,6 +121,53 @@ impl StatefulRequest for HoverRequest { } } +fn link_tooltip( + ctx: &mut AnalysisContext<'_>, + source: &Source, + cursor: usize, +) -> Option { + let mut node = LinkedNode::new(source.root()).leaf_at_compat(cursor)?; + while !matches!(node.kind(), SyntaxKind::FuncCall) { + node = node.parent()?.clone(); + } + + let mut links = get_link_exprs_in(ctx, &node)?; + links.retain(|link| link.0.contains(&cursor)); + if links.is_empty() { + return None; + } + + let mut results = vec![]; + let mut actions = vec![]; + for (_, target) in links { + // open file in tab or system application + actions.push(CommandLink { + title: Some("Open in Tab".to_string()), + command_or_links: vec![CommandOrLink::Command(Command { + id: "tinymist.openInternal".to_string(), + args: vec![JsonValue::String(target.to_string())], + })], + }); + actions.push(CommandLink { + title: Some("Open Externally".to_string()), + command_or_links: vec![CommandOrLink::Command(Command { + id: "tinymist.openExternal".to_string(), + args: vec![JsonValue::String(target.to_string())], + })], + }); + if let Some(kind) = PathPreference::from_ext(target.path()) { + let preview = format!("A `{kind:?}` file."); + results.push(MarkedString::String(preview)); + } + } + render_actions(&mut results, actions); + if results.is_empty() { + return None; + } + + Some(LspHoverContents::Array(results)) +} + fn star_tooltip( ctx: &mut AnalysisContext, source: &Source, @@ -161,8 +212,14 @@ fn star_tooltip( Some(LspHoverContents::Array(results)) } +struct Command { + id: String, + args: Vec, +} + enum CommandOrLink { Link(String), + Command(Command), } struct CommandLink { @@ -281,13 +338,27 @@ fn render_actions(results: &mut Vec, actions: Vec) { .into_iter() .map(|col| match col { CommandOrLink::Link(link) => link, + CommandOrLink::Command(command) => { + let id = command.id; + // + if command.args.is_empty() { + format!("command:{id}") + } else { + let args = serde_json::to_string(&command.args).unwrap(); + let args = percent_encoding::utf8_percent_encode( + &args, + percent_encoding::NON_ALPHANUMERIC, + ); + format!("command:{id}?{args}") + } + } }) .collect::>() .join(" "); format!("[{title}]({command_or_links})") }) .collect::>() - .join("___"); + .join(" | "); results.push(MarkedString::String(g)); } diff --git a/crates/tinymist-query/src/lib.rs b/crates/tinymist-query/src/lib.rs index f8ecc4d5..c81fbf90 100644 --- a/crates/tinymist-query/src/lib.rs +++ b/crates/tinymist-query/src/lib.rs @@ -35,6 +35,8 @@ mod document_highlight; pub use document_highlight::*; mod document_symbol; pub use document_symbol::*; +mod document_link; +pub use document_link::*; mod workspace_label; pub use workspace_label::*; mod document_metrics; @@ -246,6 +248,7 @@ mod polymorphic { References(ReferencesRequest), InlayHint(InlayHintRequest), DocumentColor(DocumentColorRequest), + DocumentLink(DocumentLinkRequest), DocumentHighlight(DocumentHighlightRequest), ColorPresentation(ColorPresentationRequest), CodeAction(CodeActionRequest), @@ -282,6 +285,7 @@ mod polymorphic { Self::References(..) => PinnedFirst, Self::InlayHint(..) => Unique, Self::DocumentColor(..) => PinnedFirst, + Self::DocumentLink(..) => PinnedFirst, Self::DocumentHighlight(..) => PinnedFirst, Self::ColorPresentation(..) => ContextFreeUnique, Self::CodeAction(..) => Unique, @@ -317,6 +321,7 @@ mod polymorphic { Self::References(req) => &req.path, Self::InlayHint(req) => &req.path, Self::DocumentColor(req) => &req.path, + Self::DocumentLink(req) => &req.path, Self::DocumentHighlight(req) => &req.path, Self::ColorPresentation(req) => &req.path, Self::CodeAction(req) => &req.path, @@ -353,6 +358,7 @@ mod polymorphic { References(Option>), InlayHint(Option>), DocumentColor(Option>), + DocumentLink(Option>), DocumentHighlight(Option>), ColorPresentation(Option>), CodeAction(Option>), @@ -388,6 +394,7 @@ mod polymorphic { Self::References(res) => serde_json::to_value(res), Self::InlayHint(res) => serde_json::to_value(res), Self::DocumentColor(res) => serde_json::to_value(res), + Self::DocumentLink(res) => serde_json::to_value(res), Self::DocumentHighlight(res) => serde_json::to_value(res), Self::ColorPresentation(res) => serde_json::to_value(res), Self::CodeAction(res) => serde_json::to_value(res), diff --git a/crates/tinymist-query/src/prelude.rs b/crates/tinymist-query/src/prelude.rs index 66a9f282..1dac7378 100644 --- a/crates/tinymist-query/src/prelude.rs +++ b/crates/tinymist-query/src/prelude.rs @@ -11,8 +11,8 @@ pub use log::{error, trace}; pub use lsp_types::{ request::GotoDeclarationResponse, CodeAction, CodeActionKind, CodeActionOrCommand, CodeLens, ColorInformation, ColorPresentation, CompletionResponse, DiagnosticRelatedInformation, - DocumentHighlight, DocumentSymbol, DocumentSymbolResponse, Documentation, FoldingRange, - GotoDefinitionResponse, Hover, HoverContents, InlayHint, LanguageString, + DocumentHighlight, DocumentLink, DocumentSymbol, DocumentSymbolResponse, Documentation, + FoldingRange, GotoDefinitionResponse, Hover, HoverContents, InlayHint, LanguageString, Location as LspLocation, LocationLink, MarkedString, MarkupContent, MarkupKind, Position as LspPosition, PrepareRenameResponse, SelectionRange, SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult, SignatureHelp, diff --git a/crates/tinymist-query/src/ty/builtin.rs b/crates/tinymist-query/src/ty/builtin.rs index a180ae31..d5ea76da 100644 --- a/crates/tinymist-query/src/ty/builtin.rs +++ b/crates/tinymist-query/src/ty/builtin.rs @@ -2,6 +2,7 @@ use core::fmt; use once_cell::sync::Lazy; use regex::RegexSet; +use strum::{EnumIter, IntoEnumIterator}; use typst::{foundations::CastInfo, syntax::Span}; use typst::{ foundations::{AutoValue, Content, Func, NoneValue, ParamInfo, Type, Value}, @@ -10,10 +11,8 @@ use typst::{ use crate::{adt::interner::Interned, ty::*}; -#[derive(Debug, Clone, Hash, PartialEq, Eq)] +#[derive(Debug, Clone, Hash, PartialEq, Eq, EnumIter)] pub enum PathPreference { - None, - Special, Source, Csv, Image, @@ -25,6 +24,8 @@ pub enum PathPreference { Bibliography, RawTheme, RawSyntax, + Special, + None, } impl PathPreference { @@ -33,7 +34,8 @@ impl PathPreference { Lazy::new(|| RegexSet::new([r"^typ$", r"^typc$"]).unwrap()); static IMAGE_REGSET: Lazy = Lazy::new(|| { RegexSet::new([ - r"^png$", r"^webp$", r"^jpg$", r"^jpeg$", r"^svg$", r"^svgz$", + r"^ico$", r"^bmp$", r"^png$", r"^webp$", r"^jpg$", r"^jpeg$", r"^jfif$", r"^tiff$", + r"^gif$", r"^svg$", r"^svgz$", ]) .unwrap() }); @@ -70,8 +72,6 @@ impl PathPreference { }); match self { - PathPreference::None => &ALL_REGSET, - PathPreference::Special => &ALL_SPECIAL_REGSET, PathPreference::Source => &SOURCE_REGSET, PathPreference::Csv => &CSV_REGSET, PathPreference::Image => &IMAGE_REGSET, @@ -83,8 +83,15 @@ impl PathPreference { PathPreference::Bibliography => &BIB_REGSET, PathPreference::RawTheme => &RAW_THEME_REGSET, PathPreference::RawSyntax => &RAW_SYNTAX_REGSET, + PathPreference::Special => &ALL_SPECIAL_REGSET, + PathPreference::None => &ALL_REGSET, } } + + pub fn from_ext(path: &str) -> Option { + let path = std::path::Path::new(path).extension()?.to_str()?; + PathPreference::iter().find(|p| p.ext_matcher().is_match(path)) + } } impl Ty { diff --git a/crates/tinymist/src/init.rs b/crates/tinymist/src/init.rs index 9570cd9e..509dbb74 100644 --- a/crates/tinymist/src/init.rs +++ b/crates/tinymist/src/init.rs @@ -236,6 +236,12 @@ impl Initializer for SuperInit { work_done_progress: None, }, })), + document_link_provider: Some(DocumentLinkOptions { + resolve_provider: None, + work_done_progress_options: WorkDoneProgressOptions { + work_done_progress: None, + }, + }), folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)), workspace: Some(WorkspaceServerCapabilities { workspace_folders: Some(WorkspaceFoldersServerCapabilities { diff --git a/crates/tinymist/src/server.rs b/crates/tinymist/src/server.rs index fdfb1d2a..de12e32b 100644 --- a/crates/tinymist/src/server.rs +++ b/crates/tinymist/src/server.rs @@ -220,6 +220,7 @@ impl LanguageState { // latency insensitive .with_request_::(State::inlay_hint) .with_request_::(State::document_color) + .with_request_::(State::document_link) .with_request_::(State::color_presentation) .with_request_::(State::hover) .with_request_::(State::code_action) @@ -722,6 +723,11 @@ impl LanguageState { run_query!(req_id, self.DocumentColor(path)) } + fn document_link(&mut self, req_id: RequestId, params: DocumentLinkParams) -> ScheduledResult { + let path = as_path(params.text_document); + run_query!(req_id, self.DocumentLink(path)) + } + fn color_presentation( &mut self, req_id: RequestId, @@ -1064,6 +1070,7 @@ impl LanguageState { InlayHint(req) => handle.run_semantic(snap, req, R::InlayHint), DocumentHighlight(req) => handle.run_semantic(snap, req, R::DocumentHighlight), DocumentColor(req) => handle.run_semantic(snap, req, R::DocumentColor), + DocumentLink(req) => handle.run_semantic(snap, req, R::DocumentLink), CodeAction(req) => handle.run_semantic(snap, req, R::CodeAction), CodeLens(req) => handle.run_semantic(snap, req, R::CodeLens), Completion(req) => handle.run_stateful(snap, req, R::Completion), diff --git a/docs/tinymist/feature/language-content.typ b/docs/tinymist/feature/language-content.typ index 94edb490..bb020bce 100644 --- a/docs/tinymist/feature/language-content.typ +++ b/docs/tinymist/feature/language-content.typ @@ -4,7 +4,7 @@ Language service (LSP) features: - #link("https://code.visualstudio.com/api/language-extensions/semantic-highlight-guide")[Semantic highlighting] - - Also known as "syntax highlighting". + - The "semantic highlighting" is supplementary to #link("https://code.visualstudio.com/api/language-extensions/syntax-highlight-guide")["syntax highlighting"]. - #link("https://code.visualstudio.com/api/language-extensions/programmatic-language-features#provide-diagnostics")[Diagnostics] - Also known as "error checking" or "error reporting". - #link("https://code.visualstudio.com/api/language-extensions/programmatic-language-features#highlight-all-occurrences-of-a-symbol-in-a-document")[Document highlight] @@ -12,6 +12,8 @@ Language service (LSP) features: - (Todo) Highlight all exit points in a function context. - (Todo) Highlight all captures in a closure context. - (Todo) Highlight all occurrences of a symbol in a document. +- #link("https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_documentLink")[Document links] + - Renders path or link references in the document, such as `image("path.png")` or `bibliography(style: "path.csl")`. - #link("https://code.visualstudio.com/docs/getstarted/userinterface#_outline-view")[Document symbols] - Also known as "document outline" or "table of contents" _in Typst_. - #link("https://burkeholland.gitbook.io/vs-code-can-do-that/exercise-3-navigation-and-refactoring/folding-sections")[Folding ranges] diff --git a/editors/emacs/README.md b/editors/emacs/README.md index 71ee92b4..545a95c3 100644 --- a/editors/emacs/README.md +++ b/editors/emacs/README.md @@ -14,7 +14,8 @@ To enable LSP, you must install `tinymist`. You can find `tinymist` by: - Night versions available at [GitHub Actions](https://github.com/Myriad-Dreamin/tinymist/actions). -- Stable versions available at [GitHub Releases](https://github.com/Myriad-Dreamin/tinymist/releases). \ +- Stable versions available at [GitHub Releases](https://github.com/Myriad-Dreamin/tinymist/releases). + If you are using the latest version of [typst-ts-mode](https://codeberg.org/meow_king/typst-ts-mode), then you can use command `typst-ts-lsp-download-binary` to download the latest diff --git a/editors/helix/README.md b/editors/helix/README.md index cf66cf7e..ad742ef3 100644 --- a/editors/helix/README.md +++ b/editors/helix/README.md @@ -14,7 +14,8 @@ To enable LSP, you must install `tinymist`. You can find `tinymist` by: - Night versions available at [GitHub Actions](https://github.com/Myriad-Dreamin/tinymist/actions). -- Stable versions available at [GitHub Releases](https://github.com/Myriad-Dreamin/tinymist/releases). \ +- Stable versions available at [GitHub Releases](https://github.com/Myriad-Dreamin/tinymist/releases). + If you are using the latest version of [typst-ts-mode](https://codeberg.org/meow_king/typst-ts-mode), then you can use command `typst-ts-lsp-download-binary` to download the latest diff --git a/editors/neovim/README.md b/editors/neovim/README.md index fd58180b..b3eeb769 100644 --- a/editors/neovim/README.md +++ b/editors/neovim/README.md @@ -14,7 +14,8 @@ To enable LSP, you must install `tinymist`. You can find `tinymist` by: - Night versions available at [GitHub Actions](https://github.com/Myriad-Dreamin/tinymist/actions). -- Stable versions available at [GitHub Releases](https://github.com/Myriad-Dreamin/tinymist/releases). \ +- Stable versions available at [GitHub Releases](https://github.com/Myriad-Dreamin/tinymist/releases). + If you are using the latest version of [typst-ts-mode](https://codeberg.org/meow_king/typst-ts-mode), then you can use command `typst-ts-lsp-download-binary` to download the latest diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts index 08c697a5..5e0a7b7a 100644 --- a/editors/vscode/src/extension.ts +++ b/editors/vscode/src/extension.ts @@ -124,6 +124,9 @@ function initClient(context: ExtensionContext, config: Record) { debug: run, }; + const trustedCommands = { + enabledCommands: ["tinymist.openInternal", "tinymist.openExternal"], + }; const clientOptions: LanguageClientOptions = { documentSelector: typstDocumentSelector, initializationOptions: config, @@ -138,6 +141,19 @@ function initClient(context: ExtensionContext, config: Record) { return substVscodeVarsInConfig(items, result); }, }, + provideHover: async (document, position, token, next) => { + const hover = await next(document, position, token); + if (!hover) { + return hover; + } + + for (const content of hover.contents) { + if (content instanceof vscode.MarkdownString) { + content.isTrusted = trustedCommands; + } + } + return hover; + }, }, }; @@ -252,6 +268,8 @@ async function startClient(client: LanguageClient, context: ExtensionContext): P // prettier-ignore context.subscriptions.push( commands.registerCommand("tinymist.onEnter", onEnterHandler), + commands.registerCommand("tinymist.openInternal", openInternal), + commands.registerCommand("tinymist.openExternal", openExternal), commands.registerCommand("tinymist.exportCurrentPdf", () => commandExport("Pdf")), commands.registerCommand("tinymist.showPdf", () => commandShow("Pdf")), @@ -361,6 +379,16 @@ async function startClient(client: LanguageClient, context: ExtensionContext): P return; } +async function openInternal(target: string): Promise { + const uri = Uri.parse(target); + await commands.executeCommand("vscode.open", uri, ViewColumn.Beside); +} + +async function openExternal(target: string): Promise { + const uri = Uri.parse(target); + await vscode.env.openExternal(uri); +} + async function commandExport( mode: "Pdf" | "Svg" | "Png", extraOpts?: any, diff --git a/package.json b/package.json index 45dad34f..cc80c390 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "build:editor-tools": "cd tools/editor-tools/ && yarn run build", "build:preview": "cd tools/typst-preview-frontend && yarn run build && rimraf ../../crates/tinymist-assets/src/typst-preview.html && cpr ./dist/index.html ../../crates/tinymist-assets/src/typst-preview.html", "docs": "shiroa serve -w . docs/tinymist", + "docs:typ": "node scripts/link-docs.mjs", "docs:rs": "cargo doc --workspace --no-deps", "test:grammar": "cd syntaxes/textmate && yarn run test", "build:typlite": "cargo build --bin typlite", diff --git a/tests/e2e/main.rs b/tests/e2e/main.rs index 3635e143..c8dc688f 100644 --- a/tests/e2e/main.rs +++ b/tests/e2e/main.rs @@ -374,7 +374,7 @@ fn e2e() { }); let hash = replay_log(&tinymist_binary, &root.join("neovim")); - insta::assert_snapshot!(hash, @"siphash128_13:95195259988d97cae5802b09f2aa6c0"); + insta::assert_snapshot!(hash, @"siphash128_13:c6f72c26cfa3725161446bfc9a84fd89"); } { @@ -385,7 +385,7 @@ fn e2e() { }); let hash = replay_log(&tinymist_binary, &root.join("vscode")); - insta::assert_snapshot!(hash, @"siphash128_13:5f3b961e94db34a9d7e9f6a405617c0d"); + insta::assert_snapshot!(hash, @"siphash128_13:cbabb6bf71ce157c4e779aecf9d22c2a"); } }