summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/diag.rs34
-rw-r--r--src/geom/mod.rs2
-rw-r--r--src/geom/smart.rs64
-rw-r--r--src/geom/stroke.rs1
-rw-r--r--src/ide/complete.rs689
-rw-r--r--src/ide/highlight.rs8
-rw-r--r--src/ide/tooltip.rs159
-rw-r--r--src/lib.rs2
-rw-r--r--src/model/cast.rs513
-rw-r--r--src/model/content.rs8
-rw-r--r--src/model/dict.rs23
-rw-r--r--src/model/func.rs59
-rw-r--r--src/model/mod.rs2
-rw-r--r--src/model/ops.rs4
-rw-r--r--src/model/str.rs8
-rw-r--r--src/model/styles.rs3
-rw-r--r--src/model/value.rs8
-rw-r--r--src/syntax/ast.rs7
-rw-r--r--src/syntax/linked.rs92
19 files changed, 1150 insertions, 536 deletions
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<T> Trace<T> for SourceResult<T> {
/// A result type with a string error message.
pub type StrResult<T> = Result<T, EcoString>;
-/// 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<T> {
/// 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<S>(buf: &mut String, parts: &[S], last: &str)
+where
+ S: AsRef<str>,
+{
+ 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<T> = Result<T, FileError>;
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<T> {
+ /// The value should be determined smartly based on the circumstances.
+ Auto,
+ /// A specific value.
+ Custom(T),
+}
+
+impl<T> Smart<T> {
+ /// Map the contained custom value with `f`.
+ pub fn map<F, U>(self, f: F) -> Smart<U>
+ 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<T>) -> 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<F>(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<T> Default for Smart<T> {
+ 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<EcoString>,
- /// Details about the completed item.
- pub detail: Option<EcoString>,
-}
-
-/// 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<EcoString>,
+ /// An optional short description, at most one sentence.
+ pub detail: Option<EcoString>,
+}
+
+/// 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::<ast::Args>();
+ if let Some(grand) = parent.parent();
+ if let Some(expr) = grand.cast::<ast::Expr>();
+ 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, &param);
+ 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<Self> {
+ 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(&param.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(&param.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<EcoString>,
+ 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<Category> {
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<Category> {
}
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<String> {
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<String> {
+ 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<String> {
+ 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::<ast::Named>();
+ 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::<ast::Expr>();
+ 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(&param.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<String> {
+ 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::<ast::Expr>();
+ 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<Item = &'a FontInfo>,
+) -> 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<V = Value>: Sized {
@@ -18,95 +22,98 @@ pub trait Cast<V = Value>: Sized {
/// Try to cast the value into an instance of `Self`.
fn cast(value: V) -> StrResult<Self>;
+
+ /// Describe the acceptable values.
+ fn describe() -> CastInfo;
+
+ /// Produce an error for an inacceptable value.
+ fn error(value: Value) -> StrResult<Self> {
+ 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<Self>),
+}
+
+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<EcoString>,
+ 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: <Self as $crate::model::Type>::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<Self> {
- <$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<Self> {
- 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<Self> {
Ok(value)
}
+
+ fn describe() -> CastInfo {
+ CastInfo::Any
+ }
}
impl<T: Cast> Cast<Spanned<Value>> for T {
@@ -125,6 +136,10 @@ impl<T: Cast> Cast<Spanned<Value>> for T {
fn cast(value: Spanned<Value>) -> StrResult<Self> {
T::cast(value.v)
}
+
+ fn describe() -> CastInfo {
+ T::describe()
+ }
}
impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
@@ -136,14 +151,64 @@ impl<T: Cast> Cast<Spanned<Value>> for Spanned<T> {
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<GenAlign>: "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<GenAlign>: "2d alignment",
-}
-
castable! {
Axes<Option<GenAlign>>,
- 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<GenAlign> => 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<GenAlign> => aligns.map(Some),
}
castable! {
Axes<Rel<Length>>,
- 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<T: Cast> Cast for Option<T> {
+/// 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<Self> {
match value {
- Value::None => Ok(None),
- v => T::cast(v).map(Some).map_err(|msg| with_alternative(msg, "none")),
+ Value::None => Ok(Self),
+ _ => <Self as Cast>::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<T> {
- /// The value should be determined smartly based on the circumstances.
- Auto,
- /// A specific value.
- Custom(T),
-}
-
-impl<T> Smart<T> {
- /// Map the contained custom value with `f`.
- pub fn map<F, U>(self, f: F) -> Smart<U>
- where
- F: FnOnce(T) -> U,
- {
- match self {
- Self::Auto => Smart::Auto,
- Self::Custom(x) => Smart::Custom(f(x)),
- }
+impl<T: Cast> Cast for Option<T> {
+ 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<T>) -> Self {
- match self {
- Self::Custom(x) => Self::Custom(x),
- Self::Auto => other,
+ fn cast(value: Value) -> StrResult<Self> {
+ match value {
+ Value::None => Ok(None),
+ v if T::is(&v) => Ok(Some(T::cast(v)?)),
+ _ => <Self as Cast>::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<F>(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<Self> {
+ match value {
+ Value::Auto => Ok(Self),
+ _ => <Self as Cast>::error(value),
+ }
}
-}
-impl<T> Default for Smart<T> {
- fn default() -> Self {
- Self::Auto
+ fn describe() -> CastInfo {
+ CastInfo::Type("auto")
}
}
@@ -403,11 +405,14 @@ impl<T: Cast> Cast for Smart<T> {
fn cast(value: Value) -> StrResult<Self> {
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)?)),
+ _ => <Self as Cast>::error(value),
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("auto")
+ }
}
impl<T> Cast for Sides<T>
@@ -420,7 +425,7 @@ where
fn cast(mut value: Value) -> StrResult<Self> {
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",
- )
- })
+ <Self as Cast>::error(value)
}
}
+
+ fn describe() -> CastInfo {
+ T::describe() + CastInfo::Type("dictionary")
+ }
}
impl<T> Cast for Corners<T>
@@ -460,7 +462,7 @@ where
fn cast(mut value: Value) -> StrResult<Self> {
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",
- )
- })
+ <Self as Cast>::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<ParamInfo>
+ where
+ Self: Sized;
+
/// Access a field on this node.
fn field(&self, name: &str) -> Option<Value>;
}
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<Value> {
+ 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<Value> {
- 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<Str, Value> {
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<Value>,
- 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<T: Node>(name: &'static str, doc: &'static str) -> Self {
+ pub fn from_node<T: 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::<T>()),
- 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<NodeId>,
/// 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<ParamInfo>,
+}
+
+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<Item = LinkedNode<'a>>
- + ExactSizeIterator<Item = LinkedNode<'a>>
- + '_ {
- 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<Self> {
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<LinkedNode<'a>>,
+ iter: std::iter::Enumerate<std::slice::Iter<'a, SyntaxNode>>,
+ front: usize,
+ back: usize,
+}
+
+impl<'a> Iterator for LinkedChildren<'a> {
+ type Item = LinkedNode<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ 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<usize>) {
+ self.iter.size_hint()
+ }
+}
+
+impl DoubleEndedIterator for LinkedChildren<'_> {
+ fn next_back(&mut self) -> Option<Self::Item> {
+ 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 });
- }
}