diff options
| author | _ov4 <ov4@ov4.org> | 2026-10-22 01:45:15 +0000 |
|---|---|---|
| committer | _ov4 <ov4@ov4.org> | 2026-10-22 01:45:15 +0000 |
| commit | 660d618f8b0df318d636cd780e547890d0248c0c (patch) | |
| tree | ea3f7746105d271558287ccd7268834245df68fd /bundle | |
| parent | 427acc420c6fce10fa77e603f7696f3e89d342d6 (diff) | |
feat(bundle): color compiler diagnostics in Output
The runner now passes -fdiagnostics-color=always, since auto only sees a
pipe. src/ansi.js turns stderr's SGR sequences into bold and colored
runs, keeping style and cut-off sequences across chunks and dropping
other sequences such as gcc's erase-line. Program stdout stays verbatim.
Colors come from --vscode-terminal-ansi* variables. 2026 Dark leaves
them unset, so vendor/vscode/terminal-colors.json pins VS Code's dark
terminal defaults from the same upstream commit as the themes; a theme
value still wins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'bundle')
| -rw-r--r-- | bundle/AGENTS/DEVELOPMENT.md | 2 | ||||
| -rw-r--r-- | bundle/README.md | 2 | ||||
| -rw-r--r-- | bundle/ThirdPartyNotices.md | 2 | ||||
| -rw-r--r-- | bundle/src-tauri/src/runner.rs | 9 | ||||
| -rw-r--r-- | bundle/src/ansi.js | 36 | ||||
| -rw-r--r-- | bundle/src/editor.js | 5 | ||||
| -rw-r--r-- | bundle/src/main.js | 32 | ||||
| -rw-r--r-- | bundle/vendor/vscode/terminal-colors.json | 23 |
8 files changed, 98 insertions, 13 deletions
diff --git a/bundle/AGENTS/DEVELOPMENT.md b/bundle/AGENTS/DEVELOPMENT.md index f87f0c0..b535dd0 100644 --- a/bundle/AGENTS/DEVELOPMENT.md +++ b/bundle/AGENTS/DEVELOPMENT.md @@ -26,7 +26,7 @@ Windows and macOS nightly jobs cache Rust dependencies from `src-tauri/target` a The language editing core (grammar, configuration, indentation, lexical masking and bracket matching) is shared in full. Platform editor API adapters, file dialogs and process management are separate. Keep the plugin free of Bundle runtime dependencies. -The Rust runner launches the packaged compiler directly, using the source directory as cwd. On Windows its absolute path is captured after extracting into the startup working directory; elsewhere it is the sibling `cpsc` executable. Tokio concurrently writes prefilled stdin and drains both output pipes; stdin is dropped after writing. A watch signal kills and reaps the process on Stop or application exit. The output Channel sends raw byte chunks (one streaming TextDecoder per stream in the frontend), then a finished event after both pipes reach EOF. EMBEDDED execution disables the host CRT stdout buffer before JIT initialization; Windows JIT lookups prefer the same msvcrt runtime. Do not introduce a shell or pseudo-terminal. +The Rust runner launches the packaged compiler directly, using the source directory as cwd. On Windows its absolute path is captured after extracting into the startup working directory; elsewhere it is the sibling `cpsc` executable. Tokio concurrently writes prefilled stdin and drains both output pipes; stdin is dropped after writing. A watch signal kills and reaps the process on Stop or application exit. The output Channel sends raw byte chunks (one streaming TextDecoder per stream in the frontend), then a finished event after both pipes reach EOF. EMBEDDED execution disables the host CRT stdout buffer before JIT initialization; Windows JIT lookups prefer the same msvcrt runtime. Do not introduce a shell or pseudo-terminal. The runner passes `-fdiagnostics-color=always`; `src/ansi.js` renders SGR bold and foreground colors from stderr only, so program stdout stays verbatim. Colors use `--vscode-terminal-ansi*`, which fall back to VS Code's terminal dark defaults pinned in `vendor/vscode/terminal-colors.json` because 2026 Dark does not set them. After building the compiler, run `cargo test --release --manifest-path src-tauri/Cargo.toml`. The two native tests exercise real compilation, prefilled input, EOF, Unicode, diagnostics, output before process exit, stopping, single-run exclusion and restart, including source paths with spaces. CI runs them on all three native platforms. diff --git a/bundle/README.md b/bundle/README.md index 6ef2bcc..b9df146 100644 --- a/bundle/README.md +++ b/bundle/README.md @@ -1,6 +1,6 @@ # CPSC Bundle -Open a `.cps` file, enter its input in **Input**, and click **Run**. The file is saved before running. Program output and errors appear in **Output**. Click **Stop** to end a running program. +Open a `.cps` file, enter its input in **Input**, and click **Run**. The file is saved before running. Program output and errors appear in **Output**, with compiler errors colored. Click **Stop** to end a running program. Shortcuts use Cmd on macOS and Ctrl on Windows: **N** new, **O** open, **S** save, **F** find, **/** toggle line comment. diff --git a/bundle/ThirdPartyNotices.md b/bundle/ThirdPartyNotices.md index 0aed0f3..df9741e 100644 --- a/bundle/ThirdPartyNotices.md +++ b/bundle/ThirdPartyNotices.md @@ -6,4 +6,6 @@ The editor uses Monaco Editor (MIT), vscode-textmate (MIT), vscode-oniguruma (MI Source: https://github.com/microsoft/vscode/tree/f93fdeb454a0f0fafe5f310408cec48dd3b6f58a/extensions/theme-defaults/themes +`vendor/vscode/terminal-colors.json` contains the dark default `terminal.ansi*` colors from `src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts` at the same commit and under the same license. + The grammar and language configuration come directly from this repository's VS Code plugin. See [its third-party notices](../plugin/ThirdPartyNotices.md) for upstream attribution. diff --git a/bundle/src-tauri/src/runner.rs b/bundle/src-tauri/src/runner.rs index e2b492b..eef35d2 100644 --- a/bundle/src-tauri/src/runner.rs +++ b/bundle/src-tauri/src/runner.rs @@ -100,6 +100,8 @@ async fn run_process( command .arg(&source) .arg("-r") + // Output renders stderr colors; auto would only see a pipe. + .arg("-fdiagnostics-color=always") .current_dir(source.parent().ok_or("Invalid source path.")?) .stdin(Stdio::piped()) .stdout(Stdio::piped()) @@ -193,7 +195,12 @@ mod tests { "done\n", 0, ), - ("OUTPUT Missing\n", "", "compilation terminated.", 1), + ( + "OUTPUT Missing\n", + "", + "\u{1b}[01;31m\u{1b}[Kerror: \u{1b}[m\u{1b}[K", + 1, + ), ( "OUTPUT 1 DIV 0\n", "", diff --git a/bundle/src/ansi.js b/bundle/src/ansi.js new file mode 100644 index 0000000..66173ea --- /dev/null +++ b/bundle/src/ansi.js @@ -0,0 +1,36 @@ +// Splits terminal text into runs styled by SGR escape sequences, keeping the +// style and any sequence cut off at a chunk boundary for the next chunk. +// Handles reset, bold and the 16 foreground colors; drops other sequences. +const names = ['Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White']; +const sequence = /\x1b\[([0-?]*)[ -/]*([@-~])/g; +const unfinished = /\x1b(\[[0-?]*[ -/]*)?$/; + +export function createAnsiParser() { + let bold = false; + let color = null; + let rest = ''; + return text => { + text = rest + text; + const cut = text.match(unfinished); + rest = cut ? cut[0] : ''; + if (cut) text = text.slice(0, cut.index); + const runs = []; + let start = 0; + for (const match of text.matchAll(sequence)) { + if (match.index > start) runs.push({ text: text.slice(start, match.index), bold, color }); + start = match.index + match[0].length; + if (match[2] !== 'm') continue; + for (const parameter of match[1].split(';')) { + const code = Number(parameter || 0); + if (code === 0) { bold = false; color = null; } + else if (code === 1) bold = true; + else if (code === 22) bold = false; + else if (code === 39) color = null; + else if (code >= 30 && code <= 37) color = `terminal.ansi${names[code - 30]}`; + else if (code >= 90 && code <= 97) color = `terminal.ansiBright${names[code - 90]}`; + } + } + if (start < text.length) runs.push({ text: text.slice(start), bold, color }); + return runs; + }; +} diff --git a/bundle/src/editor.js b/bundle/src/editor.js index 853fa82..58b0e19 100644 --- a/bundle/src/editor.js +++ b/bundle/src/editor.js @@ -33,7 +33,10 @@ export const theme = { tokenColors: [{ settings: { foreground: dark.colors['editor.foreground'], background: dark.colors['editor.background'] } }, ...dark.tokenColors] }; -for (const [key, value] of Object.entries(dark.colors)) document.documentElement.style.setProperty(`--vscode-${key.replaceAll('.', '-')}`, value); +export const themeVariable = key => `--vscode-${key.replaceAll('.', '-')}`; +// VS Code falls back to its terminal defaults when a theme leaves terminal.ansi* unset. +const colors = { ...parse(sources['../vendor/vscode/terminal-colors.json']).colors, ...dark.colors }; +for (const [key, value] of Object.entries(colors)) document.documentElement.style.setProperty(themeVariable(key), value); class TokenState { constructor(stack) { this.stack = stack; } diff --git a/bundle/src/main.js b/bundle/src/main.js index b71ed27..991ef06 100644 --- a/bundle/src/main.js +++ b/bundle/src/main.js @@ -3,7 +3,8 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import { open, save, message } from '@tauri-apps/plugin-dialog'; import { KeyCode, KeyMod } from 'monaco-editor/editor/editor.api.js'; import { StandardKeyboardEvent } from 'monaco-editor/base/browser/keyboardEvent.js'; -import { createEditor } from './editor.js'; +import { createEditor, themeVariable } from './editor.js'; +import { createAnsiParser } from './ansi.js'; import './style.css'; const status = document.querySelector('#status'); @@ -34,26 +35,39 @@ async function runFile() { output.textContent = ''; status.textContent = 'Running'; const decoders = { stdout: new TextDecoder(), stderr: new TextDecoder() }; - let pending = ''; + // Only compiler diagnostics on stderr are colored; program output stays verbatim. + const diagnostics = createAnsiParser(); + const pending = document.createDocumentFragment(); let frame = null; function flush() { frame = null; - if (!pending) return; - output.append(document.createTextNode(pending)); - pending = ''; + if (!pending.hasChildNodes()) return; + output.append(pending); output.scrollTop = output.scrollHeight; } - function append(text) { - pending += text; + function append(stream, text) { + for (const run of stream === 'stderr' ? diagnostics(text) : [{ text }]) { + if (!run.text) continue; + if (!run.bold && !run.color) { + if (pending.lastChild?.nodeType === Node.TEXT_NODE) pending.lastChild.appendData(run.text); + else pending.append(run.text); + continue; + } + const span = document.createElement('span'); + if (run.bold) span.style.fontWeight = 'bold'; + if (run.color) span.style.color = `var(${themeVariable(run.color)})`; + span.textContent = run.text; + pending.append(span); + } if (frame === null) frame = requestAnimationFrame(flush); } let finish; const finished = new Promise(resolve => { finish = resolve; }); const channel = new Channel(event => { if (event.event === 'data') { - append(decoders[event.stream].decode(Uint8Array.from(event.bytes), { stream: true })); + append(event.stream, decoders[event.stream].decode(Uint8Array.from(event.bytes), { stream: true })); } else if (event.event === 'finished') { - for (const decoder of Object.values(decoders)) append(decoder.decode()); + for (const [stream, decoder] of Object.entries(decoders)) append(stream, decoder.decode()); if (frame !== null) cancelAnimationFrame(frame); flush(); status.textContent = event.stopped ? 'Stopped' : `Exit code: ${event.code ?? 'terminated'}`; diff --git a/bundle/vendor/vscode/terminal-colors.json b/bundle/vendor/vscode/terminal-colors.json new file mode 100644 index 0000000..757a621 --- /dev/null +++ b/bundle/vendor/vscode/terminal-colors.json @@ -0,0 +1,23 @@ +// Dark defaults that VS Code's terminal uses when a theme leaves terminal.ansi* unset. +// Values from src/vs/workbench/contrib/terminal/common/terminalColorRegistry.ts at commit +// f93fdeb454a0f0fafe5f310408cec48dd3b6f58a, under the MIT license in LICENSE.txt. +{ + "colors": { + "terminal.ansiBlack": "#000000", + "terminal.ansiRed": "#cd3131", + "terminal.ansiGreen": "#0DBC79", + "terminal.ansiYellow": "#e5e510", + "terminal.ansiBlue": "#2472c8", + "terminal.ansiMagenta": "#bc3fbc", + "terminal.ansiCyan": "#11a8cd", + "terminal.ansiWhite": "#e5e5e5", + "terminal.ansiBrightBlack": "#666666", + "terminal.ansiBrightRed": "#f14c4c", + "terminal.ansiBrightGreen": "#23d18b", + "terminal.ansiBrightYellow": "#f5f543", + "terminal.ansiBrightBlue": "#3b8eea", + "terminal.ansiBrightMagenta": "#d670d6", + "terminal.ansiBrightCyan": "#29b8db", + "terminal.ansiBrightWhite": "#e5e5e5" + } +} |
