diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 6161774b9..225eb10d2 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -131,6 +131,6 @@ pub trait Numeric: } /// Round a float to two decimal places. -fn round_2(value: f64) -> f64 { +pub fn round_2(value: f64) -> f64 { (value * 100.0).round() / 100.0 } diff --git a/src/ide/analyze.rs b/src/ide/analyze.rs index 65d9ded87..12576e537 100644 --- a/src/ide/analyze.rs +++ b/src/ide/analyze.rs @@ -7,6 +7,22 @@ use crate::World; /// Try to determine a set of possible values for an expression. pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { match node.cast::() { + Some(ast::Expr::None(_)) => vec![Value::None], + Some(ast::Expr::Auto(_)) => vec![Value::Auto], + Some(ast::Expr::Bool(v)) => vec![Value::Bool(v.get())], + Some(ast::Expr::Int(v)) => vec![Value::Int(v.get())], + Some(ast::Expr::Float(v)) => vec![Value::Float(v.get())], + Some(ast::Expr::Numeric(v)) => vec![Value::numeric(v.get())], + Some(ast::Expr::Str(v)) => vec![Value::Str(v.get().into())], + + Some(ast::Expr::FieldAccess(access)) => { + let Some(child) = node.children().next() else { return vec![] }; + analyze(world, &child) + .into_iter() + .filter_map(|target| target.field(&access.field()).ok()) + .collect() + } + Some(ast::Expr::Ident(_) | ast::Expr::MathIdent(_) | ast::Expr::FuncCall(_)) => { if let Some(parent) = node.parent() { if parent.kind() == SyntaxKind::FieldAccess && node.index() > 0 { @@ -19,22 +35,9 @@ pub fn analyze(world: &(dyn World + 'static), node: &LinkedNode) -> Vec { let route = Route::default(); let mut tracer = Tracer::new(Some(span)); eval(world.track(), route.track(), tracer.track_mut(), source).ok(); - return tracer.finish(); + tracer.finish() } - Some(ast::Expr::Str(s)) => return vec![Value::Str(s.get().into())], - - Some(ast::Expr::FieldAccess(access)) => { - if let Some(child) = node.children().next() { - return analyze(world, &child) - .into_iter() - .filter_map(|target| target.field(&access.field()).ok()) - .collect(); - } - } - - _ => {} + _ => vec![], } - - vec![] } diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs index 202efd8e1..701c56225 100644 --- a/src/ide/tooltip.rs +++ b/src/ide/tooltip.rs @@ -2,6 +2,7 @@ use if_chain::if_chain; use unicode_segmentation::UnicodeSegmentation; use super::{analyze, plain_docs_sentence, summarize_font_family}; +use crate::geom::{round_2, Length, Numeric}; use crate::model::{CastInfo, Tracer, Value}; use crate::syntax::ast; use crate::syntax::{LinkedNode, Source, SyntaxKind}; @@ -22,15 +23,23 @@ pub fn tooltip( /// Tooltip for a hovered expression. fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option { - if !leaf.is::() { - return None; - } - + let expr = leaf.cast::()?; let values = analyze(world, leaf); + if let [value] = values.as_slice() { if let Some(docs) = value.docs() { return Some(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 tooltip = String::new(); @@ -61,6 +70,19 @@ fn expr_tooltip(world: &(dyn World + 'static), leaf: &LinkedNode) -> Option Option { + length.em.is_zero().then(|| { + 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()) + ) + }) +} + /// Tooltips for components of a named parameter. fn named_param_tooltip( world: &(dyn World + 'static), diff --git a/src/model/eval.rs b/src/model/eval.rs index d0751a1f6..96e7317df 100644 --- a/src/model/eval.rs +++ b/src/model/eval.rs @@ -15,7 +15,6 @@ use super::{ use crate::diag::{ bail, error, At, SourceError, SourceResult, StrResult, Trace, Tracepoint, }; -use crate::geom::{Abs, Angle, Em, Fr, Ratio}; use crate::syntax::ast::AstNode; use crate::syntax::{ast, Source, SourceId, Span, Spanned, SyntaxKind, SyntaxNode}; use crate::util::PathExt; @@ -660,14 +659,7 @@ impl Eval for ast::Numeric { type Output = Value; fn eval(&self, _: &mut Vm) -> SourceResult { - let (v, unit) = self.get(); - Ok(match unit { - ast::Unit::Length(unit) => Abs::with_unit(v, unit).into(), - ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into(), - ast::Unit::Em => Em::new(v).into(), - ast::Unit::Fr => Fr::new(v).into(), - ast::Unit::Percent => Ratio::new(v / 100.0).into(), - }) + Ok(Value::numeric(self.get())) } } diff --git a/src/model/value.rs b/src/model/value.rs index ea17349e1..b860a3f66 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -12,7 +12,7 @@ use super::{ }; use crate::diag::StrResult; use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor}; -use crate::syntax::Span; +use crate::syntax::{ast, Span}; use crate::util::{format_eco, EcoString}; /// A computational value. @@ -71,6 +71,18 @@ impl Value { Self::Dyn(Dynamic::new(any)) } + /// Create a numeric value from a number with a unit. + pub fn numeric(pair: (f64, ast::Unit)) -> Self { + let (v, unit) = pair; + match unit { + ast::Unit::Length(unit) => Abs::with_unit(v, unit).into(), + ast::Unit::Angle(unit) => Angle::with_unit(v, unit).into(), + ast::Unit::Em => Em::new(v).into(), + ast::Unit::Fr => Fr::new(v).into(), + ast::Unit::Percent => Ratio::new(v / 100.0).into(), + } + } + /// The name of the stored value's type. pub fn type_name(&self) -> &'static str { match self { diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 78d895fff..08def5338 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -345,6 +345,20 @@ impl Expr { _ => false, } } + + /// Is this a literal? + pub fn is_literal(&self) -> bool { + match self { + Self::None(_) => true, + Self::Auto(_) => true, + Self::Bool(_) => true, + Self::Int(_) => true, + Self::Float(_) => true, + Self::Numeric(_) => true, + Self::Str(_) => true, + _ => false, + } + } } impl Default for Expr { diff --git a/src/syntax/lexer.rs b/src/syntax/lexer.rs index cbddabb76..555ced849 100644 --- a/src/syntax/lexer.rs +++ b/src/syntax/lexer.rs @@ -499,7 +499,11 @@ impl Lexer<'_> { // Read the fractional part if not already done. // Make sure not to confuse a range for the decimal separator. - if c != '.' && !self.s.at("..") && self.s.eat_if('.') { + if c != '.' + && !self.s.at("..") + && !self.s.scout(1).map_or(false, is_id_start) + && self.s.eat_if('.') + { self.s.eat_while(char::is_ascii_digit); }