mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 13:48:40 +00:00
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:
parent
08abec8fd5
commit
0b5d905120
@ -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] },
|
||||
|
||||
@ -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) => {
|
||||
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);
|
||||
|
||||
@ -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]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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: [] }) });
|
||||
|
||||
@ -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');");
|
||||
|
||||
@ -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',
|
||||
name: 'm',
|
||||
},
|
||||
arguments: [{ type: 'Literal', value }],
|
||||
optional: false,
|
||||
},
|
||||
property: {
|
||||
type: 'Identifier',
|
||||
name: 'withMiniLocation',
|
||||
},
|
||||
},
|
||||
arguments: locs,
|
||||
arguments: [
|
||||
{ type: 'Literal', value },
|
||||
{ type: 'Literal', value: fromOffset },
|
||||
],
|
||||
optional: false,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user