diff --git a/packages/codemirror/block_utilities.mjs b/packages/codemirror/block_utilities.mjs index 0d13259a5..fd3dbedcd 100644 --- a/packages/codemirror/block_utilities.mjs +++ b/packages/codemirror/block_utilities.mjs @@ -44,7 +44,7 @@ export const evalBlock = (strudelMirror) => { if (block) { // Flash the block being evaluated strudelMirror.flash(200, { from: a, to: b }); - strudelMirror.repl.evaluate(block, true, true, { range }); + strudelMirror.repl.evaluateBlock(block, true, { range }); } } return true; diff --git a/packages/core/repl.mjs b/packages/core/repl.mjs index d1bb1b1ad..e3efdd3e5 100644 --- a/packages/core/repl.mjs +++ b/packages/core/repl.mjs @@ -187,6 +187,48 @@ export function repl({ }; setTime(() => scheduler.now()); // TODO: refactor? + // Helper function to apply pattern transformations (solo, each, all) + // this should be abstracted more + function applyPatternTransforms(pattern) { + const allPatterns = Object.values(pPatterns); + + if (allPatterns.length) { + let patterns = []; + let soloActive = false; + for (const [key, value] of Object.entries(pPatterns)) { + // handle soloed patterns ex: S$: s("bd!4") + const isSolod = key.length > 1 && key.startsWith('S'); + if (isSolod && soloActive === false) { + // first time we see a soloed pattern, clear existing patterns + patterns = []; + soloActive = true; + } + if (!soloActive || (soloActive && isSolod)) { + const valWithState = value.withState((state) => state.setControls({ id: key })); + patterns.push(valWithState); + } + } + if (eachTransform) { + // Explicit lambda so only element (not index and array) are passed + patterns = patterns.map((x) => eachTransform(x)); + } + pattern = stack(...patterns); + } else if (eachTransform) { + pattern = eachTransform(pattern); + } + if (allTransforms.length) { + for (const transform of allTransforms) { + pattern = transform(pattern); + } + } + + if (!isPattern(pattern)) { + pattern = silence; + } + + return pattern; + } + const stop = () => { codeBlocks = {}; blockPatterns.clear(); @@ -306,7 +348,7 @@ export function repl({ }); }; - const evaluate = async (code, autostart = true, blockBased = false, options = {}) => { + const evaluate = async (code, autostart = true) => { if (!code) { throw new Error('no code to evaluate'); } @@ -314,20 +356,60 @@ export function repl({ updateState({ code, pending: true }); await injectPatternMethods(); setTime(() => scheduler.now()); // TODO: refactor? - await beforeEval?.({ code, blockBased }); + await beforeEval?.({ code, blockBased: false }); + allTransforms = []; // reset all transforms + + codeBlocks = {}; + hush(); + + if (mondo) { + code = `mondolang\`${code}\``; + } + + let { pattern, meta } = await _evaluate(code, transpiler, transpilerOptions); + + pattern = applyPatternTransforms(pattern); + + logger(`[eval] code updated`); + pattern = await setPattern(pattern, autostart); + updateState({ + miniLocations: meta?.miniLocations || [], + widgets: meta?.widgets || [], + sliders: meta?.sliders || [], + activeCode: code, + pattern, + evalError: undefined, + schedulerError: undefined, + pending: false, + }); + + afterEval?.({ code, pattern, meta, range: undefined, widgetRemoved: false }); + return pattern; + } catch (err) { + logger(`[eval] error: ${err.message}`, 'error'); + console.error(err); + updateState({ evalError: err, pending: false }); + onEvalError?.(err); + } + }; + + const evaluateBlock = async (code, autostart = true, options = {}) => { + if (!code) { + throw new Error('no code to evaluate'); + } + try { + updateState({ code, pending: true }); + await injectPatternMethods(); + setTime(() => scheduler.now()); // TODO: refactor? + await beforeEval?.({ code, blockBased: true }); allTransforms = []; // reset all transforms const transpilerOptionsWithBlock = { ...transpilerOptions, - blockBased, + blockBased: true, range: options.range || [], }; - if (!blockBased) { - codeBlocks = {}; - hush(); - } - if (mondo) { code = `mondolang\`${code}\``; } @@ -337,101 +419,61 @@ export function repl({ // Track activeVisualizer cleanup: check if any block's visualizer was removed let widgetRemoved = false; - if (blockBased) { - const labels = meta.labels || []; + const labels = meta.labels || []; - // Store code blocks in dictionary using labels as keys - if (labels.length > 0) { - for (let i = 0; i < labels.length; i++) { - // processLabeledBlock(labels, i, code, options, meta); - // processing transpiler output instead of code is simply to avoid - // extra regex in detecting whether or not an inline widget has been commented out - processLabeledBlock(labels, i, meta.output, options, meta); - } - } else if (pattern !== silence) { - // variable/function declarations that return silence are allowed, - // but anonymous pattern blocks pose an issue for block-based evaluation - // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder - - // it's very common for users to write code prefixed with '$' - // but to modify and override existing patterns, the patterns must be labeled, - // otherwise we'll have no idea of which pattern is being overridden - - // (we probably need to update the docs on this) - - // we could easily enable it, but it would confuse a lot of people - throw new Error( - 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)', - ); + // Store code blocks in dictionary using labels as keys + if (labels.length > 0) { + for (let i = 0; i < labels.length; i++) { + // processing transpiler output instead of code is simply to avoid + // extra regex in detecting whether or not an inline widget has been commented out + processLabeledBlock(labels, i, meta.output, options, meta); } + } else if (pattern !== silence) { + // variable/function declarations that return silence are allowed, + // but anonymous pattern blocks pose an issue for block-based evaluation + // if an anonymous pattern is evaluated multiple times it will just stack and get louder and louder - // For block-based evaluation, collect from all blocks - // The highlight/widget/slider systems will handle selective updates based on the range - meta.miniLocations = collectFromBlocks('miniLocations'); - meta.widgets = collectFromBlocks('widgets'); - meta.sliders = collectFromBlocks('sliders'); + // it's very common for users to write code prefixed with '$' + // but to modify and override existing patterns, the patterns must be labeled, + // otherwise we'll have no idea of which pattern is being overridden - // Track activeVisualizer cleanup: check if any block's visualizer was removed - const blocksToUpdate = labels.map((label) => label.name); + // (we probably need to update the docs on this) - for (const [key, block] of Object.entries(codeBlocks)) { - if (blocksToUpdate.includes(key)) { - // This block was just updated - if (block.activeVisualizer !== null) { - // Block now has a visualizer, update tracking - lastActiveVisualizerLabel = key; - } else if (lastActiveVisualizerLabel === key) { - // This block lost its visualizer, trigger cleanup - widgetRemoved = true; - lastActiveVisualizerLabel = null; - } - } - } - } - - const allPatterns = Object.values(pPatterns); - - if (blockBased) { - pPatterns = Object.fromEntries( - Object.entries(pPatterns).filter(([key]) => { - return Object.keys(codeBlocks).includes(key); - }), + // we could easily enable it, but it would confuse a lot of people + throw new Error( + 'anonymous labels disabled for block based evaluation (see https://strudel.cc/blog/#label-notation)', ); } - if (allPatterns.length) { - let patterns = []; - let soloActive = false; - for (const [key, value] of Object.entries(pPatterns)) { - // handle soloed patterns ex: S$: s("bd!4") - const isSolod = key.length > 1 && key.startsWith('S'); - if (isSolod && soloActive === false) { - // first time we see a soloed pattern, clear existing patterns - patterns = []; - soloActive = true; + meta.miniLocations = collectFromBlocks('miniLocations'); + meta.widgets = collectFromBlocks('widgets'); + meta.sliders = collectFromBlocks('sliders'); + + // Track activeVisualizer cleanup: check if any block's visualizer was removed + const blocksToUpdate = labels.map((label) => label.name); + + // this is the hackiest bit + for (const [key, block] of Object.entries(codeBlocks)) { + if (blocksToUpdate.includes(key)) { + // This block was just updated + if (block.activeVisualizer !== null) { + // Block now has a visualizer, update tracking + lastActiveVisualizerLabel = key; + } else if (lastActiveVisualizerLabel === key) { + // This block lost its visualizer, trigger cleanup + widgetRemoved = true; + lastActiveVisualizerLabel = null; } - if (!soloActive || (soloActive && isSolod)) { - const valWithState = value.withState((state) => state.setControls({ id: key })); - patterns.push(valWithState); - } - } - if (eachTransform) { - // Explicit lambda so only element (not index and array) are passed - patterns = patterns.map((x) => eachTransform(x)); - } - pattern = stack(...patterns); - } else if (eachTransform) { - pattern = eachTransform(pattern); - } - if (allTransforms.length) { - for (const transform of allTransforms) { - pattern = transform(pattern); } } - if (!isPattern(pattern)) { - pattern = silence; - } + pPatterns = Object.fromEntries( + Object.entries(pPatterns).filter(([key]) => { + return Object.keys(codeBlocks).includes(key); + }), + ); + + pattern = applyPatternTransforms(pattern); logger(`[eval] code updated`); pattern = await setPattern(pattern, autostart); @@ -455,24 +497,25 @@ export function repl({ onEvalError?.(err); } }; + const setCode = (code) => updateState({ code }); - return { scheduler, evaluate, start, stop, pause, setCps, setPattern, setCode, toggle, state }; + return { scheduler, evaluate, evaluateBlock, start, stop, pause, setCps, setPattern, setCode, toggle, state }; } export const getTrigger = ({ getTime, defaultOutput }) => - async (hap, deadline, duration, cps, t) => { - // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) - // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 - try { - if (!hap.context.onTrigger || !hap.context.dominantTrigger) { - await defaultOutput(hap, deadline, duration, cps, t); - } - if (hap.context.onTrigger) { - // call signature of output / onTrigger is different... - await hap.context.onTrigger(hap, getTime(), cps, t); - } - } catch (err) { - errorLogger(err, 'getTrigger'); + async (hap, deadline, duration, cps, t) => { + // ^ this signature is different from hap.context.onTrigger, as set by Pattern.onTrigger(onTrigger) + // TODO: get rid of deadline after https://codeberg.org/uzu/strudel/pulls/1004 + try { + if (!hap.context.onTrigger || !hap.context.dominantTrigger) { + await defaultOutput(hap, deadline, duration, cps, t); } - }; + if (hap.context.onTrigger) { + // call signature of output / onTrigger is different... + await hap.context.onTrigger(hap, getTime(), cps, t); + } + } catch (err) { + errorLogger(err, 'getTrigger'); + } + }; diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index e7d4acfa1..db08c60b5 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -172,7 +172,7 @@ export function transpiler(input, options = {}) { }); } }, - leave(node, parent, prop, index) { }, + leave(node, parent, prop, index) {}, }); let { body } = ast; diff --git a/test/examples.test.mjs b/test/examples.test.mjs index 18f12bb42..a3dec47aa 100644 --- a/test/examples.test.mjs +++ b/test/examples.test.mjs @@ -20,7 +20,7 @@ const skippedExamples = [ 'accelerationX', 'defaultmidimap', 'midimaps', - 'clearScope' + 'clearScope', ]; describe('runs examples', () => {