feat: handle/add link in the hover documentation (#239)

* feat: handle/add link in the hover documentation

* dev: update snapshot

* dev: update snapshot

* dev: update snapshot
This commit is contained in:
Myriad-Dreamin 2024-05-05 21:32:09 +08:00 committed by GitHub
parent 68bcc2b571
commit 6ad9258740
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 662 additions and 21 deletions

View file

@ -3,7 +3,6 @@
use std::{collections::HashMap, ops::Range, sync::Arc};
use ecow::EcoVec;
use log::info;
use super::{prelude::*, ImportInfo};
use crate::adt::snapshot_map::SnapshotMap;
@ -219,7 +218,7 @@ impl<'a, 'w> DefUseCollector<'a, 'w> {
LexicalKind::Mod(LexicalModKind::Module(..)) => {
let mut src = self.import.imports.get(&e.info.range)?.clone();
info!("check import: {info:?} => {src:?}", info = e.info);
log::debug!("check import: {info:?} => {src:?}", info = e.info);
std::mem::swap(&mut self.ext_src, &mut src);
// todo: process import star
@ -231,7 +230,7 @@ impl<'a, 'w> DefUseCollector<'a, 'w> {
}
LexicalKind::Mod(LexicalModKind::Star) => {
if let Some(source) = &self.ext_src {
info!("diving source for def use: {:?}", source.id());
log::debug!("diving source for def use: {:?}", source.id());
let (_, external_info) =
Some(source.id()).zip(self.ctx.def_use(source.clone()))?;

View file

@ -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]($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]($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`]($table.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]($ref), put it into a\n[figure].\n\n# Example\n\nThe example below demonstrates some of the most common table options.\n```example\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`]($table.cell) to customize\nthe appearance and the position of each cell.\n\n```example\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```",
"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```example\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```example\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 documentation](https://typst.app/docs//reference/model/table/)",
"range": "0:20:0:25"
}

View file

@ -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// Values\n<module sys>\n```\n---\n```typc\nlet sys;\n```",
"contents": "```typc\n<module sys>\n```\n---\n```typc\nlet sys;\n```",
"range": "0:20:0:23"
}

View file

@ -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// Values\n<module sys>\n```\n---\n```typc\nlet sys;\n```",
"contents": "```typc\n<module sys>\n```\n---\n```typc\nlet sys;\n```",
"range": "0:20:0:23"
}

View file

@ -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// Values\n<module sys>\n```\n---\n```typc\nlet sys;\n```",
"contents": "```typc\n<module sys>\n```\n---\n```typc\nlet sys;\n```",
"range": "0:2:0:5"
}

View file

@ -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```example\nThe next page contains\nmore details on compound theory.\n#pagebreak()\n\n== Compound Theory\nIn 1984, the first ...\n```",
"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```example\nThe next page contains\nmore details on compound theory.\n#pagebreak()\n\n== Compound Theory\nIn 1984, the first ...\n```\n---\n[Open documentation](https://typst.app/docs//reference/layout/pagebreak/)",
"range": "0:20:0:29"
}

View file

@ -5,7 +5,7 @@ use crate::{
jump_from_cursor,
prelude::*,
syntax::{find_docs_before, get_deref_target, LexicalKind, LexicalVarKind},
upstream::{expr_tooltip, tooltip, Tooltip},
upstream::{expr_tooltip, plain_docs_sentence, route_of_value, tooltip, Tooltip},
LspHoverContents, StatefulRequest,
};
@ -115,6 +115,15 @@ impl StatefulRequest for HoverRequest {
}
}
enum CommandOrLink {
Link(String),
}
struct CommandLink {
title: Option<String>,
command_or_links: Vec<CommandOrLink>,
}
fn def_tooltip(
ctx: &mut AnalysisContext,
source: &Source,
@ -128,6 +137,7 @@ fn def_tooltip(
let lnk = find_definition(ctx, source.clone(), document, deref_target.clone())?;
let mut results = vec![];
let mut actions = vec![];
match lnk.kind {
LexicalKind::Mod(_)
@ -160,6 +170,11 @@ fn def_tooltip(
results.push(MarkedString::String(doc));
}
if let Some(link) = ExternalDocLink::get(ctx, &lnk) {
actions.push(link);
}
render_actions(&mut results, actions);
Some(LspHoverContents::Array(results))
}
LexicalKind::Var(LexicalVarKind::Variable) => {
@ -168,12 +183,12 @@ fn def_tooltip(
if let Some(values) = expr_tooltip(ctx.world(), deref_node) {
match values {
Tooltip::Text(values) => {
results.push(MarkedString::String(format!("Values: {values}")));
results.push(MarkedString::String(values.into()));
}
Tooltip::Code(values) => {
results.push(MarkedString::LanguageString(LanguageString {
language: "typc".to_owned(),
value: format!("// Values\n{values}"),
value: values.into(),
}));
}
}
@ -188,11 +203,41 @@ fn def_tooltip(
results.push(MarkedString::String(doc));
}
if let Some(link) = ExternalDocLink::get(ctx, &lnk) {
actions.push(link);
}
render_actions(&mut results, actions);
Some(LspHoverContents::Array(results))
}
}
}
fn render_actions(results: &mut Vec<MarkedString>, actions: Vec<CommandLink>) {
if actions.is_empty() {
return;
}
let g = actions
.into_iter()
.map(|action| {
// https://github.com/rust-lang/rust-analyzer/blob/1a5bb27c018c947dab01ab70ffe1d267b0481a17/editors/code/src/client.ts#L59
let title = action.title.unwrap_or("".to_owned());
let command_or_links = action
.command_or_links
.into_iter()
.map(|col| match col {
CommandOrLink::Link(link) => link,
})
.collect::<Vec<_>>()
.join(" ");
format!("[{title}]({command_or_links})")
})
.collect::<Vec<_>>()
.join("___");
results.push(MarkedString::String(g));
}
// todo: hover with `with_stack`
struct ParamTooltip(Option<Signature>);
@ -255,6 +300,59 @@ impl fmt::Display for ParamTooltip {
}
}
struct ExternalDocLink;
impl ExternalDocLink {
fn get(ctx: &mut AnalysisContext, lnk: &DefinitionLink) -> Option<CommandLink> {
self::ExternalDocLink::get_inner(ctx, lnk)
}
fn get_inner(_ctx: &mut AnalysisContext, lnk: &DefinitionLink) -> Option<CommandLink> {
if matches!(lnk.value, Some(Value::Func(..))) {
if let Some(builtin) = Self::builtin_func_tooltip("https://typst.app/docs/", lnk) {
return Some(builtin);
}
};
lnk.value
.as_ref()
.and_then(|value| Self::builtin_value_tooltip("https://typst.app/docs/", value))
}
}
impl ExternalDocLink {
fn builtin_func_tooltip(base: &str, lnk: &DefinitionLink) -> Option<CommandLink> {
let Some(Value::Func(func)) = &lnk.value else {
return None;
};
use typst::foundations::func::Repr;
let mut func = func;
loop {
match func.inner() {
Repr::Element(..) | Repr::Native(..) => {
return Self::builtin_value_tooltip(base, &Value::Func(func.clone()));
}
Repr::With(w) => {
func = &w.0;
}
Repr::Closure(..) => {
return None;
}
}
}
}
fn builtin_value_tooltip(base: &str, value: &Value) -> Option<CommandLink> {
let route = route_of_value(value)?;
let link = format!("{base}/{route}");
Some(CommandLink {
title: Some("Open documentation".to_owned()),
command_or_links: vec![CommandOrLink::Link(link)],
})
}
}
struct DocTooltip;
impl DocTooltip {
@ -265,7 +363,7 @@ impl DocTooltip {
fn get_inner(ctx: &mut AnalysisContext, lnk: &DefinitionLink) -> Option<String> {
if matches!(lnk.value, Some(Value::Func(..))) {
if let Some(builtin) = Self::builtin_func_tooltip(lnk) {
return Some(builtin.to_owned());
return Some(plain_docs_sentence(builtin).into());
}
};

View file

@ -611,7 +611,6 @@ pub fn param_completions<'a>(
return;
}
// Some(&plain_docs_sentence(&pos.docs))
doc = Some(plain_docs_sentence(&pos.docs));
if pos.positional

View file

@ -0,0 +1,169 @@
# This is responsible for the fact that certain math functions are grouped
# together into one documentation page although they are not part of any scope.
- name: variants
title: Variants
category: math
path: ["math"]
filter: ["serif", "sans", "frak", "mono", "bb", "cal"]
details: |
Alternate typefaces within formulas.
These functions are distinct from the [`text`] function because math fonts
contain multiple variants of each letter.
- name: styles
title: Styles
category: math
path: ["math"]
filter: ["upright", "italic", "bold"]
details: |
Alternate letterforms within formulas.
These functions are distinct from the [`text`] function because math fonts
contain multiple variants of each letter.
- name: sizes
title: Sizes
category: math
path: ["math"]
filter: ["display", "inline", "script", "sscript"]
details: |
Forced size styles for expressions within formulas.
These functions allow manual configuration of the size of equation elements
to make them look as in a display/inline equation or as if used in a root or
sub/superscripts.
- name: underover
title: Under/Over
category: math
path: ["math"]
filter: [
"underline",
"overline",
"underbrace",
"overbrace",
"underbracket",
"overbracket",
]
details: |
Delimiters above or below parts of an equation.
The braces and brackets further allow you to add an optional annotation
below or above themselves.
- name: roots
title: Roots
category: math
path: ["math"]
filter: ["root", "sqrt"]
details: |
Square and non-square roots.
# Example
```example
$ sqrt(3 - 2 sqrt(2)) = sqrt(2) - 1 $
$ root(3, x) $
```
- name: attach
title: Attach
category: math
path: ["math"]
filter: ["attach", "scripts", "limits"]
details: |
Subscript, superscripts, and limits.
Attachments can be displayed either as sub/superscripts, or limits. Typst
automatically decides which is more suitable depending on the base, but you
can also control this manually with the `scripts` and `limits` functions.
# Example
```example
$ sum_(i=0)^n a_i = 2^(1+i) $
```
# Syntax
This function also has dedicated syntax for attachments after the base: Use
the underscore (`_`) to indicate a subscript i.e. bottom attachment and the
hat (`^`) to indicate a superscript i.e. top attachment.
- name: lr
title: Left/Right
category: math
path: ["math"]
filter: ["lr", "mid", "abs", "norm", "floor", "ceil", "round"]
details: |
Delimiter matching.
The `lr` function allows you to match two delimiters and scale them with the
content they contain. While this also happens automatically for delimiters
that match syntactically, `lr` allows you to match two arbitrary delimiters
and control their size exactly. Apart from the `lr` function, Typst provides
a few more functions that create delimiter pairings for absolute, ceiled,
and floored values as well as norms.
# Example
```example
$ [a, b/2] $
$ lr(]sum_(x=1)^n] x, size: #50%) $
$ abs((x + y) / 2) $
```
- name: calc
title: Calculation
category: foundations
path: ["calc"]
details: |
Module for calculations and processing of numeric values.
These definitions are part of the `calc` module and not imported by default.
In addition to the functions listed below, the `calc` module also defines
the constants `pi`, `tau`, `e`, `inf`, and `nan`.
- name: sys
title: System
category: foundations
path: ["sys"]
details: |
Module for system interactions.
This module defines the following items:
- The `sys.version` constant (of type [`version`]) that specifies
the currently active Typst compiler version.
- The `sys.inputs` [dictionary], which makes external inputs
available to the project. An input specified in the command line as
`--input key=value` becomes available under `sys.inputs.key` as
`{"value"}`. To include spaces in the value, it may be enclosed with
single or double quotes.
The value is always of type [string]($str). More complex data
may be parsed manually using functions like [`json.decode`]($json.decode).
- name: sym
title: General
category: symbols
path: ["sym"]
details: |
Named general symbols.
For example, `#sym.arrow` produces the → symbol. Within
[formulas]($category/math), these symbols can be used without the `#sym.`
prefix.
The `d` in an integral's `dx` can be written as `[$dif x$]`.
Outside math formulas, `dif` can be accessed as `math.dif`.
- name: emoji
title: Emoji
category: symbols
path: ["emoji"]
details: |
Named emoji.
For example, `#emoji.face` produces the 😀 emoji. If you frequently use
certain emojis, you can also import them from the `emoji` module (`[#import
emoji: face]`) to use them without the `#emoji.` prefix.

View file

@ -1,7 +1,15 @@
use std::fmt::Write;
use std::{collections::HashMap, fmt::Write};
use ecow::{eco_format, EcoString};
use typst::text::{FontInfo, FontStyle};
use once_cell::sync::Lazy;
use serde::Deserialize;
use serde_yaml as yaml;
use typst::{
diag::{bail, StrResult},
foundations::{Func, Module, Type, Value},
text::{FontInfo, FontStyle},
Library,
};
mod tooltip;
pub use tooltip::*;
@ -11,8 +19,347 @@ pub use complete::*;
/// Extract the first sentence of plain text of a piece of documentation.
///
/// Removes Markdown formatting.
fn plain_docs_sentence(docs: &str) -> EcoString {
docs.into()
pub fn plain_docs_sentence(docs: &str) -> EcoString {
log::debug!("plain docs {docs:?}");
let mut s = unscanny::Scanner::new(docs);
let mut output = EcoString::new();
let mut link = false;
while let Some(c) = s.eat() {
match c {
'`' => {
let mut raw = s.eat_until('`');
if (raw.starts_with('{') && raw.ends_with('}'))
|| (raw.starts_with('[') && raw.ends_with(']'))
{
raw = &raw[1..raw.len() - 1];
}
s.eat();
output.push('`');
output.push_str(raw);
output.push('`');
}
'[' => {
link = true;
output.push('[');
}
']' if link => {
output.push(']');
let c = s.cursor();
if s.eat_if('(') {
s.eat_until(')');
let link_content = s.from(c + 1);
s.eat();
log::info!("Intra Link: {link_content}");
let link = resolve(link_content, "https://typst.app/docs/").ok();
let link = link.unwrap_or_else(|| {
log::warn!("Failed to resolve link: {link_content}");
"https://typst.app/docs/404.html".to_string()
});
output.push('(');
output.push_str(&link);
output.push(')');
} else if s.eat_if('[') {
s.eat_until(']');
s.eat();
output.push_str(s.from(c));
}
link = false
}
// '*' | '_' => {}
// '.' => {
// output.push('.');
// break;
// }
_ => output.push(c),
}
}
output
}
/// Data about a collection of functions.
#[derive(Debug, Clone, Deserialize)]
struct GroupData {
name: EcoString,
// title: EcoString,
category: EcoString,
#[serde(default)]
path: Vec<EcoString>,
#[serde(default)]
filter: Vec<EcoString>,
// details: EcoString,
}
impl GroupData {
fn module(&self) -> &'static Module {
let mut focus = &LIBRARY.global;
for path in &self.path {
focus = get_module(focus, path).unwrap();
}
focus
}
}
static GROUPS: Lazy<Vec<GroupData>> = Lazy::new(|| {
let mut groups: Vec<GroupData> = yaml::from_str(include_str!("groups.yml")).unwrap();
for group in &mut groups {
if group.filter.is_empty() {
group.filter = group
.module()
.scope()
.iter()
.filter(|(_, v)| matches!(v, Value::Func(_)))
.map(|(k, _)| k.clone())
.collect();
}
}
groups
});
/// Resolve an intra-doc link.
pub fn resolve(link: &str, base: &str) -> StrResult<String> {
if link.starts_with('#') || link.starts_with("http") {
return Ok(link.to_string());
}
let (head, tail) = split_link(link)?;
let mut route = match resolve_known(head, base) {
Some(route) => route,
None => resolve_definition(head, base)?,
};
if !tail.is_empty() {
route.push('/');
route.push_str(tail);
}
if !route.contains(['#', '?']) && !route.ends_with('/') {
route.push('/');
}
Ok(route)
}
/// Split a link at the first slash.
fn split_link(link: &str) -> StrResult<(&str, &str)> {
let first = link.split('/').next().unwrap_or(link);
let rest = link[first.len()..].trim_start_matches('/');
Ok((first, rest))
}
/// Resolve a `$` link head to a known destination.
fn resolve_known(head: &str, base: &str) -> Option<String> {
Some(match head {
"$tutorial" => format!("{base}tutorial"),
"$reference" => format!("{base}reference"),
"$category" => format!("{base}reference"),
"$syntax" => format!("{base}reference/syntax"),
"$styling" => format!("{base}reference/styling"),
"$scripting" => format!("{base}reference/scripting"),
"$context" => format!("{base}reference/context"),
"$guides" => format!("{base}guides"),
"$changelog" => format!("{base}changelog"),
"$community" => format!("{base}community"),
"$universe" => "https://typst.app/universe".into(),
_ => return None,
})
}
static LIBRARY: Lazy<Library> = Lazy::new(Library::default);
/// Extract a module from another module.
#[track_caller]
fn get_module<'a>(parent: &'a Module, name: &str) -> StrResult<&'a Module> {
match parent.scope().get(name) {
Some(Value::Module(module)) => Ok(module),
_ => bail!("module doesn't contain module `{name}`"),
}
}
/// Resolve a `$` link to a global definition.
fn resolve_definition(head: &str, base: &str) -> StrResult<String> {
let mut parts = head.trim_start_matches('$').split('.').peekable();
let mut focus = &LIBRARY.global;
let mut category = None;
while let Some(name) = parts.peek() {
if category.is_none() {
category = focus.scope().get_category(name);
}
let Ok(module) = get_module(focus, name) else {
break;
};
focus = module;
parts.next();
}
let Some(category) = category else {
bail!("{head} has no category")
};
let name = parts.next().ok_or("link is missing first part")?;
let value = focus.field(name)?;
// Handle grouped functions.
if let Some(group) = GROUPS.iter().find(|group| {
group.category == category.name() && group.filter.iter().any(|func| func == name)
}) {
let mut route = format!(
"{}reference/{}/{}/#functions-{}",
base, group.category, group.name, name
);
if let Some(param) = parts.next() {
route.push('-');
route.push_str(param);
}
return Ok(route);
}
let mut route = format!("{}reference/{}/{name}", base, category.name());
if let Some(next) = parts.next() {
if let Ok(field) = value.field(next) {
route.push_str("/#definitions-");
route.push_str(next);
if let Some(next) = parts.next() {
if field
.cast::<Func>()
.is_ok_and(|func| func.param(next).is_some())
{
route.push('-');
route.push_str(next);
}
}
} else if value
.clone()
.cast::<Func>()
.is_ok_and(|func| func.param(next).is_some())
{
route.push_str("/#parameters-");
route.push_str(next);
} else {
bail!("field {next} not found");
}
}
Ok(route)
}
#[allow(clippy::derived_hash_with_manual_eq)]
#[derive(Debug, Clone, Hash)]
enum CatKey {
Func(Func),
Type(Type),
}
impl PartialEq for CatKey {
fn eq(&self, other: &Self) -> bool {
use typst::foundations::func::Repr::*;
match (self, other) {
(CatKey::Func(a), CatKey::Func(b)) => match (a.inner(), b.inner()) {
(Native(a), Native(b)) => a == b,
(Element(a), Element(b)) => a == b,
_ => false,
},
(CatKey::Type(a), CatKey::Type(b)) => a == b,
_ => false,
}
}
}
impl Eq for CatKey {}
// todo: category of types
static ROUTE_MAPS: Lazy<HashMap<CatKey, String>> = Lazy::new(|| {
let mut map = HashMap::new();
let mut scope_to_finds = vec![
(LIBRARY.global.scope(), None, None),
(LIBRARY.math.scope(), None, None),
];
while let Some((scope, parent_name, cat)) = scope_to_finds.pop() {
for (name, value) in scope.iter() {
let cat = cat.or_else(|| scope.get_category(name));
let name = urlify(name);
match value {
Value::Func(f) => {
if let Some(cat) = cat {
let Some(name) = f.name() else {
continue;
};
// Handle grouped functions.
if let Some(group) = GROUPS.iter().find(|group| {
group.category == cat.name()
&& group.filter.iter().any(|func| func == name)
}) {
let route = format!(
"reference/{}/{}/#functions-{name}",
group.category, group.name
);
map.insert(CatKey::Func(f.clone()), route);
continue;
}
log::debug!("func: {f:?} -> {cat:?}");
let route = if let Some(parent_name) = &parent_name {
format!("reference/{}/{parent_name}/#definitions-{name}", cat.name())
} else {
format!("reference/{}/{name}/", cat.name())
};
map.insert(CatKey::Func(f.clone()), route);
}
if let Some(s) = f.scope() {
scope_to_finds.push((s, Some(name), cat));
}
}
Value::Type(t) => {
if let Some(cat) = cat {
log::debug!("type: {t:?} -> {cat:?}");
let route = if let Some(parent_name) = &parent_name {
format!("reference/{}/{parent_name}/#definitions-{name}", cat.name())
} else {
format!("reference/{}/{name}/", cat.name())
};
map.insert(CatKey::Type(*t), route);
}
scope_to_finds.push((t.scope(), Some(name), cat));
}
Value::Module(module) => {
scope_to_finds.push((module.scope(), Some(name), cat));
}
_ => {}
}
}
}
map
});
/// Turn a title into an URL fragment.
pub(crate) fn urlify(title: &str) -> EcoString {
title
.chars()
.map(|c| c.to_ascii_lowercase())
.map(|c| match c {
'a'..='z' | '0'..='9' => c,
_ => '-',
})
.collect()
}
pub fn route_of_value(k: &Value) -> Option<&'static String> {
// ROUTE_MAPS.get(&CatKey::Func(k.clone()))
let key = match k {
Value::Func(f) => CatKey::Func(f.clone()),
Value::Type(t) => CatKey::Type(*t),
_ => return None,
};
ROUTE_MAPS.get(&key)
}
/// Create a short description of a font family.
@ -45,3 +392,30 @@ fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> Ec
detail
}
#[cfg(test)]
mod tests {
#[test]
fn docs_test() {
assert_eq!(
"[citation](https://typst.app/docs/reference/model/cite/)",
super::plain_docs_sentence("[citation]($cite)")
);
assert_eq!(
"[citation][cite]",
super::plain_docs_sentence("[citation][cite]")
);
assert_eq!(
"[citation](https://typst.app/docs/reference/model/cite/)",
super::plain_docs_sentence("[citation]($cite)")
);
assert_eq!(
"[citation][cite][cite2]",
super::plain_docs_sentence("[citation][cite][cite2]")
);
assert_eq!(
"[citation][cite](test)[cite2]",
super::plain_docs_sentence("[citation][cite](test)[cite2]")
);
}
}

View file

@ -10,7 +10,7 @@ use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
use typst::util::{round_2, Numeric};
use typst::World;
use super::summarize_font_family;
use super::{plain_docs_sentence, summarize_font_family};
use crate::analysis::{analyze_expr, analyze_labels, DynLabel};
/// Describe the item under the cursor.
@ -61,7 +61,7 @@ pub fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
if let [(value, _)] = values.as_slice() {
if let Some(docs) = value.docs() {
return Some(Tooltip::Text(docs.into()));
return Some(Tooltip::Text(plain_docs_sentence(docs)));
}
if let &Value::Length(length) = value {
@ -204,7 +204,7 @@ fn named_param_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip>
if let Some(ident) = leaf.cast::<ast::Ident>();
if let Some(param) = func.param(&ident);
then {
return Some(Tooltip::Text(param.docs.into()));
return Some(Tooltip::Text(plain_docs_sentence(param.docs)));
}
}