highlight everything

This commit is contained in:
ParkerTenBroeck 2026-01-14 14:07:06 -05:00
parent 794ac8cdd8
commit bffa67069d
10 changed files with 155 additions and 159 deletions

View file

@ -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;
};

View file

@ -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<Highlight[]>();
export const highlightsField = StateField.define<DecorationSet>({
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<Decoration>();
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,

View file

@ -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[] = [

View file

@ -15,7 +15,6 @@ export type Highlight = {
type HighlightEntry = {
span: Span,
kind: HighlightKind,
count: number;
}
export const current: Map<string, HighlightEntry> = 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;
@ -141,3 +118,7 @@ document.addEventListener("mouseout", (e) => {
export function highlightable(span: Span, text: string, kind?: HighlightKind): string{
return `<span class = "cm-highlight" ${kind ? `highlight-kind="${kind}"`:""} highlight-span="${span[0]}:${span[1]}">${text}</span>`
}
export function highlight_span_attr(span: Span): string{
return `${span[0]}:${span[1]}`
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}

View file

@ -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<string, string> = new Map()
const spanNodeMap: Map<string, string> = new Map()
const nodes = new vis.DataSet<vis.Node>();
const edges = new vis.DataSet<vis.Edge>();
@ -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();

View file

@ -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 {

View file

@ -8,7 +8,6 @@
white-space: pre-wrap;
word-break: break-word;
height: 100%;
width: 100%;
overflow-y: auto;
overflow-x: auto;
}