diff --git a/editor/decoration.js b/editor/decoration.js index 649eeb0..6d8d038 100644 --- a/editor/decoration.js +++ b/editor/decoration.js @@ -3,13 +3,15 @@ import {outputLinesField} from "./outputLines"; import {RangeSetBuilder} from "@codemirror/state"; const highlight = Decoration.line({attributes: {class: "cm-output-line"}}); +const errorHighlight = Decoration.line({attributes: {class: "cm-output-line cm-error-line"}}); // const linePrefix = Decoration.mark({attributes: {class: "cm-output-line-prefix"}}); // const lineContent = Decoration.mark({attributes: {class: "cm-output-line-content"}}); function createWidgets(lines) { const builder = new RangeSetBuilder(); - for (const {from, to} of lines) { - builder.add(from, from, highlight); + for (const {from, type} of lines) { + if (type === "output") builder.add(from, from, highlight); + else if (type === "error") builder.add(from, from, errorHighlight); // builder.add(from, from + 3, linePrefix); // builder.add(from + 4, to, lineContent); } diff --git a/editor/index.css b/editor/index.css index dca6b9c..29cdb12 100644 --- a/editor/index.css +++ b/editor/index.css @@ -143,6 +143,17 @@ /*background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3E%3Cpath fill='%237d7d7d' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E");*/ } + /* We can't see background color, which is conflict with selection background color. + * So we use svg pattern to simulate the background color. + */ + .cm-output-line.cm-error-line { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4' %3E%3Cpath fill='%23ff0000' fill-opacity='0.4' d='M1 3h1v1H1V3zm2-2h1v1H3V1z'%3E%3C/path%3E%3C/svg%3E"); + } + + .cm-output-line.cm-error-line span { + color: #c83f30 !important; + } + .cm-doc-tag span { color: #cf222e; } diff --git a/editor/outputLines.js b/editor/outputLines.js index b50172e..2e99584 100644 --- a/editor/outputLines.js +++ b/editor/outputLines.js @@ -1,9 +1,11 @@ import {syntaxTree} from "@codemirror/language"; import {StateField} from "@codemirror/state"; -import {OUTPUT_MARK} from "../runtime/constant.js"; +import {OUTPUT_MARK, ERROR_MARK} from "../runtime/constant.js"; const OUTPUT_MARK_CODE_POINT = OUTPUT_MARK.codePointAt(0); +const ERROR_MARK_CODE_POINT = ERROR_MARK.codePointAt(0); + /** @type {StateField<{number: number, from: number, to: number}[]>} */ export const outputLinesField = StateField.define({ create(state) { @@ -25,11 +27,21 @@ function computeLineNumbers(state) { if (node.name === "LineComment" && node.node.parent.name === "Script") { // Check if the line comment covers the entire line. const line = state.doc.lineAt(node.from); - if (line.from === node.from && line.to === node.to && line.text.codePointAt(2) === OUTPUT_MARK_CODE_POINT) { + if (line.from !== node.from || line.to !== node.to) return; + if (line.text.codePointAt(2) === OUTPUT_MARK_CODE_POINT) { + lineNumbers.push({ + number: line.number, + from: line.from, + to: line.to, + type: "output", + }); + } + if (line.text.codePointAt(2) === ERROR_MARK_CODE_POINT) { lineNumbers.push({ number: line.number, from: line.from, to: line.to, + type: "error", }); } } diff --git a/runtime/constant.js b/runtime/constant.js index ea11373..ea75326 100644 --- a/runtime/constant.js +++ b/runtime/constant.js @@ -1 +1,3 @@ export const OUTPUT_MARK = "➜"; + +export const ERROR_MARK = "✗"; diff --git a/runtime/index.js b/runtime/index.js index 2857e7a..ec5ae60 100644 --- a/runtime/index.js +++ b/runtime/index.js @@ -5,9 +5,11 @@ import {parse} from "acorn"; import {group} from "d3-array"; import {dispatch as d3Dispatch} from "d3-dispatch"; import * as stdlib from "./stdlib.js"; -import {OUTPUT_MARK} from "./constant.js"; +import {OUTPUT_MARK, ERROR_MARK} from "./constant.js"; -const PREFIX = `//${OUTPUT_MARK}`; +const OUTPUT_PREFIX = `//${OUTPUT_MARK}`; + +const ERROR_PREFIX = `//${ERROR_MARK}`; const BUILTINS = { recho: () => stdlib, @@ -17,6 +19,10 @@ function uid() { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } +function isError(value) { + return value instanceof Error; +} + function safeEval(code, inputs) { const body = `const foo = ${code}; return foo(${inputs.join(",")})`; const fn = new Function(...inputs, body); @@ -46,10 +52,18 @@ function inspect(value, {limit = 200, quote = "double", indent = null} = {}) { return string; } -function format(value, options) { +function format(value, options, prefix) { const string = inspect(value, options); const lines = string.split("\n"); - return lines.map((line) => `${PREFIX} ${line}`).join("\n"); + return lines.map((line) => `${prefix} ${line}`).join("\n"); +} + +function formatOutput(value, options) { + return format(value, options, OUTPUT_PREFIX); +} + +function formatError(value, options) { + return format(value, options, ERROR_PREFIX); } export function createRuntime(initialCode) { @@ -72,7 +86,8 @@ export function createRuntime(initialCode) { const start = node.start; const {values} = node.state; if (values.length) { - const output = values.map(({value, options}) => format(value, options)).join("\n") + "\n"; + const f = (v, o) => (isError(v) ? formatError(v, o) : formatOutput(v, o)); + const output = values.map(({value, options}) => f(value, options)).join("\n") + "\n"; changes.push({from: start, insert: output}); } } @@ -127,7 +142,7 @@ export function createRuntime(initialCode) { } catch (error) { console.error(error); const changes = removeChanges(code); - const errorMsg = format(error) + "\n"; + const errorMsg = formatError(error) + "\n"; changes.push({from: 0, insert: errorMsg}); dispatch(changes); return null; @@ -140,7 +155,7 @@ export function createRuntime(initialCode) { } catch (error) { console.error(error); const changes = removeChanges(code); - const errorMsg = format(error) + "\n"; + const errorMsg = formatError(error) + "\n"; changes.push({from: 0, insert: errorMsg}); dispatch(changes); return null; @@ -153,7 +168,7 @@ export function createRuntime(initialCode) { const oldOutputs = code .split("\n") .map((l, i) => [l, i]) - .filter(([l]) => l.startsWith(PREFIX)) + .filter(([l]) => l.startsWith(OUTPUT_PREFIX) || l.startsWith(ERROR_PREFIX)) .map(([_, i]) => i); const lineOf = (i) => { diff --git a/test/js/runtime-error.js b/test/js/runtime-error.js index 657bfac..491559d 100644 --- a/test/js/runtime-error.js +++ b/test/js/runtime-error.js @@ -1 +1,5 @@ -export const runtimeError = `add(1, 2);`; +export const runtimeError = `const add = (a, b) => a + b; + +ad(1, 2); + +echo(add(1, 2));`; diff --git a/test/js/syntax-error.js b/test/js/syntax-error.js index 5b7813d..cd21f8d 100644 --- a/test/js/syntax-error.js +++ b/test/js/syntax-error.js @@ -1 +1,5 @@ -export const syntaxError = `function add();`; +export const syntaxError = `const a = 1; + +function add(); + +echo(1 + 2);`; diff --git a/test/output/runtimeError.js b/test/output/runtimeError.js index 0e660d4..88758ee 100644 --- a/test/output/runtimeError.js +++ b/test/output/runtimeError.js @@ -1,2 +1,7 @@ -//➜ { [RuntimeError: add is not defined] input: "add" } -add(1, 2); \ No newline at end of file +const add = (a, b) => a + b; + +//✗ { [RuntimeError: ad is not defined] input: "ad" } +ad(1, 2); + +//➜ 3 +echo(add(1, 2)); \ No newline at end of file diff --git a/test/output/syntaxError.js b/test/output/syntaxError.js index c0f49ba..083805b 100644 --- a/test/output/syntaxError.js +++ b/test/output/syntaxError.js @@ -1,2 +1,6 @@ -//➜ { [SyntaxError: Unexpected token (1:14)] pos: 14, loc: Position { line: 1, column: 14 }, raisedAt: 15 } -function add(); \ No newline at end of file +//✗ { [SyntaxError: Unexpected token (3:14)] pos: 28, loc: Position { line: 3, column: 14 }, raisedAt: 29 } +const a = 1; + +function add(); + +echo(1 + 2); \ No newline at end of file diff --git a/test/output/syntaxError2.js b/test/output/syntaxError2.js index b176b97..5c87923 100644 --- a/test/output/syntaxError2.js +++ b/test/output/syntaxError2.js @@ -1,2 +1,2 @@ -//➜ [SyntaxError: Assignment to external variable 'a' (1:0)] +//✗ [SyntaxError: Assignment to external variable 'a' (1:0)] let a = 1; a = 2; \ No newline at end of file