mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-08-03 17:58:17 +00:00
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:
parent
68bcc2b571
commit
6ad9258740
14 changed files with 662 additions and 21 deletions
|
@ -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()))?;
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
169
crates/tinymist-query/src/upstream/groups.yml
Normal file
169
crates/tinymist-query/src/upstream/groups.yml
Normal 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.
|
|
@ -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]")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue