From b6202b646a0d5ecced301d9bac8bfcaf977d7ee4 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Thu, 15 Dec 2022 22:51:55 +0100 Subject: Reflection for castables --- src/diag.rs | 34 ++- src/geom/mod.rs | 2 + src/geom/smart.rs | 64 +++++ src/geom/stroke.rs | 1 - src/ide/complete.rs | 689 ++++++++++++++++++++++++++++++++++++--------------- src/ide/highlight.rs | 8 +- src/ide/tooltip.rs | 159 +++++++++++- src/lib.rs | 2 + src/model/cast.rs | 513 +++++++++++++++++++------------------- src/model/content.rs | 8 +- src/model/dict.rs | 23 +- src/model/func.rs | 59 ++++- src/model/mod.rs | 2 +- src/model/ops.rs | 4 +- src/model/str.rs | 8 +- src/model/styles.rs | 3 +- src/model/value.rs | 8 +- src/syntax/ast.rs | 7 +- src/syntax/linked.rs | 92 ++++--- 19 files changed, 1150 insertions(+), 536 deletions(-) create mode 100644 src/geom/smart.rs (limited to 'src') diff --git a/src/diag.rs b/src/diag.rs index e244ba7c..55f16b5f 100644 --- a/src/diag.rs +++ b/src/diag.rs @@ -156,16 +156,6 @@ impl Trace for SourceResult { /// A result type with a string error message. pub type StrResult = Result; -/// Transform `expected X, found Y` into `expected X or A, found Y`. -pub fn with_alternative(msg: EcoString, alt: &str) -> EcoString { - let mut parts = msg.split(", found "); - if let (Some(a), Some(b)) = (parts.next(), parts.next()) { - format_eco!("{} or {}, found {}", a, alt, b) - } else { - msg - } -} - /// Convert a [`StrResult`] to a [`SourceResult`] by adding span information. pub trait At { /// Add the span information. @@ -181,6 +171,30 @@ where } } +/// Format the parts separated with commas and a final "and" or "or". +pub(crate) fn comma_list(buf: &mut String, parts: &[S], last: &str) +where + S: AsRef, +{ + for (i, part) in parts.iter().enumerate() { + match i { + 0 => {} + 1 if parts.len() == 2 => { + buf.push(' '); + buf.push_str(last); + buf.push(' '); + } + i if i + 1 == parts.len() => { + buf.push_str(", "); + buf.push_str(last); + buf.push(' '); + } + _ => buf.push_str(", "), + } + buf.push_str(part.as_ref()); + } +} + /// A result type with a file-related error. pub type FileResult = Result; diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 3c7c2fc9..6161774b 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -21,6 +21,7 @@ mod rounded; mod scalar; mod sides; mod size; +mod smart; mod stroke; mod transform; @@ -43,6 +44,7 @@ pub use self::rounded::*; pub use self::scalar::*; pub use self::sides::*; pub use self::size::*; +pub use self::smart::*; pub use self::stroke::*; pub use self::transform::*; diff --git a/src/geom/smart.rs b/src/geom/smart.rs new file mode 100644 index 00000000..d20bcdfe --- /dev/null +++ b/src/geom/smart.rs @@ -0,0 +1,64 @@ +use super::*; + +/// A value that can be automatically determined. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Smart { + /// The value should be determined smartly based on the circumstances. + Auto, + /// A specific value. + Custom(T), +} + +impl Smart { + /// Map the contained custom value with `f`. + pub fn map(self, f: F) -> Smart + where + F: FnOnce(T) -> U, + { + match self { + Self::Auto => Smart::Auto, + Self::Custom(x) => Smart::Custom(f(x)), + } + } + + /// Keeps `self` if it contains a custom value, otherwise returns `other`. + pub fn or(self, other: Smart) -> Self { + match self { + Self::Custom(x) => Self::Custom(x), + Self::Auto => other, + } + } + + /// Returns the contained custom value or a provided default value. + pub fn unwrap_or(self, default: T) -> T { + match self { + Self::Auto => default, + Self::Custom(x) => x, + } + } + + /// Returns the contained custom value or computes a default value. + pub fn unwrap_or_else(self, f: F) -> T + where + F: FnOnce() -> T, + { + match self { + Self::Auto => f(), + Self::Custom(x) => x, + } + } + + /// Returns the contained custom value or the default value. + pub fn unwrap_or_default(self) -> T + where + T: Default, + { + self.unwrap_or_else(T::default) + } +} + +impl Default for Smart { + fn default() -> Self { + Self::Auto + } +} diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index eae43c24..86191d33 100644 --- a/src/geom/stroke.rs +++ b/src/geom/stroke.rs @@ -1,5 +1,4 @@ use super::*; -use crate::model::Smart; /// A stroke of a geometric shape. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] diff --git a/src/ide/complete.rs b/src/ide/complete.rs index 7f312727..d4e72b3d 100644 --- a/src/ide/complete.rs +++ b/src/ide/complete.rs @@ -1,38 +1,12 @@ use if_chain::if_chain; -use crate::model::Value; -use crate::syntax::{LinkedNode, Source, SyntaxKind}; +use super::summarize_font_family; +use crate::model::{CastInfo, Scope, Value}; +use crate::syntax::ast::AstNode; +use crate::syntax::{ast, LinkedNode, Source, SyntaxKind}; use crate::util::{format_eco, EcoString}; use crate::World; -/// An autocompletion option. -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct Completion { - /// The kind of item this completes to. - pub kind: CompletionKind, - /// The label the completion is shown with. - pub label: EcoString, - /// The completed version of the input, defaults to the label. - /// - /// May use snippet syntax like `${lhs} + ${rhs}`. - pub apply: Option, - /// Details about the completed item. - pub detail: Option, -} - -/// A kind of item that can be completed. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] -pub enum CompletionKind { - /// A syntactical structure. - Syntax, - /// A function name. - Function, - /// A constant of the given type. - Constant, - /// A symbol. - Symbol, -} - /// Autocomplete a cursor position in a source file. /// /// Returns the position from which the completions apply and a list of @@ -49,6 +23,7 @@ pub fn autocomplete( let mut ctx = CompletionContext::new(world, source, cursor, explicit)?; let _ = complete_rules(&mut ctx) + || complete_params(&mut ctx) || complete_symbols(&mut ctx) || complete_markup(&mut ctx) || complete_math(&mut ctx) @@ -57,6 +32,39 @@ pub fn autocomplete( Some((ctx.from, ctx.completions)) } +/// An autocompletion option. +#[derive(Debug, Clone)] +pub struct Completion { + /// The kind of item this completes to. + pub kind: CompletionKind, + /// The label the completion is shown with. + pub label: EcoString, + /// The completed version of the input, possibly described with snippet + /// syntax like `${lhs} + ${rhs}`. + /// + /// Should default to the `label` if `None`. + pub apply: Option, + /// An optional short description, at most one sentence. + pub detail: Option, +} + +/// A kind of item that can be completed. +#[derive(Debug, Clone)] +pub enum CompletionKind { + /// A syntactical structure. + Syntax, + /// A function. + Func, + /// A function parameter. + Param, + /// A constant. + Constant, + /// A font family. + Font, + /// A symmie symbol. + Symbol(char), +} + /// Complete set and show rules. fn complete_rules(ctx: &mut CompletionContext) -> bool { // We don't want to complete directly behind the keyword. @@ -68,13 +76,15 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool { // Behind the set keyword: "set |". if matches!(prev.kind(), SyntaxKind::Set) { - ctx.set_rule_completions(ctx.cursor); + ctx.from = ctx.cursor; + ctx.set_rule_completions(); return true; } // Behind the show keyword: "show |". if matches!(prev.kind(), SyntaxKind::Show) { - ctx.show_rule_selector_completions(ctx.cursor); + ctx.from = ctx.cursor; + ctx.show_rule_selector_completions(); return true; } @@ -84,7 +94,84 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool { if matches!(prev.kind(), SyntaxKind::Colon); if matches!(prev.parent_kind(), Some(SyntaxKind::ShowRule)); then { - ctx.show_rule_recipe_completions(ctx.cursor); + ctx.from = ctx.cursor; + ctx.show_rule_recipe_completions(); + return true; + } + } + + false +} + +/// Complete call and set rule parameters. +fn complete_params(ctx: &mut CompletionContext) -> bool { + // Ensure that we are in a function call or set rule's argument list. + let (callee, args) = if_chain! { + if let Some(parent) = ctx.leaf.parent(); + if let Some(parent) = match parent.kind() { + SyntaxKind::Named => parent.parent(), + _ => Some(parent), + }; + if let Some(args) = parent.cast::(); + if let Some(grand) = parent.parent(); + if let Some(expr) = grand.cast::(); + if let Some(callee) = match expr { + ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(), + ast::Expr::Set(set) => Some(set.target()), + _ => None, + }; + then { + (callee, args) + } else { + return false; + } + }; + + // Parameter values: "func(param:|)", "func(param: |)". + if_chain! { + if let Some(prev) = ctx.leaf.prev_leaf(); + if let Some(before_colon) = match (prev.kind(), ctx.leaf.kind()) { + (_, SyntaxKind::Colon) => Some(prev), + (SyntaxKind::Colon, _) => prev.prev_leaf(), + _ => None, + }; + if let SyntaxKind::Ident(param) = before_colon.kind(); + then { + ctx.from = match ctx.leaf.kind() { + SyntaxKind::Colon | SyntaxKind::Space { .. } => ctx.cursor, + _ => ctx.leaf.offset(), + }; + ctx.param_value_completions(&callee, ¶m); + return true; + } + } + + // Parameters: "func(|)", "func(hi|)", "func(12,|)". + if_chain! { + if let Some(deciding) = if ctx.leaf.kind().is_trivia() { + ctx.leaf.prev_leaf() + } else { + Some(ctx.leaf.clone()) + }; + if matches!( + deciding.kind(), + SyntaxKind::LeftParen + | SyntaxKind::Comma + | SyntaxKind::Ident(_) + ); + then { + ctx.from = match deciding.kind() { + SyntaxKind::Ident(_) => deciding.offset(), + _ => ctx.cursor, + }; + + // Exclude arguments which are already present. + let exclude: Vec<_> = args.items().filter_map(|arg| match arg { + ast::Arg::Named(named) => Some(named.name()), + _ => None, + }).collect(); + + ctx.param_completions(&callee, &exclude); return true; } } @@ -98,7 +185,7 @@ fn complete_rules(ctx: &mut CompletionContext) -> bool { /// in `math_completions`. fn complete_symbols(ctx: &mut CompletionContext) -> bool { // Whether a colon is necessary. - let needs_colon = !ctx.text[ctx.cursor..].starts_with(':'); + let needs_colon = !ctx.after.starts_with(':'); // Behind half-completed symbol: "$arrow:|$". if_chain! { @@ -106,26 +193,28 @@ fn complete_symbols(ctx: &mut CompletionContext) -> bool { if let Some(prev) = ctx.leaf.prev_leaf(); if matches!(prev.kind(), SyntaxKind::Ident(_)); then { - ctx.symbol_completions(prev.offset(), false); + ctx.from = prev.offset(); + ctx.symbol_completions(false); return true; } } // Start of a symbol: ":|". // Checking for a text node ensures that "\:" isn't completed. - if ctx.text[..ctx.cursor].ends_with(':') + if ctx.before.ends_with(':') && matches!(ctx.leaf.kind(), SyntaxKind::Text(_) | SyntaxKind::Atom(_)) { - ctx.symbol_completions(ctx.cursor, needs_colon); + ctx.from = ctx.cursor; + ctx.symbol_completions(needs_colon); return true; } // An existing symbol: ":arrow:". if matches!(ctx.leaf.kind(), SyntaxKind::Symbol(_)) { // We want to complete behind the colon, therefore plus 1. - let has_colon = ctx.text[ctx.leaf.offset()..].starts_with(':'); - let from = ctx.leaf.offset() + (has_colon as usize); - ctx.symbol_completions(from, has_colon && needs_colon); + let has_colon = ctx.after.starts_with(':'); + ctx.from = ctx.leaf.offset() + (has_colon as usize); + ctx.symbol_completions(has_colon && needs_colon); return true; } @@ -142,8 +231,8 @@ fn complete_symbols(ctx: &mut CompletionContext) -> bool { ); then { // We want to complete behind the colon, therefore plus 1. - let from = prev.offset() + 1; - ctx.symbol_completions(from, needs_colon); + ctx.from = prev.offset() + 1; + ctx.symbol_completions(needs_colon); return true; } } @@ -160,18 +249,17 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool { // Start of an interpolated identifier: "#|". // Checking for a text node ensures that "\#" isn't completed. - if ctx.text[..ctx.cursor].ends_with('#') - && matches!(ctx.leaf.kind(), SyntaxKind::Text(_)) - { - ctx.expr_completions(ctx.cursor, true); + if ctx.before.ends_with('#') && matches!(ctx.leaf.kind(), SyntaxKind::Text(_)) { + ctx.from = ctx.cursor; + ctx.expr_completions(true); return true; } // An existing identifier: "#pa|". if matches!(ctx.leaf.kind(), SyntaxKind::Ident(_)) { // We want to complete behind the hashtag, therefore plus 1. - let from = ctx.leaf.offset() + 1; - ctx.expr_completions(from, true); + ctx.from = ctx.leaf.offset() + 1; + ctx.expr_completions(true); return true; } @@ -181,14 +269,16 @@ fn complete_markup(ctx: &mut CompletionContext) -> bool { if matches!(prev.kind(), SyntaxKind::Eq); if matches!(prev.parent_kind(), Some(SyntaxKind::LetBinding)); then { - ctx.expr_completions(ctx.cursor, false); + ctx.from = ctx.cursor; + ctx.expr_completions(false); return true; } } // Anywhere: "|". if ctx.explicit { - ctx.markup_completions(ctx.cursor); + ctx.from = ctx.cursor; + ctx.markup_completions(); return true; } @@ -206,21 +296,23 @@ fn complete_math(ctx: &mut CompletionContext) -> bool { // Start of an interpolated identifier: "#|". if matches!(ctx.leaf.kind(), SyntaxKind::Atom(s) if s == "#") { - ctx.expr_completions(ctx.cursor, true); + ctx.from = ctx.cursor; + ctx.expr_completions(true); return true; } // Behind existing atom or identifier: "$a|$" or "$abc|$". if matches!(ctx.leaf.kind(), SyntaxKind::Atom(_) | SyntaxKind::Ident(_)) { - let from = ctx.leaf.offset(); - ctx.symbol_completions(from, false); - ctx.scope_completions(from); + ctx.from = ctx.leaf.offset(); + ctx.symbol_completions(false); + ctx.scope_completions(); return true; } // Anywhere: "$|$". if ctx.explicit { - ctx.math_completions(ctx.cursor); + ctx.from = ctx.cursor; + ctx.math_completions(); return true; } @@ -238,8 +330,8 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { // An existing identifier: "{ pa| }". if matches!(ctx.leaf.kind(), SyntaxKind::Ident(_)) { - let from = ctx.leaf.offset(); - ctx.expr_completions(from, true); + ctx.from = ctx.leaf.offset(); + ctx.expr_completions(true); return true; } @@ -249,7 +341,8 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { && (ctx.leaf.kind().is_trivia() || matches!(ctx.leaf.kind(), SyntaxKind::LeftParen | SyntaxKind::LeftBrace)) { - ctx.expr_completions(ctx.cursor, false); + ctx.from = ctx.cursor; + ctx.expr_completions(false); return true; } @@ -259,7 +352,9 @@ fn complete_code(ctx: &mut CompletionContext) -> bool { /// Context for autocompletion. struct CompletionContext<'a> { world: &'a dyn World, - text: &'a str, + scope: &'a Scope, + before: &'a str, + after: &'a str, leaf: LinkedNode<'a>, cursor: usize, explicit: bool, @@ -275,10 +370,13 @@ impl<'a> CompletionContext<'a> { cursor: usize, explicit: bool, ) -> Option { + let text = source.text(); let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; Some(Self { world, - text: source.text(), + scope: &world.library().scope, + before: &text[..cursor], + after: &text[cursor..], leaf, cursor, explicit, @@ -287,224 +385,345 @@ impl<'a> CompletionContext<'a> { }) } - /// Add completions for all functions from the global scope. - fn set_rule_completions(&mut self, from: usize) { - self.scope_completions_where( - from, - |value| matches!(value, Value::Func(_)), - "(${})", - ); + /// Add a prefix and suffix to all applications. + fn enrich(&mut self, prefix: &str, suffix: &str) { + for Completion { label, apply, .. } in &mut self.completions { + let current = apply.as_ref().unwrap_or(label); + *apply = Some(format_eco!("{prefix}{current}{suffix}")); + } } - /// Add completions for selectors. - fn show_rule_selector_completions(&mut self, from: usize) { - self.snippet( - "text selector", - "\"${text}\": ${}", - "Replace occurances of specific text.", - ); + /// Add a snippet completion. + fn snippet_completion( + &mut self, + label: &'static str, + snippet: &'static str, + docs: &'static str, + ) { + self.completions.push(Completion { + kind: CompletionKind::Syntax, + label: label.into(), + apply: Some(snippet.into()), + detail: Some(docs.into()), + }); + } - self.snippet( - "regex selector", - "regex(\"${regex}\"): ${}", - "Replace matches of a regular expression.", - ); + /// Add completions for the global scope. + fn scope_completions(&mut self) { + self.scope_completions_where(|_| true); + } - self.scope_completions_where( - from, - |value| matches!(value, Value::Func(func) if func.select(None).is_ok()), - ": ${}", - ); + /// Add completions for a subset of the global scope. + fn scope_completions_where(&mut self, filter: impl Fn(&Value) -> bool) { + for (name, value) in self.scope.iter() { + if filter(value) { + self.value_completion(Some(name.clone()), value, None); + } + } } - /// Add completions for selectors. - fn show_rule_recipe_completions(&mut self, from: usize) { - self.snippet( - "replacement", - "[${content}]", - "Replace the selected element with content.", - ); + /// Add completions for the parameters of a function. + fn param_completions(&mut self, callee: &ast::Ident, exclude: &[ast::Ident]) { + let info = if_chain! { + if let Some(Value::Func(func)) = self.scope.get(callee); + if let Some(info) = func.info(); + then { info } + else { return; } + }; + + if callee.as_str() == "text" { + self.font_completions(); + } - self.snippet( - "replacement (string)", - "\"${text}\"", - "Replace the selected element with a string of text.", - ); + for param in &info.params { + if exclude.iter().any(|ident| ident.as_str() == param.name) { + continue; + } - self.snippet( - "transformation", - "element => [${content}]", - "Transform the element with a function.", - ); + self.completions.push(Completion { + kind: CompletionKind::Param, + label: param.name.into(), + apply: Some(format_eco!("{}: ${{}}", param.name)), + detail: Some(param.docs.into()), + }); + + if param.shorthand { + self.cast_completions(¶m.cast); + } + } - self.scope_completions_where(from, |value| matches!(value, Value::Func(_)), ""); + if self.before.ends_with(',') { + self.enrich(" ", ""); + } } - /// Add completions for the global scope. - fn scope_completions(&mut self, from: usize) { - self.scope_completions_where(from, |_| true, ""); + /// Add completions for the values of a function parameter. + fn param_value_completions(&mut self, callee: &ast::Ident, name: &str) { + let param = if_chain! { + if let Some(Value::Func(func)) = self.scope.get(callee); + if let Some(info) = func.info(); + if let Some(param) = info.param(name); + then { param } + else { return; } + }; + + self.cast_completions(¶m.cast); + + if self.before.ends_with(':') { + self.enrich(" ", ""); + } } - /// Add completions for a subset of the global scope. - fn scope_completions_where( + /// Add completions for a castable. + fn cast_completions(&mut self, cast: &CastInfo) { + match cast { + CastInfo::Any => {} + CastInfo::Value(value, docs) => { + self.value_completion(None, value, Some(docs)); + } + CastInfo::Type("none") => { + self.snippet_completion("none", "none", "Nonexistent.") + } + CastInfo::Type("auto") => { + self.snippet_completion("auto", "auto", "A smart default"); + } + CastInfo::Type("boolean") => { + self.snippet_completion("false", "false", "Yes / Enabled."); + self.snippet_completion("true", "true", "No / Disabled."); + } + CastInfo::Type("color") => { + self.snippet_completion( + "luma()", + "luma(${v})", + "A custom grayscale color.", + ); + self.snippet_completion( + "rgb()", + "rgb(${r}, ${g}, ${b}, ${a})", + "A custom RGBA color.", + ); + self.snippet_completion( + "cmyk()", + "cmyk(${c}, ${m}, ${y}, ${k})", + "A custom CMYK color.", + ); + self.scope_completions_where(|value| value.type_name() == "color"); + } + CastInfo::Type("function") => { + self.snippet_completion( + "function", + "(${params}) => ${output}", + "A custom function.", + ); + } + CastInfo::Type(ty) => { + self.completions.push(Completion { + kind: CompletionKind::Syntax, + label: (*ty).into(), + apply: Some(format_eco!("${{{ty}}}")), + detail: Some(format_eco!("A value of type {ty}.")), + }); + self.scope_completions_where(|value| value.type_name() == *ty); + } + CastInfo::Union(union) => { + for info in union { + self.cast_completions(info); + } + } + } + } + + /// Add a completion for a specific value. + fn value_completion( &mut self, - from: usize, - filter: fn(&Value) -> bool, - extra: &str, + label: Option, + value: &Value, + docs: Option<&'static str>, ) { - self.from = from; - for (name, value) in self.world.library().scope.iter() { - if filter(value) { - let apply = (!extra.is_empty()).then(|| format_eco!("{name}{extra}")); - self.completions.push(match value { - Value::Func(func) => Completion { - kind: CompletionKind::Function, - label: name.clone(), - apply, - detail: func.doc().map(Into::into), - }, - v => Completion { - kind: CompletionKind::Constant, - label: name.clone(), - apply, - detail: Some(format_eco!( - "Constant of type `{}`.", - v.type_name() - )), - }, - }); + let mut label = label.unwrap_or_else(|| value.repr().into()); + let mut apply = None; + + if matches!(value, Value::Func(_)) { + apply = Some(format_eco!("{label}(${{}})")); + label.push_str("()"); + } else { + if label.starts_with('"') { + let trimmed = label.trim_matches('"').into(); + apply = Some(label); + label = trimmed; } } + + let detail = docs.map(Into::into).or_else(|| match value { + Value::Func(func) => func.info().map(|info| info.docs.into()), + Value::Color(color) => Some(format_eco!("The color {color:?}.")), + Value::Auto => Some("A smart default.".into()), + _ => None, + }); + + self.completions.push(Completion { + kind: match value { + Value::Func(_) => CompletionKind::Func, + _ => CompletionKind::Constant, + }, + label, + apply, + detail, + }); + } + + /// Add completions for all font families. + fn font_completions(&mut self) { + for (family, iter) in self.world.book().families() { + let detail = summarize_font_family(iter); + self.completions.push(Completion { + kind: CompletionKind::Font, + label: family.into(), + apply: Some(format_eco!("\"{family}\"")), + detail: Some(detail.into()), + }) + } } /// Add completions for all symbols. - fn symbol_completions(&mut self, from: usize, colon: bool) { - self.from = from; + fn symbol_completions(&mut self, needs_colon: bool) { + self.symbol_completions_where(needs_colon, |_| true); + } + + /// Add completions for a subset of all symbols. + fn symbol_completions_where( + &mut self, + needs_colon: bool, + filter: impl Fn(char) -> bool, + ) { self.completions.reserve(symmie::list().len()); for &(name, c) in symmie::list() { - self.completions.push(Completion { - kind: CompletionKind::Symbol, - label: name.into(), - apply: colon.then(|| format_eco!("{name}:")), - detail: Some(c.into()), - }); + if filter(c) { + self.completions.push(Completion { + kind: CompletionKind::Symbol(c), + label: name.into(), + apply: None, + detail: None, + }); + } + } + if needs_colon { + self.enrich("", ":"); } } /// Add completions for markup snippets. #[rustfmt::skip] - fn markup_completions(&mut self, from: usize) { - self.from = from; - - self.snippet( + fn markup_completions(&mut self) { + self.snippet_completion( "linebreak", "\\\n${}", "Inserts a forced linebreak.", ); - self.snippet( + self.snippet_completion( "symbol", ":${}:", "Inserts a symbol.", ); - self.snippet( + self.snippet_completion( "strong text", "*${strong}*", "Strongly emphasizes content by increasing the font weight.", ); - self.snippet( + self.snippet_completion( "emphasized text", "_${emphasized}_", "Emphasizes content by setting it in italic font style.", ); - self.snippet( + self.snippet_completion( "raw text", "`${text}`", "Displays text verbatim, in monospace.", ); - self.snippet( + self.snippet_completion( "code listing", "```${lang}\n${code}\n```", "Inserts computer code with syntax highlighting.", ); - self.snippet( + self.snippet_completion( "hyperlink", "https://${example.com}", "Links to a URL.", ); - self.snippet( + self.snippet_completion( "math (inline)", "$${x}$", "Inserts an inline-level mathematical formula.", ); - self.snippet( + self.snippet_completion( "math (block)", "$ ${sum_x^2} $", "Inserts a block-level mathematical formula.", ); - self.snippet( + self.snippet_completion( "label", "<${name}>", "Makes the preceding element referencable.", ); - self.snippet( + self.snippet_completion( "reference", "@${name}", "Inserts a reference to a label.", ); - self.snippet( + self.snippet_completion( "heading", "= ${title}", "Inserts a section heading.", ); - self.snippet( + self.snippet_completion( "list item", "- ${item}", "Inserts an item of an unordered list.", ); - self.snippet( + self.snippet_completion( "enumeration item", "+ ${item}", "Inserts an item of an ordered list.", ); - self.snippet( + self.snippet_completion( "enumeration item (numbered)", "${number}. ${item}", "Inserts an explicitly numbered item of an ordered list.", ); - self.snippet( + self.snippet_completion( "description list item", "/ ${term}: ${description}", "Inserts an item of a description list.", ); - self.snippet( + self.snippet_completion( "expression", "#${}", "Variables, function calls, and more.", ); - self.snippet( + self.snippet_completion( "code block", "{ ${} }", "Switches into code mode.", ); - self.snippet( + self.snippet_completion( "content block", "[${content}]", "Inserts a nested content block that isolates styles.", @@ -513,23 +732,44 @@ impl<'a> CompletionContext<'a> { /// Add completions for math snippets. #[rustfmt::skip] - fn math_completions(&mut self, from: usize) { - self.symbol_completions(from, false); - self.scope_completions(from); + fn math_completions(&mut self) { + // Exclude non-technical symbols. + self.symbol_completions_where(false, |c| match c as u32 { + 9728..=9983 => false, + 9984..=10175 => false, + 127744..=128511 => false, + 128512..=128591 => false, + 128640..=128767 => false, + 129280..=129535 => false, + 129648..=129791 => false, + 127136..=127231 => false, + 127024..=127135 => false, + 126976..=127023 => false, + _ => true, + }); + + self.scope_completions_where(|value| { + matches!( + value, + Value::Func(func) if func.info().map_or(false, |info| { + info.tags.contains(&"math") + }), + ) + }); - self.snippet( + self.snippet_completion( "subscript", "${x}_${2:2}", "Sets something in subscript.", ); - self.snippet( + self.snippet_completion( "superscript", "${x}^${2:2}", "Sets something in superscript.", ); - self.snippet( + self.snippet_completion( "fraction", "${x}/${y}", "Inserts a fraction.", @@ -538,100 +778,107 @@ impl<'a> CompletionContext<'a> { /// Add completions for expression snippets. #[rustfmt::skip] - fn expr_completions(&mut self, from: usize, short_form: bool) { - self.scope_completions(from); + fn expr_completions(&mut self, short_form: bool) { + self.scope_completions_where(|value| { + !short_form || matches!( + value, + Value::Func(func) if func.info().map_or(true, |info| { + !info.tags.contains(&"math") + }), + ) + }); - self.snippet( + self.snippet_completion( "variable", "${variable}", "Accesses a variable.", ); - self.snippet( + self.snippet_completion( "function call", "${function}(${arguments})[${body}]", "Evaluates a function.", ); - self.snippet( + self.snippet_completion( "set rule", "set ${}", "Sets style properties on an element.", ); - self.snippet( + self.snippet_completion( "show rule", "show ${}", "Redefines the look of an element.", ); - self.snippet( + self.snippet_completion( "let binding", "let ${name} = ${value}", "Saves a value in a variable.", ); - self.snippet( + self.snippet_completion( "let binding (function)", "let ${name}(${params}) = ${output}", "Defines a function.", ); - self.snippet( + self.snippet_completion( "if conditional", "if ${1 < 2} {\n\t${}\n}", "Computes or inserts something conditionally.", ); - self.snippet( + self.snippet_completion( "if-else conditional", "if ${1 < 2} {\n\t${}\n} else {\n\t${}\n}", "Computes or inserts different things based on a condition.", ); - self.snippet( + self.snippet_completion( "while loop", "while ${1 < 2} {\n\t${}\n}", "Computes or inserts somthing while a condition is met.", ); - self.snippet( + self.snippet_completion( "for loop", "for ${value} in ${(1, 2, 3)} {\n\t${}\n}", "Computes or inserts somthing for each value in a collection.", ); - self.snippet( + self.snippet_completion( "for loop (with key)", "for ${key}, ${value} in ${(a: 1, b: 2)} {\n\t${}\n}", "Computes or inserts somthing for each key and value in a collection.", ); - self.snippet( + self.snippet_completion( "break", "break", "Exits early from a loop.", ); - self.snippet( + self.snippet_completion( "continue", "continue", "Continues with the next iteration of a loop.", ); - self.snippet( + self.snippet_completion( "return", "return ${output}", "Returns early from a function.", ); - self.snippet( + self.snippet_completion( "import", "import ${items} from \"${file.typ}\"", "Imports variables from another file.", ); - self.snippet( + self.snippet_completion( "include", "include \"${file.typ}\"", "Includes content from another file.", @@ -641,44 +888,90 @@ impl<'a> CompletionContext<'a> { return; } - self.snippet( + self.snippet_completion( "code block", "{ ${} }", "Inserts a nested code block.", ); - self.snippet( + self.snippet_completion( "content block", "[${content}]", "Switches into markup mode.", ); - self.snippet( + self.snippet_completion( "array", "(${1, 2, 3})", "Creates a sequence of values.", ); - self.snippet( + self.snippet_completion( "dictionary", "(${a: 1, b: 2})", "Creates a mapping from names to value.", ); - self.snippet( - "anonymous function", + self.snippet_completion( + "function", "(${params}) => ${output}", "Creates an unnamed function.", ); } - /// Add a snippet completion. - fn snippet(&mut self, label: &str, snippet: &str, detail: &str) { - self.completions.push(Completion { - kind: CompletionKind::Syntax, - label: label.into(), - apply: Some(snippet.into()), - detail: Some(detail.into()), + /// Add completions for all functions from the global scope. + fn set_rule_completions(&mut self) { + self.scope_completions_where(|value| { + matches!( + value, + Value::Func(func) if func.info().map_or(false, |info| { + info.params.iter().any(|param| param.settable) + }), + ) }); } + + /// Add completions for selectors. + fn show_rule_selector_completions(&mut self) { + self.scope_completions_where( + |value| matches!(value, Value::Func(func) if func.select(None).is_ok()), + ); + + self.enrich("", ": "); + + self.snippet_completion( + "text selector", + "\"${text}\": ${}", + "Replace occurances of specific text.", + ); + + self.snippet_completion( + "regex selector", + "regex(\"${regex}\"): ${}", + "Replace matches of a regular expression.", + ); + } + + /// Add completions for selectors. + fn show_rule_recipe_completions(&mut self) { + self.snippet_completion( + "replacement", + "[${content}]", + "Replace the selected element with content.", + ); + + self.snippet_completion( + "replacement (string)", + "\"${text}\"", + "Replace the selected element with a string of text.", + ); + + self.snippet_completion( + "transformation", + "element => [${content}]", + "Transform the element with a function.", + ); + + self.scope_completions_where(|value| matches!(value, Value::Func(_))); + } } diff --git a/src/ide/highlight.rs b/src/ide/highlight.rs index 75539a3b..ff9b8450 100644 --- a/src/ide/highlight.rs +++ b/src/ide/highlight.rs @@ -1,5 +1,3 @@ -//! Syntax highlighting for Typst source code. - use crate::syntax::{LinkedNode, SyntaxKind}; /// Syntax highlighting categories. @@ -162,7 +160,8 @@ pub fn highlight(node: &LinkedNode) -> Option { SyntaxKind::Markup { .. } if node.parent_kind() == Some(&SyntaxKind::DescItem) - && node.next_sibling_kind() == Some(&SyntaxKind::Colon) => + && node.next_sibling().as_ref().map(|v| v.kind()) + == Some(&SyntaxKind::Colon) => { Some(Category::ListTerm) } @@ -207,7 +206,8 @@ pub fn highlight(node: &LinkedNode) -> Option { } Some(SyntaxKind::SetRule) => Some(Category::Function), Some(SyntaxKind::ShowRule) - if node.prev_sibling_kind() == Some(&SyntaxKind::Show) => + if node.prev_sibling().as_ref().map(|v| v.kind()) + == Some(&SyntaxKind::Show) => { Some(Category::Function) } diff --git a/src/ide/tooltip.rs b/src/ide/tooltip.rs index df193cb2..7f6ca692 100644 --- a/src/ide/tooltip.rs +++ b/src/ide/tooltip.rs @@ -1,19 +1,162 @@ -use crate::model::Value; +use std::fmt::Write; + +use if_chain::if_chain; + +use crate::font::{FontInfo, FontStyle}; +use crate::model::{CastInfo, Value}; +use crate::syntax::ast::{self, AstNode}; use crate::syntax::{LinkedNode, Source, SyntaxKind}; use crate::World; -/// Produce a tooltip which can be shown when a cursor position is hovered. +/// Describe the item under the cursor. pub fn tooltip(world: &dyn World, source: &Source, cursor: usize) -> Option { let leaf = LinkedNode::new(source.root()).leaf_at(cursor)?; - // If a known identifier is under the cursor, provide its documentation. - if let SyntaxKind::Ident(ident) = leaf.kind() { - if let Some(value) = world.library().scope.get(ident) { - if let Value::Func(func) = value { - return func.doc().map(Into::into); - } + function_tooltip(world, &leaf) + .or_else(|| named_param_tooltip(world, &leaf)) + .or_else(|| font_family_tooltip(world, &leaf)) +} + +/// Tooltip for a function or set rule name. +fn function_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { + if_chain! { + if let SyntaxKind::Ident(ident) = leaf.kind(); + if matches!( + leaf.parent_kind(), + Some(SyntaxKind::FuncCall | SyntaxKind::SetRule), + ); + if let Some(Value::Func(func)) = world.library().scope.get(ident); + if let Some(info) = func.info(); + then { + return Some(info.docs.into()); } } None } + +/// Tooltips for components of a named parameter. +fn named_param_tooltip(world: &dyn World, 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(callee) = match expr { + ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(), + ast::Expr::Set(set) => Some(set.target()), + _ => None, + }; + + // Find metadata about the function. + if let Some(Value::Func(func)) = world.library().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 SyntaxKind::Ident(ident) = leaf.kind(); + if let Some(param) = info.param(ident); + then { + return Some(param.docs.into()); + } + } + + // Hovering over a string parameter value. + if_chain! { + if let SyntaxKind::Str(string) = leaf.kind(); + if let Some(param) = info.param(&named.name()); + if let Some(docs) = find_string_doc(¶m.cast, string); + then { + return Some(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 family. +fn font_family_tooltip(world: &dyn World, leaf: &LinkedNode) -> Option { + if_chain! { + // Ensure that we are on top of a string. + if let SyntaxKind::Str(string) = leaf.kind(); + let lower = string.to_lowercase(); + + // Ensure that we are in the arguments to the text function. + if let Some(parent) = leaf.parent(); + if matches!(parent.kind(), SyntaxKind::Args); + if let Some(grand) = parent.parent(); + if let Some(expr) = grand.cast::(); + if let Some(callee) = match expr { + ast::Expr::FuncCall(call) => call.callee().as_untyped().cast(), + ast::Expr::Set(set) => Some(set.target()), + _ => None, + }; + + // Find the font family. + if callee.as_str() == "text"; + 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(detail); + } + }; + + None +} + +/// Create a short description of a font family. +pub(super) fn summarize_font_family<'a>( + variants: impl Iterator, +) -> String { + let mut infos: Vec<_> = variants.collect(); + infos.sort_by_key(|info| info.variant); + + let mut has_italic = false; + let mut min_weight = u16::MAX; + let mut max_weight = 0; + for info in &infos { + let weight = info.variant.weight.to_number(); + has_italic |= info.variant.style == FontStyle::Italic; + min_weight = min_weight.min(weight); + max_weight = min_weight.max(weight); + } + + let count = infos.len(); + let s = if count == 1 { "" } else { "s" }; + let mut detail = format!("{count} variant{s}."); + + if min_weight == max_weight { + write!(detail, " Weight {min_weight}.").unwrap(); + } else { + write!(detail, " Weights {min_weight}–{max_weight}.").unwrap(); + } + + if has_italic { + detail.push_str(" Has italics."); + } + + detail +} diff --git a/src/lib.rs b/src/lib.rs index 631f1042..4045c02d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,8 @@ //! [PDF]: export::pdf //! [raster images]: export::render +#![recursion_limit = "1000"] + extern crate self as typst; #[macro_use] diff --git a/src/model/cast.rs b/src/model/cast.rs index bfde1bdd..833b9e9e 100644 --- a/src/model/cast.rs +++ b/src/model/cast.rs @@ -1,15 +1,19 @@ use std::num::NonZeroUsize; +use std::ops::Add; use std::str::FromStr; -use super::{Content, Regex, Selector, Transform, Value}; -use crate::diag::{with_alternative, StrResult}; +use super::{ + castable, Array, Content, Dict, Func, Label, Regex, Selector, Str, Transform, Value, +}; +use crate::diag::StrResult; use crate::doc::{Destination, Lang, Location, Region}; use crate::font::{FontStretch, FontStyle, FontWeight}; use crate::geom::{ - Axes, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Rel, Sides, + Axes, Color, Corners, Dir, GenAlign, Get, Length, Paint, PartialStroke, Point, Ratio, + Rel, Sides, Smart, }; use crate::syntax::Spanned; -use crate::util::{format_eco, EcoString}; +use crate::util::EcoString; /// Cast from a value to a specific type. pub trait Cast: Sized { @@ -18,95 +22,98 @@ pub trait Cast: Sized { /// Try to cast the value into an instance of `Self`. fn cast(value: V) -> StrResult; + + /// Describe the acceptable values. + fn describe() -> CastInfo; + + /// Produce an error for an inacceptable value. + fn error(value: Value) -> StrResult { + Err(Self::describe().error(&value)) + } } -/// Implement traits for dynamic types. -#[macro_export] -#[doc(hidden)] -macro_rules! __dynamic { - ($type:ty: $name:literal, $($tts:tt)*) => { - impl $crate::model::Type for $type { - const TYPE_NAME: &'static str = $name; +/// Describes a possible value for a cast. +#[derive(Debug, Clone)] +pub enum CastInfo { + /// Any value is okay. + Any, + /// A specific value, plus short documentation for that value. + Value(Value, &'static str), + /// Any value of a type. + Type(&'static str), + /// Multiple alternatives. + Union(Vec), +} + +impl CastInfo { + /// Produce an error message describing what was expected and what was + /// found. + pub fn error(&self, found: &Value) -> EcoString { + fn accumulate( + info: &CastInfo, + found: &Value, + parts: &mut Vec, + matching_type: &mut bool, + ) { + match info { + CastInfo::Any => parts.push("anything".into()), + CastInfo::Value(value, _) => { + parts.push(value.repr().into()); + if value.type_name() == found.type_name() { + *matching_type = true; + } + } + CastInfo::Type(ty) => parts.push((*ty).into()), + CastInfo::Union(options) => { + for option in options { + accumulate(option, found, parts, matching_type); + } + } + } } - castable! { - $type, - Expected: ::TYPE_NAME, - $($tts)* - @this: Self => this.clone(), + let mut matching_type = false; + let mut parts = vec![]; + accumulate(self, found, &mut parts, &mut matching_type); + + let mut msg = String::from("expected "); + if parts.is_empty() { + msg.push_str(" nothing"); } - impl From<$type> for $crate::model::Value { - fn from(v: $type) -> Self { - $crate::model::Value::Dyn($crate::model::Dynamic::new(v)) - } + crate::diag::comma_list(&mut msg, &parts, "or"); + + if !matching_type { + msg.push_str(", found "); + msg.push_str(found.type_name()); } - }; + + msg.into() + } } -#[doc(inline)] -pub use crate::__dynamic as dynamic; +impl Add for CastInfo { + type Output = Self; -/// Make a type castable from a value. -#[macro_export] -#[doc(hidden)] -macro_rules! __castable { - ($type:ty: $inner:ty) => { - impl $crate::model::Cast<$crate::model::Value> for $type { - fn is(value: &$crate::model::Value) -> bool { - <$inner>::is(value) + fn add(self, rhs: Self) -> Self { + Self::Union(match (self, rhs) { + (Self::Union(mut lhs), Self::Union(rhs)) => { + lhs.extend(rhs); + lhs } - - fn cast(value: $crate::model::Value) -> $crate::diag::StrResult { - <$inner>::cast(value).map(Self) + (Self::Union(mut lhs), rhs) => { + lhs.push(rhs); + lhs } - } - }; - - ( - $type:ty, - Expected: $expected:expr, - $($pattern:pat => $out:expr,)* - $(@$dyn_in:ident: $dyn_type:ty => $dyn_out:expr,)* - ) => { - #[allow(unreachable_patterns)] - impl $crate::model::Cast<$crate::model::Value> for $type { - fn is(value: &$crate::model::Value) -> bool { - #[allow(unused_variables)] - match value { - $($pattern => true,)* - $crate::model::Value::Dyn(dynamic) => { - false $(|| dynamic.is::<$dyn_type>())* - } - _ => false, - } - } - - fn cast(value: $crate::model::Value) -> $crate::diag::StrResult { - let found = match value { - $($pattern => return Ok($out),)* - $crate::model::Value::Dyn(dynamic) => { - $(if let Some($dyn_in) = dynamic.downcast::<$dyn_type>() { - return Ok($dyn_out); - })* - dynamic.type_name() - } - v => v.type_name(), - }; - - Err($crate::util::format_eco!( - "expected {}, found {}", - $expected, - found, - )) + (lhs, Self::Union(mut rhs)) => { + rhs.insert(0, lhs); + rhs } - } - }; + (lhs, rhs) => vec![lhs, rhs], + }) + } } -#[doc(inline)] -pub use crate::__castable as castable; - impl Cast for Value { fn is(_: &Value) -> bool { true @@ -115,6 +122,10 @@ impl Cast for Value { fn cast(value: Value) -> StrResult { Ok(value) } + + fn describe() -> CastInfo { + CastInfo::Any + } } impl Cast> for T { @@ -125,6 +136,10 @@ impl Cast> for T { fn cast(value: Spanned) -> StrResult { T::cast(value.v) } + + fn describe() -> CastInfo { + T::describe() + } } impl Cast> for Spanned { @@ -136,14 +151,64 @@ impl Cast> for Spanned { let span = value.span; T::cast(value.v).map(|t| Spanned::new(t, span)) } + + fn describe() -> CastInfo { + T::describe() + } +} + +castable! { + Dir: "direction", +} + +castable! { + GenAlign: "alignment", +} + +castable! { + Regex: "regular expression", +} + +castable! { + Selector: "selector", + text: EcoString => Self::text(&text), + label: Label => Self::Label(label), + func: Func => func.select(None)?, + regex: Regex => Self::Regex(regex), +} + +castable! { + Axes: "2d alignment", +} + +castable! { + PartialStroke: "stroke", + thickness: Length => Self { + paint: Smart::Auto, + thickness: Smart::Custom(thickness), + }, + color: Color => Self { + paint: Smart::Custom(color.into()), + thickness: Smart::Auto, + }, +} + +castable! { + u32, + int: i64 => int.try_into().map_err(|_| { + if int < 0 { + "number must be at least zero" + } else { + "number too large" + } + })?, } castable! { usize, - Expected: "non-negative integer", - Value::Int(int) => int.try_into().map_err(|_| { + int: i64 => int.try_into().map_err(|_| { if int < 0 { - "must be at least zero" + "number must be at least zero" } else { "number too large" } @@ -152,12 +217,11 @@ castable! { castable! { NonZeroUsize, - Expected: "positive integer", - Value::Int(int) => int + int: i64 => int .try_into() .and_then(|int: usize| int.try_into()) .map_err(|_| if int <= 0 { - "must be positive" + "number must be positive" } else { "number too large" })?, @@ -165,41 +229,23 @@ castable! { castable! { Paint, - Expected: "color", - Value::Color(color) => Paint::Solid(color), + color: Color => Self::Solid(color), } castable! { EcoString, - Expected: "string", - Value::Str(str) => str.into(), + string: Str => string.into(), } castable! { String, - Expected: "string", - Value::Str(string) => string.into(), -} - -dynamic! { - Regex: "regular expression", -} - -dynamic! { - Selector: "selector", - Value::Str(text) => Self::text(&text), - Value::Label(label) => Self::Label(label), - Value::Func(func) => func.select(None)?, - @regex: Regex => Self::Regex(regex.clone()), + string: Str => string.into(), } castable! { Transform, - Expected: "content or function", - Value::None => Self::Content(Content::empty()), - Value::Str(text) => Self::Content(item!(text)(text.into())), - Value::Content(content) => Self::Content(content), - Value::Func(func) => { + content: Content => Self::Content(content), + func: Func => { if func.argc().map_or(false, |count| count != 1) { Err("function must have exactly one parameter")? } @@ -207,45 +253,19 @@ castable! { }, } -dynamic! { - Dir: "direction", -} - -dynamic! { - GenAlign: "alignment", -} - -dynamic! { - Axes: "2d alignment", -} - castable! { Axes>, - Expected: "1d or 2d alignment", - @align: GenAlign => { + align: GenAlign => { let mut aligns = Axes::default(); - aligns.set(align.axis(), Some(*align)); + aligns.set(align.axis(), Some(align)); aligns }, - @aligns: Axes => aligns.map(Some), -} - -dynamic! { - PartialStroke: "stroke", - Value::Length(thickness) => Self { - paint: Smart::Auto, - thickness: Smart::Custom(thickness), - }, - Value::Color(color) => Self { - paint: Smart::Custom(color.into()), - thickness: Smart::Auto, - }, + aligns: Axes => aligns.map(Some), } castable! { Axes>, - Expected: "array of two relative lengths", - Value::Array(array) => { + array: Array => { let mut iter = array.into_iter(); match (iter.next(), iter.next(), iter.next()) { (Some(a), Some(b), None) => Axes::new(a.cast()?, b.cast()?), @@ -256,142 +276,124 @@ castable! { castable! { Location, - Expected: "dictionary with `page`, `x`, and `y` keys", - Value::Dict(dict) => { - let page = dict.get("page")?.clone().cast()?; - let x: Length = dict.get("x")?.clone().cast()?; - let y: Length = dict.get("y")?.clone().cast()?; + mut dict: Dict => { + let page = dict.take("page")?.cast()?; + let x: Length = dict.take("x")?.cast()?; + let y: Length = dict.take("y")?.cast()?; + dict.finish(&["page", "x", "y"])?; Self { page, pos: Point::new(x.abs, y.abs) } }, } castable! { Destination, - Expected: "string or dictionary with `page`, `x`, and `y` keys", - Value::Str(string) => Self::Url(string.into()), - v @ Value::Dict(_) => Self::Internal(v.cast()?), + loc: Location => Self::Internal(loc), + string: EcoString => Self::Url(string), } castable! { FontStyle, - Expected: "string", - Value::Str(string) => match string.as_str() { - "normal" => Self::Normal, - "italic" => Self::Italic, - "oblique" => Self::Oblique, - _ => Err(r#"expected "normal", "italic" or "oblique""#)?, - }, + /// The default style. + "normal" => Self::Normal, + /// A cursive style. + "italic" => Self::Italic, + /// A slanted style. + "oblique" => Self::Oblique, } castable! { FontWeight, - Expected: "integer or string", - Value::Int(v) => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), - Value::Str(string) => match string.as_str() { - "thin" => Self::THIN, - "extralight" => Self::EXTRALIGHT, - "light" => Self::LIGHT, - "regular" => Self::REGULAR, - "medium" => Self::MEDIUM, - "semibold" => Self::SEMIBOLD, - "bold" => Self::BOLD, - "extrabold" => Self::EXTRABOLD, - "black" => Self::BLACK, - _ => Err("unknown font weight")?, - }, + v: i64 => Self::from_number(v.clamp(0, u16::MAX as i64) as u16), + /// Thin weight (100). + "thin" => Self::THIN, + /// Extra light weight (200). + "extralight" => Self::EXTRALIGHT, + /// Light weight (300). + "light" => Self::LIGHT, + /// Regular weight (400). + "regular" => Self::REGULAR, + /// Medium weight (500). + "medium" => Self::MEDIUM, + /// Semibold weight (600). + "semibold" => Self::SEMIBOLD, + /// Bold weight (700). + "bold" => Self::BOLD, + /// Extrabold weight (800). + "extrabold" => Self::EXTRABOLD, + /// Black weight (900). + "black" => Self::BLACK, } castable! { FontStretch, - Expected: "ratio", - Value::Ratio(v) => Self::from_ratio(v.get() as f32), + v: Ratio => Self::from_ratio(v.get() as f32), } castable! { Lang, - Expected: "string", - Value::Str(string) => Self::from_str(&string)?, + string: EcoString => Self::from_str(&string)?, } castable! { Region, - Expected: "string", - Value::Str(string) => Self::from_str(&string)?, + string: EcoString => Self::from_str(&string)?, } -impl Cast for Option { +/// Castable from [`Value::None`]. +pub struct NoneValue; + +impl Cast for NoneValue { fn is(value: &Value) -> bool { - matches!(value, Value::None) || T::is(value) + matches!(value, Value::None) } fn cast(value: Value) -> StrResult { match value { - Value::None => Ok(None), - v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")), + Value::None => Ok(Self), + _ => ::error(value), } } + + fn describe() -> CastInfo { + CastInfo::Type("none") + } } -/// A value that can be automatically determined. -#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub enum Smart { - /// The value should be determined smartly based on the circumstances. - Auto, - /// A specific value. - Custom(T), -} - -impl Smart { - /// Map the contained custom value with `f`. - pub fn map(self, f: F) -> Smart - where - F: FnOnce(T) -> U, - { - match self { - Self::Auto => Smart::Auto, - Self::Custom(x) => Smart::Custom(f(x)), - } +impl Cast for Option { + fn is(value: &Value) -> bool { + matches!(value, Value::None) || T::is(value) } - /// Keeps `self` if it contains a custom value, otherwise returns `other`. - pub fn or(self, other: Smart) -> Self { - match self { - Self::Custom(x) => Self::Custom(x), - Self::Auto => other, + fn cast(value: Value) -> StrResult { + match value { + Value::None => Ok(None), + v if T::is(&v) => Ok(Some(T::cast(v)?)), + _ => ::error(value), } } - /// Returns the contained custom value or a provided default value. - pub fn unwrap_or(self, default: T) -> T { - match self { - Self::Auto => default, - Self::Custom(x) => x, - } + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("none") } +} - /// Returns the contained custom value or computes a default value. - pub fn unwrap_or_else(self, f: F) -> T - where - F: FnOnce() -> T, - { - match self { - Self::Auto => f(), - Self::Custom(x) => x, - } +/// Castable from [`Value::Auto`]. +pub struct AutoValue; + +impl Cast for AutoValue { + fn is(value: &Value) -> bool { + matches!(value, Value::Auto) } - /// Returns the contained custom value or the default value. - pub fn unwrap_or_default(self) -> T - where - T: Default, - { - self.unwrap_or_else(T::default) + fn cast(value: Value) -> StrResult { + match value { + Value::Auto => Ok(Self), + _ => ::error(value), + } } -} -impl Default for Smart { - fn default() -> Self { - Self::Auto + fn describe() -> CastInfo { + CastInfo::Type("auto") } } @@ -403,11 +405,14 @@ impl Cast for Smart { fn cast(value: Value) -> StrResult { match value { Value::Auto => Ok(Self::Auto), - v => T::cast(v) - .map(Self::Custom) - .map_err(|msg| with_alternative(msg, "auto")), + v if T::is(&v) => Ok(Self::Custom(T::cast(v)?)), + _ => ::error(value), } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("auto") + } } impl Cast for Sides @@ -420,7 +425,7 @@ where fn cast(mut value: Value) -> StrResult { if let Value::Dict(dict) = &mut value { - let mut take = |key| dict.take(key).map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); let rest = take("rest")?; let x = take("x")?.or(rest); @@ -432,22 +437,19 @@ where bottom: take("bottom")?.or(y), }; - if let Some((key, _)) = dict.iter().next() { - return Err(format_eco!("unexpected key {key:?}")); - } + dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; Ok(sides.map(Option::unwrap_or_default)) + } else if T::is(&value) { + Ok(Self::splat(T::cast(value)?)) } else { - T::cast(value).map(Self::splat).map_err(|msg| { - with_alternative( - msg, - "dictionary with any of \ - `left`, `top`, `right`, `bottom`, \ - `x`, `y`, or `rest` as keys", - ) - }) + ::error(value) } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } } impl Cast for Corners @@ -460,7 +462,7 @@ where fn cast(mut value: Value) -> StrResult { if let Value::Dict(dict) = &mut value { - let mut take = |key| dict.take(key).map(T::cast).transpose(); + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); let rest = take("rest")?; let left = take("left")?.or(rest); @@ -474,20 +476,27 @@ where bottom_left: take("bottom-left")?.or(bottom).or(left), }; - if let Some((key, _)) = dict.iter().next() { - return Err(format_eco!("unexpected key {key:?}")); - } + dict.finish(&[ + "top-left", + "top-right", + "bottom-right", + "bottom-left", + "left", + "top", + "right", + "bottom", + "rest", + ])?; Ok(corners.map(Option::unwrap_or_default)) + } else if T::is(&value) { + Ok(Self::splat(T::cast(value)?)) } else { - T::cast(value).map(Self::splat).map_err(|msg| { - with_alternative( - msg, - "dictionary with any of \ - `top-left`, `top-right`, `bottom-right`, `bottom-left`, \ - `left`, `top`, `right`, `bottom`, or `rest` as keys", - ) - }) + ::error(value) } } + + fn describe() -> CastInfo { + T::describe() + CastInfo::Type("dictionary") + } } diff --git a/src/model/content.rs b/src/model/content.rs index e73fa4a8..df910a58 100644 --- a/src/model/content.rs +++ b/src/model/content.rs @@ -11,7 +11,8 @@ use thin_vec::ThinVec; use typst_macros::node; use super::{ - capability, capable, Args, Guard, Key, Property, Recipe, Style, StyleMap, Value, Vm, + capability, capable, Args, Guard, Key, ParamInfo, Property, Recipe, Style, StyleMap, + Value, Vm, }; use crate::diag::{SourceResult, StrResult}; use crate::syntax::Span; @@ -426,6 +427,11 @@ pub trait Node: 'static + Capable { where Self: Sized; + /// List the settable properties. + fn properties() -> Vec + where + Self: Sized; + /// Access a field on this node. fn field(&self, name: &str) -> Option; } diff --git a/src/model/dict.rs b/src/model/dict.rs index d54a0e82..6e014d7e 100644 --- a/src/model/dict.rs +++ b/src/model/dict.rs @@ -62,6 +62,13 @@ impl Dict { Arc::make_mut(&mut self.0).entry(key).or_default() } + /// Remove the value if the dictionary contains the given key. + pub fn take(&mut self, key: &str) -> StrResult { + Arc::make_mut(&mut self.0) + .remove(key) + .ok_or_else(|| format_eco!("missing key: {:?}", Str::from(key))) + } + /// Whether the dictionary contains a specific key. pub fn contains(&self, key: &str) -> bool { self.0.contains_key(key) @@ -80,11 +87,6 @@ impl Dict { } } - /// Remove the value if the dictionary contains the given key. - pub fn take(&mut self, key: &str) -> Option { - Arc::make_mut(&mut self.0).remove(key) - } - /// Clear the dictionary. pub fn clear(&mut self) { if Arc::strong_count(&self.0) == 1 { @@ -118,6 +120,17 @@ impl Dict { pub fn iter(&self) -> std::collections::btree_map::Iter { self.0.iter() } + + /// Return an "unexpected key" error if there is any remaining pair. + pub fn finish(&self, expected: &[&str]) -> StrResult<()> { + if let Some((key, _)) = self.iter().next() { + let parts: Vec<_> = expected.iter().map(|s| format_eco!("\"{s}\"")).collect(); + let mut msg = format!("unexpected key {key:?}, valid keys are "); + crate::diag::comma_list(&mut msg, &parts, "and"); + return Err(msg.into()); + } + Ok(()) + } } /// The missing key access error message. diff --git a/src/model/func.rs b/src/model/func.rs index 0261b5e2..5b38b700 100644 --- a/src/model/func.rs +++ b/src/model/func.rs @@ -5,8 +5,8 @@ use std::sync::Arc; use comemo::{Track, Tracked}; use super::{ - Args, Dict, Eval, Flow, Node, NodeId, Route, Scope, Scopes, Selector, StyleMap, - Value, Vm, + Args, CastInfo, Dict, Eval, Flow, Node, NodeId, Route, Scope, Scopes, Selector, + StyleMap, Value, Vm, }; use crate::diag::{bail, SourceResult, StrResult}; use crate::syntax::ast::{self, AstNode, Expr}; @@ -39,13 +39,14 @@ impl Func { pub fn from_fn( name: &'static str, func: fn(&Vm, &mut Args) -> SourceResult, - doc: &'static str, + info: FuncInfo, ) -> Self { - Self(Arc::new(Repr::Native(Native { name, func, set: None, node: None, doc }))) + Self(Arc::new(Repr::Native(Native { name, func, set: None, node: None, info }))) } /// Create a new function from a native rust node. - pub fn from_node(name: &'static str, doc: &'static str) -> Self { + pub fn from_node(name: &'static str, mut info: FuncInfo) -> Self { + info.params.extend(T::properties()); Self(Arc::new(Repr::Native(Native { name, func: |ctx, args| { @@ -55,7 +56,7 @@ impl Func { }, set: Some(|args| T::set(args, false)), node: Some(NodeId::of::()), - doc, + info, }))) } @@ -73,11 +74,11 @@ impl Func { } } - /// Documentation for the function. - pub fn doc(&self) -> Option<&str> { + /// Extract details the function. + pub fn info(&self) -> Option<&FuncInfo> { match self.0.as_ref() { - Repr::Native(native) => Some(native.doc), - Repr::With(func, _) => func.doc(), + Repr::Native(native) => Some(&native.info), + Repr::With(func, _) => func.info(), _ => None, } } @@ -192,7 +193,7 @@ struct Native { /// The id of the node to customize with this function's show rule. node: Option, /// Documentation of the function. - doc: &'static str, + info: FuncInfo, } impl Hash for Native { @@ -201,10 +202,44 @@ impl Hash for Native { (self.func as usize).hash(state); self.set.map(|set| set as usize).hash(state); self.node.hash(state); - self.doc.hash(state); } } +/// Details about a function. +#[derive(Debug, Clone)] +pub struct FuncInfo { + /// The function's name. + pub name: &'static str, + /// Tags that categorize the function. + pub tags: &'static [&'static str], + /// Documentation for the function. + pub docs: &'static str, + /// Details about the function's parameters. + pub params: Vec, +} + +impl FuncInfo { + /// Get the parameter info for a parameter with the given name + pub fn param(&self, name: &str) -> Option<&ParamInfo> { + self.params.iter().find(|param| param.name == name) + } +} + +/// Describes a named parameter. +#[derive(Debug, Clone)] +pub struct ParamInfo { + /// The parameter's name. + pub name: &'static str, + /// Documentation for the parameter. + pub docs: &'static str, + /// Is the parameter settable with a set rule? + pub settable: bool, + /// Can the name be omitted? + pub shorthand: bool, + /// Valid values for the parameter. + pub cast: CastInfo, +} + /// A user-defined closure. #[derive(Hash)] pub(super) struct Closure { diff --git a/src/model/mod.rs b/src/model/mod.rs index 015df9b3..6ba8014c 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -26,7 +26,7 @@ mod typeset; #[doc(hidden)] pub use once_cell; -pub use typst_macros::{capability, capable, func, node}; +pub use typst_macros::{capability, capable, castable, func, node}; pub use self::args::*; pub use self::array::*; diff --git a/src/model/ops.rs b/src/model/ops.rs index 1a8dcb6b..9da9b0cc 100644 --- a/src/model/ops.rs +++ b/src/model/ops.rs @@ -1,8 +1,8 @@ //! Operations on values. -use super::{Regex, Smart, Value}; +use super::{Regex, Value}; use crate::diag::StrResult; -use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel}; +use crate::geom::{Axes, Axis, GenAlign, Length, Numeric, PartialStroke, Rel, Smart}; use crate::util::format_eco; use std::cmp::Ordering; use Value::*; diff --git a/src/model/str.rs b/src/model/str.rs index 0c288d9b..d1bf9d23 100644 --- a/src/model/str.rs +++ b/src/model/str.rs @@ -442,9 +442,8 @@ pub enum StrPattern { castable! { StrPattern, - Expected: "string or regular expression", - Value::Str(text) => Self::Str(text), - @regex: Regex => Self::Regex(regex.clone()), + text: Str => Self::Str(text), + regex: Regex => Self::Regex(regex), } /// A side of a string. @@ -459,8 +458,7 @@ pub enum StrSide { castable! { StrSide, - Expected: "start or end", - @align: GenAlign => match align { + align: GenAlign => match align { GenAlign::Start => Self::Start, GenAlign::End => Self::End, _ => Err("expected either `start` or `end`")?, diff --git a/src/model/styles.rs b/src/model/styles.rs index b2c328fa..1eaf5128 100644 --- a/src/model/styles.rs +++ b/src/model/styles.rs @@ -7,10 +7,11 @@ use std::sync::Arc; use comemo::{Prehashed, Tracked}; -use super::{Args, Content, Dict, Func, Label, NodeId, Regex, Smart, Value}; +use super::{Args, Content, Dict, Func, Label, NodeId, Regex, Value}; use crate::diag::{SourceResult, Trace, Tracepoint}; use crate::geom::{ Abs, Align, Axes, Corners, Em, GenAlign, Length, Numeric, PartialStroke, Rel, Sides, + Smart, }; use crate::syntax::Span; use crate::util::ReadableTypeId; diff --git a/src/model/value.rs b/src/model/value.rs index 98d11e15..1c687d8d 100644 --- a/src/model/value.rs +++ b/src/model/value.rs @@ -6,7 +6,9 @@ use std::sync::Arc; use siphasher::sip128::{Hasher128, SipHasher}; -use super::{format_str, ops, Args, Array, Cast, Content, Dict, Func, Label, Str}; +use super::{ + format_str, ops, Args, Array, Cast, CastInfo, Content, Dict, Func, Label, Str, +}; use crate::diag::StrResult; use crate::geom::{Abs, Angle, Color, Em, Fr, Length, Ratio, Rel, RgbaColor}; use crate::util::{format_eco, EcoString}; @@ -351,6 +353,10 @@ macro_rules! primitive { )), } } + + fn describe() -> CastInfo { + CastInfo::Type(Self::TYPE_NAME) + } } impl From<$type> for Value { diff --git a/src/syntax/ast.rs b/src/syntax/ast.rs index 77c788d3..56d4415e 100644 --- a/src/syntax/ast.rs +++ b/src/syntax/ast.rs @@ -1577,12 +1577,17 @@ impl Ident { _ => panic!("identifier is of wrong kind"), } } + + /// Get the identifier as a string slice. + pub fn as_str(&self) -> &str { + self.get() + } } impl Deref for Ident { type Target = str; fn deref(&self) -> &Self::Target { - self.get() + self.as_str() } } diff --git a/src/syntax/linked.rs b/src/syntax/linked.rs index 0d9d0c78..2826835e 100644 --- a/src/syntax/linked.rs +++ b/src/syntax/linked.rs @@ -29,6 +29,11 @@ impl<'a> LinkedNode<'a> { self.node } + /// The index of this node in its parent's children list. + pub fn index(&self) -> usize { + self.index + } + /// The absolute byte offset of the this node in the source file. pub fn offset(&self) -> usize { self.offset @@ -40,18 +45,13 @@ impl<'a> LinkedNode<'a> { } /// Get this node's children. - pub fn children( - &self, - ) -> impl DoubleEndedIterator> - + ExactSizeIterator> - + '_ { - let parent = Rc::new(self.clone()); - let mut offset = self.offset; - self.node.children().enumerate().map(move |(index, node)| { - let child = Self { node, parent: Some(parent.clone()), index, offset }; - offset += node.len(); - child - }) + pub fn children(&self) -> LinkedChildren<'a> { + LinkedChildren { + parent: Rc::new(self.clone()), + iter: self.node.children().enumerate(), + front: self.offset, + back: self.offset + self.len(), + } } } @@ -64,7 +64,7 @@ impl<'a> LinkedNode<'a> { /// Get the kind of this node's parent. pub fn parent_kind(&self) -> Option<&'a SyntaxKind> { - self.parent().map(|parent| parent.node.kind()) + Some(self.parent()?.node.kind()) } /// Get the first previous non-trivia sibling node. @@ -81,11 +81,6 @@ impl<'a> LinkedNode<'a> { } } - /// Get the kind of this node's first previous non-trivia sibling. - pub fn prev_sibling_kind(&self) -> Option<&'a SyntaxKind> { - self.prev_sibling().map(|parent| parent.node.kind()) - } - /// Get the next non-trivia sibling node. pub fn next_sibling(&self) -> Option { let parent = self.parent()?; @@ -99,11 +94,6 @@ impl<'a> LinkedNode<'a> { Some(next) } } - - /// Get the kind of this node's next non-trivia sibling. - pub fn next_sibling_kind(&self) -> Option<&'a SyntaxKind> { - self.next_sibling().map(|parent| parent.node.kind()) - } } /// Access to leafs. @@ -198,6 +188,51 @@ impl Debug for LinkedNode<'_> { } } +/// An iterator over the children of a linked node. +pub struct LinkedChildren<'a> { + parent: Rc>, + iter: std::iter::Enumerate>, + front: usize, + back: usize, +} + +impl<'a> Iterator for LinkedChildren<'a> { + type Item = LinkedNode<'a>; + + fn next(&mut self) -> Option { + self.iter.next().map(|(index, node)| { + let offset = self.front; + self.front += node.len(); + LinkedNode { + node, + parent: Some(self.parent.clone()), + index, + offset, + } + }) + } + + fn size_hint(&self) -> (usize, Option) { + self.iter.size_hint() + } +} + +impl DoubleEndedIterator for LinkedChildren<'_> { + fn next_back(&mut self) -> Option { + self.iter.next_back().map(|(index, node)| { + self.back -= node.len(); + LinkedNode { + node, + parent: Some(self.parent.clone()), + index, + offset: self.back, + } + }) + } +} + +impl ExactSizeIterator for LinkedChildren<'_> {} + #[cfg(test)] mod tests { use super::*; @@ -236,15 +271,4 @@ mod tests { assert_eq!(leaf.kind(), &SyntaxKind::Space { newlines: 0 }); assert_eq!(next.kind(), &SyntaxKind::Int(10)); } - - #[test] - fn test_linked_node_leaf_at() { - let source = Source::detached(""); - let leaf = LinkedNode::new(source.root()).leaf_at(0).unwrap(); - assert_eq!(leaf.kind(), &SyntaxKind::Markup { min_indent: 0 }); - - let source = Source::detached("Hello\n"); - let leaf = LinkedNode::new(source.root()).leaf_at(6).unwrap(); - assert_eq!(leaf.kind(), &SyntaxKind::Space { newlines: 1 }); - } } -- cgit v1.2.3