Skip to content

Commit

Permalink
Merge pull request #636 from mindofmatthew/highlight-ids
Browse files Browse the repository at this point in the history
More work on highlight IDs
  • Loading branch information
felixroos committed Jul 4, 2023
2 parents 0b5d905 + 34176ab commit c82f7bd
Show file tree
Hide file tree
Showing 17 changed files with 4,835 additions and 5,031 deletions.
106 changes: 10 additions & 96 deletions packages/codemirror/codemirror.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { EditorState } from '@codemirror/state';
import { EditorView, keymap, Decoration, lineNumbers, highlightActiveLineGutter } from '@codemirror/view';
import { defaultKeymap } from '@codemirror/commands';
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
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 { repl, Drawer } from '@strudel.cycles/core';

// https://codemirror.net/docs/guide/
export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
Expand All @@ -15,7 +16,7 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
theme,
javascript(),
lineNumbers(),
highlightField,
highlightExtension,
highlightActiveLineGutter(),
syntaxHighlighting(defaultHighlightStyle),
keymap.of(defaultKeymap),
Expand All @@ -40,101 +41,14 @@ 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 {
constructor(options) {
const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
this.code = initialCode;

this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame);
this.highlight(currentFrame, time);
onDraw?.(haps, time, currentFrame);
}, drawTime);

Expand Down Expand Up @@ -193,7 +107,7 @@ export class StrudelMirror {
flash(ms) {
flash(this.editor, ms);
}
highlight(haps) {
highlightHaps(this.editor, haps);
highlight(haps, time) {
highlightMiniLocations(this.editor.view, time, haps);
}
}
35 changes: 35 additions & 0 deletions packages/codemirror/flash.mjs
Original file line number Diff line number Diff line change
@@ -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 changes: 126 additions & 0 deletions packages/codemirror/highlight.mjs
Original file line number Diff line number Diff line change
@@ -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 changes: 3 additions & 0 deletions packages/codemirror/index.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './codemirror.mjs';
export * from './highlight.mjs';
export * from './flash.mjs';
2 changes: 1 addition & 1 deletion packages/codemirror/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@strudel/codemirror",
"version": "0.8.4",
"description": "Codemirror Extensions for Strudel",
"main": "codemirror.mjs",
"main": "index.mjs",
"publishConfig": {
"main": "dist/index.js",
"module": "dist/index.mjs"
Expand Down
2 changes: 1 addition & 1 deletion packages/codemirror/vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default defineConfig({
plugins: [],
build: {
lib: {
entry: resolve(__dirname, 'codemirror.mjs'),
entry: resolve(__dirname, 'index.mjs'),
formats: ['es', 'cjs'],
fileName: (ext) => ({ es: 'index.mjs', cjs: 'index.js' }[ext]),
},
Expand Down
3 changes: 2 additions & 1 deletion packages/react/examples/nano-repl/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,10 @@ function App() {
code,
defaultOutput: webaudioOutput,
getTime,
afterEval: ({ meta }) => setMiniLocations(meta.miniLocations),
});

useHighlighting({
const { setMiniLocations } = useHighlighting({
view,
pattern,
active: started && !activeCode?.includes('strudel disable-highlighting'),
Expand Down
1 change: 1 addition & 0 deletions packages/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@strudel.cycles/core": "workspace:*",
"@strudel.cycles/transpiler": "workspace:*",
"@strudel.cycles/webaudio": "workspace:*",
"@strudel/codemirror": "workspace:*",
"@uiw/codemirror-themes": "^4.19.16",
"@uiw/react-codemirror": "^4.19.16",
"react-hook-inview": "^4.5.0"
Expand Down

0 comments on commit c82f7bd

Please sign in to comment.