Improve autocompletion behavior
- Refactored handlers to use consistent early return pattern for better readability
- Added blocking behavior for unquoted contexts (sound(, scale(, bank()) to guide users toward correct syntax instead of showing irrelevant fallback completions
- Fixed soundHandler to show all sounds when fragment is empty (e.g., sound(""))
- Simplified bankHandler by removing complex quote handling logic in favor of consistent blocking pattern
- Made scale handler regex patterns consistent (removed leading dots)
- All handlers now follow same structure: block unquoted contexts, provide completions for quoted contexts
This commit is contained in:
parent
978616833a
commit
c8875b29c1
1 changed files with 99 additions and 77 deletions
|
|
@ -1,12 +1,9 @@
|
||||||
|
|
||||||
import jsdoc from '../../doc.json';
|
import jsdoc from '../../doc.json';
|
||||||
import { autocompletion } from '@codemirror/autocomplete';
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
import { h } from './html';
|
import { h } from './html';
|
||||||
import { Scale } from '@tonaljs/tonal';
|
import { Scale } from '@tonaljs/tonal';
|
||||||
import { soundMap } from 'superdough';
|
import { soundMap } from 'superdough';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const escapeHtml = (str) => {
|
const escapeHtml = (str) => {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
div.innerText = str;
|
div.innerText = str;
|
||||||
|
|
@ -80,7 +77,6 @@ const isValidDoc = (doc) => {
|
||||||
const hasExcludedTags = (doc) =>
|
const hasExcludedTags = (doc) =>
|
||||||
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
['superdirtOnly', 'noAutocomplete'].some((tag) => doc.tags?.find((t) => t.originalTitle === tag));
|
||||||
|
|
||||||
|
|
||||||
export function bankCompletions() {
|
export function bankCompletions() {
|
||||||
const soundDict = soundMap.get();
|
const soundDict = soundMap.get();
|
||||||
const banks = new Set();
|
const banks = new Set();
|
||||||
|
|
@ -88,10 +84,11 @@ export function bankCompletions() {
|
||||||
const [bank, suffix] = key.split('_');
|
const [bank, suffix] = key.split('_');
|
||||||
if (suffix && bank) banks.add(bank);
|
if (suffix && bank) banks.add(bank);
|
||||||
}
|
}
|
||||||
return Array.from(banks).sort().map((name) => ({ label: name, type: 'bank' }));
|
return Array.from(banks)
|
||||||
|
.sort()
|
||||||
|
.map((name) => ({ label: name, type: 'bank' }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Attempt to get all scale names from Tonal
|
// Attempt to get all scale names from Tonal
|
||||||
let scaleCompletions = [];
|
let scaleCompletions = [];
|
||||||
try {
|
try {
|
||||||
|
|
@ -138,15 +135,43 @@ const jsdocCompletions = (() => {
|
||||||
return completions;
|
return completions;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
||||||
// --- Handler functions for each context ---
|
// --- Handler functions for each context ---
|
||||||
const pitchNames = [
|
const pitchNames = [
|
||||||
'C', 'C#', 'Db', 'D', 'D#', 'Eb', 'E', 'E#', 'Fb', 'F', 'F#', 'Gb',
|
'C',
|
||||||
'G', 'G#', 'Ab', 'A', 'A#', 'Bb', 'B', 'B#', 'Cb'
|
'C#',
|
||||||
|
'Db',
|
||||||
|
'D',
|
||||||
|
'D#',
|
||||||
|
'Eb',
|
||||||
|
'E',
|
||||||
|
'E#',
|
||||||
|
'Fb',
|
||||||
|
'F',
|
||||||
|
'F#',
|
||||||
|
'Gb',
|
||||||
|
'G',
|
||||||
|
'G#',
|
||||||
|
'Ab',
|
||||||
|
'A',
|
||||||
|
'A#',
|
||||||
|
'Bb',
|
||||||
|
'B',
|
||||||
|
'B#',
|
||||||
|
'Cb',
|
||||||
];
|
];
|
||||||
|
|
||||||
function scalePreColonHandler(context) {
|
function scalePreColonHandler(context) {
|
||||||
let scalePreColonContext = context.matchBefore(/\.scale\(\s*['"][^'"]*$/);
|
// First check for scale context without quotes - block with empty completions
|
||||||
|
let scaleNoQuotesContext = context.matchBefore(/scale\(\s*$/);
|
||||||
|
if (scaleNoQuotesContext) {
|
||||||
|
return {
|
||||||
|
from: scaleNoQuotesContext.to,
|
||||||
|
options: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check for scale context with quotes - provide completions
|
||||||
|
let scalePreColonContext = context.matchBefore(/scale\(\s*['"][^'"]*$/);
|
||||||
if (scalePreColonContext) {
|
if (scalePreColonContext) {
|
||||||
if (!scalePreColonContext.text.includes(':')) {
|
if (!scalePreColonContext.text.includes(':')) {
|
||||||
if (context.explicit) {
|
if (context.explicit) {
|
||||||
|
|
@ -161,89 +186,86 @@ function scalePreColonHandler(context) {
|
||||||
return { from: scalePreColonContext.to, options: [] };
|
return { from: scalePreColonContext.to, options: [] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!scalePreColonContext.text.includes(':')) {
|
|
||||||
return { from: scalePreColonContext.to, options: [] };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function soundHandler(context) {
|
function soundHandler(context) {
|
||||||
let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/);
|
// First check for sound context without quotes - block with empty completions
|
||||||
if (soundContext) {
|
let soundNoQuotesContext = context.matchBefore(/(s|sound)\(\s*$/);
|
||||||
const text = soundContext.text;
|
if (soundNoQuotesContext) {
|
||||||
const quoteIdx = Math.max(text.lastIndexOf('"'), text.lastIndexOf("'"));
|
|
||||||
if (quoteIdx === -1) return null;
|
|
||||||
const inside = text.slice(quoteIdx + 1);
|
|
||||||
const fragMatch = inside.match(/(?:[\s\[\{\(<])([\w]*)$/);
|
|
||||||
const fragment = fragMatch ? fragMatch[1] : inside;
|
|
||||||
if (!fragment || fragment.length === 0) return null;
|
|
||||||
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 {
|
return {
|
||||||
from,
|
from: soundNoQuotesContext.to,
|
||||||
options,
|
options: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
// Then check for sound context with quotes - provide completions
|
||||||
|
let soundContext = context.matchBefore(/(s|sound)\(\s*['"][^'"]*$/);
|
||||||
|
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(/(?:[\s\[\{\(<])([\w]*)$/);
|
||||||
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function bankHandler(context) {
|
function bankHandler(context) {
|
||||||
let bankMatch = context.matchBefore(/bank\(\s*(['"])?([\w]*)$/);
|
// First check for bank context without quotes - block with empty completions
|
||||||
if (bankMatch) {
|
let bankNoQuotesContext = context.matchBefore(/bank\(\s*$/);
|
||||||
let banks = bankCompletions();
|
if (bankNoQuotesContext) {
|
||||||
// Extract quote and fragment using regex groups on match.text
|
|
||||||
const groups = bankMatch.text.match(/(['"])?([\w]*)$/);
|
|
||||||
const quote = groups ? groups[1] : undefined;
|
|
||||||
const fragment = groups ? groups[2] || '' : '';
|
|
||||||
let from;
|
|
||||||
if (quote) {
|
|
||||||
from = bankMatch.from + bankMatch.text.indexOf(quote) + 1;
|
|
||||||
} else {
|
|
||||||
from = bankMatch.to - fragment.length;
|
|
||||||
}
|
|
||||||
const filteredBanks = banks.filter((b) => b.label.startsWith(fragment));
|
|
||||||
let options;
|
|
||||||
if (!quote) {
|
|
||||||
options = filteredBanks.map((b) => ({ ...b, apply: '"' + b.label + '"' }));
|
|
||||||
} else {
|
|
||||||
const afterCursor = context.state.sliceDoc(bankMatch.to, bankMatch.to + 1);
|
|
||||||
options = filteredBanks.map((b) => {
|
|
||||||
if (afterCursor !== quote) {
|
|
||||||
return { ...b, apply: b.label + quote };
|
|
||||||
}
|
|
||||||
return b;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
from,
|
from: bankNoQuotesContext.to,
|
||||||
options,
|
options: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
|
// Then check for bank context with quotes - provide completions
|
||||||
|
let bankMatch = context.matchBefore(/bank\(\s*['"][^'"]*$/);
|
||||||
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function scaleAfterColonHandler(context) {
|
function scaleAfterColonHandler(context) {
|
||||||
let scaleContext = context.matchBefore(/\.scale\(\s*['"][^'"]*:[^'"]*$/);
|
let scaleContext = context.matchBefore(/scale\(\s*['"][^'"]*:[^'"]*$/);
|
||||||
if (scaleContext) {
|
if (!scaleContext) return null;
|
||||||
const text = scaleContext.text;
|
|
||||||
const colonIdx = text.lastIndexOf(':');
|
const text = scaleContext.text;
|
||||||
if (colonIdx === -1) return null;
|
const colonIdx = text.lastIndexOf(':');
|
||||||
const fragment = text.slice(colonIdx + 1);
|
if (colonIdx === -1) return null;
|
||||||
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
|
const fragment = text.slice(colonIdx + 1);
|
||||||
const options = filteredScales.map((s) => ({
|
const filteredScales = scaleCompletions.filter((s) => s.label.startsWith(fragment));
|
||||||
...s,
|
const options = filteredScales.map((s) => ({
|
||||||
apply: s.label.replace(/\s+/g, ':')
|
...s,
|
||||||
}));
|
apply: s.label.replace(/\s+/g, ':'),
|
||||||
const from = scaleContext.from + colonIdx + 1;
|
}));
|
||||||
return {
|
const from = scaleContext.from + colonIdx + 1;
|
||||||
from,
|
return {
|
||||||
options,
|
from,
|
||||||
};
|
options,
|
||||||
}
|
};
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fallbackHandler(context) {
|
function fallbackHandler(context) {
|
||||||
|
|
@ -264,7 +286,7 @@ const handlers = [
|
||||||
bankHandler,
|
bankHandler,
|
||||||
scaleAfterColonHandler,
|
scaleAfterColonHandler,
|
||||||
// this handler *must* be last
|
// this handler *must* be last
|
||||||
fallbackHandler
|
fallbackHandler,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const strudelAutocomplete = (context) => {
|
export const strudelAutocomplete = (context) => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue