diff --git a/packages/tidal/tidal.mjs b/packages/tidal/tidal.mjs index b89bf4b4..46293f1f 100644 --- a/packages/tidal/tidal.mjs +++ b/packages/tidal/tidal.mjs @@ -28,11 +28,6 @@ function getInfixOperators() { ops['~>'] = (l, r) => reify(l).late(reify(r)); ops['<~'] = (l, r) => reify(l).early(reify(r)); ops['<$>'] = (l, r) => reify(r).fmap(l).outerJoin(); // is this right? - ops['string'] = (node) => { - const str = node.text.slice(1, -1); - console.log('string node', node); - return m(str, 1); - }; return ops; } const ops = getInfixOperators(); @@ -51,9 +46,30 @@ export async function initTidal() { return loadParser(); } -export function tidal(code) { +// offset is expected to be passed in from transpiler +/* +1. acorn parses JS to find location of tidal call +2. haskell-tree-sitter calls "string" function with node (including location) +3. m function sets locations for individual mini notation atom + +so the location for a mini notation atom is: + +js offset + hs offset + atom offset +*/ +export function tidal(code, offset = 0) { if (Array.isArray(code)) { code = code.join(''); } - return evaluate(code, window, ops); + return evaluate(code, window, { + ...ops, + string: (node) => { + // parses strings as mini notation and passes location + const str = node.text.slice(1, -1); + const col = node.startIndex + offset; + // problem: node.startIndex is wrong because hs2js parser removes newlines + // for some reason, the parser doesn't like newlines + // this means highlighting only works in the first line for now + return m(str, col); + }, + }); } diff --git a/packages/transpiler/transpiler.mjs b/packages/transpiler/transpiler.mjs index 95156664..0012936e 100644 --- a/packages/transpiler/transpiler.mjs +++ b/packages/transpiler/transpiler.mjs @@ -26,6 +26,16 @@ export function transpiler(input, options = {}) { walk(ast, { enter(node, parent /* , prop, index */) { + if (isTidalTeplateLiteral(node)) { + const raw = node.quasi.quasis[0].value.raw; + const offset = node.quasi.start + 1; + if (emitMiniLocations) { + const stringLocs = collectHaskellMiniLocations(raw, offset); + miniLocations = miniLocations.concat(stringLocs); + } + this.skip(); + return this.replace(tidalWithLocation(raw, offset)); + } if (isBackTickString(node, parent)) { const { quasis } = node; const { raw } = quasis[0].value; @@ -207,3 +217,46 @@ function labelToP(node) { }, }; } + +// tidal highlighting +// this feels kind of stupid, when we also know the location inside the string op (tidal.mjs) +// but maybe it's the only way + +function isTidalTeplateLiteral(node) { + return node.type === 'TaggedTemplateExpression' && node.tag.name === 'tidal'; +} + +function collectHaskellMiniLocations(haskellCode, offset) { + const stringLocations = haskellCode.split('').reduce((acc, char, i) => { + if (char !== '"') { + return acc; + } + if (!acc.length || acc[acc.length - 1].length > 1) { + acc.push([i + 1]); + } else { + acc[acc.length - 1].push(i); + } + return acc; + }, []); + return stringLocations + .map(([start, end]) => { + const miniString = haskellCode.slice(start, end); + return getLeafLocations(`"${miniString}"`, offset + start - 1); + }) + .flat(); +} + +function tidalWithLocation(value, offset) { + return { + type: 'CallExpression', + callee: { + type: 'Identifier', + name: 'tidal', + }, + arguments: [ + { type: 'Literal', value }, + { type: 'Literal', value: offset }, + ], + optional: false, + }; +}