mirror of
https://github.com/eliasstepanik/strudel-docker.git
synced 2026-01-24 03:58:53 +00:00
Merge pull request #636 from mindofmatthew/highlight-ids
More work on highlight IDs
This commit is contained in:
commit
c82f7bd8fe
@ -1,11 +1,12 @@
|
|||||||
import { EditorState } from '@codemirror/state';
|
|
||||||
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
|
|
||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
import { StateField, StateEffect } from '@codemirror/state';
|
import { defaultHighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
import { EditorView, highlightActiveLineGutter, keymap, lineNumbers } from '@codemirror/view';
|
||||||
|
import { Drawer, repl } from '@strudel.cycles/core';
|
||||||
|
import { flashField, flash } from './flash.mjs';
|
||||||
|
import { highlightExtension, highlightMiniLocations } from './highlight.mjs';
|
||||||
import { oneDark } from './themes/one-dark';
|
import { oneDark } from './themes/one-dark';
|
||||||
import { repl, Drawer } from '@strudel.cycles/core';
|
|
||||||
|
|
||||||
// https://codemirror.net/docs/guide/
|
// https://codemirror.net/docs/guide/
|
||||||
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
|
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
|
||||||
@ -15,7 +16,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
|
|||||||
theme,
|
theme,
|
||||||
javascript(),
|
javascript(),
|
||||||
lineNumbers(),
|
lineNumbers(),
|
||||||
highlightField,
|
highlightExtension,
|
||||||
highlightActiveLineGutter(),
|
highlightActiveLineGutter(),
|
||||||
syntaxHighlighting(defaultHighlightStyle),
|
syntaxHighlighting(defaultHighlightStyle),
|
||||||
keymap.of(defaultKeymap),
|
keymap.of(defaultKeymap),
|
||||||
@ -40,93 +41,6 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// highlighting
|
|
||||||
//
|
|
||||||
|
|
||||||
export const setHighlights = StateEffect.define();
|
|
||||||
export const highlightField = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(highlights, tr) {
|
|
||||||
try {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setHighlights)) {
|
|
||||||
const { haps } = e.value;
|
|
||||||
const marks =
|
|
||||||
haps
|
|
||||||
.map((hap) =>
|
|
||||||
(hap.context.locations || []).map(({ start, end }) => {
|
|
||||||
// const color = hap.context.color || e.value.color || '#FFCA28';
|
|
||||||
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
|
|
||||||
}
|
|
||||||
const mark = Decoration.mark({
|
|
||||||
attributes: { style: `outline: 2px solid #FFCA28;` },
|
|
||||||
});
|
|
||||||
return mark.range(from, to);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.flat()
|
|
||||||
.filter(Boolean) || [];
|
|
||||||
highlights = Decoration.set(marks, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return highlights;
|
|
||||||
} catch (err) {
|
|
||||||
// console.warn('highlighting error', err);
|
|
||||||
return Decoration.set([]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
// helper to simply trigger highlighting for given haps
|
|
||||||
export function highlightHaps(view, haps) {
|
|
||||||
view.dispatch({ effects: setHighlights.of({ haps }) });
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// flash
|
|
||||||
//
|
|
||||||
|
|
||||||
export const setFlash = StateEffect.define();
|
|
||||||
const flashField = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(flash, tr) {
|
|
||||||
try {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setFlash)) {
|
|
||||||
if (e.value && tr.newDoc.length > 0) {
|
|
||||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
|
||||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
|
||||||
} else {
|
|
||||||
flash = Decoration.set([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return flash;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('flash error', err);
|
|
||||||
return flash;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const flash = (view, ms = 200) => {
|
|
||||||
view.dispatch({ effects: setFlash.of(true) });
|
|
||||||
setTimeout(() => {
|
|
||||||
view.dispatch({ effects: setFlash.of(false) });
|
|
||||||
}, ms);
|
|
||||||
};
|
|
||||||
|
|
||||||
export class StrudelMirror {
|
export class StrudelMirror {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
|
||||||
@ -134,7 +48,7 @@ export class StrudelMirror {
|
|||||||
|
|
||||||
this.drawer = new Drawer((haps, time) => {
|
this.drawer = new Drawer((haps, time) => {
|
||||||
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
|
||||||
this.highlight(currentFrame);
|
this.highlight(currentFrame, time);
|
||||||
onDraw?.(haps, time, currentFrame);
|
onDraw?.(haps, time, currentFrame);
|
||||||
}, drawTime);
|
}, drawTime);
|
||||||
|
|
||||||
@ -193,7 +107,7 @@ export class StrudelMirror {
|
|||||||
flash(ms) {
|
flash(ms) {
|
||||||
flash(this.editor, ms);
|
flash(this.editor, ms);
|
||||||
}
|
}
|
||||||
highlight(haps) {
|
highlight(haps, time) {
|
||||||
highlightHaps(this.editor, haps);
|
highlightMiniLocations(this.editor.view, time, haps);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
35
packages/codemirror/flash.mjs
Normal file
35
packages/codemirror/flash.mjs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { StateEffect, StateField } from '@codemirror/state';
|
||||||
|
import { Decoration, EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
export const setFlash = StateEffect.define();
|
||||||
|
export const flashField = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(flash, tr) {
|
||||||
|
try {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setFlash)) {
|
||||||
|
if (e.value && tr.newDoc.length > 0) {
|
||||||
|
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
||||||
|
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
||||||
|
} else {
|
||||||
|
flash = Decoration.set([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return flash;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('flash error', err);
|
||||||
|
return flash;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
provide: (f) => EditorView.decorations.from(f),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const flash = (view, ms = 200) => {
|
||||||
|
view.dispatch({ effects: setFlash.of(true) });
|
||||||
|
setTimeout(() => {
|
||||||
|
view.dispatch({ effects: setFlash.of(false) });
|
||||||
|
}, ms);
|
||||||
|
};
|
||||||
126
packages/codemirror/highlight.mjs
Normal file
126
packages/codemirror/highlight.mjs
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
import { RangeSetBuilder, StateEffect, StateField } from '@codemirror/state';
|
||||||
|
import { Decoration, EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
export const setMiniLocations = StateEffect.define();
|
||||||
|
export const showMiniLocations = StateEffect.define();
|
||||||
|
export const updateMiniLocations = (view, locations) => {
|
||||||
|
view.dispatch({ effects: setMiniLocations.of(locations) });
|
||||||
|
};
|
||||||
|
export const highlightMiniLocations = (view, atTime, haps) => {
|
||||||
|
view.dispatch({ effects: showMiniLocations.of({ atTime, haps }) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const miniLocations = StateField.define({
|
||||||
|
create() {
|
||||||
|
return Decoration.none;
|
||||||
|
},
|
||||||
|
update(locations, tr) {
|
||||||
|
if (tr.docChanged) {
|
||||||
|
locations = locations.map(tr.changes);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(setMiniLocations)) {
|
||||||
|
// this is called on eval, with the mini locations obtained from the transpiler
|
||||||
|
// codemirror will automatically remap the marks when the document is edited
|
||||||
|
// create a mark for each mini location, adding the range to the spec to find it later
|
||||||
|
const marks = e.value
|
||||||
|
.filter(([from]) => from < tr.newDoc.length)
|
||||||
|
.map(([from, to]) => [from, Math.min(to, tr.newDoc.length)])
|
||||||
|
.map(
|
||||||
|
(range) =>
|
||||||
|
Decoration.mark({
|
||||||
|
id: range.join(':'),
|
||||||
|
// 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: #00CA2880` },
|
||||||
|
}).range(...range), // -> Decoration
|
||||||
|
);
|
||||||
|
|
||||||
|
locations = Decoration.set(marks, true); // -> DecorationSet === RangeSet<Decoration>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return locations;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleMiniLocations = StateField.define({
|
||||||
|
create() {
|
||||||
|
return { atTime: 0, haps: new Map() };
|
||||||
|
},
|
||||||
|
update(visible, tr) {
|
||||||
|
for (let e of tr.effects) {
|
||||||
|
if (e.is(showMiniLocations)) {
|
||||||
|
// this is called every frame to show the locations that are currently active
|
||||||
|
// 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 haps = new Map();
|
||||||
|
for (let hap of e.value.haps) {
|
||||||
|
for (let { start, end } of hap.context.locations) {
|
||||||
|
let id = `${start}:${end}`;
|
||||||
|
if (!haps.has(id) || haps.get(id).whole.begin.lt(hap.whole.begin)) {
|
||||||
|
haps.set(id, hap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible = { atTime: e.value.atTime, haps };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// // Derive the set of decorations from the miniLocations and visibleLocations
|
||||||
|
const miniLocationHighlights = EditorView.decorations.compute([miniLocations, visibleMiniLocations], (state) => {
|
||||||
|
const iterator = state.field(miniLocations).iter();
|
||||||
|
const { haps } = state.field(visibleMiniLocations);
|
||||||
|
const builder = new RangeSetBuilder();
|
||||||
|
|
||||||
|
while (iterator.value) {
|
||||||
|
const {
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
value: {
|
||||||
|
spec: { id },
|
||||||
|
},
|
||||||
|
} = iterator;
|
||||||
|
|
||||||
|
if (haps.has(id)) {
|
||||||
|
const hap = haps.get(id);
|
||||||
|
const color = hap.context.color ?? 'var(--foreground)';
|
||||||
|
// Get explicit channels for color values
|
||||||
|
/*
|
||||||
|
const swatch = document.createElement('div');
|
||||||
|
swatch.style.color = color;
|
||||||
|
document.body.appendChild(swatch);
|
||||||
|
let channels = getComputedStyle(swatch)
|
||||||
|
.color.match(/^rgba?\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})(?:,\s*(\d*(?:\.\d+)?))?\)$/)
|
||||||
|
.slice(1)
|
||||||
|
.map((c) => parseFloat(c || 1));
|
||||||
|
document.body.removeChild(swatch);
|
||||||
|
|
||||||
|
// Get percentage of event
|
||||||
|
const percent = 1 - (atTime - hap.whole.begin) / hap.whole.duration;
|
||||||
|
channels[3] *= percent;
|
||||||
|
*/
|
||||||
|
|
||||||
|
builder.add(
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
Decoration.mark({
|
||||||
|
// attributes: { style: `outline: solid 2px rgba(${channels.join(', ')})` },
|
||||||
|
attributes: { style: `outline: solid 2px ${color}` },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
|
||||||
3
packages/codemirror/index.mjs
Normal file
3
packages/codemirror/index.mjs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './codemirror.mjs';
|
||||||
|
export * from './highlight.mjs';
|
||||||
|
export * from './flash.mjs';
|
||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "@strudel/codemirror",
|
"name": "@strudel/codemirror",
|
||||||
"version": "0.8.4",
|
"version": "0.8.4",
|
||||||
"description": "Codemirror Extensions for Strudel",
|
"description": "Codemirror Extensions for Strudel",
|
||||||
"main": "codemirror.mjs",
|
"main": "index.mjs",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"module": "dist/index.mjs"
|
"module": "dist/index.mjs"
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export default defineConfig({
|
|||||||
plugins: [],
|
plugins: [],
|
||||||
build: {
|
build: {
|
||||||
lib: {
|
lib: {
|
||||||
entry: resolve(__dirname, 'codemirror.mjs'),
|
entry: resolve(__dirname, 'index.mjs'),
|
||||||
formats: ['es', 'cjs'],
|
formats: ['es', 'cjs'],
|
||||||
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
|
||||||
},
|
},
|
||||||
|
|||||||
@ -82,9 +82,10 @@ function App() {
|
|||||||
code,
|
code,
|
||||||
defaultOutput: webaudioOutput,
|
defaultOutput: webaudioOutput,
|
||||||
getTime,
|
getTime,
|
||||||
|
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
|
||||||
});
|
});
|
||||||
|
|
||||||
useHighlighting({
|
const { setMiniLocations } = useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||||
|
|||||||
@ -42,6 +42,7 @@
|
|||||||
"@strudel.cycles/core": "workspace:*",
|
"@strudel.cycles/core": "workspace:*",
|
||||||
"@strudel.cycles/transpiler": "workspace:*",
|
"@strudel.cycles/transpiler": "workspace:*",
|
||||||
"@strudel.cycles/webaudio": "workspace:*",
|
"@strudel.cycles/webaudio": "workspace:*",
|
||||||
|
"@strudel/codemirror": "workspace:*",
|
||||||
"@uiw/codemirror-themes": "^4.19.16",
|
"@uiw/codemirror-themes": "^4.19.16",
|
||||||
"@uiw/react-codemirror": "^4.19.16",
|
"@uiw/react-codemirror": "^4.19.16",
|
||||||
"react-hook-inview": "^4.5.0"
|
"react-hook-inview": "^4.5.0"
|
||||||
|
|||||||
@ -1,184 +1,24 @@
|
|||||||
import React, { useMemo } from 'react';
|
|
||||||
import _CodeMirror from '@uiw/react-codemirror';
|
|
||||||
import { EditorView, Decoration } from '@codemirror/view';
|
|
||||||
import { StateField, StateEffect } from '@codemirror/state';
|
|
||||||
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
|
||||||
import strudelTheme from '../themes/strudel-theme';
|
|
||||||
import './style.css';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
import { autocompletion } from '@codemirror/autocomplete';
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
import { strudelAutocomplete } from './Autocomplete';
|
import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
|
||||||
import { vim } from '@replit/codemirror-vim';
|
import { EditorView } from '@codemirror/view';
|
||||||
import { emacs } from '@replit/codemirror-emacs';
|
import { emacs } from '@replit/codemirror-emacs';
|
||||||
|
import { vim } from '@replit/codemirror-vim';
|
||||||
|
import _CodeMirror from '@uiw/react-codemirror';
|
||||||
|
import React, { useCallback, useMemo } from 'react';
|
||||||
|
import strudelTheme from '../themes/strudel-theme';
|
||||||
|
import { strudelAutocomplete } from './Autocomplete';
|
||||||
|
import {
|
||||||
|
highlightExtension,
|
||||||
|
flashField,
|
||||||
|
flash,
|
||||||
|
highlightMiniLocations,
|
||||||
|
updateMiniLocations,
|
||||||
|
} from '@strudel/codemirror';
|
||||||
|
import './style.css';
|
||||||
|
|
||||||
export const setFlash = StateEffect.define();
|
export { flash, highlightMiniLocations, updateMiniLocations };
|
||||||
const flashField = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(flash, tr) {
|
|
||||||
try {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setFlash)) {
|
|
||||||
if (e.value && tr.newDoc?.length?.length > 0) {
|
|
||||||
const mark = Decoration.mark({ attributes: { style: `background-color: #FFCA2880` } });
|
|
||||||
flash = Decoration.set([mark.range(0, tr.newDoc.length)]);
|
|
||||||
} else {
|
|
||||||
flash = Decoration.set([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return flash;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('flash error', err);
|
|
||||||
return flash;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const flash = (view) => {
|
const staticExtensions = [javascript(), flashField, highlightExtension];
|
||||||
view.dispatch({ effects: setFlash.of(true) });
|
|
||||||
setTimeout(() => {
|
|
||||||
view.dispatch({ effects: setFlash.of(false) });
|
|
||||||
}, 200);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setMiniLocations = StateEffect.define();
|
|
||||||
export const showMiniLocations = StateEffect.define();
|
|
||||||
export const updateMiniLocations = (view, locations) => {
|
|
||||||
view.dispatch({ effects: setMiniLocations.of(locations) });
|
|
||||||
};
|
|
||||||
export const highlightMiniLocations = (view, haps) => {
|
|
||||||
view.dispatch({ effects: showMiniLocations.of(haps) });
|
|
||||||
};
|
|
||||||
|
|
||||||
const miniLocations = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(locations, tr) {
|
|
||||||
locations = locations.map(tr.changes);
|
|
||||||
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setMiniLocations)) {
|
|
||||||
// this is called on eval, with the mini locations obtained from the transpiler
|
|
||||||
// codemirror will automatically remap the marks when the document is edited
|
|
||||||
// create a mark for each mini location, adding the range to the spec to find it later
|
|
||||||
const marks = e.value.map(
|
|
||||||
(range) =>
|
|
||||||
Decoration.mark({
|
|
||||||
range,
|
|
||||||
// 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: #00CA2880` },
|
|
||||||
}), // -> Decoration
|
|
||||||
);
|
|
||||||
//
|
|
||||||
const decorations = marks
|
|
||||||
.map((mark) => {
|
|
||||||
let { range } = mark.spec;
|
|
||||||
range = range.map((v) => Math.min(v, tr.newDoc.length));
|
|
||||||
const [from, to] = range;
|
|
||||||
if (from < to) {
|
|
||||||
return mark.range(from, to); // -> Range<Decoration>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
locations = Decoration.set(decorations); // -> DecorationSet === RangeSet<Decoration>
|
|
||||||
}
|
|
||||||
if (e.is(showMiniLocations)) {
|
|
||||||
// this is called every frame to show the locations that are currently active
|
|
||||||
// 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}:${end}`))
|
|
||||||
.flat()
|
|
||||||
.filter((v, i, a) => a.indexOf(v) === i);
|
|
||||||
// console.log('visible', visible); // e.g. [ "1:3", "8:9", "4:6" ]
|
|
||||||
|
|
||||||
const iterator = locations.iter();
|
|
||||||
|
|
||||||
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;
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setHighlights = StateEffect.define();
|
|
||||||
const highlightField = StateField.define({
|
|
||||||
create() {
|
|
||||||
return Decoration.none;
|
|
||||||
},
|
|
||||||
update(highlights, tr) {
|
|
||||||
highlights = highlights.map(tr.changes);
|
|
||||||
try {
|
|
||||||
for (let e of tr.effects) {
|
|
||||||
if (e.is(setHighlights)) {
|
|
||||||
const { haps } = e.value;
|
|
||||||
const marks =
|
|
||||||
haps
|
|
||||||
.map((hap) =>
|
|
||||||
(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; */
|
|
||||||
const l = tr.newDoc.length;
|
|
||||||
if (from > l || to > l) {
|
|
||||||
return; // dont mark outside of range, as it will throw an error
|
|
||||||
}
|
|
||||||
let mark;
|
|
||||||
if (color) {
|
|
||||||
mark = Decoration.mark({ attributes: { style: `outline: 2px solid ${color};` } });
|
|
||||||
} else {
|
|
||||||
mark = Decoration.mark({ attributes: { class: `outline outline-2 outline-foreground` } });
|
|
||||||
}
|
|
||||||
return mark.range(from, to);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.flat()
|
|
||||||
.filter(Boolean) || [];
|
|
||||||
highlights = Decoration.set(marks, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return highlights;
|
|
||||||
} catch (err) {
|
|
||||||
// console.warn('highlighting error', err);
|
|
||||||
return Decoration.set([]);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
provide: (f) => EditorView.decorations.from(f),
|
|
||||||
});
|
|
||||||
|
|
||||||
const staticExtensions = [javascript(), highlightField, flashField, miniLocations];
|
|
||||||
|
|
||||||
export default function CodeMirror({
|
export default function CodeMirror({
|
||||||
value,
|
value,
|
||||||
@ -193,8 +33,6 @@ export default function CodeMirror({
|
|||||||
isLineWrappingEnabled,
|
isLineWrappingEnabled,
|
||||||
fontSize = 18,
|
fontSize = 18,
|
||||||
fontFamily = 'monospace',
|
fontFamily = 'monospace',
|
||||||
options,
|
|
||||||
editorDidMount,
|
|
||||||
}) {
|
}) {
|
||||||
const handleOnChange = useCallback(
|
const handleOnChange = useCallback(
|
||||||
(value) => {
|
(value) => {
|
||||||
@ -262,103 +100,3 @@ export default function CodeMirror({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parenMark;
|
|
||||||
export const markParens = (editor, data) => {
|
|
||||||
const v = editor.getDoc().getValue();
|
|
||||||
const marked = getCurrentParenArea(v, data);
|
|
||||||
parenMark?.clear();
|
|
||||||
parenMark = editor.getDoc().markText(...marked, { css: 'background-color: #00007720' }); //
|
|
||||||
};
|
|
||||||
|
|
||||||
// returns { line, ch } from absolute character offset
|
|
||||||
export function offsetToPosition(offset, code) {
|
|
||||||
const lines = code.split('\n');
|
|
||||||
let line = 0;
|
|
||||||
let ch = 0;
|
|
||||||
for (let i = 0; i < offset; i++) {
|
|
||||||
if (ch === lines[line].length) {
|
|
||||||
line++;
|
|
||||||
ch = 0;
|
|
||||||
} else {
|
|
||||||
ch++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { line, ch };
|
|
||||||
}
|
|
||||||
|
|
||||||
// returns absolute character offset from { line, ch }
|
|
||||||
export function positionToOffset(position, code) {
|
|
||||||
const lines = code.split('\n');
|
|
||||||
if (position.line > lines.length) {
|
|
||||||
// throw new Error('positionToOffset: position.line > lines.length');
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let offset = 0;
|
|
||||||
for (let i = 0; i < position.line; i++) {
|
|
||||||
offset += lines[i].length + 1;
|
|
||||||
}
|
|
||||||
offset += position.ch;
|
|
||||||
return offset;
|
|
||||||
}
|
|
||||||
|
|
||||||
// given code and caret position, the functions returns the indices of the parens we are in
|
|
||||||
export function getCurrentParenArea(code, caretPosition) {
|
|
||||||
const caret = positionToOffset(caretPosition, code);
|
|
||||||
let open, i, begin, end;
|
|
||||||
// walk left
|
|
||||||
i = caret;
|
|
||||||
open = 0;
|
|
||||||
while (i > 0) {
|
|
||||||
if (code[i - 1] === '(') {
|
|
||||||
open--;
|
|
||||||
} else if (code[i - 1] === ')') {
|
|
||||||
open++;
|
|
||||||
}
|
|
||||||
if (open === -1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
begin = i;
|
|
||||||
// walk right
|
|
||||||
i = caret;
|
|
||||||
open = 0;
|
|
||||||
while (i < code.length) {
|
|
||||||
if (code[i] === '(') {
|
|
||||||
open--;
|
|
||||||
} else if (code[i] === ')') {
|
|
||||||
open++;
|
|
||||||
}
|
|
||||||
if (open === 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
end = i;
|
|
||||||
return [begin, end].map((o) => offsetToPosition(o, code));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
export const markEvent = (editor) => (time, event) => {
|
|
||||||
const locs = event.context.locations;
|
|
||||||
if (!locs || !editor) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const col = event.context?.color || '#FFCA28';
|
|
||||||
// mark active event
|
|
||||||
const marks = locs.map(({ start, end }) =>
|
|
||||||
editor.getDoc().markText(
|
|
||||||
{ line: start.line - 1, ch: start.column },
|
|
||||||
{ line: end.line - 1, ch: end.column },
|
|
||||||
//{ css: 'background-color: #FFCA28; color: black' } // background-color is now used by parent marking
|
|
||||||
{ css: 'outline: 1px solid ' + col + '; box-sizing:border-box' },
|
|
||||||
//{ css: `background-color: ${col};border-radius:5px` },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
//Tone.Transport.schedule(() => { // problem: this can be cleared by scheduler...
|
|
||||||
setTimeout(() => {
|
|
||||||
marks.forEach((mark) => mark.clear());
|
|
||||||
// }, '+' + event.duration * 0.5);
|
|
||||||
}, event.duration * 1000);
|
|
||||||
}; */
|
|
||||||
|
|||||||
@ -71,6 +71,7 @@ export function MiniRepl({
|
|||||||
evalOnMount,
|
evalOnMount,
|
||||||
drawContext,
|
drawContext,
|
||||||
drawTime,
|
drawTime,
|
||||||
|
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [view, setView] = useState();
|
const [view, setView] = useState();
|
||||||
@ -84,7 +85,7 @@ export function MiniRepl({
|
|||||||
}
|
}
|
||||||
return isVisible || wasVisible.current;
|
return isVisible || wasVisible.current;
|
||||||
}, [isVisible, hideOutsideView]);
|
}, [isVisible, hideOutsideView]);
|
||||||
useHighlighting({
|
const { setMiniLocations } = useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||||
|
|||||||
@ -1,10 +1,18 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { setHighlights, highlightMiniLocations } from '../components/CodeMirror6';
|
import { highlightMiniLocations, updateMiniLocations } from '../components/CodeMirror6';
|
||||||
const round = (x) => Math.round(x * 1000) / 1000;
|
const round = (x) => Math.round(x * 1000) / 1000;
|
||||||
|
|
||||||
function useHighlighting({ view, pattern, active, getTime }) {
|
function useHighlighting({ view, pattern, active, getTime }) {
|
||||||
const highlights = useRef([]);
|
const highlights = useRef([]);
|
||||||
const lastEnd = useRef(0);
|
const lastEnd = useRef(0);
|
||||||
|
|
||||||
|
const [miniLocations, setMiniLocations] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
if (view) {
|
||||||
|
updateMiniLocations(view, miniLocations);
|
||||||
|
}
|
||||||
|
}, [view, miniLocations]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (view) {
|
if (view) {
|
||||||
if (pattern && active) {
|
if (pattern && active) {
|
||||||
@ -20,10 +28,9 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
|||||||
highlights.current = highlights.current.filter((hap) => hap.endClipped > audioTime); // keep only highlights that are still active
|
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());
|
const haps = pattern.queryArc(...span).filter((hap) => hap.hasOnset());
|
||||||
highlights.current = highlights.current.concat(haps); // add potential new onsets
|
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
|
highlightMiniLocations(view, begin, highlights.current);
|
||||||
highlightMiniLocations(view, highlights.current); // <- new method, replaces above line when done
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
highlightMiniLocations(view, 0, []);
|
||||||
}
|
}
|
||||||
frame = requestAnimationFrame(updateHighlights);
|
frame = requestAnimationFrame(updateHighlights);
|
||||||
});
|
});
|
||||||
@ -31,11 +38,14 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
|||||||
cancelAnimationFrame(frame);
|
cancelAnimationFrame(frame);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
console.log('not active');
|
||||||
highlights.current = [];
|
highlights.current = [];
|
||||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
highlightMiniLocations(view, 0, highlights.current);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [pattern, active, view]);
|
}, [pattern, active, view]);
|
||||||
|
|
||||||
|
return { setMiniLocations };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useHighlighting;
|
export default useHighlighting;
|
||||||
|
|||||||
@ -12,9 +12,7 @@ const simple = { wrapAsync: false, addReturn: false, simpleLocs: true };
|
|||||||
describe('transpiler', () => {
|
describe('transpiler', () => {
|
||||||
it('wraps double quote string with mini and adds location', () => {
|
it('wraps double quote string with mini and adds location', () => {
|
||||||
expect(transpiler('"c3"', simple).output).toEqual("m('c3', 0);");
|
expect(transpiler('"c3"', simple).output).toEqual("m('c3', 0);");
|
||||||
expect(transpiler('stack("c3","bd sd")', simple).output).toEqual(
|
expect(transpiler('stack("c3","bd sd")', simple).output).toEqual("stack(m('c3', 6), m('bd sd', 11));");
|
||||||
"stack(m('c3', 6), m('bd sd', 11));",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
it('wraps backtick string with mini and adds location', () => {
|
it('wraps backtick string with mini and adds location', () => {
|
||||||
expect(transpiler('`c3`', simple).output).toEqual("m('c3', 0);");
|
expect(transpiler('`c3`', simple).output).toEqual("m('c3', 0);");
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -292,6 +292,9 @@ importers:
|
|||||||
'@strudel.cycles/webaudio':
|
'@strudel.cycles/webaudio':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../webaudio
|
version: link:../webaudio
|
||||||
|
'@strudel/codemirror':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../codemirror
|
||||||
'@uiw/codemirror-themes':
|
'@uiw/codemirror-themes':
|
||||||
specifier: ^4.19.16
|
specifier: ^4.19.16
|
||||||
version: 4.19.16(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
|
version: 4.19.16(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -188,7 +188,7 @@ evalScope(
|
|||||||
|
|
||||||
export const queryCode = async (code, cycles = 1) => {
|
export const queryCode = async (code, cycles = 1) => {
|
||||||
const { pattern } = await evaluate(code);
|
const { pattern } = await evaluate(code);
|
||||||
const haps = pattern.queryArc(0, cycles);
|
const haps = pattern.sortHapsByPart().queryArc(0, cycles);
|
||||||
return haps.map((h) => h.show(true));
|
return haps.map((h) => h.show(true));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -138,9 +138,7 @@ export function Repl({ embedded = false }) {
|
|||||||
cleanupDraw();
|
cleanupDraw();
|
||||||
},
|
},
|
||||||
afterEval: ({ code, meta }) => {
|
afterEval: ({ code, meta }) => {
|
||||||
console.log('miniLocations', meta.miniLocations, view);
|
setMiniLocations(meta.miniLocations);
|
||||||
// TODO: find a way to get hold of the codemirror view
|
|
||||||
// then call updateMiniLocations
|
|
||||||
setPending(false);
|
setPending(false);
|
||||||
setLatestCode(code);
|
setLatestCode(code);
|
||||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||||
@ -190,7 +188,7 @@ export function Repl({ embedded = false }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// highlighting
|
// highlighting
|
||||||
useHighlighting({
|
const { setMiniLocations } = useHighlighting({
|
||||||
view,
|
view,
|
||||||
pattern,
|
pattern,
|
||||||
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
active: started && !activeCode?.includes('strudel disable-highlighting'),
|
||||||
@ -213,29 +211,6 @@ export function Repl({ embedded = false }) {
|
|||||||
// console.log('selectino change', selection.ranges[0].from);
|
// console.log('selectino change', selection.ranges[0].from);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleDocChanged = useCallback(
|
|
||||||
({ view }) => {
|
|
||||||
if (!init) {
|
|
||||||
// this is only for testing! try this pattern:
|
|
||||||
/*
|
|
||||||
stack(
|
|
||||||
s("bd"),
|
|
||||||
s("hh oh*<2 3>")
|
|
||||||
)
|
|
||||||
*/
|
|
||||||
updateMiniLocations(view, [
|
|
||||||
[12, 14],
|
|
||||||
[23, 25],
|
|
||||||
[26, 28],
|
|
||||||
[30, 31],
|
|
||||||
[32, 33],
|
|
||||||
]);
|
|
||||||
init = true;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[view],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTogglePlay = async () => {
|
const handleTogglePlay = async () => {
|
||||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||||
if (!started) {
|
if (!started) {
|
||||||
@ -330,7 +305,6 @@ stack(
|
|||||||
onChange={handleChangeCode}
|
onChange={handleChangeCode}
|
||||||
onViewChanged={handleViewChanged}
|
onViewChanged={handleViewChanged}
|
||||||
onSelectionChange={handleSelectionChange}
|
onSelectionChange={handleSelectionChange}
|
||||||
onDocChange={handleDocChanged}
|
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{error && (
|
{error && (
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user