Jev in the code, in Jev's colour:
- installJevSyntax: always, every call into Jev's API:
jev(, the question builderschoice(,score(andnoul(,walk(, the variable a song'sjev({...})andwalk(...)are bound to (djJev,form), and the answers destructured fromdjJev.every(...)wherever they are used. - highlightMove: while a song plays, the names Jev chose for the playing
section (the form's section, each choice), boxed wherever the song
writes them as keys: in the question's options and in the
.pick({...})that plays them, and their lines tinted (and on the minimap) in Jev's colour. JevBoothEffects calls it on every change.
Jev's colour, in the code and on the minimap
17const JEV_COLOR = '#ffcc00';
19const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
── always: the API's calls ─────────────────────────
22const call = Decoration.mark({ class: 'jev-call' });
Where the code is text, not code: strings and comments, as [from, to).
25function textSpans(code) { 26 const spans = []; 27 for (let i = 0; i < code.length; i++) { 28 const c = code[i]; 29 if (c === '/' && code[i + 1] === '/') { 30 const end = code.indexOf('\n', i); 31 spans.push([i, end < 0 ? code.length : end]); 32 i = end < 0 ? code.length : end; 33 } else if (c === '/' && code[i + 1] === '*') { 34 const end = code.indexOf('*/', i + 2); 35 spans.push([i, end < 0 ? code.length : end + 2]); 36 i = end < 0 ? code.length : end + 1; 37 } else if (c === "'" || c === '"' || c === '`') { 38 let j = i + 1; 39 while (j < code.length && code[j] !== c) j += code[j] === '\\' ? 2 : 1; 40 spans.push([i, j + 1]); 41 i = j; 42 } 43 } 44 return spans; 45} 46const inside = (spans, at) => spans.some(([from, to]) => at >= from && at < to);
48function jevRanges(code) { 49 const ranges = []; 50 const add = (from, to) => ranges.push([from, to]); 51 for (const m of code.matchAll(/\b(jev|choice|score|noul|walk)(?=\s*\()/g)) add(m.index, m.index + m[1].length); 52 // const djJev = jev(…), const form = walk(…) 53 const bound = [...code.matchAll(/\b(?:const|let|var)\s+(\w+)\s*=\s*(?:jev|walk)\s*\(/g)].map((m) => m[1]); 54 // const { move, energy } = djJev.every(8, …) 55 const answers = []; 56 for (const name of bound) { 57 const re = new RegExp(`\\{([^}]*)\\}\\s*=\\s*${escape(name)}\\s*\\.\\s*every\\s*\\(`, 'g'); 58 for (const m of code.matchAll(re)) { 59 answers.push( 60 ...m[1] 61 .split(',') 62 .map((x) => x.split(':').pop().trim()) 63 .filter(Boolean), 64 ); 65 } 66 } 67 for (const name of [...bound, ...answers]) { 68 for (const m of code.matchAll(new RegExp(`\\b${escape(name)}\\b`, 'g'))) add(m.index, m.index + name.length); 69 } 70 const spans = textSpans(code); 71 return ranges.filter(([from]) => !inside(spans, from)).sort((a, b) => a[0] - b[0] || a[1] - b[1]); 72} 73 74function build(view) { 75 const builder = new RangeSetBuilder(); 76 let end = -1; 77 for (const [from, to] of jevRanges(view.state.doc.toString())) { 78 if (from < end) continue; // no overlaps 79 builder.add(from, to, call); 80 end = to; 81 } 82 return builder.finish(); 83} 84 85const syntax = ViewPlugin.fromClass( 86 class { 87 constructor(view) { 88 this.decorations = build(view); 89 } 90 update(u) { 91 if (u.docChanged) this.decorations = build(u.view); 92 } 93 }, 94 { decorations: (v) => v.decorations }, 95);
── while playing: Jev's choices ────────────────────
101const field = StateField.define({ 102 create: () => Decoration.none, 103 update(decorations, tr) { 104 for (const effect of tr.effects) { 105 if (effect.is(setMove)) 106 return Decoration.set( 107 effect.value.map((r) => mark.range(r.from, r.to)), 108 true, 109 ); 110 } 111 return decorations.map(tr.changes); 112 }, 113 provide: (f) => EditorView.decorations.from(f), 114});
The same choices' lines, tinted and on the minimap in Jev's colour (packages/codemirror/lineHighlights.mjs), after any pattern's.
126const theme = EditorView.baseTheme({ 127 '.jev-call': { color: '#ffcc00 !important', fontWeight: 'bold' }, 128 '.jev-call *': { color: '#ffcc00 !important' }, 129 '.jev-move-playing': { 130 backgroundColor: 'rgba(255, 204, 0, 0.3)', 131 outline: '1px solid #ffcc00', 132 borderRadius: '2px', 133 }, 134}); 135 136const installed = new WeakSet(); 137function install(view) { 138 // its own transaction: a field added in a transaction does not see that 139 // transaction's effects 140 if (installed.has(view)) return; 141 installed.add(view); 142 view.dispatch({ effects: StateEffect.appendConfig.of([field, moveLines, syntax, theme]) }); 143} 144 145export const installJevSyntax = install;
Every place name: is written as a key, after the song's jev(.
148function find(code, name) { 149 const start = Math.max(0, code.search(/\bjev\s*\(/)); 150 const key = new RegExp(`(^|[\\s{,])(${escape(name)})\\s*:`, 'gm'); 151 const ranges = []; 152 const spans = textSpans(code); 153 for (const m of code.slice(start).matchAll(key)) { 154 const from = start + m.index + m[1].length; 155 if (!inside(spans, from)) ranges.push({ from, to: from + name.length }); 156 } 157 return ranges; 158}
view is the CodeMirror EditorView (StrudelMirror's .editor).
161export function highlightMove(view, names) { 162 if (!view) return; 163 install(view); 164 const code = view.state.doc.toString(); 165 const ranges = names 166 .filter(Boolean) 167 .flatMap((name) => find(code, name)) 168 .sort((a, b) => a.from - b.from); 169 view.dispatch({ effects: setMove.of(ranges) }); 170}