From 9f2237e11d00b8d46f473540bdc487d804f5e9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?John=20Bj=C3=B6rk?= Date: Fri, 30 Jan 2026 20:52:15 +0100 Subject: [PATCH 01/21] Prebake in settings Update prebake without reload --- packages/codemirror/codemirror.mjs | 85 ++++++++++++- packages/codemirror/index.mjs | 2 +- packages/codemirror/keybindings.mjs | 28 ++++- packages/transpiler/index.mjs | 1 + packages/transpiler/transpiler.mjs | 7 +- .../repl/components/button/action-button.jsx | 29 ++++- .../panel/ImportPrebakeScriptButton.jsx | 30 +++-- .../src/repl/components/panel/SettingsTab.jsx | 114 +++++++++++++++--- website/src/repl/useReplContext.jsx | 14 ++- website/src/repl/util.mjs | 1 + website/src/settings.mjs | 18 ++- 11 files changed, 292 insertions(+), 37 deletions(-) diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs index 6014b9b0a..d6763700e 100644 --- a/packages/codemirror/codemirror.mjs +++ b/packages/codemirror/codemirror.mjs @@ -19,7 +19,7 @@ import { basicSetup } from './basicSetup.mjs'; import { evalBlock } from './block_utilities.mjs'; import { flash, isFlashEnabled } from './flash.mjs'; import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs'; -import { keybindings } from './keybindings.mjs'; +import { keybindings, prebakeField } from './keybindings.mjs'; import { jumpToCharacter } from './labelJump.mjs'; import { getSliderWidgets, sliderPlugin, updateSliderWidgets } from './slider.mjs'; import { activateTheme, initTheme, theme } from './themes.mjs'; @@ -412,6 +412,10 @@ export class StrudelMirror { setAutocompletionEnabled(enabled) { this.reconfigureExtension('isAutoCompletionEnabled', enabled); } + updatePrebake(prebake) { + logger('[repl] prebake updated'); + this.prebaked = prebake(); + } updateSettings(settings) { this.setFontSize(settings.fontSize); this.setFontFamily(settings.fontFamily); @@ -472,6 +476,85 @@ export class StrudelMirror { } } +export class PrebakeCodeMirror { + constructor(initialCode, storePrebake, containerRef, editorRef, settings) { + this.storePrebake = storePrebake; + const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()])); + const initialSettings = Object.keys(compartments).map((key) => + compartments[key].of(extensions[key](parseBooleans(settings[key]))), + ); + initTheme(settings.theme); + let state = EditorState.create({ + doc: initialCode, + extensions: [ + ...initialSettings, + basicSetup, + javascript(), + javascriptLanguage.data.of({ + closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] }, + bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] }, + }), + syntaxHighlighting(defaultHighlightStyle), + EditorView.updateListener.of((v) => { + if (v.docChanged) { + this.code = v.state.doc.toString(); + } + }), + drawSelection({ cursorBlinkRate: 0 }), + Prec.highest( + keymap.of([ + { + key: 'Ctrl-Enter', + mac: 'Meta-Enter', + run: () => { + this.savePrebake(); + }, + }, + { + key: 'Alt-Enter', + run: () => { + this.savePrebake(); + }, + }, + ]), + ), + prebakeField, + ], + }); + editorRef.current = state; + this.code = initialCode; + this.view = new EditorView({ + state, + parent: containerRef.current, + }); + } + + async savePrebake() { + flash(this.view); + this.storePrebake(this.code); + logger('[prebake] prebake saved'); + } + + toggleComment() { + try { + // Honor selections; toggleLineComment handles both selections and + // single line + toggleLineComment(this.view); + } catch (err) { + console.error('Error handling repl-toggle-comment event', err); + } + } + + setCode(code) { + const changes = { + from: 0, + to: this.view.state.doc.length, + insert: code, + }; + this.view.dispatch({ changes }); + } +} + export function parseBooleans(value) { return { true: true, false: false }[value] ?? value; } diff --git a/packages/codemirror/index.mjs b/packages/codemirror/index.mjs index 2d5b3de21..ea78f8221 100644 --- a/packages/codemirror/index.mjs +++ b/packages/codemirror/index.mjs @@ -4,4 +4,4 @@ export * from './flash.mjs'; export * from './slider.mjs'; export * from './themes.mjs'; export * from './widget.mjs'; -export { Vim } from './keybindings.mjs'; +export { Vim, prebakeField } from './keybindings.mjs'; diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs index b8d9a6167..674216fb1 100644 --- a/packages/codemirror/keybindings.mjs +++ b/packages/codemirror/keybindings.mjs @@ -1,5 +1,5 @@ import { defaultKeymap } from '@codemirror/commands'; -import { Prec, EditorState } from '@codemirror/state'; +import { Prec, EditorState, StateField } from '@codemirror/state'; import { keymap, ViewPlugin } from '@codemirror/view'; // import { searchKeymap } from '@codemirror/search'; import { emacs } from '@replit/codemirror-emacs'; @@ -9,6 +9,15 @@ import { vscodeKeymap } from '@replit/codemirror-vscode-keymap'; import { helix, commands } from 'codemirror-helix'; import { logger } from '@strudel/core'; +export const prebakeField = StateField.define({ + create() { + return 'PrebakeEditor'; + }, + update(value, _tr) { + return value; + }, +}); + const vscodePlugin = ViewPlugin.fromClass( class { constructor() {} @@ -98,7 +107,11 @@ try { Vim.defineAction('strudelToggleComment', (cm) => { const view = cm?.view || cm; try { - const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true }); + const toggleEventName = + view?.cm6?.state?.field(prebakeField, false) !== undefined + ? 'prebake-toggle-comment' + : 'repl-toggle-comment'; + const ev = new CustomEvent(toggleEventName, { detail: { source: 'vim', view }, cancelable: true }); document.dispatchEvent(ev); } catch (e) { console.error('strudelToggleComment dispatch failed', e); @@ -119,6 +132,17 @@ try { // :w to evaluate Vim.defineEx('write', 'w', (cm) => { const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself + const isPrebake = view?.cm6?.state?.field(prebakeField, false) !== undefined; + if (isPrebake) { + let prebakeEventHandled = false; + try { + const ev = new CustomEvent('prebake-evaluate', { detail: { source: 'vim', view }, cancelable: true }); + prebakeEventHandled = document.dispatchEvent(ev) === false; // false means preventDefault was called + return; + } catch (e) { + console.error('Error dispatching repl-evaluate event', e); + } + } try { view?.focus?.(); // Let the app know this came from Vim :w diff --git a/packages/transpiler/index.mjs b/packages/transpiler/index.mjs index 484c3e70c..9ce11a9ff 100644 --- a/packages/transpiler/index.mjs +++ b/packages/transpiler/index.mjs @@ -3,3 +3,4 @@ import { transpiler } from './transpiler.mjs'; export * from './transpiler.mjs'; export const evaluate = (code) => _evaluate(code, transpiler); +export const evaluateUserPrebake = (code) => _evaluate(code, transpiler, { prebake: true }); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 210d40852..031a7e99f 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -25,6 +25,7 @@ export function transpiler(input, options = {}) { emitMiniLocations = true, emitWidgets = true, blockBased = false, + prebake = false, range = [], } = options; @@ -260,7 +261,9 @@ export function transpiler(input, options = {}) { // For block-based eval, add silence as the return value when block ends with declaration body.push(silenceExpression); } else { - throw new Error('unexpected ast format without body expression'); + if (!prebake) { + throw new Error('unexpected ast format without body expression'); + } } } @@ -273,7 +276,7 @@ export function transpiler(input, options = {}) { } // add return to last statement - if (addReturn) { + if (addReturn && !prebake) { const { expression } = body[body.length - 1]; body[body.length - 1] = { type: 'ReturnStatement', diff --git a/website/src/repl/components/button/action-button.jsx b/website/src/repl/components/button/action-button.jsx index 245cf961e..0143e5f90 100644 --- a/website/src/repl/components/button/action-button.jsx +++ b/website/src/repl/components/button/action-button.jsx @@ -17,22 +17,39 @@ export function SpecialActionButton(props) { ); } -export function ActionInput({ label, className, ...props }) { - return ( -