summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.tsx4
-rw-r--r--src/css/style.css23
-rw-r--r--src/engine/TheAbstractionEngine.ts2
-rw-r--r--src/engine/config/constants.ts1
-rw-r--r--src/engine/entities/LambdaFactory.ts157
-rw-r--r--src/engine/utils/modal.ts33
-rw-r--r--src/interpreter/parser.ts4
-rw-r--r--src/main.tsx1
8 files changed, 174 insertions, 51 deletions
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 (
<div className="main">
- <div id="modal" className="modal">
- <div className="modal-content">
+ <div id={Miscellaneous.MODAL_ID} className="modal">
+ <div id={Miscellaneous.MODAL_CONTENT_ID} className="modal-content">
<span className="close">&times;</span>
<p>Some text in the Modal..</p>
</div>
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<Range<Decoration>[]>();
+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 onHighlight(direction: Direction) {
- if (direction === Direction.LEFT || direction === Direction.RIGHT) {
- const interaction = () => {
- const spawner = this.getComponent<LambdaSpawn>(
- ComponentNames.LambdaSpawn,
- );
- spawner.spawn(direction);
+ private spawnNewLambda(direction: Direction) {
+ const spawner = this.getComponent<LambdaSpawn>(ComponentNames.LambdaSpawn);
+ spawner.spawn(direction);
- const text = this.getComponent<Text>(ComponentNames.Text);
- text.text = spawner.spawnsLeft.toString();
- this.addComponent(text);
- };
+ const text = this.getComponent<Text>(ComponentNames.Text);
+ text.text = spawner.spawnsLeft.toString();
+ this.addComponent(text);
+ }
- this.addComponent(new Interactable(interaction));
+ private onHighlight(direction: Direction) {
+ if (direction === Direction.LEFT || direction === Direction.RIGHT) {
+ 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<LambdaSpawn>(
+ ComponentNames.LambdaSpawn,
+ );
+ spawner.code = text;
+ this.addComponent(spawner);
+ }
+
modalOpen = false;
closeModal();
- const spawner = this.getComponent<LambdaSpawn>(
- 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(
+ "<div class='code'><div id='code'></div><br><p id='syntax-error' class='error'></p><button id='close-modal'>Save</button></div>",
+ );
+ const spawner = this.getComponent<LambdaSpawn>(
+ 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 `
- <div>
- <textarea id="code" rows="10" cols="50">${code}</textarea>
- <button id="close-modal">Close</button>
- </div>
- `;
- }
}
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 = `<div class="modal-content">${content}</div>`;
- const modalContent = document.querySelector<HTMLElement>(".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<HTMLElement>(".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(<App />);