diff --git a/backend/src/common.rs b/backend/src/common.rs index fccf258..e3ed112 100644 --- a/backend/src/common.rs +++ b/backend/src/common.rs @@ -19,6 +19,7 @@ pub enum LeadErrCode { Syntax, Server, Unsupported, + Invalid, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/backend/src/evaluator.rs b/backend/src/evaluator.rs index 2df45ca..5b217fe 100644 --- a/backend/src/evaluator.rs +++ b/backend/src/evaluator.rs @@ -7,6 +7,7 @@ use crate::common::Literal; use crate::grid::Grid; use crate::parser::*; use std::collections::HashSet; +use std::f64; use std::fmt; #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] @@ -117,6 +118,15 @@ fn evaluate_expr( Expr::Group(g) => evaluate_expr(g, precs, grid)?, Expr::Function { name, args } => match name.as_str() { "AVG" => eval_avg(args, precs, grid)?, + "EXP" => eval_single_arg_numeric(args, precs, grid, |x| x.exp(), "EXP".into())?, + "SIN" => eval_single_arg_numeric(args, precs, grid, |x| x.sin(), "SIN".into())?, + "COS" => eval_single_arg_numeric(args, precs, grid, |x| x.cos(), "COS".into())?, + "TAN" => eval_single_arg_numeric(args, precs, grid, |x| x.tan(), "TAN".into())?, + "ASIN" => eval_single_arg_numeric(args, precs, grid, |x| x.asin(), "ASIN".into())?, + "ACOS" => eval_single_arg_numeric(args, precs, grid, |x| x.acos(), "ACOS".into())?, + "ATAN" => eval_single_arg_numeric(args, precs, grid, |x| x.atan(), "ATAN".into())?, + "PI" => eval_const(args, Eval::Literal(Literal::Number(f64::consts::PI)))?, + "TAU" => eval_const(args, Eval::Literal(Literal::Number(f64::consts::TAU)))?, it => { return Err(LeadErr { title: "Evaluation error.".into(), @@ -256,6 +266,49 @@ fn eval_avg( } } +fn eval_single_arg_numeric( + args: &Vec, + precs: &mut HashSet, + grid: Option<&Grid>, + func: fn(f64) -> f64, + func_name: String, +) -> Result { + if args.len() != 1 { + return Err(LeadErr { + title: "Evaluation error.".into(), + desc: format!("{func_name} function requires a single argument."), + code: LeadErrCode::Invalid, + }); + } + + let err = LeadErr { + title: "Evaluation error.".into(), + desc: format!("{func_name} function requires a numeric argument."), + code: LeadErrCode::TypeErr, + }; + + match evaluate_expr(&args[0], precs, grid)? { + Eval::Literal(Literal::Number(num)) => Ok(Eval::Literal(Literal::Number(func(num)))), + Eval::CellRef { eval, .. } => match *eval { + Eval::Literal(Literal::Number(n)) => Ok(Eval::Literal(Literal::Number(func(n)))), + _ => Err(err), + }, + _ => Err(err), + } +} + +fn eval_const(args: &Vec, value: Eval) -> Result { + if args.len() != 0 { + return Err(LeadErr { + title: "Evaluation error.".into(), + desc: format!("PI function requires no arguments."), + code: LeadErrCode::Invalid, + }); + } + + Ok(value) +} + fn eval_add(lval: &Eval, rval: &Eval) -> Result { match (lval, rval) { (Eval::Literal(a), Eval::Literal(b)) => { diff --git a/backend/src/main.rs b/backend/src/main.rs index 3f7019f..c6993da 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -79,17 +79,23 @@ async fn accept_connection(stream: TcpStream) { } } - let msg = LeadMsg { - cell: None, - raw: None, - eval: None, - bulk_msgs: Some(msgs), - msg_type: MsgType::Bulk, - }; + if msgs.len() == 1 { + let _ = write + .send(serde_json::to_string(&msgs.get(0)).unwrap().into()) + .await; + } else if msgs.len() > 1 { + 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; + let _ = write + .send(serde_json::to_string(&msg).unwrap().into()) + .await; + } } Err(e) => { let res = LeadMsg { diff --git a/backend/src/messages.rs b/backend/src/messages.rs index a07dab6..bba7c6c 100644 --- a/backend/src/messages.rs +++ b/backend/src/messages.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::{cell::CellRef, evaluator::Eval}; -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "lowercase")] pub enum MsgType { Set, @@ -11,7 +11,7 @@ pub enum MsgType { Bulk, } -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct LeadMsg { pub msg_type: MsgType, pub cell: Option, diff --git a/frontend/src/lib/components/grid/cell.svelte b/frontend/src/lib/components/grid/cell.svelte index 1f01234..dbcf0e4 100644 --- a/frontend/src/lib/components/grid/cell.svelte +++ b/frontend/src/lib/components/grid/cell.svelte @@ -1,7 +1,8 @@ +
+ + getActiveCell().raw_val, (raw) => setActiveCellRaw(raw)} + class="relative w-[200px] rounded-none p-1 !transition-none delay-0 duration-0 + focus:z-20 focus:shadow-[0_0_0_1px_var(--color-primary)] focus:outline-none" + > +
+
diff --git a/frontend/src/lib/components/grid/utils.ts b/frontend/src/lib/components/grid/utils.ts index 355d441..d3aeea7 100644 --- a/frontend/src/lib/components/grid/utils.ts +++ b/frontend/src/lib/components/grid/utils.ts @@ -1,7 +1,6 @@ export interface CellT { raw_val: string; - val: LiteralValue | undefined; - isErr: boolean; + val?: Eval; } /** @@ -44,16 +43,38 @@ export function refToStr(row: number, col: number): string { return colToStr(col) + (row + 1).toString(); } -export function getEvalLiteral(value: Eval): LiteralValue { +export function getEvalLiteral(value: Eval | undefined): LiteralValue { + if (value === undefined) return ''; if (value === 'unset') return ''; - if ('literal' in value) return value.literal.value; + if ('literal' in value) { + if (value.literal.value == null) return 'NaN'; + return value.literal.value; + } if ('cellref' in value) return getEvalLiteral(value.cellref.eval); - if ('err' in value) return `err: ${value.err.code}`; + if ('err' in value) return `#${value.err.code.toUpperCase()}`; // if ('range' in value) return 'err'; return 'todo!'; } -export function isErr(value: Eval): boolean { +export function isErr(value: Eval | undefined): boolean { + if (value === undefined) return false; if (value === 'unset') return false; + if ('cellref' in value) return isErr(value.cellref.eval); return 'err' in value; } + +export function getErrTitle(value: Eval | undefined): string { + if (value === undefined) return ''; + if (value === 'unset') return ''; + if ('cellref' in value) return getErrTitle(value.cellref.eval); + if (!('err' in value)) return ''; + return value.err.title; +} + +export function getErrDesc(value: Eval | undefined): string { + if (value === undefined) return ''; + if (value === 'unset') return ''; + if ('cellref' in value) return getErrDesc(value.cellref.eval); + if (!('err' in value)) return ''; + return value.err.desc; +} diff --git a/frontend/src/lib/components/ui/hover-card/hover-card-content.svelte b/frontend/src/lib/components/ui/hover-card/hover-card-content.svelte new file mode 100644 index 0000000..0a7e4f3 --- /dev/null +++ b/frontend/src/lib/components/ui/hover-card/hover-card-content.svelte @@ -0,0 +1,29 @@ + + + + + diff --git a/frontend/src/lib/components/ui/hover-card/hover-card-trigger.svelte b/frontend/src/lib/components/ui/hover-card/hover-card-trigger.svelte new file mode 100644 index 0000000..322172b --- /dev/null +++ b/frontend/src/lib/components/ui/hover-card/hover-card-trigger.svelte @@ -0,0 +1,7 @@ + + + diff --git a/frontend/src/lib/components/ui/hover-card/index.ts b/frontend/src/lib/components/ui/hover-card/index.ts new file mode 100644 index 0000000..85f3949 --- /dev/null +++ b/frontend/src/lib/components/ui/hover-card/index.ts @@ -0,0 +1,14 @@ +import { LinkPreview as HoverCardPrimitive } from "bits-ui"; +import Content from "./hover-card-content.svelte"; +import Trigger from "./hover-card-trigger.svelte"; + +const Root = HoverCardPrimitive.Root; + +export { + Root, + Content, + Trigger, + Root as HoverCard, + Content as HoverCardContent, + Trigger as HoverCardTrigger, +}; diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index e412480..f233bcf 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -21,7 +21,9 @@
- +
+ +
@@ -32,6 +34,6 @@