use std::fmt::Write; use ecow::{eco_format, EcoString}; use if_chain::if_chain; use super::analyze::analyze_labels; use super::{analyze_expr, plain_docs_sentence, summarize_font_family}; use crate::doc::Frame; use crate::eval::{CastInfo, Tracer, Value}; use crate::geom::{round_2, Length, Numeric}; use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::util::pretty_comma_list; use crate::World; /// Describe the item under the cursor. pub fn tooltip( world: &(dyn World + 'static), frames: &[Frame], source: &Source, cursor: usize, ) -> Option { let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; named_param_tooltip(world, &leaf) .or_else(|| font_tooltip(world, &leaf)) .or_else(|| ref_tooltip(world, frames, &leaf)) .or_else(|| expr_tooltip(world, &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. fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option { let mut ancestor = leaf; while !ancestor.is::() { ancestor = ancestor.parent()?; } let expr = ancestor.cast::()?; if !expr.hashtag() && !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(plain_docs_sentence(docs))); } 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 = vec![]; let mut iter = values.iter(); for value in (&mut iter).take(Tracer::MAX - 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().into()); 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 = pretty_comma_list(&pieces, false); (!tooltip.is_empty()).then(|| Tooltip::Code(tooltip.into())) } /// Tooltip text for a hovered length. fn length_tooltip(length: Length) -> Option { 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. fn ref_tooltip( world: &(dyn World + 'static), frames: &[Frame], leaf: &LinkedNode, ) -> Option { if leaf.kind() != SyntaxKind::RefMarker { return None; } let target = leaf.text().trim_start_matches('@'); for (label, detail) in analyze_labels(world, frames).0 { if label.0 == target { return Some(Tooltip::Text(detail?.into())); } } None } /// Tooltips for components of a named parameter. fn named_param_tooltip( world: &(dyn World + 'static), leaf: &LinkedNode, ) -> Option { let (info, 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::(); 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::(); 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); if let Some(info) = func.info(); then { (info, named) } else { return None; } }; // Hovering over the parameter name. if_chain! { if leaf.index() == 0; if let Some(ident) = leaf.cast::(); if let Some(param) = info.param(&ident); then { return Some(Tooltip::Text(plain_docs_sentence(param.docs))); } } // Hovering over a string parameter value. if_chain! { if let Some(string) = leaf.cast::(); if let Some(param) = info.param(&named.name()); if let Some(docs) = find_string_doc(¶m.cast, &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 { if_chain! { // Ensure that we are on top of a string. if let Some(string) = leaf.cast::(); 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::(); 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 }