import { autocompletion } from "@codemirror/autocomplete"; const WORD_RE = /[A-Za-z0-9_]{2,}/; function rank(query, label) { const q = query.toLowerCase(); const l = label.toLowerCase(); if (l === q) return 0; if (l.startsWith(q)) return 1 + l.length; // shorter prefix matches first const i = l.indexOf(q); if (i !== -1) return 10000 + i; // substring matches after all prefix matches return -1; } export function vocabCompletions(vocab) { // vocab: { cvars: [{name, desc?}, …], commands: [{name, desc?}, …] } const entries = [ ...vocab.cvars.map(e => ({ ...e, kind: "cvar" })), ...vocab.commands.map(e => ({ ...e, kind: "command" })), ]; return (context) => { const word = context.matchBefore(WORD_RE); if (!word || (word.from === word.to && !context.explicit)) return null; const q = word.text; const scored = []; for (const e of entries) { const r = rank(q, e.name); if (r === -1) continue; scored.push([r, e]); if (scored.length > 200) break; // bound work; we cap to 50 below } scored.sort((a, b) => a[0] - b[0]); const options = scored.slice(0, 50).map(([, e]) => ({ label: e.name, info: e.desc || e.kind, type: e.kind === "command" ? "function" : "variable", })); return { from: word.from, options, validFor: WORD_RE }; }; } export function autocompleteExtension(vocab) { return autocompletion({ override: [vocabCompletions(vocab)], activateOnTyping: true, maxRenderedOptions: 8, }); }