diff --git a/.gitignore b/.gitignore index 7532adaf0..2be3ee698 100644 --- a/.gitignore +++ b/.gitignore @@ -45,20 +45,6 @@ tidal-drum-machines webaudiofontdata src-tauri/target - - -### BEGIN Visual Studio Code ### -# Blanket, recursive exclude for .vscode directory and files -.vscode/**/* -# Unexclude specific files and directories within .vscode -!.vscode/settings.json -!.vscode/tasks.json -!.vscode/launch.json -!.vscode/extensions.json -### END Visual Studio Code ### - - - # BEGIN JetBrains -> END JetBrains # for JetBrains IDE users, e.g. WebStorm. Source: https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore diff --git a/packages/codemirror/autocomplete.mjs b/packages/codemirror/autocomplete.mjs index 20c128188..69aa3bc59 100644 --- a/packages/codemirror/autocomplete.mjs +++ b/packages/codemirror/autocomplete.mjs @@ -1,11 +1,6 @@ import jsdoc from '../../doc.json'; import { autocompletion } from '@codemirror/autocomplete'; import { h } from './html'; -//TODO: fix tonal scale import -// import { Scale } from '@tonaljs/tonal'; -// import { soundMap } from '@strudel/webaudio'; -let soundMap = undefined; -import { complex } from '@strudel/tonal'; const escapeHtml = (str) => { const div = document.createElement('div'); @@ -80,53 +75,6 @@ const isValidDoc = (doc) => { const hasExcludedTags = (doc) => ['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag)); -export function bankCompletions() { - // TODO: FIX IMPORT - 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 TODO: FIX IMPORT -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); @@ -165,299 +113,19 @@ const jsdocCompletions = (() => { 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; + const word = context.matchBefore(/\w*/); + if (word.from === word.to && !context.explicit) return null; + + return { + from: word.from, + options: jsdocCompletions, + /* options: [ + { label: 'match', type: 'keyword' }, + { label: 'hello', type: 'variable', info: '(World)' }, + { label: 'magic', type: 'text', apply: '⠁⭒*.✩.*⭒⠁', detail: 'macro' }, + ], */ + }; }; export const isAutoCompletionEnabled = (enabled) => diff --git a/packages/codemirror/package.json b/packages/codemirror/package.json index d2432ef83..016184052 100644 --- a/packages/codemirror/package.json +++ b/packages/codemirror/package.json @@ -47,11 +47,8 @@ "codemirror-helix": "^0.5.0", "@strudel/core": "workspace:*", "@strudel/draw": "workspace:*", - "@strudel/tonal": "workspace:*", "@strudel/transpiler": "workspace:*", - "@tonaljs/tonal": "^4.10.0", - "nanostores": "^0.11.3", - "superdough": "workspace:*" + "nanostores": "^0.11.3" }, "devDependencies": { "vite": "^6.0.11" diff --git a/packages/tonal/index.mjs b/packages/tonal/index.mjs index ebbcf0851..a3c0cba39 100644 --- a/packages/tonal/index.mjs +++ b/packages/tonal/index.mjs @@ -1,9 +1,9 @@ import './tonal.mjs'; import './voicings.mjs'; -import './ireal.mjs'; export * from './tonal.mjs'; export * from './voicings.mjs'; -export * from './ireal.mjs'; + +import './ireal.mjs'; export const packageName = '@strudel/tonal'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c4c045c9f..e0a698a6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -242,24 +242,15 @@ importers: '@strudel/draw': specifier: workspace:* version: link:../draw - '@strudel/tonal': - specifier: workspace:* - version: link:../tonal '@strudel/transpiler': specifier: workspace:* version: link:../transpiler - '@tonaljs/tonal': - specifier: ^4.10.0 - version: 4.10.0 codemirror-helix: specifier: ^0.5.0 version: 0.5.0(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/search@6.5.8)(@codemirror/state@6.5.1)(@codemirror/view@6.36.2) nanostores: specifier: ^0.11.3 version: 0.11.3 - superdough: - specifier: workspace:* - version: link:../superdough devDependencies: vite: specifier: ^6.0.11