mirror of
https://github.com/Myriad-Dreamin/tinymist.git
synced 2025-07-24 13:13:43 +00:00
feat: hover tooltip on functions (#76)
* dev: introduce upstream tooltip * feat: basic function definition
This commit is contained in:
parent
b780a2a885
commit
14fc4819f1
10 changed files with 490 additions and 18 deletions
|
@ -42,6 +42,7 @@ typst-ts-core = { version = "0.4.2-rc8", default-features = false, features = [
|
|||
] }
|
||||
|
||||
lsp-types.workspace = true
|
||||
if_chain = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
once_cell.workspace = true
|
||||
|
|
|
@ -72,6 +72,7 @@ impl SyntaxRequest for GotoDefinitionRequest {
|
|||
}
|
||||
|
||||
pub(crate) struct DefinitionLink {
|
||||
pub kind: LexicalKind,
|
||||
pub value: Option<Value>,
|
||||
pub fid: TypstFileId,
|
||||
pub name: String,
|
||||
|
@ -97,6 +98,7 @@ pub(crate) fn find_definition(
|
|||
let e = parent.cast::<ast::ModuleImport>()?;
|
||||
let source = find_source_by_import(ctx.world, def_fid, e)?;
|
||||
return Some(DefinitionLink {
|
||||
kind: LexicalKind::Mod(LexicalModKind::PathVar),
|
||||
name: String::new(),
|
||||
value: None,
|
||||
fid: source.id(),
|
||||
|
@ -151,6 +153,7 @@ pub(crate) fn find_definition(
|
|||
let source = ctx.source_by_id(fid).ok()?;
|
||||
|
||||
return Some(DefinitionLink {
|
||||
kind: LexicalKind::Var(LexicalVarKind::Function),
|
||||
name: name.to_owned(),
|
||||
value: Some(Value::Func(f.clone())),
|
||||
fid,
|
||||
|
@ -178,6 +181,7 @@ pub(crate) fn find_definition(
|
|||
| LexicalModKind::Alias { .. }
|
||||
| LexicalModKind::Ident,
|
||||
) => Some(DefinitionLink {
|
||||
kind: def.kind.clone(),
|
||||
name: def.name.clone(),
|
||||
value: None,
|
||||
fid: def_fid,
|
||||
|
@ -190,18 +194,13 @@ pub(crate) fn find_definition(
|
|||
let def_name = root.leaf_at(def.range.start + 1)?;
|
||||
log::info!("def_name for function: {def_name:?}", def_name = def_name);
|
||||
let values = analyze_expr(ctx.world, &def_name);
|
||||
let Some(func) = values.into_iter().find_map(|v| match v.0 {
|
||||
Value::Func(f) => Some(f),
|
||||
_ => None,
|
||||
}) else {
|
||||
log::info!("no func found... {:?}", def.name);
|
||||
return None;
|
||||
};
|
||||
let func = values.into_iter().find(|v| matches!(v.0, Value::Func(..)));
|
||||
log::info!("okay for function: {func:?}");
|
||||
|
||||
Some(DefinitionLink {
|
||||
kind: def.kind.clone(),
|
||||
name: def.name.clone(),
|
||||
value: Some(Value::Func(func.clone())),
|
||||
value: func.map(|v| v.0),
|
||||
fid: def_fid,
|
||||
def_range: def.range.clone(),
|
||||
name_range: Some(def.range.clone()),
|
||||
|
|
|
@ -1,4 +1,14 @@
|
|||
use crate::{prelude::*, StatefulRequest};
|
||||
use core::fmt;
|
||||
|
||||
use ecow::eco_format;
|
||||
|
||||
use crate::{
|
||||
analyze_signature, find_definition,
|
||||
prelude::*,
|
||||
syntax::{get_deref_target, LexicalVarKind},
|
||||
upstream::{expr_tooltip, tooltip, Tooltip},
|
||||
StatefulRequest,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HoverRequest {
|
||||
|
@ -21,7 +31,8 @@ impl StatefulRequest for HoverRequest {
|
|||
// the typst's cursor is 1-based, so we need to add 1 to the offset
|
||||
let cursor = offset + 1;
|
||||
|
||||
let typst_tooltip = typst_ide::tooltip(ctx.world, doc, &source, cursor)?;
|
||||
let typst_tooltip = def_tooltip(ctx, &source, cursor)
|
||||
.or_else(|| tooltip(ctx.world, doc, &source, cursor))?;
|
||||
|
||||
let ast_node = LinkedNode::new(source.root()).leaf_at(cursor)?;
|
||||
let range = ctx.to_lsp_range(ast_node.range(), &source);
|
||||
|
@ -32,3 +43,131 @@ impl StatefulRequest for HoverRequest {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn def_tooltip(ctx: &mut AnalysisContext, source: &Source, cursor: usize) -> Option<Tooltip> {
|
||||
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
|
||||
|
||||
let deref_target = get_deref_target(leaf.clone())?;
|
||||
|
||||
let lnk = find_definition(ctx, source.clone(), deref_target.clone())?;
|
||||
|
||||
match lnk.kind {
|
||||
crate::syntax::LexicalKind::Mod(_)
|
||||
| crate::syntax::LexicalKind::Var(LexicalVarKind::Label)
|
||||
| crate::syntax::LexicalKind::Var(LexicalVarKind::LabelRef)
|
||||
| crate::syntax::LexicalKind::Var(LexicalVarKind::ValRef)
|
||||
| crate::syntax::LexicalKind::Block
|
||||
| crate::syntax::LexicalKind::Heading(..) => None,
|
||||
crate::syntax::LexicalKind::Var(LexicalVarKind::Function) => {
|
||||
let f = lnk.value.as_ref();
|
||||
Some(Tooltip::Text(eco_format!(
|
||||
r#"```typc
|
||||
let {}({});
|
||||
```{}"#,
|
||||
lnk.name,
|
||||
ParamTooltip(f),
|
||||
DocTooltip(f),
|
||||
)))
|
||||
}
|
||||
crate::syntax::LexicalKind::Var(LexicalVarKind::Variable) => {
|
||||
let deref_node = deref_target.node();
|
||||
let v = expr_tooltip(ctx.world, deref_node)
|
||||
.map(|t| match t {
|
||||
Tooltip::Text(s) => format!("\n\nValues: {}", s),
|
||||
Tooltip::Code(s) => format!("\n\nValues: ```{}```", s),
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(Tooltip::Text(eco_format!(
|
||||
r#"
|
||||
```typc
|
||||
let {};
|
||||
```{v}"#,
|
||||
lnk.name
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ParamTooltip<'a>(Option<&'a Value>);
|
||||
|
||||
impl<'a> fmt::Display for ParamTooltip<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let Some(Value::Func(func)) = self.0 else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let sig = analyze_signature(func.clone());
|
||||
|
||||
let mut is_first = true;
|
||||
let mut write_sep = |f: &mut fmt::Formatter<'_>| {
|
||||
if is_first {
|
||||
is_first = false;
|
||||
return Ok(());
|
||||
}
|
||||
f.write_str(", ")
|
||||
};
|
||||
for p in &sig.pos {
|
||||
write_sep(f)?;
|
||||
write!(f, "{}", p.name)?;
|
||||
}
|
||||
if let Some(rest) = &sig.rest {
|
||||
write_sep(f)?;
|
||||
write!(f, "{}", rest.name)?;
|
||||
}
|
||||
|
||||
if !sig.named.is_empty() {
|
||||
let mut name_prints = vec![];
|
||||
for v in sig.named.values() {
|
||||
name_prints.push((v.name.clone(), v.expr.clone()))
|
||||
}
|
||||
name_prints.sort();
|
||||
for (k, v) in name_prints {
|
||||
write_sep(f)?;
|
||||
let v = v.as_deref().unwrap_or("any");
|
||||
let mut v = v.trim();
|
||||
if v.starts_with('{') && v.ends_with('}') && v.len() > 30 {
|
||||
v = "{ ... }"
|
||||
}
|
||||
if v.starts_with('`') && v.ends_with('`') && v.len() > 30 {
|
||||
v = "raw"
|
||||
}
|
||||
if v.starts_with('[') && v.ends_with(']') && v.len() > 30 {
|
||||
v = "content"
|
||||
}
|
||||
write!(f, "{k}: {v}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct DocTooltip<'a>(Option<&'a Value>);
|
||||
|
||||
impl<'a> fmt::Display for DocTooltip<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let Some(Value::Func(func)) = self.0 else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
f.write_str("\n\n")?;
|
||||
|
||||
use typst::foundations::func::Repr;
|
||||
let mut func = func;
|
||||
let docs = 'search: loop {
|
||||
match func.inner() {
|
||||
Repr::Native(n) => break 'search n.docs,
|
||||
Repr::Element(e) => break 'search e.docs(),
|
||||
Repr::With(w) => {
|
||||
func = &w.0;
|
||||
}
|
||||
Repr::Closure(..) => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
f.write_str(docs)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use core::fmt;
|
||||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use ecow::eco_vec;
|
||||
use ecow::{eco_format, eco_vec};
|
||||
use log::debug;
|
||||
use lsp_types::{InlayHintKind, InlayHintLabel};
|
||||
use typst::{
|
||||
foundations::{Args, Closure},
|
||||
foundations::{Args, CastInfo, Closure},
|
||||
syntax::SyntaxNode,
|
||||
util::LazyHash,
|
||||
};
|
||||
|
@ -486,6 +487,8 @@ fn analyze_call_no_cache(func: Func, args: ast::Args<'_>) -> Option<CallInfo> {
|
|||
pub struct ParamSpec {
|
||||
/// The parameter's name.
|
||||
pub name: Cow<'static, str>,
|
||||
/// The parameter's default name.
|
||||
pub expr: Option<EcoString>,
|
||||
/// Creates an instance of the parameter's default value.
|
||||
pub default: Option<fn() -> Value>,
|
||||
/// Is the parameter positional?
|
||||
|
@ -503,6 +506,7 @@ impl ParamSpec {
|
|||
fn from_static(s: &ParamInfo) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
name: Cow::Borrowed(s.name),
|
||||
expr: Some(eco_format!("{}", TypeExpr(&s.input))),
|
||||
default: s.default,
|
||||
positional: s.positional,
|
||||
named: s.named,
|
||||
|
@ -512,16 +516,16 @@ impl ParamSpec {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Signature {
|
||||
pos: Vec<Arc<ParamSpec>>,
|
||||
named: HashMap<Cow<'static, str>, Arc<ParamSpec>>,
|
||||
pub struct Signature {
|
||||
pub pos: Vec<Arc<ParamSpec>>,
|
||||
pub named: HashMap<Cow<'static, str>, Arc<ParamSpec>>,
|
||||
has_fill_or_size_or_stroke: bool,
|
||||
rest: Option<Arc<ParamSpec>>,
|
||||
pub rest: Option<Arc<ParamSpec>>,
|
||||
_broken: bool,
|
||||
}
|
||||
|
||||
#[comemo::memoize]
|
||||
fn analyze_signature(func: Func) -> Arc<Signature> {
|
||||
pub(crate) fn analyze_signature(func: Func) -> Arc<Signature> {
|
||||
use typst::foundations::func::Repr;
|
||||
let params = match func.inner() {
|
||||
Repr::With(..) => unreachable!(),
|
||||
|
@ -595,6 +599,7 @@ fn analyze_closure_signature(c: Arc<LazyHash<Closure>>) -> Vec<Arc<ParamSpec>> {
|
|||
ast::Param::Pos(ast::Pattern::Placeholder(..)) => {
|
||||
params.push(Arc::new(ParamSpec {
|
||||
name: Cow::Borrowed("_"),
|
||||
expr: None,
|
||||
default: None,
|
||||
positional: true,
|
||||
named: false,
|
||||
|
@ -611,15 +616,18 @@ fn analyze_closure_signature(c: Arc<LazyHash<Closure>>) -> Vec<Arc<ParamSpec>> {
|
|||
|
||||
params.push(Arc::new(ParamSpec {
|
||||
name: Cow::Owned(name.to_owned()),
|
||||
expr: None,
|
||||
default: None,
|
||||
positional: true,
|
||||
named: false,
|
||||
variadic: false,
|
||||
}));
|
||||
}
|
||||
// todo: pattern
|
||||
ast::Param::Named(n) => {
|
||||
params.push(Arc::new(ParamSpec {
|
||||
name: Cow::Owned(n.name().as_str().to_owned()),
|
||||
expr: Some(unwrap_expr(n.expr()).to_untyped().clone().into_text()),
|
||||
default: None,
|
||||
positional: false,
|
||||
named: true,
|
||||
|
@ -630,6 +638,7 @@ fn analyze_closure_signature(c: Arc<LazyHash<Closure>>) -> Vec<Arc<ParamSpec>> {
|
|||
let ident = n.sink_ident().map(|e| e.as_str());
|
||||
params.push(Arc::new(ParamSpec {
|
||||
name: Cow::Owned(ident.unwrap_or_default().to_owned()),
|
||||
expr: None,
|
||||
default: None,
|
||||
positional: false,
|
||||
named: true,
|
||||
|
@ -654,6 +663,34 @@ fn is_one_line_(src: &Source, arg_node: &LinkedNode<'_>) -> Option<bool> {
|
|||
Some(ll == rl)
|
||||
}
|
||||
|
||||
fn unwrap_expr(mut e: ast::Expr) -> ast::Expr {
|
||||
while let ast::Expr::Parenthesized(p) = e {
|
||||
e = p.expr();
|
||||
}
|
||||
|
||||
e
|
||||
}
|
||||
|
||||
struct TypeExpr<'a>(&'a CastInfo);
|
||||
|
||||
impl<'a> fmt::Display for TypeExpr<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match self.0 {
|
||||
CastInfo::Any => "any",
|
||||
CastInfo::Value(.., v) => v,
|
||||
CastInfo::Type(v) => {
|
||||
f.write_str(v.short_name())?;
|
||||
return Ok(());
|
||||
}
|
||||
CastInfo::Union(v) => {
|
||||
let mut values = v.iter().map(|e| TypeExpr(e).to_string());
|
||||
f.write_str(&values.join(" | "))?;
|
||||
return Ok(());
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mod adt;
|
||||
pub mod analysis;
|
||||
pub mod syntax;
|
||||
mod upstream;
|
||||
|
||||
pub(crate) mod diagnostics;
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ pub type TypstSpan = typst::syntax::Span;
|
|||
pub type LspRange = lsp_types::Range;
|
||||
pub type TypstRange = std::ops::Range<usize>;
|
||||
|
||||
pub type TypstTooltip = typst_ide::Tooltip;
|
||||
pub type TypstTooltip = crate::upstream::Tooltip;
|
||||
pub type LspHoverContents = lsp_types::HoverContents;
|
||||
|
||||
pub type LspDiagnostic = lsp_types::Diagnostic;
|
||||
|
|
|
@ -381,6 +381,7 @@ impl LexicalHierarchyWorker {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// todo: for loop variable
|
||||
match node.kind() {
|
||||
SyntaxKind::LetBinding => 'let_binding: {
|
||||
let name = node.children().find(|n| n.cast::<ast::Pattern>().is_some());
|
||||
|
|
38
crates/tinymist-query/src/upstream/mod.rs
Normal file
38
crates/tinymist-query/src/upstream/mod.rs
Normal file
|
@ -0,0 +1,38 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use typst::text::{FontInfo, FontStyle};
|
||||
|
||||
mod tooltip;
|
||||
pub use tooltip::*;
|
||||
|
||||
/// Create a short description of a font family.
|
||||
fn summarize_font_family<'a>(variants: impl Iterator<Item = &'a FontInfo>) -> EcoString {
|
||||
let mut infos: Vec<_> = variants.collect();
|
||||
infos.sort_by_key(|info: &&FontInfo| info.variant);
|
||||
|
||||
let mut has_italic = false;
|
||||
let mut min_weight = u16::MAX;
|
||||
let mut max_weight = 0;
|
||||
for info in &infos {
|
||||
let weight = info.variant.weight.to_number();
|
||||
has_italic |= info.variant.style == FontStyle::Italic;
|
||||
min_weight = min_weight.min(weight);
|
||||
max_weight = min_weight.max(weight);
|
||||
}
|
||||
|
||||
let count = infos.len();
|
||||
let mut detail = eco_format!("{count} variant{}.", if count == 1 { "" } else { "s" });
|
||||
|
||||
if min_weight == max_weight {
|
||||
write!(detail, " Weight {min_weight}.").unwrap();
|
||||
} else {
|
||||
write!(detail, " Weights {min_weight}–{max_weight}.").unwrap();
|
||||
}
|
||||
|
||||
if has_italic {
|
||||
detail.push_str(" Has italics.");
|
||||
}
|
||||
|
||||
detail
|
||||
}
|
255
crates/tinymist-query/src/upstream/tooltip.rs
Normal file
255
crates/tinymist-query/src/upstream/tooltip.rs
Normal file
|
@ -0,0 +1,255 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
use if_chain::if_chain;
|
||||
use typst::eval::{CapturesVisitor, Tracer};
|
||||
use typst::foundations::{repr, Capturer, CastInfo, Repr, Value};
|
||||
use typst::layout::Length;
|
||||
use typst::model::Document;
|
||||
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
|
||||
use typst::util::{round_2, Numeric};
|
||||
use typst::World;
|
||||
|
||||
use super::summarize_font_family;
|
||||
use crate::analysis::{analyze_expr, analyze_labels};
|
||||
|
||||
/// 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> {
|
||||
let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?;
|
||||
if leaf.kind().is_trivia() {
|
||||
return None;
|
||||
}
|
||||
|
||||
named_param_tooltip(world, &leaf)
|
||||
.or_else(|| font_tooltip(world, &leaf))
|
||||
.or_else(|| document.and_then(|doc| label_tooltip(doc, &leaf)))
|
||||
.or_else(|| expr_tooltip(world, &leaf))
|
||||
.or_else(|| closure_tooltip(&leaf))
|
||||
}
|
||||
|
||||
/// A hover tooltip.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Tooltip {
|
||||
/// A string of text.
|
||||
Text(EcoString),
|
||||
/// A string of Typst code.
|
||||
Code(EcoString),
|
||||
}
|
||||
|
||||
/// Tooltip for a hovered expression.
|
||||
pub fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
let mut ancestor = leaf;
|
||||
while !ancestor.is::<ast::Expr>() {
|
||||
ancestor = ancestor.parent()?;
|
||||
}
|
||||
|
||||
let expr = ancestor.cast::<ast::Expr>()?;
|
||||
if !expr.hash() && !matches!(expr, ast::Expr::MathIdent(_)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let values = analyze_expr(world, ancestor);
|
||||
|
||||
if let [(value, _)] = values.as_slice() {
|
||||
if let Some(docs) = value.docs() {
|
||||
return Some(Tooltip::Text(docs.into()));
|
||||
}
|
||||
|
||||
if let &Value::Length(length) = value {
|
||||
if let Some(tooltip) = length_tooltip(length) {
|
||||
return Some(tooltip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if expr.is_literal() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut last = None;
|
||||
let mut pieces: Vec<EcoString> = vec![];
|
||||
let mut iter = values.iter();
|
||||
for (value, _) in (&mut iter).take(Tracer::MAX_VALUES - 1) {
|
||||
if let Some((prev, count)) = &mut last {
|
||||
if *prev == value {
|
||||
*count += 1;
|
||||
continue;
|
||||
} else if *count > 1 {
|
||||
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
|
||||
}
|
||||
}
|
||||
pieces.push(value.repr());
|
||||
last = Some((value, 1));
|
||||
}
|
||||
|
||||
if let Some((_, count)) = last {
|
||||
if count > 1 {
|
||||
write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
if iter.next().is_some() {
|
||||
pieces.push("...".into());
|
||||
}
|
||||
|
||||
let tooltip = repr::pretty_comma_list(&pieces, false);
|
||||
(!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into()))
|
||||
}
|
||||
|
||||
/// Tooltip for a hovered closure.
|
||||
fn closure_tooltip(leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
// Only show this tooltip when hovering over the equals sign or arrow of
|
||||
// the closure. Showing it across the whole subtree is too noisy.
|
||||
if !matches!(leaf.kind(), SyntaxKind::Eq | SyntaxKind::Arrow) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find the closure to analyze.
|
||||
let parent = leaf.parent()?;
|
||||
if parent.kind() != SyntaxKind::Closure {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Analyze the closure's captures.
|
||||
let mut visitor = CapturesVisitor::new(None, Capturer::Function);
|
||||
visitor.visit(parent);
|
||||
|
||||
let captures = visitor.finish();
|
||||
let mut names: Vec<_> = captures
|
||||
.iter()
|
||||
.map(|(name, _)| eco_format!("`{name}`"))
|
||||
.collect();
|
||||
if names.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
names.sort();
|
||||
|
||||
let tooltip = repr::separated_list(&names, "and");
|
||||
Some(Tooltip::Text(eco_format!(
|
||||
"This closure captures {tooltip}."
|
||||
)))
|
||||
}
|
||||
|
||||
/// Tooltip text for a hovered length.
|
||||
fn length_tooltip(length: Length) -> Option<Tooltip> {
|
||||
length.em.is_zero().then(|| {
|
||||
Tooltip::Code(eco_format!(
|
||||
"{}pt = {}mm = {}cm = {}in",
|
||||
round_2(length.abs.to_pt()),
|
||||
round_2(length.abs.to_mm()),
|
||||
round_2(length.abs.to_cm()),
|
||||
round_2(length.abs.to_inches())
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
/// 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 (label, 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! {
|
||||
// Ensure that we are in a named pair in the arguments to a function
|
||||
// call or set rule.
|
||||
if let Some(parent) = leaf.parent();
|
||||
if let Some(named) = parent.cast::<ast::Named>();
|
||||
if let Some(grand) = parent.parent();
|
||||
if matches!(grand.kind(), SyntaxKind::Args);
|
||||
if let Some(grand_grand) = grand.parent();
|
||||
if let Some(expr) = grand_grand.cast::<ast::Expr>();
|
||||
if let Some(ast::Expr::Ident(callee)) = match expr {
|
||||
ast::Expr::FuncCall(call) => Some(call.callee()),
|
||||
ast::Expr::Set(set) => Some(set.target()),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Find metadata about the function.
|
||||
if let Some(Value::Func(func)) = world.library().global.scope().get(&callee);
|
||||
then { (func, named) }
|
||||
else { return None; }
|
||||
};
|
||||
|
||||
// Hovering over the parameter name.
|
||||
if_chain! {
|
||||
if leaf.index() == 0;
|
||||
if let Some(ident) = leaf.cast::<ast::Ident>();
|
||||
if let Some(param) = func.param(&ident);
|
||||
then {
|
||||
return Some(Tooltip::Text(param.docs.into()));
|
||||
}
|
||||
}
|
||||
|
||||
// Hovering over a string parameter value.
|
||||
if_chain! {
|
||||
if let Some(string) = leaf.cast::<ast::Str>();
|
||||
if let Some(param) = func.param(&named.name());
|
||||
if let Some(docs) = find_string_doc(¶m.input, &string.get());
|
||||
then {
|
||||
return Some(Tooltip::Text(docs.into()));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Find documentation for a castable string.
|
||||
fn find_string_doc(info: &CastInfo, string: &str) -> Option<&'static str> {
|
||||
match info {
|
||||
CastInfo::Value(Value::Str(s), docs) if s.as_str() == string => Some(docs),
|
||||
CastInfo::Union(options) => options
|
||||
.iter()
|
||||
.find_map(|option| find_string_doc(option, string)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Tooltip for font.
|
||||
fn font_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
|
||||
if_chain! {
|
||||
// Ensure that we are on top of a string.
|
||||
if let Some(string) = leaf.cast::<ast::Str>();
|
||||
let lower = string.get().to_lowercase();
|
||||
|
||||
// Ensure that we are in the arguments to the text function.
|
||||
if let Some(parent) = leaf.parent();
|
||||
if let Some(named) = parent.cast::<ast::Named>();
|
||||
if named.name().as_str() == "font";
|
||||
|
||||
// Find the font family.
|
||||
if let Some((_, iter)) = world
|
||||
.book()
|
||||
.families()
|
||||
.find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str());
|
||||
|
||||
then {
|
||||
let detail = summarize_font_family(iter);
|
||||
return Some(Tooltip::Text(detail));
|
||||
}
|
||||
};
|
||||
|
||||
None
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue