Merge pull request 'Edit prebake in settings' (#1958) from JohnBjrk/strudel:edit-prebake-in-settings into main

Reviewed-on: https://codeberg.org/uzu/strudel/pulls/1958
This commit is contained in:
froos 2026-02-13 17:13:37 +01:00
commit 6f108f7f28
18 changed files with 338 additions and 119 deletions

View file

@ -293,6 +293,9 @@ export class StrudelMirror {
// Handle global evaluation requests (e.g., from Vim :w) // Handle global evaluation requests (e.g., from Vim :w)
this.onEvaluateRequest = (e) => { this.onEvaluateRequest = (e) => {
try { try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
// Evaluate current editor on repl-evaluate // Evaluate current editor on repl-evaluate
logger('[repl] evaluate via event'); logger('[repl] evaluate via event');
this.evaluate(); this.evaluate();
@ -307,6 +310,9 @@ export class StrudelMirror {
// Toggle comments requested from Vim (gc) // Toggle comments requested from Vim (gc)
this.onToggleComment = (e) => { this.onToggleComment = (e) => {
try { try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
// Honor selections; toggleLineComment handles both selections and // Honor selections; toggleLineComment handles both selections and
// single line // single line
toggleLineComment(this.editor); toggleLineComment(this.editor);
@ -347,6 +353,9 @@ export class StrudelMirror {
// Listen for global stop requests (e.g., from Vim :q) // Listen for global stop requests (e.g., from Vim :q)
onStopRequest = (e) => { onStopRequest = (e) => {
try { try {
if (e.detail.view !== this.editor) {
return; // ignore events from other editors
}
this.stop(); this.stop();
e?.cancelable && e.preventDefault?.(); e?.cancelable && e.preventDefault?.();
} catch (err) { } catch (err) {

View file

@ -5,3 +5,4 @@ export * from './slider.mjs';
export * from './themes.mjs'; export * from './themes.mjs';
export * from './widget.mjs'; export * from './widget.mjs';
export { Vim } from './keybindings.mjs'; export { Vim } from './keybindings.mjs';
export * from './basicSetup.mjs';

View file

@ -96,7 +96,7 @@ try {
// internal actions and works with current selections/visual mode. // internal actions and works with current selections/visual mode.
try { try {
Vim.defineAction('strudelToggleComment', (cm) => { Vim.defineAction('strudelToggleComment', (cm) => {
const view = cm?.view || cm; const view = cm.cm6;
try { try {
const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true }); const ev = new CustomEvent('repl-toggle-comment', { detail: { source: 'vim', view }, cancelable: true });
document.dispatchEvent(ev); document.dispatchEvent(ev);
@ -112,15 +112,16 @@ try {
// :q to pause/stop // :q to pause/stop
Vim.defineEx('quit', 'q', (cm) => { Vim.defineEx('quit', 'q', (cm) => {
const view = cm?.view || cm; const view = cm.cm6;
logger('[vim] :q — stopping repl');
replStop(view); replStop(view);
}); });
// :w to evaluate // :w to evaluate
Vim.defineEx('write', 'w', (cm) => { Vim.defineEx('write', 'w', (cm) => {
const view = cm?.view || cm; // CM6 Vim passes either an object with view or the view itself const view = cm.cm6;
try { try {
view?.focus?.(); view.focus?.();
// Let the app know this came from Vim :w // Let the app know this came from Vim :w
try { try {
logger('[vim] :w — evaluating code'); logger('[vim] :w — evaluating code');

View file

@ -32,13 +32,13 @@
}, },
"homepage": "https://codeberg.org/uzu/strudel#readme", "homepage": "https://codeberg.org/uzu/strudel#readme",
"dependencies": { "dependencies": {
"@codemirror/autocomplete": "^6.18.4", "@codemirror/autocomplete": "catalog:",
"@codemirror/commands": "^6.8.0", "@codemirror/commands": "catalog:",
"@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-javascript": "catalog:",
"@codemirror/language": "^6.10.8", "@codemirror/language": "catalog:",
"@codemirror/search": "^6.5.8", "@codemirror/search": "catalog:",
"@codemirror/state": "^6.5.1", "@codemirror/state": "catalog:",
"@codemirror/view": "^6.36.2", "@codemirror/view": "catalog:",
"@lezer/highlight": "^1.2.1", "@lezer/highlight": "^1.2.1",
"@nanostores/persistent": "^0.10.2", "@nanostores/persistent": "^0.10.2",
"@replit/codemirror-emacs": "^6.1.0", "@replit/codemirror-emacs": "^6.1.0",

View file

@ -2,4 +2,4 @@ import { evaluate as _evaluate } from '@strudel/core';
import { transpiler } from './transpiler.mjs'; import { transpiler } from './transpiler.mjs';
export * from './transpiler.mjs'; export * from './transpiler.mjs';
export const evaluate = (code) => _evaluate(code, transpiler); export const evaluate = (code, transpilerOptions) => _evaluate(code, transpiler, transpilerOptions);

View file

@ -256,12 +256,7 @@ export function transpiler(input, options = {}) {
body.push(silenceExpression); body.push(silenceExpression);
} else if (!body?.[body.length - 1]?.expression) { } else if (!body?.[body.length - 1]?.expression) {
// Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration) // Last statement is not an expression (e.g., VariableDeclaration, FunctionDeclaration)
if (blockBased) { body.push(silenceExpression);
// 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');
}
} }
// For block-based eval, add scope assignments before the return statement // For block-based eval, add scope assignments before the return statement

53
pnpm-lock.yaml generated
View file

@ -4,6 +4,30 @@ settings:
autoInstallPeers: true autoInstallPeers: true
excludeLinksFromLockfile: false excludeLinksFromLockfile: false
catalogs:
default:
'@codemirror/autocomplete':
specifier: ^6.18.4
version: 6.18.4
'@codemirror/commands':
specifier: ^6.8.0
version: 6.8.0
'@codemirror/lang-javascript':
specifier: ^6.2.2
version: 6.2.2
'@codemirror/language':
specifier: ^6.10.8
version: 6.10.8
'@codemirror/search':
specifier: ^6.5.8
version: 6.5.8
'@codemirror/state':
specifier: ^6.5.1
version: 6.5.1
'@codemirror/view':
specifier: ^6.36.2
version: 6.36.2
importers: importers:
.: .:
@ -171,25 +195,25 @@ importers:
packages/codemirror: packages/codemirror:
dependencies: dependencies:
'@codemirror/autocomplete': '@codemirror/autocomplete':
specifier: ^6.18.4 specifier: 'catalog:'
version: 6.18.4 version: 6.18.4
'@codemirror/commands': '@codemirror/commands':
specifier: ^6.8.0 specifier: 'catalog:'
version: 6.8.0 version: 6.8.0
'@codemirror/lang-javascript': '@codemirror/lang-javascript':
specifier: ^6.2.2 specifier: 'catalog:'
version: 6.2.2 version: 6.2.2
'@codemirror/language': '@codemirror/language':
specifier: ^6.10.8 specifier: 'catalog:'
version: 6.10.8 version: 6.10.8
'@codemirror/search': '@codemirror/search':
specifier: ^6.5.8 specifier: 'catalog:'
version: 6.5.8 version: 6.5.8
'@codemirror/state': '@codemirror/state':
specifier: ^6.5.1 specifier: 'catalog:'
version: 6.5.1 version: 6.5.1
'@codemirror/view': '@codemirror/view':
specifier: ^6.36.2 specifier: 'catalog:'
version: 6.36.2 version: 6.36.2
'@lezer/highlight': '@lezer/highlight':
specifier: ^1.2.1 specifier: ^1.2.1
@ -705,6 +729,21 @@ importers:
'@astrojs/tailwind': '@astrojs/tailwind':
specifier: ^5.1.5 specifier: ^5.1.5
version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17) version: 5.1.5(astro@5.1.9(@types/node@22.10.10)(jiti@2.4.2)(lightningcss@1.29.1)(rollup@2.79.2)(terser@5.37.0)(typescript@5.7.3)(yaml@2.7.0))(tailwindcss@3.4.17)
'@codemirror/commands':
specifier: 'catalog:'
version: 6.8.0
'@codemirror/lang-javascript':
specifier: 'catalog:'
version: 6.2.2
'@codemirror/language':
specifier: 'catalog:'
version: 6.10.8
'@codemirror/state':
specifier: 'catalog:'
version: 6.5.1
'@codemirror/view':
specifier: 'catalog:'
version: 6.36.2
'@docsearch/css': '@docsearch/css':
specifier: ^3.8.3 specifier: ^3.8.3
version: 3.8.3 version: 3.8.3

View file

@ -10,3 +10,12 @@ onlyBuiltDependencies:
- sharp - sharp
- tree-sitter - tree-sitter
- tree-sitter-haskell - tree-sitter-haskell
catalog:
"@codemirror/commands": "^6.8.0"
"@codemirror/language": "^6.10.8"
"@codemirror/lang-javascript": "^6.2.2"
"@codemirror/state": "^6.5.1"
"@codemirror/view": "^6.36.2"
"@codemirror/autocomplete": "^6.18.4"
"@codemirror/search": "^6.5.8"

View file

@ -17,6 +17,11 @@
"@astrojs/react": "^4.1.6", "@astrojs/react": "^4.1.6",
"@astrojs/rss": "^4.0.11", "@astrojs/rss": "^4.0.11",
"@astrojs/tailwind": "^5.1.5", "@astrojs/tailwind": "^5.1.5",
"@codemirror/commands": "catalog:",
"@codemirror/language": "catalog:",
"@codemirror/lang-javascript": "catalog:",
"@codemirror/state": "catalog:",
"@codemirror/view": "catalog:",
"@docsearch/css": "^3.8.3", "@docsearch/css": "^3.8.3",
"@docsearch/react": "^3.8.3", "@docsearch/react": "^3.8.3",
"@headlessui/react": "^2.2.0", "@headlessui/react": "^2.2.0",

View file

@ -10,50 +10,50 @@
opacity: 0.5; opacity: 0.5;
} }
#code .cm-scroller { .code-container .cm-scroller {
padding-top: 10px !important; padding-top: 10px !important;
height: 100%; height: 100%;
font-family: inherit; font-family: inherit;
} }
#code .cm-content { .code-container .cm-content {
padding-bottom: 50vh; padding-bottom: 50vh;
} }
#code .cm-line > * { .code-container .cm-line > * {
background: var(--lineBackground); background: var(--lineBackground);
} }
#code .cm-editor { .code-container .cm-editor {
background-color: transparent !important; background-color: transparent !important;
height: 100%; height: 100%;
z-index: 11; z-index: 11;
} }
#code .cm-theme { .code-container .cm-theme {
width: 100%; width: 100%;
height: 100%; height: 100%;
} }
#code .cm-theme-light { .code-container .cm-theme-light {
width: 100%; width: 100%;
} }
#code .cm-cursorLayer { .code-container .cm-cursorLayer {
animation-name: inherit !important; animation-name: inherit !important;
} }
#code .cm-cursor { .code-container .cm-cursor {
border-left: 2px solid currentcolor !important; border-left: 2px solid currentcolor !important;
} }
#code .cm-foldGutter { .code-container .cm-foldGutter {
display: none !important; display: none !important;
} }
#code .cm-focused { .code-container .cm-focused {
outline: none; outline: none;
} }
#code .cm-matchingBracket { .code-container .cm-matchingBracket {
text-decoration: underline 0.18rem; text-decoration: underline 0.18rem;
text-underline-offset: 0.22rem; text-underline-offset: 0.22rem;
} }

View file

@ -8,8 +8,7 @@ export function Code(Props) {
return ( return (
<section <section
className={'text-gray-100 cursor-text pb-0 overflow-auto grow z-10'} className={'code-container text-gray-100 cursor-text pb-0 overflow-auto grow z-10'}
id="code"
ref={(el) => { ref={(el) => {
containerRef.current = el; containerRef.current = el;
if (!editorRef.current) { if (!editorRef.current) {

View file

@ -8,31 +8,3 @@ export function ActionButton({ children, label, labelIsHidden, className, ...but
</button> </button>
); );
} }
export function SpecialActionButton(props) {
const { className, ...buttonProps } = props;
return (
<ActionButton {...buttonProps} className={cx('bg-background p-2 max-w-[300px] hover:opacity-50', className)} />
);
}
export function ActionInput({ label, className, ...props }) {
return (
<label className={cx('inline-flex items-center cursor-pointer ', className)}>
<input {...props} className="sr-only peer" />
<span className="inline-flex items-center peer-hover:opacity-50">{label}</span>
</label>
);
}
export function SpecialActionInput({ className, ...props }) {
return (
<ActionInput
{...props}
className={className}
label={<span className="bg-background p-2 max-w-[300px]">{props.label}</span>}
/>
);
}

View file

@ -1,42 +1,50 @@
import { errorLogger } from '@strudel/core'; import { errorLogger } from '@strudel/core';
import { useSettings, storePrebakeScript } from '../../../settings.mjs'; import { storePrebakeScript } from '../../../settings.mjs';
import { SpecialActionInput } from '../button/action-button'; import { confirmDialog } from '@src/repl/util.mjs';
import { confirmDialog, SETTING_CHANGE_RELOAD_MSG } from '@src/repl/util.mjs';
async function importScript(script) { async function importScript(script, updateEditor) {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsText(script); reader.readAsText(script);
reader.onload = () => { reader.onload = () => {
const text = reader.result; const text = reader.result;
storePrebakeScript(text); storePrebakeScript(text);
updateEditor && updateEditor(text);
}; };
reader.onerror = () => { reader.onerror = () => {
errorLogger(new Error('failed to import prebake script'), 'importScript'); errorLogger(new Error('failed to import prebake script'), 'importScript');
}; };
} }
export function ImportPrebakeScriptButton() {
const settings = useSettings();
export async function exportScript(script) {
const blob = new Blob([script], { type: 'application/javascript' });
const downloadLink = document.createElement('a');
downloadLink.href = window.URL.createObjectURL(blob);
const date = new Date().toISOString().split('T')[0];
downloadLink.download = `prebake_${date}.strudel`;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
}
export function ImportPrebakeScriptButton({ updateEditor }) {
const handleChange = async (e) => {
const file = e.target.files[0];
const confirmed = await confirmDialog('Warning: This will overwrite the current prebake.\nContinue?');
if (!confirmed) {
return;
}
try {
await importScript(file, updateEditor);
} catch (e) {
errorLogger(e);
}
};
return ( return (
<SpecialActionInput <label className="space-x-1 inline-flex items-center cursor-pointer">
type="file" <input type="file" accept=".strudel,.js" className="sr-only peer" onChange={handleChange} />
label="import prebake script" <span className="inline-flex items-center peer-hover:opacity-50 text-xs max-w-[300px]">import</span>
accept=".strudel" </label>
onChange={async (e) => {
const file = e.target.files[0];
const confirmed = await confirmDialog(SETTING_CHANGE_RELOAD_MSG);
if (!confirmed) {
return;
}
try {
await importScript(file);
window.location.reload();
} catch (e) {
errorLogger(e);
}
}}
/>
); );
} }

View file

@ -1,18 +1,16 @@
import { defaultSettings, settingsMap, useSettings } from '../../../settings.mjs'; import { defaultSettings, settingsMap, useSettings, storePrebakeScript, setSettingsTab } from '../../../settings.mjs';
import { themes } from '@strudel/codemirror'; import { themes } from '@strudel/codemirror';
import { PrebakeCodeMirror } from '../../../repl/prebakeCodeMirror.mjs';
import { confirmAndReloadPage, isUdels } from '../../util.mjs'; import { confirmAndReloadPage, isUdels } from '../../util.mjs';
import { ButtonGroup } from './Forms.jsx'; import { ButtonGroup } from './Forms.jsx';
import { AudioDeviceSelector } from './AudioDeviceSelector.jsx'; import { AudioDeviceSelector } from './AudioDeviceSelector.jsx';
import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx'; import { AudioEngineTargetSelector } from './AudioEngineTargetSelector.jsx';
import { confirmDialog } from '../../util.mjs'; import { confirmDialog } from '../../util.mjs';
import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio'; import { DEFAULT_MAX_POLYPHONY, setMaxPolyphony, setMultiChannelOrbits } from '@strudel/webaudio';
import { SpecialActionButton } from '../button/action-button.jsx'; import { ActionButton } from '../button/action-button.jsx';
import { ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx'; import { exportScript, ImportPrebakeScriptButton } from './ImportPrebakeScriptButton.jsx';
import { useEffect, useRef } from 'react';
function cx(...classes) { import cx from '@src/cx.mjs';
// : Array<string | undefined>
return classes.filter(Boolean).join(' ');
}
const inputClass = const inputClass =
'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground'; 'bg-background text-xs h-8 max-h-8 border border-box rounded-0 text-foreground border-muted placeholder-muted focus:outline-none focus:ring-0 focus:border-foreground';
@ -123,7 +121,7 @@ const fontFamilyOptions = {
galactico: 'galactico', galactico: 'galactico',
}; };
export function SettingsTab({ started }) { function MainSettingsContent({ started }) {
const { const {
theme, theme,
keybindings, keybindings,
@ -149,13 +147,12 @@ export function SettingsTab({ started }) {
isTabIndentationEnabled, isTabIndentationEnabled,
isMultiCursorEnabled, isMultiCursorEnabled,
patternAutoStart, patternAutoStart,
includePrebakeScriptInShare,
isBlockBasedEvalEnabled, isBlockBasedEvalEnabled,
} = useSettings(); } = useSettings();
const shouldAlwaysSync = isUdels(); const shouldAlwaysSync = isUdels();
const canChangeAudioDevice = AudioContext.prototype.setSinkId != null; const canChangeAudioDevice = AudioContext.prototype.setSinkId != null;
return ( return (
<div className="p-4 text-foreground space-y-4 w-full" style={{ fontFamily }}> <div className="p-4 text-foreground space-y-4 w-full overflow-auto" style={{ fontFamily }}>
{canChangeAudioDevice && ( {canChangeAudioDevice && (
<FormItem label="Audio Output Device"> <FormItem label="Audio Output Device">
<AudioDeviceSelector <AudioDeviceSelector
@ -233,14 +230,6 @@ export function SettingsTab({ started }) {
/> />
</FormItem> </FormItem>
</div> </div>
<FormItem label="Prebake">
<ImportPrebakeScriptButton />
<Checkbox
label="Include prebake script in share"
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
value={includePrebakeScriptInShare}
/>
</FormItem>
<FormItem label="Keybindings"> <FormItem label="Keybindings">
<ButtonGroup <ButtonGroup
@ -346,7 +335,7 @@ export function SettingsTab({ started }) {
</FormItem> </FormItem>
<FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem> <FormItem label="Zen Mode">Try clicking the logo in the top left!</FormItem>
<FormItem label="Reset Settings"> <FormItem label="Reset Settings">
<SpecialActionButton <ActionButton
onClick={() => { onClick={() => {
confirmDialog('Sure?').then((r) => { confirmDialog('Sure?').then((r) => {
if (r) { if (r) {
@ -355,10 +344,70 @@ export function SettingsTab({ started }) {
} }
}); });
}} }}
className="bg-background p-2 max-w-[300px] hover:opacity-50"
> >
restore default settings restore default settings
</SpecialActionButton> </ActionButton>
</FormItem> </FormItem>
</div> </div>
); );
} }
function PrebakeSettingsContent() {
const { fontFamily, includePrebakeScriptInShare, prebakeScript } = useSettings();
const editorRef = useRef();
useEffect(() => {
return () => {
editorRef.current?.cleanup();
};
});
return (
<div className="flex flex-col h-full text-foreground w-full overflow-auto" style={{ fontFamily }}>
<div className="flex flex-col grow overflow-hidden h-full bg-background">
<section
className="pb-0 overflow-auto grow z-10 code-container"
ref={(el) => {
if (editorRef.current) {
return;
}
editorRef.current = new PrebakeCodeMirror(prebakeScript, (code) => storePrebakeScript(code), el);
}}
></section>
</div>
<div className="flex justify-between items-center border-t border-muted px-4 whitespace-nowrap">
<Checkbox
label="share with patterns"
className="whitespace-nowrap max-w-[200px]"
onChange={(cbEvent) => settingsMap.setKey('includePrebakeScriptInShare', cbEvent.target.checked)}
value={includePrebakeScriptInShare}
/>
<div className="py-2 flex flex-row items-center space-x-3 ">
<ImportPrebakeScriptButton updateEditor={(code) => editorRef?.current.setCode(code)} />
<ActionButton onClick={() => exportScript(prebakeScript)}>export</ActionButton>
<ActionButton onClick={() => editorRef.current?.savePrebake()}>save</ActionButton>
</div>
</div>
</div>
);
}
export function SettingsTab({ started }) {
const { settingsTab } = useSettings();
return (
<div className="w-full h-full text-foreground flex flex-col overflow-hidden">
<div className="px-2 shrink-0 h-8 space-x-4 flex max-w-full overflow-x-auto border-b border-muted">
<ButtonGroup
wrap
value={settingsTab}
onChange={(value) => setSettingsTab(value)}
items={{
settings: 'settings',
prebake: 'prebake',
}}
></ButtonGroup>
</div>
{settingsTab === 'settings' && <MainSettingsContent started={started} />}
{settingsTab === 'prebake' && <PrebakeSettingsContent />}
</div>
);
}

View file

@ -0,0 +1,115 @@
import { toggleLineComment } from '@codemirror/commands';
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { Compartment, EditorState, Prec } from '@codemirror/state';
import { drawSelection, EditorView, keymap } from '@codemirror/view';
import { logger } from '@strudel/core';
import { basicSetup, flash, initTheme, extensions, parseBooleans, codemirrorSettings } from '@strudel/codemirror';
import { evaluate } from '@strudel/transpiler';
export class PrebakeCodeMirror {
constructor(initialCode, storePrebake, container) {
const settings = codemirrorSettings.get();
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([
{
mac: 'Meta-Enter',
run: () => {
this.savePrebake();
},
},
{
key: 'Ctrl-Enter',
run: () => {
this.savePrebake();
},
},
{
key: 'Alt-Enter',
run: () => {
this.savePrebake();
},
},
]),
),
],
});
this.code = initialCode;
this.view = new EditorView({
state,
parent: container,
});
const handleSaveEvent = async (e) => {
if (e.detail.view !== this.view) {
return; // ignore events from other editors
}
await this.savePrebake();
e?.cancelable && e.preventDefault?.();
};
const handleToggleComment = (e) => {
if (e.detail.view !== this.view) {
return; // ignore events from other editors
}
this.toggleComment();
e?.cancelable && e.preventDefault?.();
};
document.addEventListener('repl-evaluate', handleSaveEvent);
document.addEventListener('repl-toggle-comment', handleToggleComment);
this.cleanup = () => {
document.removeEventListener('prebake-evaluate', handleSaveEvent);
document.removeEventListener('prebake-toggle-comment', handleToggleComment);
};
}
async savePrebake() {
flash(this.view);
this.storePrebake(this.code);
evaluate(this.code, { addReturn: false }); // run prebake
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 });
}
}

View file

@ -6,7 +6,7 @@ This program is free software: you can redistribute it and/or modify it under th
import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core'; import { code2hash, getPerformanceTimeSeconds, logger, silence } from '@strudel/core';
import { getDrawContext } from '@strudel/draw'; import { getDrawContext } from '@strudel/draw';
import { evaluate, transpiler } from '@strudel/transpiler'; import { transpiler, evaluate } from '@strudel/transpiler';
import { import {
getAudioContextCurrentTime, getAudioContextCurrentTime,
renderPatternAudio, renderPatternAudio,
@ -87,12 +87,12 @@ export function useReplContext() {
pattern: silence, pattern: silence,
drawTime, drawTime,
drawContext, drawContext,
prebake: async () => prebake: async () => {
Promise.all([modulesLoading, presets]).then(() => { await Promise.all([modulesLoading, presets]);
if (prebakeScript?.length) { if (prebakeScript) {
return evaluate(prebakeScript ?? ''); return evaluate(prebakeScript, { addReturn: false });
} }
}), },
onUpdateState: (state) => { onUpdateState: (state) => {
setReplState({ ...state }); setReplState({ ...state });
}, },

View file

@ -108,6 +108,7 @@ export function confirmDialog(msg) {
}); });
} }
export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?'; export const SETTING_CHANGE_RELOAD_MSG = 'Changing this setting requires the window to reload itself. OK?';
export function confirmAndReloadPage(onSuccess) { export function confirmAndReloadPage(onSuccess) {
confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => { confirmDialog(SETTING_CHANGE_RELOAD_MSG).then((r) => {
if (r == true) { if (r == true) {

View file

@ -18,6 +18,20 @@ export const soundFilterType = {
ALL: 'all', ALL: 'all',
}; };
const initialPrebakeScript = `// Prebake script
//
// This is code that is loaded before your pattern is run.
// You can use it to define custom functions to use in any pattern.
//
// This is an initial example script. You can edit it to add
// your own funtions.
//
// To use a script shared by some other user you can use
// the import-button or paste the script in this editor.
const ratchet = register('ratchet', (pat) => pat.sometimes(ply(2)))
`;
export const defaultSettings = { export const defaultSettings = {
activeFooter: 'intro', activeFooter: 'intro',
keybindings: 'codemirror', keybindings: 'codemirror',
@ -47,13 +61,14 @@ export const defaultSettings = {
isPanelPinned: false, isPanelPinned: false,
isPanelOpen: true, isPanelOpen: true,
userPatterns: '{}', userPatterns: '{}',
prebakeScript: '', prebakeScript: initialPrebakeScript,
audioEngineTarget: audioEngineTargets.webaudio, audioEngineTarget: audioEngineTargets.webaudio,
isButtonRowHidden: false, isButtonRowHidden: false,
isCSSAnimationDisabled: false, isCSSAnimationDisabled: false,
maxPolyphony: 128, maxPolyphony: 128,
multiChannelOrbits: false, multiChannelOrbits: false,
includePrebakeScriptInShare: true, includePrebakeScriptInShare: true,
settingsTab: 'settings',
}; };
let search = null; let search = null;
@ -115,6 +130,7 @@ export function useSettings() {
export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab); export const setActiveFooter = (tab) => settingsMap.setKey('activeFooter', tab);
export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool); export const setPanelPinned = (bool) => settingsMap.setKey('isPanelPinned', bool);
export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool); export const setIsPanelOpened = (bool) => settingsMap.setKey('isPanelOpen', bool);
export const setSettingsTab = (tab) => settingsMap.setKey('settingsTab', tab);
export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script); export const storePrebakeScript = (script) => settingsMap.setKey('prebakeScript', script);