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 + ? ` +
+

Parameters

+ +
+ ` + : ''; + +const buildExamples = (examples) => + examples?.length + ? ` +
+

Examples

+ ${examples + .map( + (example) => ` +
${escapeHtml(example)}
+ `, + ) + .join('')} +
+ ` + : ''; + +export const Autocomplete = (doc) => + h` +
+
+

${getDocLabel(doc)}

+ ${doc.synonyms_text ? `
Synonyms: ${doc.synonyms_text}
` : ''} + ${doc.description ? `
${doc.description}
` : ''} + ${buildParamsList(doc.params)} + ${buildExamples(doc.examples)} +
+
+`[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 }); +});