diff --git a/crates/doc/src/proto_expr.rs b/crates/doc/src/proto_expr.rs index 2ec8e91f..f4404dc7 100644 --- a/crates/doc/src/proto_expr.rs +++ b/crates/doc/src/proto_expr.rs @@ -143,7 +143,7 @@ impl Expr { return "…".into(); } match self { - Expr::Value(VariableValue::Alias(id)) => vars.get(id).map_or_else(|| "(missing variable)".into(), |v| v.name.clone()), + Expr::Value(VariableValue::Alias(id)) => vars.get(id).map_or_else(|| "(missing variable)".into(), |v| var_ref(vars, v)), Expr::Value(VariableValue::String(s)) => quote(s), // Exact numbers and `#` colours, so the text reads back as the same expression. Expr::Value(VariableValue::Number(n)) => format!("{n}"), @@ -178,6 +178,18 @@ impl Expr { } } +/// A variable as written in an expression: its bare name when that reads back as this variable, +/// otherwise `{name}` (or `{id}` when even the braced name wouldn't), so names like `1`, `2x` or +/// `"q"` don't read back as literals or errors. +fn var_ref(vars: &Variables, v: &crate::Variable) -> String { + let this = Expr::var(v.id.clone()); + if Expr::parse(&v.name, vars).is_ok_and(|e| e == this) { + return v.name.clone(); + } + let braced = !v.name.contains('}') && v.name.trim() == v.name && vars.get(&v.name).is_some_and(|x| x.id == v.id); + if braced { format!("{{{}}}", v.name) } else { format!("{{{}}}", v.id) } +} + /// A collection or mode name as written inside `mode(…)` or `[…]`: as it is, or quoted when it has /// spaces at its ends, quotes or brackets. fn name_arg(name: &str) -> String { diff --git a/crates/doc/src/proto_expr_parse_tests.rs b/crates/doc/src/proto_expr_parse_tests.rs index 52f909fd..e106121d 100644 --- a/crates/doc/src/proto_expr_parse_tests.rs +++ b/crates/doc/src/proto_expr_parse_tests.rs @@ -176,3 +176,22 @@ fn hostile_text_is_bounded() { } } } + +#[test] +fn names_that_look_like_literals_read_back_as_variables() { + for name in ["count", "count total", "2x", "1", "-3", ".5", "(x)", "#ff", "\"q\"", "not", "true"] { + let mut v = vars(); + if let Some(var) = v.variables.iter_mut().find(|x| x.id == "n") { + var.name = name.into(); + } + for e in [Expr::var("n"), Expr::op(ExprOp::Add, vec![Expr::var("n"), n(1.0)]), Expr::InMode { variable: "n".into(), mode: "dark".into() }] { + let shown = e.display(&v); + assert_eq!(Expr::parse(&shown, &v), Ok(e.clone()), "{name:?} shown as {shown:?}"); + } + } + // Plain names stay bare, and literals still read as literals. + let v = vars(); + assert_eq!(Expr::var("n").display(&v), "n-name"); + assert_eq!(parse("1"), n(1.0)); + assert_eq!(parse("\"q\""), Expr::Value(VariableValue::String("q".into()))); +}