tinymist_analysis/upstream/
tooltip.rs1use 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
16pub 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 .or_else(|| expr_tooltip(world, &leaf))
31 .or_else(|| closure_tooltip(&leaf))
32}
33
34#[derive(Debug, Clone)]
36pub enum Tooltip {
37 Text(EcoString),
39 Code(EcoString),
41}
42
43pub 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 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 (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into()))
120}
121
122fn closure_tooltip(leaf: &LinkedNode) -> Option<Tooltip> {
124 if !matches!(leaf.kind(), SyntaxKind::Eq | SyntaxKind::Arrow) {
127 return None;
128 }
129
130 let parent = leaf.parent()?;
132 if parent.kind() != SyntaxKind::Closure {
133 return None;
134 }
135
136 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
157fn 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
170fn font_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
172 if let Some(string) = leaf.cast::<ast::Str>()
174 &&let lower = string.get().to_lowercase()
175
176 && let Some(parent) = leaf.parent()
178 && let Some(named) = parent.cast::<ast::Named>()
179 && named.name().as_str() == "font"
180
181 && 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}