feat: rearrange hover providers (#1108)

* feat: rearrange hover providers

* fix: warnings

* test: update snapshot

* test: update hover snapshot
This commit is contained in:
Myriad-Dreamin 2025-01-05 14:05:13 +08:00 committed by GitHub
parent 671783a964
commit 975e4a27bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 273 additions and 301 deletions

View file

@ -11,16 +11,14 @@ use reflexo::debug_loc::DataSource;
use reflexo::hash::{hash128, FxDashMap};
use reflexo_typst::{EntryReader, WorldDeps};
use rustc_hash::FxHashMap;
use tinymist_world::LspWorld;
use tinymist_world::DETACHED_ENTRY;
use tinymist_world::{LspWorld, DETACHED_ENTRY};
use typst::diag::{eco_format, At, FileError, FileResult, SourceResult, StrResult};
use typst::engine::{Route, Sink, Traced};
use typst::eval::Eval;
use typst::foundations::{Bytes, Module, Styles};
use typst::layout::Position;
use typst::model::Document;
use typst::syntax::package::PackageManifest;
use typst::syntax::{package::PackageSpec, Span, VirtualPath};
use typst::syntax::package::{PackageManifest, PackageSpec};
use typst::syntax::{Span, VirtualPath};
use crate::adt::revision::{RevisionLock, RevisionManager, RevisionManagerLike, RevisionSlot};
use crate::analysis::prelude::*;
@ -880,14 +878,9 @@ impl SharedContext {
/// Passing a `document` (from a previous compilation) is optional, but
/// enhances the autocompletions. Label completions, for instance, are
/// only generated when the document is available.
pub fn tooltip(
&self,
document: Option<&Document>,
source: &Source,
cursor: usize,
) -> Option<Tooltip> {
pub fn tooltip(&self, source: &Source, cursor: usize) -> Option<Tooltip> {
let token = &self.analysis.workers.tooltip;
token.enter(|| tooltip_(&self.world, document, source, cursor))
token.enter(|| tooltip_(&self.world, source, cursor))
}
/// Get the manifest of a package by file id.

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/annotate_dict_param.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet show-example(\n ..options: arguments,\n inherited-scope: dictionary = (:),\n) = none;\n```\n\n---\n- <!-- typlite:begin:list-item 0 -->inherited-scope (dictionary): Definitions that are made available to the entire parsed\n module. This parameter is only used internally.<!-- typlite:end:list-item 0 -->\n\n# Rest Parameters\n\n## options\n\n```typc\ntype: arguments\n```\n\n\n\n# Named Parameters\n\n## inherited-scope\n\n```typc\ntype: dictionary\n```\n\nDefinitions that are made available to the entire parsed\n module. This parameter is only used internally.",
"contents": "```typc\nlet show-example(\n ..options: arguments,\n inherited-scope: dictionary = (:),\n) = none;\n```\n\n---\n\n- <!-- typlite:begin:list-item 0 -->inherited-scope (dictionary): Definitions that are made available to the entire parsed\n module. This parameter is only used internally.<!-- typlite:end:list-item 0 -->\n\n# Rest Parameters\n\n## options\n\n```typc\ntype: arguments\n```\n\n\n\n# Named Parameters\n\n## inherited-scope\n\n```typc\ntype: dictionary\n```\n\nDefinitions that are made available to the entire parsed\n module. This parameter is only used internally.",
"range": "7:20:7:32"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/annotate_dict_param2.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet inherited-scope = dictionary;\n```\n\n---\nDefinitions that are made available to the entire parsed\n module. This parameter is only used internally.",
"contents": "```typc\nlet inherited-scope = dictionary;\n```\n\n---\n\nDefinitions that are made available to the entire parsed\n module. This parameter is only used internally.",
"range": "6:21:6:36"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/annotate_docs_error.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet speaker-note(\n note: any,\n mode: str = \"typ\",\n setting: (any) => any = Closure(..),\n) = none;\n```\n\n---\nSpeaker notes are a way to add additional information to your slides that is not visible to the audience. This can be useful for providing additional context or reminders to yourself.\n\n ## Example\n\n ```typ\n#speaker-note[This is a speaker note]\n\n```\n```\nRender Error\ncompiling node: error: unknown variable: speaker-note at \"/__render__.typ\":201..213\nHint: if you meant to use subtraction, try adding spaces around the minus sign: \\`speaker - note\\`\n\n```\n\n# Positional Parameters\n\n## note\n\n```typc\ntype: \n```\n\n\n\n# Named Parameters\n\n## mode\n\n```typc\ntype: \"typ\"\n```\n\n\n\n## setting (named)\n\n```typc\ntype: (any) => any\n```\n\n",
"contents": "```typc\nlet speaker-note(\n note: any,\n mode: str = \"typ\",\n setting: (any) => any = Closure(..),\n) = none;\n```\n\n---\n\nSpeaker notes are a way to add additional information to your slides that is not visible to the audience. This can be useful for providing additional context or reminders to yourself.\n\n ## Example\n\n ```typ\n#speaker-note[This is a speaker note]\n\n```\n```\nRender Error\ncompiling node: error: unknown variable: speaker-note at \"/__render__.typ\":201..213\nHint: if you meant to use subtraction, try adding spaces around the minus sign: \\`speaker - note\\`\n\n```\n\n# Positional Parameters\n\n## note\n\n```typc\ntype: \n```\n\n\n\n# Named Parameters\n\n## mode\n\n```typc\ntype: \"typ\"\n```\n\n\n\n## setting (named)\n\n```typc\ntype: (any) => any\n```\n\n",
"range": "11:20:11:32"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/annotate_fn.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet touying-fn-wrapper(\n fn: (..: any) => any | function,\n ..args: arguments,\n max-repetitions: int | none = none,\n repetitions: int | none = none,\n) = none;\n```\n\n---\n\n\n# Positional Parameters\n\n## fn\n\n```typc\ntype: (..: any) => any | function\n```\n\nThe `fn`.\n\n# Rest Parameters\n\n## args\n\n```typc\ntype: arguments\n```\n\nThe `args`.\n\n# Named Parameters\n\n## max-repetitions\n\n```typc\ntype: int | none\n```\n\nThe `max-repetitions`.\n\n## repetitions (named)\n\n```typc\ntype: int | none\n```\n\nThe `repetitions`.",
"contents": "```typc\nlet touying-fn-wrapper(\n fn: (..: any) => any | function,\n ..args: arguments,\n max-repetitions: int | none = none,\n repetitions: int | none = none,\n) = none;\n```\n\n---\n\n\n\n# Positional Parameters\n\n## fn\n\n```typc\ntype: (..: any) => any | function\n```\n\nThe `fn`.\n\n# Rest Parameters\n\n## args\n\n```typc\ntype: arguments\n```\n\nThe `args`.\n\n# Named Parameters\n\n## max-repetitions\n\n```typc\ntype: int | none\n```\n\nThe `max-repetitions`.\n\n## repetitions (named)\n\n```typc\ntype: int | none\n```\n\nThe `repetitions`.",
"range": "8:20:8:38"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/annotate_ret.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet _delayed-wrapper(\n body: any,\n) = content;\n```\n\n---\n\n\n# Positional Parameters\n\n## body\n\n```typc\ntype: \n```\n\n",
"contents": "```typc\nlet _delayed-wrapper(\n body: any,\n) = content;\n```\n\n---\n\n\n\n# Positional Parameters\n\n## body\n\n```typc\ntype: \n```\n\n",
"range": "6:20:6:36"
}

File diff suppressed because one or more lines are too long

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/builtin_module.typ
snapshot_kind: text
---
{
"contents": "```typc\n<module sys>\n```\n\n---\n```typc\nlet sys;\n```",
"contents": "```typc\nlet sys;\n```\n\n---\n\n### Sampled Values\n```typc\n<module sys>\n```",
"range": "0:20:0:23"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/builtin_var.typ
snapshot_kind: text
---
{
"contents": "```typc\nrgb(\"#ff4136\")\n```",
"contents": "### Sampled Values\n```typc\nrgb(\"#ff4136\")\n```",
"range": "0:20:0:23"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/builtin_var2.typ
snapshot_kind: text
---
{
"contents": "```typc\n<module sys>\n```\n\n---\n```typc\nlet sys;\n```",
"contents": "```typc\nlet sys;\n```\n\n---\n\n### Sampled Values\n```typc\n<module sys>\n```",
"range": "0:20:0:23"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/builtin_var3.typ
snapshot_kind: text
---
{
"contents": "```typc\n<module sys>\n```\n\n---\n```typc\nlet sys;\n```",
"contents": "```typc\nlet sys;\n```\n\n---\n\n### Sampled Values\n```typc\n<module sys>\n```",
"range": "0:2:0:5"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/module_alias.typ
snapshot_kind: text
---
{
"contents": "# The Module (Alias)",
"contents": "### Sampled Values\n```typc\n<module themod>\n```\n\n---\n\n# The Module (Alias)",
"range": "2:24:2:31"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/module_star.typ
snapshot_kind: text
---
{
"contents": "This star imports line",
"contents": "This star imports line\n\n---\n\n### Sampled Values\n```typc\nnone\n```",
"range": "0:41:0:42"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/module_star_const_eval.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet line() = int;\n```\n\n---\nThe draw line.",
"contents": "```typc\nlet line() = int;\n```\n\n---\n\nThe draw line.",
"range": "2:23:2:27"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/module_star_rename.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet line() = int;\n```\n\n---\nThe draw line.",
"contents": "```typc\nlet line() = int;\n```\n\n---\n\nThe draw line.",
"range": "3:23:3:27"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/module_star_shadow.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet line() = int;\n```\n\n---\nThe draw line.",
"contents": "```typc\nlet line() = int;\n```\n\n---\n\nThe draw line.",
"range": "2:23:2:27"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/module_var.typ
snapshot_kind: text
---
{
"contents": "# The Module",
"contents": "### Sampled Values\n```typc\n<module themod>\n```\n\n---\n\n# The Module",
"range": "2:24:2:30"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/pagebreak.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet pagebreak(\n to: none | str = none,\n weak: bool = false,\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# Named Parameters\n\n## to\n\n```typc\ntype: \"even\" | \"odd\" | none\n```\n\nIf given, ensures that the next page will be an even/odd page, with an\nempty page in between if necessary.\n\n```typ\n#set page(height: 30pt)\n\nFirst.\n#pagebreak(to: \"odd\")\nThird.\n```\n\n## weak (named)\n\n```typc\ntype: bool\n```\n\nIf `true`, the page break is skipped if the current page is already\nempty.\n\n---\n[Open docs](https://typst.app/docs/reference/layout/pagebreak/)",
"contents": "```typc\nlet pagebreak(\n to: none | str = none,\n weak: bool = false,\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\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# Named Parameters\n\n## to\n\n```typc\ntype: \"even\" | \"odd\" | none\n```\n\nIf given, ensures that the next page will be an even/odd page, with an\nempty page in between if necessary.\n\n```typ\n#set page(height: 30pt)\n\nFirst.\n#pagebreak(to: \"odd\")\nThird.\n```\n\n## weak (named)\n\n```typc\ntype: bool\n```\n\nIf `true`, the page break is skipped if the current page is already\nempty.\n\n---\n\n[Open docs](https://typst.app/docs/reference/layout/pagebreak/)",
"range": "0:20:0:29"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/param.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet param = int;\n```\n\n---\nThe `parameter`.",
"contents": "```typc\nlet param = int;\n```\n\n---\n\nThe `parameter`.",
"range": "3:25:3:30"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/render_equation.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet lam(\n A: type,\n B: type,\n) = dictionary;\n```\n\n---\nLambda constructor.\n\n Typing Rule:\n\n <p align=\"center\"><img alt=\"typst-block\" src=\"data:image-hash/svg+xml;base64,redacted\" /></p>\n\n# Positional Parameters\n\n## A\n\n```typc\ntype: type\n```\n\nThe type of the argument.\n - <!-- typlite:begin:list-item 1 -->It can be also regarded as the condition of the proposition.<!-- typlite:end:list-item 1 -->\n\n## B (positional)\n\n```typc\ntype: type\n```\n\nThe type of the body.\n - <!-- typlite:begin:list-item 1 -->It can be also regarded as the conclusion of the proposition.<!-- typlite:end:list-item 1 -->",
"contents": "```typc\nlet lam(\n A: type,\n B: type,\n) = dictionary;\n```\n\n---\n\nLambda constructor.\n\n Typing Rule:\n\n <p align=\"center\"><img alt=\"typst-block\" src=\"data:image-hash/svg+xml;base64,redacted\" /></p>\n\n# Positional Parameters\n\n## A\n\n```typc\ntype: type\n```\n\nThe type of the argument.\n - <!-- typlite:begin:list-item 1 -->It can be also regarded as the condition of the proposition.<!-- typlite:end:list-item 1 -->\n\n## B (positional)\n\n```typc\ntype: type\n```\n\nThe type of the body.\n - <!-- typlite:begin:list-item 1 -->It can be also regarded as the conclusion of the proposition.<!-- typlite:end:list-item 1 -->",
"range": "12:20:12:23"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/render_equation_no_html.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet lam(\n A: type,\n B: type,\n) = dictionary;\n```\n\n---\nLambda constructor.\n\n Typing Rule:\n\n ```typc\n$ (Γ , x : A ⊢ M : B #h(2em) Γ ⊢ a:B)/(Γ ⊢ λ (x : A) → M : π (x : A) → B) $\n```\n\n# Positional Parameters\n\n## A\n\n```typc\ntype: type\n```\n\nThe type of the argument.\n - It can be also regarded as the condition of the proposition.\n\n## B (positional)\n\n```typc\ntype: type\n```\n\nThe type of the body.\n - It can be also regarded as the conclusion of the proposition.",
"contents": "```typc\nlet lam(\n A: type,\n B: type,\n) = dictionary;\n```\n\n---\n\nLambda constructor.\n\n Typing Rule:\n\n ```typc\n$ (Γ , x : A ⊢ M : B #h(2em) Γ ⊢ a:B)/(Γ ⊢ λ (x : A) → M : π (x : A) → B) $\n```\n\n# Positional Parameters\n\n## A\n\n```typc\ntype: type\n```\n\nThe type of the argument.\n - It can be also regarded as the condition of the proposition.\n\n## B (positional)\n\n```typc\ntype: type\n```\n\nThe type of the body.\n - It can be also regarded as the conclusion of the proposition.",
"range": "14:20:14:23"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/user.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet f() = int;\n```\n\n---\nTest",
"contents": "```typc\nlet f() = int;\n```\n\n---\n\nTest",
"range": "3:20:3:21"
}

View file

@ -5,6 +5,6 @@ input_file: crates/tinymist-query/src/fixtures/hover/value_repr.typ
snapshot_kind: text
---
{
"contents": "```typc\nlet f(\n x: any,\n y: any,\n z: any,\n w01: int = 1,\n w02: str = \"test\",\n w03: any = 1 + 2,\n w04: any = Label(test),\n w05: (content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box = (content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box,\n w06: (content) => item | any = (body-indent: length, indent: length, marker: array | content | function, spacing: auto | length, tight: bool, ..: content) => list.item,\n w07: content = Expr(..),\n w08: any = Expr(..),\n w09: any = 1 + 2,\n w10: array = (\n 1,\n 2,\n ),\n w11: array = (),\n w12: dictionary = (:),\n w13: dictionary = (a: 1),\n w14: dictionary = (a: (content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box),\n w15: dictionary = (a: (body-indent: length, indent: length, marker: array | content | function, spacing: auto | length, tight: bool, ..: content) => list.item),\n) = int;\n```\n\n---\n\n\n# Positional Parameters\n\n## x\n\n```typc\ntype: \n```\n\n\n\n## y (positional)\n\n```typc\ntype: \n```\n\n\n\n## z (positional)\n\n```typc\ntype: \n```\n\n\n\n# Named Parameters\n\n## w01\n\n```typc\ntype: 1\n```\n\n\n\n## w02 (named)\n\n```typc\ntype: \"test\"\n```\n\n\n\n## w03 (named)\n\n```typc\ntype: any\n```\n\n\n\n## w04 (named)\n\n```typc\ntype: \n```\n\n\n\n## w05 (named)\n\n```typc\ntype: (content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box\n```\n\n\n\n## w06 (named)\n\n```typc\ntype: (content) => item | any\n```\n\n\n\n## w07 (named)\n\n```typc\ntype: content\n```\n\n\n\n## w08 (named)\n\n```typc\ntype: any\n```\n\n\n\n## w09 (named)\n\n```typc\ntype: any\n```\n\n\n\n## w10 (named)\n\n```typc\ntype: array\n```\n\n\n\n## w11 (named)\n\n```typc\ntype: array\n```\n\n\n\n## w12 (named)\n\n```typc\ntype: dictionary\n```\n\n\n\n## w13 (named)\n\n```typc\ntype: dictionary\n```\n\n\n\n## w14 (named)\n\n```typc\ntype: dictionary\n```\n\n\n\n## w15 (named)\n\n```typc\ntype: dictionary\n```\n\n",
"contents": "```typc\nlet f(\n x: any,\n y: any,\n z: any,\n w01: int = 1,\n w02: str = \"test\",\n w03: any = 1 + 2,\n w04: any = Label(test),\n w05: (content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box = (content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box,\n w06: (content) => item | any = (body-indent: length, indent: length, marker: array | content | function, spacing: auto | length, tight: bool, ..: content) => list.item,\n w07: content = Expr(..),\n w08: any = Expr(..),\n w09: any = 1 + 2,\n w10: array = (\n 1,\n 2,\n ),\n w11: array = (),\n w12: dictionary = (:),\n w13: dictionary = (a: 1),\n w14: dictionary = (a: (content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box),\n w15: dictionary = (a: (body-indent: length, indent: length, marker: array | content | function, spacing: auto | length, tight: bool, ..: content) => list.item),\n) = int;\n```\n\n---\n\n\n\n# Positional Parameters\n\n## x\n\n```typc\ntype: \n```\n\n\n\n## y (positional)\n\n```typc\ntype: \n```\n\n\n\n## z (positional)\n\n```typc\ntype: \n```\n\n\n\n# Named Parameters\n\n## w01\n\n```typc\ntype: 1\n```\n\n\n\n## w02 (named)\n\n```typc\ntype: \"test\"\n```\n\n\n\n## w03 (named)\n\n```typc\ntype: any\n```\n\n\n\n## w04 (named)\n\n```typc\ntype: \n```\n\n\n\n## w05 (named)\n\n```typc\ntype: (content | none, baseline: relative, clip: bool, fill: color, height: auto | relative, inset: inset, outset: outset, radius: radius, stroke: stroke, width: auto | fraction | relative) => box\n```\n\n\n\n## w06 (named)\n\n```typc\ntype: (content) => item | any\n```\n\n\n\n## w07 (named)\n\n```typc\ntype: content\n```\n\n\n\n## w08 (named)\n\n```typc\ntype: any\n```\n\n\n\n## w09 (named)\n\n```typc\ntype: any\n```\n\n\n\n## w10 (named)\n\n```typc\ntype: array\n```\n\n\n\n## w11 (named)\n\n```typc\ntype: array\n```\n\n\n\n## w12 (named)\n\n```typc\ntype: dictionary\n```\n\n\n\n## w13 (named)\n\n```typc\ntype: dictionary\n```\n\n\n\n## w14 (named)\n\n```typc\ntype: dictionary\n```\n\n\n\n## w15 (named)\n\n```typc\ntype: dictionary\n```\n\n",
"range": "23:20:23:21"
}

View file

@ -6,7 +6,7 @@ use typst_shim::syntax::LinkedNodeExt;
use crate::analysis::get_link_exprs_in;
use crate::jump_from_cursor;
use crate::prelude::*;
use crate::upstream::{expr_tooltip, route_of_value, truncated_repr, Tooltip};
use crate::upstream::{route_of_value, truncated_repr, Tooltip};
/// The [`textDocument/hover`] request asks the server for hover information at
/// a given text document position.
@ -31,8 +31,6 @@ impl StatefulRequest for HoverRequest {
ctx: &mut LocalContext,
doc: Option<VersionedDocument>,
) -> Option<Self::Response> {
let doc_ref = doc.as_ref().map(|doc| doc.document.as_ref());
let source = ctx.source_by_path(&self.path).ok()?;
let offset = ctx.to_typst_pos(self.position, &source)?;
// the typst's cursor is 1-based, so we need to add 1 to the offset
@ -41,242 +39,245 @@ impl StatefulRequest for HoverRequest {
let node = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
let range = ctx.to_lsp_range(node.range(), &source);
let contents = def_tooltip(ctx, &source, doc.as_ref(), cursor)
.or_else(|| star_tooltip(ctx, &node))
.or_else(|| link_tooltip(ctx, &node, cursor))
.or_else(|| Some(to_lsp_tooltip(&ctx.tooltip(doc_ref, &source, cursor)?)))?;
// Neovim shows ugly hover if the hover content is in array, so we join them
// manually with divider bars.
let mut contents = match contents {
HoverContents::Array(contents) => contents
.into_iter()
.map(|content| match content {
MarkedString::LanguageString(content) => {
format!("```{}\n{}\n```", content.language, content.value)
}
MarkedString::String(content) => content,
})
.join("\n\n---\n"),
HoverContents::Scalar(MarkedString::String(contents)) => contents,
HoverContents::Scalar(MarkedString::LanguageString(contents)) => {
format!("```{}\n{}\n```", contents.language, contents.value)
}
lsp_types::HoverContents::Markup(content) => {
match content.kind {
MarkupKind::Markdown => content.value,
// todo: escape
MarkupKind::PlainText => content.value,
}
}
let mut worker = HoverWorker {
ctx,
source,
doc,
cursor,
def: Default::default(),
value: Default::default(),
preview: Default::default(),
docs: Default::default(),
actions: Default::default(),
};
if let Some(provider) = ctx.analysis.periscope.clone() {
if let Some(doc) = doc.clone() {
let position = jump_from_cursor(&doc.document, &source, cursor);
let position = position.or_else(|| {
for idx in 1..100 {
let next_cursor = cursor + idx;
if next_cursor < source.text().len() {
let position = jump_from_cursor(&doc.document, &source, next_cursor);
if position.is_some() {
return position;
}
}
let prev_cursor = cursor.checked_sub(idx);
if let Some(prev_cursor) = prev_cursor {
let position = jump_from_cursor(&doc.document, &source, prev_cursor);
if position.is_some() {
return position;
}
}
}
worker.work();
None
});
let mut contents = vec![];
log::info!("telescope position: {:?}", position);
let content = position.and_then(|pos| provider.periscope_at(ctx, doc, pos));
if let Some(preview_content) = content {
contents = format!("{preview_content}\n---\n{contents}");
}
}
contents.append(&mut worker.def);
contents.append(&mut worker.value);
contents.append(&mut worker.preview);
contents.append(&mut worker.docs);
if !worker.actions.is_empty() {
let content = worker.actions.into_iter().join(" | ");
contents.push(content);
}
if contents.is_empty() {
return None;
}
Some(Hover {
contents: HoverContents::Scalar(MarkedString::String(contents)),
// Neovim shows ugly hover if the hover content is in array, so we join them
// manually with divider bars.
contents: HoverContents::Scalar(MarkedString::String(contents.join("\n\n---\n\n"))),
range: Some(range),
})
}
}
fn def_tooltip(
ctx: &mut LocalContext,
source: &Source,
document: Option<&VersionedDocument>,
struct HoverWorker<'a> {
ctx: &'a mut LocalContext,
source: Source,
doc: Option<VersionedDocument>,
cursor: usize,
) -> Option<HoverContents> {
let leaf = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
let syntax = classify_syntax(leaf.clone(), cursor)?;
let def = ctx.def_of_syntax(source, document, syntax.clone())?;
def: Vec<String>,
value: Vec<String>,
preview: Vec<String>,
docs: Vec<String>,
actions: Vec<CommandLink>,
}
let mut results = vec![];
let mut actions = vec![];
impl HoverWorker<'_> {
fn work(&mut self) {
self.static_analysis();
self.preview();
self.dynamic_analysis();
}
use Decl::*;
match def.decl.as_ref() {
Label(..) => {
results.push(MarkedString::String(format!("Label: {}\n", def.name())));
// todo: type repr
if let Some(val) = def.term.as_ref().and_then(|v| v.value()) {
let repr = truncated_repr(&val);
results.push(MarkedString::String(format!("{repr}")));
/// Static analysis results
fn static_analysis(&mut self) -> Option<()> {
let source = self.source.clone();
let leaf = LinkedNode::new(source.root()).leaf_at_compat(self.cursor)?;
self.definition()
.or_else(|| self.star(&leaf))
.or_else(|| self.link(&leaf))
}
/// Dynamic analysis results
fn dynamic_analysis(&mut self) -> Option<()> {
let typst_tooltip = self.ctx.tooltip(&self.source, self.cursor)?;
self.value.push(match typst_tooltip {
Tooltip::Text(text) => text.to_string(),
Tooltip::Code(code) => format!("### Sampled Values\n```typc\n{code}\n```"),
});
Some(())
}
/// Definition analysis results
fn definition(&mut self) -> Option<()> {
let leaf = LinkedNode::new(self.source.root()).leaf_at_compat(self.cursor)?;
let syntax = classify_syntax(leaf.clone(), self.cursor)?;
let def = self
.ctx
.def_of_syntax(&self.source, self.doc.as_ref(), syntax.clone())?;
use Decl::*;
match def.decl.as_ref() {
Label(..) => {
self.def.push(format!("Label: {}\n", def.name()));
// todo: type repr
if let Some(val) = def.term.as_ref().and_then(|v| v.value()) {
self.def.push(truncated_repr(&val).into());
}
}
Some(HoverContents::Array(results))
}
BibEntry(..) => {
results.push(MarkedString::String(format!(
"Bibliography: @{}",
def.name()
)));
BibEntry(..) => {
self.def.push(format!("Bibliography: @{}", def.name()));
}
_ => {
let sym_docs = self.ctx.def_docs(&def);
Some(HoverContents::Array(results))
}
_ => {
let sym_docs = ctx.def_docs(&def);
// todo: hover with `with_stack`
if matches!(def.decl.kind(), DefKind::Variable | DefKind::Constant) {
// todo: check sensible length, value highlighting
if let Some(values) = expr_tooltip(ctx.world(), syntax.node()) {
match values {
Tooltip::Text(values) => {
results.push(MarkedString::String(values.into()));
if matches!(
def.decl.kind(),
DefKind::Function | DefKind::Variable | DefKind::Constant
) && !def.name().is_empty()
{
let mut type_doc = String::new();
type_doc.push_str("let ");
type_doc.push_str(def.name());
match &sym_docs {
Some(DefDocs::Variable(docs)) => {
push_result_ty(def.name(), docs.return_ty.as_ref(), &mut type_doc);
}
Tooltip::Code(values) => {
results.push(MarkedString::LanguageString(LanguageString {
language: "typc".to_owned(),
value: values.into(),
}));
Some(DefDocs::Function(docs)) => {
let _ = docs.print(&mut type_doc);
push_result_ty(def.name(), docs.ret_ty.as_ref(), &mut type_doc);
}
_ => {}
}
self.def.push(format!("```typc\n{type_doc};\n```"));
}
if let Some(doc) = sym_docs {
let hover_docs = doc.hover_docs();
if !hover_docs.trim().is_empty() {
self.docs.push(hover_docs.into());
}
}
if let Some(link) = ExternalDocLink::get(&def) {
self.actions.push(link);
}
}
}
Some(())
}
fn star(&mut self, mut node: &LinkedNode) -> Option<()> {
if !matches!(node.kind(), SyntaxKind::Star) {
return None;
}
while !matches!(node.kind(), SyntaxKind::ModuleImport) {
node = node.parent()?;
}
let import_node = node.cast::<ast::ModuleImport>()?;
let scope_val = self
.ctx
.module_by_syntax(import_node.source().to_untyped())?;
let scope_items = scope_val.scope()?.iter();
let mut names = scope_items.map(|item| item.0.as_str()).collect::<Vec<_>>();
names.sort();
let content = format!("This star imports {}", separated_list(&names, "and"));
self.def.push(content);
Some(())
}
fn link(&mut self, mut node: &LinkedNode) -> Option<()> {
while !matches!(node.kind(), SyntaxKind::FuncCall) {
node = node.parent()?;
}
let links = get_link_exprs_in(node)?;
let links = links
.objects
.iter()
.filter(|link| link.range.contains(&self.cursor))
.collect::<Vec<_>>();
if links.is_empty() {
return None;
}
for obj in links {
let Some(target) = obj.target.resolve(self.ctx) else {
continue;
};
// open file in tab or system application
self.actions.push(CommandLink {
title: Some("Open in Tab".to_string()),
command_or_links: vec![CommandOrLink::Command {
id: "tinymist.openInternal".to_string(),
args: vec![JsonValue::String(target.to_string())],
}],
});
self.actions.push(CommandLink {
title: Some("Open Externally".to_string()),
command_or_links: vec![CommandOrLink::Command {
id: "tinymist.openExternal".to_string(),
args: vec![JsonValue::String(target.to_string())],
}],
});
if let Some(kind) = PathPreference::from_ext(target.path()) {
self.def.push(format!("A `{kind:?}` file."));
}
}
Some(())
}
fn preview(&mut self) -> Option<()> {
// Preview results
let provider = self.ctx.analysis.periscope.clone()?;
let doc = self.doc.as_ref()?;
let position = jump_from_cursor(&doc.document, &self.source, self.cursor);
let position = position.or_else(|| {
for idx in 1..100 {
let next_cursor = self.cursor + idx;
if next_cursor < self.source.text().len() {
let position = jump_from_cursor(&doc.document, &self.source, next_cursor);
if position.is_some() {
return position;
}
}
let prev_cursor = self.cursor.checked_sub(idx);
if let Some(prev_cursor) = prev_cursor {
let position = jump_from_cursor(&doc.document, &self.source, prev_cursor);
if position.is_some() {
return position;
}
}
}
// todo: hover with `with_stack`
if matches!(
def.decl.kind(),
DefKind::Function | DefKind::Variable | DefKind::Constant
) && !def.name().is_empty()
{
results.push(MarkedString::LanguageString(LanguageString {
language: "typc".to_owned(),
value: {
let mut type_doc = String::new();
type_doc.push_str("let ");
type_doc.push_str(def.name());
match &sym_docs {
Some(DefDocs::Variable(docs)) => {
push_result_ty(def.name(), docs.return_ty.as_ref(), &mut type_doc);
}
Some(DefDocs::Function(docs)) => {
let _ = docs.print(&mut type_doc);
push_result_ty(def.name(), docs.ret_ty.as_ref(), &mut type_doc);
}
_ => {}
}
type_doc.push(';');
type_doc
},
}));
}
if let Some(doc) = sym_docs {
results.push(MarkedString::String(doc.hover_docs().into()));
}
if let Some(link) = ExternalDocLink::get(&def) {
actions.push(link);
}
render_actions(&mut results, actions);
Some(HoverContents::Array(results))
}
}
}
fn star_tooltip(ctx: &mut LocalContext, mut node: &LinkedNode) -> Option<HoverContents> {
if !matches!(node.kind(), SyntaxKind::Star) {
return None;
}
while !matches!(node.kind(), SyntaxKind::ModuleImport) {
node = node.parent()?;
}
let import_node = node.cast::<ast::ModuleImport>()?;
let scope_val = ctx.module_by_syntax(import_node.source().to_untyped())?;
let scope_items = scope_val.scope()?.iter();
let mut names = scope_items.map(|item| item.0.as_str()).collect::<Vec<_>>();
names.sort();
let content = format!("This star imports {}", separated_list(&names, "and"));
Some(HoverContents::Scalar(MarkedString::String(content)))
}
fn link_tooltip(
ctx: &mut LocalContext,
mut node: &LinkedNode,
cursor: usize,
) -> Option<HoverContents> {
while !matches!(node.kind(), SyntaxKind::FuncCall) {
node = node.parent()?;
}
let links = get_link_exprs_in(node)?;
let links = links
.objects
.iter()
.filter(|link| link.range.contains(&cursor))
.collect::<Vec<_>>();
if links.is_empty() {
return None;
}
let mut results = vec![];
let mut actions = vec![];
for obj in links {
let Some(target) = obj.target.resolve(ctx) else {
continue;
};
// open file in tab or system application
actions.push(CommandLink {
title: Some("Open in Tab".to_string()),
command_or_links: vec![CommandOrLink::Command {
id: "tinymist.openInternal".to_string(),
args: vec![JsonValue::String(target.to_string())],
}],
None
});
actions.push(CommandLink {
title: Some("Open Externally".to_string()),
command_or_links: vec![CommandOrLink::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(HoverContents::Array(results))
log::info!("telescope position: {position:?}");
let preview_content = provider.periscope_at(self.ctx, doc.clone(), position?)?;
self.preview.push(preview_content);
Some(())
}
}
fn push_result_ty(
@ -294,14 +295,6 @@ fn push_result_ty(
let _ = write!(type_doc, " = {short}");
}
fn render_actions(results: &mut Vec<MarkedString>, actions: Vec<CommandLink>) {
if actions.is_empty() {
return;
}
results.push(MarkedString::String(actions.into_iter().join(" | ")));
}
struct ExternalDocLink;
impl ExternalDocLink {
@ -390,17 +383,6 @@ impl fmt::Display for CommandOrLink {
}
}
fn to_lsp_tooltip(typst_tooltip: &Tooltip) -> HoverContents {
let lsp_marked_string = match typst_tooltip {
Tooltip::Text(text) => MarkedString::String(text.to_string()),
Tooltip::Code(code) => MarkedString::LanguageString(LanguageString {
language: "typc".to_owned(),
value: code.to_string(),
}),
};
HoverContents::Scalar(lsp_marked_string)
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -11,7 +11,7 @@ pub use lsp_types::{
ColorInformation, ColorPresentation, Diagnostic, DiagnosticRelatedInformation,
DiagnosticSeverity, DocumentHighlight, DocumentLink, DocumentSymbol, DocumentSymbolResponse,
Documentation, FoldingRange, GotoDefinitionResponse, Hover, HoverContents, InlayHint,
LanguageString, Location as LspLocation, LocationLink, MarkedString, MarkupContent, MarkupKind,
Location as LspLocation, LocationLink, MarkedString, MarkupContent, MarkupKind,
ParameterInformation, Position as LspPosition, PrepareRenameResponse, SelectionRange,
SemanticTokens, SemanticTokensDelta, SemanticTokensFullDeltaResult, SemanticTokensResult,
SignatureHelp, SignatureInformation, SymbolInformation, TextEdit, Url, WorkspaceEdit,

View file

@ -6,26 +6,20 @@ use typst::engine::Sink;
use typst::eval::CapturesVisitor;
use typst::foundations::{repr, Capturer, CastInfo, Value};
use typst::layout::Length;
use typst::model::Document;
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
use typst::World;
use typst_shim::syntax::LinkedNodeExt;
use typst_shim::utils::{round_2, Numeric};
use super::{plain_docs_sentence, summarize_font_family, truncated_repr};
use crate::analysis::{analyze_expr, analyze_labels, DynLabel};
use crate::analysis::analyze_expr;
/// Describe the item under the cursor.
///
/// Passing a `document` (from a previous compilation) is optional, but enhances
/// the autocompletions. Label completions, for instance, are only generated
/// when the document is available.
pub fn tooltip_(
world: &dyn World,
document: Option<&Document>,
source: &Source,
cursor: usize,
) -> Option<Tooltip> {
pub fn tooltip_(world: &dyn World, source: &Source, cursor: usize) -> Option<Tooltip> {
let leaf = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
if leaf.kind().is_trivia() {
return None;
@ -33,7 +27,8 @@ pub fn tooltip_(
named_param_tooltip(world, &leaf)
.or_else(|| font_tooltip(world, &leaf))
.or_else(|| document.and_then(|doc| label_tooltip(doc, &leaf)))
// todo: test that label_tooltip can be removed safely
// .or_else(|| document.and_then(|doc| label_tooltip(doc, &leaf)))
.or_else(|| expr_tooltip(world, &leaf))
.or_else(|| closure_tooltip(&leaf))
}
@ -79,6 +74,8 @@ pub fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
let mut last = None;
let mut pieces: Vec<EcoString> = vec![];
let mut unique_func: Option<Value> = None;
let mut unique = true;
let mut iter = values.iter();
for (value, _) in (&mut iter).take(Sink::MAX_VALUES - 1) {
if let Some((prev, count)) = &mut last {
@ -89,10 +86,32 @@ pub fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
}
}
if matches!(value, Value::Func(..) | Value::Type(..)) {
match &unique_func {
Some(unique_func) if unique => {
unique = unique_func == value;
}
Some(_) => {}
None => {
unique_func = Some(value.clone());
}
}
} else {
unique = false;
}
pieces.push(truncated_repr(value));
last = Some((value, 1));
}
// Don't report the only function reference...
// Note we usually expect the `definition` analyzer work in this case, otherwise
// please open an issue for this.
if unique_func.is_some() && unique {
return None;
}
if let Some((_, count)) = last {
if count > 1 {
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
@ -104,6 +123,7 @@ pub fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
}
let tooltip = repr::pretty_comma_list(&pieces, false);
// todo: check sensible length, value highlighting
(!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into()))
}
@ -155,29 +175,6 @@ fn length_tooltip(length: Length) -> Option<Tooltip> {
})
}
/// Tooltip for a hovered reference or label.
fn label_tooltip(document: &Document, leaf: &LinkedNode) -> Option<Tooltip> {
let target = match leaf.kind() {
SyntaxKind::RefMarker => leaf.text().trim_start_matches('@'),
SyntaxKind::Label => leaf.text().trim_start_matches('<').trim_end_matches('>'),
_ => return None,
};
for DynLabel {
label,
label_desc: _,
detail,
..
} in analyze_labels(document).0
{
if label.as_str() == target {
return Some(Tooltip::Text(detail?));
}
}
None
}
/// Tooltips for components of a named parameter.
fn named_param_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
let (func, named) = if_chain! {