diff --git a/src/strudel/codemirror/README.md b/src/strudel/codemirror/README.md
new file mode 100644
index 0000000..300e30b
--- /dev/null
+++ b/src/strudel/codemirror/README.md
@@ -0,0 +1,3 @@
+# @strudel/codemirror
+
+This package contains helpers and extensions to use codemirror6. See [vite-vanilla-repl-cm6](../core/examples/vite-vanilla-repl-cm6/main.js) as an example of using it.
diff --git a/src/strudel/codemirror/autocomplete.mjs b/src/strudel/codemirror/autocomplete.mjs
new file mode 100644
index 0000000..b8a569b
--- /dev/null
+++ b/src/strudel/codemirror/autocomplete.mjs
@@ -0,0 +1,460 @@
+import jsdoc from '../../doc.json';
+import { autocompletion } from '@codemirror/autocomplete';
+import { h } from './html';
+import { Scale } from '@tonaljs/tonal';
+import { soundMap } from 'superdough';
+import { complex } from '@strudel/tonal';
+
+const escapeHtml = (str) => {
+ const div = document.createElement('div');
+ div.innerText = str;
+ return div.innerHTML;
+};
+
+const stripHtml = (html) => {
+ const div = document.createElement('div');
+ div.innerHTML = html;
+ return div.textContent || div.innerText || '';
+};
+
+const getDocLabel = (doc) => doc.name || doc.longname;
+
+const buildParamsList = (params) =>
+ params?.length
+ ? `
+
+ `
+ : '';
+
+const buildExamples = (examples) =>
+ examples?.length
+ ? `
+
+
Examples
+ ${examples
+ .map(
+ (example) => `
+
${escapeHtml(example)}
+ `,
+ )
+ .join('')}
+
+ `
+ : '';
+
+export const Autocomplete = (doc) =>
+ h`
+
+`[0];
+
+const isValidDoc = (doc) => {
+ const label = getDocLabel(doc);
+ return label && !label.startsWith('_') && !['package'].includes(doc.kind);
+};
+
+const hasExcludedTags = (doc) =>
+ ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
+
+export function bankCompletions() {
+ const soundDict = soundMap.get();
+ const banks = new Set();
+ for (const key of Object.keys(soundDict)) {
+ const [bank, suffix] = key.split('_');
+ if (suffix && bank) banks.add(bank);
+ }
+ return Array.from(banks)
+ .sort()
+ .map((name) => ({ label: name, type: 'bank' }));
+}
+
+// Attempt to get all scale names from Tonal
+let scaleCompletions = [];
+try {
+ scaleCompletions = (Scale.names ? Scale.names() : []).map((name) => ({ label: name, type: 'scale' }));
+} catch (e) {
+ console.warn('[autocomplete] Could not load scale names from Tonal:', e);
+}
+
+// Valid mode values for voicing
+const modeCompletions = [
+ { label: 'below', type: 'mode' },
+ { label: 'above', type: 'mode' },
+ { label: 'duck', type: 'mode' },
+ { label: 'root', type: 'mode' },
+];
+
+// Valid chord symbols from ireal dictionary plus empty string for major triads
+const chordSymbols = ['', ...Object.keys(complex)].sort();
+const chordSymbolCompletions = chordSymbols.map((symbol) => {
+ if (symbol === '') {
+ return {
+ label: 'major',
+ apply: '',
+ type: 'chord-symbol',
+ };
+ }
+ return {
+ label: symbol,
+ apply: symbol,
+ type: 'chord-symbol',
+ };
+});
+
+export const getSynonymDoc = (doc, synonym) => {
+ const synonyms = doc.synonyms || [];
+ const docLabel = getDocLabel(doc);
+ // Swap `doc.name` in for `s` in the list of synonyms
+ const synonymsWithDoc = [docLabel, ...synonyms].filter((x) => x && x !== synonym);
+ return {
+ ...doc,
+ name: synonym,
+ longname: synonym,
+ synonyms: synonymsWithDoc,
+ synonyms_text: synonymsWithDoc.join(', '),
+ };
+};
+
+const jsdocCompletions = (() => {
+ const seen = new Set(); // avoid repetition
+ const completions = [];
+ for (const doc of jsdoc.docs) {
+ if (!isValidDoc(doc) || hasExcludedTags(doc)) continue;
+ const docLabel = getDocLabel(doc);
+ // Remove duplicates
+ const synonyms = doc.synonyms || [];
+ let labels = [docLabel, ...synonyms];
+ for (const label of labels) {
+ // https://codemirror.net/docs/ref/#autocomplete.Completion
+ if (label && !seen.has(label)) {
+ seen.add(label);
+ completions.push({
+ label,
+ info: () => Autocomplete(getSynonymDoc(doc, label)),
+ type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
+ });
+ }
+ }
+ }
+ return completions;
+})();
+
+// --- Handler functions for each context ---
+const pitchNames = [
+ 'C',
+ 'C#',
+ 'Db',
+ 'D',
+ 'D#',
+ 'Eb',
+ 'E',
+ 'E#',
+ 'Fb',
+ 'F',
+ 'F#',
+ 'Gb',
+ 'G',
+ 'G#',
+ 'Ab',
+ 'A',
+ 'A#',
+ 'Bb',
+ 'B',
+ 'B#',
+ 'Cb',
+];
+
+// Cached regex patterns for scaleHandler
+const SCALE_NO_QUOTES_REGEX = /scale\(\s*$/;
+const SCALE_AFTER_COLON_REGEX = /scale\(\s*['"][^'"]*:[^'"]*$/;
+const SCALE_PRE_COLON_REGEX = /scale\(\s*['"][^'"]*$/;
+const SCALE_PITCH_MATCH_REGEX = /([A-Ga-g][#b]*)?$/;
+const SCALE_SPACES_TO_COLON_REGEX = /\s+/g;
+
+function scaleHandler(context) {
+ // First check for scale context without quotes - block with empty completions
+ let scaleNoQuotesContext = context.matchBefore(SCALE_NO_QUOTES_REGEX);
+ if (scaleNoQuotesContext) {
+ return {
+ from: scaleNoQuotesContext.to,
+ options: [],
+ };
+ }
+
+ // Check for after-colon context first (more specific)
+ let scaleAfterColonContext = context.matchBefore(SCALE_AFTER_COLON_REGEX);
+ if (scaleAfterColonContext) {
+ const text = scaleAfterColonContext.text;
+ const colonIdx = text.lastIndexOf(':');
+ if (colonIdx !== -1) {
+ const fragment = text.slice(colonIdx + 1);
+ const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
+ const options = filteredScales.map((s) => ({
+ ...s,
+ apply: s.label.replace(SCALE_SPACES_TO_COLON_REGEX, ':'),
+ }));
+ const from = scaleAfterColonContext.from + colonIdx + 1;
+ return {
+ from,
+ options,
+ };
+ }
+ }
+
+ // Then check for pre-colon context
+ let scalePreColonContext = context.matchBefore(SCALE_PRE_COLON_REGEX);
+ if (scalePreColonContext) {
+ if (!scalePreColonContext.text.includes(':')) {
+ if (context.explicit) {
+ const text = scalePreColonContext.text;
+ const match = text.match(SCALE_PITCH_MATCH_REGEX);
+ const fragment = match ? match[0] : '';
+ const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
+ const from = scalePreColonContext.to - fragment.length;
+ const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
+ return { from, options };
+ } else {
+ return { from: scalePreColonContext.to, options: [] };
+ }
+ }
+ }
+ return null;
+}
+
+// Cached regex patterns for soundHandler
+const SOUND_NO_QUOTES_REGEX = /(s|sound)\(\s*$/;
+const SOUND_WITH_QUOTES_REGEX = /(s|sound)\(\s*['"][^'"]*$/;
+const SOUND_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w]*)$/;
+
+function soundHandler(context) {
+ // First check for sound context without quotes - block with empty completions
+ let soundNoQuotesContext = context.matchBefore(SOUND_NO_QUOTES_REGEX);
+ if (soundNoQuotesContext) {
+ return {
+ from: soundNoQuotesContext.to,
+ options: [],
+ };
+ }
+
+ // Then check for sound context with quotes - provide completions
+ let soundContext = context.matchBefore(SOUND_WITH_QUOTES_REGEX);
+ if (!soundContext) return null;
+
+ const text = soundContext.text;
+ const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
+ if (quoteIdx === -1) return null;
+ const inside = text.slice(quoteIdx + 1);
+ const fragMatch = inside.match(SOUND_FRAGMENT_MATCH_REGEX);
+ const fragment = fragMatch ? fragMatch[1] : inside;
+ const soundNames = Object.keys(soundMap.get()).sort();
+ const filteredSounds = soundNames.filter((name) => name.includes(fragment));
+ let options = filteredSounds.map((name) => ({ label: name, type: 'sound' }));
+ const from = soundContext.to - fragment.length;
+ return {
+ from,
+ options,
+ };
+}
+
+// Cached regex patterns for bankHandler
+const BANK_NO_QUOTES_REGEX = /bank\(\s*$/;
+const BANK_WITH_QUOTES_REGEX = /bank\(\s*['"][^'"]*$/;
+
+function bankHandler(context) {
+ // First check for bank context without quotes - block with empty completions
+ let bankNoQuotesContext = context.matchBefore(BANK_NO_QUOTES_REGEX);
+ if (bankNoQuotesContext) {
+ return {
+ from: bankNoQuotesContext.to,
+ options: [],
+ };
+ }
+
+ // Then check for bank context with quotes - provide completions
+ let bankMatch = context.matchBefore(BANK_WITH_QUOTES_REGEX);
+ if (!bankMatch) return null;
+
+ const text = bankMatch.text;
+ const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
+ if (quoteIdx === -1) return null;
+ const inside = text.slice(quoteIdx + 1);
+ const fragment = inside;
+ let banks = bankCompletions();
+ const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
+ const from = bankMatch.to - fragment.length;
+ return {
+ from,
+ options: filteredBanks,
+ };
+}
+
+// Cached regex patterns for modeHandler
+const MODE_NO_QUOTES_REGEX = /mode\(\s*$/;
+const MODE_AFTER_COLON_REGEX = /mode\(\s*['"][^'"]*:[^'"]*$/;
+const MODE_PRE_COLON_REGEX = /mode\(\s*['"][^'"]*$/;
+const MODE_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w:]*)$/;
+
+function modeHandler(context) {
+ // First check for mode context without quotes - block with empty completions
+ let modeNoQuotesContext = context.matchBefore(MODE_NO_QUOTES_REGEX);
+ if (modeNoQuotesContext) {
+ return {
+ from: modeNoQuotesContext.to,
+ options: [],
+ };
+ }
+
+ // Check for after-colon context first (more specific)
+ let modeAfterColonContext = context.matchBefore(MODE_AFTER_COLON_REGEX);
+ if (modeAfterColonContext) {
+ const text = modeAfterColonContext.text;
+ const colonIdx = text.lastIndexOf(':');
+ if (colonIdx !== -1) {
+ const fragment = text.slice(colonIdx + 1);
+ // For anchor after colon, we can suggest pitch names
+ const filtered = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
+ const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
+ const from = modeAfterColonContext.from + colonIdx + 1;
+ return {
+ from,
+ options,
+ };
+ }
+ }
+
+ // Then check for pre-colon context
+ let modeContext = context.matchBefore(MODE_PRE_COLON_REGEX);
+ if (!modeContext) return null;
+
+ const text = modeContext.text;
+ const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
+ if (quoteIdx === -1) return null;
+ const inside = text.slice(quoteIdx + 1);
+ const fragMatch = inside.match(MODE_FRAGMENT_MATCH_REGEX);
+ const fragment = fragMatch ? fragMatch[1] : inside;
+ const filteredModes = modeCompletions.filter((m) => m.label.startsWith(fragment));
+ const from = modeContext.to - fragment.length;
+ return {
+ from,
+ options: filteredModes,
+ };
+}
+
+// Cached regex patterns for chordHandler
+const CHORD_NO_QUOTES_REGEX = /chord\(\s*$/;
+const CHORD_WITH_QUOTES_REGEX = /chord\(\s*['"][^'"]*$/;
+const CHORD_FRAGMENT_MATCH_REGEX = /(?:[\s[{(<])([\w#b+^:-]*)$/;
+
+function chordHandler(context) {
+ // First check for chord context without quotes - block with empty completions
+ let chordNoQuotesContext = context.matchBefore(CHORD_NO_QUOTES_REGEX);
+ if (chordNoQuotesContext) {
+ return {
+ from: chordNoQuotesContext.to,
+ options: [],
+ };
+ }
+
+ // Then check for chord context with quotes - provide completions
+ let chordContext = context.matchBefore(CHORD_WITH_QUOTES_REGEX);
+ if (!chordContext) return null;
+
+ const text = chordContext.text;
+ const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
+ if (quoteIdx === -1) return null;
+ const inside = text.slice(quoteIdx + 1);
+
+ // Use same fragment matching as sound/mode for expressions like ""
+ const fragMatch = inside.match(CHORD_FRAGMENT_MATCH_REGEX);
+ const fragment = fragMatch ? fragMatch[1] : inside;
+
+ // Check if fragment contains any pitch name at start (for root + symbol)
+ let rootMatch = null;
+ let symbolFragment = fragment;
+ for (const pitch of pitchNames) {
+ if (fragment.toLowerCase().startsWith(pitch.toLowerCase())) {
+ rootMatch = pitch;
+ symbolFragment = fragment.slice(pitch.length);
+ break;
+ }
+ }
+
+ if (rootMatch) {
+ // We have a root, now complete chord symbols
+ const filteredSymbols = chordSymbolCompletions.filter((s) =>
+ s.label.toLowerCase().startsWith(symbolFragment.toLowerCase()),
+ );
+
+ // Create completions that replace the entire chord, not just the symbol part
+ const options = filteredSymbols;
+
+ const from = chordContext.to - symbolFragment.length;
+ return { from, options };
+ } else {
+ // No root yet, complete with pitch names
+ const filteredPitches = pitchNames.filter((p) => p.toLowerCase().startsWith(fragment.toLowerCase()));
+ const options = filteredPitches.map((p) => ({ label: p, type: 'pitch' }));
+ const from = chordContext.to - fragment.length;
+ return { from, options };
+ }
+}
+
+// Cached regex patterns for fallbackHandler
+const FALLBACK_WORD_REGEX = /\w*/;
+
+function fallbackHandler(context) {
+ const word = context.matchBefore(FALLBACK_WORD_REGEX);
+ if (word && word.from === word.to && !context.explicit) return null;
+ if (word) {
+ return {
+ from: word.from,
+ options: jsdocCompletions,
+ };
+ }
+ return null;
+}
+
+const handlers = [
+ soundHandler,
+ bankHandler,
+ chordHandler,
+ scaleHandler,
+ modeHandler,
+ // this handler *must* be last
+ fallbackHandler,
+];
+
+export const strudelAutocomplete = (context) => {
+ for (const handler of handlers) {
+ const result = handler(context);
+ if (result) {
+ return result;
+ }
+ }
+ return null;
+};
+
+export const isAutoCompletionEnabled = (enabled) =>
+ enabled ? [autocompletion({ override: [strudelAutocomplete], closeOnBlur: false })] : [];
diff --git a/src/strudel/codemirror/basicSetup.mjs b/src/strudel/codemirror/basicSetup.mjs
new file mode 100644
index 0000000..02294b9
--- /dev/null
+++ b/src/strudel/codemirror/basicSetup.mjs
@@ -0,0 +1,63 @@
+import {
+ keymap,
+ highlightSpecialChars,
+ drawSelection,
+ highlightActiveLine,
+ dropCursor,
+ rectangularSelection,
+ crosshairCursor,
+ lineNumbers,
+ highlightActiveLineGutter,
+} from '@codemirror/view';
+import {
+ defaultHighlightStyle,
+ syntaxHighlighting,
+ bracketMatching,
+ foldGutter,
+ foldKeymap,
+} from '@codemirror/language';
+import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
+import { searchKeymap, highlightSelectionMatches } from '@codemirror/search';
+import { completionKeymap, closeBracketsKeymap } from '@codemirror/autocomplete';
+
+// Taken + slightly modified from https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
+
+export const basicSetup = (() => [
+ // lineNumbers(),
+ // highlightActiveLineGutter(),
+ highlightSpecialChars(),
+ history(),
+ // foldGutter(),
+ // drawSelection(),
+ dropCursor(),
+ // EditorState.allowMultipleSelections.of(true),
+ // indentOnInput(),
+ // syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
+ // autocompletion(),
+ rectangularSelection(),
+ crosshairCursor(),
+ // highlightActiveLine(),
+ // highlightSelectionMatches(),
+ keymap.of([
+ ...closeBracketsKeymap,
+ ...defaultKeymap,
+ // ...searchKeymap,
+ ...historyKeymap,
+ // ...foldKeymap,
+ // ...completionKeymap,
+ ]),
+])();
+
+/// A minimal set of extensions to create a functional editor. Only
+/// includes [the default keymap](#commands.defaultKeymap), [undo
+/// history](#commands.history), [special character
+/// highlighting](#view.highlightSpecialChars), [custom selection
+/// drawing](#view.drawSelection), and [default highlight
+/// style](#language.defaultHighlightStyle).
+export const minimalSetup = (() => [
+ highlightSpecialChars(),
+ history(),
+ drawSelection(),
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
+ keymap.of([...defaultKeymap, ...historyKeymap]),
+])();
diff --git a/src/strudel/codemirror/codemirror.mjs b/src/strudel/codemirror/codemirror.mjs
new file mode 100644
index 0000000..4dc2399
--- /dev/null
+++ b/src/strudel/codemirror/codemirror.mjs
@@ -0,0 +1,386 @@
+import { closeBrackets } from '@codemirror/autocomplete';
+export { toggleComment, toggleBlockComment, toggleLineComment, toggleBlockCommentByLine } from '@codemirror/commands';
+// import { search, highlightSelectionMatches } from '@codemirror/search';
+import { indentWithTab } from '@codemirror/commands';
+import { javascript, javascriptLanguage } from '@codemirror/lang-javascript';
+import { defaultHighlightStyle, syntaxHighlighting, bracketMatching } from '@codemirror/language';
+import { Compartment, EditorState, Prec } from '@codemirror/state';
+import {
+ EditorView,
+ highlightActiveLineGutter,
+ highlightActiveLine,
+ keymap,
+ lineNumbers,
+ drawSelection,
+} from '@codemirror/view';
+import { repl, registerControl } from '@strudel/core';
+import { Drawer, cleanupDraw } from '@strudel/draw';
+import { isAutoCompletionEnabled } from './autocomplete.mjs';
+import { isTooltipEnabled } from './tooltip.mjs';
+import { flash, isFlashEnabled } from './flash.mjs';
+import { highlightMiniLocations, isPatternHighlightingEnabled, updateMiniLocations } from './highlight.mjs';
+import { keybindings } from './keybindings.mjs';
+import { initTheme, activateTheme, theme } from './themes.mjs';
+import { sliderPlugin, updateSliderWidgets } from './slider.mjs';
+import { widgetPlugin, updateWidgets } from './widget.mjs';
+import { persistentAtom } from '@nanostores/persistent';
+import { basicSetup } from './basicSetup.mjs';
+
+const extensions = {
+ isLineWrappingEnabled: (on) => (on ? EditorView.lineWrapping : []),
+ isBracketMatchingEnabled: (on) => (on ? bracketMatching({ brackets: '()[]{}<>' }) : []),
+ isBracketClosingEnabled: (on) => (on ? closeBrackets() : []),
+ isLineNumbersDisplayed: (on) => (on ? lineNumbers() : []),
+ theme,
+ isAutoCompletionEnabled,
+ isTooltipEnabled,
+ isPatternHighlightingEnabled,
+ isActiveLineHighlighted: (on) => (on ? [highlightActiveLine(), highlightActiveLineGutter()] : []),
+ isFlashEnabled,
+ keybindings,
+ isTabIndentationEnabled: (on) => (on ? keymap.of([indentWithTab]) : []),
+ isMultiCursorEnabled: (on) =>
+ on
+ ? [
+ EditorState.allowMultipleSelections.of(true),
+ EditorView.clickAddsSelectionRange.of((ev) => ev.metaKey || ev.ctrlKey),
+ ]
+ : [],
+};
+const compartments = Object.fromEntries(Object.keys(extensions).map((key) => [key, new Compartment()]));
+
+export const defaultSettings = {
+ keybindings: 'codemirror',
+ isBracketMatchingEnabled: false,
+ isBracketClosingEnabled: true,
+ isLineNumbersDisplayed: true,
+ isActiveLineHighlighted: false,
+ isAutoCompletionEnabled: false,
+ isPatternHighlightingEnabled: true,
+ isFlashEnabled: true,
+ isTooltipEnabled: false,
+ isLineWrappingEnabled: false,
+ isTabIndentationEnabled: false,
+ isMultiCursorEnabled: false,
+ theme: 'strudelTheme',
+ fontFamily: 'monospace',
+ fontSize: 18,
+};
+
+export const codemirrorSettings = persistentAtom('codemirror-settings', defaultSettings, {
+ encode: JSON.stringify,
+ decode: JSON.parse,
+});
+
+// https://codemirror.net/docs/guide/
+export function initEditor({ initialCode = '', onChange, onEvaluate, onStop, root, mondo }) {
+ const settings = codemirrorSettings.get();
+ const initialSettings = Object.keys(compartments).map((key) =>
+ compartments[key].of(extensions[key](parseBooleans(settings[key]))),
+ );
+
+ initTheme(settings.theme);
+ let state = EditorState.create({
+ doc: initialCode,
+ extensions: [
+ /* search(),
+ highlightSelectionMatches(), */
+ ...initialSettings,
+ basicSetup,
+ mondo ? [] : javascript(),
+ javascriptLanguage.data.of({
+ closeBrackets: { brackets: ['(', '[', '{', "'", '"', '<'] },
+ bracketMatching: { brackets: ['(', '[', '{', "'", '"', '<'] },
+ }),
+ sliderPlugin,
+ widgetPlugin,
+ // indentOnInput(), // works without. already brought with javascript extension?
+ // bracketMatching(), // does not do anything
+ syntaxHighlighting(defaultHighlightStyle),
+ EditorView.updateListener.of((v) => onChange(v)),
+ drawSelection({ cursorBlinkRate: 0 }),
+ Prec.highest(
+ keymap.of([
+ {
+ key: 'Ctrl-Enter',
+ run: () => onEvaluate?.(),
+ },
+ {
+ key: 'Alt-Enter',
+ run: () => onEvaluate?.(),
+ },
+ {
+ key: 'Ctrl-.',
+ run: () => onStop?.(),
+ },
+ {
+ key: 'Alt-.',
+ preventDefault: true,
+ run: () => onStop?.(),
+ },
+ /* {
+ key: 'Ctrl-Shift-.',
+ run: () => (onPanic ? onPanic() : onStop?.()),
+ },
+ {
+ key: 'Ctrl-Shift-Enter',
+ run: () => (onReEvaluate ? onReEvaluate() : onEvaluate?.()),
+ }, */
+ ]),
+ ),
+ ],
+ });
+
+ return new EditorView({
+ state,
+ parent: root,
+ });
+}
+
+export class StrudelMirror {
+ constructor(options) {
+ const {
+ root,
+ id,
+ initialCode = '',
+ onDraw,
+ drawContext,
+ drawTime = [0, 0],
+ autodraw,
+ prebake,
+ bgFill = true,
+ solo = true,
+ ...replOptions
+ } = options;
+ this.code = initialCode;
+ this.root = root;
+ this.miniLocations = [];
+ this.widgets = [];
+ this.drawTime = drawTime;
+ this.drawContext = drawContext;
+ this.onDraw = onDraw || this.draw;
+ this.id = id || s4();
+ this.solo = solo;
+
+ this.drawer = new Drawer((haps, time, _, painters) => {
+ const currentFrame = haps.filter((hap) => hap.isActive(time));
+ this.highlight(currentFrame, time);
+ this.onDraw(haps, time, painters);
+ }, drawTime);
+
+ this.prebaked = prebake();
+ autodraw && this.drawFirstFrame();
+ this.repl = repl({
+ ...replOptions,
+ id,
+ onToggle: (started) => {
+ replOptions?.onToggle?.(started);
+ if (started) {
+ this.drawer.start(this.repl.scheduler);
+ if (this.solo) {
+ // stop other repls when this one is started
+ document.dispatchEvent(
+ new CustomEvent('start-repl', {
+ detail: this.id,
+ }),
+ );
+ }
+ } else {
+ this.drawer.stop();
+ updateMiniLocations(this.editor, []);
+ cleanupDraw(true, id);
+ }
+ },
+ beforeEval: async () => {
+ cleanupDraw(true, id);
+ 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;
+ const sliders = this.widgets.filter((w) => w.type === 'slider');
+ updateSliderWidgets(this.editor, sliders);
+ const widgets = this.widgets.filter((w) => w.type !== 'slider');
+ updateWidgets(this.editor, widgets);
+ updateMiniLocations(this.editor, this.miniLocations);
+ replOptions?.afterEval?.(options);
+ // if no painters are set (.onPaint was not called), then we only need the present moment (for highlighting)
+ const drawTime = options.pattern.getPainters().length ? this.drawTime : [0, 0];
+ this.drawer.setDrawTime(drawTime);
+ // invalidate drawer after we've set the appropriate drawTime
+ this.drawer.invalidate(this.repl.scheduler);
+ },
+ });
+ this.editor = initEditor({
+ root,
+ initialCode,
+ onChange: (v) => {
+ if (v.docChanged) {
+ this.code = v.state.doc.toString();
+ this.repl.setCode?.(this.code);
+ }
+ },
+ onEvaluate: () => this.evaluate(),
+ onStop: () => this.stop(),
+ mondo: replOptions.mondo,
+ });
+ const cmEditor = this.root.querySelector('.cm-editor');
+ if (cmEditor) {
+ this.root.style.display = 'block';
+ if (bgFill) {
+ this.root.style.backgroundColor = 'var(--background)';
+ }
+ cmEditor.style.backgroundColor = 'transparent';
+ }
+ const settings = codemirrorSettings.get();
+ this.setFontSize(settings.fontSize);
+ this.setFontFamily(settings.fontFamily);
+
+ // stop this repl when another repl is started
+ this.onStartRepl = (e) => {
+ if (this.solo && e.detail !== this.id) {
+ this.stop();
+ }
+ };
+ document.addEventListener('start-repl', this.onStartRepl);
+ }
+ draw(haps, time, painters) {
+ painters?.forEach((painter) => painter(this.drawContext, time, haps, this.drawTime));
+ }
+ 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, -0.001);
+ // draw at -0.001 to avoid haps at 0 to be visualized as active
+ this.onDraw?.(this.drawer.visibleHaps, -0.001, this.drawer.painters);
+ } catch (err) {
+ console.warn('first frame could not be painted');
+ }
+ }
+ async evaluate() {
+ this.flash();
+ await this.repl.evaluate(this.code);
+ }
+ async stop() {
+ this.repl.scheduler.stop();
+ }
+ async toggle() {
+ if (this.repl.scheduler.started) {
+ this.repl.stop();
+ } else {
+ this.evaluate();
+ }
+ }
+ flash(ms) {
+ flash(this.editor, ms);
+ }
+ highlight(haps, time) {
+ highlightMiniLocations(this.editor, time, haps);
+ }
+ setFontSize(size) {
+ this.root.style.fontSize = size + 'px';
+ }
+ setFontFamily(family) {
+ this.root.style.fontFamily = family;
+ const scroller = this.root.querySelector('.cm-scroller');
+ if (scroller) {
+ scroller.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),
+ });
+ if (key === 'theme') {
+ activateTheme(value);
+ }
+ }
+ setLineWrappingEnabled(enabled) {
+ this.reconfigureExtension('isLineWrappingEnabled', enabled);
+ }
+ setBracketMatchingEnabled(enabled) {
+ this.reconfigureExtension('isBracketMatchingEnabled', enabled);
+ }
+ setLineNumbersDisplayed(enabled) {
+ this.reconfigureExtension('isLineNumbersDisplayed', enabled);
+ }
+ setBracketClosingEnabled(enabled) {
+ this.reconfigureExtension('isBracketClosingEnabled', 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]);
+ }
+ const updated = { ...codemirrorSettings.get(), ...settings };
+ codemirrorSettings.set(updated);
+ }
+ 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 });
+ }
+ clear() {
+ this.onStartRepl && document.removeEventListener('start-repl', this.onStartRepl);
+ }
+ getCursorLocation() {
+ return this.editor.state.selection.main.head;
+ }
+ setCursorLocation(col) {
+ return this.editor.dispatch({ selection: { anchor: col } });
+ }
+ appendCode(code) {
+ const cursor = this.getCursorLocation();
+ this.setCode(this.code + code);
+ this.setCursorLocation(cursor);
+ }
+}
+
+function parseBooleans(value) {
+ return { true: true, false: false }[value] ?? value;
+}
+
+// helper function to generate repl ids
+function s4() {
+ return Math.floor((1 + Math.random()) * 0x10000)
+ .toString(16)
+ .substring(1);
+}
+
+/**
+ * Overrides the css of highlighted events. Make sure to use single quotes!
+ * @name markcss
+ * @example
+ * note("c a f e")
+ * .markcss('text-decoration:underline')
+ */
+export const markcss = registerControl('markcss');
diff --git a/src/strudel/codemirror/flash.mjs b/src/strudel/codemirror/flash.mjs
new file mode 100644
index 0000000..243500b
--- /dev/null
+++ b/src/strudel/codemirror/flash.mjs
@@ -0,0 +1,39 @@
+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: rgba(255,255,255, .4); filter: invert(10%)` },
+ });
+ 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 const isFlashEnabled = (on) => (on ? flashField : []);
diff --git a/src/strudel/codemirror/highlight.mjs b/src/strudel/codemirror/highlight.mjs
new file mode 100644
index 0000000..f9f977c
--- /dev/null
+++ b/src/strudel/codemirror/highlight.mjs
@@ -0,0 +1,138 @@
+import { RangeSetBuilder, StateEffect, StateField, Prec } 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
+ }
+ }
+
+ 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) {
+ if (!hap.context?.locations || !hap.whole) {
+ continue;
+ }
+ 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.value?.color ?? 'var(--foreground)';
+ const style = hap.value?.markcss || `outline: solid 2px ${color}`;
+ // 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 },
+ }),
+ );
+ }
+
+ iterator.next();
+ }
+
+ return builder.finish();
+});
+
+export const highlightExtension = [miniLocations, visibleMiniLocations, miniLocationHighlights];
+
+export const isPatternHighlightingEnabled = (on, config) => {
+ on &&
+ config &&
+ setTimeout(() => {
+ updateMiniLocations(config.editor, config.miniLocations);
+ }, 100);
+ return on ? Prec.highest(highlightExtension) : [];
+};
diff --git a/src/strudel/codemirror/html.mjs b/src/strudel/codemirror/html.mjs
new file mode 100644
index 0000000..f240059
--- /dev/null
+++ b/src/strudel/codemirror/html.mjs
@@ -0,0 +1,18 @@
+export let html = (string) => {
+ const template = document.createElement('template');
+ template.innerHTML = string.trim();
+ return template.content.childNodes;
+};
+let parseChunk = (chunk) => {
+ if (Array.isArray(chunk)) return chunk.flat().join('');
+ if (chunk === undefined) return '';
+ return chunk;
+};
+export let h = (strings, ...vars) => {
+ let string = '';
+ for (let i in strings) {
+ string += parseChunk(strings[i]);
+ string += parseChunk(vars[i]);
+ }
+ return html(string);
+};
diff --git a/src/strudel/codemirror/index.mjs b/src/strudel/codemirror/index.mjs
new file mode 100644
index 0000000..3a5f2a2
--- /dev/null
+++ b/src/strudel/codemirror/index.mjs
@@ -0,0 +1,6 @@
+export * from './codemirror.mjs';
+export * from './highlight.mjs';
+export * from './flash.mjs';
+export * from './slider.mjs';
+export * from './themes.mjs';
+export * from './widget.mjs';
diff --git a/src/strudel/codemirror/keybindings.mjs b/src/strudel/codemirror/keybindings.mjs
new file mode 100644
index 0000000..ca5f34f
--- /dev/null
+++ b/src/strudel/codemirror/keybindings.mjs
@@ -0,0 +1,32 @@
+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 { vim } from './vim_test.mjs';
+import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
+import { defaultKeymap } 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,
+ codemirror: () => keymap.of(defaultKeymap),
+ vscode: vscodeExtension,
+};
+
+export function keybindings(name) {
+ const active = keymaps[name];
+ return [active ? Prec.high(active()) : []];
+}
diff --git a/src/strudel/codemirror/package.json b/src/strudel/codemirror/package.json
new file mode 100644
index 0000000..b4e7d27
--- /dev/null
+++ b/src/strudel/codemirror/package.json
@@ -0,0 +1,58 @@
+{
+ "name": "@strudel/codemirror",
+ "version": "1.2.5",
+ "description": "Codemirror Extensions for Strudel",
+ "main": "index.mjs",
+ "publishConfig": {
+ "main": "dist/index.mjs"
+ },
+ "scripts": {
+ "build": "vite build",
+ "prepublishOnly": "npm run build"
+ },
+ "type": "module",
+ "repository": {
+ "type": "git",
+ "url": "git+https://codeberg.org/uzu/strudel.git"
+ },
+ "keywords": [
+ "tidalcycles",
+ "strudel",
+ "pattern",
+ "livecoding",
+ "algorave"
+ ],
+ "author": "Felix Roos ",
+ "contributors": [
+ "Alex McLean "
+ ],
+ "license": "AGPL-3.0-or-later",
+ "bugs": {
+ "url": "https://codeberg.org/uzu/strudel/issues"
+ },
+ "homepage": "https://codeberg.org/uzu/strudel#readme",
+ "dependencies": {
+ "@codemirror/autocomplete": "^6.18.4",
+ "@codemirror/commands": "^6.8.0",
+ "@codemirror/lang-javascript": "^6.2.2",
+ "@codemirror/language": "^6.10.8",
+ "@codemirror/search": "^6.5.8",
+ "@codemirror/state": "^6.5.1",
+ "@codemirror/view": "^6.36.2",
+ "@lezer/highlight": "^1.2.1",
+ "@nanostores/persistent": "^0.10.2",
+ "@replit/codemirror-emacs": "^6.1.0",
+ "@replit/codemirror-vim": "^6.3.0",
+ "@replit/codemirror-vscode-keymap": "^6.0.2",
+ "@strudel/core": "workspace:*",
+ "@strudel/draw": "workspace:*",
+ "@strudel/tonal": "workspace:*",
+ "@strudel/transpiler": "workspace:*",
+ "@tonaljs/tonal": "^4.10.0",
+ "superdough": "workspace:*",
+ "nanostores": "^0.11.3"
+ },
+ "devDependencies": {
+ "vite": "^6.0.11"
+ }
+}
diff --git a/src/strudel/codemirror/slider.mjs b/src/strudel/codemirror/slider.mjs
new file mode 100644
index 0000000..72f9512
--- /dev/null
+++ b/src/strudel/codemirror/slider.mjs
@@ -0,0 +1,146 @@
+import { ref, pure } from '@strudel/core';
+import { WidgetType, ViewPlugin, Decoration } from '@codemirror/view';
+import { StateEffect } from '@codemirror/state';
+
+export let sliderValues = {};
+const getSliderID = (from) => `slider_${from}`;
+
+export class SliderWidget extends WidgetType {
+ constructor(value, min, max, from, to, step, view) {
+ super();
+ this.value = value;
+ this.min = min;
+ this.max = max;
+ this.from = from;
+ this.originalFrom = from;
+ this.to = to;
+ this.step = step;
+ this.view = view;
+ }
+
+ eq() {
+ return false;
+ }
+
+ toDOM() {
+ let wrap = document.createElement('span');
+ wrap.setAttribute('aria-hidden', 'true');
+ wrap.className = 'cm-slider'; // inline-flex items-center
+ let slider = wrap.appendChild(document.createElement('input'));
+ slider.type = 'range';
+ slider.min = this.min;
+ slider.max = this.max;
+ slider.step = this.step ?? (this.max - this.min) / 1000;
+ slider.originalValue = this.value;
+ // to make sure the code stays in sync, let's save the original value
+ // becuase .value automatically clamps values so it'll desync with the code
+ slider.value = slider.originalValue;
+ slider.from = this.from;
+ slider.originalFrom = this.originalFrom;
+ slider.to = this.to;
+ slider.style = 'width:64px;margin-right:4px;transform:translateY(4px)';
+ this.slider = slider;
+ slider.addEventListener('input', (e) => {
+ const next = e.target.value;
+ let insert = next;
+ //let insert = next.toFixed(2);
+ const to = slider.from + slider.originalValue.length;
+ let change = { from: slider.from, to, insert };
+ slider.originalValue = insert;
+ slider.value = insert;
+ this.view.dispatch({ changes: change });
+ const id = getSliderID(slider.originalFrom); // matches id generated in transpiler
+ window.postMessage({ type: 'cm-slider', value: Number(next), id });
+ });
+ return wrap;
+ }
+
+ ignoreEvent(e) {
+ return true;
+ }
+}
+
+export const setSliderWidgets = StateEffect.define();
+
+export const updateSliderWidgets = (view, widgets) => {
+ view.dispatch({ effects: setSliderWidgets.of(widgets) });
+};
+
+function getSliders(widgetConfigs, view) {
+ return widgetConfigs
+ .filter((w) => w.type === 'slider')
+ .map(({ from, to, value, min, max, step }) => {
+ return Decoration.widget({
+ widget: new SliderWidget(value, min, max, from, to, step, view),
+ side: 0,
+ }).range(from /* , to */);
+ });
+}
+
+export const sliderPlugin = ViewPlugin.fromClass(
+ class {
+ decorations; //: DecorationSet
+
+ constructor(view /* : EditorView */) {
+ this.decorations = Decoration.set([]);
+ }
+
+ update(update /* : ViewUpdate */) {
+ update.transactions.forEach((tr) => {
+ if (tr.docChanged) {
+ this.decorations = this.decorations.map(tr.changes);
+ const iterator = this.decorations.iter();
+ while (iterator.value) {
+ // when the widgets are moved, we need to tell the dom node the current position
+ // this is important because the updateSliderValue function has to work with the dom node
+ if (iterator.value?.widget?.slider) {
+ iterator.value.widget.slider.from = iterator.from;
+ iterator.value.widget.slider.to = iterator.to;
+ }
+ iterator.next();
+ }
+ }
+ for (let e of tr.effects) {
+ if (e.is(setSliderWidgets)) {
+ this.decorations = Decoration.set(getSliders(e.value, update.view));
+ }
+ }
+ });
+ }
+ },
+ {
+ decorations: (v) => v.decorations,
+ },
+);
+
+/**
+ * Displays a slider widget to allow the user manipulate a value
+ *
+ * @name slider
+ * @param {number} value Initial value
+ * @param {number} min Minimum value - optional, defaults to 0
+ * @param {number} max Maximum value - optional, defaults to 1
+ * @param {number} step Step size - optional
+ */
+export let slider = (value) => {
+ console.warn('slider will only work when the transpiler is used... passing value as is');
+ return pure(value);
+};
+// function transpiled from slider = (value, min, max)
+export let sliderWithID = (id, value, min, max) => {
+ sliderValues[id] = value; // sync state at eval time (code -> state)
+ return ref(() => sliderValues[id]); // use state at query time
+};
+// update state when sliders are moved
+if (typeof window !== 'undefined') {
+ window.addEventListener('message', (e) => {
+ if (e.data.type === 'cm-slider') {
+ if (sliderValues[e.data.id] !== undefined) {
+ // update state when slider is moved
+ sliderValues[e.data.id] = e.data.value;
+ } else {
+ console.warn(`slider with id "${e.data.id}" is not registered. Only ${Object.keys(sliderValues)}`);
+ }
+ }
+ });
+}
diff --git a/src/strudel/codemirror/themes.mjs b/src/strudel/codemirror/themes.mjs
new file mode 100644
index 0000000..1a523b3
--- /dev/null
+++ b/src/strudel/codemirror/themes.mjs
@@ -0,0 +1,218 @@
+import strudelTheme, { settings as strudelThemeSettings } from './themes/strudel-theme.mjs';
+import bluescreen, { settings as bluescreenSettings } from './themes/bluescreen.mjs';
+import blackscreen, { settings as blackscreenSettings } from './themes/blackscreen.mjs';
+import whitescreen, { settings as whitescreenSettings } from './themes/whitescreen.mjs';
+import teletext, { settings as teletextSettings } from './themes/teletext.mjs';
+import algoboy, { settings as algoboySettings } from './themes/algoboy.mjs';
+import CutiePi, { settings as CutiePiSettings } from './themes/CutiePi.mjs';
+import sonicPink, { settings as sonicPinkSettings } from './themes/sonic-pink.mjs';
+import redText, { settings as redTextSettings } from './themes/red-text.mjs';
+import greenText, { settings as greenTextSettings } from './themes/green-text.mjs';
+import archBtw, { settings as archBtwSettings } from './themes/archBtw.mjs';
+import fruitDaw, { settings as fruitDawSettings } from './themes/fruitDaw.mjs';
+
+import bluescreenlight, { settings as bluescreenlightsettings } from './themes/bluescreenlight.mjs';
+
+import androidstudio, { settings as androidstudioSettings } from './themes/androidstudio.mjs';
+import atomone, { settings as atomOneSettings } from './themes/atomone.mjs';
+import aura, { settings as auraSettings } from './themes/aura.mjs';
+import darcula, { settings as darculaSettings } from './themes/darcula.mjs';
+import dracula, { settings as draculaSettings } from './themes/dracula.mjs';
+import duotoneDark, { settings as duotoneDarkSettings } from './themes/duotoneDark.mjs';
+import eclipse, { settings as eclipseSettings } from './themes/eclipse.mjs';
+import githubDark, { settings as githubDarkSettings } from './themes/githubDark.mjs';
+import githubLight, { settings as githubLightSettings } from './themes/githubLight.mjs';
+import gruvboxDark, { settings as gruvboxDarkSettings } from './themes/gruvboxDark.mjs';
+import gruvboxLight, { settings as gruvboxLightSettings } from './themes/gruvboxLight.mjs';
+import materialDark, { settings as materialDarkSettings } from './themes/materialDark.mjs';
+import materialLight, { settings as materialLightSettings } from './themes/materialLight.mjs';
+import nord, { settings as nordSettings } from './themes/nord.mjs';
+import monokai, { settings as monokaiSettings } from './themes/monokai.mjs';
+import solarizedDark, { settings as solarizedDarkSettings } from './themes/solarizedDark.mjs';
+import solarizedLight, { settings as solarizedLightSettings } from './themes/solarizedLight.mjs';
+import sublime, { settings as sublimeSettings } from './themes/sublime.mjs';
+import tokyoNight, { settings as tokyoNightSettings } from './themes/tokyoNight.mjs';
+import tokyoNightStorm, { settings as tokyoNightStormSettings } from './themes/tokioNightStorm.mjs';
+import tokyoNightDay, { settings as tokyoNightDaySettings } from './themes/tokyoNightDay.mjs';
+import vscodeDark, { settings as vscodeDarkSettings } from './themes/vscodeDark.mjs';
+import vscodeLight, { settings as vscodeLightSettings } from './themes/vscodeLight.mjs';
+import xcodeLight, { settings as xcodeLightSettings } from './themes/xcodeLight.mjs';
+import bbedit, { settings as bbeditSettings } from './themes/bbedit.mjs';
+import noctisLilac, { settings as noctisLilacSettings } from './themes/noctisLilac.mjs';
+
+import { setTheme } from '@strudel/draw';
+export const themes = {
+ strudelTheme,
+ algoboy,
+ archBtw,
+ androidstudio,
+ atomone,
+ aura,
+ bbedit,
+ blackscreen,
+ bluescreen,
+ bluescreenlight,
+ CutiePi,
+ darcula,
+ dracula,
+ duotoneDark,
+ eclipse,
+ fruitDaw,
+ githubDark,
+ githubLight,
+ greenText,
+ gruvboxDark,
+ gruvboxLight,
+ sonicPink,
+ materialDark,
+ materialLight,
+ monokai,
+ noctisLilac,
+ nord,
+ redText,
+ solarizedDark,
+ solarizedLight,
+ sublime,
+ teletext,
+ tokyoNight,
+ tokyoNightDay,
+ tokyoNightStorm,
+ vscodeDark,
+ vscodeLight,
+ whitescreen,
+ xcodeLight,
+};
+
+export const settings = {
+ strudelTheme: strudelThemeSettings,
+ bluescreen: bluescreenSettings,
+ bluescreenlight: bluescreenlightsettings,
+ blackscreen: blackscreenSettings,
+ whitescreen: whitescreenSettings,
+ teletext: teletextSettings,
+ algoboy: algoboySettings,
+ archBtw: archBtwSettings,
+ androidstudio: androidstudioSettings,
+ atomone: atomOneSettings,
+ aura: auraSettings,
+ bbedit: bbeditSettings,
+ darcula: darculaSettings,
+ dracula: draculaSettings,
+ duotoneDark: duotoneDarkSettings,
+ eclipse: eclipseSettings,
+ CutiePi: CutiePiSettings,
+ sonicPink: sonicPinkSettings,
+ fruitDaw: fruitDawSettings,
+ githubLight: githubLightSettings,
+ githubDark: githubDarkSettings,
+ greenText: greenTextSettings,
+
+ gruvboxDark: gruvboxDarkSettings,
+ gruvboxLight: gruvboxLightSettings,
+ materialDark: materialDarkSettings,
+ materialLight: materialLightSettings,
+ noctisLilac: noctisLilacSettings,
+ nord: nordSettings,
+ monokai: monokaiSettings,
+ redText: redTextSettings,
+ solarizedLight: solarizedLightSettings,
+ solarizedDark: solarizedDarkSettings,
+ sublime: sublimeSettings,
+ tokyoNight: tokyoNightSettings,
+ tokyoNightStorm: tokyoNightStormSettings,
+ vscodeDark: vscodeDarkSettings,
+ vscodeLight: vscodeLightSettings,
+ xcodeLight: xcodeLightSettings,
+ tokyoNightDay: tokyoNightDaySettings,
+};
+
+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 const theme = (theme) => themes[theme] || themes.strudelTheme;
+
+// css style injection helpers
+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);
+}
+
+let currentTheme,
+ resetThemeStyle,
+ themeStyle,
+ styleID = 'strudel-theme-vars';
+export function initTheme(theme) {
+ if (!document.getElementById(styleID)) {
+ themeStyle = document.createElement('style');
+ themeStyle.id = styleID;
+ document.head.append(themeStyle);
+ }
+ activateTheme(theme);
+}
+
+export function activateTheme(name) {
+ if (currentTheme === name) {
+ return;
+ }
+ currentTheme = name;
+ if (!settings[name]) {
+ console.warn('theme', name, 'has no settings.. defaulting to strudelTheme settings');
+ }
+ const themeSettings = settings[name] || settings.strudelTheme;
+ // set css variables
+ themeStyle.innerHTML = `:root {
+ ${Object.entries(themeSettings)
+ // important to override fallback
+ .map(([key, value]) => `--${key}: ${value} !important;`)
+ .join('\n')}
+ }`;
+ setTheme(themeSettings);
+ // tailwind dark mode
+ if (themeSettings.light) {
+ document.documentElement.classList.remove('dark');
+ } else {
+ document.documentElement.classList.add('dark');
+ }
+ resetThemeStyle?.();
+ resetThemeStyle = undefined;
+ if (themeSettings.customStyle) {
+ resetThemeStyle = injectStyle(themeSettings.customStyle);
+ }
+}
diff --git a/src/strudel/codemirror/themes/CutiePi.mjs b/src/strudel/codemirror/themes/CutiePi.mjs
new file mode 100644
index 0000000..64b38ea
--- /dev/null
+++ b/src/strudel/codemirror/themes/CutiePi.mjs
@@ -0,0 +1,45 @@
+/*
+ * Cutie Pi
+ * by Switch Angel
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+const deepPurple = '#5c019a';
+const yellowPink = '#fbeffc';
+const grey = '#272C35';
+const pinkAccent = '#fee1ff';
+const lightGrey = '#465063';
+const bratGreen = '#9acd3f';
+const lighterGrey = '#97a1b7';
+const pink = '#f6a6fd';
+
+export const settings = {
+ background: 'white',
+ lineBackground: 'transparent',
+ foreground: deepPurple,
+ caret: '#797977',
+ selection: yellowPink,
+ selectionMatch: '#2B323D',
+ gutterBackground: grey,
+ gutterForeground: lightGrey,
+ gutterBorder: 'transparent',
+ lineHighlight: pinkAccent,
+};
+
+export default createTheme({
+ theme: 'light',
+ settings,
+ styles: [
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
+ color: deepPurple,
+ },
+ { tag: [t.tagName, t.heading], color: settings.foreground },
+ { tag: t.comment, color: lighterGrey },
+ { tag: [t.variableName, t.propertyName, t.labelName], color: pink },
+ { tag: [t.attributeName, t.number], color: '#d19a66' },
+ { tag: t.className, color: grey },
+ { tag: t.keyword, color: deepPurple },
+ { tag: [t.string, t.regexp, t.special(t.propertyName)], color: bratGreen },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/algoboy.mjs b/src/strudel/codemirror/themes/algoboy.mjs
new file mode 100644
index 0000000..049ed59
--- /dev/null
+++ b/src/strudel/codemirror/themes/algoboy.mjs
@@ -0,0 +1,61 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+const palettes = {
+ // https://www.deviantart.com/advancedfan2020/art/Game-Boy-Palette-Set-Color-HEX-Part-09-920495662
+ 'Central Florida A': ['#FFF630', '#B3AC22', '#666213', '#191905'],
+ 'Central Florida B': ['#38CEBA', '#279082', '#16524A', '#061513'],
+ 'Central Florida C': ['#FF8836', '#B35F26', '#663616', '#190E05'],
+ 'Central Florida D': ['#E07070', '#9D4E4E', '#5A2D2D', '#160B0B'],
+ 'Central Florida E': ['#7AA4CB', '#55738E', '#314251', '#0C1014'],
+ 'Feminine Energy A': ['#DC5686', '#9A415E', '#582536', '#16090D'],
+ 'Feminine Energy B': ['#D0463C', '#92312A', '#531c18', '#150706'],
+ 'Feminine Energy C': ['#D86918', '#974A11', '#562A0A', '#160A02'],
+ 'Feminine Energy D': ['#EFC54F', '#A78A36', '#604F20', '#181408'],
+ 'Feminine Energy E': ['#866399', '#5e456b', '#36283d', '#0d0a0f'],
+ 'Sour Watermelon A': ['#993366', '#6B2447', '#3D1429', '#0F050A'],
+ 'Sour Watermelon B': ['#996666', '#6B4747', '#3D2929', '#0F0A0A'],
+ 'Sour Watermelon C': ['#999966', '#686B47', '#3d3d29', '#0f0f0A'],
+ 'Sour Watermelon D': ['#99cc66', '#6b8f47', '#3d5229', '#0f140a'],
+ 'Sour Watermelon E': ['#99ff66', '#6bb347', '#3d6629', '#0f190a'],
+ //https://www.deviantart.com/advancedfan2020/art/Game-Boy-Palette-Set-Color-HEX-Part-02-920073260
+ 'Peri Peaceful A': ['#909BE9', '#656DA3', '#3A3E5D', '#0e0f17'],
+ 'Peri Peaceful B': ['#68628d', '#494563', '#2a2738', '#0a0a0e'], // pretty dim
+ 'Peri Peaceful E': ['#b5a0a9', '#7f7076', '#484044', '#121011'],
+ 'Hichem Palette B': ['#4fa3a5', '#377273', '#204142', '#081010'],
+ 'Hichem Palette C': ['#Fe6f9b', '#b24e6d', '#662c3e', '#190b0f'],
+ 'Hichem Palette D': ['#ffbb5a', '#b3833f', '#664b24', '#191309'],
+ 'JSR2 A': ['#E0EFC0', '#9da786', '#5a604d', '#161813'],
+};
+const palette = palettes['Sour Watermelon B'];
+export const settings = {
+ background: palette[3],
+ foreground: palette[1],
+ caret: palette[0],
+ selection: palette[0],
+ selectionMatch: palette[1],
+ lineHighlight: palette[3],
+ lineBackground: palette[3] + '90',
+ //lineBackground: 'transparent',
+ gutterBackground: 'transparent',
+ gutterForeground: palette[0],
+ light: false,
+ // customStyle: '.cm-line { line-height: 1 }',
+};
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ { tag: t.comment, color: palette[2] },
+ { tag: t.string, color: palette[1] },
+ { tag: [t.atom, t.number], color: palette[1] },
+ { tag: [t.meta, t.labelName, t.variableName], color: palette[0] },
+ {
+ tag: [t.keyword, t.tagName, t.arithmeticOperator],
+ color: palette[1],
+ },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: palette[0] },
+ { tag: [t.function(t.variableName), t.propertyName], color: palette[0] },
+ { tag: t.atom, color: palette[1] },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/androidstudio.mjs b/src/strudel/codemirror/themes/androidstudio.mjs
new file mode 100644
index 0000000..77c16c3
--- /dev/null
+++ b/src/strudel/codemirror/themes/androidstudio.mjs
@@ -0,0 +1,43 @@
+/*
+ * androidstudio
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#282b2e',
+ lineBackground: '#282b2e99',
+ foreground: '#a9b7c6',
+ caret: '#00FF00',
+ selection: '#343739',
+ selectionMatch: '#343739',
+ lineHighlight: '#343739',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#282b2e',
+ foreground: '#a9b7c6',
+ caret: '#00FF00',
+ selection: '#4e5254',
+ selectionMatch: '#4e5254',
+ gutterForeground: '#cccccc50',
+ lineHighlight: '#7f85891f',
+ },
+ styles: [
+ { tag: t.labelName, color: 'inherit' },
+ { tag: [t.keyword, t.deleted, t.className], color: '#a9b7c6' },
+ { tag: [t.number, t.literal], color: '#6897bb' },
+ //{ tag: [t.link, t.variableName], color: '#629755' },
+ { tag: [t.link, t.variableName], color: '#a9b7c6' },
+ { tag: [t.comment, t.quote], color: 'grey' },
+ { tag: [t.meta, t.documentMeta], color: '#bbb529' },
+ //{ tag: [t.string, t.propertyName, t.attributeValue], color: '#6a8759' },
+ { tag: [t.propertyName, t.attributeValue], color: '#a9b7c6' },
+ { tag: [t.string], color: '#6a8759' },
+ { tag: [t.heading, t.typeName], color: '#ffc66d' },
+ { tag: [t.attributeName], color: '#a9b7c6' },
+ { tag: [t.emphasis], fontStyle: 'italic' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/archBtw.mjs b/src/strudel/codemirror/themes/archBtw.mjs
new file mode 100644
index 0000000..8754656
--- /dev/null
+++ b/src/strudel/codemirror/themes/archBtw.mjs
@@ -0,0 +1,38 @@
+/*
+ * Arch Btw
+ * Modern terminal inspired theme
+ * made by Jade
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+const hex = ['rgb(0, 0, 0)', 'rgb(82, 208, 250)', 'rgba(113, 208, 250, .4)', 'rgba(113, 208, 250, .15)'];
+
+export const settings = {
+ background: hex[0],
+ lineBackground: 'transparent',
+ foreground: hex[1],
+ selection: hex[2],
+ selectionMatch: hex[0],
+ gutterBackground: hex[0],
+ gutterForeground: hex[2],
+ gutterBorder: 'transparent',
+ lineHighlight: hex[0],
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
+ color: hex[1],
+ },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
+ { tag: [t.comment, t.brace, t.bracket], color: hex[2] },
+ { tag: [t.variableName, t.propertyName, t.labelName], color: hex[1] },
+ { tag: [t.attributeName, t.number], color: hex[1] },
+ { tag: t.keyword, color: hex[1] },
+ { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/atomone.mjs b/src/strudel/codemirror/themes/atomone.mjs
new file mode 100644
index 0000000..ab9cd36
--- /dev/null
+++ b/src/strudel/codemirror/themes/atomone.mjs
@@ -0,0 +1,51 @@
+/*
+ * Atom One
+ * Atom One dark syntax theme
+ *
+ * https://github.com/atom/one-dark-syntax
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#272C35',
+ lineBackground: '#272C3599',
+ foreground: 'hsl(220, 14%, 71%)',
+ caret: '#797977',
+ selection: '#ffffff30',
+ selectionMatch: '#2B323D',
+ gutterBackground: '#272C35',
+ gutterForeground: '#465063',
+ gutterBorder: 'transparent',
+ lineHighlight: '#2B323D',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#272C35',
+ foreground: '#9d9b97',
+ caret: '#797977',
+ selection: '#3d4c64',
+ selectionMatch: '#3d4c64',
+ gutterBackground: '#272C35',
+ gutterForeground: '#465063',
+ gutterBorder: 'transparent',
+ lineHighlight: '#2e3f5940',
+ },
+ styles: [
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
+ color: 'hsl(207, 82%, 66%)',
+ },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: 'hsl( 29, 54%, 61%)' },
+ { tag: [t.tagName, t.heading], color: '#e06c75' },
+ { tag: t.comment, color: '#54636D' },
+ { tag: [t.variableName, t.propertyName, t.labelName], color: 'hsl(220, 14%, 71%)' },
+ { tag: [t.attributeName, t.number], color: 'hsl( 29, 54%, 61%)' },
+ { tag: t.className, color: 'hsl( 39, 67%, 69%)' },
+ { tag: t.keyword, color: 'hsl(286, 60%, 67%)' },
+
+ { tag: [t.string, t.regexp, t.special(t.propertyName)], color: '#98c379' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/aura.mjs b/src/strudel/codemirror/themes/aura.mjs
new file mode 100644
index 0000000..b42a583
--- /dev/null
+++ b/src/strudel/codemirror/themes/aura.mjs
@@ -0,0 +1,51 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#21202e',
+ lineBackground: '#21202e99',
+ foreground: '#edecee',
+ caret: '#a277ff',
+ selection: '#3d375e7f',
+ selectionMatch: '#3d375e7f',
+ gutterBackground: '#21202e',
+ gutterForeground: '#edecee',
+ gutterBorder: 'transparent',
+ lineHighlight: '#a394f033',
+};
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#21202e',
+ foreground: '#edecee',
+ caret: '#a277ff',
+ selection: '#5a51898f',
+ selectionMatch: '#5a51898f',
+ gutterBackground: '#21202e',
+ gutterForeground: '#edecee',
+ gutterBorder: 'transparent',
+ lineHighlight: '#a394f033',
+ },
+ styles: [
+ { tag: t.keyword, color: '#a277ff' },
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: '#edecee' },
+ { tag: [t.propertyName], color: '#ffca85' },
+ { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#61ffca' },
+ { tag: [t.function(t.variableName), t.labelName], color: '#ffca85' },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#61ffca' },
+ { tag: [t.definition(t.name), t.separator], color: '#edecee' },
+ { tag: [t.className], color: '#82e2ff' },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#61ffca' },
+ { tag: [t.typeName], color: '#82e2ff' },
+ { tag: [t.operator, t.operatorKeyword], color: '#a277ff' },
+ { tag: [t.url, t.escape, t.regexp, t.link], color: '#61ffca' },
+ { tag: [t.meta, t.comment], color: '#6d6d6d' },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.heading, fontWeight: 'bold', color: '#a277ff' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#edecee' },
+ { tag: t.invalid, color: '#ff6767' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/bbedit.mjs b/src/strudel/codemirror/themes/bbedit.mjs
new file mode 100644
index 0000000..535bf9e
--- /dev/null
+++ b/src/strudel/codemirror/themes/bbedit.mjs
@@ -0,0 +1,46 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#FFFFFF',
+ lineBackground: '#FFFFFF99',
+ foreground: '#000000',
+ caret: '#FBAC52',
+ selection: '#FFD420',
+ selectionMatch: '#FFD420',
+ gutterBackground: '#f5f5f5',
+ gutterForeground: '#4D4D4C',
+ gutterBorder: 'transparent',
+ lineHighlight: '#00000012',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#FFFFFF',
+ foreground: '#000000',
+ caret: '#FBAC52',
+ selection: '#FFD420',
+ selectionMatch: '#FFD420',
+ gutterBackground: '#f5f5f5',
+ gutterForeground: '#4D4D4C',
+ gutterBorder: 'transparent',
+ lineHighlight: '#00000012',
+ },
+ styles: [
+ { tag: [t.meta, t.comment], color: '#804000' },
+ { tag: [t.keyword, t.strong], color: '#0000FF' },
+ { tag: [t.number], color: '#FF0080' },
+ { tag: [t.string], color: '#FF0080' },
+ { tag: [t.variableName], color: '#006600' },
+ { tag: [t.escape], color: '#33CC33' },
+ { tag: [t.tagName], color: '#1C02FF' },
+ { tag: [t.heading], color: '#0C07FF' },
+ { tag: [t.quote], color: '#000000' },
+ { tag: [t.list], color: '#B90690' },
+ { tag: [t.documentMeta], color: '#888888' },
+ { tag: [t.function(t.variableName)], color: '#0000A2' },
+ { tag: [t.definition(t.typeName), t.typeName], color: '#6D79DE' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/blackscreen.mjs b/src/strudel/codemirror/themes/blackscreen.mjs
new file mode 100644
index 0000000..2c45df1
--- /dev/null
+++ b/src/strudel/codemirror/themes/blackscreen.mjs
@@ -0,0 +1,39 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+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.labelName, color: 'inherit' },
+ { tag: t.keyword, color: 'inherit' },
+ { tag: t.operator, color: 'inherit' },
+ { tag: t.special(t.variableName), color: 'inherit' },
+ { tag: t.typeName, color: 'inherit' },
+ { tag: t.atom, color: 'inherit' },
+ { tag: t.number, color: 'inherit' },
+ { tag: t.definition(t.variableName), color: 'inherit' },
+ { tag: t.string, color: 'inherit' },
+ { tag: t.special(t.string), color: 'inherit' },
+ { tag: t.comment, color: 'inherit' },
+ { tag: t.variableName, color: 'inherit' },
+ { tag: t.tagName, color: 'inherit' },
+ { tag: t.bracket, color: 'inherit' },
+ { tag: t.meta, color: 'inherit' },
+ { tag: t.attributeName, color: 'inherit' },
+ { tag: t.propertyName, color: 'inherit' },
+ { tag: t.className, color: 'inherit' },
+ { tag: t.invalid, color: 'inherit' },
+ { tag: [t.unit, t.punctuation], color: 'inherit' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/bluescreen.mjs b/src/strudel/codemirror/themes/bluescreen.mjs
new file mode 100644
index 0000000..97d165c
--- /dev/null
+++ b/src/strudel/codemirror/themes/bluescreen.mjs
@@ -0,0 +1,42 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+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.labelName, color: 'inherit' },
+ { tag: t.keyword, color: 'inherit' },
+ { tag: t.operator, color: 'inherit' },
+ { tag: t.special(t.variableName), color: 'inherit' },
+ { tag: t.typeName, color: 'inherit' },
+ { tag: t.atom, color: 'inherit' },
+ { tag: t.number, color: 'inherit' },
+ { tag: t.definition(t.variableName), color: 'inherit' },
+ { tag: t.string, color: 'inherit' },
+ { tag: t.special(t.string), color: 'inherit' },
+ { tag: t.comment, color: 'inherit' },
+ { tag: t.variableName, color: 'inherit' },
+ { tag: t.tagName, color: 'inherit' },
+ { tag: t.bracket, color: 'inherit' },
+ { tag: t.meta, color: 'inherit' },
+ { tag: t.attributeName, color: 'inherit' },
+ { tag: t.propertyName, color: 'inherit' },
+ { tag: t.className, color: 'inherit' },
+ { tag: t.invalid, color: 'inherit' },
+ { tag: [t.unit, t.punctuation], color: 'inherit' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/bluescreenlight.mjs b/src/strudel/codemirror/themes/bluescreenlight.mjs
new file mode 100644
index 0000000..031a3da
--- /dev/null
+++ b/src/strudel/codemirror/themes/bluescreenlight.mjs
@@ -0,0 +1,37 @@
+/*
+ * A lighter blue screen theme
+ * made by Jade
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+const hex = ['rgb(75, 130, 247)', 'rgb(47, 108, 246)', 'rgb(255, 255, 255)', 'rgba(255, 255, 255,.3)'];
+
+export const settings = {
+ background: hex[0],
+ lineBackground: 'transparent',
+ foreground: hex[2],
+ selection: hex[3],
+ selectionMatch: hex[0],
+ gutterBackground: hex[0],
+ gutterForeground: hex[2],
+ gutterBorder: 'transparent',
+ lineHighlight: hex[1],
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
+ color: hex[2],
+ },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[2] },
+ { tag: [t.comment, t.bracket, t.brace, t.compareOperator], color: hex[3] },
+ { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
+ { tag: [t.attributeName, t.number], color: hex[2] },
+ { tag: t.keyword, color: hex[2] },
+ { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/darcula.mjs b/src/strudel/codemirror/themes/darcula.mjs
new file mode 100644
index 0000000..ead66e6
--- /dev/null
+++ b/src/strudel/codemirror/themes/darcula.mjs
@@ -0,0 +1,47 @@
+/*
+ * darcula
+ * Name: IntelliJ IDEA darcula theme
+ * From IntelliJ IDEA by JetBrains
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+export const settings = {
+ background: '#242424',
+ lineBackground: '#24242499',
+ 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)',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#242424',
+ foreground: '#f8f8f2',
+ caret: '#FFFFFF',
+ selection: 'rgba(255, 255, 255, 0.1)',
+ selectionMatch: 'rgba(255, 255, 255, 0.2)',
+ gutterBackground: 'transparent',
+ gutterForeground: '#999',
+ gutterBorder: 'transparent',
+ lineHighlight: 'rgba(255, 255, 255, 0.1)',
+ },
+ styles: [
+ { tag: t.labelName, color: '#CCCCCC' },
+ { tag: [t.atom, t.number], color: '#7A9EC2' },
+ { tag: [t.comment], color: '#707070' },
+ { tag: [t.string], color: '#6A8759' },
+ { tag: [t.variableName, t.operator], color: '#CCCCCC' },
+ { tag: [t.function(t.variableName), t.propertyName], color: '#FFC66D' },
+ { tag: [t.meta, t.className], color: '#FFC66D' },
+ { tag: [t.propertyName], color: '#FFC66D' },
+ { tag: [t.keyword], color: '#CC7832' },
+ { tag: [t.tagName], color: '#ff79c6' },
+ { tag: [t.typeName], color: '#ffb86c' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/dracula.mjs b/src/strudel/codemirror/themes/dracula.mjs
new file mode 100644
index 0000000..bdd9617
--- /dev/null
+++ b/src/strudel/codemirror/themes/dracula.mjs
@@ -0,0 +1,50 @@
+/*
+ * @name dracula
+ * Michael Kaminsky (http://github.com/mkaminsky11)
+ * Original dracula color scheme by Zeno Rocha (https://github.com/zenorocha/dracula-theme)
+ */
+// this is different from https://thememirror.net/dracula
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ 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: '#6272a4',
+ gutterBorder: 'transparent',
+ lineHighlight: 'rgba(255, 255, 255, 0.1)',
+};
+
+const purple = '#BD93F9';
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#282a36',
+ foreground: '#f8f8f2',
+ caret: '#f8f8f0',
+ selection: 'rgba(255, 255, 255, 0.1)',
+ selectionMatch: 'rgba(255, 255, 255, 0.2)',
+ gutterBackground: '#282a36',
+ gutterForeground: '#6272a4',
+ gutterBorder: 'transparent',
+ lineHighlight: 'rgba(255, 255, 255, 0.1)',
+ },
+ styles: [
+ { tag: t.comment, color: '#6272a4' },
+ { tag: t.string, color: '#f1fa8c' },
+ { tag: [t.atom, t.number], color: purple },
+ { tag: [t.meta, t.labelName, t.variableName], color: '#f8f8f2' },
+ {
+ tag: [t.keyword, t.tagName, t.arithmeticOperator],
+ color: '#ff79c6',
+ },
+ { tag: [t.function(t.variableName), t.propertyName], color: '#50fa7b' },
+ { tag: t.atom, color: '#bd93f9' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/duotoneDark.mjs b/src/strudel/codemirror/themes/duotoneDark.mjs
new file mode 100644
index 0000000..f54973e
--- /dev/null
+++ b/src/strudel/codemirror/themes/duotoneDark.mjs
@@ -0,0 +1,42 @@
+/*
+ * duotone
+ * author Bram de Haan
+ * by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#2a2734',
+ lineBackground: '#2a273499',
+ foreground: '#eeebff',
+ caret: '#ffad5c',
+ selection: 'rgba(255, 255, 255, 0.1)',
+ gutterBackground: '#2a2734',
+ gutterForeground: '#545167',
+ lineHighlight: '#36334280',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#2a2734',
+ foreground: '#6c6783',
+ caret: '#ffad5c',
+ selection: '#9a86fd',
+ selectionMatch: '#9a86fd',
+ gutterBackground: '#2a2734',
+ gutterForeground: '#545167',
+ lineHighlight: '#36334280',
+ },
+ styles: [
+ { tag: [t.comment, t.bracket, t.operator], color: '#6c6783' },
+ { tag: [t.atom, t.number, t.keyword, t.link, t.attributeName, t.quote], color: '#ffcc99' },
+ { tag: [t.emphasis, t.heading, t.tagName, t.propertyName, t.className, t.variableName], color: '#eeebff' },
+ { tag: [t.typeName, t.url], color: '#eeebff' },
+ { tag: t.string, color: '#ffb870' },
+ /* { tag: [t.propertyName], color: '#9a86fd' }, */
+ { tag: [t.propertyName], color: '#eeebff' },
+ { tag: t.labelName, color: '#eeebff' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/duotoneLight.mjs b/src/strudel/codemirror/themes/duotoneLight.mjs
new file mode 100644
index 0000000..81aa348
--- /dev/null
+++ b/src/strudel/codemirror/themes/duotoneLight.mjs
@@ -0,0 +1,45 @@
+/*
+ * duotone
+ * author Bram de Haan
+ * by Bram de Haan, adapted from DuoTone themes by Simurai (http://simurai.com/projects/2016/01/01/duotone-themes)
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#faf8f5',
+ lineBackground: '#faf8f599',
+ foreground: '#b29762',
+ caret: '#93abdc',
+ selection: '#e3dcce',
+ selectionMatch: '#e3dcce',
+ gutterBackground: '#faf8f5',
+ gutterForeground: '#cdc4b1',
+ gutterBorder: 'transparent',
+ lineHighlight: '#EFEFEF',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#faf8f5',
+ foreground: '#b29762',
+ caret: '#93abdc',
+ selection: '#e3dcce',
+ selectionMatch: '#e3dcce',
+ gutterBackground: '#faf8f5',
+ gutterForeground: '#cdc4b1',
+ gutterBorder: 'transparent',
+ lineHighlight: '#ddceb154',
+ },
+ styles: [
+ { tag: [t.comment, t.bracket], color: '#b6ad9a' },
+ { tag: [t.atom, t.number, t.keyword, t.link, t.attributeName, t.quote], color: '#063289' },
+ { tag: [t.emphasis, t.heading, t.tagName, t.propertyName, t.variableName], color: '#2d2006' },
+ { tag: [t.typeName, t.url, t.string], color: '#896724' },
+ { tag: [t.operator, t.string], color: '#1659df' },
+ { tag: [t.propertyName], color: '#b29762' },
+ { tag: [t.unit, t.punctuation], color: '#063289' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/eclipse.mjs b/src/strudel/codemirror/themes/eclipse.mjs
new file mode 100644
index 0000000..8082b59
--- /dev/null
+++ b/src/strudel/codemirror/themes/eclipse.mjs
@@ -0,0 +1,46 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#fff',
+ lineBackground: '#ffffff99',
+ foreground: '#000',
+ caret: '#FFFFFF',
+ selection: '#d7d4f0',
+ selectionMatch: '#d7d4f0',
+ gutterBackground: '#f7f7f7',
+ gutterForeground: '#999',
+ lineHighlight: '#e8f2ff',
+ gutterBorder: 'transparent',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#fff',
+ foreground: '#000',
+ caret: '#FFFFFF',
+ selection: '#d7d4f0',
+ selectionMatch: '#d7d4f0',
+ gutterBackground: '#f7f7f7',
+ gutterForeground: '#999',
+ lineHighlight: '#006fff1c',
+ gutterBorder: 'transparent',
+ },
+ styles: [
+ { tag: [t.comment], color: '#3F7F5F' },
+ { tag: [t.documentMeta], color: '#FF1717' },
+ { tag: t.keyword, color: '#7F0055', fontWeight: 'bold' },
+ { tag: t.atom, color: '#00f' },
+ { tag: t.number, color: '#164' },
+ { tag: t.propertyName, color: '#164' },
+ { tag: [t.variableName, t.definition(t.variableName)], color: '#0000C0' },
+ { tag: t.function(t.variableName), color: '#0000C0' },
+ { tag: t.string, color: '#2A00FF' },
+ { tag: t.operator, color: 'black' },
+ { tag: t.tagName, color: '#170' },
+ { tag: t.attributeName, color: '#00c' },
+ { tag: t.link, color: '#219' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/fruitDaw.mjs b/src/strudel/codemirror/themes/fruitDaw.mjs
new file mode 100644
index 0000000..c9e5577
--- /dev/null
+++ b/src/strudel/codemirror/themes/fruitDaw.mjs
@@ -0,0 +1,50 @@
+/*
+ * Fruit Daw
+ * made by Jade
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+const hex = [
+ 'rgb(84, 93, 98)',
+ 'rgb(255, 255, 255)',
+ 'rgba(255, 255, 255, .25)',
+ 'rgb(67, 76, 81)',
+ 'rgb(186, 230, 115)',
+ 'rgb(252, 184, 67)',
+ 'rgb(124, 206, 254)',
+ 'rgb(83, 101, 102)',
+ 'rgba(46, 62, 72,.5)',
+ 'rgb(94, 100, 108)',
+ 'rgb(167, 216, 177)',
+];
+
+export const settings = {
+ background: hex[0],
+ lineBackground: 'transparent',
+ foreground: hex[10],
+ selection: hex[8],
+ selectionMatch: hex[0],
+ gutterBackground: hex[3],
+ gutterForeground: hex[2],
+ gutterBorder: 'transparent',
+ lineHighlight: hex[3],
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
+ color: hex[1],
+ },
+ { tag: [t.bool, t.special(t.variableName)], color: hex[1] },
+ { tag: [t.comment, t.brace, t.bracket], color: hex[2] },
+ { tag: [t.variableName], color: hex[1] },
+ { tag: [t.labelName, t.propertyName, t.self, t.atom], color: hex[5] },
+ { tag: [t.attributeName, t.number], color: hex[6] },
+ { tag: t.keyword, color: hex[5] },
+ { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[4] },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/githubDark.mjs b/src/strudel/codemirror/themes/githubDark.mjs
new file mode 100644
index 0000000..477e7d7
--- /dev/null
+++ b/src/strudel/codemirror/themes/githubDark.mjs
@@ -0,0 +1,42 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#0d1117',
+ lineBackground: '#0d111799',
+ foreground: '#c9d1d9',
+ caret: '#c9d1d9',
+ selection: '#003d73',
+ selectionMatch: '#003d73',
+ lineHighlight: '#36334280',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#0d1117',
+ foreground: '#c9d1d9',
+ caret: '#c9d1d9',
+ selection: '#003d73',
+ selectionMatch: '#003d73',
+ lineHighlight: '#36334280',
+ },
+ styles: [
+ { tag: t.labelName, color: '#d2a8ff' },
+ { tag: [t.standard(t.tagName), t.tagName], color: '#7ee787' },
+ { tag: [t.comment, t.bracket], color: '#8b949e' },
+ { tag: [t.className, t.propertyName], color: '#d2a8ff' },
+ { tag: [t.variableName, t.attributeName], color: '#d2a8ff' },
+ { tag: [t.number, t.operator], color: '#79c0ff' },
+ { tag: [t.keyword, t.typeName, t.typeOperator, t.typeName], color: '#ff7b72' },
+ { tag: [t.string, t.meta, t.regexp], color: '#a5d6ff' },
+ { tag: [t.name, t.quote], color: '#7ee787' },
+ { tag: [t.heading, t.strong], color: '#d2a8ff', fontWeight: 'bold' },
+ { tag: [t.emphasis], color: '#d2a8ff', fontStyle: 'italic' },
+ { tag: [t.deleted], color: '#ffdcd7', backgroundColor: 'ffeef0' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#ffab70' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ { tag: t.invalid, color: '#f97583' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/githubLight.mjs b/src/strudel/codemirror/themes/githubLight.mjs
new file mode 100644
index 0000000..2ccec93
--- /dev/null
+++ b/src/strudel/codemirror/themes/githubLight.mjs
@@ -0,0 +1,42 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#fff',
+ lineBackground: '#ffffff99',
+ foreground: '#24292e',
+ selection: '#BBDFFF',
+ selectionMatch: '#BBDFFF',
+ gutterBackground: '#fff',
+ gutterForeground: '#6e7781',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#fff',
+ foreground: '#24292e',
+ selection: '#BBDFFF',
+ selectionMatch: '#BBDFFF',
+ gutterBackground: '#fff',
+ gutterForeground: '#6e7781',
+ },
+ styles: [
+ { tag: [t.standard(t.tagName), t.tagName], color: '#116329' },
+ { tag: [t.comment, t.bracket], color: '#6a737d' },
+ { tag: [t.className, t.propertyName], color: '#6f42c1' },
+ { tag: [t.variableName, t.attributeName, t.number, t.operator], color: '#005cc5' },
+ { tag: [t.keyword, t.typeName, t.typeOperator, t.typeName], color: '#d73a49' },
+ { tag: [t.string, t.meta, t.regexp], color: '#032f62' },
+ { tag: [t.name, t.quote], color: '#22863a' },
+ { tag: [t.heading, t.strong], color: '#24292e', fontWeight: 'bold' },
+ { tag: [t.emphasis], color: '#24292e', fontStyle: 'italic' },
+ { tag: [t.deleted], color: '#b31d28', backgroundColor: 'ffeef0' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#e36209' },
+ { tag: [t.url, t.escape, t.regexp, t.link], color: '#032f62' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ { tag: t.invalid, color: '#cb2431' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/green-text.mjs b/src/strudel/codemirror/themes/green-text.mjs
new file mode 100644
index 0000000..08c7a4b
--- /dev/null
+++ b/src/strudel/codemirror/themes/green-text.mjs
@@ -0,0 +1,39 @@
+/*
+ * Atom One
+ * Atom One dark syntax theme
+ *
+ * https://github.com/atom/one-dark-syntax
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+const hex = ['#000000', '#8ed675', '#56bd2a', '#54636D', '#171717'];
+
+export const settings = {
+ background: hex[0],
+ lineBackground: 'transparent',
+ foreground: hex[2],
+ selection: hex[4],
+ selectionMatch: hex[0],
+ gutterBackground: hex[0],
+ gutterForeground: hex[3],
+ gutterBorder: 'transparent',
+ lineHighlight: hex[0],
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
+ color: hex[2],
+ },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
+ { tag: t.comment, color: hex[3] },
+ { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
+ { tag: [t.attributeName, t.number], color: hex[1] },
+ { tag: t.keyword, color: hex[2] },
+ { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/gruvboxDark.mjs b/src/strudel/codemirror/themes/gruvboxDark.mjs
new file mode 100644
index 0000000..67d8bff
--- /dev/null
+++ b/src/strudel/codemirror/themes/gruvboxDark.mjs
@@ -0,0 +1,82 @@
+/*
+ * gruvbox-dark
+ * author morhetz
+ * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-dark.css
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#282828',
+ lineBackground: '#28282899',
+ foreground: '#ebdbb2',
+ caret: '#ebdbb2',
+ selection: '#bdae93',
+ selectionMatch: '#bdae93',
+ lineHighlight: '#3c3836',
+ gutterBackground: '#282828',
+ gutterForeground: '#7c6f64',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#282828',
+ foreground: '#ebdbb2',
+ caret: '#ebdbb2',
+ selection: '#b99d555c',
+ selectionMatch: '#b99d555c',
+ lineHighlight: '#baa1602b',
+ gutterBackground: '#282828',
+ gutterForeground: '#7c6f64',
+ },
+ styles: [
+ { tag: t.keyword, color: '#fb4934' },
+ { tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: '#8ec07c' },
+ { tag: [t.variableName], color: '#83a598' },
+ { tag: [t.function(t.variableName)], color: '#8ec07c', fontStyle: 'bold' },
+ { tag: [t.labelName], color: '#ebdbb2' },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#d3869b' },
+ { tag: [t.definition(t.name), t.separator], color: '#ebdbb2' },
+ { tag: [t.brace], color: '#ebdbb2' },
+ { tag: [t.annotation], color: '#fb4934d' },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#d3869b' },
+ { tag: [t.typeName, t.className], color: '#fabd2f' },
+ { tag: [t.operatorKeyword], color: '#fb4934' },
+ {
+ tag: [t.tagName],
+ color: '#8ec07c',
+ fontStyle: 'bold',
+ },
+ { tag: [t.squareBracket], color: '#fe8019' },
+ { tag: [t.angleBracket], color: '#83a598' },
+ { tag: [t.attributeName], color: '#8ec07c' },
+ { tag: [t.regexp], color: '#8ec07c' },
+ { tag: [t.quote], color: '#928374' },
+ { tag: [t.string], color: '#ebdbb2' },
+ {
+ tag: t.link,
+ color: '#a89984',
+ textDecoration: 'underline',
+ textUnderlinePosition: 'under',
+ },
+ { tag: [t.url, t.escape, t.special(t.string)], color: '#d3869b' },
+ { tag: [t.meta], color: '#fabd2f' },
+ { tag: [t.comment], color: '#928374', fontStyle: 'italic' },
+ { tag: t.strong, fontWeight: 'bold', color: '#fe8019' },
+ { tag: t.emphasis, fontStyle: 'italic', color: '#b8bb26' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ { tag: t.heading, fontWeight: 'bold', color: '#b8bb26' },
+ { tag: [t.heading1, t.heading2], fontWeight: 'bold', color: '#b8bb26' },
+ {
+ tag: [t.heading3, t.heading4],
+ fontWeight: 'bold',
+ color: '#fabd2f',
+ },
+ { tag: [t.heading5, t.heading6], color: '#fabd2f' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#d3869b' },
+ { tag: [t.processingInstruction, t.inserted], color: '#83a598' },
+ { tag: [t.contentSeparator], color: '#fb4934' },
+ { tag: t.invalid, color: '#fe8019', borderBottom: `1px dotted #fb4934d` },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/gruvboxLight.mjs b/src/strudel/codemirror/themes/gruvboxLight.mjs
new file mode 100644
index 0000000..5d4b4ec
--- /dev/null
+++ b/src/strudel/codemirror/themes/gruvboxLight.mjs
@@ -0,0 +1,130 @@
+/*
+ * gruvbox-light
+ * author morhetz
+ * From github.com/codemirror/codemirror5/blob/master/theme/gruvbox-light.css
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#fbf1c7',
+ lineBackground: '#fbf1c799',
+ foreground: '#3c3836',
+ caret: '#af3a03',
+ selection: '#ebdbb2',
+ selectionMatch: '#bdae93',
+ lineHighlight: '#ebdbb2',
+ gutterBackground: '#ebdbb2',
+ gutterForeground: '#665c54',
+ gutterBorder: 'transparent',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#fbf1c7',
+ foreground: '#3c3836',
+ caret: '#af3a03',
+ selection: '#bdae9391',
+ selectionMatch: '#bdae9391',
+ lineHighlight: '#a37f2238',
+ gutterBackground: '#ebdbb2',
+ gutterForeground: '#665c54',
+ gutterBorder: 'transparent',
+ },
+ styles: [
+ { tag: t.keyword, color: '#9d0006' },
+ {
+ tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName],
+ color: '#427b58',
+ },
+ { tag: [t.variableName], color: '#076678' },
+ { tag: [t.function(t.variableName)], color: '#79740e', fontStyle: 'bold' },
+ { tag: [t.labelName], color: '#3c3836' },
+ {
+ tag: [t.color, t.constant(t.name), t.standard(t.name)],
+ color: '#8f3f71',
+ },
+ { tag: [t.definition(t.name), t.separator], color: '#3c3836' },
+ { tag: [t.brace], color: '#3c3836' },
+ {
+ tag: [t.annotation],
+ color: '#9d0006',
+ },
+ {
+ tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace],
+ color: '#8f3f71',
+ },
+ {
+ tag: [t.typeName, t.className],
+ color: '#b57614',
+ },
+ {
+ tag: [t.operator, t.operatorKeyword],
+ color: '#9d0006',
+ },
+ {
+ tag: [t.tagName],
+ color: '#427b58',
+ fontStyle: 'bold',
+ },
+ {
+ tag: [t.squareBracket],
+ color: '#af3a03',
+ },
+ {
+ tag: [t.angleBracket],
+ color: '#076678',
+ },
+ {
+ tag: [t.attributeName],
+ color: '#427b58',
+ },
+ {
+ tag: [t.regexp],
+ color: '#427b58',
+ },
+ {
+ tag: [t.quote],
+ color: '#928374',
+ },
+ { tag: [t.string], color: '#3c3836' },
+ {
+ tag: t.link,
+ color: '#7c6f64',
+ textDecoration: 'underline',
+ textUnderlinePosition: 'under',
+ },
+ {
+ tag: [t.url, t.escape, t.special(t.string)],
+ color: '#8f3f71',
+ },
+ { tag: [t.meta], color: '#b57614' },
+ { tag: [t.comment], color: '#928374', fontStyle: 'italic' },
+ { tag: t.strong, fontWeight: 'bold', color: '#af3a03' },
+ { tag: t.emphasis, fontStyle: 'italic', color: '#79740e' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ { tag: t.heading, fontWeight: 'bold', color: '#79740e' },
+ { tag: [t.heading1, t.heading2], fontWeight: 'bold', color: '#79740e' },
+ {
+ tag: [t.heading3, t.heading4],
+ fontWeight: 'bold',
+ color: '#b57614',
+ },
+ {
+ tag: [t.heading5, t.heading6],
+ color: '#b57614',
+ },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#8f3f71' },
+ {
+ tag: [t.processingInstruction, t.inserted],
+ color: '#076678',
+ },
+ {
+ tag: [t.contentSeparator],
+ color: '#9d0006',
+ },
+ { tag: t.invalid, color: '#af3a03', borderBottom: `1px dotted #9d0006` },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/materialDark.mjs b/src/strudel/codemirror/themes/materialDark.mjs
new file mode 100644
index 0000000..f134128
--- /dev/null
+++ b/src/strudel/codemirror/themes/materialDark.mjs
@@ -0,0 +1,77 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#212121',
+ lineBackground: '#21212199',
+ foreground: '#bdbdbd',
+ caret: '#a0a4ae',
+ selection: '#d7d4f0',
+ selectionMatch: '#d7d4f0',
+ gutterBackground: '#212121',
+ gutterForeground: '#999',
+ gutterActiveForeground: '#4f5b66',
+ lineHighlight: '#111111',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#212121',
+ foreground: '#bdbdbd',
+ caret: '#a0a4ae',
+ selection: '#d7d4f063',
+ selectionMatch: '#d7d4f063',
+ gutterBackground: '#212121',
+ gutterForeground: '#999',
+ gutterActiveForeground: '#4f5b66',
+ lineHighlight: '#333333',
+ },
+ styles: [
+ { tag: t.keyword, color: '#cf6edf' },
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: '#56c8d8' },
+ { tag: [t.propertyName], color: '#82AAFF' },
+ { tag: [t.variableName], color: '#bdbdbd' },
+ { tag: [t.function(t.variableName)], color: '#82AAFF' },
+ { tag: [t.labelName], color: '#cf6edf' },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#facf4e' },
+ { tag: [t.definition(t.name), t.separator], color: '#56c8d8' },
+ { tag: [t.brace], color: '#cf6edf' },
+ { tag: [t.annotation], color: '#f07178' },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#f07178' },
+ { tag: [t.typeName, t.className], color: '#f07178' },
+ { tag: [t.operator, t.operatorKeyword], color: '#82AAFF' },
+ { tag: [t.tagName], color: '#99d066' },
+ { tag: [t.squareBracket], color: '#f07178' },
+ { tag: [t.angleBracket], color: '#606f7a' },
+ { tag: [t.attributeName], color: '#bdbdbd' },
+ { tag: [t.regexp], color: '#f07178' },
+ { tag: [t.quote], color: '#6abf69' },
+ { tag: [t.string], color: '#99d066' },
+ {
+ tag: t.link,
+ color: '#56c8d8',
+ textDecoration: 'underline',
+ textUnderlinePosition: 'under',
+ },
+ { tag: [t.url, t.escape, t.special(t.string)], color: '#facf4e' },
+ { tag: [t.meta], color: '#707d8b' },
+ { tag: [t.comment], color: '#707d8b', fontStyle: 'italic' },
+ { tag: t.monospace, color: '#bdbdbd' },
+ { tag: t.strong, fontWeight: 'bold', color: '#f07178' },
+ { tag: t.emphasis, fontStyle: 'italic', color: '#99d066' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ { tag: t.heading, fontWeight: 'bold', color: '#facf4e' },
+ { tag: t.heading1, fontWeight: 'bold', color: '#facf4e' },
+ {
+ tag: [t.heading2, t.heading3, t.heading4],
+ fontWeight: 'bold',
+ color: '#facf4e',
+ },
+ { tag: [t.heading5, t.heading6], color: '#facf4e' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#56c8d8' },
+ { tag: [t.processingInstruction, t.inserted], color: '#f07178' },
+ { tag: [t.contentSeparator], color: '#56c8d8' },
+ { tag: t.invalid, color: '#606f7a', borderBottom: `1px dotted #f07178` },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/materialLight.mjs b/src/strudel/codemirror/themes/materialLight.mjs
new file mode 100644
index 0000000..d2598c6
--- /dev/null
+++ b/src/strudel/codemirror/themes/materialLight.mjs
@@ -0,0 +1,52 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#FAFAFA',
+ lineBackground: '#FAFAFA99',
+ foreground: '#90A4AE',
+ caret: '#272727',
+ selection: '#80CBC440',
+ selectionMatch: '#FAFAFA',
+ gutterBackground: '#FAFAFA',
+ gutterForeground: '#90A4AE',
+ gutterBorder: 'transparent',
+ lineHighlight: '#CCD7DA50',
+};
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#FAFAFA',
+ foreground: '#90A4AE',
+ caret: '#272727',
+ selection: '#80CBC440',
+ selectionMatch: '#80CBC440',
+ gutterBackground: '#FAFAFA',
+ gutterForeground: '#90A4AE',
+ gutterBorder: 'transparent',
+ lineHighlight: '#CCD7DA50',
+ },
+ styles: [
+ { tag: t.keyword, color: '#39ADB5' },
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: '#90A4AE' },
+ { tag: [t.propertyName], color: '#6182B8' },
+ { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#91B859' },
+ { tag: [t.function(t.variableName), t.labelName], color: '#6182B8' },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#39ADB5' },
+ { tag: [t.definition(t.name), t.separator], color: '#90A4AE' },
+ { tag: [t.className], color: '#E2931D' },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#F76D47' },
+ { tag: [t.typeName], color: '#E2931D', fontStyle: '#E2931D' },
+ { tag: [t.operator, t.operatorKeyword], color: '#39ADB5' },
+ { tag: [t.url, t.escape, t.regexp, t.link], color: '#91B859' },
+ { tag: [t.meta, t.comment], color: '#90A4AE' },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.heading, fontWeight: 'bold', color: '#39ADB5' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#90A4AE' },
+ { tag: t.invalid, color: '#E5393570' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/monokai.mjs b/src/strudel/codemirror/themes/monokai.mjs
new file mode 100644
index 0000000..294b946
--- /dev/null
+++ b/src/strudel/codemirror/themes/monokai.mjs
@@ -0,0 +1,45 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#272822',
+ lineBackground: '#27282299',
+ foreground: '#FFFFFF',
+ caret: '#FFFFFF',
+ selection: '#49483E',
+ selectionMatch: '#49483E',
+ gutterBackground: '#272822',
+ gutterForeground: '#FFFFFF70',
+ lineHighlight: '#00000059',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#272822',
+ foreground: '#FFFFFF',
+ caret: '#FFFFFF',
+ selection: '#49483E',
+ selectionMatch: '#49483E',
+ gutterBackground: '#272822',
+ gutterForeground: '#FFFFFF70',
+ lineHighlight: '#0000003b',
+ },
+ styles: [
+ { tag: t.labelName, color: '#bababa' },
+ { tag: [t.comment, t.documentMeta], color: '#8292a2' },
+ { tag: [t.number, t.bool, t.null, t.atom], color: '#ae81ff' },
+ { tag: [t.attributeValue, t.className, t.name], color: '#e6db74' },
+ { tag: [t.propertyName, t.attributeName], color: '#a6e22e' },
+ { tag: [t.variableName], color: '#9effff' },
+ { tag: [t.squareBracket], color: '#bababa' },
+ { tag: [t.string, t.special(t.brace)], color: '#e6db74' },
+ { tag: [t.regexp, t.className, t.typeName, t.definition(t.typeName)], color: '#66d9ef' },
+ {
+ tag: [t.definition(t.variableName), t.definition(t.propertyName), t.function(t.variableName)],
+ color: '#a6e22e',
+ },
+ // { tag: t.keyword, color: '#f92672' },
+ { tag: [t.keyword, t.definitionKeyword, t.modifier, t.tagName, t.angleBracket], color: '#f92672' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/noctisLilac.mjs b/src/strudel/codemirror/themes/noctisLilac.mjs
new file mode 100644
index 0000000..b11c6b4
--- /dev/null
+++ b/src/strudel/codemirror/themes/noctisLilac.mjs
@@ -0,0 +1,50 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#f2f1f8',
+ lineBackground: '#f2f1f899',
+ foreground: '#0c006b',
+ caret: '#5c49e9',
+ selection: '#d5d1f2',
+ selectionMatch: '#d5d1f2',
+ gutterBackground: '#f2f1f8',
+ gutterForeground: '#0c006b70',
+ lineHighlight: '#e1def3',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#f2f1f8',
+ foreground: '#0c006b',
+ caret: '#5c49e9',
+ selection: '#d5d1f2',
+ selectionMatch: '#d5d1f2',
+ gutterBackground: '#f2f1f8',
+ gutterForeground: '#0c006b70',
+ lineHighlight: '#16067911',
+ },
+ styles: [
+ { tag: t.comment, color: '#9995b7' },
+ {
+ tag: t.keyword,
+ color: '#ff5792',
+ fontWeight: 'bold',
+ },
+ { tag: [t.definitionKeyword, t.modifier], color: '#ff5792' },
+ { tag: [t.className, t.tagName, t.definition(t.typeName)], color: '#0094f0' },
+ { tag: [t.number, t.bool, t.null, t.special(t.brace)], color: '#5842ff' },
+ { tag: [t.definition(t.propertyName), t.function(t.variableName)], color: '#0095a8' },
+ { tag: t.typeName, color: '#b3694d' },
+ { tag: [t.propertyName, t.variableName], color: '#fa8900' },
+ { tag: t.operator, color: '#ff5792' },
+ { tag: t.self, color: '#e64100' },
+ { tag: [t.string, t.regexp], color: '#00b368' },
+ { tag: [t.paren, t.bracket], color: '#0431fa' },
+ { tag: t.labelName, color: '#00bdd6' },
+ { tag: t.attributeName, color: '#e64100' },
+ { tag: t.angleBracket, color: '#9995b7' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/nord.mjs b/src/strudel/codemirror/themes/nord.mjs
new file mode 100644
index 0000000..ce96bca
--- /dev/null
+++ b/src/strudel/codemirror/themes/nord.mjs
@@ -0,0 +1,78 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#2e3440',
+ lineBackground: '#2e344099',
+ foreground: '#FFFFFF',
+ caret: '#FFFFFF',
+ selection: '#3b4252',
+ selectionMatch: '#e5e9f0',
+ gutterBackground: '#2e3440',
+ gutterForeground: '#4c566a',
+ gutterActiveForeground: '#d8dee9',
+ lineHighlight: '#4c566a',
+};
+
+// Colors from https://www.nordtheme.com/docs/colors-and-palettes
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#2e3440',
+ foreground: '#FFFFFF',
+ caret: '#FFFFFF',
+ selection: '#00000073',
+ selectionMatch: '#00000073',
+ gutterBackground: '#2e3440',
+ gutterForeground: '#4c566a',
+ gutterActiveForeground: '#d8dee9',
+ lineHighlight: '#4c566a29',
+ },
+ styles: [
+ { tag: t.keyword, color: '#5e81ac' },
+ { tag: [t.name, t.deleted, t.character, t.propertyName, t.macroName], color: '#88c0d0' },
+ { tag: [t.variableName], color: '#8fbcbb' },
+ { tag: [t.function(t.variableName)], color: '#8fbcbb' },
+ { tag: [t.labelName], color: '#81a1c1' },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#5e81ac' },
+ { tag: [t.definition(t.name), t.separator], color: '#a3be8c' },
+ { tag: [t.brace], color: '#8fbcbb' },
+ { tag: [t.annotation], color: '#d30102' },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#b48ead' },
+ { tag: [t.typeName, t.className], color: '#ebcb8b' },
+ { tag: [t.operator, t.operatorKeyword], color: '#a3be8c' },
+ { tag: [t.tagName], color: '#b48ead' },
+ { tag: [t.squareBracket], color: '#bf616a' },
+ { tag: [t.angleBracket], color: '#d08770' },
+ { tag: [t.attributeName], color: '#ebcb8b' },
+ { tag: [t.regexp], color: '#5e81ac' },
+ { tag: [t.quote], color: '#b48ead' },
+ { tag: [t.string], color: '#a3be8c' },
+ {
+ tag: t.link,
+ color: '#a3be8c',
+ textDecoration: 'underline',
+ textUnderlinePosition: 'under',
+ },
+ { tag: [t.url, t.escape, t.special(t.string)], color: '#8fbcbb' },
+ { tag: [t.meta], color: '#88c0d0' },
+ { tag: [t.monospace], color: '#d8dee9', fontStyle: 'italic' },
+ { tag: [t.comment], color: '#4c566a', fontStyle: 'italic' },
+ { tag: t.strong, fontWeight: 'bold', color: '#5e81ac' },
+ { tag: t.emphasis, fontStyle: 'italic', color: '#5e81ac' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ { tag: t.heading, fontWeight: 'bold', color: '#5e81ac' },
+ { tag: t.special(t.heading1), fontWeight: 'bold', color: '#5e81ac' },
+ { tag: t.heading1, fontWeight: 'bold', color: '#5e81ac' },
+ {
+ tag: [t.heading2, t.heading3, t.heading4],
+ fontWeight: 'bold',
+ color: '#5e81ac',
+ },
+ { tag: [t.heading5, t.heading6], color: '#5e81ac' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#d08770' },
+ { tag: [t.processingInstruction, t.inserted], color: '#8fbcbb' },
+ { tag: [t.contentSeparator], color: '#ebcb8b' },
+ { tag: t.invalid, color: '#434c5e', borderBottom: `1px dotted #d30102` },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/red-text.mjs b/src/strudel/codemirror/themes/red-text.mjs
new file mode 100644
index 0000000..44dda5d
--- /dev/null
+++ b/src/strudel/codemirror/themes/red-text.mjs
@@ -0,0 +1,39 @@
+/*
+ * Atom One
+ * Atom One dark syntax theme
+ *
+ * https://github.com/atom/one-dark-syntax
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+const hex = ['#000000', '#ff5356', '#bd312a', '#54636D', '#171717'];
+
+export const settings = {
+ background: hex[0],
+ lineBackground: 'transparent',
+ foreground: hex[2],
+ selection: hex[4],
+ selectionMatch: hex[0],
+ gutterBackground: hex[0],
+ gutterForeground: hex[3],
+ gutterBorder: 'transparent',
+ lineHighlight: hex[0],
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
+ color: hex[2],
+ },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[1] },
+ { tag: t.comment, color: hex[3] },
+ { tag: [t.variableName, t.propertyName, t.labelName], color: hex[2] },
+ { tag: [t.attributeName, t.number], color: hex[1] },
+ { tag: t.keyword, color: hex[2] },
+ { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[1] },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/solarizedDark.mjs b/src/strudel/codemirror/themes/solarizedDark.mjs
new file mode 100644
index 0000000..c6b645f
--- /dev/null
+++ b/src/strudel/codemirror/themes/solarizedDark.mjs
@@ -0,0 +1,79 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#002b36',
+ lineBackground: '#002b3699',
+ foreground: '#93a1a1',
+ caret: '#839496',
+ selection: '#173541',
+ selectionMatch: '#aafe661a',
+ gutterBackground: '#00252f',
+ gutterForeground: '#839496',
+ lineHighlight: '#173541',
+};
+
+const c = {
+ background: '#002B36',
+ foreground: '#839496',
+ selection: '#004454AA',
+ selectionMatch: '#005A6FAA',
+ cursor: '#D30102',
+ dropdownBackground: '#00212B',
+ dropdownBorder: '#2AA19899',
+ activeLine: '#00cafe11',
+ matchingBracket: '#073642',
+ keyword: '#859900',
+ storage: '#93A1A1',
+ variable: '#268BD2',
+ parameter: '#268BD2',
+ function: '#268BD2',
+ string: '#2AA198',
+ constant: '#CB4B16',
+ type: '#859900',
+ class: '#268BD2',
+ number: '#D33682',
+ comment: '#586E75',
+ heading: '#268BD2',
+ invalid: '#DC322F',
+ regexp: '#DC322F',
+ tag: '#268BD2',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: c.background,
+ foreground: c.foreground,
+ caret: c.cursor,
+ selection: c.selection,
+ selectionMatch: c.selection,
+ gutterBackground: c.background,
+ gutterForeground: c.foreground,
+ gutterBorder: 'transparent',
+ lineHighlight: c.activeLine,
+ },
+ styles: [
+ { tag: t.keyword, color: c.keyword },
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: c.variable },
+ { tag: [t.propertyName], color: c.function },
+ { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: c.string },
+ { tag: [t.function(t.variableName), t.labelName], color: c.function },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: c.constant },
+ { tag: [t.definition(t.name), t.separator], color: c.variable },
+ { tag: [t.className], color: c.class },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: c.number },
+ { tag: [t.typeName], color: c.type, fontStyle: c.type },
+ { tag: [t.operator, t.operatorKeyword], color: c.keyword },
+ { tag: [t.url, t.escape, t.regexp, t.link], color: c.regexp },
+ { tag: [t.meta, t.comment], color: c.comment },
+ { tag: t.tagName, color: c.tag },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.heading, fontWeight: 'bold', color: c.heading },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: c.variable },
+ { tag: t.invalid, color: c.invalid },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/solarizedLight.mjs b/src/strudel/codemirror/themes/solarizedLight.mjs
new file mode 100644
index 0000000..b6828ff
--- /dev/null
+++ b/src/strudel/codemirror/themes/solarizedLight.mjs
@@ -0,0 +1,82 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+// this is slightly different from https://thememirror.net/solarized-light
+
+export const settings = {
+ light: true,
+ background: '#fdf6e3',
+ lineBackground: '#fdf6e399',
+ foreground: '#657b83',
+ caret: '#586e75',
+ selection: '#dfd9c8',
+ selectionMatch: '#dfd9c8',
+ gutterBackground: '#00000010',
+ gutterForeground: '#657b83',
+ lineHighlight: '#dfd9c8',
+};
+
+const c = {
+ background: '#FDF6E3',
+ foreground: '#657B83',
+ selection: '#EEE8D5',
+ selectionMatch: '#EEE8D5',
+ cursor: '#657B83',
+ dropdownBackground: '#EEE8D5',
+ dropdownBorder: '#D3AF86',
+ activeLine: '#3d392d11',
+ matchingBracket: '#EEE8D5',
+ keyword: '#859900',
+ storage: '#586E75',
+ variable: '#268BD2',
+ parameter: '#268BD2',
+ function: '#268BD2',
+ string: '#2AA198',
+ constant: '#CB4B16',
+ type: '#859900',
+ class: '#268BD2',
+ number: '#D33682',
+ comment: '#93A1A1',
+ heading: '#268BD2',
+ invalid: '#DC322F',
+ regexp: '#DC322F',
+ tag: '#268BD2',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: c.background,
+ foreground: c.foreground,
+ caret: c.cursor,
+ selection: c.selection,
+ selectionMatch: c.selectionMatch,
+ gutterBackground: c.background,
+ gutterForeground: c.foreground,
+ gutterBorder: 'transparent',
+ lineHighlight: c.activeLine,
+ },
+ styles: [
+ { tag: t.keyword, color: c.keyword },
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: c.variable },
+ { tag: [t.propertyName], color: c.function },
+ { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: c.string },
+ { tag: [t.function(t.variableName), t.labelName], color: c.function },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: c.constant },
+ { tag: [t.definition(t.name), t.separator], color: c.variable },
+ { tag: [t.className], color: c.class },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: c.number },
+ { tag: [t.typeName], color: c.type, fontStyle: c.type },
+ { tag: [t.operator, t.operatorKeyword], color: c.keyword },
+ { tag: [t.url, t.escape, t.regexp, t.link], color: c.regexp },
+ { tag: [t.meta, t.comment], color: c.comment },
+ { tag: t.tagName, color: c.tag },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.heading, fontWeight: 'bold', color: c.heading },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: c.variable },
+ { tag: t.invalid, color: c.invalid },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/sonic-pink.mjs b/src/strudel/codemirror/themes/sonic-pink.mjs
new file mode 100644
index 0000000..5a19555
--- /dev/null
+++ b/src/strudel/codemirror/themes/sonic-pink.mjs
@@ -0,0 +1,39 @@
+/*
+ * Atom One
+ * Atom One dark syntax theme
+ *
+ * https://github.com/atom/one-dark-syntax
+ */
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+const hex = ['#1e1e1e', '#fbde2d', '#ff1493', '#4c83ff', '#ededed', '#cccccc', '#ffffff30', '#dc2f8c'];
+
+export const settings = {
+ background: '#000000',
+ lineBackground: 'transparent',
+ foreground: hex[4],
+ selection: hex[6],
+ gutterBackground: hex[0],
+ gutterForeground: hex[5],
+ gutterBorder: 'transparent',
+ lineHighlight: hex[0],
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ {
+ tag: [t.function(t.variableName), t.function(t.propertyName), t.url, t.processingInstruction],
+ color: hex[4],
+ },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: hex[3] },
+
+ { tag: t.comment, color: '#54636D' },
+ { tag: [t.variableName, t.propertyName, t.labelName], color: hex[4] },
+ { tag: [t.attributeName, t.number], color: hex[3] },
+ { tag: t.keyword, color: hex[1] },
+ { tag: [t.string, t.regexp, t.special(t.propertyName)], color: hex[2] },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/strudel-theme.mjs b/src/strudel/codemirror/themes/strudel-theme.mjs
new file mode 100644
index 0000000..7c4a3a9
--- /dev/null
+++ b/src/strudel/codemirror/themes/strudel-theme.mjs
@@ -0,0 +1,49 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#222',
+ lineBackground: '#22222299',
+ foreground: '#fff',
+ caret: '#ffcc00',
+ selection: 'rgba(128, 203, 196, 0.5)',
+ selectionMatch: '#036dd626',
+ lineHighlight: '#00000050',
+ gutterBackground: 'transparent',
+ gutterForeground: '#8a919966',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings,
+ styles: [
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#89ddff' },
+ { tag: t.labelName, color: '#89ddff' },
+ { 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/src/strudel/codemirror/themes/sublime.mjs b/src/strudel/codemirror/themes/sublime.mjs
new file mode 100644
index 0000000..068b9ba
--- /dev/null
+++ b/src/strudel/codemirror/themes/sublime.mjs
@@ -0,0 +1,43 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#303841',
+ lineBackground: '#30384199',
+ foreground: '#FFFFFF',
+ caret: '#FBAC52',
+ selection: '#4C5964',
+ selectionMatch: '#3A546E',
+ gutterBackground: '#303841',
+ gutterForeground: '#FFFFFF70',
+ lineHighlight: '#00000059',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#303841',
+ foreground: '#FFFFFF',
+ caret: '#FBAC52',
+ selection: '#4C5964',
+ selectionMatch: '#3A546E',
+ gutterBackground: '#303841',
+ gutterForeground: '#FFFFFF70',
+ lineHighlight: '#00000059',
+ },
+ styles: [
+ { tag: t.labelName, color: '#A2A9B5' },
+ { tag: [t.meta, t.comment], color: '#A2A9B5' },
+ { tag: [t.attributeName, t.keyword], color: '#B78FBA' },
+ { tag: t.function(t.variableName), color: '#5AB0B0' },
+ { tag: [t.string, t.regexp, t.attributeValue], color: '#99C592' },
+ { tag: t.operator, color: '#f47954' },
+ // { tag: t.moduleKeyword, color: 'red' },
+ { tag: [t.tagName, t.modifier], color: '#E35F63' },
+ { tag: [t.number, t.definition(t.tagName), t.className, t.definition(t.variableName)], color: '#fbac52' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#E35F63' },
+ { tag: t.variableName, color: '#539ac4' },
+ { tag: [t.propertyName, t.typeName], color: '#629ccd' },
+ { tag: t.propertyName, color: '#36b7b5' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/teletext.mjs b/src/strudel/codemirror/themes/teletext.mjs
new file mode 100644
index 0000000..7857cb1
--- /dev/null
+++ b/src/strudel/codemirror/themes/teletext.mjs
@@ -0,0 +1,51 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+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.labelName, color: colorB },
+ { 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/src/strudel/codemirror/themes/terminal.mjs b/src/strudel/codemirror/themes/terminal.mjs
new file mode 100644
index 0000000..b07bdb8
--- /dev/null
+++ b/src/strudel/codemirror/themes/terminal.mjs
@@ -0,0 +1,37 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+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.labelName, color: 'inherit' },
+ { tag: t.keyword, color: 'inherit' },
+ { tag: t.operator, color: 'inherit' },
+ { tag: t.special(t.variableName), color: 'inherit' },
+ { tag: t.typeName, color: 'inherit' },
+ { tag: t.atom, color: 'inherit' },
+ { tag: t.number, color: 'inherit' },
+ { tag: t.definition(t.variableName), color: 'inherit' },
+ { tag: t.string, color: 'inherit' },
+ { tag: t.special(t.string), color: 'inherit' },
+ { tag: t.comment, color: 'inherit' },
+ { tag: t.variableName, color: 'inherit' },
+ { tag: t.tagName, color: 'inherit' },
+ { tag: t.bracket, color: 'inherit' },
+ { tag: t.meta, color: 'inherit' },
+ { tag: t.attributeName, color: 'inherit' },
+ { tag: t.propertyName, color: 'inherit' },
+ { tag: t.className, color: 'inherit' },
+ { tag: t.invalid, color: 'inherit' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/theme-helper.mjs b/src/strudel/codemirror/themes/theme-helper.mjs
new file mode 100644
index 0000000..ee9bad8
--- /dev/null
+++ b/src/strudel/codemirror/themes/theme-helper.mjs
@@ -0,0 +1,42 @@
+import { EditorView } from '@codemirror/view';
+import { syntaxHighlighting } from '@codemirror/language';
+import { HighlightStyle } from '@codemirror/language';
+
+export const createTheme = ({ theme, settings, styles }) => {
+ const _theme = EditorView.theme(
+ {
+ '&': {
+ color: settings.foreground,
+ backgroundColor: settings.background,
+ },
+ '.cm-gutters': {
+ backgroundColor: settings.gutterBackground,
+ color: settings.gutterForeground,
+ //borderRightColor: settings.gutterBorder
+ },
+ '.cm-content': {
+ caretColor: settings.caret,
+ },
+ '.cm-cursor, .cm-dropCursor': {
+ borderLeftColor: settings.caret,
+ },
+ '.cm-activeLineGutter': {
+ // color: settings.gutterActiveForeground
+ backgroundColor: settings.lineHighlight,
+ },
+ '.cm-activeLine': {
+ backgroundColor: settings.lineHighlight,
+ },
+ '&.cm-focused .cm-selectionBackground, & .cm-line::selection, & .cm-selectionLayer .cm-selectionBackground, .cm-content ::selection':
+ {
+ background: settings.selection + ' !important',
+ },
+ '& .cm-selectionMatch': {
+ backgroundColor: settings.selectionMatch,
+ },
+ },
+ { dark: theme === 'dark' },
+ );
+ const highlightStyle = HighlightStyle.define(styles);
+ return [_theme, syntaxHighlighting(highlightStyle)];
+};
diff --git a/src/strudel/codemirror/themes/tokioNightStorm.mjs b/src/strudel/codemirror/themes/tokioNightStorm.mjs
new file mode 100644
index 0000000..be973d5
--- /dev/null
+++ b/src/strudel/codemirror/themes/tokioNightStorm.mjs
@@ -0,0 +1,52 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#24283b',
+ lineBackground: '#24283b99',
+ foreground: '#7982a9',
+ caret: '#c0caf5',
+ selection: '#6f7bb630',
+ selectionMatch: '#1f2335',
+ gutterBackground: '#24283b',
+ gutterForeground: '#7982a9',
+ gutterBorder: 'transparent',
+ lineHighlight: '#292e42',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#24283b',
+ foreground: '#7982a9',
+ caret: '#c0caf5',
+ selection: '#6f7bb630',
+ selectionMatch: '#343b5f',
+ gutterBackground: '#24283b',
+ gutterForeground: '#7982a9',
+ gutterBorder: 'transparent',
+ lineHighlight: '#292e427a',
+ },
+ styles: [
+ { tag: t.keyword, color: '#bb9af7' },
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: '#c0caf5' },
+ { tag: [t.propertyName], color: '#7aa2f7' },
+ { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#9ece6a' },
+ { tag: [t.function(t.variableName), t.labelName], color: '#7aa2f7' },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#bb9af7' },
+ { tag: [t.definition(t.name), t.separator], color: '#c0caf5' },
+ { tag: [t.className], color: '#c0caf5' },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#ff9e64' },
+ { tag: [t.typeName], color: '#2ac3de', fontStyle: '#2ac3de' },
+ { tag: [t.operator, t.operatorKeyword], color: '#bb9af7' },
+ { tag: [t.url, t.escape, t.regexp, t.link], color: '#b4f9f8' },
+ { tag: [t.meta, t.comment], color: '#565f89' },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.heading, fontWeight: 'bold', color: '#89ddff' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#c0caf5' },
+ { tag: t.invalid, color: '#ff5370' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/tokyoNight.mjs b/src/strudel/codemirror/themes/tokyoNight.mjs
new file mode 100644
index 0000000..c325b4a
--- /dev/null
+++ b/src/strudel/codemirror/themes/tokyoNight.mjs
@@ -0,0 +1,52 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#1a1b26',
+ lineBackground: '#1a1b2699',
+ foreground: '#787c99',
+ caret: '#c0caf5',
+ selection: '#515c7e40',
+ selectionMatch: '#16161e',
+ gutterBackground: '#1a1b26',
+ gutterForeground: '#787c99',
+ gutterBorder: 'transparent',
+ lineHighlight: '#1e202e',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#1a1b26',
+ foreground: '#787c99',
+ caret: '#c0caf5',
+ selection: '#515c7e40',
+ selectionMatch: '#16161e',
+ gutterBackground: '#1a1b26',
+ gutterForeground: '#787c99',
+ gutterBorder: 'transparent',
+ lineHighlight: '#474b6611',
+ },
+ styles: [
+ { tag: t.keyword, color: '#bb9af7' },
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: '#c0caf5' },
+ { tag: [t.propertyName], color: '#7aa2f7' },
+ { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#9ece6a' },
+ { tag: [t.function(t.variableName), t.labelName], color: '#7aa2f7' },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#bb9af7' },
+ { tag: [t.definition(t.name), t.separator], color: '#c0caf5' },
+ { tag: [t.className], color: '#c0caf5' },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#ff9e64' },
+ { tag: [t.typeName], color: '#0db9d7' },
+ { tag: [t.operator, t.operatorKeyword], color: '#bb9af7' },
+ { tag: [t.url, t.escape, t.regexp, t.link], color: '#b4f9f8' },
+ { tag: [t.meta, t.comment], color: '#444b6a' },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.heading, fontWeight: 'bold', color: '#89ddff' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#c0caf5' },
+ { tag: t.invalid, color: '#ff5370' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/tokyoNightDay.mjs b/src/strudel/codemirror/themes/tokyoNightDay.mjs
new file mode 100644
index 0000000..1cd58a4
--- /dev/null
+++ b/src/strudel/codemirror/themes/tokyoNightDay.mjs
@@ -0,0 +1,53 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#e1e2e7',
+ lineBackground: '#e1e2e799',
+ foreground: '#3760bf',
+ caret: '#3760bf',
+ selection: '#99a7df',
+ selectionMatch: '#99a7df',
+ gutterBackground: '#e1e2e7',
+ gutterForeground: '#3760bf',
+ gutterBorder: 'transparent',
+ lineHighlight: '#5f5faf11',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#e1e2e7',
+ foreground: '#3760bf',
+ caret: '#3760bf',
+ selection: '#99a7df',
+ selectionMatch: '#99a7df',
+ gutterBackground: '#e1e2e7',
+ gutterForeground: '#3760bf',
+ gutterBorder: 'transparent',
+ lineHighlight: '#5f5faf11',
+ },
+ styles: [
+ { tag: t.keyword, color: '#007197' },
+ { tag: [t.name, t.deleted, t.character, t.macroName], color: '#3760bf' },
+ { tag: [t.propertyName], color: '#3760bf' },
+ { tag: [t.processingInstruction, t.string, t.inserted, t.special(t.string)], color: '#587539' },
+ { tag: [t.function(t.variableName), t.labelName], color: '#3760bf' },
+ { tag: [t.color, t.constant(t.name), t.standard(t.name)], color: '#3760bf' },
+ { tag: [t.definition(t.name), t.separator], color: '#3760bf' },
+ { tag: [t.className], color: '#3760bf' },
+ { tag: [t.number, t.changed, t.annotation, t.modifier, t.self, t.namespace], color: '#b15c00' },
+ { tag: [t.typeName], color: '#007197', fontStyle: '#007197' },
+ { tag: [t.operator, t.operatorKeyword], color: '#007197' },
+ { tag: [t.url, t.escape, t.regexp, t.link], color: '#587539' },
+ { tag: [t.meta, t.comment], color: '#848cb5' },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.link, textDecoration: 'underline' },
+ { tag: t.heading, fontWeight: 'bold', color: '#b15c00' },
+ { tag: [t.atom, t.bool, t.special(t.variableName)], color: '#3760bf' },
+ { tag: t.invalid, color: '#f52a65' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/vscodeDark.mjs b/src/strudel/codemirror/themes/vscodeDark.mjs
new file mode 100644
index 0000000..703790f
--- /dev/null
+++ b/src/strudel/codemirror/themes/vscodeDark.mjs
@@ -0,0 +1,80 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#1e1e1e',
+ lineBackground: '#1e1e1e99',
+ foreground: '#fff',
+ caret: '#c6c6c6',
+ selection: '#6199ff2f',
+ selectionMatch: '#72a1ff59',
+ lineHighlight: '#ffffff0f',
+ gutterBackground: '#1e1e1e',
+ gutterForeground: '#838383',
+ gutterActiveForeground: '#fff',
+};
+
+export default createTheme({
+ theme: 'dark',
+ settings: {
+ background: '#1e1e1e',
+ foreground: '#fff',
+ caret: '#c6c6c6',
+ selection: '#6199ff2f',
+ selectionMatch: '#72a1ff59',
+ lineHighlight: '#ffffff0f',
+ gutterBackground: '#1e1e1e',
+ gutterForeground: '#838383',
+ gutterActiveForeground: '#fff',
+ fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
+ },
+ styles: [
+ {
+ tag: [
+ t.keyword,
+ t.operatorKeyword,
+ t.modifier,
+ t.color,
+ t.constant(t.name),
+ t.standard(t.name),
+ t.standard(t.tagName),
+ t.special(t.brace),
+ t.atom,
+ t.bool,
+ t.special(t.variableName),
+ ],
+ color: '#569cd6',
+ },
+ { tag: [t.controlKeyword, t.moduleKeyword], color: '#c586c0' },
+ {
+ tag: [
+ t.name,
+ t.deleted,
+ t.character,
+ t.macroName,
+ t.propertyName,
+ t.variableName,
+ t.labelName,
+ t.definition(t.name),
+ ],
+ color: '#9cdcfe',
+ },
+ { tag: t.heading, fontWeight: 'bold', color: '#9cdcfe' },
+ {
+ tag: [t.typeName, t.className, t.tagName, t.number, t.changed, t.annotation, t.self, t.namespace],
+ color: '#4ec9b0',
+ },
+ { tag: [t.function(t.variableName), t.function(t.propertyName)], color: '#dcdcaa' },
+ { tag: [t.number], color: '#b5cea8' },
+ { tag: [t.operator, t.punctuation, t.separator, t.url, t.escape, t.regexp], color: '#d4d4d4' },
+ { tag: [t.regexp], color: '#d16969' },
+ { tag: [t.special(t.string), t.processingInstruction, t.string, t.inserted], color: '#ce9178' },
+ { tag: [t.angleBracket], color: '#808080' },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ { tag: [t.meta, t.comment], color: '#6a9955' },
+ { tag: t.link, color: '#6a9955', textDecoration: 'underline' },
+ { tag: t.invalid, color: '#ff0000' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/vscodeLight.mjs b/src/strudel/codemirror/themes/vscodeLight.mjs
new file mode 100644
index 0000000..9a73451
--- /dev/null
+++ b/src/strudel/codemirror/themes/vscodeLight.mjs
@@ -0,0 +1,81 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ background: '#ffffff',
+ lineBackground: '#ffffff50',
+ foreground: '#383a42',
+ caret: '#000',
+ selection: '#add6ff',
+ selectionMatch: '#a8ac94',
+ lineHighlight: '#99999926',
+ gutterBackground: '#fff',
+ gutterForeground: '#237893',
+ gutterActiveForeground: '#0b216f',
+ fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#ffffff',
+ foreground: '#383a42',
+ caret: '#000',
+ selection: '#add6ff',
+ selectionMatch: '#a8ac94',
+ lineHighlight: '#99999926',
+ gutterBackground: '#fff',
+ gutterForeground: '#237893',
+ gutterActiveForeground: '#0b216f',
+ fontFamily: 'Menlo, Monaco, Consolas, "Andale Mono", "Ubuntu Mono", "Courier New", monospace',
+ },
+ styles: [
+ {
+ tag: [
+ t.keyword,
+ t.operatorKeyword,
+ t.modifier,
+ t.color,
+ t.constant(t.name),
+ t.standard(t.name),
+ t.standard(t.tagName),
+ t.special(t.brace),
+ t.atom,
+ t.bool,
+ t.special(t.variableName),
+ ],
+ color: '#0000ff',
+ },
+ { tag: [t.moduleKeyword, t.controlKeyword], color: '#af00db' },
+ {
+ tag: [
+ t.name,
+ t.deleted,
+ t.character,
+ t.macroName,
+ t.propertyName,
+ t.variableName,
+ t.labelName,
+ t.definition(t.name),
+ ],
+ color: '#0070c1',
+ },
+ { tag: t.heading, fontWeight: 'bold', color: '#0070c1' },
+ {
+ tag: [t.typeName, t.className, t.tagName, t.number, t.changed, t.annotation, t.self, t.namespace],
+ color: '#267f99',
+ },
+ { tag: [t.function(t.variableName), t.function(t.propertyName)], color: '#795e26' },
+ { tag: [t.number], color: '#098658' },
+ { tag: [t.operator, t.punctuation, t.separator, t.url, t.escape, t.regexp], color: '#383a42' },
+ { tag: [t.regexp], color: '#af00db' },
+ { tag: [t.special(t.string), t.processingInstruction, t.string, t.inserted], color: '#a31515' },
+ { tag: [t.angleBracket], color: '#383a42' },
+ { tag: t.strong, fontWeight: 'bold' },
+ { tag: t.emphasis, fontStyle: 'italic' },
+ { tag: t.strikethrough, textDecoration: 'line-through' },
+ { tag: [t.meta, t.comment], color: '#008000' },
+ { tag: t.link, color: '#4078f2', textDecoration: 'underline' },
+ { tag: t.invalid, color: '#e45649' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/whitescreen.mjs b/src/strudel/codemirror/themes/whitescreen.mjs
new file mode 100644
index 0000000..a2937c3
--- /dev/null
+++ b/src/strudel/codemirror/themes/whitescreen.mjs
@@ -0,0 +1,39 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+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.labelName, color: 'inherit' },
+ { tag: t.keyword, color: 'inherit' },
+ { tag: t.operator, color: 'inherit' },
+ { tag: t.special(t.variableName), color: 'inherit' },
+ { tag: t.typeName, color: 'inherit' },
+ { tag: t.atom, color: 'inherit' },
+ { tag: t.number, color: 'inherit' },
+ { tag: t.definition(t.variableName), color: 'inherit' },
+ { tag: t.string, color: 'inherit' },
+ { tag: t.special(t.string), color: 'inherit' },
+ { tag: t.comment, color: 'inherit' },
+ { tag: t.variableName, color: 'inherit' },
+ { tag: t.tagName, color: 'inherit' },
+ { tag: t.bracket, color: 'inherit' },
+ { tag: t.meta, color: 'inherit' },
+ { tag: t.attributeName, color: 'inherit' },
+ { tag: t.propertyName, color: 'inherit' },
+ { tag: t.className, color: 'inherit' },
+ { tag: t.invalid, color: 'inherit' },
+ ],
+});
diff --git a/src/strudel/codemirror/themes/xcodeLight.mjs b/src/strudel/codemirror/themes/xcodeLight.mjs
new file mode 100644
index 0000000..8977c13
--- /dev/null
+++ b/src/strudel/codemirror/themes/xcodeLight.mjs
@@ -0,0 +1,38 @@
+import { tags as t } from '@lezer/highlight';
+import { createTheme } from './theme-helper.mjs';
+
+export const settings = {
+ light: true,
+ background: '#fff',
+ lineBackground: '#ffffff99',
+ foreground: '#3D3D3D',
+ selection: '#BBDFFF',
+ selectionMatch: '#BBDFFF',
+ gutterBackground: '#fff',
+ gutterForeground: '#AFAFAF',
+ lineHighlight: '#EDF4FF',
+};
+
+export default createTheme({
+ theme: 'light',
+ settings: {
+ background: '#fff',
+ foreground: '#3D3D3D',
+ selection: '#BBDFFF',
+ selectionMatch: '#BBDFFF',
+ gutterBackground: '#fff',
+ gutterForeground: '#AFAFAF',
+ lineHighlight: '#d5e6ff69',
+ },
+ styles: [
+ { tag: [t.comment, t.quote], color: '#707F8D' },
+ { tag: [t.typeName, t.typeOperator], color: '#aa0d91' },
+ { tag: [t.keyword], color: '#aa0d91', fontWeight: 'bold' },
+ { tag: [t.string, t.meta], color: '#D23423' },
+ { tag: [t.name], color: '#032f62' },
+ { tag: [t.typeName], color: '#522BB2' },
+ { tag: [t.variableName], color: '#23575C' },
+ { tag: [t.definition(t.variableName)], color: '#327A9E' },
+ { tag: [t.regexp, t.link], color: '#0e0eff' },
+ ],
+});
diff --git a/src/strudel/codemirror/tooltip.mjs b/src/strudel/codemirror/tooltip.mjs
new file mode 100644
index 0000000..d1d0479
--- /dev/null
+++ b/src/strudel/codemirror/tooltip.mjs
@@ -0,0 +1,79 @@
+import { hoverTooltip } from '@codemirror/view';
+import jsdoc from '../../doc.json';
+import { Autocomplete, getSynonymDoc } from './autocomplete.mjs';
+
+const getDocLabel = (doc) => doc.name || doc.longname;
+
+let ctrlDown = false;
+
+if (typeof window !== 'undefined') {
+ // Record Control key event to trigger or block the tooltip depending on the state
+ window.addEventListener(
+ 'keyup',
+ function (e) {
+ if (e.key == 'Control') {
+ ctrlDown = false;
+ }
+ },
+ true,
+ );
+
+ window.addEventListener(
+ 'keydown',
+ function (e) {
+ if (e.key == 'Control') {
+ ctrlDown = true;
+ }
+ },
+ true,
+ );
+}
+
+export const strudelTooltip = hoverTooltip(
+ (view, pos, side) => {
+ // Word selection from CodeMirror Hover Tooltip example https://codemirror.net/examples/tooltip/#hover-tooltips
+ if (!ctrlDown) {
+ return null;
+ }
+ let { from, to, text } = view.state.doc.lineAt(pos);
+ let start = pos,
+ end = pos;
+ while (start > from && /\w/.test(text[start - from - 1])) {
+ start--;
+ }
+ while (end < to && /\w/.test(text[end - from])) {
+ end++;
+ }
+ if ((start == pos && side < 0) || (end == pos && side > 0)) {
+ return null;
+ }
+ let word = text.slice(start - from, end - from);
+ // Get entry from Strudel documentation
+ let entry = jsdoc.docs.filter((doc) => getDocLabel(doc) === word)[0];
+ if (!entry) {
+ // Try for synonyms
+ const doc = jsdoc.docs.filter((doc) => doc.synonyms && doc.synonyms.includes(word))[0];
+ if (!doc) {
+ return null;
+ }
+ entry = getSynonymDoc(doc, word);
+ }
+
+ return {
+ pos: start,
+ end,
+ above: false,
+ arrow: true,
+ create(view) {
+ let dom = document.createElement('div');
+ dom.className = 'strudel-tooltip';
+ const ac = Autocomplete(entry);
+ dom.appendChild(ac);
+ return { dom };
+ },
+ };
+ },
+ { hoverTime: 10 },
+);
+
+export const isTooltipEnabled = (on) => (on ? strudelTooltip : []);
diff --git a/src/strudel/codemirror/vite.config.js b/src/strudel/codemirror/vite.config.js
new file mode 100644
index 0000000..5df3edc
--- /dev/null
+++ b/src/strudel/codemirror/vite.config.js
@@ -0,0 +1,19 @@
+import { defineConfig } from 'vite';
+import { dependencies } from './package.json';
+import { resolve } from 'path';
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [],
+ build: {
+ lib: {
+ entry: resolve(__dirname, 'index.mjs'),
+ formats: ['es'],
+ fileName: (ext) => ({ es: 'index.mjs' })[ext],
+ },
+ rollupOptions: {
+ external: [...Object.keys(dependencies)],
+ },
+ target: 'esnext',
+ },
+});
diff --git a/src/strudel/codemirror/widget.mjs b/src/strudel/codemirror/widget.mjs
new file mode 100644
index 0000000..42d3b15
--- /dev/null
+++ b/src/strudel/codemirror/widget.mjs
@@ -0,0 +1,142 @@
+import { StateEffect, StateField } from '@codemirror/state';
+import { Decoration, EditorView, WidgetType } from '@codemirror/view';
+import { getWidgetID, registerWidgetType } from '@strudel/transpiler';
+import { Pattern } from '@strudel/core';
+
+export const addWidget = StateEffect.define({
+ map: ({ from, to }, change) => {
+ return { from: change.mapPos(from), to: change.mapPos(to) };
+ },
+});
+
+export const updateWidgets = (view, widgets) => {
+ view.dispatch({ effects: addWidget.of(widgets) });
+};
+
+function getWidgets(widgetConfigs) {
+ return (
+ widgetConfigs
+ // codemirror throws an error if we don't sort
+ .sort((a, b) => a.to - b.to)
+ .map((widgetConfig) => {
+ return Decoration.widget({
+ widget: new BlockWidget(widgetConfig),
+ side: 0,
+ block: true,
+ }).range(widgetConfig.to);
+ })
+ );
+}
+
+const widgetField = StateField.define(
+ /* */ {
+ create() {
+ return Decoration.none;
+ },
+ update(widgets, tr) {
+ widgets = widgets.map(tr.changes);
+ for (let e of tr.effects) {
+ if (e.is(addWidget)) {
+ try {
+ widgets = widgets.update({
+ filter: () => false,
+ add: getWidgets(e.value),
+ });
+ } catch (error) {
+ console.log('err', error);
+ }
+ }
+ }
+ return widgets;
+ },
+ provide: (f) => EditorView.decorations.from(f),
+ },
+);
+
+const widgetElements = {};
+export function setWidget(id, el) {
+ widgetElements[id] = el;
+ el.id = id;
+}
+
+export class BlockWidget extends WidgetType {
+ constructor(widgetConfig) {
+ super();
+ this.widgetConfig = widgetConfig;
+ }
+ eq() {
+ return true;
+ }
+ toDOM() {
+ const id = getWidgetID(this.widgetConfig);
+ const el = widgetElements[id];
+ return el;
+ }
+ ignoreEvent(e) {
+ return true;
+ }
+}
+
+export const widgetPlugin = [widgetField];
+
+// widget implementer API to create a new widget type
+export function registerWidget(type, fn) {
+ registerWidgetType(type);
+ if (fn) {
+ Pattern.prototype[type] = function (id, options = { fold: 1 }) {
+ // fn is expected to create a dom element and call setWidget(id, el);
+ // fn should also return the pattern
+ return fn(id, options, this);
+ };
+ }
+}
+
+// wire up @strudel/draw functions
+
+function getCanvasWidget(id, options = {}) {
+ const { width = 500, height = 60, pixelRatio = window.devicePixelRatio } = options;
+ let canvas = document.getElementById(id) || document.createElement('canvas');
+ canvas.width = width * pixelRatio;
+ canvas.height = height * pixelRatio;
+ canvas.style.width = width + 'px';
+ canvas.style.height = height + 'px';
+ setWidget(id, canvas);
+ return canvas;
+}
+
+registerWidget('_pianoroll', (id, options = {}, pat) => {
+ const ctx = getCanvasWidget(id, options).getContext('2d');
+ return pat.tag(id).pianoroll({ fold: 1, ...options, ctx, id });
+});
+
+registerWidget('_punchcard', (id, options = {}, pat) => {
+ const ctx = getCanvasWidget(id, options).getContext('2d');
+ return pat.tag(id).punchcard({ fold: 1, ...options, ctx, id });
+});
+
+registerWidget('_spiral', (id, options = {}, pat) => {
+ let _size = options.size || 275;
+ options = { width: _size, height: _size, ...options, size: _size / 5 };
+ const ctx = getCanvasWidget(id, options).getContext('2d');
+ return pat.tag(id).spiral({ ...options, ctx, id });
+});
+
+registerWidget('_scope', (id, options = {}, pat) => {
+ options = { width: 500, height: 60, pos: 0.5, scale: 1, ...options };
+ const ctx = getCanvasWidget(id, options).getContext('2d');
+ return pat.tag(id).scope({ ...options, ctx, id });
+});
+
+registerWidget('_pitchwheel', (id, options = {}, pat) => {
+ let _size = options.size || 200;
+ options = { width: _size, height: _size, ...options, size: _size / 5 };
+ const ctx = getCanvasWidget(id, options).getContext('2d');
+ return pat.pitchwheel({ ...options, ctx, id });
+});
+
+registerWidget('_spectrum', (id, options = {}, pat) => {
+ let _size = options.size || 200;
+ options = { width: _size, height: _size, ...options, size: _size / 5 };
+ const ctx = getCanvasWidget(id, options).getContext('2d');
+ return pat.spectrum({ ...options, ctx, id });
+});