diff --git a/package.json b/package.json
index 8813d940c7..1c65720e10 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"dotenv": "^16.3.1",
"firebase-admin": "^11.10.1",
"js-beautify": "^1.15.1",
+ "js-levenshtein": "^1.1.6",
"loops": "^1.0.1",
"ms": "^2.1.3",
"nanoid": "^4.0.1",
@@ -59,6 +60,7 @@
"@types/esprima": "^4.0.3",
"@types/grecaptcha": "^3.0.4",
"@types/js-beautify": "^1.14.3",
+ "@types/js-levenshtein": "^1.1.3",
"@types/node-statsd": "^0.1.6",
"@types/three": "^0.149.0",
"@types/throttle-debounce": "^5.0.0",
diff --git a/src/components/codemirror-widgets/open-button.tsx b/src/components/codemirror-widgets/open-button.tsx
index ede25dd23c..18280cbcbb 100644
--- a/src/components/codemirror-widgets/open-button.tsx
+++ b/src/components/codemirror-widgets/open-button.tsx
@@ -6,30 +6,17 @@ import { runGameHeadless } from '../../lib/engine'
interface OpenButtonProps {
kind: EditorKind
text: string
+ range: { from: number, to: number },
}
export default function OpenButton(props: OpenButtonProps) {
const { label, icon: Icon } = editors[props.kind]
-
return (
)
-}
\ No newline at end of file
+}
diff --git a/src/components/popups-etc/editor-modal.tsx b/src/components/popups-etc/editor-modal.tsx
index 6078ca51d6..025e06e6a2 100644
--- a/src/components/popups-etc/editor-modal.tsx
+++ b/src/components/popups-etc/editor-modal.tsx
@@ -3,14 +3,15 @@ import { useEffect } from 'preact/hooks'
import { IoClose } from 'react-icons/io5'
import tinykeys from 'tinykeys'
import { usePopupCloseClick } from '../../lib/utils/popup-close-click'
-import { codeMirror, editors, openEditor } from '../../lib/state'
+import { codeMirror, editors, openEditor, codeMirrorEditorText, _foldRanges, _widgets, OpenEditor } from '../../lib/state'
import styles from './editor-modal.module.css'
+import levenshtein from 'js-levenshtein'
+import { runGameHeadless } from '../../lib/engine'
export default function EditorModal() {
const Content = openEditor.value ? editors[openEditor.value.kind].modalContent : () => null
const text = useSignal(openEditor.value?.text ?? '')
- // Sync code changes with editor text
useSignalEffect(() => {
if (openEditor.value) text.value = openEditor.value.text
})
@@ -29,7 +30,7 @@ export default function EditorModal() {
insert: _text
}
})
-
+
openEditor.value = {
..._openEditor,
text: _text,
@@ -40,6 +41,59 @@ export default function EditorModal() {
}
})
+ // the challenge now is making the editor keep track of what map editor it's currently focused on and streaming the changes in the map editor
+ // it's tricky because maps can grow and shrink
+
+ useEffect(() => {
+ // just do this to sync the editor text with the code mirror text
+
+ const code = codeMirror.value?.state.doc.toString() ?? '';
+ const levenshtainDistances = _foldRanges.value.map((foldRange, foldRangeIndex) => {
+ const widgetKind = _widgets.value[foldRangeIndex]?.value.spec.widget.props.kind;
+
+ // if the widget kind is not the same as the open editor kind, don't do anything
+ if (widgetKind !== openEditor.value?.kind) return -1;
+
+ const theCode = code.slice(foldRange?.from, foldRange?.to);
+
+ const distance = levenshtein(text.value, theCode)
+ return distance;
+ });
+
+
+
+ // if (levenshtainDistances.length === 0) alert(`You are currently editing a deleted ${openEditor.value?.kind}`);
+ if (levenshtainDistances.length === 0) return;
+
+ // compute the index of the min distance
+ let indexOfMinDistance = 0;
+ levenshtainDistances.forEach((distance, didx) => {
+ if (levenshtainDistances[indexOfMinDistance]! < 0) indexOfMinDistance = didx;
+ const min = levenshtainDistances[indexOfMinDistance]!;
+ if (distance >= 0 && distance <= min) indexOfMinDistance = didx;
+ });
+
+ // update the open editor if the index is not -1
+ if (indexOfMinDistance !== -1) {
+ const editRange = _foldRanges.value[indexOfMinDistance]
+ const openEditorCode = code.slice(editRange?.from, editRange?.to)
+
+ // the map editor needs to get the bitmaps after running the code
+ if (openEditor.value?.kind === 'map') runGameHeadless(code ?? '');
+
+ openEditor.value = {
+ ...openEditor.value as OpenEditor,
+ editRange: {
+ from: editRange!.from,
+ to: editRange!.to
+ },
+ text: openEditorCode
+ }
+ }
+
+ }, [codeMirrorEditorText.value]);
+
+
usePopupCloseClick(styles.content!, () => openEditor.value = null, !!openEditor.value)
useEffect(() => tinykeys(window, {
'Escape': () => openEditor.value = null
@@ -54,4 +108,4 @@ export default function EditorModal() {
)
-}
\ No newline at end of file
+}
diff --git a/src/lib/codemirror/init.ts b/src/lib/codemirror/init.ts
index e24aba3c11..f8d5b1f77a 100644
--- a/src/lib/codemirror/init.ts
+++ b/src/lib/codemirror/init.ts
@@ -10,6 +10,7 @@ import SearchBox from '../../components/search-box'
import { EditorView, keymap, lineNumbers, highlightActiveLineGutter, highlightSpecialChars, drawSelection, dropCursor, rectangularSelection, crosshairCursor, highlightActiveLine } from '@codemirror/view'
import { lintGutter } from "@codemirror/lint";
import type { NormalizedError } from '../state'
+import { codeMirrorEditorText } from '../state'
export function diagnosticsFromErrorLog(view: EditorView, errorLog: NormalizedError[]) {
return errorLog.filter(error => error.line)
@@ -25,6 +26,10 @@ export function diagnosticsFromErrorLog(view: EditorView, errorLog: NormalizedEr
}
export const initialExtensions = (onUpdate: any, onRunShortcut: any, yCollab?: any) => ([
+ EditorView.updateListener.of(update => {
+ const newEditorText = update.state.doc.toString();
+ codeMirrorEditorText.value = newEditorText;
+ }),
lintGutter(),
lineNumbers(),
highlightActiveLineGutter(),
diff --git a/src/lib/codemirror/widgets.ts b/src/lib/codemirror/widgets.ts
index 906b2976c5..92a0495ba9 100644
--- a/src/lib/codemirror/widgets.ts
+++ b/src/lib/codemirror/widgets.ts
@@ -5,7 +5,7 @@ import { palette } from '../../../engine/src/base'
import { FromTo, getTag, makeWidget } from './util'
import OpenButton from '../../components/codemirror-widgets/open-button'
import Swatch from '../../components/codemirror-widgets/swatch'
-import { editorKinds, editors } from '../state'
+import { editorKinds, editors, _foldRanges, _widgets } from '../state'
const OpenButtonWidget = makeWidget(OpenButton)
const SwatchWidget = makeWidget(Swatch)
@@ -21,10 +21,11 @@ function makeValue(state: EditorState) {
for (const kind of editorKinds) {
const tag = getTag(editors[kind].label, node, syntax, state.doc)
if (!tag) continue
+
if (tag.nameFrom === tag.nameTo) continue
widgets.push(Decoration.replace({
- widget: new OpenButtonWidget({ kind, text: tag.text })
+ widget: new OpenButtonWidget({ kind, text: tag.text, range: { from: tag.textFrom, to: tag.textTo } })
}).range(tag.nameFrom, tag.nameTo))
if (kind === 'palette') {
@@ -41,6 +42,8 @@ function makeValue(state: EditorState) {
}
})
+ _foldRanges.value = foldRanges;
+ _widgets.value = widgets;
return {
decorations: Decoration.set(widgets),
foldRanges
diff --git a/src/lib/state.ts b/src/lib/state.ts
index 2cc2fff760..e2075114ea 100644
--- a/src/lib/state.ts
+++ b/src/lib/state.ts
@@ -1,4 +1,5 @@
-import type { EditorView } from '@codemirror/view'
+import type { EditorView, Decoration } from '@codemirror/view'
+import type { Range } from '@codemirror/state'
import { Signal, signal } from '@preact/signals'
import { IoColorPalette, IoImage, IoMap, IoMusicalNotes } from 'react-icons/io5'
import type { FromTo } from './codemirror/util'
@@ -106,6 +107,7 @@ export type RoomState = {
}
export const codeMirror = signal(null)
+export const codeMirrorEditorText = signal('');
export const muted = signal(false)
export const errorLog = signal([])
export const openEditor = signal(null)
@@ -114,6 +116,8 @@ export const editSessionLength = signal(new Date());
export const showKeyBinding = signal(false);
export const showSaveConflictModal = signal(false);
export const continueSaving = signal(true);
+export const _foldRanges = signal([]);
+export const _widgets = signal[]>([]);
export const LAST_SAVED_SESSION_ID = 'lastSavedSessionId';
export type ThemeType = "dark" | "light" | "busker";
diff --git a/yarn.lock b/yarn.lock
index df47dc5eec..08f1d6d16c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1734,6 +1734,11 @@
resolved "https://registry.yarnpkg.com/@types/js-beautify/-/js-beautify-1.14.3.tgz#6ced76f79935e37e0d613110dea369881d93c1ff"
integrity sha512-FMbQHz+qd9DoGvgLHxeqqVPaNRffpIu5ZjozwV8hf9JAGpIOzuAf4wGbRSo8LNITHqGjmmVjaMggTT5P4v4IHg==
+"@types/js-levenshtein@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.3.tgz#a6fd0bdc8255b274e5438e0bfb25f154492d1106"
+ integrity sha512-jd+Q+sD20Qfu9e2aEXogiO3vpOC1PYJOUdyN9gvs4Qrvkg4wF43L5OhqrPeokdv8TL0/mXoYfpkcoGZMNN2pkQ==
+
"@types/json5@^0.0.30":
version "0.0.30"
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.30.tgz#44cb52f32a809734ca562e685c6473b5754a7818"
@@ -3971,6 +3976,11 @@ js-cookie@^3.0.5:
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
+js-levenshtein@^1.1.6:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
+ integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
+
js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"