Skip to content

Commit

Permalink
sync sub editor changes (#1992)
Browse files Browse the repository at this point in the history
* crap

* add levenshtein distance package

* store codemirror text state and widgets/foldranges state

* remove foldRangeIndex prop

* use fold range index computed when code changes, {_foldRanges, _widgets}

* update editor modal when code changes in a colab session

* same changes as in prev?

* Fixed bugs that were in my game. The game is already in the gallery (#1983)

* Added my space invaders game

I have tried to model the original, and plan on coming back to this later to add new levels and a few other ideas I have but don't know how to implement yet.

* Fixed naming issues

* Update Space_Invaders.js

* Add files via upload

* Fixing metadata

* Fixed major bug in Space_Invaders.js

There was a major bug stopping you from restarting the game, which has been fixed

---------

Co-authored-by: graham <[email protected]>
Co-authored-by: Marios Mitsios <[email protected]>

* Updating github action for unit tests (#1989)

* Fixing github action - Moving synchronize to the right file (#1991)

Moving synchronize to the write file

* Add line offset to prevent failures (#1993)

* specify openEditor type to fix typescript error

* push widget before foldranges

fixes ranges error in codemirror

* revert file

* inline

---------

Co-authored-by: Atharva <[email protected]>
Co-authored-by: graham <[email protected]>
Co-authored-by: Marios Mitsios <[email protected]>
Co-authored-by: Sofia Egan <[email protected]>
  • Loading branch information
5 people authored Aug 26, 2024
1 parent af55b1c commit 4af82d7
Show file tree
Hide file tree
Showing 7 changed files with 88 additions and 23 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
19 changes: 3 additions & 16 deletions src/components/codemirror-widgets/open-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<button
class={styles.openButton}
onClick={async (event) => {
if (!codeMirror.value) return
const doc = codeMirror.value.state.doc.toString();
let pos = codeMirror.value.posAtCoords({ x: event.pageX, y: event.pageY })!
let from = -1

while (true) {
if (doc[pos] === '`') {
if (from === -1) {
from = pos + 1
} else {
break;
}
}
pos++
}

if (editors[props.kind].needsBitmaps) {
// Run the game headless to update bitmaps
Expand All @@ -40,7 +27,7 @@ export default function OpenButton(props: OpenButtonProps) {
openEditor.value = {
kind: props.kind,
text: props.text,
editRange: { from, to: pos }
editRange: { from: props.range.from, to: props.range.to },
}
}}
>
Expand All @@ -49,4 +36,4 @@ export default function OpenButton(props: OpenButtonProps) {
{props.kind === 'bitmap' && <BitmapPreview text={props.text} class={styles.bitmapPreview} />}
</button>
)
}
}
62 changes: 58 additions & 4 deletions src/components/popups-etc/editor-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand All @@ -29,7 +30,7 @@ export default function EditorModal() {
insert: _text
}
})

openEditor.value = {
..._openEditor,
text: _text,
Expand All @@ -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
Expand All @@ -54,4 +108,4 @@ export default function EditorModal() {
</div>
</div>
)
}
}
5 changes: 5 additions & 0 deletions src/lib/codemirror/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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(),
Expand Down
7 changes: 5 additions & 2 deletions src/lib/codemirror/widgets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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') {
Expand All @@ -41,6 +42,8 @@ function makeValue(state: EditorState) {
}
})

_foldRanges.value = foldRanges;
_widgets.value = widgets;
return {
decorations: Decoration.set(widgets),
foldRanges
Expand Down
6 changes: 5 additions & 1 deletion src/lib/state.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -106,6 +107,7 @@ export type RoomState = {
}

export const codeMirror = signal<EditorView | null>(null)
export const codeMirrorEditorText = signal<string>('');
export const muted = signal<boolean>(false)
export const errorLog = signal<NormalizedError[]>([])
export const openEditor = signal<OpenEditor | null>(null)
Expand All @@ -114,6 +116,8 @@ export const editSessionLength = signal<Date>(new Date());
export const showKeyBinding = signal(false);
export const showSaveConflictModal = signal<boolean>(false);
export const continueSaving = signal<boolean>(true);
export const _foldRanges = signal<FromTo[]>([]);
export const _widgets = signal<Range<Decoration>[]>([]);
export const LAST_SAVED_SESSION_ID = 'lastSavedSessionId';

export type ThemeType = "dark" | "light" | "busker";
Expand Down
10 changes: 10 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit 4af82d7

Please sign in to comment.