diff --git a/CHANGES.md b/CHANGES.md index 6ab53f46..73d91bf5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +* 0.2.11 + * FEATURE: ranges + * `padding` function + * `width` function * 0.2.10 * BUGFIX: `on_mount` is now called after the children are generated * BUGFIX: `on_tick` is now run before the cycle call diff --git a/Cargo.toml b/Cargo.toml index 02dd4062..280f0f7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "anathema" edition = "2024" -version = "0.2.10" +version = "0.2.11" license = "MIT" description = "Create beautiful, easily customisable terminal applications" keywords = ["tui", "terminal", "widgets", "ui", "layout"] @@ -40,7 +40,7 @@ workspace = true [workspace.package] edition = "2024" -version = "0.2.10" +version = "0.2.11" [workspace.dependencies] bitflags = "2.4.1" @@ -48,16 +48,16 @@ crossterm = "0.28.1" unicode-width = "0.1.11" flume = "0.11.0" notify = "6.1.1" -anathema-default-widgets = { path = "./anathema-default-widgets", version = "0.2.10" } -anathema-backend = { path = "./anathema-backend", version = "0.2.10" } -anathema-runtime = { path = "./anathema-runtime", version = "0.2.10" } -anathema-state = { path = "./anathema-state", version = "0.2.10" } -anathema-state-derive = { path = "./anathema-state-derive", version = "0.2.10" } -anathema-store = { path = "./anathema-store", version = "0.2.10" } -anathema-templates = { path = "./anathema-templates", version = "0.2.10" } -anathema-widgets = { path = "./anathema-widgets", version = "0.2.10" } -anathema-geometry = { path = "./anathema-geometry", version = "0.2.10" } -anathema-value-resolver = { path = "./anathema-value-resolver", version = "0.2.10" } +anathema-default-widgets = { path = "./anathema-default-widgets", version = "0.2.11" } +anathema-backend = { path = "./anathema-backend", version = "0.2.11" } +anathema-runtime = { path = "./anathema-runtime", version = "0.2.11" } +anathema-state = { path = "./anathema-state", version = "0.2.11" } +anathema-state-derive = { path = "./anathema-state-derive", version = "0.2.11" } +anathema-store = { path = "./anathema-store", version = "0.2.11" } +anathema-templates = { path = "./anathema-templates", version = "0.2.11" } +anathema-widgets = { path = "./anathema-widgets", version = "0.2.11" } +anathema-geometry = { path = "./anathema-geometry", version = "0.2.11" } +anathema-value-resolver = { path = "./anathema-value-resolver", version = "0.2.11" } [workspace] members = [ diff --git a/anathema-state/src/store/subscriber.rs b/anathema-state/src/store/subscriber.rs index fb6d420b..dc3a70fa 100644 --- a/anathema-state/src/store/subscriber.rs +++ b/anathema-state/src/store/subscriber.rs @@ -214,18 +214,21 @@ impl Subscribers { fn insert(&mut self, sub: Subscriber) { match self { Self::Empty => *self = Self::One(sub), - Self::One(key) => *self = Self::Arr([*key, sub, Subscriber::MAX], KeyIndex::TWO), + Self::One(key) if *key != sub => *self = Self::Arr([*key, sub, Subscriber::MAX], KeyIndex::TWO), Self::Arr(arr_keys, index) if *index == KeyIndex::MAX => { let mut keys = Vec::with_capacity(KeyIndex::max() + 1); keys.extend_from_slice(arr_keys); keys.push(sub); *self = Self::Heap(keys); } - Self::Arr(keys, index) => { + Self::Arr(keys, index) if !keys.contains(&sub) => { keys[index.0 as usize] = sub; index.add(); } - Self::Heap(keys) => keys.push(sub), + Self::Heap(keys) if !keys.contains(&sub) => keys.push(sub), + + // The sub is already registered + Self::Arr(..) | Self::One(_) | Self::Heap(_) => (), } } diff --git a/anathema-templates/src/expressions/eval.rs b/anathema-templates/src/expressions/eval.rs index 4fa129b1..88a63601 100644 --- a/anathema-templates/src/expressions/eval.rs +++ b/anathema-templates/src/expressions/eval.rs @@ -37,6 +37,10 @@ pub(super) fn eval(expr: Expr, strings: &Strings) -> Result { + let (lhs, rhs) = (eval(*lhs, strings)?.into(), eval(*rhs, strings)?.into()); + Expression::Range(lhs, rhs) + } Operator::Mul | Operator::Plus | Operator::Minus | Operator::Div | Operator::Mod => { let (lhs, rhs) = (eval(*lhs, strings)?.into(), eval(*rhs, strings)?.into()); let op = match op { diff --git a/anathema-templates/src/expressions/mod.rs b/anathema-templates/src/expressions/mod.rs index 980582a1..25d82faf 100644 --- a/anathema-templates/src/expressions/mod.rs +++ b/anathema-templates/src/expressions/mod.rs @@ -78,6 +78,9 @@ pub enum Expression { // Either Either(Box, Box), + // Range + Range(Box, Box), + // Function call Call { fun: Box, args: Vec }, } @@ -121,6 +124,7 @@ impl Display for Expression { write!(f, "{lhs} {op} {rhs}") } Self::Either(lhs, rhs) => write!(f, "{lhs} ? {rhs}"), + Self::Range(lhs, rhs) => write!(f, "{lhs} .. {rhs}"), Self::List(list) => { write!( f, @@ -192,6 +196,10 @@ pub fn either(lhs: Box, rhs: Box) -> Box { Expression::Either(lhs, rhs).into() } +pub fn range(lhs: Box, rhs: Box) -> Box { + Expression::Range(lhs, rhs).into() +} + // ----------------------------------------------------------------------------- // - Maths - // ----------------------------------------------------------------------------- diff --git a/anathema-templates/src/expressions/parser.rs b/anathema-templates/src/expressions/parser.rs index 53d7c909..238cab5d 100644 --- a/anathema-templates/src/expressions/parser.rs +++ b/anathema-templates/src/expressions/parser.rs @@ -10,13 +10,14 @@ use crate::token::{Kind, Operator, Tokens, Value}; pub(crate) mod prec { pub const INITIAL: u8 = 0; pub const CONDITIONAL: u8 = 2; - pub const EQUALITY: u8 = 3; - pub const LOGICAL: u8 = 4; - pub const SUM: u8 = 5; - pub const PRODUCT: u8 = 6; - pub const PREFIX: u8 = 8; - pub const CALL: u8 = 10; - pub const SUBCRIPT: u8 = 11; + pub const RANGE: u8 = 3; + pub const EQUALITY: u8 = 4; + pub const LOGICAL: u8 = 5; + pub const SUM: u8 = 6; + pub const PRODUCT: u8 = 7; + pub const PREFIX: u8 = 9; + pub const CALL: u8 = 11; + pub const SUBCRIPT: u8 = 12; } fn get_precedence(op: Operator) -> u8 { @@ -30,6 +31,7 @@ fn get_precedence(op: Operator) -> u8 { } Operator::EqualEqual | Operator::NotEqual => prec::EQUALITY, Operator::Or | Operator::And | Operator::Either => prec::CONDITIONAL, + Operator::DotDot => prec::RANGE, _ => prec::INITIAL, } @@ -384,4 +386,18 @@ mod test { let actual = parse(input); assert_eq!(actual, "(? (? ) )"); } + + #[test] + fn range() { + let input = "a..b"; + let actual = parse(input); + assert_eq!(actual, "(.. )"); + } + + #[test] + fn range_2() { + let input = "1 + 0..1 + 5"; + let actual = parse(input); + assert_eq!(actual, "(.. (+ 1 0) (+ 1 5))"); + } } diff --git a/anathema-templates/src/lexer.rs b/anathema-templates/src/lexer.rs index ffe7d9ee..2544fbf5 100644 --- a/anathema-templates/src/lexer.rs +++ b/anathema-templates/src/lexer.rs @@ -6,6 +6,36 @@ use crate::error::{ParseError, ParseErrorKind, Result}; use crate::strings::Strings; use crate::token::{Kind, Operator, Token, Value}; +// TODO: +// const EOF: char = '\0'; +// * change the lexer to use this one instead. +// * split the spans and tokens +// * spans can point to tokens, know their lines and cols +// struct Cursor<'src> { +// chars: Peekable>, +// } + +// impl<'src> Cursor<'src> { +// fn new(chars: Peekable>) -> Self { +// Self { chars } +// } + +// fn next(&mut self) -> char { +// self.chars.next().unwrap_or(EOF) +// } + +// fn first(&self) -> char { +// let mut iter = self.chars.clone(); +// iter.next().unwrap_or(EOF) +// } + +// fn second(&self) -> char { +// let mut iter = self.chars.clone(); +// iter.next(); +// iter.next().unwrap_or(EOF) +// } +// } + impl<'src, 'consts> Iterator for Lexer<'src, 'consts> { type Item = Result; @@ -58,6 +88,10 @@ impl<'src, 'strings> Lexer<'src, 'strings> { let _ = self.chars.next(); Ok(Kind::Op(Operator::And).to_token(index)) } + ('.', Some('.')) => { + let _ = self.chars.next(); + Ok(Kind::Op(Operator::DotDot).to_token(index)) + } ('|', Some('|')) => { let _ = self.chars.next(); Ok(Kind::Op(Operator::Or).to_token(index)) @@ -179,14 +213,27 @@ impl<'src, 'strings> Lexer<'src, 'strings> { let mut end = index; let mut parse_float = &self.src[index..=index] == "."; - let _signed = &self.src[index..=index] == "-" || self.chars.peek().map(|(_, c)| *c == '-').unwrap_or(false); + loop { + if let Some((e, '0'..='9')) = self.chars.peek() { + end = *e; + self.chars.next(); + continue; + } - while let Some((e, c @ ('0'..='9' | '.'))) = self.chars.peek() { - if *c == '.' { - parse_float = true; + if let Some((_, '.')) = self.chars.peek() { + let mut chars = self.chars.clone(); + _ = chars.next(); + // If the next character is a dot, then the following character has to be a number + // or this is not a valid float. + if let Some((e, '0'..='9')) = chars.peek() { + parse_float = true; + end = *e; + self.chars.next(); + continue; + } } - end = *e; - self.chars.next(); + + break; } let input = &self.src[index..=end]; @@ -527,4 +574,10 @@ mod test { let decl = token_kind("?"); assert_eq!(decl, Kind::Op(Operator::Either)); } + + #[test] + fn double_dot() { + let decl = token_kind(".."); + assert_eq!(decl, Kind::Op(Operator::DotDot)); + } } diff --git a/anathema-templates/src/statements/const_eval.rs b/anathema-templates/src/statements/const_eval.rs index 2f12ebc4..b45b768c 100644 --- a/anathema-templates/src/statements/const_eval.rs +++ b/anathema-templates/src/statements/const_eval.rs @@ -58,6 +58,7 @@ pub(crate) fn const_eval(expr: impl Into, ctx: &Context<'_>) -> Opti E::Negative(expr) => E::Negative(ce!(*expr)), E::Equality(lhs, rhs, eq) => E::Equality(ce!(*lhs), ce!(*rhs), eq), E::LogicalOp(lhs, rhs, op) => E::LogicalOp(ce!(*lhs), ce!(*rhs), op), + E::Range(from, to) => E::Range(ce!(*from), ce!(*to)), E::Ident(_) | E::Index(..) => eval_path(expr, ctx)?, E::Variable(_) => unreachable!("const eval is not recursive so this can never happen"), @@ -100,7 +101,7 @@ pub(crate) fn const_eval(expr: impl Into, ctx: &Context<'_>) -> Opti #[cfg(test)] mod test { use super::*; - use crate::expressions::{add, div, either, ident, index, list, mul, num, strlit, sub}; + use crate::expressions::{add, div, either, ident, index, list, mul, num, range, strlit, sub}; use crate::statements::with_context; #[test] @@ -177,4 +178,14 @@ mod test { assert_eq!(output, *expr); }); } + + #[test] + fn const_range() { + with_context(|ctx| { + let expr = range(num(1), num(2)); + + let output = const_eval(expr, &ctx).unwrap(); + assert_eq!(output, *range(num(1), num(2))); + }); + } } diff --git a/anathema-templates/src/statements/parser.rs b/anathema-templates/src/statements/parser.rs index da2c965f..9255abee 100644 --- a/anathema-templates/src/statements/parser.rs +++ b/anathema-templates/src/statements/parser.rs @@ -640,7 +640,7 @@ impl Iterator for Parser<'_, '_, '_> { mod test { use super::*; use crate::error::ErrorKind; - use crate::expressions::{boolean, ident, map, num, strlit, text_segments}; + use crate::expressions::{boolean, ident, map, num, range, strlit, text_segments}; use crate::lexer::Lexer; use crate::statements::test::{ associated_fun, case, component, else_stmt, eof, for_loop, global, if_else, if_stmt, load_attrib, load_value, @@ -977,6 +977,13 @@ mod test { ); } + #[test] + fn parse_range() { + let src = "for i in x..y"; + let mut statements = parse_ok(src); + assert_eq!(statements.remove(0), for_loop(2, range(ident("x"), ident("y")))); + } + #[test] fn parse_local_declaration() { let src = "let x = 1"; diff --git a/anathema-templates/src/token.rs b/anathema-templates/src/token.rs index dfe29e75..8fff2790 100644 --- a/anathema-templates/src/token.rs +++ b/anathema-templates/src/token.rs @@ -32,6 +32,7 @@ pub enum Operator { And, Or, Dot, + DotDot, Comma, Colon, Semicolon, @@ -66,6 +67,7 @@ impl Display for Operator { Self::And => write!(f, "&&"), Self::Or => write!(f, "||"), Self::Dot => write!(f, "."), + Self::DotDot => write!(f, ".."), Self::Comma => write!(f, ","), Self::Colon => write!(f, ":"), Self::Semicolon => write!(f, ";"), diff --git a/anathema-testutils/Cargo.toml b/anathema-testutils/Cargo.toml index 30784387..7a1e6a8c 100644 --- a/anathema-testutils/Cargo.toml +++ b/anathema-testutils/Cargo.toml @@ -9,7 +9,7 @@ homepage = "https://github.com/togglebyte/anathema" repository = "https://github.com/togglebyte/anathema" [dependencies] -anathema = { path = "../", version = "0.2.10" } +anathema = { path = "../", version = "0.2.11" } [lints] workspace = true diff --git a/anathema-value-resolver/src/expression.rs b/anathema-value-resolver/src/expression.rs index 24d95230..77a0d342 100644 --- a/anathema-value-resolver/src/expression.rs +++ b/anathema-value-resolver/src/expression.rs @@ -56,6 +56,7 @@ pub enum ValueExpr<'bp> { List(Box<[Self]>), Map(HashMap<&'bp str, Self>), Index(Box, Box), + Range(Box, Box), Attributes(Key), Not(Box), @@ -236,6 +237,18 @@ pub(crate) fn resolve_value<'a, 'bp>( } resolve_value(second, ctx) } + ValueExpr::Range(from, to) => { + let from = match resolve_int(from, ctx) { + Some(i) => i, + None => return ValueKind::Null, + }; + + let to = match resolve_int(to, ctx) { + Some(i) => i, + None => return ValueKind::Null, + }; + ValueKind::Range(from, to) + } // ----------------------------------------------------------------------------- // - Maps, lists and maybe - @@ -437,6 +450,7 @@ fn resolve_int<'bp>(index: &ValueExpr<'bp>, ctx: &mut ValueResolutionContext<'_, | ValueKind::DynMap(_) | ValueKind::Attributes | ValueKind::List(_) + | ValueKind::Range(..) | ValueKind::DynList(_) => None, } } diff --git a/anathema-value-resolver/src/functions/mod.rs b/anathema-value-resolver/src/functions/mod.rs index 20c00b86..edb0c74a 100644 --- a/anathema-value-resolver/src/functions/mod.rs +++ b/anathema-value-resolver/src/functions/mod.rs @@ -59,6 +59,8 @@ impl FunctionTable { inner.insert("to_lower".into(), Function::from(string::to_lower)); inner.insert("truncate".into(), Function::from(string::truncate)); inner.insert("to_str".into(), Function::from(string::to_str)); + inner.insert("pad".into(), Function::from(string::pad)); + inner.insert("width".into(), Function::from(string::width)); inner.insert("to_int".into(), Function::from(number::to_int)); inner.insert("to_float".into(), Function::from(number::to_float)); inner.insert("round".into(), Function::from(number::round)); diff --git a/anathema-value-resolver/src/functions/number.rs b/anathema-value-resolver/src/functions/number.rs index 18b71e8f..8b30d88d 100644 --- a/anathema-value-resolver/src/functions/number.rs +++ b/anathema-value-resolver/src/functions/number.rs @@ -26,6 +26,7 @@ pub(super) fn to_int<'bp>(args: &[ValueKind<'bp>]) -> ValueKind<'bp> { | ValueKind::List(_) | ValueKind::DynList(_) | ValueKind::DynMap(_) + | ValueKind::Range(..) | ValueKind::Composite(_) => ValueKind::Null, } } diff --git a/anathema-value-resolver/src/functions/string.rs b/anathema-value-resolver/src/functions/string.rs index 8f8fb745..908b2e44 100644 --- a/anathema-value-resolver/src/functions/string.rs +++ b/anathema-value-resolver/src/functions/string.rs @@ -73,6 +73,45 @@ pub(super) fn truncate<'bp>(args: &[ValueKind<'bp>]) -> ValueKind<'bp> { ValueKind::Str(buffer.into()) } +pub(super) fn pad<'bp>(args: &[ValueKind<'bp>]) -> ValueKind<'bp> { + if args.len() != 2 { + return ValueKind::Null; + } + + let Some(width) = args[1].as_int() else { return ValueKind::Null }; + if width < 0 { + return ValueKind::Null; + } + let width = width as usize; + + let mut buffer = String::new(); + args[0].strings(|s| { + buffer.push_str(s); + true + }); + + while width > buffer.width() { + buffer.push(' '); + } + + ValueKind::Str(buffer.into()) +} + +pub(super) fn width<'bp>(args: &[ValueKind<'bp>]) -> ValueKind<'bp> { + if args.len() != 1 { + return ValueKind::Null; + } + + let mut width = 0; + + args[0].strings(|s| { + width += s.width(); + true + }); + + ValueKind::Int(width as i64) +} + #[cfg(test)] mod test { use super::*; @@ -140,4 +179,40 @@ mod test { let val = truncate(&[val, value(3)]); assert_eq!(val, value("🐇")); } + + #[test] + fn pad_string() { + let val = value("hi"); + let val = pad(&[val, value(3)]); + assert_eq!(val, value("hi ")); + + let val = value("bye"); + let val = pad(&[val, value(3)]); + assert_eq!(val, value("bye")); + + let val = value("hello"); + let val = pad(&[val, value(3)]); + assert_eq!(val, value("hello")); + } + + #[test] + fn pad_non_string() { + let val = value(1); + let val = pad(&[val, value(3)]); + assert_eq!(val, value("1 ")); + } + + #[test] + fn width_of_string() { + let val = value("one"); + let val = width(&[val]); + assert_eq!(val, value(3)); + } + + #[test] + fn width_of_int() { + let val = value(1); + let val = width(&[val]); + assert_eq!(val, value(1)); + } } diff --git a/anathema-value-resolver/src/immediate.rs b/anathema-value-resolver/src/immediate.rs index 5c249741..234975dd 100644 --- a/anathema-value-resolver/src/immediate.rs +++ b/anathema-value-resolver/src/immediate.rs @@ -71,6 +71,11 @@ impl<'a, 'frame, 'bp> Resolver<'a, 'frame, 'bp> { let index = self.resolve(index); ValueExpr::Index(source.into(), index.into()) } + Expression::Range(from, to) => { + let from = self.resolve(from); + let to = self.resolve(to); + ValueExpr::Range(from.into(), to.into()) + } Expression::Call { fun, args } => { match &**fun { // function(args) diff --git a/anathema-value-resolver/src/scope.rs b/anathema-value-resolver/src/scope.rs index dcb4faca..041bb79c 100644 --- a/anathema-value-resolver/src/scope.rs +++ b/anathema-value-resolver/src/scope.rs @@ -116,6 +116,11 @@ impl<'parent, 'bp> Scope<'parent, 'bp> { let value = list.lookup(index)?; Some(value.into()) } + &ValueKind::Range(from, to) => (from..to) + .skip(index) + .take(1) + .map(|num| ValueExpr::Int(Kind::Static(num as i64))) + .next(), _ => unreachable!("none of the other values can be a collection"), }, _ => unreachable!("the parent scope is always a collection"), diff --git a/anathema-value-resolver/src/value.rs b/anathema-value-resolver/src/value.rs index 63d86bed..a45d7742 100644 --- a/anathema-value-resolver/src/value.rs +++ b/anathema-value-resolver/src/value.rs @@ -43,6 +43,7 @@ impl<'bp> Collection<'bp> { let Some(list) = state.as_any_list() else { return 0 }; list.len() } + ValueKind::Range(from, to) => *to - *from, ValueKind::Int(_) | ValueKind::Float(_) | ValueKind::Bool(_) @@ -183,6 +184,7 @@ pub enum ValueKind<'bp> { DynList(PendingValue), DynMap(PendingValue), Composite(PendingValue), + Range(usize, usize), } impl ValueKind<'_> { @@ -258,6 +260,7 @@ impl ValueKind<'_> { ValueKind::Str(cow) => f(cow.as_ref()), ValueKind::List(vec) => vec.iter().take_while(|val| val.internal_strings(f)).count() == vec.len(), ValueKind::DynList(value) => dyn_string(*value, f), + ValueKind::Range(from, to) => f(&format!("{from}..{to}")), ValueKind::DynMap(_) => f(""), ValueKind::Map => f(""), ValueKind::Composite(_) => f(""), diff --git a/anathema-widgets/src/nodes/loops.rs b/anathema-widgets/src/nodes/loops.rs index f8c2829d..1790af17 100644 --- a/anathema-widgets/src/nodes/loops.rs +++ b/anathema-widgets/src/nodes/loops.rs @@ -82,7 +82,6 @@ impl<'bp> For<'bp> { // then truncate the tree self.collection.reload(ctx.attribute_storage); ctx.truncate_children(&mut tree); - // tree.truncate_children(); } } diff --git a/anathema-widgets/src/scope.rs b/anathema-widgets/src/scope.rs deleted file mode 100644 index d368df2e..00000000 --- a/anathema-widgets/src/scope.rs +++ /dev/null @@ -1,415 +0,0 @@ -use std::fmt::{self, Debug, Write}; - -use anathema_debug::DebugWriter; -use anathema_state::{Path, PendingValue, StateId, States}; -use anathema_templates::Expression; - -use crate::expressions::{Downgraded, EvalValue, NameThis}; -use crate::values::ValueId; -use crate::WidgetId; - -#[derive(Debug)] -pub struct ScopeLookup<'bp> { - path: Path<'bp>, - id: ValueId, -} - -impl<'bp> ScopeLookup<'bp> { - /// Get and subscribe to a value - pub(crate) fn new(path: impl Into>, value_id: ValueId) -> Self { - Self { - path: path.into(), - id: value_id, - } - } -} - -#[derive(Default)] -enum Entry<'bp> { - /// Scope(size of previous scope) - Scope(usize), - Downgraded(Path<'bp>, Downgraded<'bp>), - Pending(Path<'bp>, PendingValue), - Expressions(Path<'bp>, &'bp [Expression]), - Expression(Path<'bp>, &'bp Expression), - State(StateId), - ComponentAttributes(WidgetId), - /// This is marking the entry as free, and another entry can be written here. - /// This is not indicative of a missing value - #[default] - Empty, -} - -impl<'bp> Entry<'bp> { - fn get(&self, lookup: Path<'_>) -> Option<&Self> { - match self { - Self::Downgraded(path, _) if *path == lookup => Some(self), - Self::Pending(path, _) if *path == lookup => Some(self), - Self::Expression(path, _) if *path == lookup => Some(self), - Self::Expressions(path, _) if *path == lookup => Some(self), - Self::State(_) => Some(self), - _ => None, - } - } -} - -impl Debug for Entry<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Entry::Scope(scope) => f.debug_tuple("Scope").field(scope).finish(), - Entry::Pending(path, pending_value) => f.debug_tuple("Pending").field(path).field(pending_value).finish(), - Entry::Downgraded(path, value) => f.debug_tuple("Downgraded").field(path).field(value).finish(), - Entry::State(state) => f.debug_tuple("State").field(&state).finish(), - Entry::ComponentAttributes(component_id) => { - f.debug_tuple("ComponentAttributes").field(&component_id).finish() - } - Entry::Empty => f.debug_tuple("Empty").finish(), - Entry::Expressions(binding, expressions) => { - f.debug_tuple("Expressions").field(binding).field(expressions).finish() - } - Entry::Expression(binding, expression) => { - f.debug_tuple("Expression").field(binding).field(expression).finish() - } - } - } -} - -/// `Scope` should be created once and then re-used by the runtime -/// to avoid unnecessary allocations. -/// -/// The scope is recreated for the update path of the nodes. -#[derive(Debug, Default)] -pub struct Scope<'bp> { - storage: Vec>, - current_scope_size: usize, - storage_index: usize, - level: usize, -} - -impl<'bp> Scope<'bp> { - pub fn new() -> Self { - Self { - storage: vec![], - current_scope_size: 0, - storage_index: 0, - level: 0, - } - } - - pub fn len(&self) -> usize { - self.storage_index - } - - pub fn with_capacity(cap: usize) -> Self { - let mut storage = Vec::with_capacity(cap); - storage.fill_with(Default::default); - Self { - storage, - current_scope_size: 0, - storage_index: 0, - level: 0, - } - } - - /// Clear the storage by writing `Entry::Empty` over every - /// existing entry, and reset the storage index - pub fn clear(&mut self) { - self.storage[..self.storage_index].fill_with(Default::default); - self.storage_index = 0; - } - - fn insert_entry(&mut self, entry: Entry<'bp>) { - match self.storage_index == self.storage.len() { - true => self.storage.push(entry), - false => self.storage[self.storage_index] = entry, - } - self.current_scope_size += 1; - self.storage_index += 1; - } - - fn inner_get(&self, lookup: &ScopeLookup<'bp>, offset: &mut Option, _states: &States) -> NameThis<'bp> { - let mut current_offset = offset.unwrap_or(self.storage.len()); - - loop { - let Some((new_offset, entry)) = self.storage[..current_offset] - .iter() - .enumerate() - .rev() - .find_map(|(i, e)| e.get(lookup.path).map(|e| (i, e))) - else { - return NameThis::Nothing; - }; - - current_offset = new_offset; - *offset = Some(new_offset); - - match entry { - // Pending - Entry::Pending(_, pending) => panic!(),//break EvalValue::Dyn(pending.subscribe(lookup.id)).into(), - - // Downgraded - Entry::Downgraded(_, downgrade) => break downgrade.upgrade(lookup.id).into(), - - // Expression - Entry::Expression(path, expression) => break NameThis::ResolveThisNow(expression), - - // Expressions - Entry::Expressions(path, expressions) => panic!(), - - // State value - // &Entry::State(state_id) => { - // let state = states.get(state_id)?; - // if let Some(value) = state.state_get(lookup.path, lookup.id) { - // break Some(EvalValue::Dyn(value)); - // } - // } - _ => continue, - } - } - } - - pub(crate) fn get_expressions(&self, b: Path<'_>) -> Option<&'bp [Expression]> { - self.storage.iter().rev().find_map(|e| match e { - Entry::Expressions(binding, expressions) if b.eq(binding) => Some(*expressions), - _ => None, - }) - } - - // This gets the most recently scoped state. - // - // There is always a state for each component - // (if no explicit state is given a unit is assumed) - // - // TODO: This is not entirely correct given that the root template - // has no component, perhaps this should change so there is always - // a component in the root. - pub(crate) fn get_state(&self) -> EvalValue<'bp> { - self.storage - .iter() - .rev() - .find_map(|e| match e { - Entry::State(state) => Some(EvalValue::State(*state)), - _ => None, - }) - // Note that this `expect` is false until we force a root component - .expect("there should always be at least one state entry") - } - - pub(crate) fn get_component_attributes(&self) -> EvalValue<'bp> { - self.storage - .iter() - .rev() - .find_map(|e| match e { - Entry::ComponentAttributes(component_id) => Some(EvalValue::ComponentAttributes(*component_id)), - _ => None, - }) - // Note that this `expect` is false until we force a root component - .expect("there should always be at least one attribute entry") - } - - /// Get can never return an eval value that is downgraded or pending - pub(crate) fn get(&self, lookup: ScopeLookup<'bp>, offset: &mut Option, states: &States) -> NameThis<'bp> { - self.inner_get(&lookup, offset, states) - } - - pub fn insert_state(&mut self, state_id: StateId) { - let entry = Entry::State(state_id); - self.insert_entry(entry); - } - - pub(crate) fn push(&mut self) { - self.insert_entry(Entry::Scope(self.current_scope_size)); - self.current_scope_size = 0; - self.level += 1; - } - - pub(crate) fn pop(&mut self) { - if self.storage_index == 0 { - return; - } - - let index = self.storage_index - 1 - self.current_scope_size; - let &Entry::Scope(size) = &self.storage[index] else { panic!() }; - self.storage[index..].fill_with(|| Entry::Empty); - self.storage_index = index; - self.current_scope_size = size; - self.level -= 1; - } - - pub(crate) fn scope_pending(&mut self, key: &'bp str, iter_value: PendingValue) { - let entry = Entry::Pending(Path::from(key), iter_value); - self.insert_entry(entry); - } - - // TODO: scope this expression at the scope level for which it was inserted. - pub(crate) fn scope_expression(&mut self, key: &'bp str, expression: &'bp Expression) { - let entry = Entry::Expression(Path::from(key), expression); - self.insert_entry(entry); - } - - // TODO: scope this expression at the scope level for which it was inserted. - pub(crate) fn scope_expressions(&mut self, key: &'bp str, expressions: &'bp [Expression]) { - let entry = Entry::Expressions(Path::from(key), expressions); - self.insert_entry(entry); - } - - pub(crate) fn scope_component_attributes(&mut self, widget_id: WidgetId) { - let entry = Entry::ComponentAttributes(widget_id); - self.insert_entry(entry); - } - - pub(crate) fn scope_downgrade(&mut self, binding: &'bp str, downgrade: Downgraded<'bp>) { - let entry = Entry::Downgraded(Path::from(binding), downgrade); - self.insert_entry(entry); - } - - pub(crate) fn scope_indexed(&mut self, binding: &'bp str, index: usize, mut offset: Option) { - panic!("scope has moved to the resolver"); - // // 1. find the value by binding - - // let mut current_offset = offset.unwrap_or(self.storage.len()); - - // loop { - // let Some((new_offset, entry)) = self.storage[..current_offset] - // .iter() - // .enumerate() - // .rev() - // .find_map(|(i, e)| e.get(binding.into()).map(|e| (i, e))) - // else { - // return; - // }; - - // current_offset = new_offset; - // offset = Some(new_offset); - - // // Loops only scope pending values or collections - // match entry { - // // Pending - // Entry::Pending(_, pending) => { - // let Some(value) = pending.as_state(|state| state.state_lookup(index.into())) else { break }; - // let entry = Entry::Pending(binding.into(), value); - // self.insert_entry(entry); - // } - - // // // Expressions - // Entry::Expressions(path, expressions) => { - // let expression = &expressions[index]; - // let entry = Entry::Expression(binding.into(), expression); - // self.insert_entry(entry); - // } - - // // State value - // // &Entry::State(state_id) => { - // // let state = states.get(state_id)?; - // // if let Some(value) = state.state_get(lookup.path, lookup.id) { - // // break Some(EvalValue::Dyn(value)); - // // } - // // } - // _ => continue, - // }; - // } - - // // let value_id = ValueId::from((iter.widget_id, SmallIndex::ZERO)); - // // let lookup = ScopeLookup::new(iter.binding, value_id); - // // match ctx.scope.get_expressions(iter.binding.into()) { - // // Some(exprs) => { - // // ctx.scope.scope_expression(iter.binding, &exprs[loop_index]); - // // } - // // None => match ctx.scope.get(lookup, &mut None, ctx.states) { - // // crate::expressions::NameThis::Nothing => panic!("missing collection"), - // // crate::expressions::NameThis::Value(eval_value) => { - // // match eval_value.get(loop_index.into(), value_id, ctx.states, ctx.attribute_storage) { - // // crate::expressions::NameThis::Nothing => todo!(), - // // crate::expressions::NameThis::Value(eval_value) => { - // // ctx.scope.scope_downgrade(iter.binding, eval_value.downgrade()) - // // } - // // crate::expressions::NameThis::ResolveThisNow(expr) => { - // // ctx.scope.scope_expression(iter.binding, expr) - // // } - // // } - // // } - // // crate::expressions::NameThis::ResolveThisNow(expr) => { - // // unreachable!() - // // } - // // }, - // // } - } -} - -pub struct DebugScope<'a, 'b>(pub &'a Scope<'b>); - -impl DebugWriter for DebugScope<'_, '_> { - fn write(&mut self, output: &mut impl Write) -> std::fmt::Result { - for (i, entry) in self.0.storage.iter().enumerate() { - writeln!(output, "{i:02} {entry:?}")?; - } - Ok(()) - } -} - -// #[cfg(test)] -impl<'bp> Scope<'bp> { - pub fn debug(&self) -> String { - let mut s = String::new(); - - for (i, entry) in self.storage.iter().enumerate() { - s += &format!("{i:02} {entry:?}\n"); - } - - s += "-----------------------\n"; - s += &format!( - "current scope size: {} | level: {}\n", - self.current_scope_size, self.level - ); - - s - } -} - -#[cfg(test)] -mod test { - use anathema_state::{List, Map, Value}; - use anathema_strings::HStrings; - use anathema_templates::{Expression, Globals}; - - use super::*; - use crate::expressions::{eval_collection, ExprEvalCtx}; - use crate::AttributeStorage; - - #[test] - fn scope_collection() { - let mut map = Map::>>::empty(); - map.insert("list", Value::>::from_iter([1u8, 2, 3])); - - let states = States::new(); - let scope = Scope::new(); - let attributes = AttributeStorage::empty(); - let expr = Expression::Ident("list".into()); - let globals = Globals::new(Default::default()); - let mut strings = HStrings::empty(); - let ctx = ExprEvalCtx { - scope: &scope, - states: &states, - attributes: &attributes, - globals: &globals, - }; - eval_collection(&expr, &ctx, &mut strings, ValueId::ZERO); - - // let one = [Expression::Primitive(1i64.into())]; - - // scope.scope_pending("a", &one); - // scope.scope_downgraded("a", 0); - - // scope.push(); - // let two = [Expression::Primitive(2i64.into())]; - // scope.scope_static_collection("a", &two); // |_ a = 2i64 - // scope.scope_index_lookup("a", 0); // | - // scope.pop(); - - // let ScopeValue::Dyn(expr) = scope.get(ScopeLookup::lookup("a"), &mut None).unwrap() else { - // panic!() - // }; - // let val = eval(expr, &scope, Subscriber::ZERO); - // assert_eq!(1, val.load_number().unwrap().as_int()); - } -}