tinymist_analysis/upstream/
tooltip.rs

1use std::fmt::Write;
2
3use ecow::{EcoString, eco_format};
4use typst::World;
5use typst::engine::Sink;
6use typst::foundations::{Capturer, Value, repr};
7use typst::layout::Length;
8use typst::syntax::{LinkedNode, Source, SyntaxKind, ast};
9use typst_shim::eval::CapturesVisitor;
10use typst_shim::syntax::LinkedNodeExt;
11use typst_shim::utils::{Numeric, round_2};
12
13use super::{summarize_font_family, truncated_repr};
14use crate::analyze_expr;
15
16/// Describe the item under the cursor.
17///
18/// Passing a `document` (from a previous compilation) is optional, but enhances
19/// the autocompletions. Label completions, for instance, are only generated
20/// when the document is available.
21pub fn tooltip_(world: &dyn World, source: &Source, cursor: usize) -> Option<Tooltip> {
22    let leaf = LinkedNode::new(source.root()).leaf_at_compat(cursor)?;
23    if leaf.kind().is_trivia() {
24        return None;
25    }
26
27    font_tooltip(world, &leaf)
28        // todo: test that label_tooltip can be removed safely
29        // .or_else(|| document.and_then(|doc| label_tooltip(doc, &leaf)))
30        .or_else(|| expr_tooltip(world, &leaf))
31        .or_else(|| closure_tooltip(&leaf))
32}
33
34/// A hover tooltip.
35#[derive(Debug, Clone)]
36pub enum Tooltip {
37    /// A string of text.
38    Text(EcoString),
39    /// A string of Typst code.
40    Code(EcoString),
41}
42
43/// Tooltip for a hovered expression.
44pub fn expr_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
45    let mut ancestor = leaf;
46    while !ancestor.is::<ast::Expr>() {
47        ancestor = ancestor.parent()?;
48    }
49
50    let expr = ancestor.cast::<ast::Expr>()?;
51    if !expr.hash() && !matches!(expr, ast::Expr::MathIdent(_)) {
52        return None;
53    }
54
55    let values = analyze_expr(world, ancestor);
56
57    if let [(Value::Length(length), _)] = values.as_slice()
58        && let Some(tooltip) = length_tooltip(*length)
59    {
60        return Some(tooltip);
61    }
62
63    if expr.is_literal() {
64        return None;
65    }
66
67    let mut last = None;
68    let mut pieces: Vec<EcoString> = vec![];
69    let mut unique_func: Option<Value> = None;
70    let mut unique = true;
71    let mut iter = values.iter();
72    for (value, _) in (&mut iter).take(Sink::MAX_VALUES - 1) {
73        if let Some((prev, count)) = &mut last {
74            if *prev == value {
75                *count += 1;
76                continue;
77            } else if *count > 1 {
78                write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
79            }
80        }
81
82        if matches!(value, Value::Func(..) | Value::Type(..)) {
83            match &unique_func {
84                Some(unique_func) if unique => {
85                    unique = unique_func == value;
86                }
87                Some(_) => {}
88                None => {
89                    unique_func = Some(value.clone());
90                }
91            }
92        } else {
93            unique = false;
94        }
95
96        pieces.push(truncated_repr(value));
97        last = Some((value, 1));
98    }
99
100    // Don't report the only function reference...
101    // Note we usually expect the `definition` analyzer work in this case, otherwise
102    // please open an issue for this.
103    if unique_func.is_some() && unique {
104        return None;
105    }
106
107    if let Some((_, count)) = last
108        && count > 1
109    {
110        write!(pieces.last_mut().unwrap(), " (x{count})").unwrap();
111    }
112
113    if iter.next().is_some() {
114        pieces.push("...".into());
115    }
116
117    let tooltip = repr::pretty_comma_list(&pieces, false);
118    // todo: check sensible length, value highlighting
119    (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into()))
120}
121
122/// Tooltip for a hovered closure.
123fn closure_tooltip(leaf: &LinkedNode) -> Option<Tooltip> {
124    // Only show this tooltip when hovering over the equals sign or arrow of
125    // the closure. Showing it across the whole subtree is too noisy.
126    if !matches!(leaf.kind(), SyntaxKind::Eq | SyntaxKind::Arrow) {
127        return None;
128    }
129
130    // Find the closure to analyze.
131    let parent = leaf.parent()?;
132    if parent.kind() != SyntaxKind::Closure {
133        return None;
134    }
135
136    // Analyze the closure's captures.
137    let mut visitor = CapturesVisitor::new(None, Capturer::Function);
138    visitor.visit(parent);
139
140    let captures = visitor.finish();
141    let mut names: Vec<_> = captures
142        .iter()
143        .map(|(name, _)| eco_format!("`{name}`"))
144        .collect();
145    if names.is_empty() {
146        return None;
147    }
148
149    names.sort();
150
151    let tooltip = repr::separated_list(&names, "and");
152    Some(Tooltip::Text(eco_format!(
153        "This closure captures {tooltip}."
154    )))
155}
156
157/// Tooltip text for a hovered length.
158fn length_tooltip(length: Length) -> Option<Tooltip> {
159    length.em.is_zero().then(|| {
160        Tooltip::Code(eco_format!(
161            "{}pt = {}mm = {}cm = {}in",
162            round_2(length.abs.to_pt()),
163            round_2(length.abs.to_mm()),
164            round_2(length.abs.to_cm()),
165            round_2(length.abs.to_inches())
166        ))
167    })
168}
169
170/// Tooltip for font.
171fn font_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
172    // Ensure that we are on top of a string.
173    if let Some(string) = leaf.cast::<ast::Str>()
174        &&let lower = string.get().to_lowercase()
175
176        // Ensure that we are in the arguments to the text function.
177        && let Some(parent) = leaf.parent()
178        && let Some(named) = parent.cast::<ast::Named>()
179        && named.name().as_str() == "font"
180
181        // Find the font family.
182        && let Some((_, iter)) = world
183            .book()
184            .families()
185            .find(|&(family, _)| family.to_lowercase().as_str() == lower.as_str())
186    {
187        let detail = summarize_font_family(iter);
188        return Some(Tooltip::Text(detail));
189    }
190
191    None
192}