tinymist_analysis/upstream/
tooltip.rsuse std::fmt::Write;
use ecow::{eco_format, EcoString};
use if_chain::if_chain;
use typst::engine::Sink;
use typst::foundations::{repr, Capturer, Value};
use typst::layout::Length;
use typst::syntax::{ast, LinkedNode, Source, SyntaxKind};
use typst::World;
use typst_shim::eval::CapturesVisitor;
use typst_shim::syntax::LinkedNodeExt;
use typst_shim::utils::{round_2, Numeric};
use super::{summarize_font_family, truncated_repr};
use crate::analyze_expr;
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;
}
font_tooltip(world, &leaf)
.or_else(|| expr_tooltip(world, &leaf))
.or_else(|| closure_tooltip(&leaf))
}
#[derive(Debug, Clone)]
pub enum Tooltip {
Text(EcoString),
Code(EcoString),
}
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::Length(length), _)] = values.as_slice() {
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 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 {
if *prev == value {
*count += 1;
continue;
} else if *count > 1 {
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));
}
if unique_func.is_some() && unique {
return None;
}
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()))
}
fn closure_tooltip(leaf: &LinkedNode) -> Option<Tooltip> {
if !matches!(leaf.kind(), SyntaxKind::Eq | SyntaxKind::Arrow) {
return None;
}
let parent = leaf.parent()?;
if parent.kind() != SyntaxKind::Closure {
return None;
}
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}."
)))
}
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())
))
})
}
fn font_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option<Tooltip> {
if_chain! {
if let Some(string) = leaf.cast::<ast::Str>();
let lower = string.get().to_lowercase();
if let Some(parent) = leaf.parent();
if let Some(named) = parent.cast::<ast::Named>();
if named.name().as_str() == "font";
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
}