From 0b5d905120e63cfee90c2e25c929a02b63d096b2 Mon Sep 17 00:00:00 2001 From: Felix Roos Date: Mon, 3 Jul 2023 05:15:32 +0200 Subject: [PATCH] fix: adaptive highlighting - transpiler now uses m function with globalOffset - patternifyAST now accepts global offset - patternifyAST now calls .withLoc with global leaf location - .withLoc replaces .withLocation + .withMiniLocation - simple locs (offsets) are now used everywhere - some tests fail, seems some haps have reordered... - wip: Repl still uses hardcoded updateMiniLocations - todo: find way to call updateMiniLocations dynamically --- packages/core/pattern.mjs | 20 +++++ packages/mini/mini.mjs | 48 ++++++++---- packages/mini/test/mini.test.mjs | 10 +-- packages/react/src/components/CodeMirror6.jsx | 53 ++++++++----- packages/react/src/hooks/useHighlighting.mjs | 2 +- packages/transpiler/test/transpiler.test.mjs | 6 +- packages/transpiler/transpiler.mjs | 74 ++++--------------- test/runtime.mjs | 3 +- website/src/repl/Repl.jsx | 2 +- 9 files changed, 108 insertions(+), 110 deletions(-) diff --git a/packages/core/pattern.mjs b/packages/core/pattern.mjs index 6396d97b..f7cb6ce8 100644 --- a/packages/core/pattern.mjs +++ b/packages/core/pattern.mjs @@ -472,6 +472,25 @@ export class Pattern { /** * Returns a new pattern with the given location information added to the * context of every hap. + * @param {Number} start start offset + * @param {Number} end end offset + * @returns Pattern + * @noAutocomplete + */ + withLoc(start, end) { + const location = { + start, + end, + }; + return this.withContext((context) => { + const locations = (context.locations || []).concat([location]); + return { ...context, locations }; + }); + } + + /** + * Deprecated: Returns a new pattern with the given location information added to the + * context of every hap. * @param {Number} start * @param {Number} end * @returns Pattern @@ -488,6 +507,7 @@ export class Pattern { }); } + // DEPRECATED: withMiniLocation(start, end) { const offset = { start: { line: start[0], column: start[1], offset: start[2] }, diff --git a/packages/mini/mini.mjs b/packages/mini/mini.mjs index 10a82410..3321e7bc 100644 --- a/packages/mini/mini.mjs +++ b/packages/mini/mini.mjs @@ -69,9 +69,9 @@ function resolveReplications(ast) { } // expects ast from mini2ast + quoted mini string + optional callback when a node is entered -export function patternifyAST(ast, code, onEnter) { +export function patternifyAST(ast, code, onEnter, offset = 0) { onEnter?.(ast); - const enter = (node) => patternifyAST(node, code, onEnter); + const enter = (node) => patternifyAST(node, code, onEnter, offset); switch (ast.type_) { case 'pattern': { resolveReplications(ast); @@ -121,8 +121,12 @@ export function patternifyAST(ast, code, onEnter) { return ast.source_; } const value = !isNaN(Number(ast.source_)) ? Number(ast.source_) : ast.source_; - const [from, to] = getLeafLocation(code, ast); - return strudel.pure(value).withLocation(from, to); + if (offset === -1) { + // skip location handling (used when getting leaves to avoid confusion) + return strudel.pure(value); + } + const [from, to] = getLeafLocation(code, ast, offset); + return strudel.pure(value).withLoc(from, to); } case 'stretch': return enter(ast.source_).slow(enter(ast.arguments_.amount)); @@ -133,7 +137,7 @@ export function patternifyAST(ast, code, onEnter) { } // takes quoted mini string + leaf node within, returns source location of node (whitespace corrected) -export const getLeafLocation = (code, leaf) => { +export const getLeafLocation = (code, leaf, globalOffset = 0) => { // value is expected without quotes! const { start, end } = leaf.location_; const actual = code?.split('').slice(start.offset, end.offset).join(''); @@ -141,10 +145,7 @@ export const getLeafLocation = (code, leaf) => { const [offsetStart = 0, offsetEnd = 0] = actual ? actual.split(leaf.source_).map((p) => p.split('').filter((c) => c === ' ').length) : []; - return [ - [start.line, start.column + offsetStart, start.offset + offsetStart], - [start.line, end.column - offsetEnd, end.offset - offsetEnd], - ]; + return [start.offset + offsetStart + globalOffset, end.offset - offsetEnd + globalOffset]; }; // takes quoted mini string, returns ast @@ -154,17 +155,22 @@ export const mini2ast = (code) => krill.parse(code); export const getLeaves = (code) => { const ast = mini2ast(code); let leaves = []; - patternifyAST(ast, code, (node) => { - if (node.type_ === 'atom') { - leaves.push(node); - } - }); + patternifyAST( + ast, + code, + (node) => { + if (node.type_ === 'atom') { + leaves.push(node); + } + }, + -1, + ); return leaves; }; // takes quoted mini string, returns locations [fromCol,toCol] of all leaf nodes -export const getLeafLocations = (code) => { - return getLeaves(code).map((l) => getLeafLocation(code, l).map((l) => l[2])); +export const getLeafLocations = (code, offset = 0) => { + return getLeaves(code).map((l) => getLeafLocation(code, l, offset)); }; // mini notation only (wraps in "") @@ -177,6 +183,16 @@ export const mini = (...strings) => { return strudel.sequence(...pats); }; +// turns str mini string (without quotes) into pattern +// offset is the position of the mini string in the JS code +// each leaf node will get .withLoc added +// this function is used by the transpiler for double quoted strings +export const m = (str, offset) => { + const code = `"${str}"`; + const ast = mini2ast(code); + return patternifyAST(ast, code, null, offset); +}; + // includes haskell style (raw krill parsing) export const h = (string) => { const ast = mini2ast(string); diff --git a/packages/mini/test/mini.test.mjs b/packages/mini/test/mini.test.mjs index 677dc80a..0c7f381e 100644 --- a/packages/mini/test/mini.test.mjs +++ b/packages/mini/test/mini.test.mjs @@ -192,16 +192,10 @@ describe('getLeafLocation', () => { const ast = mini2ast(code); const bd = ast.source_[0].source_; - expect(getLeafLocation(code, bd)).toEqual([ - [1, 2, 1], - [1, 4, 3], - ]); + expect(getLeafLocation(code, bd)).toEqual([1, 3]); const sd = ast.source_[1].source_; - expect(getLeafLocation(code, sd)).toEqual([ - [1, 5, 4], - [1, 7, 6], - ]); + expect(getLeafLocation(code, sd)).toEqual([4, 6]); }); }); diff --git a/packages/react/src/components/CodeMirror6.jsx b/packages/react/src/components/CodeMirror6.jsx index e9734421..e695eeed 100644 --- a/packages/react/src/components/CodeMirror6.jsx +++ b/packages/react/src/components/CodeMirror6.jsx @@ -92,27 +92,40 @@ const miniLocations = StateField.define({ // we can NOT create new marks because the context.locations haven't changed since eval time // this is why we need to find a way to update the existing decorations, showing the ones that have an active range const visible = e.value - .map((hap) => hap.context.locations.map(({ start, end }) => `${start.offset}:${end.offset}`)) + .map((hap) => hap.context.locations.map(({ start, end }) => `${start}:${end}`)) .flat() .filter((v, i, a) => a.indexOf(v) === i); - console.log('visible', visible); // e.g. [ "1:3", "8:9", "4:6" ] + // console.log('visible', visible); // e.g. [ "1:3", "8:9", "4:6" ] - // TODO: iterate over "locations" variable, get access to underlying mark.spec.range - // for each mark that is visible, change color (later remove green color...) - // How to iterate over DecorationSet ??? + const iterator = locations.iter(); - /* console.log('iter', iter.value.spec.range); - while (iter.next().value) { - console.log('iter', iter.value); - } */ - /* locations = locations.update({ - filter: (from, to) => { - //console.log('filter', from, to); - // const id = `${from}:${to}`; - //return visible.includes(`${from}:${to}`); - return true; - }, - }); */ + let mapping = {}; + while (!!iterator.value) { + const { + from, + to, + value: { + spec: { range }, + }, + } = iterator; + const id = `${range[0]}:${range[1]}`; + mapping[id] = [from, to]; + iterator.next(); + } + + const decorations = Object.entries(mapping) + .map(([range, [from, to]]) => { + let color = visible.includes(range) ? 'red' : 'transparent'; + const mark = Decoration.mark({ + range: range.split(':'), + // this green is only to verify that the decoration moves when the document is edited + // it will be removed later, so the mark is not visible by default + attributes: { style: `background-color: ${color}` }, + }); + return mark.range(from, to); // -> Range + }) + .filter(Boolean); + locations = Decoration.set(decorations); } } return locations; @@ -134,10 +147,10 @@ const highlightField = StateField.define({ const marks = haps .map((hap) => - (hap.context.locations || []).map(({ start, end }) => { + (hap.context.locations || []).map(({ start: from, end: to }) => { const color = hap.context.color || e.value.color; - let from = tr.newDoc.line(start.line).from + start.column; - let to = tr.newDoc.line(end.line).from + end.column; + /* let from = tr.newDoc.line(start.line).from + start.column; + let to = tr.newDoc.line(end.line).from + end.column; */ const l = tr.newDoc.length; if (from > l || to > l) { return; // dont mark outside of range, as it will throw an error diff --git a/packages/react/src/hooks/useHighlighting.mjs b/packages/react/src/hooks/useHighlighting.mjs index f622bdd9..a678fdc5 100644 --- a/packages/react/src/hooks/useHighlighting.mjs +++ b/packages/react/src/hooks/useHighlighting.mjs @@ -20,7 +20,7 @@ function useHighlighting({ view, pattern, active, getTime }) { highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset()); highlights.current = highlights.current.concat(haps); // add potential new onsets - view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps + // view.dispatch({ effects: setHighlights.of({ haps: highlights.current }) }); // highlight all still active + new active haps highlightMiniLocations(view, highlights.current); // <- new method, replaces above line when done } catch (err) { view.dispatch({ effects: setHighlights.of({ haps: [] }) }); diff --git a/packages/transpiler/test/transpiler.test.mjs b/packages/transpiler/test/transpiler.test.mjs index 23b0e64f..c65b014a 100644 --- a/packages/transpiler/test/transpiler.test.mjs +++ b/packages/transpiler/test/transpiler.test.mjs @@ -11,13 +11,13 @@ const simple = { wrapAsync: false, addReturn: false, simpleLocs: true }; describe('transpiler', () => { it('wraps double quote string with mini and adds location', () => { - expect(transpiler('"c3"', simple).output).toEqual("mini('c3').withMiniLocation(0, 4);"); + expect(transpiler('"c3"', simple).output).toEqual("m('c3', 0);"); expect(transpiler('stack("c3","bd sd")', simple).output).toEqual( - "stack(mini('c3').withMiniLocation(6, 10), mini('bd sd').withMiniLocation(11, 18));", + "stack(m('c3', 6), m('bd sd', 11));", ); }); it('wraps backtick string with mini and adds location', () => { - expect(transpiler('`c3`', simple).output).toEqual("mini('c3').withMiniLocation(0, 4);"); + expect(transpiler('`c3`', simple).output).toEqual("m('c3', 0);"); }); it('replaces note variables with note strings', () => { expect(transpiler('seq(c3, d3)', simple).output).toEqual("seq('c3', 'd3');"); diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index d2f9eed6..78aae9f7 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -5,7 +5,7 @@ import { isNoteWithOctave } from '@strudel.cycles/core'; import { getLeafLocations } from '@strudel.cycles/mini'; export function transpiler(input, options = {}) { - const { wrapAsync = false, addReturn = true, simpleLocs = false, emitMiniLocations = true } = options; + const { wrapAsync = false, addReturn = true, emitMiniLocations = true } = options; let ast = parse(input, { ecmaVersion: 2022, @@ -15,9 +15,9 @@ export function transpiler(input, options = {}) { let miniLocations = []; const collectMiniLocations = (value, node) => { - const leafLocs = getLeafLocations(`"${value}"`); - const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start)); - miniLocations = miniLocations.concat(withOffset); + const leafLocs = getLeafLocations(`"${value}"`, node.start); // stimmt! + //const withOffset = leafLocs.map((offsets) => offsets.map((o) => o + node.start)); + miniLocations = miniLocations.concat(leafLocs); }; walk(ast, { @@ -27,13 +27,13 @@ export function transpiler(input, options = {}) { const { raw } = quasis[0].value; this.skip(); emitMiniLocations && collectMiniLocations(raw, node); - return this.replace(miniWithLocation(raw, node, simpleLocs, miniLocations)); + return this.replace(miniWithLocation(raw, node)); } if (isStringWithDoubleQuotes(node)) { const { value } = node; this.skip(); emitMiniLocations && collectMiniLocations(value, node); - return this.replace(miniWithLocation(value, node, simpleLocs, miniLocations)); + return this.replace(miniWithLocation(value, node)); } // TODO: remove pseudo note variables? if (node.type === 'Identifier' && isNoteWithOctave(node.name)) { @@ -79,64 +79,18 @@ function isBackTickString(node, parent) { return node.type === 'TemplateLiteral' && parent.type !== 'TaggedTemplateExpression'; } -function miniWithLocation(value, node, simpleLocs) { - let locs; - const { start: fromOffset, end: toOffset } = node; - if (simpleLocs) { - locs = [ - { - type: 'Literal', - value: fromOffset, - }, - { - type: 'Literal', - value: toOffset, - }, - ]; - } else { - const { - loc: { - start: { line: fromLine, column: fromColumn }, - end: { line: toLine, column: toColumn }, - }, - } = node; - locs = [ - { - type: 'ArrayExpression', - elements: [fromLine, fromColumn, fromOffset].map((value) => ({ - type: 'Literal', - value, - })), - }, - { - type: 'ArrayExpression', - elements: [toLine, toColumn, toOffset].map((value) => ({ - type: 'Literal', - value, - })), - }, - ]; - } - // with location +function miniWithLocation(value, node) { + const { start: fromOffset } = node; return { type: 'CallExpression', callee: { - type: 'MemberExpression', - object: { - type: 'CallExpression', - callee: { - type: 'Identifier', - name: 'mini', - }, - arguments: [{ type: 'Literal', value }], - optional: false, - }, - property: { - type: 'Identifier', - name: 'withMiniLocation', - }, + type: 'Identifier', + name: 'm', }, - arguments: locs, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: fromOffset }, + ], optional: false, }; } diff --git a/test/runtime.mjs b/test/runtime.mjs index 08acf831..b43898c4 100644 --- a/test/runtime.mjs +++ b/test/runtime.mjs @@ -10,7 +10,7 @@ import * as strudel from '@strudel.cycles/core'; import * as webaudio from '@strudel.cycles/webaudio'; import controls from '@strudel.cycles/core/controls.mjs'; // import gist from '@strudel.cycles/core/gist.js'; -import { mini } from '@strudel.cycles/mini/mini.mjs'; +import { mini, m } from '@strudel.cycles/mini/mini.mjs'; // import * as toneHelpers from '@strudel.cycles/tone/tone.mjs'; // import * as voicingHelpers from '@strudel.cycles/tonal/voicings.mjs'; // import * as uiHelpers from '@strudel.cycles/tone/ui.mjs'; @@ -174,6 +174,7 @@ evalScope( csound: id, loadOrc: id, mini, + m, getDrawContext, getAudioContext, loadSoundfont, diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx index e22e0421..8629a9ac 100644 --- a/website/src/repl/Repl.jsx +++ b/website/src/repl/Repl.jsx @@ -220,7 +220,7 @@ export function Repl({ embedded = false }) { /* stack( s("bd"), - s("hh oh*<2 3>") + s("hh oh*<2 3>") ) */ updateMiniLocations(view, [