🙃
This commit is contained in:
@@ -19,6 +19,7 @@ pub enum LeadErrCode {
|
||||
Syntax,
|
||||
Server,
|
||||
Unsupported,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -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<Expr>,
|
||||
precs: &mut HashSet<CellRef>,
|
||||
grid: Option<&Grid>,
|
||||
func: fn(f64) -> f64,
|
||||
func_name: String,
|
||||
) -> Result<Eval, LeadErr> {
|
||||
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<Expr>, value: Eval) -> Result<Eval, LeadErr> {
|
||||
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<Eval, LeadErr> {
|
||||
match (lval, rval) {
|
||||
(Eval::Literal(a), Eval::Literal(b)) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<CellRef>,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import clsx from 'clsx';
|
||||
import type { CellT } from './utils';
|
||||
import { getErrDesc, getErrTitle, getEvalLiteral, isErr, type CellT } from './utils';
|
||||
import * as HoverCard from '$lib/components/ui/hover-card/index.js';
|
||||
|
||||
let {
|
||||
cla = '',
|
||||
@@ -56,15 +57,30 @@
|
||||
focus:z-20 focus:shadow-[0_0_0_1px_var(--color-primary)] focus:outline-none"
|
||||
onblur={(e) => {
|
||||
cell = {
|
||||
isErr: false,
|
||||
val: undefined,
|
||||
val: cell?.val,
|
||||
raw_val: (e.target as HTMLInputElement).value
|
||||
};
|
||||
stopediting();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else if cell && isErr(cell.val)}
|
||||
<HoverCard.Root openDelay={500} closeDelay={500}>
|
||||
<HoverCard.Trigger>
|
||||
{@render InnerCell()}
|
||||
</HoverCard.Trigger>
|
||||
<HoverCard.Content side="right">
|
||||
<h2 class="text-md font-semibold tracking-tight transition-colors">
|
||||
{getErrTitle(cell.val)}
|
||||
</h2>
|
||||
{getErrDesc(cell.val)}
|
||||
</HoverCard.Content>
|
||||
</HoverCard.Root>
|
||||
{:else}
|
||||
{@render InnerCell()}
|
||||
{/if}
|
||||
|
||||
{#snippet InnerCell()}
|
||||
<div
|
||||
ondblclick={startediting}
|
||||
{onmousedown}
|
||||
@@ -72,17 +88,17 @@
|
||||
style:height
|
||||
class={clsx('placeholder bg-background p-1', { active }, cla)}
|
||||
>
|
||||
{#if cell && (cell.raw_val !== '' || cell.val !== '')}
|
||||
<span class={clsx('pointer-events-none select-none', { err: cell.isErr })}>
|
||||
{#if cell && (cell.raw_val !== '' || getEvalLiteral(cell.val) !== '')}
|
||||
<span class={clsx('pointer-events-none select-none', { err: isErr(cell.val) })}>
|
||||
{#if cell.val}
|
||||
{cell.val}
|
||||
{getEvalLiteral(cell.val)}
|
||||
{:else}
|
||||
{cell.raw_val}
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<style>
|
||||
.placeholder {
|
||||
@@ -101,6 +117,7 @@
|
||||
.active:has(.err),
|
||||
.placeholder:has(.err) {
|
||||
position: relative; /* needed for absolute positioning */
|
||||
color: red;
|
||||
}
|
||||
|
||||
.active:has(.err)::after,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { Pencil } from '@lucide/svelte';
|
||||
import Cell from '$lib/components/grid/cell.svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import CellHeader from './cell-header.svelte';
|
||||
import { colToStr, getEvalLiteral, isErr, refToStr, type CellT } from './utils';
|
||||
import { colToStr, refToStr, type CellT } from './utils';
|
||||
import clsx from 'clsx';
|
||||
import { Input } from '../ui/input';
|
||||
|
||||
let {
|
||||
socket,
|
||||
@@ -44,7 +46,12 @@
|
||||
console.error('Expected cell value for SET msgponse from server.');
|
||||
return;
|
||||
}
|
||||
setCellVal(msg.cell.row, msg.cell.col, getEvalLiteral(msg.eval), isErr(msg.eval));
|
||||
|
||||
grid_vals[key(msg.cell.row, msg.cell.col)] = {
|
||||
raw_val: msg.raw ?? '',
|
||||
val: msg.eval
|
||||
};
|
||||
|
||||
break;
|
||||
}
|
||||
case 'bulk': {
|
||||
@@ -108,44 +115,24 @@
|
||||
const key = (i: number, j: number) => `${i}:${j}`;
|
||||
|
||||
const getCell = (i: number, j: number) => grid_vals[key(i, j)];
|
||||
|
||||
const getCellRaw = (i: number, j: number) => getCell(i, j)?.raw_val ?? '';
|
||||
const setCellRaw = (i: number, j: number, val: string) => {
|
||||
if (grid_vals[key(i, j)] === undefined) {
|
||||
grid_vals[key(i, j)] = {
|
||||
raw_val: val,
|
||||
isErr: false,
|
||||
val: undefined
|
||||
};
|
||||
} else {
|
||||
grid_vals[key(i, j)].raw_val = val;
|
||||
const setCell = (row: number, col: number, v: CellT) => {
|
||||
if (v?.raw_val == null || v.raw_val === '') {
|
||||
delete grid_vals[key(row, col)];
|
||||
return;
|
||||
}
|
||||
};
|
||||
const getCellVal = (i: number, j: number) => getCell(i, j);
|
||||
const setCellVal = (i: number, j: number, val: LiteralValue, isErr: boolean) => {
|
||||
if (grid_vals[key(i, j)] === undefined) {
|
||||
console.warn('Cell raw value was undefined but recieved eval.');
|
||||
} else {
|
||||
let cell = grid_vals[key(i, j)];
|
||||
cell.val = val;
|
||||
cell.isErr = isErr;
|
||||
}
|
||||
};
|
||||
|
||||
const setCell = (row: number, col: number, v: CellT | undefined) => {
|
||||
// ignore “no value” so we don’t create keys on mount
|
||||
if (v?.raw_val == null || v.raw_val === '') delete grid_vals[key(row, col)];
|
||||
else {
|
||||
setCellRaw(row, col, v.raw_val);
|
||||
grid_vals[key(row, col)] = {
|
||||
raw_val: v.raw_val,
|
||||
val: v.val
|
||||
};
|
||||
|
||||
let msg: LeadMsg = {
|
||||
msg_type: 'set',
|
||||
cell: { row, col },
|
||||
raw: v.raw_val
|
||||
};
|
||||
let msg: LeadMsg = {
|
||||
msg_type: 'set',
|
||||
cell: { row, col },
|
||||
raw: v.raw_val
|
||||
};
|
||||
|
||||
socket.send(JSON.stringify(msg));
|
||||
}
|
||||
socket.send(JSON.stringify(msg));
|
||||
};
|
||||
|
||||
function handleCellInteraction(i: number, j: number, e: MouseEvent) {
|
||||
@@ -180,6 +167,33 @@
|
||||
active_cell = [i, j];
|
||||
}
|
||||
|
||||
function getActiveCell(): CellT {
|
||||
if (active_cell != null && grid_vals[key(active_cell[0], active_cell[1])])
|
||||
return grid_vals[key(active_cell[0], active_cell[1])];
|
||||
else
|
||||
return {
|
||||
raw_val: '',
|
||||
val: undefined
|
||||
};
|
||||
}
|
||||
|
||||
function setActiveCellRaw(raw: string): void {
|
||||
if (active_cell == null) return;
|
||||
|
||||
if (grid_vals[key(active_cell[0], active_cell[1])]) {
|
||||
let cell = grid_vals[key(active_cell[0], active_cell[1])];
|
||||
setCell(active_cell[0], active_cell[1], {
|
||||
raw_val: raw,
|
||||
val: cell.val
|
||||
});
|
||||
} else {
|
||||
setCell(active_cell[0], active_cell[1], {
|
||||
raw_val: raw,
|
||||
val: undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
// optional: check if click target is outside grid container
|
||||
@@ -192,6 +206,15 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-5 ml-5 flex items-center gap-5">
|
||||
<Pencil />
|
||||
<Input
|
||||
bind:value={() => 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"
|
||||
></Input>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={clsx('grid-wrapper relative h-full min-h-0 max-w-full min-w-0 overflow-auto', className)}
|
||||
>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import { LinkPreview as HoverCardPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
class: className,
|
||||
align = 'center',
|
||||
sideOffset = 4,
|
||||
portalProps,
|
||||
...restProps
|
||||
}: HoverCardPrimitive.ContentProps & {
|
||||
portalProps?: HoverCardPrimitive.PortalProps;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<HoverCardPrimitive.Portal {...portalProps}>
|
||||
<HoverCardPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="hover-card-content"
|
||||
{align}
|
||||
{sideOffset}
|
||||
class={cn(
|
||||
'z-[200] mt-3 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden outline-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
</HoverCardPrimitive.Portal>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { LinkPreview as HoverCardPrimitive } from "bits-ui";
|
||||
|
||||
let { ref = $bindable(null), ...restProps }: HoverCardPrimitive.TriggerProps = $props();
|
||||
</script>
|
||||
|
||||
<HoverCardPrimitive.Trigger bind:ref data-slot="hover-card-trigger" {...restProps} />
|
||||
14
frontend/src/lib/components/ui/hover-card/index.ts
Normal file
14
frontend/src/lib/components/ui/hover-card/index.ts
Normal file
@@ -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,
|
||||
};
|
||||
@@ -21,7 +21,9 @@
|
||||
<div class="absolute left-0 min-h-0 w-full">
|
||||
<div class="flex h-[100vh] flex-col">
|
||||
<div class="h-[60px] w-full p-3">
|
||||
<Sidebar.Trigger />
|
||||
<div class="flex items-center gap-5">
|
||||
<Sidebar.Trigger />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-wrapper min-h-0 w-full flex-1">
|
||||
@@ -32,6 +34,6 @@
|
||||
|
||||
<style>
|
||||
.grid-wrapper {
|
||||
border-top: 2px solid var(--color-input);
|
||||
/* border-top: 2px solid var(--color-input); */
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user