mirror of
https://github.com/eliasstepanik/strudel.git
synced 2026-01-11 21:58:37 +00:00
wip: adaptive highlighting
This commit is contained in:
parent
f7bd373ce6
commit
63c23736ad
@ -44,12 +44,89 @@ export const flash = (view) => {
|
||||
}, 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.offset}:${end.offset}`))
|
||||
.flat()
|
||||
.filter((v, i, a) => a.indexOf(v) === i);
|
||||
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 ???
|
||||
|
||||
/* 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;
|
||||
},
|
||||
}); */
|
||||
}
|
||||
}
|
||||
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)) {
|
||||
@ -88,13 +165,14 @@ const highlightField = StateField.define({
|
||||
provide: (f) => EditorView.decorations.from(f),
|
||||
});
|
||||
|
||||
const staticExtensions = [javascript(), highlightField, flashField];
|
||||
const staticExtensions = [javascript(), highlightField, flashField, miniLocations];
|
||||
|
||||
export default function CodeMirror({
|
||||
value,
|
||||
onChange,
|
||||
onViewChanged,
|
||||
onSelectionChange,
|
||||
onDocChange,
|
||||
theme,
|
||||
keybindings,
|
||||
isLineNumbersDisplayed,
|
||||
@ -121,6 +199,9 @@ export default function CodeMirror({
|
||||
|
||||
const handleOnUpdate = useCallback(
|
||||
(viewUpdate) => {
|
||||
if (viewUpdate.docChanged && onDocChange) {
|
||||
onDocChange?.(viewUpdate);
|
||||
}
|
||||
if (viewUpdate.selectionSet && onSelectionChange) {
|
||||
onSelectionChange?.(viewUpdate.state.selection);
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { setHighlights } from '../components/CodeMirror6';
|
||||
import { setHighlights, highlightMiniLocations } from '../components/CodeMirror6';
|
||||
const round = (x) => Math.round(x * 1000) / 1000;
|
||||
|
||||
function useHighlighting({ view, pattern, active, getTime }) {
|
||||
@ -21,6 +21,7 @@ function useHighlighting({ view, pattern, active, getTime }) {
|
||||
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
|
||||
highlightMiniLocations(view, highlights.current); // <- new method, replaces above line when done
|
||||
} catch (err) {
|
||||
view.dispatch({ effects: setHighlights.of({ haps: [] }) });
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
// import 'tailwindcss/tailwind.css';
|
||||
|
||||
export { default as CodeMirror, flash } from './components/CodeMirror6'; // !SSR
|
||||
export { default as CodeMirror, flash, updateMiniLocations, highlightMiniLocations } from './components/CodeMirror6'; // !SSR
|
||||
export * from './components/MiniRepl'; // !SSR
|
||||
export { default as useHighlighting } from './hooks/useHighlighting'; // !SSR
|
||||
export { default as useStrudel } from './hooks/useStrudel'; // !SSR
|
||||
|
||||
@ -5,7 +5,15 @@ This program is free software: you can redistribute it and/or modify it under th
|
||||
*/
|
||||
|
||||
import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
|
||||
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
|
||||
import {
|
||||
CodeMirror,
|
||||
cx,
|
||||
flash,
|
||||
useHighlighting,
|
||||
useStrudel,
|
||||
useKeydown,
|
||||
updateMiniLocations,
|
||||
} from '@strudel.cycles/react';
|
||||
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import { nanoid } from 'nanoid';
|
||||
@ -101,12 +109,13 @@ const { code: randomTune, name } = getRandomTune();
|
||||
|
||||
export const ReplContext = createContext(null);
|
||||
|
||||
let init = false; // this is bad! only for testing!
|
||||
|
||||
export function Repl({ embedded = false }) {
|
||||
const isEmbedded = embedded || window.location !== window.parent.location;
|
||||
const [view, setView] = useState(); // codemirror view
|
||||
const [lastShared, setLastShared] = useState();
|
||||
const [pending, setPending] = useState(true);
|
||||
|
||||
const {
|
||||
theme,
|
||||
keybindings,
|
||||
@ -129,7 +138,9 @@ export function Repl({ embedded = false }) {
|
||||
cleanupDraw();
|
||||
},
|
||||
afterEval: ({ code, meta }) => {
|
||||
console.log('miniLocations', meta.miniLocations);
|
||||
console.log('miniLocations', meta.miniLocations, view);
|
||||
// TODO: find a way to get hold of the codemirror view
|
||||
// then call updateMiniLocations
|
||||
setPending(false);
|
||||
setLatestCode(code);
|
||||
window.location.hash = '#' + encodeURIComponent(btoa(code));
|
||||
@ -201,6 +212,30 @@ export function Repl({ embedded = false }) {
|
||||
// TODO: scroll to selected function in reference
|
||||
// console.log('selectino change', selection.ranges[0].from);
|
||||
}, []);
|
||||
|
||||
const handleDocChanged = useCallback(
|
||||
(v) => {
|
||||
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 () => {
|
||||
await getAudioContext().resume(); // fixes no sound in ios webkit
|
||||
if (!started) {
|
||||
@ -295,6 +330,7 @@ export function Repl({ embedded = false }) {
|
||||
onChange={handleChangeCode}
|
||||
onViewChanged={handleViewChanged}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onDocChange={handleDocChanged}
|
||||
/>
|
||||
</section>
|
||||
{error && (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user