From fa3fe561b7102fc16ea62ba31708a664d6a007d5 Mon Sep 17 00:00:00 2001 From: pearmini Date: Wed, 8 Oct 2025 15:51:56 -0400 Subject: [PATCH 1/5] =?UTF-8?q?Use=20=E2=9C=97=20as=20the=20error=20output?= =?UTF-8?q?=20mark?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- runtime/constant.js | 2 ++ runtime/index.js | 26 ++++++++++++++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) 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..3928f36 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, @@ -46,10 +48,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 +82,7 @@ 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 output = values.map(({value, options}) => formatOutput(value, options)).join("\n") + "\n"; changes.push({from: start, insert: output}); } } @@ -127,7 +137,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 +150,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 +163,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) => { From ca0bb0f660785b1ebfb7e51c900f612c06b38e20 Mon Sep 17 00:00:00 2001 From: pearmini Date: Wed, 8 Oct 2025 18:56:26 -0400 Subject: [PATCH 2/5] Update error styles --- editor/decoration.js | 6 ++++-- editor/index.css | 8 ++++++++ editor/outputLines.js | 16 ++++++++++++++-- runtime/index.js | 7 ++++++- test/js/runtime-error.js | 6 +++++- test/js/syntax-error.js | 6 +++++- 6 files changed, 42 insertions(+), 7 deletions(-) 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..3dcc297 100644 --- a/editor/index.css +++ b/editor/index.css @@ -143,6 +143,14 @@ /*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");*/ } + .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"); + + .ͼ1l { + 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/index.js b/runtime/index.js index 3928f36..5e6a16c 100644 --- a/runtime/index.js +++ b/runtime/index.js @@ -19,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); @@ -82,7 +86,8 @@ export function createRuntime(initialCode) { const start = node.start; const {values} = node.state; if (values.length) { - const output = values.map(({value, options}) => formatOutput(value, options)).join("\n") + "\n"; + const f = (v) => (isError(v) ? formatError(v) : formatOutput(v)); + const output = values.map(({value, options}) => f(value, options)).join("\n") + "\n"; changes.push({from: start, insert: output}); } } 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);`; From 464b1349d122c02c82a120091acbb83b27ff7160 Mon Sep 17 00:00:00 2001 From: pearmini Date: Thu, 9 Oct 2025 09:34:47 -0400 Subject: [PATCH 3/5] Fix style selector --- editor/index.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editor/index.css b/editor/index.css index 3dcc297..89f192d 100644 --- a/editor/index.css +++ b/editor/index.css @@ -145,10 +145,10 @@ .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"); + } - .ͼ1l { - color: #c83f30 !important; - } + .cm-output-line.cm-error-line span { + color: #c83f30 !important; } .cm-doc-tag span { From 9eb9dfd103fb096b31f7392709b7f98a57112b73 Mon Sep 17 00:00:00 2001 From: pearmini Date: Sat, 11 Oct 2025 11:36:15 -0400 Subject: [PATCH 4/5] Update comments --- editor/index.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/editor/index.css b/editor/index.css index 89f192d..29cdb12 100644 --- a/editor/index.css +++ b/editor/index.css @@ -143,6 +143,9 @@ /*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"); } From 0761ea6a27551534f5af58cbcf00f7a8a1ac713e Mon Sep 17 00:00:00 2001 From: pearmini Date: Sat, 11 Oct 2025 11:45:33 -0400 Subject: [PATCH 5/5] Fix CI --- runtime/index.js | 2 +- test/output/runtimeError.js | 9 +++++++-- test/output/syntaxError.js | 8 ++++++-- test/output/syntaxError2.js | 2 +- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/runtime/index.js b/runtime/index.js index 5e6a16c..ec5ae60 100644 --- a/runtime/index.js +++ b/runtime/index.js @@ -86,7 +86,7 @@ export function createRuntime(initialCode) { const start = node.start; const {values} = node.state; if (values.length) { - const f = (v) => (isError(v) ? formatError(v) : formatOutput(v)); + 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}); } 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