wip: adaptive highlighting

This commit is contained in:
Felix Roos 2023-07-02 18:33:44 +02:00
parent f7bd373ce6
commit 63c23736ad
4 changed files with 124 additions and 6 deletions

View File

@ -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);
}

View File

@ -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: [] }) });
}

View File

@ -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

View File

@ -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 && (