diff --git a/packages/codemirror/Autocomplete.jsx b/packages/codemirror/Autocomplete.jsx
new file mode 100644
index 00000000..18f172ee
--- /dev/null
+++ b/packages/codemirror/Autocomplete.jsx
@@ -0,0 +1,88 @@
+import { createRoot } from 'react-dom/client';
+import jsdoc from '../../doc.json';
+// import { javascriptLanguage } from '@codemirror/lang-javascript';
+import { autocompletion } from '@codemirror/autocomplete';
+
+const getDocLabel = (doc) => doc.name || doc.longname;
+const getInnerText = (html) => {
+ var div = document.createElement('div');
+ div.innerHTML = html;
+ return div.textContent || div.innerText || '';
+};
+
+export function Autocomplete({ doc }) {
+ return (
+
+
{getDocLabel(doc)}
+
+
+ {doc.params?.map(({ name, type, description }, i) => (
+ -
+ {name} : {type.names?.join(' | ')} {description ? <> - {getInnerText(description)}> : ''}
+
+ ))}
+
+
+ {doc.examples?.map((example, i) => (
+
+
{
+ console.log('ola!');
+ navigator.clipboard.writeText(example);
+ e.stopPropagation();
+ }}
+ >
+ {example}
+
+
+ ))}
+
+
+ );
+}
+
+const jsdocCompletions = jsdoc.docs
+ .filter(
+ (doc) =>
+ getDocLabel(doc) &&
+ !getDocLabel(doc).startsWith('_') &&
+ !['package'].includes(doc.kind) &&
+ !['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)),
+ )
+ // https://codemirror.net/docs/ref/#autocomplete.Completion
+ .map((doc) /*: Completion */ => ({
+ label: getDocLabel(doc),
+ // detail: 'xxx', // An optional short piece of information to show (with a different style) after the label.
+ info: () => {
+ const node = document.createElement('div');
+ // if Autocomplete is non-interactive, it could also be rendered at build time..
+ // .. using renderToStaticMarkup
+ createRoot(node).render();
+ return node;
+ },
+ type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
+ }));
+
+export const strudelAutocomplete = (context /* : CompletionContext */) => {
+ let word = context.matchBefore(/\w*/);
+ if (word.from == word.to && !context.explicit) return null;
+ return {
+ from: word.from,
+ options: jsdocCompletions,
+ /* options: [
+ { label: 'match', type: 'keyword' },
+ { label: 'hello', type: 'variable', info: '(World)' },
+ { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' },
+ ], */
+ };
+};
+
+export function isAutoCompletionEnabled(on) {
+ return on
+ ? [
+ autocompletion({ override: [strudelAutocomplete] }),
+ //javascriptLanguage.data.of({ autocomplete: strudelAutocomplete }),
+ ]
+ : []; // autocompletion({ override: [] })
+}
diff --git a/packages/codemirror/codemirror.mjs b/packages/codemirror/codemirror.mjs
index 2094416e..7933600e 100644
--- a/packages/codemirror/codemirror.mjs
+++ b/packages/codemirror/codemirror.mjs
@@ -1,36 +1,77 @@
-import { defaultKeymap } from '@codemirror/commands';
+import { closeBrackets } from '@codemirror/autocomplete';
+// import { search, highlightSelectionMatches } from '@codemirror/search';
+import { history } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
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 { Compartment, EditorState } from '@codemirror/state';
+import { EditorView, highlightActiveLineGutter, highlightActiveLine, keymap, lineNumbers } from '@codemirror/view';
+import { Pattern, Drawer, repl, cleanupDraw } from '@strudel.cycles/core';
+// import { isAutoCompletionEnabled } from './Autocomplete';
+import { flash, isFlashEnabled } from './flash.mjs';
+import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
+import { keybindings } from './keybindings.mjs';
+import { theme } from './themes.mjs';
+import { updateWidgets, sliderPlugin } from './slider.mjs';
+
+const extensions = {
+ isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
+ isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
+ theme,
+ // isAutoCompletionEnabled,
+ isPatternHighlightingEnabled,
+ isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
+ isFlashEnabled,
+ keybindings,
+};
+const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
// https://codemirror.net/docs/guide/
-export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, theme = oneDark, root }) {
+export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, settings, root }) {
+ const initialSettings = Object.keys(compartments).map((key) =>
+ compartments[key].of(extensions[key](parseBooleans(settings[key]))),
+ );
let state = EditorState.create({
doc: initialCode,
extensions: [
- theme,
+ /* search(),
+ highlightSelectionMatches(), */
+ ...initialSettings,
javascript(),
- lineNumbers(),
- highlightExtension,
- highlightActiveLineGutter(),
+ sliderPlugin,
+ // indentOnInput(), // works without. already brought with javascript extension?
+ // bracketMatching(), // does not do anything
+ closeBrackets(),
syntaxHighlighting(defaultHighlightStyle),
- keymap.of(defaultKeymap),
- flashField,
+ history(),
EditorView.updateListener.of((v) => onChange(v)),
keymap.of([
{
key: 'Ctrl-Enter',
- run: () => onEvaluate(),
+ run: () => onEvaluate?.(),
+ },
+ {
+ key: 'Alt-Enter',
+ run: () => onEvaluate?.(),
},
{
key: 'Ctrl-.',
- run: () => onStop(),
+ run: () => onStop?.(),
},
+ {
+ key: 'Alt-.',
+ run: (_, e) => {
+ e.preventDefault();
+ onStop?.();
+ },
+ },
+ /* {
+ key: 'Ctrl-Shift-.',
+ run: () => (onPanic ? onPanic() : onStop?.()),
+ },
+ {
+ key: 'Ctrl-Shift-Enter',
+ run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
+ }, */
]),
],
});
@@ -43,71 +84,159 @@ export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, the
export class StrudelMirror {
constructor(options) {
- const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, ...replOptions } = options;
+ const { root, initialCode = '', onDraw, drawTime = [-2, 2], prebake, settings, ...replOptions } = options;
this.code = initialCode;
+ this.root = root;
+ this.miniLocations = [];
+ this.widgets = [];
+ this.painters = [];
+ this.onDraw = onDraw;
+ const self = this;
this.drawer = new Drawer((haps, time) => {
const currentFrame = haps.filter((hap) => time >= hap.whole.begin && time <= hap.endClipped);
this.highlight(currentFrame, time);
- onDraw?.(haps, time, currentFrame);
+ this.onDraw?.(haps, time, currentFrame, this.painters);
}, drawTime);
- const prebaked = prebake();
- prebaked.then(async () => {
- if (!onDraw) {
- return;
- }
- const { scheduler, evaluate } = await this.repl;
- // draw first frame instantly
- prebaked.then(async () => {
- await evaluate(this.code, false);
- this.drawer.invalidate(scheduler);
- onDraw?.(this.drawer.visibleHaps, 0, []);
- });
- });
+ // this approach might not work with multiple repls on screen..
+ Pattern.prototype.onPaint = function (onPaint) {
+ self.painters.push(onPaint);
+ return this;
+ };
+
+ this.prebaked = prebake();
+ // this.drawFirstFrame();
this.repl = repl({
...replOptions,
- onToggle: async (started) => {
+ onToggle: (started) => {
replOptions?.onToggle?.(started);
- const { scheduler } = await this.repl;
if (started) {
- this.drawer.start(scheduler);
+ this.drawer.start(this.repl.scheduler);
} else {
this.drawer.stop();
+ updateMiniLocations(this.editor, []);
+ cleanupDraw(false);
}
},
beforeEval: async () => {
- await prebaked;
+ cleanupDraw();
+ this.painters = [];
+ await this.prebaked;
+ await replOptions?.beforeEval?.();
},
afterEval: (options) => {
+ // remember for when highlighting is toggled on
+ this.miniLocations = options.meta?.miniLocations;
+ this.widgets = options.meta?.widgets;
+ updateWidgets(this.editor, this.widgets);
+ updateMiniLocations(this.editor, this.miniLocations);
replOptions?.afterEval?.(options);
this.drawer.invalidate();
},
});
this.editor = initEditor({
root,
+ settings,
initialCode,
onChange: (v) => {
- this.code = v.state.doc.toString();
+ if (v.docChanged) {
+ this.code = v.state.doc.toString();
+ // TODO: repl is still untouched to make sure the old Repl.jsx stays untouched..
+ // this.repl.setCode(this.code);
+ }
},
onEvaluate: () => this.evaluate(),
onStop: () => this.stop(),
});
}
+ async drawFirstFrame() {
+ if (!this.onDraw) {
+ return;
+ }
+ // draw first frame instantly
+ await this.prebaked;
+ try {
+ await this.repl.evaluate(this.code, false);
+ this.drawer.invalidate(this.repl.scheduler);
+ this.onDraw?.(this.drawer.visibleHaps, 0, []);
+ } catch (err) {
+ console.warn('first frame could not be painted');
+ }
+ }
async evaluate() {
- const { evaluate } = await this.repl;
this.flash();
- await evaluate(this.code);
+ await this.repl.evaluate(this.code);
}
async stop() {
- const { scheduler } = await this.repl;
- scheduler.stop();
+ this.repl.scheduler.stop();
+ }
+ async toggle() {
+ if (this.repl.scheduler.started) {
+ this.repl.scheduler.stop();
+ } else {
+ this.evaluate();
+ }
}
flash(ms) {
flash(this.editor, ms);
}
highlight(haps, time) {
- highlightMiniLocations(this.editor.view, time, haps);
+ highlightMiniLocations(this.editor, time, haps);
+ }
+ setFontSize(size) {
+ this.root.style.fontSize = size + 'px';
+ }
+ setFontFamily(family) {
+ this.root.style.fontFamily = family;
+ }
+ reconfigureExtension(key, value) {
+ if (!extensions[key]) {
+ console.warn(`extension ${key} is not known`);
+ return;
+ }
+ value = parseBooleans(value);
+ const newValue = extensions[key](value, this);
+ this.editor.dispatch({
+ effects: compartments[key].reconfigure(newValue),
+ });
+ }
+ setLineWrappingEnabled(enabled) {
+ this.reconfigureExtension('isLineWrappingEnabled', enabled);
+ }
+ setLineNumbersDisplayed(enabled) {
+ this.reconfigureExtension('isLineNumbersDisplayed', enabled);
+ }
+ setTheme(theme) {
+ this.reconfigureExtension('theme', theme);
+ }
+ setAutocompletionEnabled(enabled) {
+ this.reconfigureExtension('isAutoCompletionEnabled', enabled);
+ }
+ updateSettings(settings) {
+ this.setFontSize(settings.fontSize);
+ this.setFontFamily(settings.fontFamily);
+ for (let key in extensions) {
+ this.reconfigureExtension(key, settings[key]);
+ }
+ }
+ changeSetting(key, value) {
+ if (extensions[key]) {
+ this.reconfigureExtension(key, value);
+ return;
+ } else if (key === 'fontFamily') {
+ this.setFontFamily(value);
+ } else if (key === 'fontSize') {
+ this.setFontSize(value);
+ }
+ }
+ setCode(code) {
+ const changes = { from: 0, to: this.editor.state.doc.length, insert: code };
+ this.editor.dispatch({ changes });
}
}
+
+function parseBooleans(value) {
+ return { true: true, false: false }[value] ?? value;
+}
diff --git a/packages/codemirror/examples/strudelmirror/.gitignore b/packages/codemirror/examples/strudelmirror/.gitignore
new file mode 100644
index 00000000..a547bf36
--- /dev/null
+++ b/packages/codemirror/examples/strudelmirror/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/packages/codemirror/examples/strudelmirror/index.html b/packages/codemirror/examples/strudelmirror/index.html
new file mode 100644
index 00000000..0e1d43ce
--- /dev/null
+++ b/packages/codemirror/examples/strudelmirror/index.html
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+ StrudelMirror Example
+
+
+
+
+
+
+
+
+
diff --git a/packages/codemirror/examples/strudelmirror/main.js b/packages/codemirror/examples/strudelmirror/main.js
new file mode 100644
index 00000000..676f4b97
--- /dev/null
+++ b/packages/codemirror/examples/strudelmirror/main.js
@@ -0,0 +1,199 @@
+import { logger, getDrawContext, silence, controls, evalScope, hash2code, code2hash } from '@strudel.cycles/core';
+import { StrudelMirror } from '@strudel/codemirror';
+import { transpiler } from '@strudel.cycles/transpiler';
+import {
+ getAudioContext,
+ webaudioOutput,
+ registerSynthSounds,
+ registerZZFXSounds,
+ samples,
+} from '@strudel.cycles/webaudio';
+import './style.css';
+
+let editor;
+const initialSettings = {
+ keybindings: 'codemirror',
+ isLineNumbersDisplayed: true,
+ isActiveLineHighlighted: true,
+ isAutoCompletionEnabled: false,
+ isPatternHighlightingEnabled: true,
+ isFlashEnabled: true,
+ isTooltipEnabled: false,
+ isLineWrappingEnabled: false,
+ theme: 'teletext',
+ fontFamily: 'monospace',
+ fontSize: 18,
+};
+
+async function run() {
+ const container = document.getElementById('code');
+ if (!container) {
+ console.warn('could not init: no container found');
+ return;
+ }
+
+ const drawContext = getDrawContext();
+ const drawTime = [-2, 2];
+ editor = new StrudelMirror({
+ defaultOutput: webaudioOutput,
+ getTime: () => getAudioContext().currentTime,
+ transpiler,
+ root: container,
+ initialCode: '// LOADING',
+ pattern: silence,
+ settings: initialSettings,
+ drawTime,
+ onDraw: (haps, time, frame, painters) => {
+ painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
+ painters?.forEach((painter) => {
+ // ctx time haps drawTime paintOptions
+ painter(drawContext, time, haps, drawTime, { clear: false });
+ });
+ },
+ prebake: async () => {
+ // populate scope / lazy load modules
+ const modulesLoading = evalScope(
+ import('@strudel.cycles/core'),
+ import('@strudel.cycles/tonal'),
+ import('@strudel.cycles/mini'),
+ // import('@strudel.cycles/xen'),
+ import('@strudel.cycles/webaudio'),
+ import('@strudel/codemirror'),
+ /* import('@strudel/hydra'), */
+ // import('@strudel.cycles/serial'),
+ /* import('@strudel.cycles/soundfonts'), */
+ // import('@strudel.cycles/csound'),
+ /* import('@strudel.cycles/midi'), */
+ // import('@strudel.cycles/osc'),
+ controls, // sadly, this cannot be exported from core directly (yet)
+ );
+ // load samples
+ const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
+ await Promise.all([
+ modulesLoading,
+ registerSynthSounds(),
+ registerZZFXSounds(),
+ samples(`${ds}/tidal-drum-machines.json`),
+ samples(`${ds}/piano.json`),
+ samples(`${ds}/Dirt-Samples.json`),
+ samples(`${ds}/EmuSP12.json`),
+ samples(`${ds}/vcsl.json`),
+ ]);
+ },
+ afterEval: ({ code }) => {
+ window.location.hash = '#' + code2hash(code);
+ },
+ });
+
+ // init settings
+ editor.updateSettings(initialSettings);
+
+ logger(`Welcome to Strudel! Click into the editor and then hit ctrl+enter to run the code!`, 'highlight');
+ const codeParam = window.location.href.split('#')[1] || '';
+
+ const initialCode = codeParam
+ ? hash2code(codeParam)
+ : `// @date 23-11-30
+// "teigrührgerät" @by froos
+
+stack(
+ stack(
+ s("bd(<3!3 5>,6)/2").bank('RolandTR707')
+ ,
+ s("~ sd:<0 1>").bank('RolandTR707').room("<0 .5>")
+ .lastOf(8, x=>x.segment("12").end(.2).gain(isaw))
+ ,
+ s("[tb ~ tb]").bank('RolandTR707')
+ .clip(0).release(.08).room(.2)
+ ).off(-1/6, x=>x.speed(.7).gain(.2).degrade())
+ ,
+ stack(
+ note(",6) ~!2 [f1?]*2>")
+ .s("sawtooth").lpf(perlin.range(400,1000))
+ .lpa(.1).lpenv(-3).room(.2)
+ .lpq(8).noise(.2)
+ .add(note("0,.1"))
+ ,
+ chord("<~ Gm9 ~!2>")
+ .dict('ireal').voicing()
+ .s("sawtooth").vib("2:.1")
+ .lpf(1000).lpa(.1).lpenv(-4)
+ .room(.5)
+ ,
+ n(run(3)).chord("/8")
+ .dict('ireal-ext')
+ .off(1/2, add(n(4)))
+ .voicing()
+ .clip(.1).release(.05)
+ .s("sine").jux(rev)
+ .sometimesBy(sine.slow(16), add(note(12)))
+ .room(.75)
+ .lpf(sine.range(200,2000).slow(16))
+ .gain(saw.slow(4).div(2))
+ ).add(note(perlin.range(0,.5)))
+)`;
+
+ editor.setCode(initialCode); // simpler alternative to above init
+
+ // settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key]));
+ onEvent('strudel-toggle-play', () => editor.toggle());
+}
+
+run();
+
+function onEvent(key, callback) {
+ const listener = (e) => {
+ if (e.data === key) {
+ callback();
+ }
+ };
+ window.addEventListener('message', listener);
+ return () => window.removeEventListener('message', listener);
+}
+
+// settings form
+function getInput(form, name) {
+ return form.querySelector(`input[name=${name}]`) || form.querySelector(`select[name=${name}]`);
+}
+function getFormValues(form, initial) {
+ const entries = Object.entries(initial).map(([key, initialValue]) => {
+ const input = getInput(form, key);
+ if (!input) {
+ return [key, initialValue]; // fallback
+ }
+ if (input.type === 'checkbox') {
+ return [key, input.checked];
+ }
+ if (input.type === 'number') {
+ return [key, Number(input.value)];
+ }
+ if (input.tagName === 'SELECT') {
+ return [key, input.value];
+ }
+ return [key, input.value];
+ });
+ return Object.fromEntries(entries);
+}
+function setFormValues(form, values) {
+ Object.entries(values).forEach(([key, value]) => {
+ const input = getInput(form, key);
+ if (!input) {
+ return;
+ }
+ if (input.type === 'checkbox') {
+ input.checked = !!value;
+ } else if (input.type === 'number') {
+ input.value = value;
+ } else if (input.tagName) {
+ input.value = value;
+ }
+ });
+}
+
+const form = document.querySelector('form[name=settings]');
+setFormValues(form, initialSettings);
+form.addEventListener('change', () => {
+ const values = getFormValues(form, initialSettings);
+ // console.log('values', values);
+ editor.updateSettings(values);
+});
diff --git a/packages/codemirror/examples/strudelmirror/package.json b/packages/codemirror/examples/strudelmirror/package.json
new file mode 100644
index 00000000..5c946bff
--- /dev/null
+++ b/packages/codemirror/examples/strudelmirror/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "strudelmirror",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "vite": "^5.0.8"
+ },
+ "dependencies": {
+ "@strudel/codemirror": "workspace:*",
+ "@strudel.cycles/core":"workspace:*",
+ "@strudel.cycles/transpiler":"workspace:*",
+ "@strudel.cycles/tonal":"workspace:*",
+ "@strudel.cycles/mini":"workspace:*",
+ "@strudel.cycles/xen":"workspace:*",
+ "@strudel.cycles/webaudio":"workspace:*",
+ "@strudel/hydra":"workspace:*",
+ "@strudel.cycles/serial":"workspace:*",
+ "@strudel.cycles/soundfonts":"workspace:*",
+ "@strudel.cycles/csound":"workspace:*",
+ "@strudel.cycles/midi":"workspace:*",
+ "@strudel.cycles/osc":"workspace:*"
+ }
+}
diff --git a/packages/codemirror/examples/strudelmirror/style.css b/packages/codemirror/examples/strudelmirror/style.css
new file mode 100644
index 00000000..fabc795c
--- /dev/null
+++ b/packages/codemirror/examples/strudelmirror/style.css
@@ -0,0 +1,33 @@
+:root {
+ --foreground: white;
+}
+
+body,
+input {
+ font-family: monospace;
+ background: black;
+ color: white;
+}
+
+html,
+body,
+#code,
+.cm-editor,
+.cm-scroller {
+ padding: 0;
+ margin: 0;
+ height: 100%;
+}
+
+.settings {
+ position: fixed;
+ right: 0;
+ top: 0;
+ z-index: 1000;
+ display: flex-col;
+ padding: 10px;
+}
+
+.settings > form > * + * {
+ margin-top: 10px;
+}
diff --git a/packages/codemirror/flash.mjs b/packages/codemirror/flash.mjs
index 9bc5c593..6b37038f 100644
--- a/packages/codemirror/flash.mjs
+++ b/packages/codemirror/flash.mjs
@@ -33,3 +33,5 @@ export const flash = (view, ms = 200) => {
view.dispatch({ effects: setFlash.of(false) });
}, ms);
};
+
+export const isFlashEnabled = (on) => (on ? flashField : []);
diff --git a/packages/codemirror/highlight.mjs b/packages/codemirror/highlight.mjs
index 317c5fdf..79724f8f 100644
--- a/packages/codemirror/highlight.mjs
+++ b/packages/codemirror/highlight.mjs
@@ -124,3 +124,12 @@ const miniLocationHighlights = EditorView.decorations.compute([miniLocations, vi
});
export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
+
+export const isPatternHighlightingEnabled = (on, config) => {
+ on &&
+ config &&
+ setTimeout(() => {
+ updateMiniLocations(config.editor, config.miniLocations);
+ }, 100);
+ return on ? highlightExtension : [];
+};
diff --git a/packages/codemirror/keybindings.mjs b/packages/codemirror/keybindings.mjs
new file mode 100644
index 00000000..6fe00eda
--- /dev/null
+++ b/packages/codemirror/keybindings.mjs
@@ -0,0 +1,31 @@
+import { Prec } from '@codemirror/state';
+import { keymap, ViewPlugin } from '@codemirror/view';
+// import { searchKeymap } from '@codemirror/search';
+import { emacs } from '@replit/codemirror-emacs';
+import { vim } from '@replit/codemirror-vim';
+import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
+import { defaultKeymap, historyKeymap } from '@codemirror/commands';
+
+const vscodePlugin = ViewPlugin.fromClass(
+ class {
+ constructor() {}
+ },
+ {
+ provide: () => {
+ return Prec.highest(keymap.of([...vscodeKeymap]));
+ },
+ },
+);
+const vscodeExtension = (options) => [vscodePlugin].concat(options ?? []);
+
+const keymaps = {
+ vim,
+ emacs,
+ vscode: vscodeExtension,
+};
+
+export function keybindings(name) {
+ const active = keymaps[name];
+ return [keymap.of(defaultKeymap), keymap.of(historyKeymap), active ? active() : []];
+ // keymap.of(searchKeymap),
+}
diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json
index 4e443648..a309efa2 100644
--- a/packages/codemirror/package.json
+++ b/packages/codemirror/package.json
@@ -33,13 +33,21 @@
},
"homepage": "https://github.com/tidalcycles/strudel#readme",
"dependencies": {
+ "@codemirror/autocomplete": "^6.6.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.7",
"@codemirror/language": "^6.6.0",
+ "@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.2.0",
"@codemirror/view": "^6.10.0",
"@lezer/highlight": "^1.1.4",
- "@strudel.cycles/core": "workspace:*"
+ "@replit/codemirror-emacs": "^6.0.1",
+ "@replit/codemirror-vim": "^6.0.14",
+ "@replit/codemirror-vscode-keymap": "^6.0.2",
+ "@strudel.cycles/core": "workspace:*",
+ "@uiw/codemirror-themes": "^4.19.16",
+ "@uiw/codemirror-themes-all": "^4.19.16",
+ "react-dom": "^18.2.0"
},
"devDependencies": {
"vite": "^4.3.3"
diff --git a/packages/codemirror/themes.mjs b/packages/codemirror/themes.mjs
new file mode 100644
index 00000000..1f520623
--- /dev/null
+++ b/packages/codemirror/themes.mjs
@@ -0,0 +1,484 @@
+import {
+ abcdef,
+ androidstudio,
+ atomone,
+ aura,
+ bespin,
+ darcula,
+ dracula,
+ duotoneDark,
+ eclipse,
+ githubDark,
+ gruvboxDark,
+ materialDark,
+ nord,
+ okaidia,
+ solarizedDark,
+ sublime,
+ tokyoNight,
+ tokyoNightStorm,
+ vscodeDark,
+ xcodeDark,
+ bbedit,
+ duotoneLight,
+ githubLight,
+ gruvboxLight,
+ materialLight,
+ noctisLilac,
+ solarizedLight,
+ tokyoNightDay,
+ xcodeLight,
+} from '@uiw/codemirror-themes-all';
+
+import strudelTheme from './themes/strudel-theme';
+import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen';
+import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen';
+import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen';
+import teletext, { settings as teletextSettings } from './themes/teletext';
+import algoboy, { settings as algoboySettings } from './themes/algoboy';
+import terminal, { settings as terminalSettings } from './themes/terminal';
+
+export const themes = {
+ strudelTheme,
+ bluescreen,
+ blackscreen,
+ whitescreen,
+ teletext,
+ algoboy,
+ terminal,
+ abcdef,
+ androidstudio,
+ atomone,
+ aura,
+ bespin,
+ darcula,
+ dracula,
+ duotoneDark,
+ eclipse,
+ githubDark,
+ gruvboxDark,
+ materialDark,
+ nord,
+ okaidia,
+ solarizedDark,
+ sublime,
+ tokyoNight,
+ tokyoNightStorm,
+ vscodeDark,
+ xcodeDark,
+ bbedit,
+ duotoneLight,
+ githubLight,
+ gruvboxLight,
+ materialLight,
+ noctisLilac,
+ solarizedLight,
+ tokyoNightDay,
+ xcodeLight,
+};
+
+// lineBackground is background with 50% opacity, to make sure the selection below is visible
+
+export const settings = {
+ strudelTheme: {
+ background: '#222',
+ lineBackground: '#22222299',
+ foreground: '#fff',
+ // foreground: '#75baff',
+ caret: '#ffcc00',
+ selection: 'rgba(128, 203, 196, 0.5)',
+ selectionMatch: '#036dd626',
+ // lineHighlight: '#8a91991a', // original
+ lineHighlight: '#00000050',
+ gutterBackground: 'transparent',
+ // gutterForeground: '#8a919966',
+ gutterForeground: '#8a919966',
+ },
+ bluescreen: bluescreenSettings,
+ blackscreen: blackscreenSettings,
+ whitescreen: whitescreenSettings,
+ teletext: teletextSettings,
+ algoboy: algoboySettings,
+ terminal: terminalSettings,
+ abcdef: {
+ background: '#0f0f0f',
+ lineBackground: '#0f0f0f99',
+ foreground: '#defdef',
+ caret: '#00FF00',
+ selection: '#515151',
+ selectionMatch: '#515151',
+ gutterBackground: '#555',
+ gutterForeground: '#FFFFFF',
+ lineHighlight: '#314151',
+ },
+ androidstudio: {
+ background: '#282b2e',
+ lineBackground: '#282b2e99',
+ foreground: '#a9b7c6',
+ caret: '#00FF00',
+ selection: '#343739',
+ selectionMatch: '#343739',
+ lineHighlight: '#343739',
+ },
+ atomone: {
+ background: '#272C35',
+ lineBackground: '#272C3599',
+ foreground: '#9d9b97',
+ caret: '#797977',
+ selection: '#ffffff30',
+ selectionMatch: '#2B323D',
+ gutterBackground: '#272C35',
+ gutterForeground: '#465063',
+ gutterBorder: 'transparent',
+ lineHighlight: '#2B323D',
+ },
+ aura: {
+ background: '#21202e',
+ lineBackground: '#21202e99',
+ foreground: '#edecee',
+ caret: '#a277ff',
+ selection: '#3d375e7f',
+ selectionMatch: '#3d375e7f',
+ gutterBackground: '#21202e',
+ gutterForeground: '#edecee',
+ gutterBorder: 'transparent',
+ lineHighlight: '#a394f033',
+ },
+ bbedit: {
+ light: true,
+ background: '#FFFFFF',
+ lineBackground: '#FFFFFF99',
+ foreground: '#000000',
+ caret: '#FBAC52',
+ selection: '#FFD420',
+ selectionMatch: '#FFD420',
+ gutterBackground: '#f5f5f5',
+ gutterForeground: '#4D4D4C',
+ gutterBorder: 'transparent',
+ lineHighlight: '#00000012',
+ },
+ bespin: {
+ background: '#28211c',
+ lineBackground: '#28211c99',
+ foreground: '#9d9b97',
+ caret: '#797977',
+ selection: '#36312e',
+ selectionMatch: '#4f382b',
+ gutterBackground: '#28211c',
+ gutterForeground: '#666666',
+ lineHighlight: 'rgba(255, 255, 255, 0.1)',
+ },
+ darcula: {
+ background: '#2B2B2B',
+ lineBackground: '#2B2B2B99',
+ foreground: '#f8f8f2',
+ caret: '#FFFFFF',
+ selection: 'rgba(255, 255, 255, 0.1)',
+ selectionMatch: 'rgba(255, 255, 255, 0.2)',
+ gutterBackground: 'rgba(255, 255, 255, 0.1)',
+ gutterForeground: '#999',
+ gutterBorder: 'transparent',
+ lineHighlight: 'rgba(255, 255, 255, 0.1)',
+ },
+ dracula: {
+ background: '#282a36',
+ lineBackground: '#282a3699',
+ foreground: '#f8f8f2',
+ caret: '#f8f8f0',
+ selection: 'rgba(255, 255, 255, 0.1)',
+ selectionMatch: 'rgba(255, 255, 255, 0.2)',
+ gutterBackground: '#282a36',
+ gutterForeground: '#6D8A88',
+ gutterBorder: 'transparent',
+ lineHighlight: 'rgba(255, 255, 255, 0.1)',
+ },
+ duotoneLight: {
+ light: true,
+ background: '#faf8f5',
+ lineBackground: '#faf8f599',
+ foreground: '#b29762',
+ caret: '#93abdc',
+ selection: '#e3dcce',
+ selectionMatch: '#e3dcce',
+ gutterBackground: '#faf8f5',
+ gutterForeground: '#cdc4b1',
+ gutterBorder: 'transparent',
+ lineHighlight: '#EFEFEF',
+ },
+ duotoneDark: {
+ background: '#2a2734',
+ lineBackground: '#2a273499',
+ foreground: '#6c6783',
+ caret: '#ffad5c',
+ selection: 'rgba(255, 255, 255, 0.1)',
+ gutterBackground: '#2a2734',
+ gutterForeground: '#545167',
+ lineHighlight: '#36334280',
+ },
+ eclipse: {
+ light: true,
+ background: '#fff',
+ lineBackground: '#ffffff99',
+ foreground: '#000',
+ caret: '#FFFFFF',
+ selection: '#d7d4f0',
+ selectionMatch: '#d7d4f0',
+ gutterBackground: '#f7f7f7',
+ gutterForeground: '#999',
+ lineHighlight: '#e8f2ff',
+ gutterBorder: 'transparent',
+ },
+ githubLight: {
+ light: true,
+ background: '#fff',
+ lineBackground: '#ffffff99',
+ foreground: '#24292e',
+ selection: '#BBDFFF',
+ selectionMatch: '#BBDFFF',
+ gutterBackground: '#fff',
+ gutterForeground: '#6e7781',
+ },
+ githubDark: {
+ background: '#0d1117',
+ lineBackground: '#0d111799',
+ foreground: '#c9d1d9',
+ caret: '#c9d1d9',
+ selection: '#003d73',
+ selectionMatch: '#003d73',
+ lineHighlight: '#36334280',
+ },
+ gruvboxDark: {
+ background: '#282828',
+ lineBackground: '#28282899',
+ foreground: '#ebdbb2',
+ caret: '#ebdbb2',
+ selection: '#bdae93',
+ selectionMatch: '#bdae93',
+ lineHighlight: '#3c3836',
+ gutterBackground: '#282828',
+ gutterForeground: '#7c6f64',
+ },
+ gruvboxLight: {
+ light: true,
+ background: '#fbf1c7',
+ lineBackground: '#fbf1c799',
+ foreground: '#3c3836',
+ caret: '#af3a03',
+ selection: '#ebdbb2',
+ selectionMatch: '#bdae93',
+ lineHighlight: '#ebdbb2',
+ gutterBackground: '#ebdbb2',
+ gutterForeground: '#665c54',
+ gutterBorder: 'transparent',
+ },
+ materialDark: {
+ background: '#2e3235',
+ lineBackground: '#2e323599',
+ foreground: '#bdbdbd',
+ caret: '#a0a4ae',
+ selection: '#d7d4f0',
+ selectionMatch: '#d7d4f0',
+ gutterBackground: '#2e3235',
+ gutterForeground: '#999',
+ gutterActiveForeground: '#4f5b66',
+ lineHighlight: '#545b61',
+ },
+ materialLight: {
+ light: true,
+ background: '#FAFAFA',
+ lineBackground: '#FAFAFA99',
+ foreground: '#90A4AE',
+ caret: '#272727',
+ selection: '#80CBC440',
+ selectionMatch: '#FAFAFA',
+ gutterBackground: '#FAFAFA',
+ gutterForeground: '#90A4AE',
+ gutterBorder: 'transparent',
+ lineHighlight: '#CCD7DA50',
+ },
+ noctisLilac: {
+ light: true,
+ background: '#f2f1f8',
+ lineBackground: '#f2f1f899',
+ foreground: '#0c006b',
+ caret: '#5c49e9',
+ selection: '#d5d1f2',
+ selectionMatch: '#d5d1f2',
+ gutterBackground: '#f2f1f8',
+ gutterForeground: '#0c006b70',
+ lineHighlight: '#e1def3',
+ },
+ nord: {
+ background: '#2e3440',
+ lineBackground: '#2e344099',
+ foreground: '#FFFFFF',
+ caret: '#FFFFFF',
+ selection: '#3b4252',
+ selectionMatch: '#e5e9f0',
+ gutterBackground: '#2e3440',
+ gutterForeground: '#4c566a',
+ gutterActiveForeground: '#d8dee9',
+ lineHighlight: '#4c566a',
+ },
+ okaidia: {
+ background: '#272822',
+ lineBackground: '#27282299',
+ foreground: '#FFFFFF',
+ caret: '#FFFFFF',
+ selection: '#49483E',
+ selectionMatch: '#49483E',
+ gutterBackground: '#272822',
+ gutterForeground: '#FFFFFF70',
+ lineHighlight: '#00000059',
+ },
+ solarizedLight: {
+ light: true,
+ background: '#fdf6e3',
+ lineBackground: '#fdf6e399',
+ foreground: '#657b83',
+ caret: '#586e75',
+ selection: '#dfd9c8',
+ selectionMatch: '#dfd9c8',
+ gutterBackground: '#00000010',
+ gutterForeground: '#657b83',
+ lineHighlight: '#dfd9c8',
+ },
+ solarizedDark: {
+ background: '#002b36',
+ lineBackground: '#002b3699',
+ foreground: '#93a1a1',
+ caret: '#839496',
+ selection: '#173541',
+ selectionMatch: '#aafe661a',
+ gutterBackground: '#00252f',
+ gutterForeground: '#839496',
+ lineHighlight: '#173541',
+ },
+ sublime: {
+ background: '#303841',
+ lineBackground: '#30384199',
+ foreground: '#FFFFFF',
+ caret: '#FBAC52',
+ selection: '#4C5964',
+ selectionMatch: '#3A546E',
+ gutterBackground: '#303841',
+ gutterForeground: '#FFFFFF70',
+ lineHighlight: '#00000059',
+ },
+ tokyoNightDay: {
+ light: true,
+ background: '#e1e2e7',
+ lineBackground: '#e1e2e799',
+ foreground: '#3760bf',
+ caret: '#3760bf',
+ selection: '#99a7df',
+ selectionMatch: '#99a7df',
+ gutterBackground: '#e1e2e7',
+ gutterForeground: '#3760bf',
+ gutterBorder: 'transparent',
+ lineHighlight: '#5f5faf11',
+ },
+ tokyoNightStorm: {
+ background: '#24283b',
+ lineBackground: '#24283b99',
+ foreground: '#7982a9',
+ caret: '#c0caf5',
+ selection: '#6f7bb630',
+ selectionMatch: '#1f2335',
+ gutterBackground: '#24283b',
+ gutterForeground: '#7982a9',
+ gutterBorder: 'transparent',
+ lineHighlight: '#292e42',
+ },
+ tokyoNight: {
+ background: '#1a1b26',
+ lineBackground: '#1a1b2699',
+ foreground: '#787c99',
+ caret: '#c0caf5',
+ selection: '#515c7e40',
+ selectionMatch: '#16161e',
+ gutterBackground: '#1a1b26',
+ gutterForeground: '#787c99',
+ gutterBorder: 'transparent',
+ lineHighlight: '#1e202e',
+ },
+ vscodeDark: {
+ background: '#1e1e1e',
+ lineBackground: '#1e1e1e99',
+ foreground: '#9cdcfe',
+ caret: '#c6c6c6',
+ selection: '#6199ff2f',
+ selectionMatch: '#72a1ff59',
+ lineHighlight: '#ffffff0f',
+ gutterBackground: '#1e1e1e',
+ gutterForeground: '#838383',
+ gutterActiveForeground: '#fff',
+ },
+ xcodeLight: {
+ light: true,
+ background: '#fff',
+ lineBackground: '#ffffff99',
+ foreground: '#3D3D3D',
+ selection: '#BBDFFF',
+ selectionMatch: '#BBDFFF',
+ gutterBackground: '#fff',
+ gutterForeground: '#AFAFAF',
+ lineHighlight: '#EDF4FF',
+ },
+ xcodeDark: {
+ background: '#292A30',
+ lineBackground: '#292A3099',
+ foreground: '#CECFD0',
+ caret: '#fff',
+ selection: '#727377',
+ selectionMatch: '#727377',
+ lineHighlight: '#2F3239',
+ },
+};
+
+function getColors(str) {
+ const colorRegex = /#([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})/g;
+ const colors = [];
+
+ let match;
+ while ((match = colorRegex.exec(str)) !== null) {
+ const color = match[0];
+ if (!colors.includes(color)) {
+ colors.push(color);
+ }
+ }
+
+ return colors;
+}
+
+// TODO: remove
+export function themeColors(theme) {
+ return getColors(stringifySafe(theme));
+}
+
+function getCircularReplacer() {
+ const seen = new WeakSet();
+ return (key, value) => {
+ if (typeof value === 'object' && value !== null) {
+ if (seen.has(value)) {
+ return;
+ }
+ seen.add(value);
+ }
+ return value;
+ };
+}
+
+function stringifySafe(json) {
+ return JSON.stringify(json, getCircularReplacer());
+}
+
+export function injectStyle(rule) {
+ const newStyle = document.createElement('style');
+ document.head.appendChild(newStyle);
+ const styleSheet = newStyle.sheet;
+ const ruleIndex = styleSheet.insertRule(rule, 0);
+ return () => styleSheet.deleteRule(ruleIndex);
+}
+
+export const theme = (theme) => themes[theme] || themes.strudelTheme;
diff --git a/packages/codemirror/themes/algoboy.mjs b/packages/codemirror/themes/algoboy.mjs
new file mode 100644
index 00000000..399370e1
--- /dev/null
+++ b/packages/codemirror/themes/algoboy.mjs
@@ -0,0 +1,41 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+export const settings = {
+ background: '#9bbc0f',
+ foreground: '#0f380f', // whats that?
+ caret: '#0f380f',
+ selection: '#306230',
+ selectionMatch: '#ffffff26',
+ lineHighlight: '#8bac0f',
+ lineBackground: '#9bbc0f50',
+ //lineBackground: 'transparent',
+ gutterBackground: 'transparent',
+ gutterForeground: '#0f380f',
+ light: true,
+ customStyle: '.cm-line { line-height: 1 }',
+};
+export default createTheme({
+ theme: 'light',
+ settings,
+ styles: [
+ { tag: t.keyword, color: '#0f380f' },
+ { tag: t.operator, color: '#0f380f' },
+ { tag: t.special(t.variableName), color: '#0f380f' },
+ { tag: t.typeName, color: '#0f380f' },
+ { tag: t.atom, color: '#0f380f' },
+ { tag: t.number, color: '#0f380f' },
+ { tag: t.definition(t.variableName), color: '#0f380f' },
+ { tag: t.string, color: '#0f380f' },
+ { tag: t.special(t.string), color: '#0f380f' },
+ { tag: t.comment, color: '#0f380f' },
+ { tag: t.variableName, color: '#0f380f' },
+ { tag: t.tagName, color: '#0f380f' },
+ { tag: t.bracket, color: '#0f380f' },
+ { tag: t.meta, color: '#0f380f' },
+ { tag: t.attributeName, color: '#0f380f' },
+ { tag: t.propertyName, color: '#0f380f' },
+ { tag: t.className, color: '#0f380f' },
+ { tag: t.invalid, color: '#0f380f' },
+ { tag: [t.unit, t.punctuation], color: '#0f380f' },
+ ],
+});
diff --git a/packages/codemirror/themes/blackscreen.mjs b/packages/codemirror/themes/blackscreen.mjs
new file mode 100644
index 00000000..135285a3
--- /dev/null
+++ b/packages/codemirror/themes/blackscreen.mjs
@@ -0,0 +1,38 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+export const settings = {
+ background: 'black',
+ foreground: 'white', // whats that?
+ caret: 'white',
+ selection: '#ffffff20',
+ selectionMatch: '#036dd626',
+ lineHighlight: '#ffffff10',
+ lineBackground: '#00000050',
+ gutterBackground: 'transparent',
+ gutterForeground: '#8a919966',
+};
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ { tag: t.keyword, color: 'white' },
+ { tag: t.operator, color: 'white' },
+ { tag: t.special(t.variableName), color: 'white' },
+ { tag: t.typeName, color: 'white' },
+ { tag: t.atom, color: 'white' },
+ { tag: t.number, color: 'white' },
+ { tag: t.definition(t.variableName), color: 'white' },
+ { tag: t.string, color: 'white' },
+ { tag: t.special(t.string), color: 'white' },
+ { tag: t.comment, color: 'white' },
+ { tag: t.variableName, color: 'white' },
+ { tag: t.tagName, color: 'white' },
+ { tag: t.bracket, color: 'white' },
+ { tag: t.meta, color: 'white' },
+ { tag: t.attributeName, color: 'white' },
+ { tag: t.propertyName, color: 'white' },
+ { tag: t.className, color: 'white' },
+ { tag: t.invalid, color: 'white' },
+ { tag: [t.unit, t.punctuation], color: 'white' },
+ ],
+});
diff --git a/packages/codemirror/themes/bluescreen.mjs b/packages/codemirror/themes/bluescreen.mjs
new file mode 100644
index 00000000..aa6489d6
--- /dev/null
+++ b/packages/codemirror/themes/bluescreen.mjs
@@ -0,0 +1,41 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+export const settings = {
+ background: '#051DB5',
+ lineBackground: '#051DB550',
+ foreground: 'white', // whats that?
+ caret: 'white',
+ selection: 'rgba(128, 203, 196, 0.5)',
+ selectionMatch: '#036dd626',
+ // lineHighlight: '#8a91991a', // original
+ lineHighlight: '#00000050',
+ gutterBackground: 'transparent',
+ // gutterForeground: '#8a919966',
+ gutterForeground: '#8a919966',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ { tag: t.keyword, color: 'white' },
+ { tag: t.operator, color: 'white' },
+ { tag: t.special(t.variableName), color: 'white' },
+ { tag: t.typeName, color: 'white' },
+ { tag: t.atom, color: 'white' },
+ { tag: t.number, color: 'white' },
+ { tag: t.definition(t.variableName), color: 'white' },
+ { tag: t.string, color: 'white' },
+ { tag: t.special(t.string), color: 'white' },
+ { tag: t.comment, color: 'white' },
+ { tag: t.variableName, color: 'white' },
+ { tag: t.tagName, color: 'white' },
+ { tag: t.bracket, color: 'white' },
+ { tag: t.meta, color: 'white' },
+ { tag: t.attributeName, color: 'white' },
+ { tag: t.propertyName, color: 'white' },
+ { tag: t.className, color: 'white' },
+ { tag: t.invalid, color: 'white' },
+ { tag: [t.unit, t.punctuation], color: 'white' },
+ ],
+});
diff --git a/packages/codemirror/themes/one-dark.mjs b/packages/codemirror/themes/one-dark.mjs
deleted file mode 100644
index cce83699..00000000
--- a/packages/codemirror/themes/one-dark.mjs
+++ /dev/null
@@ -1,139 +0,0 @@
-import { EditorView } from '@codemirror/view';
-import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
-import { tags as t } from '@lezer/highlight';
-
-// Using https://github.com/one-dark/vscode-one-dark-theme/ as reference for the colors
-
-const chalky = '#e5c07b',
- coral = '#e06c75',
- cyan = '#56b6c2',
- invalid = '#ffffff',
- ivory = '#abb2bf',
- stone = '#7d8799', // Brightened compared to original to increase contrast
- malibu = '#61afef',
- sage = '#98c379',
- whiskey = '#d19a66',
- violet = '#c678dd',
- darkBackground = '#21252b',
- highlightBackground = '#2c313a',
- background = '#282c34',
- tooltipBackground = '#353a42',
- selection = '#3E4451',
- cursor = '#528bff';
-
-/// The colors used in the theme, as CSS color strings.
-export const color = {
- chalky,
- coral,
- cyan,
- invalid,
- ivory,
- stone,
- malibu,
- sage,
- whiskey,
- violet,
- darkBackground,
- highlightBackground,
- background,
- tooltipBackground,
- selection,
- cursor,
-};
-
-/// The editor theme styles for One Dark.
-export const oneDarkTheme = EditorView.theme(
- {
- '&': {
- color: ivory,
- backgroundColor: background,
- },
-
- '.cm-content': {
- caretColor: cursor,
- },
-
- '.cm-cursor, .cm-dropCursor': { borderLeftColor: cursor },
- '&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection':
- { backgroundColor: selection },
-
- '.cm-panels': { backgroundColor: darkBackground, color: ivory },
- '.cm-panels.cm-panels-top': { borderBottom: '2px solid black' },
- '.cm-panels.cm-panels-bottom': { borderTop: '2px solid black' },
-
- '.cm-searchMatch': {
- backgroundColor: '#72a1ff59',
- outline: '1px solid #457dff',
- },
- '.cm-searchMatch.cm-searchMatch-selected': {
- backgroundColor: '#6199ff2f',
- },
-
- '.cm-activeLine': { backgroundColor: '#6699ff0b' },
- '.cm-selectionMatch': { backgroundColor: '#aafe661a' },
-
- '&.cm-focused .cm-matchingBracket, &.cm-focused .cm-nonmatchingBracket': {
- backgroundColor: '#bad0f847',
- },
-
- '.cm-gutters': {
- backgroundColor: background,
- color: stone,
- border: 'none',
- },
-
- '.cm-activeLineGutter': {
- backgroundColor: highlightBackground,
- },
-
- '.cm-foldPlaceholder': {
- backgroundColor: 'transparent',
- border: 'none',
- color: '#ddd',
- },
-
- '.cm-tooltip': {
- border: 'none',
- backgroundColor: tooltipBackground,
- },
- '.cm-tooltip .cm-tooltip-arrow:before': {
- borderTopColor: 'transparent',
- borderBottomColor: 'transparent',
- },
- '.cm-tooltip .cm-tooltip-arrow:after': {
- borderTopColor: tooltipBackground,
- borderBottomColor: tooltipBackground,
- },
- '.cm-tooltip-autocomplete': {
- '& > ul > li[aria-selected]': {
- backgroundColor: highlightBackground,
- color: ivory,
- },
- },
- },
- { dark: true },
-);
-
-/// The highlighting style for code in the One Dark theme.
-export const oneDarkHighlightStyle = HighlightStyle.define([
- { tag: t.keyword, color: violet },
- { tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: coral },
- { tag: [t.function(t.variableName), t.labelName], color: malibu },
- { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: whiskey },
- { tag: [t.definition(t.name), t.separator], color: ivory },
- { tag: [t.typeName, t.className, t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: chalky },
- { tag: [t.operator, t.operatorKeyword, t.url, t.escape, t.regexp, t.link, t.special(t.string)], color: cyan },
- { tag: [t.meta, t.comment], color: stone },
- { tag: t.strong, fontWeight: 'bold' },
- { tag: t.emphasis, fontStyle: 'italic' },
- { tag: t.strikethrough, textDecoration: 'line-through' },
- { tag: t.link, color: stone, textDecoration: 'underline' },
- { tag: t.heading, fontWeight: 'bold', color: coral },
- { tag: [t.atom, t.bool, t.special(t.variableName)], color: whiskey },
- { tag: [t.processingInstruction, t.string, t.inserted], color: sage },
- { tag: t.invalid, color: invalid },
-]);
-
-/// Extension to enable the One Dark theme (both the editor theme and
-/// the highlight style).
-export const oneDark = [oneDarkTheme, syntaxHighlighting(oneDarkHighlightStyle)];
diff --git a/packages/codemirror/themes/strudel-theme.mjs b/packages/codemirror/themes/strudel-theme.mjs
new file mode 100644
index 00000000..4ae31060
--- /dev/null
+++ b/packages/codemirror/themes/strudel-theme.mjs
@@ -0,0 +1,45 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#222',
+ foreground: '#75baff', // whats that?
+ caret: '#ffcc00',
+ selection: 'rgba(128, 203, 196, 0.5)',
+ selectionMatch: '#036dd626',
+ // lineHighlight: '#8a91991a', // original
+ lineHighlight: '#00000050',
+ gutterBackground: 'transparent',
+ // gutterForeground: '#8a919966',
+ gutterForeground: '#8a919966',
+ },
+ styles: [
+ { tag: t.keyword, color: '#c792ea' },
+ { tag: t.operator, color: '#89ddff' },
+ { tag: t.special(t.variableName), color: '#eeffff' },
+ // { tag: t.typeName, color: '#f07178' }, // original
+ { tag: t.typeName, color: '#c3e88d' },
+ { tag: t.atom, color: '#f78c6c' },
+ // { tag: t.number, color: '#ff5370' }, // original
+ { tag: t.number, color: '#c3e88d' },
+ { tag: t.definition(t.variableName), color: '#82aaff' },
+ { tag: t.string, color: '#c3e88d' },
+ // { tag: t.special(t.string), color: '#f07178' }, // original
+ { tag: t.special(t.string), color: '#c3e88d' },
+ { tag: t.comment, color: '#7d8799' },
+ // { tag: t.variableName, color: '#f07178' }, // original
+ { tag: t.variableName, color: '#c792ea' },
+ // { tag: t.tagName, color: '#ff5370' }, // original
+ { tag: t.tagName, color: '#c3e88d' },
+ { tag: t.bracket, color: '#525154' },
+ // { tag: t.bracket, color: '#a2a1a4' }, // original
+ { tag: t.meta, color: '#ffcb6b' },
+ { tag: t.attributeName, color: '#c792ea' },
+ { tag: t.propertyName, color: '#c792ea' },
+
+ { tag: t.className, color: '#decb6b' },
+ { tag: t.invalid, color: '#ffffff' },
+ { tag: [t.unit, t.punctuation], color: '#82aaff' },
+ ],
+});
diff --git a/packages/codemirror/themes/teletext.mjs b/packages/codemirror/themes/teletext.mjs
new file mode 100644
index 00000000..5fd9a557
--- /dev/null
+++ b/packages/codemirror/themes/teletext.mjs
@@ -0,0 +1,50 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+
+let colorA = '#6edee4';
+//let colorB = 'magenta';
+let colorB = 'white';
+let colorC = 'red';
+let colorD = '#f8fc55';
+
+export const settings = {
+ background: '#000000',
+ foreground: colorA, // whats that?
+ caret: colorC,
+ selection: colorD,
+ selectionMatch: colorA,
+ lineHighlight: '#6edee440', // panel bg
+ lineBackground: '#00000040',
+ gutterBackground: 'transparent',
+ gutterForeground: '#8a919966',
+ customStyle: '.cm-line { line-height: 1 }',
+};
+
+let punctuation = colorD;
+let mini = colorB;
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ { tag: t.keyword, color: colorA },
+ { tag: t.operator, color: mini },
+ { tag: t.special(t.variableName), color: colorA },
+ { tag: t.typeName, color: colorA },
+ { tag: t.atom, color: colorA },
+ { tag: t.number, color: mini },
+ { tag: t.definition(t.variableName), color: colorA },
+ { tag: t.string, color: mini },
+ { tag: t.special(t.string), color: mini },
+ { tag: t.comment, color: punctuation },
+ { tag: t.variableName, color: colorA },
+ { tag: t.tagName, color: colorA },
+ { tag: t.bracket, color: punctuation },
+ { tag: t.meta, color: colorA },
+ { tag: t.attributeName, color: colorA },
+ { tag: t.propertyName, color: colorA }, // methods
+ { tag: t.className, color: colorA },
+ { tag: t.invalid, color: colorC },
+ { tag: [t.unit, t.punctuation], color: punctuation },
+ ],
+});
diff --git a/packages/codemirror/themes/terminal.mjs b/packages/codemirror/themes/terminal.mjs
new file mode 100644
index 00000000..1374bb86
--- /dev/null
+++ b/packages/codemirror/themes/terminal.mjs
@@ -0,0 +1,36 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+export const settings = {
+ background: 'black',
+ foreground: '#41FF00', // whats that?
+ caret: '#41FF00',
+ selection: '#ffffff20',
+ selectionMatch: '#036dd626',
+ lineHighlight: '#ffffff10',
+ gutterBackground: 'transparent',
+ gutterForeground: '#8a919966',
+};
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ { tag: t.keyword, color: '#41FF00' },
+ { tag: t.operator, color: '#41FF00' },
+ { tag: t.special(t.variableName), color: '#41FF00' },
+ { tag: t.typeName, color: '#41FF00' },
+ { tag: t.atom, color: '#41FF00' },
+ { tag: t.number, color: '#41FF00' },
+ { tag: t.definition(t.variableName), color: '#41FF00' },
+ { tag: t.string, color: '#41FF00' },
+ { tag: t.special(t.string), color: '#41FF00' },
+ { tag: t.comment, color: '#41FF00' },
+ { tag: t.variableName, color: '#41FF00' },
+ { tag: t.tagName, color: '#41FF00' },
+ { tag: t.bracket, color: '#41FF00' },
+ { tag: t.meta, color: '#41FF00' },
+ { tag: t.attributeName, color: '#41FF00' },
+ { tag: t.propertyName, color: '#41FF00' },
+ { tag: t.className, color: '#41FF00' },
+ { tag: t.invalid, color: '#41FF00' },
+ ],
+});
diff --git a/packages/codemirror/themes/whitescreen.mjs b/packages/codemirror/themes/whitescreen.mjs
new file mode 100644
index 00000000..22abad9e
--- /dev/null
+++ b/packages/codemirror/themes/whitescreen.mjs
@@ -0,0 +1,38 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from '@uiw/codemirror-themes';
+export const settings = {
+ background: 'white',
+ foreground: 'black', // whats that?
+ caret: 'black',
+ selection: 'rgba(128, 203, 196, 0.5)',
+ selectionMatch: '#ffffff26',
+ lineHighlight: '#cccccc50',
+ lineBackground: '#ffffff50',
+ gutterBackground: 'transparent',
+ gutterForeground: 'black',
+ light: true,
+};
+export default createTheme({
+ theme: 'light',
+ settings,
+ styles: [
+ { tag: t.keyword, color: 'black' },
+ { tag: t.operator, color: 'black' },
+ { tag: t.special(t.variableName), color: 'black' },
+ { tag: t.typeName, color: 'black' },
+ { tag: t.atom, color: 'black' },
+ { tag: t.number, color: 'black' },
+ { tag: t.definition(t.variableName), color: 'black' },
+ { tag: t.string, color: 'black' },
+ { tag: t.special(t.string), color: 'black' },
+ { tag: t.comment, color: 'black' },
+ { tag: t.variableName, color: 'black' },
+ { tag: t.tagName, color: 'black' },
+ { tag: t.bracket, color: 'black' },
+ { tag: t.meta, color: 'black' },
+ { tag: t.attributeName, color: 'black' },
+ { tag: t.propertyName, color: 'black' },
+ { tag: t.className, color: 'black' },
+ { tag: t.invalid, color: 'black' },
+ ],
+});
diff --git a/packages/core/util.mjs b/packages/core/util.mjs
index 695eaacf..2b190c82 100644
--- a/packages/core/util.mjs
+++ b/packages/core/util.mjs
@@ -274,3 +274,31 @@ export const sol2note = (n, notation = 'letters') => {
const oct = Math.floor(n / 12) - 1;
return note + oct;
};
+
+// code hashing helpers
+
+export function unicodeToBase64(text) {
+ const utf8Bytes = new TextEncoder().encode(text);
+ const base64String = btoa(String.fromCharCode(...utf8Bytes));
+ return base64String;
+}
+
+export function base64ToUnicode(base64String) {
+ const utf8Bytes = new Uint8Array(
+ atob(base64String)
+ .split('')
+ .map((char) => char.charCodeAt(0)),
+ );
+ const decodedText = new TextDecoder().decode(utf8Bytes);
+ return decodedText;
+}
+
+export function code2hash(code) {
+ return encodeURIComponent(unicodeToBase64(code));
+ //return '#' + encodeURIComponent(btoa(code));
+}
+
+export function hash2code(hash) {
+ return base64ToUnicode(decodeURIComponent(hash));
+ //return atob(decodeURIComponent(codeParam || ''));
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 98f6441f..a2416a62 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1,5 +1,9 @@
lockfileVersion: '6.0'
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
importers:
.:
@@ -71,6 +75,9 @@ importers:
packages/codemirror:
dependencies:
+ '@codemirror/autocomplete':
+ specifier: ^6.6.0
+ version: 6.6.0(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)(@lezer/common@1.0.2)
'@codemirror/commands':
specifier: ^6.2.4
version: 6.2.4
@@ -80,6 +87,9 @@ importers:
'@codemirror/language':
specifier: ^6.6.0
version: 6.6.0
+ '@codemirror/search':
+ specifier: ^6.0.0
+ version: 6.2.3
'@codemirror/state':
specifier: ^6.2.0
version: 6.2.0
@@ -89,14 +99,78 @@ importers:
'@lezer/highlight':
specifier: ^1.1.4
version: 1.1.4
+ '@replit/codemirror-emacs':
+ specifier: ^6.0.1
+ version: 6.0.1(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
+ '@replit/codemirror-vim':
+ specifier: ^6.0.14
+ version: 6.0.14(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
+ '@replit/codemirror-vscode-keymap':
+ specifier: ^6.0.2
+ version: 6.0.2(@codemirror/autocomplete@6.6.0)(@codemirror/commands@6.2.4)(@codemirror/language@6.6.0)(@codemirror/lint@6.1.0)(@codemirror/search@6.2.3)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
'@strudel.cycles/core':
specifier: workspace:*
version: link:../core
+ '@uiw/codemirror-themes':
+ specifier: ^4.19.16
+ version: 4.19.16(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
+ '@uiw/codemirror-themes-all':
+ specifier: ^4.19.16
+ version: 4.19.16(@codemirror/language@6.6.0)(@codemirror/state@6.2.0)(@codemirror/view@6.10.0)
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.2.0(react@18.2.0)
devDependencies:
vite:
specifier: ^4.3.3
version: 4.3.3
+ packages/codemirror/examples/strudelmirror:
+ dependencies:
+ '@strudel.cycles/core':
+ specifier: workspace:*
+ version: link:../../../core
+ '@strudel.cycles/csound':
+ specifier: workspace:*
+ version: link:../../../csound
+ '@strudel.cycles/midi':
+ specifier: workspace:*
+ version: link:../../../midi
+ '@strudel.cycles/mini':
+ specifier: workspace:*
+ version: link:../../../mini
+ '@strudel.cycles/osc':
+ specifier: workspace:*
+ version: link:../../../osc
+ '@strudel.cycles/serial':
+ specifier: workspace:*
+ version: link:../../../serial
+ '@strudel.cycles/soundfonts':
+ specifier: workspace:*
+ version: link:../../../soundfonts
+ '@strudel.cycles/tonal':
+ specifier: workspace:*
+ version: link:../../../tonal
+ '@strudel.cycles/transpiler':
+ specifier: workspace:*
+ version: link:../../../transpiler
+ '@strudel.cycles/webaudio':
+ specifier: workspace:*
+ version: link:../../../webaudio
+ '@strudel.cycles/xen':
+ specifier: workspace:*
+ version: link:../../../xen
+ '@strudel/codemirror':
+ specifier: workspace:*
+ version: link:../..
+ '@strudel/hydra':
+ specifier: workspace:*
+ version: link:../../../hydra
+ devDependencies:
+ vite:
+ specifier: ^5.0.8
+ version: 5.0.8
+
packages/core:
dependencies:
fraction.js:
@@ -4053,6 +4127,110 @@ packages:
rollup: 2.79.1
dev: true
+ /@rollup/rollup-android-arm-eabi@4.9.0:
+ resolution: {integrity: sha512-+1ge/xmaJpm1KVBuIH38Z94zj9fBD+hp+/5WLaHgyY8XLq1ibxk/zj6dTXaqM2cAbYKq8jYlhHd6k05If1W5xA==}
+ cpu: [arm]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-android-arm64@4.9.0:
+ resolution: {integrity: sha512-im6hUEyQ7ZfoZdNvtwgEJvBWZYauC9KVKq1w58LG2Zfz6zMd8gRrbN+xCVoqA2hv/v6fm9lp5LFGJ3za8EQH3A==}
+ cpu: [arm64]
+ os: [android]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-darwin-arm64@4.9.0:
+ resolution: {integrity: sha512-u7aTMskN6Dmg1lCT0QJ+tINRt+ntUrvVkhbPfFz4bCwRZvjItx2nJtwJnJRlKMMaQCHRjrNqHRDYvE4mBm3DlQ==}
+ cpu: [arm64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-darwin-x64@4.9.0:
+ resolution: {integrity: sha512-8FvEl3w2ExmpcOmX5RJD0yqXcVSOqAJJUJ29Lca29Ik+3zPS1yFimr2fr5JSZ4Z5gt8/d7WqycpgkX9nocijSw==}
+ cpu: [x64]
+ os: [darwin]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-arm-gnueabihf@4.9.0:
+ resolution: {integrity: sha512-lHoKYaRwd4gge+IpqJHCY+8Vc3hhdJfU6ukFnnrJasEBUvVlydP8PuwndbWfGkdgSvZhHfSEw6urrlBj0TSSfg==}
+ cpu: [arm]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-arm64-gnu@4.9.0:
+ resolution: {integrity: sha512-JbEPfhndYeWHfOSeh4DOFvNXrj7ls9S/2omijVsao+LBPTPayT1uKcK3dHW3MwDJ7KO11t9m2cVTqXnTKpeaiw==}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-arm64-musl@4.9.0:
+ resolution: {integrity: sha512-ahqcSXLlcV2XUBM3/f/C6cRoh7NxYA/W7Yzuv4bDU1YscTFw7ay4LmD7l6OS8EMhTNvcrWGkEettL1Bhjf+B+w==}
+ cpu: [arm64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-riscv64-gnu@4.9.0:
+ resolution: {integrity: sha512-uwvOYNtLw8gVtrExKhdFsYHA/kotURUmZYlinH2VcQxNCQJeJXnkmWgw2hI9Xgzhgu7J9QvWiq9TtTVwWMDa+w==}
+ cpu: [riscv64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-x64-gnu@4.9.0:
+ resolution: {integrity: sha512-m6pkSwcZZD2LCFHZX/zW2aLIISyzWLU3hrLLzQKMI12+OLEzgruTovAxY5sCZJkipklaZqPy/2bEEBNjp+Y7xg==}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-linux-x64-musl@4.9.0:
+ resolution: {integrity: sha512-VFAC1RDRSbU3iOF98X42KaVicAfKf0m0OvIu8dbnqhTe26Kh6Ym9JrDulz7Hbk7/9zGc41JkV02g+p3BivOdAg==}
+ cpu: [x64]
+ os: [linux]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-win32-arm64-msvc@4.9.0:
+ resolution: {integrity: sha512-9jPgMvTKXARz4inw6jezMLA2ihDBvgIU9Ml01hjdVpOcMKyxFBJrn83KVQINnbeqDv0+HdO1c09hgZ8N0s820Q==}
+ cpu: [arm64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-win32-ia32-msvc@4.9.0:
+ resolution: {integrity: sha512-WE4pT2kTXQN2bAv40Uog0AsV7/s9nT9HBWXAou8+++MBCnY51QS02KYtm6dQxxosKi1VIz/wZIrTQO5UP2EW+Q==}
+ cpu: [ia32]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
+ /@rollup/rollup-win32-x64-msvc@4.9.0:
+ resolution: {integrity: sha512-aPP5Q5AqNGuT0tnuEkK/g4mnt3ZhheiXrDIiSVIHN9mcN21OyXDVbEMqmXPE7e2OplNLDkcvV+ZoGJa2ZImFgw==}
+ cpu: [x64]
+ os: [win32]
+ requiresBuild: true
+ dev: true
+ optional: true
+
/@sigstore/protobuf-specs@0.1.0:
resolution: {integrity: sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -5572,6 +5750,7 @@ packages:
/b4a@1.6.4:
resolution: {integrity: sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==}
+ requiresBuild: true
optional: true
/babel-plugin-add-module-exports@0.2.1:
@@ -5968,7 +6147,7 @@ packages:
normalize-path: 3.0.0
readdirp: 3.6.0
optionalDependencies:
- fsevents: 2.3.2
+ fsevents: 2.3.3
/chord-voicings@0.0.1:
resolution: {integrity: sha512-SutgB/4ynkkuiK6qdQ/k3QvCFcH0Vj8Ch4t6LbRyRQbVzP/TOztiCk3kvXd516UZ6fqk7ijDRELEFcKN+6V8sA==}
@@ -6154,6 +6333,7 @@ packages:
/color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
+ requiresBuild: true
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
@@ -6166,6 +6346,7 @@ packages:
/color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
+ requiresBuild: true
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
@@ -6629,6 +6810,7 @@ packages:
/detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
+ requiresBuild: true
optional: true
/detective-amd@4.0.1:
@@ -7472,6 +7654,7 @@ packages:
/fast-fifo@1.3.2:
resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==}
+ requiresBuild: true
optional: true
/fast-glob@3.2.12:
@@ -7736,8 +7919,8 @@ packages:
/fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
- /fsevents@2.3.2:
- resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+ /fsevents@2.3.3:
+ resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
requiresBuild: true
@@ -10521,6 +10704,12 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
+ /nanoid@3.3.7:
+ resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+ dev: true
+
/nanoid@4.0.2:
resolution: {integrity: sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==}
engines: {node: ^14 || ^16 || >=18}
@@ -10579,6 +10768,7 @@ packages:
/node-addon-api@6.1.0:
resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==}
+ requiresBuild: true
optional: true
/node-domexception@1.0.0:
@@ -11675,6 +11865,15 @@ packages:
picocolors: 1.0.0
source-map-js: 1.0.2
+ /postcss@8.4.32:
+ resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==}
+ engines: {node: ^10 || ^12 || >=14}
+ dependencies:
+ nanoid: 3.3.7
+ picocolors: 1.0.0
+ source-map-js: 1.0.2
+ dev: true
+
/prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'}
@@ -11875,6 +12074,7 @@ packages:
/queue-tick@1.0.1:
resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==}
+ requiresBuild: true
optional: true
/quick-lru@4.0.1:
@@ -12504,7 +12704,7 @@ packages:
engines: {node: '>=10.0.0'}
hasBin: true
optionalDependencies:
- fsevents: 2.3.2
+ fsevents: 2.3.3
dev: true
/rollup@3.21.0:
@@ -12512,7 +12712,7 @@ packages:
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
- fsevents: 2.3.2
+ fsevents: 2.3.3
dev: true
/rollup@3.28.0:
@@ -12520,7 +12720,28 @@ packages:
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
optionalDependencies:
- fsevents: 2.3.2
+ fsevents: 2.3.3
+
+ /rollup@4.9.0:
+ resolution: {integrity: sha512-bUHW/9N21z64gw8s6tP4c88P382Bq/L5uZDowHlHx6s/QWpjJXivIAbEw6LZthgSvlEizZBfLC4OAvWe7aoF7A==}
+ engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+ hasBin: true
+ optionalDependencies:
+ '@rollup/rollup-android-arm-eabi': 4.9.0
+ '@rollup/rollup-android-arm64': 4.9.0
+ '@rollup/rollup-darwin-arm64': 4.9.0
+ '@rollup/rollup-darwin-x64': 4.9.0
+ '@rollup/rollup-linux-arm-gnueabihf': 4.9.0
+ '@rollup/rollup-linux-arm64-gnu': 4.9.0
+ '@rollup/rollup-linux-arm64-musl': 4.9.0
+ '@rollup/rollup-linux-riscv64-gnu': 4.9.0
+ '@rollup/rollup-linux-x64-gnu': 4.9.0
+ '@rollup/rollup-linux-x64-musl': 4.9.0
+ '@rollup/rollup-win32-arm64-msvc': 4.9.0
+ '@rollup/rollup-win32-ia32-msvc': 4.9.0
+ '@rollup/rollup-win32-x64-msvc': 4.9.0
+ fsevents: 2.3.3
+ dev: true
/run-async@2.4.1:
resolution: {integrity: sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==}
@@ -12730,6 +12951,7 @@ packages:
/simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
+ requiresBuild: true
dependencies:
is-arrayish: 0.3.2
optional: true
@@ -12943,6 +13165,7 @@ packages:
/streamx@2.15.2:
resolution: {integrity: sha512-b62pAV/aeMjUoRN2C/9F0n+G8AfcJjNC0zw/ZmOHeFsIe4m4GzjVW9m6VHXVjk536NbdU9JRwKMJRfkc+zUFTg==}
+ requiresBuild: true
dependencies:
fast-fifo: 1.3.2
queue-tick: 1.0.1
@@ -13220,6 +13443,7 @@ packages:
/tar-fs@3.0.4:
resolution: {integrity: sha512-5AFQU8b9qLfZCX9zp2duONhPmZv0hGYiBPJsyUdqMjzq/mqVpy/rEUSeHk1+YitmxugaptgBh5oDGU3VsAJq4w==}
+ requiresBuild: true
dependencies:
mkdirp-classic: 0.5.3
pump: 3.0.0
@@ -13238,6 +13462,7 @@ packages:
/tar-stream@3.1.6:
resolution: {integrity: sha512-B/UyjYwPpMBv+PaFSWAmtYjwdrlEaZQEhMIBFNC5oEG8lpiW8XjcSdmEaClj28ArfKScKHs2nshz3k2le6crsg==}
+ requiresBuild: true
dependencies:
b4a: 1.6.4
fast-fifo: 1.3.2
@@ -14008,7 +14233,7 @@ packages:
mlly: 1.4.0
pathe: 1.1.1
picocolors: 1.0.0
- vite: 4.4.5(@types/node@18.16.3)
+ vite: 4.5.0(@types/node@18.16.3)
transitivePeerDependencies:
- '@types/node'
- less
@@ -14067,7 +14292,7 @@ packages:
postcss: 8.4.23
rollup: 3.21.0
optionalDependencies:
- fsevents: 2.3.2
+ fsevents: 2.3.3
dev: true
/vite@4.4.5(@types/node@18.16.3):
@@ -14103,7 +14328,7 @@ packages:
postcss: 8.4.27
rollup: 3.28.0
optionalDependencies:
- fsevents: 2.3.2
+ fsevents: 2.3.3
dev: true
/vite@4.5.0(@types/node@18.16.3):
@@ -14139,7 +14364,42 @@ packages:
postcss: 8.4.31
rollup: 3.28.0
optionalDependencies:
- fsevents: 2.3.2
+ fsevents: 2.3.3
+
+ /vite@5.0.8:
+ resolution: {integrity: sha512-jYMALd8aeqR3yS9xlHd0OzQJndS9fH5ylVgWdB+pxTwxLKdO1pgC5Dlb398BUxpfaBxa4M9oT7j1g503Gaj5IQ==}
+ engines: {node: ^18.0.0 || >=20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@types/node': ^18.0.0 || >=20.0.0
+ less: '*'
+ lightningcss: ^1.21.0
+ sass: '*'
+ stylus: '*'
+ sugarss: '*'
+ terser: ^5.4.0
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ dependencies:
+ esbuild: 0.19.5
+ postcss: 8.4.32
+ rollup: 4.9.0
+ optionalDependencies:
+ fsevents: 2.3.3
+ dev: true
/vitefu@0.2.4(vite@4.5.0):
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index 615475e4..baafa3c3 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -7,3 +7,4 @@ packages:
- "packages/react/examples/nano-repl"
- "packages/web/examples/repl-example"
- "packages/superdough/example"
+ - "packages/codemirror/examples/strudelmirror"
diff --git a/website/src/pages/vanilla/index.astro b/website/src/pages/vanilla/index.astro
new file mode 100644
index 00000000..5377c08f
--- /dev/null
+++ b/website/src/pages/vanilla/index.astro
@@ -0,0 +1,98 @@
+---
+import HeadCommon from '../../components/HeadCommon.astro';
+---
+
+
+
+
+ Strudel Vanilla REPL
+
+
+
+
+
+
+
+
+
diff --git a/website/src/repl/Repl.jsx b/website/src/repl/Repl.jsx
index a606d67a..d4f85434 100644
--- a/website/src/repl/Repl.jsx
+++ b/website/src/repl/Repl.jsx
@@ -4,7 +4,16 @@ Copyright (C) 2022 Strudel contributors - see .
*/
-import { cleanupDraw, cleanupUi, controls, evalScope, getDrawContext, logger } from '@strudel.cycles/core';
+import {
+ cleanupDraw,
+ cleanupUi,
+ controls,
+ evalScope,
+ getDrawContext,
+ logger,
+ code2hash,
+ hash2code,
+} from '@strudel.cycles/core';
import { CodeMirror, cx, flash, useHighlighting, useStrudel, useKeydown } from '@strudel.cycles/react';
import { getAudioContext, initAudioOnFirstClick, resetLoadedSounds, webaudioOutput } from '@strudel.cycles/webaudio';
import { createClient } from '@supabase/supabase-js';
@@ -29,7 +38,6 @@ import {
} from '../settings.mjs';
import Loader from './Loader';
import { settingPatterns } from '../settings.mjs';
-import { code2hash, hash2code } from './helpers.mjs';
import { isTauri } from '../tauri.mjs';
import { useWidgets } from '@strudel.cycles/react/src/hooks/useWidgets.mjs';
import { writeText } from '@tauri-apps/api/clipboard';
diff --git a/website/src/repl/helpers.mjs b/website/src/repl/helpers.mjs
deleted file mode 100644
index b86e76f1..00000000
--- a/website/src/repl/helpers.mjs
+++ /dev/null
@@ -1,25 +0,0 @@
-export function unicodeToBase64(text) {
- const utf8Bytes = new TextEncoder().encode(text);
- const base64String = btoa(String.fromCharCode(...utf8Bytes));
- return base64String;
-}
-
-export function base64ToUnicode(base64String) {
- const utf8Bytes = new Uint8Array(
- atob(base64String)
- .split('')
- .map((char) => char.charCodeAt(0)),
- );
- const decodedText = new TextDecoder().decode(utf8Bytes);
- return decodedText;
-}
-
-export function code2hash(code) {
- return encodeURIComponent(unicodeToBase64(code));
- //return '#' + encodeURIComponent(btoa(code));
-}
-
-export function hash2code(hash) {
- return base64ToUnicode(decodeURIComponent(hash));
- //return atob(decodeURIComponent(codeParam || ''));
-}
diff --git a/website/src/repl/vanilla/vanilla.css b/website/src/repl/vanilla/vanilla.css
new file mode 100644
index 00000000..7c985be7
--- /dev/null
+++ b/website/src/repl/vanilla/vanilla.css
@@ -0,0 +1,38 @@
+:root {
+ --foreground: white;
+}
+
+body,
+input {
+ font-family: monospace;
+ background-color: black;
+ color: white;
+}
+
+input,
+select {
+ background-color: black !important;
+}
+
+html,
+body,
+#code,
+.cm-editor,
+.cm-scroller {
+ padding: 0;
+ margin: 0;
+ height: 100%;
+}
+
+.settings {
+ position: fixed;
+ right: 0;
+ top: 0;
+ z-index: 1000;
+ display: flex-col;
+ padding: 10px;
+}
+
+.settings > form > * + * {
+ margin-top: 10px;
+}
diff --git a/website/src/repl/vanilla/vanilla.mjs b/website/src/repl/vanilla/vanilla.mjs
new file mode 100644
index 00000000..6e5c54a3
--- /dev/null
+++ b/website/src/repl/vanilla/vanilla.mjs
@@ -0,0 +1,199 @@
+import { logger, getDrawContext, silence, controls, evalScope, hash2code, code2hash } from '@strudel.cycles/core';
+import { StrudelMirror } from '@strudel/codemirror';
+import { transpiler } from '@strudel.cycles/transpiler';
+import {
+ getAudioContext,
+ webaudioOutput,
+ registerSynthSounds,
+ registerZZFXSounds,
+ samples,
+} from '@strudel.cycles/webaudio';
+import './vanilla.css';
+
+let editor;
+const initialSettings = {
+ keybindings: 'codemirror',
+ isLineNumbersDisplayed: true,
+ isActiveLineHighlighted: true,
+ isAutoCompletionEnabled: false,
+ isPatternHighlightingEnabled: true,
+ isFlashEnabled: true,
+ isTooltipEnabled: false,
+ isLineWrappingEnabled: false,
+ theme: 'teletext',
+ fontFamily: 'monospace',
+ fontSize: 18,
+};
+
+async function run() {
+ const container = document.getElementById('code');
+ if (!container) {
+ console.warn('could not init: no container found');
+ return;
+ }
+
+ const drawContext = getDrawContext();
+ const drawTime = [-2, 2];
+ editor = new StrudelMirror({
+ defaultOutput: webaudioOutput,
+ getTime: () => getAudioContext().currentTime,
+ transpiler,
+ root: container,
+ initialCode: '// LOADING',
+ pattern: silence,
+ settings: initialSettings,
+ drawTime,
+ onDraw: (haps, time, frame, painters) => {
+ painters.length && drawContext.clearRect(0, 0, drawContext.canvas.width * 2, drawContext.canvas.height * 2);
+ painters?.forEach((painter) => {
+ // ctx time haps drawTime paintOptions
+ painter(drawContext, time, haps, drawTime, { clear: false });
+ });
+ },
+ prebake: async () => {
+ // populate scope / lazy load modules
+ const modulesLoading = evalScope(
+ import('@strudel.cycles/core'),
+ import('@strudel.cycles/tonal'),
+ import('@strudel.cycles/mini'),
+ // import('@strudel.cycles/xen'),
+ import('@strudel.cycles/webaudio'),
+ import('@strudel/codemirror'),
+ /* import('@strudel/hydra'), */
+ // import('@strudel.cycles/serial'),
+ /* import('@strudel.cycles/soundfonts'), */
+ // import('@strudel.cycles/csound'),
+ /* import('@strudel.cycles/midi'), */
+ // import('@strudel.cycles/osc'),
+ controls, // sadly, this cannot be exported from core directly (yet)
+ );
+ // load samples
+ const ds = 'https://raw.githubusercontent.com/felixroos/dough-samples/main/';
+ await Promise.all([
+ modulesLoading,
+ registerSynthSounds(),
+ registerZZFXSounds(),
+ samples(`${ds}/tidal-drum-machines.json`),
+ samples(`${ds}/piano.json`),
+ samples(`${ds}/Dirt-Samples.json`),
+ samples(`${ds}/EmuSP12.json`),
+ samples(`${ds}/vcsl.json`),
+ ]);
+ },
+ afterEval: ({ code }) => {
+ window.location.hash = '#' + code2hash(code);
+ },
+ });
+
+ // init settings
+ editor.updateSettings(initialSettings);
+
+ logger(`Welcome to Strudel! Click into the editor and then hit ctrl+enter to run the code!`, 'highlight');
+ const codeParam = window.location.href.split('#')[1] || '';
+
+ const initialCode = codeParam
+ ? hash2code(codeParam)
+ : `// @date 23-11-30
+// "teigrührgerät" @by froos
+
+stack(
+ stack(
+ s("bd(<3!3 5>,6)/2").bank('RolandTR707')
+ ,
+ s("~ sd:<0 1>").bank('RolandTR707').room("<0 .5>")
+ .lastOf(8, x=>x.segment("12").end(.2).gain(isaw))
+ ,
+ s("[tb ~ tb]").bank('RolandTR707')
+ .clip(0).release(.08).room(.2)
+ ).off(-1/6, x=>x.speed(.7).gain(.2).degrade())
+ ,
+ stack(
+ note(",6) ~!2 [f1?]*2>")
+ .s("sawtooth").lpf(perlin.range(400,1000))
+ .lpa(.1).lpenv(-3).room(.2)
+ .lpq(8).noise(.2)
+ .add(note("0,.1"))
+ ,
+ chord("<~ Gm9 ~!2>")
+ .dict('ireal').voicing()
+ .s("sawtooth").vib("2:.1")
+ .lpf(1000).lpa(.1).lpenv(-4)
+ .room(.5)
+ ,
+ n(run(3)).chord("/8")
+ .dict('ireal-ext')
+ .off(1/2, add(n(4)))
+ .voicing()
+ .clip(.1).release(.05)
+ .s("sine").jux(rev)
+ .sometimesBy(sine.slow(16), add(note(12)))
+ .room(.75)
+ .lpf(sine.range(200,2000).slow(16))
+ .gain(saw.slow(4).div(2))
+ ).add(note(perlin.range(0,.5)))
+)`;
+
+ editor.setCode(initialCode); // simpler alternative to above init
+
+ // settingsMap.listen((settings, key) => editor.changeSetting(key, settings[key]));
+ onEvent('strudel-toggle-play', () => editor.toggle());
+}
+
+run();
+
+function onEvent(key, callback) {
+ const listener = (e) => {
+ if (e.data === key) {
+ callback();
+ }
+ };
+ window.addEventListener('message', listener);
+ return () => window.removeEventListener('message', listener);
+}
+
+// settings form
+function getInput(form, name) {
+ return form.querySelector(`input[name=${name}]`) || form.querySelector(`select[name=${name}]`);
+}
+function getFormValues(form, initial) {
+ const entries = Object.entries(initial).map(([key, initialValue]) => {
+ const input = getInput(form, key);
+ if (!input) {
+ return [key, initialValue]; // fallback
+ }
+ if (input.type === 'checkbox') {
+ return [key, input.checked];
+ }
+ if (input.type === 'number') {
+ return [key, Number(input.value)];
+ }
+ if (input.tagName === 'SELECT') {
+ return [key, input.value];
+ }
+ return [key, input.value];
+ });
+ return Object.fromEntries(entries);
+}
+function setFormValues(form, values) {
+ Object.entries(values).forEach(([key, value]) => {
+ const input = getInput(form, key);
+ if (!input) {
+ return;
+ }
+ if (input.type === 'checkbox') {
+ input.checked = !!value;
+ } else if (input.type === 'number') {
+ input.value = value;
+ } else if (input.tagName) {
+ input.value = value;
+ }
+ });
+}
+
+const form = document.querySelector('form[name=settings]');
+setFormValues(form, initialSettings);
+form.addEventListener('change', () => {
+ const values = getFormValues(form, initialSettings);
+ // console.log('values', values);
+ editor.updateSettings(values);
+});