From 35b0ba27e626f760f7c60f7f877d65ac8f1d4f19 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Sun, 27 Jun 2021 11:37:42 +0100 Subject: [PATCH] Improves error reporting in code editor --- components/code-panel/code-editor.tsx | 20 ++--- components/code-panel/code-panel.tsx | 105 +++++++++++++++----------- state/code/generate.ts | 51 ++++++++----- styles/globals.css | 7 ++ styles/stitches.config.ts | 1 + types.ts | 24 ++++-- utils/code.ts | 43 +++++++++++ 7 files changed, 169 insertions(+), 82 deletions(-) create mode 100644 utils/code.ts diff --git a/components/code-panel/code-editor.tsx b/components/code-panel/code-editor.tsx index 10a033a26..507c0a8cd 100644 --- a/components/code-panel/code-editor.tsx +++ b/components/code-panel/code-editor.tsx @@ -1,20 +1,20 @@ import Editor, { Monaco } from '@monaco-editor/react' import useTheme from 'hooks/useTheme' -import prettier from 'prettier/standalone' -import parserTypeScript from 'prettier/parser-typescript' -// import codeAsString from './code-as-string' import typesImport from './types-import' import React, { useCallback, useEffect, useRef } from 'react' import styled from 'styles' import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' +import { getFormattedCode } from 'utils/code' export type IMonaco = typeof monaco export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor +const modifierKeys = ['Escape', 'Meta', 'Control', 'Shift', 'Option', 'Alt'] + interface Props { value: string - error: { line: number } + error: { line: number; column: number } fontSize: number monacoRef?: React.MutableRefObject editorRef?: React.MutableRefObject @@ -92,13 +92,7 @@ export default function CodeEditor({ monaco.languages.registerDocumentFormattingEditProvider('typescript', { async provideDocumentFormattingEdits(model) { try { - const text = prettier.format(model.getValue(), { - parser: 'typescript', - plugins: [parserTypeScript], - singleQuote: true, - trailingComma: 'es5', - semi: false, - }) + const text = getFormattedCode(model.getValue()) return [ { @@ -126,6 +120,8 @@ export default function CodeEditor({ editor.updateOptions({ fontSize, + fontFamily: "'Recursive', monospace", + fontWeight: '420', wordBasedSuggestions: false, minimap: { enabled: false }, lightbulb: { @@ -141,8 +137,8 @@ export default function CodeEditor({ const handleKeydown = useCallback( (e: React.KeyboardEvent) => { - onKey && onKey() e.stopPropagation() + !modifierKeys.includes(e.key) && onKey?.() const metaKey = navigator.platform.match('Mac') ? e.metaKey : e.ctrlKey if (e.key === 's' && metaKey) { const editor = rEditor.current diff --git a/components/code-panel/code-panel.tsx b/components/code-panel/code-panel.tsx index 446581256..155f16468 100644 --- a/components/code-panel/code-panel.tsx +++ b/components/code-panel/code-panel.tsx @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ import styled from 'styles' import { useStateDesigner } from '@state-designer/react' -import React, { useEffect, useRef } from 'react' +import React, { useCallback, useEffect, useRef } from 'react' import state, { useSelector } from 'state' -import { CodeFile } from 'types' +import { CodeError, CodeFile, CodeResult } from 'types' import CodeDocs from './code-docs' import { generateFromCode } from 'state/code/generate' import * as Panel from '../panel' @@ -19,16 +19,9 @@ import { import dynamic from 'next/dynamic' const CodeEditor = dynamic(() => import('./code-editor')) -const getErrorLineAndColumn = (e: any) => { - if ('line' in e) { - return { line: Number(e.line), column: e.column } - } - - const result = e.stack.match(/:([0-9]+):([0-9]+)/) - if (result) { - return { line: Number(result[1]) - 1, column: result[2] } - } -} +const increaseCodeSize = () => state.send('INCREASED_CODE_FONT_SIZE') +const decreaseCodeSize = () => state.send('DECREASED_CODE_FONT_SIZE') +const toggleCodePanel = () => state.send('TOGGLED_CODE_PANEL_OPEN') export default function CodePanel(): JSX.Element { const rContainer = useRef(null) @@ -43,7 +36,7 @@ export default function CodePanel(): JSX.Element { const local = useStateDesigner({ data: { code: file.code, - error: null as { message: string; line: number; column: number } | null, + error: null as CodeError | null, }, on: { MOUNTED: 'setCode', @@ -53,13 +46,23 @@ export default function CodePanel(): JSX.Element { states: { editingCode: { on: { - RAN_CODE: ['saveCode', 'runCode'], - SAVED_CODE: ['saveCode', 'runCode'], + RAN_CODE: { do: 'saveCode', to: 'evaluatingCode' }, + SAVED_CODE: { do: 'saveCode', to: 'evaluatingCode' }, CHANGED_CODE: { secretlyDo: 'setCode' }, CLEARED_ERROR: { if: 'hasError', do: 'clearError' }, TOGGLED_DOCS: { to: 'viewingDocs' }, }, }, + evaluatingCode: { + async: { + await: 'evalCode', + onResolve: { + do: ['clearError', 'sendResultToGlobalState'], + to: 'editingCode', + }, + onReject: { do: 'setErrorFromResult', to: 'editingCode' }, + }, + }, viewingDocs: { on: { TOGGLED_DOCS: { to: 'editingCode' }, @@ -78,22 +81,6 @@ export default function CodePanel(): JSX.Element { setCode(data, payload: { code: string }) { data.code = payload.code }, - runCode(data) { - let error = null - - try { - generateFromCode(state.data, data.code).then( - ({ shapes, controls }) => { - state.send('GENERATED_FROM_CODE', { shapes, controls }) - } - ) - } catch (e) { - console.error('Got an error!', e) - error = { message: e.message, ...getErrorLineAndColumn(e) } - } - - data.error = error - }, saveCode(data) { const { code } = data state.send('SAVED_CODE', { code }) @@ -101,6 +88,25 @@ export default function CodePanel(): JSX.Element { clearError(data) { data.error = null }, + setErrorFromResult(data, payload, result: CodeResult) { + data.error = result.error + }, + sendResultToGlobalState(data, payload, result: CodeResult) { + state.send('GENERATED_FROM_CODE', result) + }, + }, + asyncs: { + evalCode(data) { + return new Promise((resolve, reject) => { + generateFromCode(state.data, data.code).then((result) => { + if (result.error !== null) { + reject(result) + } else { + resolve(result) + } + }) + }) + }, }, }) @@ -115,6 +121,17 @@ export default function CodePanel(): JSX.Element { } }, []) + const handleCodeChange = useCallback( + (code: string) => local.send('CHANGED_CODE', { code }), + [local] + ) + + const handleSave = useCallback(() => local.send('SAVED_CODE'), [local]) + + const handleKey = useCallback(() => local.send('CLEARED_ERROR'), [local]) + + const toggleDocs = useCallback(() => local.send('TOGGLED_DOCS'), [local]) + const { error } = local.data return ( @@ -131,7 +148,7 @@ export default function CodePanel(): JSX.Element { state.send('TOGGLED_CODE_PANEL_OPEN')} + onClick={toggleCodePanel} > @@ -142,14 +159,14 @@ export default function CodePanel(): JSX.Element { bp={{ '@initial': 'mobile', '@sm': 'small' }} size="small" disabled={!local.isIn('editingCode')} - onClick={() => state.send('INCREASED_CODE_FONT_SIZE')} + onClick={increaseCodeSize} > state.send('DECREASED_CODE_FONT_SIZE')} + onClick={decreaseCodeSize} > @@ -157,7 +174,7 @@ export default function CodePanel(): JSX.Element { local.send('TOGGLED_DOCS')} + onClick={toggleDocs} > @@ -165,7 +182,7 @@ export default function CodePanel(): JSX.Element { bp={{ '@initial': 'mobile', '@sm': 'small' }} size="small" disabled={!local.isIn('editingCode')} - onClick={() => local.send('SAVED_CODE')} + onClick={handleSave} > @@ -177,24 +194,20 @@ export default function CodePanel(): JSX.Element { readOnly={isReadOnly} value={file.code} error={error} - onChange={(code) => local.send('CHANGED_CODE', { code })} - onSave={() => local.send('SAVED_CODE')} - onKey={() => local.send('CLEARED_ERROR')} + onChange={handleCodeChange} + onSave={handleSave} + onKey={handleKey} /> - - {error && - (error.line - ? `(${Number(error.line) - 2}:${error.column}) ${error.message}` - : error.message)} - + + {error && {error.message}} ) : ( state.send('TOGGLED_CODE_PANEL_OPEN')} + onClick={toggleCodePanel} > diff --git a/state/code/generate.ts b/state/code/generate.ts index abdd9e186..ba77703ad 100644 --- a/state/code/generate.ts +++ b/state/code/generate.ts @@ -19,9 +19,11 @@ import { ColorStyle, FontSize, SizeStyle, + CodeError, } from 'types' import { getPage, getShapes } from 'utils' import { transform } from 'sucrase' +import { getErrorWithLineAndColumn, getFormattedCode } from 'utils/code' const baseScope = { Dot, @@ -54,6 +56,7 @@ export async function generateFromCode( ): Promise<{ shapes: Shape[] controls: CodeControl[] + error: CodeError }> { codeControls.clear() codeShapes.clear() @@ -63,29 +66,41 @@ export async function generateFromCode( const { currentPageId } = data const scope = { ...baseScope, controls, currentPageId } - const transformed = transform(code, { - transforms: ['typescript'], - }).code + let generatedShapes: Shape[] = [] + let generatedControls: CodeControl[] = [] + let error: CodeError | null = null - new Function(...Object.keys(scope), `${transformed}`)(...Object.values(scope)) + try { + const formattedCode = getFormattedCode(code) - const startingChildIndex = - getShapes(data) - .filter((shape) => shape.parentId === data.currentPageId) - .sort((a, b) => a.childIndex - b.childIndex)[0]?.childIndex || 1 + const transformedCode = transform(formattedCode, { + transforms: ['typescript'], + })?.code - const generatedShapes = Array.from(codeShapes.values()) - .sort((a, b) => a.shape.childIndex - b.shape.childIndex) - .map((instance, i) => ({ - ...instance.shape, - isGenerated: true, - parentId: getPage(data).id, - childIndex: startingChildIndex + i, - })) + new Function(...Object.keys(scope), `${transformedCode}`)( + ...Object.values(scope) + ) - const generatedControls = Array.from(codeControls.values()) + const startingChildIndex = + getShapes(data) + .filter((shape) => shape.parentId === data.currentPageId) + .sort((a, b) => a.childIndex - b.childIndex)[0]?.childIndex || 1 - return { shapes: generatedShapes, controls: generatedControls } + generatedShapes = Array.from(codeShapes.values()) + .sort((a, b) => a.shape.childIndex - b.shape.childIndex) + .map((instance, i) => ({ + ...instance.shape, + isGenerated: true, + parentId: getPage(data).id, + childIndex: startingChildIndex + i, + })) + + generatedControls = Array.from(codeControls.values()) + } catch (e) { + error = getErrorWithLineAndColumn(e) + } + + return { shapes: generatedShapes, controls: generatedControls, error } } /** diff --git a/styles/globals.css b/styles/globals.css index 25696c2f8..867099b3b 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -1,7 +1,14 @@ @import url('https://fonts.googleapis.com/css2?family=Recursive:wght@500;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Recursive:wght,MONO@420,1&display=swap'); + @font-face { font-family: 'Verveine Regular'; font-style: normal; font-weight: normal; src: local('Verveine Regular'), url('/VerveineRegular.woff') format('woff'); } + + +.margin { + user-select: none; +} \ No newline at end of file diff --git a/styles/stitches.config.ts b/styles/stitches.config.ts index 925a61b9b..16ba932dc 100644 --- a/styles/stitches.config.ts +++ b/styles/stitches.config.ts @@ -23,6 +23,7 @@ const { styled, global, css, theme, getCssString } = createCss({ muted: '#777', input: '#f3f3f3', inputBorder: '#ddd', + lineError: 'rgba(255, 0, 0, .1)', }, space: {}, fontSizes: { diff --git a/types.ts b/types.ts index 33a7a1ebf..5b3c93dba 100644 --- a/types.ts +++ b/types.ts @@ -227,12 +227,6 @@ export type Shape = Readonly export type ShapeByType = Shapes[T] -export interface CodeFile { - id: string - name: string - code: string -} - export enum Decoration { Arrow = 'Arrow', } @@ -249,6 +243,24 @@ export interface ShapeHandle { point: number[] } +export interface CodeFile { + id: string + name: string + code: string +} + +export interface CodeError { + message: string + line: number + column: number +} + +export interface CodeResult { + shapes: Shape[] + controls: CodeControl[] + error: CodeError +} + /* -------------------------------------------------- */ /* Editor UI */ /* -------------------------------------------------- */ diff --git a/utils/code.ts b/utils/code.ts new file mode 100644 index 000000000..01044b969 --- /dev/null +++ b/utils/code.ts @@ -0,0 +1,43 @@ +import prettier from 'prettier/standalone' +import parserTypeScript from 'prettier/parser-typescript' +import { CodeError } from 'types' + +/** + * Format code with prettier + * @param code + */ +export function getFormattedCode(code: string): string { + return prettier.format(code, { + parser: 'typescript', + plugins: [parserTypeScript], + singleQuote: true, + trailingComma: 'es5', + semi: false, + }) +} + +/** + * Get line and column from error. + * @param e + */ +export const getErrorWithLineAndColumn = (e: Error | any): CodeError => { + if ('line' in e) { + return { message: e.message, line: Number(e.line), column: e.column } + } + + const result = e.stack.split('/n')[0].match(/(.*)\(([0-9]+):([0-9]+)/) + + if (result) { + return { + message: result[1], + line: Number(result[2]) + 1, + column: result[3], + } + } else { + return { + message: e.message, + line: null, + column: null, + } + } +}