🙃
This commit is contained in:
@@ -4,59 +4,93 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::evaluator::*;
|
use crate::evaluator::*;
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct Cell {
|
|
||||||
eval: Eval,
|
|
||||||
raw: String,
|
|
||||||
i_dep: HashSet<CellRef>,
|
|
||||||
they_dep: HashSet<CellRef>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Cell {
|
|
||||||
pub fn new(eval: Eval, raw: String) -> Self {
|
|
||||||
Self {
|
|
||||||
eval,
|
|
||||||
raw,
|
|
||||||
i_dep: HashSet::new(),
|
|
||||||
they_dep: HashSet::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn raw(&self) -> String {
|
|
||||||
self.raw.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval(&self) -> Eval {
|
|
||||||
self.eval.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_i_dep(&mut self, dep: CellRef) {
|
|
||||||
self.i_dep.insert(dep);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_they_dep(&mut self, dep: CellRef) {
|
|
||||||
self.they_dep.insert(dep);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_i_dep(&mut self) {
|
|
||||||
self.i_dep.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn clear_they_dep(&mut self) {
|
|
||||||
self.they_dep.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_eval(&mut self, eval: Eval) {
|
|
||||||
self.eval = eval;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||||
pub struct CellRef {
|
pub struct CellRef {
|
||||||
pub row: usize,
|
pub row: usize,
|
||||||
pub col: usize,
|
pub col: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Cell {
|
||||||
|
reference: CellRef,
|
||||||
|
eval: Eval,
|
||||||
|
raw: String,
|
||||||
|
precedents: HashSet<CellRef>, // Cells that this cell reads
|
||||||
|
dependents: HashSet<CellRef>, // Cells that read this cell
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cell {
|
||||||
|
pub fn new(reference: CellRef, eval: Eval, raw: String) -> Self {
|
||||||
|
Self {
|
||||||
|
reference,
|
||||||
|
eval,
|
||||||
|
raw,
|
||||||
|
precedents: HashSet::new(),
|
||||||
|
dependents: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_all(
|
||||||
|
reference: CellRef,
|
||||||
|
eval: Eval,
|
||||||
|
raw: String,
|
||||||
|
precedents: HashSet<CellRef>,
|
||||||
|
dependents: HashSet<CellRef>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
reference,
|
||||||
|
eval,
|
||||||
|
raw,
|
||||||
|
precedents,
|
||||||
|
dependents,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raw(&self) -> String {
|
||||||
|
self.raw.to_owned()
|
||||||
|
}
|
||||||
|
pub fn eval(&self) -> Eval {
|
||||||
|
self.eval.to_owned()
|
||||||
|
}
|
||||||
|
pub fn reference(&self) -> CellRef {
|
||||||
|
self.reference.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_raw(&mut self, raw: String) {
|
||||||
|
self.raw = raw;
|
||||||
|
}
|
||||||
|
pub fn set_eval(&mut self, eval: Eval) {
|
||||||
|
self.eval = eval;
|
||||||
|
}
|
||||||
|
pub fn set_ref(&mut self, reference: CellRef) {
|
||||||
|
self.reference = reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_dep(&mut self, it: CellRef) {
|
||||||
|
self.dependents.insert(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_dep(&mut self, it: &CellRef) {
|
||||||
|
self.dependents.remove(&it);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_prec(&mut self, it: CellRef) {
|
||||||
|
self.precedents.insert(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_precs(&mut self, it: HashSet<CellRef>) {
|
||||||
|
self.precedents = it;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deps(&self) -> HashSet<CellRef> {
|
||||||
|
self.dependents.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn precs(&self) -> HashSet<CellRef> {
|
||||||
|
self.precedents.to_owned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl CellRef {
|
impl CellRef {
|
||||||
// Zero indexed
|
// Zero indexed
|
||||||
pub fn new(s: String) -> Result<CellRef, String> {
|
pub fn new(s: String) -> Result<CellRef, String> {
|
||||||
|
|||||||
@@ -8,49 +8,72 @@ use std::fmt;
|
|||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub enum Eval {
|
pub enum Eval {
|
||||||
Literal(Literal),
|
Literal(Literal),
|
||||||
|
CellRef { eval: Box<Eval>, reference: CellRef },
|
||||||
|
Range(Vec<Eval>),
|
||||||
|
Unset,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Eval {
|
impl fmt::Display for Eval {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Eval::Literal(lit) => write!(f, "{lit:?}"),
|
Eval::Literal(lit) => write!(f, "{lit:?}"),
|
||||||
|
Eval::Range(it) => write!(f, "Range({it:?})"),
|
||||||
|
Eval::CellRef { eval, reference } => write!(f, "EvalRef({eval:?}, {reference:?})"),
|
||||||
|
Eval::Unset => write!(f, "Unset"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluate(str: String, grid: Option<&Grid>) -> Result<(Eval, HashSet<CellRef>), String> {
|
pub fn evaluate(str: String, grid: Option<&Grid>) -> Result<(Eval, HashSet<CellRef>), String> {
|
||||||
let (expr, deps) = parse(&str)?;
|
let (expr, _) = parse(&str)?;
|
||||||
|
|
||||||
match evaluate_expr(&expr, grid) {
|
let mut precs = HashSet::new();
|
||||||
Ok(it) => Ok((it, deps)),
|
|
||||||
|
// Make evaulator adds precs for ranges
|
||||||
|
match evaluate_expr(&expr, &mut precs, grid) {
|
||||||
|
Ok(it) => Ok((it, precs)),
|
||||||
Err(it) => Err(it),
|
Err(it) => Err(it),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_expr(expr: &Expr, grid: Option<&Grid>) -> Result<Eval, String> {
|
fn evaluate_expr(
|
||||||
|
expr: &Expr,
|
||||||
|
precs: &mut HashSet<CellRef>,
|
||||||
|
grid: Option<&Grid>,
|
||||||
|
) -> Result<Eval, String> {
|
||||||
let res = match expr {
|
let res = match expr {
|
||||||
Expr::Literal(lit) => Eval::Literal(lit.clone()),
|
Expr::Literal(lit) => Eval::Literal(lit.clone()),
|
||||||
Expr::CellRef(re) => {
|
Expr::CellRef(re) => {
|
||||||
if let Some(g) = grid {
|
if let Some(g) = grid {
|
||||||
g.get_cell(re.to_owned())?
|
Eval::CellRef {
|
||||||
|
eval: Box::new(
|
||||||
|
g.get_cell(re.to_owned())
|
||||||
|
.map_or(Eval::Unset, |cell| cell.eval()),
|
||||||
|
),
|
||||||
|
reference: {
|
||||||
|
precs.insert(*re);
|
||||||
|
*re
|
||||||
|
},
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err("Evaluation error: Found cell reference but no grid.".into());
|
return Err("Evaluation error: Found cell reference but no grid.".into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Infix { op, lhs, rhs } => {
|
Expr::Infix { op, lhs, rhs } => {
|
||||||
let lval = evaluate_expr(lhs, grid)?;
|
let lval = evaluate_expr(lhs, precs, grid)?;
|
||||||
let rval = evaluate_expr(rhs, grid)?;
|
let rval = evaluate_expr(rhs, precs, grid)?;
|
||||||
|
|
||||||
match op {
|
match op {
|
||||||
InfixOp::ADD => eval_add(&lval, &rval)?,
|
InfixOp::ADD => eval_add(&lval, &rval)?,
|
||||||
InfixOp::SUB => eval_sub(&lval, &rval)?,
|
InfixOp::SUB => eval_sub(&lval, &rval)?,
|
||||||
InfixOp::MUL => eval_mul(&lval, &rval)?,
|
InfixOp::MUL => eval_mul(&lval, &rval)?,
|
||||||
InfixOp::DIV => eval_div(&lval, &rval)?,
|
InfixOp::DIV => eval_div(&lval, &rval)?,
|
||||||
|
InfixOp::RANGE => eval_range(&lval, &rval, precs, grid)?,
|
||||||
_ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)),
|
_ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Prefix { op, expr } => {
|
Expr::Prefix { op, expr } => {
|
||||||
let val = evaluate_expr(expr, grid)?;
|
let val = evaluate_expr(expr, precs, grid)?;
|
||||||
|
|
||||||
match op {
|
match op {
|
||||||
PrefixOp::POS => eval_pos(&val)?,
|
PrefixOp::POS => eval_pos(&val)?,
|
||||||
@@ -59,13 +82,115 @@ fn evaluate_expr(expr: &Expr, grid: Option<&Grid>) -> Result<Eval, String> {
|
|||||||
// _ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)),
|
// _ => return Err(format!("Evaluation error: Unsupported operator {:?}", op)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Expr::Group(g) => evaluate_expr(g, grid)?,
|
Expr::Group(g) => evaluate_expr(g, precs, grid)?,
|
||||||
|
Expr::Function { name, args } => match name.as_str() {
|
||||||
|
"AVG" => eval_avg(args, precs, grid)?,
|
||||||
|
it => return Err(format!("Evaluation error: Unsupported function {}.", it)),
|
||||||
|
},
|
||||||
it => return Err(format!("Evaluation error: Unsupported expression {:?}", it)),
|
it => return Err(format!("Evaluation error: Unsupported expression {:?}", it)),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn eval_range(
|
||||||
|
lval: &Eval,
|
||||||
|
rval: &Eval,
|
||||||
|
precs: &mut HashSet<CellRef>,
|
||||||
|
grid: Option<&Grid>,
|
||||||
|
) -> Result<Eval, String> {
|
||||||
|
match (lval, rval) {
|
||||||
|
(
|
||||||
|
Eval::CellRef {
|
||||||
|
eval: _,
|
||||||
|
reference: a_ref,
|
||||||
|
},
|
||||||
|
Eval::CellRef {
|
||||||
|
eval: _,
|
||||||
|
reference: b_ref,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let mut cells = Vec::new();
|
||||||
|
|
||||||
|
// assume row-major expansion
|
||||||
|
let row_start = a_ref.row.min(b_ref.row);
|
||||||
|
let row_end = a_ref.row.max(b_ref.row);
|
||||||
|
let col_start = a_ref.col.min(b_ref.col);
|
||||||
|
let col_end = a_ref.col.max(b_ref.col);
|
||||||
|
|
||||||
|
for row in row_start..=row_end {
|
||||||
|
for col in col_start..=col_end {
|
||||||
|
let reference = CellRef { row, col };
|
||||||
|
|
||||||
|
let Some(g) = grid else {
|
||||||
|
return Err("Evaluation error: Found cell range but no grid.".into());
|
||||||
|
};
|
||||||
|
|
||||||
|
cells.push(Eval::CellRef {
|
||||||
|
eval: Box::new(
|
||||||
|
g.get_cell(reference.to_owned())
|
||||||
|
.map_or(Eval::Unset, |cell| cell.eval()),
|
||||||
|
),
|
||||||
|
reference: {
|
||||||
|
precs.insert(reference);
|
||||||
|
reference
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Eval::Range(cells))
|
||||||
|
}
|
||||||
|
_ => Err("Evaluation error: expected cellref types for RANGE function.".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_avg(
|
||||||
|
args: &Vec<Expr>,
|
||||||
|
precs: &mut HashSet<CellRef>,
|
||||||
|
grid: Option<&Grid>,
|
||||||
|
) -> Result<Eval, String> {
|
||||||
|
let mut res = 0.0;
|
||||||
|
let mut count = 0;
|
||||||
|
|
||||||
|
for arg in args {
|
||||||
|
match evaluate_expr(arg, precs, grid)? {
|
||||||
|
Eval::Literal(Literal::Number(num)) => {
|
||||||
|
res += num;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
Eval::Range(range) => {
|
||||||
|
for cell in range {
|
||||||
|
let Eval::CellRef { eval, reference: _ } = cell else {
|
||||||
|
panic!("Found non cellref in evaluation time RANGE!.");
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Eval::Literal(Literal::Number(num)) = *eval {
|
||||||
|
res += num;
|
||||||
|
count += 1;
|
||||||
|
} else if matches!(*eval, Eval::Unset) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Err("Evaluation error: expected numeric types for AVG function."
|
||||||
|
.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
return Err(
|
||||||
|
"Evaluation error: expected numeric types for AVG function.".to_string()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count == 0 {
|
||||||
|
Err("Evaluation error: attempted to divide by zero.".to_string())
|
||||||
|
} else {
|
||||||
|
Ok(Eval::Literal(Literal::Number(res / count as f64)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn eval_add(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
fn eval_add(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
||||||
match (lval, rval) {
|
match (lval, rval) {
|
||||||
(Eval::Literal(a), Eval::Literal(b)) => {
|
(Eval::Literal(a), Eval::Literal(b)) => {
|
||||||
@@ -82,6 +207,9 @@ fn eval_add(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
|||||||
|
|
||||||
Err("Evaluation error: expected string or numeric types for ADD function.".to_string())
|
Err("Evaluation error: expected string or numeric types for ADD function.".to_string())
|
||||||
}
|
}
|
||||||
|
_ => {
|
||||||
|
Err("Evaluation error: expected string or numeric types for ADD function.".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,8 +220,9 @@ fn eval_sub(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
|||||||
return Ok(Eval::Literal(res));
|
return Ok(Eval::Literal(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err("Evaluation error: expected string or numeric types for SUB function.".to_string())
|
Err("Evaluation error: expected numeric types for SUB function.".to_string())
|
||||||
}
|
}
|
||||||
|
_ => Err("Evaluation error: expected numeric types for SUB function.".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn eval_mul(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
fn eval_mul(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
||||||
@@ -103,8 +232,9 @@ fn eval_mul(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
|||||||
return Ok(Eval::Literal(res));
|
return Ok(Eval::Literal(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err("Evaluation error: expected string or numeric types for MUL function.".to_string())
|
Err("Evaluation error: expected numeric types for MUL function.".to_string())
|
||||||
}
|
}
|
||||||
|
_ => Err("Evaluation error: expected numeric types for MUL function.".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn eval_div(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
fn eval_div(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
||||||
@@ -122,8 +252,9 @@ fn eval_div(lval: &Eval, rval: &Eval) -> Result<Eval, String> {
|
|||||||
return Ok(Eval::Literal(res));
|
return Ok(Eval::Literal(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
Err("Evaluation error: expected string or numeric types for DIV function.".to_string())
|
Err("Evaluation error: expected numeric types for DIV function.".to_string())
|
||||||
}
|
}
|
||||||
|
_ => Err("Evaluation error: expected numeric types for DIV function.".to_string()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use log::info;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cell::{Cell, CellRef},
|
cell::{Cell, CellRef},
|
||||||
evaluator::{Eval, evaluate},
|
evaluator::{Eval, evaluate},
|
||||||
@@ -16,38 +18,43 @@ impl Grid {
|
|||||||
cells: HashMap::new(),
|
cells: HashMap::new(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl Grid {
|
pub fn update_cell(
|
||||||
pub fn set_cell(&mut self, cell_ref: CellRef, raw_val: String) -> Result<Eval, String> {
|
&mut self,
|
||||||
|
cell_ref: CellRef,
|
||||||
|
raw_val: String,
|
||||||
|
) -> Result<Vec<CellRef>, String> {
|
||||||
if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val {
|
if self.cells.contains_key(&cell_ref) && self.cells[&cell_ref].raw() == raw_val {
|
||||||
return self.get_cell(cell_ref);
|
return Ok(Vec::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
let eval: Eval;
|
let eval: Eval;
|
||||||
let deps: HashSet<CellRef>;
|
let mut precs: HashSet<CellRef> = HashSet::new();
|
||||||
|
let mut updated_cells = vec![cell_ref];
|
||||||
|
|
||||||
if let Some(c) = raw_val.chars().nth(0)
|
if raw_val.chars().nth(0) != Some('=') {
|
||||||
&& c == '='
|
eval = Eval::Literal(Literal::String(raw_val.to_owned()));
|
||||||
{
|
|
||||||
(eval, deps) = evaluate(raw_val[1..].to_owned(), Some(&self))?;
|
|
||||||
// for dep in deps {}
|
|
||||||
} else {
|
} else {
|
||||||
match evaluate(raw_val.to_owned(), Some(&self)) {
|
// Evaluate raw expr and get precedents
|
||||||
Ok(e) => {
|
let (res_eval, res_precs) = evaluate(raw_val[1..].to_owned(), Some(&self))?;
|
||||||
(eval, deps) = e;
|
eval = res_eval;
|
||||||
}
|
precs = res_precs;
|
||||||
Err(_) => eval = Eval::Literal(Literal::String(raw_val.to_owned())),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cells
|
if self.cells.contains_key(&cell_ref) {
|
||||||
.insert(cell_ref, Cell::new(eval.clone(), raw_val));
|
updated_cells = self
|
||||||
Ok(eval)
|
.update_exisiting_cell(raw_val, eval, precs, cell_ref)?
|
||||||
|
.into_iter()
|
||||||
|
.chain(updated_cells)
|
||||||
|
.collect();
|
||||||
|
} else {
|
||||||
|
self.create_cell(raw_val, eval, precs, cell_ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(updated_cells)
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn get_cell(&mut self, cell_ref: CellRef) -> Result<(String, Eval), String> {
|
pub fn get_cell(&self, cell_ref: CellRef) -> Result<Cell, String> {
|
||||||
pub fn get_cell(&self, cell_ref: CellRef) -> Result<Eval, String> {
|
|
||||||
if !self.cells.contains_key(&cell_ref) {
|
if !self.cells.contains_key(&cell_ref) {
|
||||||
return Err(format!("Cell at {:?} not found.", cell_ref));
|
return Err(format!("Cell at {:?} not found.", cell_ref));
|
||||||
}
|
}
|
||||||
@@ -55,18 +62,200 @@ impl Grid {
|
|||||||
let cell = &self.cells[&cell_ref];
|
let cell = &self.cells[&cell_ref];
|
||||||
|
|
||||||
// Ok((cell.raw(), cell.eval()))
|
// Ok((cell.raw(), cell.eval()))
|
||||||
Ok(cell.eval())
|
Ok(cell.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_cell_dep(&mut self, cell_ref: CellRef, dep_ref: CellRef) -> Result<(), String> {
|
pub fn get_cell_mut(&mut self, cell_ref: CellRef) -> Result<&mut Cell, String> {
|
||||||
if !self.cells.contains_key(&cell_ref) {
|
if let Some(res) = self.cells.get_mut(&cell_ref) {
|
||||||
return Err(format!("Cell at {:?} not found.", cell_ref));
|
Ok(res)
|
||||||
|
} else {
|
||||||
|
Err(format!("Cell at {:?} not found.", cell_ref))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a topological order on the precedents graph
|
||||||
|
// i.e. if a requires b (e.g. a = 1 + b) then a -> b
|
||||||
|
// so a comes before b in the topo order
|
||||||
|
fn topo_order(&self, from: CellRef) -> Result<Vec<CellRef>, String> {
|
||||||
|
let mut res: Vec<CellRef> = Vec::new();
|
||||||
|
let mut search_set = Vec::new();
|
||||||
|
let mut temp = HashSet::new();
|
||||||
|
let mut perm = HashSet::new();
|
||||||
|
|
||||||
|
search_set.push(from);
|
||||||
|
|
||||||
|
let cell_data = &self.cells[&from];
|
||||||
|
search_set.extend(cell_data.deps().iter());
|
||||||
|
|
||||||
|
temp.insert(from);
|
||||||
|
perm.insert(from);
|
||||||
|
|
||||||
|
let mut searched = 1;
|
||||||
|
|
||||||
|
while searched != search_set.len() {
|
||||||
|
if perm.contains(&search_set[searched]) {
|
||||||
|
searched += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.topo_visit(
|
||||||
|
search_set[searched],
|
||||||
|
&mut temp,
|
||||||
|
&mut perm,
|
||||||
|
&mut search_set,
|
||||||
|
&mut res,
|
||||||
|
)?;
|
||||||
|
searched += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cell) = self.cells.get_mut(&cell_ref) {
|
Ok(res)
|
||||||
cell.add_i_dep(dep_ref);
|
}
|
||||||
|
|
||||||
|
fn topo_visit(
|
||||||
|
&self,
|
||||||
|
cell: CellRef,
|
||||||
|
temp: &mut HashSet<CellRef>,
|
||||||
|
perm: &mut HashSet<CellRef>,
|
||||||
|
search_set: &mut Vec<CellRef>,
|
||||||
|
res: &mut Vec<CellRef>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if perm.contains(&cell) {
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
if temp.contains(&cell) {
|
||||||
|
return Err("Evalutation error: Cycle detected in cell refs.".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
temp.insert(cell);
|
||||||
|
|
||||||
|
if !self.cells.contains_key(&cell) {
|
||||||
|
perm.insert(cell);
|
||||||
|
res.push(cell);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell_data = &self.cells[&cell];
|
||||||
|
|
||||||
|
search_set.extend(cell_data.deps().iter());
|
||||||
|
// search_set.extend(cell_data.precedents.iter().cloned());
|
||||||
|
|
||||||
|
for prec in cell_data.precs().iter() {
|
||||||
|
self.topo_visit(*prec, temp, perm, search_set, res)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
perm.insert(cell);
|
||||||
|
|
||||||
|
res.push(cell);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_exisiting_cell(
|
||||||
|
&mut self,
|
||||||
|
raw: String,
|
||||||
|
new_eval: Eval,
|
||||||
|
new_precs: HashSet<CellRef>,
|
||||||
|
cell_ref: CellRef,
|
||||||
|
) -> Result<Vec<CellRef>, String> {
|
||||||
|
let (old_precs, old_eval) = match self.cells.get_mut(&cell_ref) {
|
||||||
|
Some(cell) => {
|
||||||
|
cell.set_raw(raw);
|
||||||
|
(cell.precs().clone(), cell.eval().clone())
|
||||||
|
}
|
||||||
|
None => return Ok(Vec::new()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// diffs (outside any borrow)
|
||||||
|
let removed: Vec<_> = old_precs.difference(&new_precs).cloned().collect(); // old \ new
|
||||||
|
let added: Vec<_> = new_precs.difference(&old_precs).cloned().collect(); // new \ old
|
||||||
|
let eval_changed = old_eval != new_eval;
|
||||||
|
|
||||||
|
// ---- phase 2: apply (fresh borrows) ----
|
||||||
|
for p in &removed {
|
||||||
|
if let Some(c) = self.cells.get_mut(p) {
|
||||||
|
c.remove_dep(&cell_ref);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for p in &added {
|
||||||
|
if let Some(c) = self.cells.get_mut(p) {
|
||||||
|
c.add_dep(cell_ref);
|
||||||
|
} else {
|
||||||
|
self.cells.insert(
|
||||||
|
*p,
|
||||||
|
Cell::new_all(
|
||||||
|
*p,
|
||||||
|
Eval::Unset,
|
||||||
|
"".into(),
|
||||||
|
HashSet::new(),
|
||||||
|
HashSet::from([cell_ref]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell = self.cells.get_mut(&cell_ref).unwrap(); // Should be impossible to crash
|
||||||
|
cell.set_precs(new_precs);
|
||||||
|
cell.set_eval(new_eval);
|
||||||
|
|
||||||
|
if eval_changed {
|
||||||
|
self.propagate(cell_ref)
|
||||||
|
} else {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_cell(&mut self, raw: String, eval: Eval, precs: HashSet<CellRef>, cell_ref: CellRef) {
|
||||||
|
for prec in &precs {
|
||||||
|
if let Some(it) = self.cells.get_mut(&prec) {
|
||||||
|
it.add_dep(cell_ref);
|
||||||
|
} else {
|
||||||
|
self.cells.insert(
|
||||||
|
*prec,
|
||||||
|
Cell::new_all(
|
||||||
|
*prec,
|
||||||
|
Eval::Unset,
|
||||||
|
"".into(),
|
||||||
|
HashSet::new(),
|
||||||
|
HashSet::from([cell_ref]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("{:?}", self.cells.get(&prec));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cells.insert(
|
||||||
|
cell_ref,
|
||||||
|
Cell::new_all(cell_ref, eval, raw, precs, HashSet::new()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn propagate(&mut self, from: CellRef) -> Result<Vec<CellRef>, String> {
|
||||||
|
let mut res = Vec::new();
|
||||||
|
let topo = self.topo_order(from)?;
|
||||||
|
|
||||||
|
for cell_ref in topo {
|
||||||
|
res.push(cell_ref);
|
||||||
|
|
||||||
|
let raw = if let Some(cell) = self.cells.get(&cell_ref) {
|
||||||
|
let s = cell.raw();
|
||||||
|
if let Some(rest) = s.strip_prefix('=') {
|
||||||
|
rest.to_owned()
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Now we dropped the borrow of self.cells before this point
|
||||||
|
let (e, _) = evaluate(raw, Some(self))?;
|
||||||
|
|
||||||
|
if let Some(cell) = self.cells.get_mut(&cell_ref) {
|
||||||
|
cell.set_eval(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ use crate::{
|
|||||||
evaluator::Eval,
|
evaluator::Eval,
|
||||||
grid::Grid,
|
grid::Grid,
|
||||||
messages::{LeadMsg, MsgType},
|
messages::{LeadMsg, MsgType},
|
||||||
|
tokenizer::Literal,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -63,26 +64,45 @@ async fn accept_connection(stream: TcpStream) {
|
|||||||
let Some(cell_ref) = req.cell else { continue };
|
let Some(cell_ref) = req.cell else { continue };
|
||||||
let Some(raw) = req.raw else { continue };
|
let Some(raw) = req.raw else { continue };
|
||||||
|
|
||||||
match grid.set_cell(cell_ref.clone(), raw.to_owned()) {
|
match grid.update_cell(cell_ref.clone(), raw.to_owned()) {
|
||||||
Ok(eval) => match eval {
|
Ok(updates) => {
|
||||||
Eval::Literal(lit) => {
|
let mut msgs = Vec::new();
|
||||||
let res = LeadMsg {
|
|
||||||
msg_type: MsgType::Set,
|
for update in &updates {
|
||||||
cell: Some(cell_ref),
|
if let Ok(cell) = grid.get_cell(*update) {
|
||||||
raw: Some(raw.to_string()),
|
let Eval::Literal(lit) = cell.eval() else {
|
||||||
eval: Some(lit),
|
continue;
|
||||||
};
|
};
|
||||||
let _ = write
|
|
||||||
.send(serde_json::to_string(&res).unwrap().into())
|
msgs.push(LeadMsg {
|
||||||
.await;
|
msg_type: MsgType::Set,
|
||||||
|
cell: Some(*update),
|
||||||
|
raw: Some(cell.raw()),
|
||||||
|
eval: Some(lit),
|
||||||
|
bulk_msgs: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
|
let msg = LeadMsg {
|
||||||
|
cell: None,
|
||||||
|
raw: None,
|
||||||
|
eval: None,
|
||||||
|
bulk_msgs: Some(msgs),
|
||||||
|
msg_type: MsgType::Bulk,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _ = write
|
||||||
|
.send(serde_json::to_string(&msg).unwrap().into())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
let res = LeadMsg {
|
let res = LeadMsg {
|
||||||
msg_type: MsgType::Error,
|
msg_type: MsgType::Error,
|
||||||
cell: Some(cell_ref),
|
cell: Some(cell_ref),
|
||||||
raw: Some(e.to_string()),
|
raw: Some(e.to_string()),
|
||||||
eval: None,
|
eval: None,
|
||||||
|
bulk_msgs: None,
|
||||||
};
|
};
|
||||||
let _ = write
|
let _ = write
|
||||||
.send(serde_json::to_string(&res).unwrap().into())
|
.send(serde_json::to_string(&res).unwrap().into())
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ pub enum MsgType {
|
|||||||
Set,
|
Set,
|
||||||
Get,
|
Get,
|
||||||
Error,
|
Error,
|
||||||
|
Bulk,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
@@ -16,4 +17,5 @@ pub struct LeadMsg {
|
|||||||
pub cell: Option<CellRef>,
|
pub cell: Option<CellRef>,
|
||||||
pub raw: Option<String>,
|
pub raw: Option<String>,
|
||||||
pub eval: Option<Literal>,
|
pub eval: Option<Literal>,
|
||||||
|
pub bulk_msgs: Option<Vec<LeadMsg>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use log::info;
|
||||||
|
|
||||||
use crate::{cell::CellRef, tokenizer::*};
|
use crate::{cell::CellRef, tokenizer::*};
|
||||||
use std::{collections::HashSet, fmt};
|
use std::{collections::HashSet, fmt};
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ pub enum InfixOp {
|
|||||||
SUB,
|
SUB,
|
||||||
AND,
|
AND,
|
||||||
OR,
|
OR,
|
||||||
|
RANGE,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
@@ -57,6 +60,7 @@ pub trait Precedence {
|
|||||||
impl Precedence for InfixOp {
|
impl Precedence for InfixOp {
|
||||||
fn prec(&self) -> (u8, u8) {
|
fn prec(&self) -> (u8, u8) {
|
||||||
match self {
|
match self {
|
||||||
|
InfixOp::RANGE => (7, 8),
|
||||||
InfixOp::MUL | InfixOp::DIV | InfixOp::AND => (3, 4),
|
InfixOp::MUL | InfixOp::DIV | InfixOp::AND => (3, 4),
|
||||||
InfixOp::ADD | InfixOp::SUB | InfixOp::OR => (1, 2),
|
InfixOp::ADD | InfixOp::SUB | InfixOp::OR => (1, 2),
|
||||||
}
|
}
|
||||||
@@ -157,20 +161,21 @@ impl Expr {
|
|||||||
|
|
||||||
pub fn parse(input: &str) -> Result<(Expr, HashSet<CellRef>), String> {
|
pub fn parse(input: &str) -> Result<(Expr, HashSet<CellRef>), String> {
|
||||||
let mut tokenizer = Tokenizer::new(input)?;
|
let mut tokenizer = Tokenizer::new(input)?;
|
||||||
// println!("{:?}", tokenizer.tokens);
|
let mut precs = HashSet::new();
|
||||||
let mut deps = HashSet::new();
|
let expr = _parse(&mut tokenizer, 0, &mut precs)?;
|
||||||
Ok((_parse(&mut tokenizer, 0, &mut deps)?, deps))
|
info!("{}", expr.pretty());
|
||||||
|
Ok((expr, precs))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _parse(
|
pub fn _parse(
|
||||||
input: &mut Tokenizer,
|
input: &mut Tokenizer,
|
||||||
min_prec: u8,
|
min_prec: u8,
|
||||||
deps: &mut HashSet<CellRef>,
|
precedents: &mut HashSet<CellRef>,
|
||||||
) -> Result<Expr, String> {
|
) -> Result<Expr, String> {
|
||||||
let mut lhs = match input.next() {
|
let mut lhs = match input.next() {
|
||||||
Token::Literal(it) => Expr::Literal(it),
|
Token::Literal(it) => Expr::Literal(it),
|
||||||
Token::OpenParen => {
|
Token::OpenParen => {
|
||||||
let lhs = _parse(input, 0, deps)?;
|
let lhs = _parse(input, 0, precedents)?;
|
||||||
if input.next() != Token::CloseParen {
|
if input.next() != Token::CloseParen {
|
||||||
return Err(format!("Parse error: expected closing paren."));
|
return Err(format!("Parse error: expected closing paren."));
|
||||||
}
|
}
|
||||||
@@ -184,7 +189,7 @@ pub fn _parse(
|
|||||||
it => return Err(format!("Parse error: unknown prefix operator {:?}.", it)),
|
it => return Err(format!("Parse error: unknown prefix operator {:?}.", it)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let rhs = _parse(input, prefix_op.prec().1, deps)?;
|
let rhs = _parse(input, prefix_op.prec().1, precedents)?;
|
||||||
|
|
||||||
Expr::Prefix {
|
Expr::Prefix {
|
||||||
op: prefix_op,
|
op: prefix_op,
|
||||||
@@ -213,7 +218,7 @@ pub fn _parse(
|
|||||||
input.next(); // Skip comma
|
input.next(); // Skip comma
|
||||||
}
|
}
|
||||||
|
|
||||||
let arg = _parse(input, 0, deps)?;
|
let arg = _parse(input, 0, precedents)?;
|
||||||
args.push(arg);
|
args.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,7 +229,7 @@ pub fn _parse(
|
|||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let cell_ref = CellRef::new(id)?;
|
let cell_ref = CellRef::new(id)?;
|
||||||
deps.insert(cell_ref);
|
precedents.insert(cell_ref);
|
||||||
Expr::CellRef(cell_ref)
|
Expr::CellRef(cell_ref)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -235,7 +240,7 @@ pub fn _parse(
|
|||||||
// In the reference article this is a loop with match
|
// In the reference article this is a loop with match
|
||||||
// statement that breaks on Eof and closing paren but this is simpler and works as expected
|
// statement that breaks on Eof and closing paren but this is simpler and works as expected
|
||||||
while let Token::Operator(op) = input.peek() {
|
while let Token::Operator(op) = input.peek() {
|
||||||
if "+-*/&|".contains(op) {
|
if OPERATORS_STR.contains(op) {
|
||||||
let infix_op = match op {
|
let infix_op = match op {
|
||||||
'+' => InfixOp::ADD,
|
'+' => InfixOp::ADD,
|
||||||
'-' => InfixOp::SUB,
|
'-' => InfixOp::SUB,
|
||||||
@@ -243,6 +248,7 @@ pub fn _parse(
|
|||||||
'/' => InfixOp::DIV,
|
'/' => InfixOp::DIV,
|
||||||
'&' => InfixOp::AND,
|
'&' => InfixOp::AND,
|
||||||
'|' => InfixOp::OR,
|
'|' => InfixOp::OR,
|
||||||
|
':' => InfixOp::RANGE,
|
||||||
it => {
|
it => {
|
||||||
return Err(format!("Parse error: do not know infix operator {:?}.", it));
|
return Err(format!("Parse error: do not know infix operator {:?}.", it));
|
||||||
}
|
}
|
||||||
@@ -254,7 +260,7 @@ pub fn _parse(
|
|||||||
}
|
}
|
||||||
|
|
||||||
input.next();
|
input.next();
|
||||||
let rhs = _parse(input, r_prec, deps)?;
|
let rhs = _parse(input, r_prec, precedents)?;
|
||||||
lhs = Expr::Infix {
|
lhs = Expr::Infix {
|
||||||
op: infix_op,
|
op: infix_op,
|
||||||
lhs: Box::new(lhs),
|
lhs: Box::new(lhs),
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ pub enum Token {
|
|||||||
Eof,
|
Eof,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const OPERATORS_STR: &str = "+-*/^!%&|:";
|
||||||
|
|
||||||
pub struct Tokenizer {
|
pub struct Tokenizer {
|
||||||
pub tokens: Vec<Token>,
|
pub tokens: Vec<Token>,
|
||||||
}
|
}
|
||||||
@@ -97,7 +99,7 @@ impl Tokenizer {
|
|||||||
string.push(ch);
|
string.push(ch);
|
||||||
}
|
}
|
||||||
tokens.push(Token::Literal(Literal::String(string)));
|
tokens.push(Token::Literal(Literal::String(string)));
|
||||||
} else if "+-*/^!%&|".contains(c) {
|
} else if OPERATORS_STR.contains(c) {
|
||||||
tokens.push(Token::Operator(c));
|
tokens.push(Token::Operator(c));
|
||||||
chars.next();
|
chars.next();
|
||||||
} else if "()".contains(c) {
|
} else if "()".contains(c) {
|
||||||
|
|||||||
@@ -25,26 +25,38 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (res.msg_type) {
|
handle_msg(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handle_msg(msg: LeadMsg) {
|
||||||
|
switch (msg.msg_type) {
|
||||||
case 'error': {
|
case 'error': {
|
||||||
toast.error('Error', {
|
toast.error('Error', {
|
||||||
description: res.raw
|
description: msg.raw
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'set': {
|
case 'set': {
|
||||||
if (res.cell === undefined) {
|
if (msg.cell === undefined) {
|
||||||
console.error('Expected cell ref for SET response from server.');
|
console.error('Expected cell ref for SET msgponse from server.');
|
||||||
return;
|
return;
|
||||||
} else if (res.eval === undefined) {
|
} else if (msg.eval === undefined) {
|
||||||
console.error('Expected cell value for SET response from server.');
|
console.error('Expected cell value for SET msgponse from server.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCellVal(res.cell.row, res.cell.col, res.eval.value);
|
setCellVal(msg.cell.row, msg.cell.col, msg.eval.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'bulk': {
|
||||||
|
if (msg.bulk_msgs === undefined) {
|
||||||
|
console.error('Expected bulk_msgs field to be defined for BULK message.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const m of msg.bulk_msgs) handle_msg(m);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
let rows = 50;
|
let rows = 50;
|
||||||
let cols = 40;
|
let cols = 40;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
interface LeadMsg {
|
interface LeadMsg {
|
||||||
msg_type: 'set' | 'get' | 'error';
|
msg_type: 'set' | 'get' | 'error' | 'bulk';
|
||||||
cell?: CellRef;
|
cell?: CellRef;
|
||||||
raw?: string;
|
raw?: string;
|
||||||
eval?: Literal;
|
eval?: Literal;
|
||||||
|
bulk_msgs?: Array<LeadMsg>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CellRef {
|
interface CellRef {
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
<div
|
<div
|
||||||
data-slot="sidebar-gap"
|
data-slot="sidebar-gap"
|
||||||
class={cn(
|
class={cn(
|
||||||
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-100 ease-linear',
|
'relative w-(--sidebar-width) bg-transparent transition-[width] duration-150 ease-linear',
|
||||||
'group-data-[collapsible=offcanvas]:w-0',
|
'group-data-[collapsible=offcanvas]:w-0',
|
||||||
'group-data-[side=right]:rotate-180',
|
'group-data-[side=right]:rotate-180',
|
||||||
variant === 'floating' || variant === 'inset'
|
variant === 'floating' || variant === 'inset'
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
<div
|
<div
|
||||||
data-slot="sidebar-container"
|
data-slot="sidebar-container"
|
||||||
class={cn(
|
class={cn(
|
||||||
'fixed inset-y-0 z-[1000] hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-100 ease-linear md:flex',
|
'fixed inset-y-0 z-[1000] hidden h-svh w-(--sidebar-width) transition-[left,right,width] duration-150 ease-linear md:flex',
|
||||||
side === 'left'
|
side === 'left'
|
||||||
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
|
||||||
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
|
||||||
|
|||||||
Reference in New Issue
Block a user