some form of highlighting

This commit is contained in:
ParkerTenBroeck 2026-01-12 17:00:13 -05:00
parent 58fb1b956c
commit 7f519cd7f3
12 changed files with 209 additions and 114 deletions

View file

@ -176,8 +176,8 @@ impl<'a, 'b> FaCompiler<'a, 'b> {
self.ctx.emit_error(format!("unknown item {name:?}, expected states | alphabet | final states | initial state"), dest_s); self.ctx.emit_error(format!("unknown item {name:?}, expected states | alphabet | final states | initial state"), dest_s);
} }
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => {
self.compile_transition_function(args, list) self.compile_transition_function(args, func, list)
} }
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
self.ctx.emit_error( self.ctx.emit_error(
@ -313,6 +313,7 @@ impl<'a, 'b> FaCompiler<'a, 'b> {
fn compile_transition_function( fn compile_transition_function(
&mut self, &mut self,
args: Spanned<ast::Tuple<'a>>, args: Spanned<ast::Tuple<'a>>,
function: Span,
list: Spanned<ast::Item<'a>>, list: Spanned<ast::Item<'a>>,
) { ) {
let list = list.set_weak(); let list = list.set_weak();
@ -368,8 +369,7 @@ impl<'a, 'b> FaCompiler<'a, 'b> {
} }
if let Some(previous) = entry.replace(TransitionTo { if let Some(previous) = entry.replace(TransitionTo {
state: State(next_state.0), state: State(next_state.0),
function,
function: args.1,
transition: item.1, transition: item.1,
}) { }) {
self.ctx self.ctx

View file

@ -264,8 +264,8 @@ impl<'a, 'b> PdaCompiler<'a, 'b> {
self.ctx.emit_error(format!("unknown item {name:?}, expected states | stack symbols | alphabet | accept by | final states | initial state | initial stack"), dest_s); self.ctx.emit_error(format!("unknown item {name:?}, expected states | stack symbols | alphabet | accept by | final states | initial state | initial stack"), dest_s);
} }
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => {
self.compile_transition_function(args, list) self.compile_transition_function(args, func, list)
} }
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
self.ctx.emit_error( self.ctx.emit_error(
@ -476,6 +476,7 @@ impl<'a, 'b> PdaCompiler<'a, 'b> {
fn compile_transition_function( fn compile_transition_function(
&mut self, &mut self,
args: Spanned<ast::Tuple<'a>>, args: Spanned<ast::Tuple<'a>>,
function: Span,
list: Spanned<ast::Item<'a>>, list: Spanned<ast::Item<'a>>,
) { ) {
let list = list.set_weak(); let list = list.set_weak();
@ -560,7 +561,7 @@ impl<'a, 'b> PdaCompiler<'a, 'b> {
state: State(next_state.0), state: State(next_state.0),
stack, stack,
function: args.1, function,
transition: item.1, transition: item.1,
}) { }) {
self.ctx.emit_warning("duplicate transition", item.1); self.ctx.emit_warning("duplicate transition", item.1);

View file

@ -193,8 +193,8 @@ impl<'a, 'b> TmCompiler<'a, 'b> {
self.ctx.emit_error(format!("unknown item {name:?}, expected states | symbols | final states | initial state | blank symbol"), dest_s); self.ctx.emit_error(format!("unknown item {name:?}, expected states | symbols | final states | initial state | blank symbol"), dest_s);
} }
TL::TransitionFunc(S((S(delta_lower!(pat), _), args), _), list) => { TL::TransitionFunc(S((S(delta_lower!(pat), _), args), func), list) => {
self.compile_transition_function(args, list) self.compile_transition_function(args, func, list)
} }
TL::TransitionFunc(S((S(name, _), _), dest_s), _) => { TL::TransitionFunc(S((S(name, _), _), dest_s), _) => {
self.ctx.emit_error( self.ctx.emit_error(
@ -349,6 +349,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> {
fn compile_transition_function( fn compile_transition_function(
&mut self, &mut self,
args: Spanned<ast::Tuple<'a>>, args: Spanned<ast::Tuple<'a>>,
function: Span,
list: Spanned<ast::Item<'a>>, list: Spanned<ast::Item<'a>>,
) { ) {
let list = list.set_weak(); let list = list.set_weak();
@ -398,7 +399,7 @@ impl<'a, 'b> TmCompiler<'a, 'b> {
symbol: Symbol(to_tape.0), symbol: Symbol(to_tape.0),
direction: direction.0, direction: direction.0,
function: args.1, function,
transition: item.1, transition: item.1,
}) { }) {
self.ctx.emit_warning("duplicate transition", item.1); self.ctx.emit_warning("duplicate transition", item.1);

View file

@ -1,10 +1,11 @@
// deno-lint-ignore-file // deno-lint-ignore-file
import type { Machine } from "./automata.ts"; import type { Machine, Span } from "./automata.ts";
import type { Example } from "./examples.ts"; import type { Example } from "./examples.ts";
import type { Sim, SimStepResult } from "./simulation.ts"; import type { Sim, SimStepResult } from "./simulation.ts";
import type wasm from "./wasm.ts"; import type wasm from "./wasm.ts";
import type { Text } from "npm:@codemirror/state"; import type { Text } from "npm:@codemirror/state";
import type { Highlight } from "./highlight.ts";
type Unsubscribe = () => void; type Unsubscribe = () => void;
@ -69,14 +70,14 @@ type AppEvents = {
"editor/change": {text: string, doc: Text}; "editor/change": {text: string, doc: Text};
"compiled": {log: wasm.CompileLog[], ansi_log: string, machine: string|undefined}; "compiled": {log: wasm.CompileLog[], ansi_log: string, machine: string|undefined};
"automata/sim/update": { simulation: Sim|null }; "automata/sim/update": Sim|null;
"automata/sim/before_step": { simulation: Sim }; "automata/sim/before_step": { simulation: Sim };
"automata/sim/after_step": { simulation: Sim, result: SimStepResult }; "automata/sim/after_step": { simulation: Sim, result: SimStepResult };
"automata/update": { automaton: Machine }; "automata/update": Machine;
"example/selected": {example: Example}; "example/selected": Example;
"controls/editor/set_text": {text: string}; "controls/editor/set_text": string;
"controls/vis/physics": {enabled: boolean}; "controls/vis/physics": {enabled: boolean};
"controls/vis/reset_network": void; "controls/vis/reset_network": void;
@ -85,6 +86,12 @@ type AppEvents = {
"controls/sim/reload": void; "controls/sim/reload": void;
"controls/sim/clear": void; "controls/sim/clear": void;
"highlight/one/add": Highlight;
"highlight/one/remove": Highlight;
"highlight/all/remove": void;
"highlight/update": void;
"theme/update": void; "theme/update": void;
}; };

View file

@ -89,7 +89,7 @@ class Controls {
if (Controls.running) Controls.setRunning(false); if (Controls.running) Controls.setRunning(false);
}); });
bus.on("automata/sim/update", ({ simulation }) => { bus.on("automata/sim/update", simulation => {
Controls.simulation_active = !!simulation; Controls.simulation_active = !!simulation;
if (!simulation) Controls.stop(); if (!simulation) Controls.stop();
}); });

View file

@ -25,6 +25,7 @@ import wasm from "./wasm.ts";
import { Share } from "./share.ts"; import { Share } from "./share.ts";
import { examples } from "./examples.ts"; import { examples } from "./examples.ts";
import { bus } from "./bus.ts"; import { bus } from "./bus.ts";
import { current, Highlight, HighlightKind } from "./highlight.ts";
function tokenize(text: string): wasm.Tok[] { function tokenize(text: string): wasm.Tok[] {
try { try {
@ -47,25 +48,16 @@ function compile(
} }
export type HighlightKind = "focus" | "success" | "warning" | "error";
export type HighlightSpan = {
from: number;
to: number;
kind: HighlightKind;
};
function decoForKind(kind: HighlightKind) { function decoForKind(kind: HighlightKind) {
// Use a class per kind so each gets a distinct color via CSS // Use a class per kind so each gets a distinct color via CSS
return Decoration.mark({ class: `cm-highlight cm-highlight-${kind}` }); return Decoration.mark({ class: `cm-highlight cm-highlight-${kind}` });
} }
export function applyHighlights(view: EditorView, spans: HighlightSpan[]) { bus.on("highlight/update", _ => {
view.dispatch({ effects: setHighlights.of(spans) }); 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<Highlight[]>();
export const highlightsField = StateField.define<DecorationSet>({ export const highlightsField = StateField.define<DecorationSet>({
create() { create() {
return Decoration.none; return Decoration.none;
@ -73,7 +65,7 @@ export const highlightsField = StateField.define<DecorationSet>({
update(highlights, tr) { update(highlights, tr) {
// Keep highlights aligned with document edits // Keep highlights aligned with document edits
// highlights = highlights.map(tr.changes); highlights = highlights.map(tr.changes);
for (const e of tr.effects) { for (const e of tr.effects) {
if (e.is(setHighlights)) { if (e.is(setHighlights)) {
@ -81,8 +73,9 @@ export const highlightsField = StateField.define<DecorationSet>({
const builder = new RangeSetBuilder<Decoration>(); const builder = new RangeSetBuilder<Decoration>();
for (const s of spans) { for (const s of spans) {
const from = Math.max(0, Math.min(s.from, tr.state.doc.length));
const to = Math.max(0, Math.min(s.to, tr.state.doc.length)); 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)); if (to > from) builder.add(from, to, decoForKind(s.kind));
} }
highlights = builder.finish(); highlights = builder.finish();
@ -95,7 +88,6 @@ export const highlightsField = StateField.define<DecorationSet>({
provide: (f) => EditorView.decorations.from(f), provide: (f) => EditorView.decorations.from(f),
}); });
export const setHighlights = StateEffect.define<HighlightSpan[]>();
const eventBusConnection = StateField.define({ const eventBusConnection = StateField.define({
create(state) { create(state) {
@ -264,23 +256,15 @@ const editor = new EditorView({
bus.on( bus.on(
"begin", "begin",
(_) => bus.emit("controls/editor/set_text", { text: defaultText() }), (_) => bus.emit("controls/editor/set_text", defaultText()),
); );
bus.on("controls/editor/set_text", ({ text }) => { bus.on("controls/editor/set_text", text => {
editor.dispatch({ editor.dispatch({
changes: { from: 0, to: editor.state.doc.length, insert: text }, changes: { from: 0, to: editor.state.doc.length, insert: text },
}); });
}); });
bus.on("example/selected", ({ example }) => { bus.on("example/selected", example => {
bus.emit("controls/editor/set_text", { text: example.machine }); bus.emit("controls/editor/set_text", example.machine);
}); });
applyHighlights(editor, [
{ from: 0, to: 10, kind: "focus" },
{ from: 10, to: 20, kind: "success" },
{ from: 20, to: 30, kind: "warning" },
{ from: 30, to: 40, kind: "error" },
])

View file

@ -245,5 +245,5 @@ function buildExamplesDropdown(
const selectEl = document.getElementById("exampleSelect") as HTMLSelectElement; const selectEl = document.getElementById("exampleSelect") as HTMLSelectElement;
buildExamplesDropdown(selectEl, examples, (example) => { buildExamplesDropdown(selectEl, examples, (example) => {
bus.emit("example/selected", {example}); bus.emit("example/selected", example);
}); });

View file

85
web/root/src/highlight.ts Normal file
View file

@ -0,0 +1,85 @@
import type { Span } from "./automata.ts";
import { bus } from "./bus.ts";
import { automaton } from "./simulation.ts";
export type HighlightKind = "focus" | "error" | "warning" | "success";
export type Highlight = {
span: Span,
kind: HighlightKind,
}
type HighlightEntry = {
span: Span,
kind: HighlightKind,
count: number;
}
export const current: Map<string, HighlightEntry> = new Map();
function asKey(highlight: Highlight): string {
return `${highlight.span[0]}:${highlight.span[1]}:${highlight.kind}`
}
export function highlight_from_node_id(node_id: string) {
const state = automaton.states.get(node_id);
if (state) {
bus.emit("highlight/one/add", { kind: "success", span: state.definition })
}
}
export function dehighlight_from_node_id(node_id: string) {
const state = automaton.states.get(node_id);
if (state) {
bus.emit("highlight/one/remove", { kind: "success", span: state.definition })
}
}
export function highlight_from_edge_id(node_id: string) {
for (const edge_value of automaton.edges.get(node_id)!) {
bus.emit("highlight/one/add", { kind: "focus", span: edge_value.function })
bus.emit("highlight/one/add", { kind: "warning", span: edge_value.transition })
}
}
export function dehighlight_from_edge_id(node_id: string) {
for (const edge_value of automaton.edges.get(node_id)!) {
bus.emit("highlight/one/remove", { kind: "focus", span: edge_value.function })
bus.emit("highlight/one/remove", { kind: "warning", span: edge_value.transition })
}
}
bus.on("automata/update", _ => {
bus.emit("highlight/all/remove", undefined);
})
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 });
bus.emit("highlight/update", undefined);
}
});
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);
bus.emit("highlight/update", undefined);
}
}
});
bus.on("highlight/all/remove", (_) => {
if (current.size !== 0) {
current.clear();
bus.emit("highlight/update", undefined);
}
});

View file

@ -11,8 +11,9 @@ import {parse_machine_from_json} from "./automata.ts";
export type SimStepResult = "pending" | "accept" | "reject"; export type SimStepResult = "pending" | "accept" | "reject";
export type Sim = FaSim | PdaSim | TmSim; export type Sim = FaSim | PdaSim | TmSim;
let simulation: Sim | null = null;
let automaton: Machine = { export let simulation: Sim | null = null;
export let automaton: Machine = {
type: "fa", type: "fa",
alphabet: new Map(), alphabet: new Map(),
final_states: new Map(), final_states: new Map(),
@ -28,7 +29,7 @@ bus.on("compiled", ({ machine }) => {
try { try {
bus.emit("controls/sim/clear", undefined); bus.emit("controls/sim/clear", undefined);
automaton = parse_machine_from_json(machine); automaton = parse_machine_from_json(machine);
bus.emit("automata/update", { automaton }); bus.emit("automata/update", automaton);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
} }
@ -36,7 +37,7 @@ bus.on("compiled", ({ machine }) => {
}); });
bus.on("controls/sim/clear", (_) => { bus.on("controls/sim/clear", (_) => {
simulation = null; simulation = null;
bus.emit("automata/sim/update", { simulation: null }); bus.emit("automata/sim/update", null);
}); });
bus.on("controls/sim/step", (_) => { bus.on("controls/sim/step", (_) => {
if (simulation) { if (simulation) {
@ -48,7 +49,7 @@ bus.on("controls/sim/step", (_) => {
} }
}); });
const machineInput = document.getElementById("machineInput") as HTMLInputElement; const machineInput = document.getElementById("machineInput") as HTMLInputElement;
machineInput.addEventListener("input", () => bus.emit("automata/sim/update", {simulation: null})); machineInput.addEventListener("input", () => bus.emit("controls/sim/clear", undefined));
machineInput.addEventListener("keydown", (e) => { machineInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
bus.emit("controls/sim/reload", undefined) bus.emit("controls/sim/reload", undefined)
@ -67,10 +68,10 @@ bus.on("controls/sim/reload", (_) => {
simulation = new TmSim(automaton as Tm, input); simulation = new TmSim(automaton as Tm, input);
break; break;
} }
bus.emit("automata/sim/update", { simulation }); bus.emit("automata/sim/update", simulation);
}); });
const simulationStatus = document.getElementById("simulationStatus") as HTMLInputElement; const simulationStatus = document.getElementById("simulationStatus") as HTMLInputElement;
bus.on("automata/sim/update", ({simulation}) => { bus.on("automata/sim/update", simulation => {
if (!simulation){ if (!simulation){
simulationStatus.innerText = "N/A" simulationStatus.innerText = "N/A"
simulationStatus.style.color = "var(--fg-2)"; simulationStatus.style.color = "var(--fg-2)";

View file

@ -3,8 +3,8 @@
import * as vis from "npm:vis-network/standalone"; import * as vis from "npm:vis-network/standalone";
import { bus } from "./bus.ts"; import { bus } from "./bus.ts";
import type { Sim } from "./simulation.ts"; import { automaton, simulation } from "./simulation.ts";
import type { Machine } from "./automata.ts"; import { dehighlight_from_edge_id, dehighlight_from_node_id, highlight_from_edge_id, highlight_from_node_id } from "./highlight.ts";
bus.on("controls/vis/physics", ({ enabled }) => { bus.on("controls/vis/physics", ({ enabled }) => {
@ -29,16 +29,12 @@ bus.on("automata/sim/after_step", _ => {
network.redraw(); network.redraw();
}); });
let simulation: Sim | null = null; bus.on("automata/sim/update", _ => {
bus.on("automata/sim/update", ({simulation: sim}) => {
simulation = sim;
network.redraw(); network.redraw();
}); });
let automaton: Machine bus.on("automata/update", automaton => {
bus.on("automata/update", ({automaton: auto}) => {
automaton = auto;
// Populate nodes // Populate nodes
for (const state of automaton.states.keys()) { for (const state of automaton.states.keys()) {
@ -238,22 +234,6 @@ function measureTextWidth(text: string, font: string): number {
return ctx.measureText(text).width; return ctx.measureText(text).width;
} }
function chosen_edge(
_: vis.ChosenNodeValues,
id: vis.IdType,
selected: boolean,
hovered: boolean,
) {
}
function chosen_node(
_: vis.ChosenNodeValues,
id: vis.IdType,
selected: boolean,
hovered: boolean,
) {
}
const network: vis.Network = createGraph(); const network: vis.Network = createGraph();
function createGraph(): vis.Network { function createGraph(): vis.Network {
@ -286,16 +266,9 @@ function createGraph(): vis.Network {
shape: "custom", shape: "custom",
size: 18, size: 18,
// @ts-expect-error bad library // @ts-expect-error bad library
chosen: {
node: chosen_node,
},
ctxRenderer: renderNode, ctxRenderer: renderNode,
}, },
edges: { edges: {
chosen: {
// @ts-expect-error bad library
edge: chosen_edge,
},
arrowStrikethrough: false, arrowStrikethrough: false,
arrows: "to", arrows: "to",
}, },
@ -311,6 +284,49 @@ function createGraph(): vis.Network {
} }
}); });
network.on("hoverEdge", ({ edge }: { edge: string }) => {
highlight_from_edge_id(edge)
});
network.on('blurEdge', ({edge}: {edge: string}) => {
dehighlight_from_edge_id(edge)
});
network.on("hoverNode", ({ node }: { node: string }) => {
highlight_from_node_id(node);
});
network.on('blurNode', ({ node }: { node: string }) => {
dehighlight_from_node_id(node)
});
network.on("selectEdge", item => {
const id = network.getEdgeAt(item.pointer.DOM);
if(id)highlight_from_edge_id(id as string);
});
network.on('deselectEdge', item => {
console.log(item);
for (const edge of item.previousSelection.edges){
console.log(edge);
dehighlight_from_edge_id(edge.id)
}
});
network.on("selectNode", item => {
const id = network.getNodeAt(item.pointer.DOM);
if(id)highlight_from_node_id(id as string);
});
network.on('deselectNode', item => {
console.log(item);
for (const node of item.previousSelection.nodes){
console.log(node);
dehighlight_from_node_id(node.id)
}
});
return network; return network;
} }

View file

@ -136,7 +136,7 @@
.cm-highlight { .cm-highlight {
border-radius: 4px; border-radius: 4px;
padding: 0 1px; // padding: 0 1px;
} }
.cm-highlight-warning { background: color-mix(in srgb, var(--warning) 40%, var(--bg-0)); } .cm-highlight-warning { background: color-mix(in srgb, var(--warning) 40%, var(--bg-0)); }