diff --git a/web/root/src/bus.ts b/web/root/src/bus.ts index eb284fb..0afe553 100644 --- a/web/root/src/bus.ts +++ b/web/root/src/bus.ts @@ -5,7 +5,7 @@ import type { Example } from "./examples.ts"; import type { Sim, SimStepResult } from "./simulation.ts"; import type wasm from "./wasm.ts"; import type { Text } from "npm:@codemirror/state"; -import type { Highlight } from "./highlight.ts"; +import type { Highlight, HighlightKind } from "./highlight.ts"; type Unsubscribe = () => void; @@ -68,7 +68,7 @@ type AppEvents = { "begin": void; "editor/change": {text: string, doc: Text}; - "compiled": {log: wasm.CompileLog[], ansi_log: string, machine: string|undefined}; + "compiled": {log: wasm.CompileLog[], ansi_log: string, machine: Machine|undefined}; "automata/sim/update": Sim|null; "automata/sim/before_step": { simulation: Sim }; @@ -88,9 +88,8 @@ type AppEvents = { "highlight/one/add": Highlight; "highlight/one/remove": Highlight; - "highlight/all/remove": void; - "highlight/update": void; + "highlight/update": {span: Span, kind: HighlightKind, repr: string, remove: boolean}; "theme/update": void; }; diff --git a/web/root/src/editor.ts b/web/root/src/editor.ts index dbadd4c..13cb35f 100644 --- a/web/root/src/editor.ts +++ b/web/root/src/editor.ts @@ -25,7 +25,8 @@ import wasm from "./wasm.ts"; import { Share } from "./share.ts"; import { examples } from "./examples.ts"; import { bus } from "./bus.ts"; -import { current, Highlight, HighlightKind } from "./highlight.ts"; +import { current, Highlight, highlight_span_attr, HighlightKind } from "./highlight.ts"; +import { Machine, parse_machine_from_json, Span } from "./automata.ts"; function tokenize(text: string): wasm.Tok[] { try { @@ -38,57 +39,17 @@ function tokenize(text: string): wasm.Tok[] { function compile( text: string, -): { log: wasm.CompileLog[]; ansi_log: string; machine: string | undefined } { +): { log: wasm.CompileLog[]; ansi_log: string; machine: Machine | undefined } { try { - return wasm.compile(text); + const res = wasm.compile(text); + return {machine: res.machine ? parse_machine_from_json(res.machine):undefined, log: res.log, ansi_log: res.ansi_log}; } catch (e) { console.log(e); - return { log: [], ansi_log: "", machine: "" }; + return { log: [], ansi_log: "", machine: undefined }; } } -function decoForKind(kind: HighlightKind) { - // Use a class per kind so each gets a distinct color via CSS - return Decoration.mark({ class: `cm-highlight cm-highlight-${kind}` }); -} - -bus.on("highlight/update", _ => { - const arr = current.values().toArray().sort((a, b) => a.span[0]-b.span[0]); - editor.dispatch({ effects: setHighlights.of(arr) }); -}); -export const setHighlights = StateEffect.define(); -export const highlightsField = StateField.define({ - create() { - return Decoration.none; - }, - - update(highlights, tr) { - // Keep highlights aligned with document edits - highlights = highlights.map(tr.changes); - - for (const e of tr.effects) { - if (e.is(setHighlights)) { - const spans = e.value; - - const builder = new RangeSetBuilder(); - for (const s of spans) { - - const from = Math.max(0, Math.min(s.span[0], tr.state.doc.length)); - const to = Math.max(0, Math.min(s.span[1], tr.state.doc.length)); - if (to > from) builder.add(from, to, decoForKind(s.kind)); - } - highlights = builder.finish(); - } - } - - return highlights; - }, - - provide: (f) => EditorView.decorations.from(f), -}); - - const eventBusConnection = StateField.define({ create(state) { const text = state.doc.toString(); @@ -142,6 +103,28 @@ function buildAnalysis(text: string, doc: Text) { } } + const addDeco = (kind: HighlightKind, highlight: Span, location?: Span) => { + if(!location) location = highlight; + marks.push(Decoration.mark({attributes: {"highlight-kind": kind, "highlight-span": highlight_span_attr(highlight)}}).range(location[0], location[1])); + }; + + for (const transitions of machine?.transitions ?? []){ + for(const transition of transitions[1]){ + addDeco("focus", transition.function); + addDeco("warning", transition.transition); + } + } + + for (const state of machine?.states.values() ?? []){ + addDeco("success", state.definition); + } + + for (const [state, info] of machine?.final_states?.entries() ?? []){ + try{ + addDeco("success", machine?.states.get(state)!.definition!, info.definition); + }catch(e){} + } + const deco = Decoration.set(marks, true); return { tokens, log, ansi_log, deco }; } @@ -242,7 +225,6 @@ const state = EditorState.create({ keymap.of([...defaultKeymap, ...historyKeymap]), eventBusConnection, - highlightsField, diagHover, EditorView.lineWrapping, diff --git a/web/root/src/examples.ts b/web/root/src/examples.ts index 6d2bc42..dc0e723 100644 --- a/web/root/src/examples.ts +++ b/web/root/src/examples.ts @@ -392,34 +392,34 @@ d(q3,Y)=(q3,y,R) d(q3,B)=(q4,B,R) `), - new Example("CFG", "definition", - `// CFG's aren't supported yet, and this definition is not complete. -// This is the definition for the grammar the definition has itself +// new Example("CFG", "definition", +// `// CFG's aren't supported yet, and this definition is not complete. +// // This is the definition for the grammar the definition has itself -type=CFG +// type=CFG -S -> TopLevel | TopLevel S +// S -> TopLevel | TopLevel S -TopLevel -> Ident "=" Item // Item -TopLevel -> Ident Tuple "=" Item // Transition Functions -TopLevel -> Production | Table +// TopLevel -> Ident "=" Item // Item +// TopLevel -> Ident Tuple "=" Item // Transition Functions +// TopLevel -> Production | Table -Item -> Symbol | String | Tuple | List +// Item -> Symbol | String | Tuple | List -Symbol -> Ident | "~" -String -> "\"" "\"" -Tuple -> "(" ItemList ")" -List -> "{" ItemList "}" | "[" ItemList "]" +// Symbol -> Ident | "~" +// String -> "\"" "\"" +// Tuple -> "(" ItemList ")" +// List -> "{" ItemList "}" | "[" ItemList "]" -ItemList -> ~ | Item ItemList | Item "," ItemList +// ItemList -> ~ | Item ItemList | Item "," ItemList -Production -> ProductionGroup "->" ProductionGroupList -ProductionGroupList -> ProductionGroup | ProductionGroupList "|" ProductionGroup -ProductionGroup -> ProductionUnit | ProductionGroup ProductionUnit -ProductionUnit -> Ident | "~" | String +// Production -> ProductionGroup "->" ProductionGroupList +// ProductionGroupList -> ProductionGroup | ProductionGroupList "|" ProductionGroup +// ProductionGroup -> ProductionUnit | ProductionGroup ProductionUnit +// ProductionUnit -> Ident | "~" | String -`) +// `) ]; const CATEGORY_ORDER: Category[] = [ diff --git a/web/root/src/highlight.ts b/web/root/src/highlight.ts index 6864ddc..6cb42c1 100644 --- a/web/root/src/highlight.ts +++ b/web/root/src/highlight.ts @@ -15,7 +15,6 @@ export type Highlight = { type HighlightEntry = { span: Span, kind: HighlightKind, - count: number; } export const current: Map = new Map(); @@ -53,68 +52,46 @@ export function dehighlight_from_edge_id(node_id: string) { } } -bus.on("automata/update", _ => { - bus.emit("highlight/all/remove", undefined); -}) - function decoForKind(kind: HighlightKind): string { return `cm-highlight-${kind}`; } bus.on("highlight/one/add", (highlight) => { const key = asKey(highlight); - if (current.has(key)) { - current.get(key)!.count += 1; - } else { - current.set(key, { count: 1, ...highlight }); + if (!current.has(key)) { + current.set(key, {...highlight }); const cname = decoForKind(highlight.kind); - globalThis.document.querySelectorAll(`[highlight-span="${highlight.span[0]}:${highlight.span[1]}"]`).forEach(el => el.classList.add(cname)) + const repr = `${highlight.span[0]}:${highlight.span[1]}`; + globalThis.document.querySelectorAll(`[highlight-span="${repr}"]`).forEach(el => el.classList.add(cname)) - bus.emit("highlight/update", undefined); + bus.emit("highlight/update", {repr, remove: false, ...highlight}); } }); bus.on("highlight/one/remove", (highlight) => { const key = asKey(highlight); - if (current.has(key)) { - const value = current.get(key)! - value.count -= 1; - if (value.count === 0) { - current.delete(key); + if (current.delete(key)) { + const cname = decoForKind(highlight.kind); + const repr = `${highlight.span[0]}:${highlight.span[1]}`; + globalThis.document.querySelectorAll(`[highlight-span="${repr}"]`).forEach(el => el.classList.remove(cname)) - const cname = decoForKind(highlight.kind); - globalThis.document.querySelectorAll(`[highlight-span="${highlight.span[0]}:${highlight.span[1]}"]`).forEach(el => el.classList.remove(cname)) - - bus.emit("highlight/update", undefined); - } + bus.emit("highlight/update", {repr, remove: true, ...highlight}); } }); -bus.on("highlight/all/remove", (_) => { - if (current.size !== 0) { - current.clear(); - const warning = decoForKind("warning"); - const focus = decoForKind("focus"); - const success = decoForKind("success"); - const error = decoForKind("error"); - globalThis.document.querySelectorAll(`[highlight-span"]`).forEach(el => { - el.classList.remove(warning) - el.classList.remove(focus) - el.classList.remove(success) - el.classList.remove(error) - }) - - - bus.emit("highlight/update", undefined); - } -}); globalThis.document.addEventListener("mouseover", (e) => { - const target = (e.target instanceof Element) - ? e.target.closest("[highlight-span]") + if (!(e.target instanceof Element)) return; + + const target = e.target.closest("[highlight-span]"); + if (!target) return; + + const related = e.relatedTarget instanceof Element + ? e.relatedTarget.closest("[highlight-span]") : null; - if (!target) return; + // Mouse is still inside the same highlight span → ignore + if (related === target) return; const kind = (target.getAttribute("highlight-kind") ?? "focus") as unknown as HighlightKind; const span = target.getAttribute("highlight-span")!.split(":").map(Number) as unknown as Span; @@ -134,10 +111,14 @@ document.addEventListener("mouseout", (e) => { const kind = (from.getAttribute("highlight-kind") ?? "focus") as unknown as HighlightKind; const span = from.getAttribute("highlight-span")!.split(":").map(Number) as unknown as Span; - + bus.emit("highlight/one/remove", {span, kind}); }); export function highlightable(span: Span, text: string, kind?: HighlightKind): string{ return `${text}` +} + +export function highlight_span_attr(span: Span): string{ + return `${span[0]}:${span[1]}` } \ No newline at end of file diff --git a/web/root/src/paths.ts b/web/root/src/paths.ts index 39a3e20..b6a6f71 100644 --- a/web/root/src/paths.ts +++ b/web/root/src/paths.ts @@ -100,7 +100,7 @@ function renderTmPath(state: TmState, index: number) { + highlightable(step.function, `${DELTA}(${step.from_state}, ${step.from_symbol})`, "focus") + " = " + highlightable(step.transition, `(${step.state}, ${step.symbol}, ${step.direction})`, "warning"); - console.log(div.innerHTML); + steps.appendChild(div); } diff --git a/web/root/src/simulation.ts b/web/root/src/simulation.ts index 4ff39aa..ecd449e 100644 --- a/web/root/src/simulation.ts +++ b/web/root/src/simulation.ts @@ -5,7 +5,6 @@ import type { Pda, Tm, } from "./automata.ts"; -import {parse_machine_from_json} from "./automata.ts"; import { FaSim } from "./simulation/fa.ts"; export { FaSim } from "./simulation/fa.ts"; @@ -34,7 +33,7 @@ export let automaton: Machine = { bus.on("compiled", ({ machine }) => { if (machine) { try { - automaton = parse_machine_from_json(machine); + automaton = machine; bus.emit("automata/update", automaton); } catch (e) { console.log(e); diff --git a/web/root/src/splitters.ts b/web/root/src/splitters.ts index 058c938..fe220de 100644 --- a/web/root/src/splitters.ts +++ b/web/root/src/splitters.ts @@ -98,7 +98,6 @@ function enableFlexSplitters() { { const r = parent.getBoundingClientRect(); const px = clamp((defPct / 100) * r.height, minA, r.height - gap - minB); - console.log(r.height, px) setFixedSize(a, "y", px); } diff --git a/web/root/src/visualizer.ts b/web/root/src/visualizer.ts index 21e58c7..8af20a4 100644 --- a/web/root/src/visualizer.ts +++ b/web/root/src/visualizer.ts @@ -8,8 +8,7 @@ import { dehighlight_from_edge_id, dehighlight_from_node_id, highlight_from_edge bus.on("controls/vis/physics", ({ enabled }) => { - network.setOptions({ physics: { enabled } }); - network.setOptions({ edges: { smooth: enabled } }); + network.setOptions({nodes: {physics: enabled}}); }); bus.on("controls/vis/reset_network", _ => { try { @@ -34,10 +33,12 @@ bus.on("automata/sim/update", _ => { }); bus.on("automata/update", automaton => { + spanEdgeMap.clear(); + spanNodeMap.clear(); // Populate nodes - for (const state of automaton.states.keys()) { - + for (const [state, value] of automaton.states.entries()) { + spanNodeMap.set(`${value.definition[0]}:${value.definition[1]}`, state); const size = measureTextWidth(state, getGraphTheme().node_font) / 2 + 10 if (nodes.get(state)) { nodes.update({ @@ -65,6 +66,10 @@ bus.on("automata/update", automaton => { vadjust } }; + transitions.forEach(edge => { + spanEdgeMap.set(`${edge.function[0]}:${edge.function[1]}`, edge_id); + spanEdgeMap.set(`${edge.transition[0]}:${edge.transition[1]}`, edge_id); + }) if (edges.get(edge_id)) { edges.update({ id: edge_id, @@ -99,6 +104,29 @@ bus.on("automata/update", automaton => { } }); +bus.on("highlight/update", ({repr, remove}) => { + if(spanNodeMap.has(repr)){ + const id = spanNodeMap.get(repr)!; + if(remove){ + // @ts-expect-error bad library + nodes.update({id, color: null}); + }else{ + nodes.update({id, color: getGraphTheme().current_node_border}); + } + } + if(spanEdgeMap.has(repr)){ + const id = spanEdgeMap.get(repr)!; + if(remove){ + // @ts-expect-error bad library + edges.update({id, font: null}); + }else{ + edges.update({id, font: {color: getGraphTheme().node_anchor}}); + } + } +}) + +const spanEdgeMap: Map = new Map() +const spanNodeMap: Map = new Map() const nodes = new vis.DataSet(); const edges = new vis.DataSet(); @@ -184,6 +212,7 @@ function updateGraphTheme() { network.setOptions({ nodes: { labelHighlightBold: false, + color: gt.fg_0, font: { color: gt.fg_0, bold: { @@ -378,7 +407,7 @@ function renderNode({ } ctx.lineWidth = 2; - ctx.fillStyle = t.fg_0; + ctx.fillStyle = (style.color ?? t.fg_0) as string; ctx.strokeStyle = t.bg_0; ctx.strokeText(label, x, y); ctx.fillText(label, x, y); @@ -487,7 +516,6 @@ function drawInitialArrow( ctx.restore(); } - function drawPinIndicator( ctx: CanvasRenderingContext2D, x: number, @@ -495,55 +523,56 @@ function drawPinIndicator( r: number, color: string, ) { - const size = Math.max(7, Math.round(r * 0.28)); - const ox = x + r - size * 0.55; - const oy = y + r - size * 0.55; +const size = Math.max(7, Math.round(r * 0.28)); - const stroke = color; - const fill = "rgba(0,0,0,0)"; + // Position near bottom-right of node + const cx = x + r - size * 0.6; + const cy = y + r - size * 0.55; + + const headRadius = size * 0.45; + const rimRadius = headRadius * 0.85; + const needleLength = size * 1.1; ctx.save(); + + const strokeWidth = Math.max(1.25, Math.round(r * 0.06)); + ctx.lineWidth = strokeWidth; + ctx.strokeStyle = color; + ctx.fillStyle = "rgba(0,0,0,0)"; + ctx.shadowColor = "rgba(0,0,0,0)"; - ctx.shadowBlur = 6; - ctx.shadowOffsetX = 0; - ctx.shadowOffsetY = 2; + ctx.shadowBlur = 4; + ctx.shadowOffsetY = 1; - // Pin head (circle) + // ---- Head (top disc) ctx.beginPath(); - ctx.arc(ox, oy, size * 0.55, 0, Math.PI * 2); - ctx.fillStyle = fill; - ctx.fill(); - - // Pin stem (triangle-ish) - ctx.beginPath(); - ctx.moveTo(ox, oy + size * 0.25); - ctx.lineTo(ox - size * 0.35, oy + size * 0.95); - ctx.lineTo(ox + size * 0.35, oy + size * 0.95); - ctx.closePath(); - ctx.fillStyle = fill; - ctx.fill(); - - // Outline - ctx.shadowBlur = 0; - ctx.lineWidth = Math.max(1.25, Math.round(r * 0.06)); - ctx.strokeStyle = stroke; - - ctx.beginPath(); - ctx.arc(ox, oy, size * 0.55, 0, Math.PI * 2); + ctx.arc(cx, cy, headRadius, 0, Math.PI * 2); ctx.stroke(); + // ---- Rim (inner ring) ctx.beginPath(); - ctx.moveTo(ox, oy + size * 0.25); - ctx.lineTo(ox - size * 0.35, oy + size * 0.95); - ctx.lineTo(ox + size * 0.35, oy + size * 0.95); - ctx.closePath(); + ctx.arc(cx, cy, rimRadius, 0, Math.PI * 2); ctx.stroke(); - // Inner dot + // ---- Needle ctx.beginPath(); - ctx.arc(ox, oy, size * 0.18, 0, Math.PI * 2); - ctx.fillStyle = stroke; + ctx.moveTo(cx, cy + rimRadius * 0.9); + ctx.lineTo(cx, cy + rimRadius * 0.9 + needleLength); + ctx.stroke(); + + // ---- Needle tip + ctx.beginPath(); + ctx.moveTo(cx - strokeWidth * 0.6, cy + rimRadius * 0.9 + needleLength); + ctx.lineTo(cx, cy + rimRadius * 0.9 + needleLength + strokeWidth * 1.6); + ctx.lineTo(cx + strokeWidth * 0.6, cy + rimRadius * 0.9 + needleLength); + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); + + // ---- Center dot (plastic reflection) + ctx.beginPath(); + ctx.arc(cx, cy, headRadius * 0.18, 0, Math.PI * 2); ctx.fill(); ctx.restore(); diff --git a/web/root/style/editor.scss b/web/root/style/editor.scss index 2a0a13c..452aae6 100644 --- a/web/root/style/editor.scss +++ b/web/root/style/editor.scss @@ -5,6 +5,14 @@ .editor { height: 100%; width: 100%; + +} + +.cm-lineWrapping{ + word-break: keep-all!important; + word-wrap: normal!important; + white-space: nowrap!important; + overflow-wrap: normal!important; } .cm-scroller { diff --git a/web/root/style/terminal.scss b/web/root/style/terminal.scss index ae32e8b..142d7a5 100644 --- a/web/root/style/terminal.scss +++ b/web/root/style/terminal.scss @@ -8,7 +8,6 @@ white-space: pre-wrap; word-break: break-word; height: 100%; - width: 100%; overflow-y: auto; overflow-x: auto; }