feat(editor-v2): autocomplete completion source
CompletionSource over the srccfg-vocab.json shape. Word fragment
matched via /[A-Za-z0-9_]{2,}/ at the caret; ranking is
prefix-match-first (shorter prefixes preferred) then substring;
cap 50 candidates, top 8 rendered. Each option carries the kind
('cvar'/'command') as cm6's autocomplete `type` so the popup
shows the appropriate icon.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5289ae307f
commit
3440bbc131
1 changed files with 50 additions and 0 deletions
50
l4d2web/scripts/editor-src/autocomplete.js
Normal file
50
l4d2web/scripts/editor-src/autocomplete.js
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue