diff options
author | Elizabeth Hunt <elizabeth.hunt@simponic.xyz> | 2024-03-04 16:31:27 -0700 |
---|---|---|
committer | Elizabeth Hunt <elizabeth.hunt@simponic.xyz> | 2024-03-04 16:31:27 -0700 |
commit | d74523d15d37a1809a797beda962c957fba181b2 (patch) | |
tree | f5a7e7866940e477602249da8191d75988784ee7 | |
parent | f4ad269f8b9f9b0bfcc60bb2584316c3fdd10d10 (diff) | |
download | the-abstraction-engine-d74523d15d37a1809a797beda962c957fba181b2.tar.gz the-abstraction-engine-d74523d15d37a1809a797beda962c957fba181b2.zip |
checkpoint
-rw-r--r-- | package-lock.json | 136 | ||||
-rw-r--r-- | package.json | 2 | ||||
-rw-r--r-- | src/App.tsx | 4 | ||||
-rw-r--r-- | src/css/style.css | 23 | ||||
-rw-r--r-- | src/engine/TheAbstractionEngine.ts | 2 | ||||
-rw-r--r-- | src/engine/config/constants.ts | 1 | ||||
-rw-r--r-- | src/engine/entities/LambdaFactory.ts | 157 | ||||
-rw-r--r-- | src/engine/utils/modal.ts | 33 | ||||
-rw-r--r-- | src/interpreter/parser.ts | 4 | ||||
-rw-r--r-- | src/main.tsx | 1 |
10 files changed, 312 insertions, 51 deletions
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 ( <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">×</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 />); |