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
This commit is contained in:
Felix Roos 2023-07-03 05:15:32 +02:00
parent 08abec8fd5
commit 0b5d905120
9 changed files with 108 additions and 110 deletions

View File

@ -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] },

View File

@ -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);

View File

@ -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]);
});
});

View File

@ -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<Decoration>
})
.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

View File

@ -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: [] }) });

View File

@ -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');");

View File

@ -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,
};
}

View File

@ -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,

View File

@ -220,7 +220,7 @@ export function Repl({ embedded = false }) {
/*
stack(
s("bd"),
s("hh oh*<2 3>")
s("hh oh*<2 3>")
)
*/
updateMiniLocations(view, [