diff --git a/package-lock.json b/package-lock.json
index 2b68a71..340cfaf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
"name": "the-abstraction-engine",
"version": "0.0.0",
"dependencies": {
+ "codemirror": "^6.0.1",
+ "rainbowbrackets": "github:eriknewland/rainbowbrackets",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
@@ -392,6 +394,82 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@codemirror/autocomplete": {
+ "version": "6.13.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.13.0.tgz",
+ "integrity": "sha512-SuDrho1klTINfbcMPnyro1ZxU9xJtwDMtb62R8TjL/tOl71IoOsvBo1a9x+hDvHhIzkTcJHy2VC+rmpGgYkRSw==",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.17.0",
+ "@lezer/common": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@codemirror/commands": {
+ "version": "6.3.3",
+ "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.3.3.tgz",
+ "integrity": "sha512-dO4hcF0fGT9tu1Pj1D2PvGvxjeGkbC6RGcZw6Qs74TH+Ed1gw98jmUgd2axWvIZEqTeTuFrg1lEB1KV6cK9h1A==",
+ "dependencies": {
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/state": "^6.4.0",
+ "@codemirror/view": "^6.0.0",
+ "@lezer/common": "^1.1.0"
+ }
+ },
+ "node_modules/@codemirror/language": {
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.1.tgz",
+ "integrity": "sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.23.0",
+ "@lezer/common": "^1.1.0",
+ "@lezer/highlight": "^1.0.0",
+ "@lezer/lr": "^1.0.0",
+ "style-mod": "^4.0.0"
+ }
+ },
+ "node_modules/@codemirror/lint": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.5.0.tgz",
+ "integrity": "sha512-+5YyicIaaAZKU8K43IQi8TBy6mF6giGeWAH7N96Z5LC30Wm5JMjqxOYIE9mxwMG1NbhT2mA3l9hA4uuKUM3E5g==",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/search": {
+ "version": "6.5.6",
+ "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.6.tgz",
+ "integrity": "sha512-rpMgcsh7o0GuCDUXKPvww+muLA1pDJaFrpq/CCHtpQJYz8xopu4D1hPcKRoDD0YlF8gZaqTNIRa4VRBWyhyy7Q==",
+ "dependencies": {
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0",
+ "crelt": "^1.0.5"
+ }
+ },
+ "node_modules/@codemirror/state": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.4.1.tgz",
+ "integrity": "sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A=="
+ },
+ "node_modules/@codemirror/view": {
+ "version": "6.25.0",
+ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.25.0.tgz",
+ "integrity": "sha512-XnMGOm6qXB8znzCko0N7k97qZayVdvqpA0JebxA5fHtgBjC/XlCPhH9TK92TahsoCKMPQlaTCUep06Dwj/+GXQ==",
+ "dependencies": {
+ "@codemirror/state": "^6.4.0",
+ "style-mod": "^4.1.0",
+ "w3c-keyname": "^2.2.4"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.19.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
@@ -956,6 +1034,27 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@lezer/common": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.1.tgz",
+ "integrity": "sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ=="
+ },
+ "node_modules/@lezer/highlight": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.0.tgz",
+ "integrity": "sha512-WrS5Mw51sGrpqjlh3d4/fOwpEV2Hd3YOkp9DBt4k8XZQcoTHZFB7sx030A6OcahF4J1nDQAa3jXlTVVYH50IFA==",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
+ "node_modules/@lezer/lr": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.0.tgz",
+ "integrity": "sha512-Wst46p51km8gH0ZUmeNrtpRYmdlRHUpN1DQd3GFAyKANi8WVz8c2jHYTf1CVScFaCjQw1iO3ZZdqGDxQPRErTg==",
+ "dependencies": {
+ "@lezer/common": "^1.0.0"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1641,6 +1740,20 @@
"node": ">=4"
}
},
+ "node_modules/codemirror": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.1.tgz",
+ "integrity": "sha512-J8j+nZ+CdWmIeFIGXEFbFPtpiYacFMDR8GlHK3IyHQJMCaVRfGx9NT+Hxivv1ckLWPvNdZqndbr/7lVhrf/Svg==",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.0.0",
+ "@codemirror/commands": "^6.0.0",
+ "@codemirror/language": "^6.0.0",
+ "@codemirror/lint": "^6.0.0",
+ "@codemirror/search": "^6.0.0",
+ "@codemirror/state": "^6.0.0",
+ "@codemirror/view": "^6.0.0"
+ }
+ },
"node_modules/color-convert": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -1668,6 +1781,11 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
+ "node_modules/crelt": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
+ "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="
+ },
"node_modules/cross-spawn": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -2814,6 +2932,14 @@
}
]
},
+ "node_modules/rainbowbrackets": {
+ "version": "2.0.2",
+ "resolved": "git+ssh://git@github.com/eriknewland/rainbowbrackets.git#9352b73c715aac548690936ac393e0cf6e50cb98",
+ "license": "MIT",
+ "dependencies": {
+ "@codemirror/view": "^6.9.5"
+ }
+ },
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -3039,6 +3165,11 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/style-mod": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
+ "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw=="
+ },
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -3221,6 +3352,11 @@
}
}
},
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index f4b2a67..9ccb2eb 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
+ "codemirror": "^6.0.1",
+ "rainbowbrackets": "github:eriknewland/rainbowbrackets",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
diff --git a/src/App.tsx b/src/App.tsx
index 0ae052f..3f3f67d 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -4,8 +4,8 @@ import { Miscellaneous } from "./engine/config";
export const App = () => {
return (
-
-
+
+
×
Some text in the Modal..
diff --git a/src/css/style.css b/src/css/style.css
index ab76e98..c14682b 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -94,3 +94,26 @@ a:visited {
border-radius: 0.5rem;
margin: 0;
}
+
+.code {
+ width: 100%;
+}
+
+button {
+ padding: 0.5rem;
+ border: none;
+ border-radius: 0.5rem;
+ background-color: var(--yellow);
+ color: var(--bg);
+ cursor: pointer;
+ transition: background 0.2s ease-in-out;
+}
+
+button:hover {
+ background-color: var(--blue);
+}
+
+.syntax-error {
+ color: var(--red);
+ background-color: var(--yellow);
+}
diff --git a/src/engine/TheAbstractionEngine.ts b/src/engine/TheAbstractionEngine.ts
index 30c3422..09419ff 100644
--- a/src/engine/TheAbstractionEngine.ts
+++ b/src/engine/TheAbstractionEngine.ts
@@ -58,7 +58,7 @@ export class TheAbstractionEngine {
const wall = new Wall({ x: 5, y: 3 });
this.game.addEntity(wall);
- const factory = new LambdaFactory({ x: 6, y: 6 }, "(λ (x) . x)", 10);
+ const factory = new LambdaFactory({ x: 3, y: 3 }, "(λ (x) . x)", 10);
this.game.addEntity(factory);
const lockedDoor = new LockedDoor({ x: 8, y: 8 });
diff --git a/src/engine/config/constants.ts b/src/engine/config/constants.ts
index c6b592e..89f7e92 100644
--- a/src/engine/config/constants.ts
+++ b/src/engine/config/constants.ts
@@ -60,5 +60,6 @@ export namespace Miscellaneous {
export const GRID_CELL_HEIGHT = Math.floor(HEIGHT / GRID_ROWS);
export const MODAL_ID = "modal";
+ export const MODAL_CONTENT_ID = "modal-content";
export const CANVAS_ID = "canvas";
}
diff --git a/src/engine/entities/LambdaFactory.ts b/src/engine/entities/LambdaFactory.ts
index 0721f80..d8fc7a2 100644
--- a/src/engine/entities/LambdaFactory.ts
+++ b/src/engine/entities/LambdaFactory.ts
@@ -19,12 +19,42 @@ import {
} from "../components";
import { Coord2D, Direction } from "../interfaces";
import { openModal, closeModal } from "../utils";
+import { EditorState, StateField, StateEffect, Range } from "@codemirror/state";
+import { Decoration, EditorView, keymap } from "@codemirror/view";
+import { defaultKeymap } from "@codemirror/commands";
+import rainbowBrackets from "rainbowbrackets";
+import { basicSetup } from "codemirror";
+import { parse } from "../../interpreter";
+
+interface CodeEditorState {
+ view: EditorView;
+ editorElement: HTMLElement;
+}
+
+const highlightEffect = StateEffect.define
[]>();
+const highlightExtension = StateField.define({
+ create() {
+ return Decoration.none;
+ },
+ update(value, transaction) {
+ value = value.map(transaction.changes);
+
+ for (let effect of transaction.effects) {
+ if (effect.is(highlightEffect))
+ value = value.update({ add: effect.value, sort: true });
+ }
+
+ return value;
+ },
+ provide: (f) => EditorView.decorations.from(f),
+});
export class LambdaFactory extends Entity {
private static spriteSpec: SpriteSpec = SPRITE_SPECS.get(
Sprites.LAMBDA_FACTORY,
) as SpriteSpec;
+ private codeEditorState: CodeEditorState | null;
private code: string;
private spawns: number;
@@ -33,6 +63,7 @@ export class LambdaFactory extends Entity {
this.code = code;
this.spawns = spawns;
+ this.codeEditorState = null;
this.addComponent(
new BoundingBox(
@@ -82,36 +113,86 @@ export class LambdaFactory extends Entity {
this.removeComponent(ComponentNames.Interactable);
}
+ private spawnNewLambda(direction: Direction) {
+ const spawner = this.getComponent(ComponentNames.LambdaSpawn);
+ spawner.spawn(direction);
+
+ const text = this.getComponent(ComponentNames.Text);
+ text.text = spawner.spawnsLeft.toString();
+ this.addComponent(text);
+ }
+
private onHighlight(direction: Direction) {
if (direction === Direction.LEFT || direction === Direction.RIGHT) {
- const interaction = () => {
- const spawner = this.getComponent(
- ComponentNames.LambdaSpawn,
- );
- spawner.spawn(direction);
-
- const text = this.getComponent(ComponentNames.Text);
- text.text = spawner.spawnsLeft.toString();
- this.addComponent(text);
- };
-
- this.addComponent(new Interactable(interaction));
+ this.addComponent(new Interactable(() => this.spawnNewLambda(direction)));
return;
}
let modalOpen = false;
+ let editorView: EditorView | null = null;
+
+ const syntaxErrorDecoration = Decoration.mark({
+ class: "syntax-error",
+ });
+
const close = () => {
+ if (editorView) {
+ const text = editorView.state.doc.toString();
+
+ // remove all text from the editor
+ editorView.dispatch({
+ changes: {
+ from: 0,
+ to: text.length,
+ insert: "",
+ },
+ });
+ // add the new text to the editor
+ editorView.dispatch({
+ changes: {
+ from: 0,
+ to: 0,
+ insert: text,
+ },
+ });
+
+ try {
+ parse(text);
+ } catch (e: any) {
+ if (!e.location) {
+ return;
+ }
+ const {
+ location: {
+ start: { offset: start },
+ end: { offset: end },
+ },
+ } = e;
+
+ editorView.dispatch({
+ effects: highlightEffect.of([
+ syntaxErrorDecoration.range(
+ start === end ? start - 1 : start,
+ end,
+ ),
+ ]),
+ });
+
+ document.getElementById("syntax-error")!.innerText = e.message;
+
+ return;
+ }
+
+ const spawner = this.getComponent(
+ ComponentNames.LambdaSpawn,
+ );
+ spawner.code = text;
+ this.addComponent(spawner);
+ }
+
modalOpen = false;
closeModal();
- const spawner = this.getComponent(
- ComponentNames.LambdaSpawn,
- );
- spawner.code = (
- document.getElementById("code") as HTMLTextAreaElement
- ).value;
- this.addComponent(spawner);
-
document.getElementById(Miscellaneous.CANVAS_ID)!.focus();
return;
};
@@ -121,22 +202,36 @@ export class LambdaFactory extends Entity {
close();
return;
}
-
modalOpen = true;
- openModal(this.codeEditor(this.code));
+
+ openModal(
+ "",
+ );
+ const spawner = this.getComponent(
+ ComponentNames.LambdaSpawn,
+ );
+
+ const startState = EditorState.create({
+ doc: spawner.code,
+ extensions: [
+ basicSetup,
+ keymap.of(defaultKeymap),
+ rainbowBrackets(),
+ highlightExtension,
+ ],
+ });
+
+ const codeBox = document.getElementById("code")!;
+ editorView = new EditorView({
+ state: startState,
+ parent: codeBox,
+ });
+
+ editorView.focus();
document.getElementById("close-modal")!.addEventListener("click", close);
};
this.addComponent(new Interactable(interaction));
}
-
- private codeEditor(code: string) {
- return `
-
-
-
-
- `;
- }
}
diff --git a/src/engine/utils/modal.ts b/src/engine/utils/modal.ts
index e7b36b1..48afae8 100644
--- a/src/engine/utils/modal.ts
+++ b/src/engine/utils/modal.ts
@@ -2,34 +2,37 @@ import { Miscellaneous } from "../config";
let modalOpen = false;
-export const openModal = (content: string, id = Miscellaneous.MODAL_ID) => {
+export const openModal = (
+ content: string,
+ id = Miscellaneous.MODAL_ID,
+ contentId = Miscellaneous.MODAL_CONTENT_ID,
+) => {
const modal = document.getElementById(id);
- if (modal && !modalOpen) {
+ const modalContent = document.getElementById(contentId);
+ if (modal && !modalOpen && modalContent) {
modal.style.display = "flex";
modal.style.animation = "fadeIn 0.25s";
- modal.innerHTML = `${content}
`;
- const modalContent = document.querySelector(".modal-content");
- if (modalContent) {
- modalContent.style.animation = "scaleUp 0.25s";
- }
+ modalContent.innerHTML = content;
+ modalContent.style.animation = "scaleUp 0.25s";
modalOpen = true;
}
};
-export const closeModal = (id = Miscellaneous.MODAL_ID) => {
+export const closeModal = (
+ id = Miscellaneous.MODAL_ID,
+ contentId = Miscellaneous.MODAL_CONTENT_ID,
+) => {
const modal = document.getElementById(id);
- if (modal && modalOpen) {
- modal.style.animation = "fadeOut 0.25s";
+ const modalContent = document.getElementById(contentId);
- const modalContent = document.querySelector(".modal-content");
- if (modalContent) {
- modalContent.style.animation = "scaleDown 0.25s";
- }
+ if (modal && modalOpen && modalContent) {
+ modal.style.animation = "fadeOut 0.25s";
+ modalContent.style.animation = "scaleDown 0.25s";
setTimeout(() => {
- modal.innerHTML = "";
+ modalContent.innerHTML = "";
modal.style.display = "none";
modalOpen = false;
diff --git a/src/interpreter/parser.ts b/src/interpreter/parser.ts
index ea07796..5e3be0f 100644
--- a/src/interpreter/parser.ts
+++ b/src/interpreter/parser.ts
@@ -30,6 +30,6 @@ export const isVariable = (term: LambdaTerm): term is Variable => {
return typeof term === "string";
};
-export const parse = (term: string) => {
- return peggyParser.parse(term, { library: true });
+export const parse = (term: string, library = false) => {
+ return peggyParser.parse(term, { peg$library: library });
};
diff --git a/src/main.tsx b/src/main.tsx
index 7404467..8191a98 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,4 +1,5 @@
import ReactDOM from "react-dom/client";
import { App } from "./App.tsx";
import "./css/style.css";
+
ReactDOM.createRoot(document.getElementById("root")!).render();