diff --git a/src/components/big-interactive-pages/editor.tsx b/src/components/big-interactive-pages/editor.tsx index 21c2005b6..5cd3f26d7 100644 --- a/src/components/big-interactive-pages/editor.tsx +++ b/src/components/big-interactive-pages/editor.tsx @@ -14,6 +14,7 @@ import Help from '../popups-etc/help' import { collapseRanges } from '../../lib/codemirror/util' import { defaultExampleCode } from '../../lib/examples' import MigrateToast from '../popups-etc/migrate-toast' +import { highlightError, clearErrorHighlight } from '../../lib/engine/3-editor/error' import { nanoid } from 'nanoid' interface EditorProps { @@ -86,7 +87,7 @@ export default function Editor({ persistenceState, cookies }: EditorProps) { return () => window.removeEventListener('resize', updateMaxSize) }, []) const realOutputAreaSize = useComputed(() => Math.min(maxOutputAreaSize.value, Math.max(minOutputAreaSize, outputAreaSize.value))) - + // Resize bar logic const resizeState = useSignal(null) useEffect(() => { @@ -106,15 +107,18 @@ export default function Editor({ persistenceState, cookies }: EditorProps) { const onRun = async () => { foldAllTemplateLiterals() if (!screen.current) return - + if (cleanup.current) cleanup.current() errorLog.value = [] const code = codeMirror.value?.state.doc.toString() ?? '' const res = runGame(code, screen.current, (error) => { errorLog.value = [ ...errorLog.value, error ] + if (error.line) { + highlightError(error.line); + } }) - + screen.current.focus() screenShake.value++ setTimeout(() => screenShake.value--, 200) @@ -123,6 +127,12 @@ export default function Editor({ persistenceState, cookies }: EditorProps) { if (res.error) { console.error(res.error.raw) errorLog.value = [ ...errorLog.value, res.error ] + + if (res.error.line) { + highlightError(res.error.line); + } + } else { + clearErrorHighlight(); } } useEffect(() => () => cleanup.current?.(), []) @@ -189,7 +199,7 @@ export default function Editor({ persistenceState, cookies }: EditorProps) { return (
- +
errorLog.value = []}> - + {errorLog.value.map((error, i) => (
{error.description}
))} @@ -273,7 +283,7 @@ export default function Editor({ persistenceState, cookies }: EditorProps) { {persistenceState.value.kind === 'IN_MEMORY' && persistenceState.value.showInitialWarning && ( )} - +
diff --git a/src/global.css b/src/global.css index 29e99faf1..a227259af 100644 --- a/src/global.css +++ b/src/global.css @@ -84,4 +84,8 @@ a:hover { .grecaptcha-badge { visibility: hidden; +} + +.cm-lineNumbers > .cm-gutterElement[err-line] { + background: #ecb2b2!important; } \ No newline at end of file diff --git a/src/lib/engine/3-editor/error.ts b/src/lib/engine/3-editor/error.ts index 585e79156..9e7da93b4 100644 --- a/src/lib/engine/3-editor/error.ts +++ b/src/lib/engine/3-editor/error.ts @@ -7,7 +7,7 @@ export type EsprimaError = Error & { column: number } -export type GameError = +export type GameError = | { kind: 'parse', error: EsprimaError } | { kind: 'runtime', error: unknown } | { kind: 'page', error: unknown } @@ -52,7 +52,7 @@ const normalizeStack = (stack: string): StackItem[] | null => { let fileUrl = match[2]! while (chromeStackUrlRegex.test(fileUrl)) fileUrl = fileUrl.match(chromeStackUrlRegex)![1]! - + return { callSite, fileUrl, @@ -82,22 +82,27 @@ export const normalizeGameError = (gameError: GameError): NormalizedError => { const line = lineNumber - 1 return { description: `SyntaxError: ${description}\n at :${line}:${column}`, - raw: gameError.error + raw: gameError.error, + line: line, + column: column } } else if (gameError.error instanceof Error) { const descriptionLines: string[] = [] const stack = (gameError.error.stack ? normalizeStack(gameError.error.stack) : null) ?? [] + + let [line, col] = findErrorLineCol(gameError.error.stack) + stack.reverse() let foundEval = false for (const item of stack) { - if (!foundEval && [ 'eval', 'anonymous' ].includes(item.callSite)) { + if (!foundEval && ['eval', 'anonymous'].includes(item.callSite)) { foundEval = true if (item.lineNumber) item.lineNumber -= lineOffset } if (!foundEval) continue - + let fileName try { const url = new URL(item.fileUrl) @@ -118,9 +123,64 @@ export const normalizeGameError = (gameError: GameError): NormalizedError => { descriptionLines.unshift(`${gameError.error.name}: ${gameError.error.message}`) return { description: descriptionLines.join('\n'), - raw: gameError.error + raw: gameError.error, + line: line, + column: col } } else { return { description: `Runtime Error: ${gameError.error}`, raw: gameError.error } } +} + +/* + * Finds the line and column of innermost error from a stack. + * This is modified code from V1. + */ +function findErrorLineCol(stack: string | undefined): [number | null, number | null] { + if (!stack) return [null, null] + + let line = null + let col = null + + // Get the most outer (first) error that is part of ther user's code (not part of the engine and anonymous) + let location = stack.match(/:(.+)\)/) + + if (location) { + let lineCol = location[1].split(":").map(Number) + line = lineCol[0] - 2 - 1 + col = lineCol[1] + } + + return [line, col] +} + +/* + * Highlights the line gutter of the speciefied line number to indicate an error. + * This uses a custom attribute to style the gutter. + * This is modified code from V1. + */ +export function highlightError(line: number) { + const cmLineGutters = document.querySelectorAll(".cm-lineNumbers > .cm-gutterElement") // Get all the line gutters + // Find the gutter that matches the line number and is not hidden + for (let i = 0; i < cmLineGutters.length; i++) { + const cmLineGutter = cmLineGutters[i] as HTMLElement; + const innerNumber = cmLineGutter.innerText; + const height = cmLineGutter.style.height; + if (Number(innerNumber) !== line || height === "0px") { + cmLineGutter.removeAttribute("err-line"); + continue; + }; + cmLineGutter.setAttribute("err-line", ""); + } +} + +/* + * Clears all exsisting error highlighted from the gutter + */ +export function clearErrorHighlight() { + const cmLineGutters = document.querySelectorAll(".cm-lineNumbers > .cm-gutterElement") + for (let i = 0; i < cmLineGutters.length; i++) { + const cmLineGutter = cmLineGutters[i] as HTMLElement; + cmLineGutter.removeAttribute("err-line"); + } } \ No newline at end of file diff --git a/src/lib/state.ts b/src/lib/state.ts index f4fdd2e0a..a0e051cd7 100644 --- a/src/lib/state.ts +++ b/src/lib/state.ts @@ -13,6 +13,8 @@ import type { Game, SessionInfo } from './game-saving/account' export interface NormalizedError { raw: unknown description: string + line?: number | null + column?: number | null } // Editor types