refactor: modularize autocomplete context handlers using strategy array

This commit is contained in:
drdozer 2025-09-11 18:59:26 +01:00
parent d87ce960d2
commit 0b67473bd3

View file

@ -114,23 +114,18 @@ const jsdocCompletions = jsdoc.docs
type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type type: 'function', // https://codemirror.net/docs/ref/#autocomplete.Completion.type
})); }));
export const strudelAutocomplete = (context) => {
// --- Pitch names for scale key completion --- // --- Handler functions for each context ---
// Ideally, these should be imported from packages/tonal/tonleiter.mjs const pitchNames = [
// but are duplicated here as they are not exported. 'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb',
const pitchNames = [ 'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb'
'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'Fb', 'F', 'F#', 'Gb', ];
'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'Cb'
]; function scalePreColonHandler(context) {
// Block fallback completions inside .scale("..."), even before the colon
let scalePreColonContext = context.matchBefore(/\.scale\(\s*['"][^'"]*$/); let scalePreColonContext = context.matchBefore(/\.scale\(\s*['"][^'"]*$/);
if (scalePreColonContext) { if (scalePreColonContext) {
// Only yield completions if a colon is present (handled below)
if (!scalePreColonContext.text.includes(':')) { if (!scalePreColonContext.text.includes(':')) {
if (context.explicit) { if (context.explicit) {
// Provide pitch completions on explicit request
// (Union of sharps and flats from tonleiter.mjs)
const text = scalePreColonContext.text; const text = scalePreColonContext.text;
const match = text.match(/([A-Ga-g][#b]*)?$/); const match = text.match(/([A-Ga-g][#b]*)?$/);
const fragment = match ? match[0] : ''; const fragment = match ? match[0] : '';
@ -139,19 +134,17 @@ export const strudelAutocomplete = (context) => {
const options = filtered.map((p) => ({ label: p, type: 'pitch' })); const options = filtered.map((p) => ({ label: p, type: 'pitch' }));
return { from, options }; return { from, options };
} else { } else {
// Block fallback completions
return { from: scalePreColonContext.to, options: [] }; return { from: scalePreColonContext.to, options: [] };
} }
} }
// (second block removed, handled above)
if (!scalePreColonContext.text.includes(':')) { if (!scalePreColonContext.text.includes(':')) {
return { from: scalePreColonContext.to, options: [] }; return { from: scalePreColonContext.to, options: [] };
} }
} }
return null;
// 1. Check for sound context: s("..."), sound("...") }
// Trigger after at least one letter is typed after whitespace or bracket inside the quotes
function soundHandler(context) {
let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/); let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/);
if (soundContext) { if (soundContext) {
const text = soundContext.text; const text = soundContext.text;
@ -170,13 +163,13 @@ export const strudelAutocomplete = (context) => {
options, options,
}; };
} }
return null;
}
function bankHandler(context) {
let bankMatch = context.matchBefore(/bank\(\s*(['"])?([\w]*)$/); let bankMatch = context.matchBefore(/bank\(\s*(['"])?([\w]*)$/);
if (bankMatch) { if (bankMatch) {
let banks = bankCompletions(); let banks = bankCompletions();
console.log('[autocomplete] Bank context detected:', bankMatch.text);
console.log('[autocomplete] soundMap keys:', Object.keys(soundMap.get()));
console.log('[autocomplete] bankCompletions:', banks);
// Extract quote and fragment using regex groups on match.text // Extract quote and fragment using regex groups on match.text
const groups = bankMatch.text.match(/(['"])?([\w]*)$/); const groups = bankMatch.text.match(/(['"])?([\w]*)$/);
const quote = groups ? groups[1] : undefined; const quote = groups ? groups[1] : undefined;
@ -205,19 +198,17 @@ export const strudelAutocomplete = (context) => {
options, options,
}; };
} }
return null;
}
// 3. Check for scale context: .scale("..."), only after colon function scaleAfterColonHandler(context) {
let scaleContext = context.matchBefore(/\.scale\(\s*['"][^'"]*:[^'"]*$/); let scaleContext = context.matchBefore(/\.scale\(\s*['"][^'"]*:[^'"]*$/);
if (scaleContext) { if (scaleContext) {
// Find the last colon in the text
const text = scaleContext.text; const text = scaleContext.text;
const colonIdx = text.lastIndexOf(':'); const colonIdx = text.lastIndexOf(':');
if (colonIdx === -1) return null; if (colonIdx === -1) return null;
// Get the fragment after the colon
const fragment = text.slice(colonIdx + 1); const fragment = text.slice(colonIdx + 1);
// Filter scale completions by fragment
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment)); const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
// Insert colon-form (replace spaces with colons) for completions
const options = filteredScales.map((s) => ({ const options = filteredScales.map((s) => ({
...s, ...s,
apply: s.label.replace(/\s+/g, ':') apply: s.label.replace(/\s+/g, ':')
@ -228,19 +219,36 @@ export const strudelAutocomplete = (context) => {
options, options,
}; };
} }
return null;
}
// fallback: original logic function fallbackHandler(context) {
const word = context.matchBefore(/\w*/); const word = context.matchBefore(/\w*/);
if (word && word.from === word.to && !context.explicit) return null; if (word && word.from === word.to && !context.explicit) return null;
if (word) { if (word) {
console.log('[autocomplete] Default context:', word.text);
return { return {
from: word.from, from: word.from,
options: jsdocCompletions, options: jsdocCompletions,
}; };
} }
return null; return null;
}
const handlers = [
scalePreColonHandler,
soundHandler,
bankHandler,
scaleAfterColonHandler,
// 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) => export const isAutoCompletionEnabled = (enabled) =>