Download src/modules/editor/lib/autocomplete/inlineExtension.ts from lekmikdok/termix: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/spaces/lekmikdok/termix/resolve/main/src/modules/editor/lib/autocomplete/inlineExtension.ts
- Command line
-
hf download hf://spaces/lekmikdok/termix/src/modules/editor/lib/autocomplete/inlineExtension.ts
-
curl -L -o inlineExtension.ts https://huggingface.co/spaces/lekmikdok/termix/resolve/main/src/modules/editor/lib/autocomplete/inlineExtension.ts
13.4 kB
| import { | |
| Prec, | |
| StateEffect, | |
| StateField, | |
| Transaction, | |
| type EditorState, | |
| type Extension, | |
| } from "@codemirror/state"; | |
| import { | |
| Decoration, | |
| EditorView, | |
| keymap, | |
| ViewPlugin, | |
| WidgetType, | |
| type PluginValue, | |
| type ViewUpdate, | |
| } from "@codemirror/view"; | |
| import { requestCompletion, type CompletionDeps } from "./provider"; | |
| export type AutocompletePrefs = CompletionDeps & { | |
| enabled: boolean; | |
| }; | |
| export type AutocompleteContext = { | |
| getPrefs: () => AutocompletePrefs; | |
| getPath: () => string | null; | |
| getLanguage: () => string | null; | |
| }; | |
| type Suggestion = { | |
| from: number; | |
| text: string; | |
| }; | |
| const setSuggestion = StateEffect.define<Suggestion | null>(); | |
| const suggestionField = StateField.define<Suggestion | null>({ | |
| create: () => null, | |
| update(value, tr) { | |
| for (const e of tr.effects) { | |
| if (e.is(setSuggestion)) return e.value; | |
| } | |
| if (!value) return value; | |
| if (tr.docChanged) { | |
| return consumeIfTypedAhead(value, tr); | |
| } | |
| if (tr.selection) return null; | |
| return value; | |
| }, | |
| }); | |
| function consumeIfTypedAhead( | |
| current: Suggestion, | |
| tr: Transaction, | |
| ): Suggestion | null { | |
| let consumed: string | null = null; | |
| let originDelta = 0; | |
| let abort = false; | |
| tr.changes.iterChanges((fromA, toA, _fromB, _toB, inserted) => { | |
| if (abort) return; | |
| const ins = inserted.toString(); | |
| if (fromA !== toA || fromA !== current.from || !ins) { | |
| abort = true; | |
| return; | |
| } | |
| if (current.text.startsWith(ins)) { | |
| consumed = ins; | |
| originDelta = ins.length; | |
| } else { | |
| abort = true; | |
| } | |
| }); | |
| if (abort || !consumed) return null; | |
| const remaining = current.text.slice((consumed as string).length); | |
| if (!remaining) return null; | |
| return { from: current.from + originDelta, text: remaining }; | |
| } | |
| class GhostWidget extends WidgetType { | |
| constructor(readonly text: string) { | |
| super(); | |
| } | |
| override eq(other: GhostWidget): boolean { | |
| return other.text === this.text; | |
| } | |
| toDOM(): HTMLElement { | |
| const span = document.createElement("span"); | |
| span.className = "cm-ai-ghost"; | |
| const lines = this.text.split("\n"); | |
| lines.forEach((line, i) => { | |
| if (i > 0) span.appendChild(document.createElement("br")); | |
| span.appendChild(document.createTextNode(line)); | |
| }); | |
| return span; | |
| } | |
| override ignoreEvent(): boolean { | |
| return true; | |
| } | |
| } | |
| const ghostTheme = EditorView.theme({ | |
| ".cm-ai-ghost": { | |
| opacity: "0.45", | |
| fontStyle: "italic", | |
| pointerEvents: "none", | |
| }, | |
| }); | |
| const ghostDecorations = EditorView.decorations.compute( | |
| [suggestionField], | |
| (state) => { | |
| const sug = state.field(suggestionField); | |
| if (!sug) return Decoration.none; | |
| return Decoration.set([ | |
| Decoration.widget({ | |
| widget: new GhostWidget(sug.text), | |
| side: 1, | |
| }).range(sug.from), | |
| ]); | |
| }, | |
| ); | |
| const DEBOUNCE_MS = 350; | |
| const CHAIN_DELAY_MS = 80; | |
| const MIN_PREFIX_CHARS = 2; | |
| const MAX_LINES = 6; | |
| const CACHE_SIZE = 32; | |
| const CACHE_TAIL = 512; | |
| const CACHE_HEAD = 128; | |
| const PREFIX_WINDOW = 4000; | |
| const SUFFIX_WINDOW = 2000; | |
| class LRU<K, V> { | |
| private map = new Map<K, V>(); | |
| constructor(private readonly cap: number) {} | |
| get(k: K): V | undefined { | |
| const v = this.map.get(k); | |
| if (v === undefined) return undefined; | |
| this.map.delete(k); | |
| this.map.set(k, v); | |
| return v; | |
| } | |
| set(k: K, v: V) { | |
| if (this.map.has(k)) this.map.delete(k); | |
| this.map.set(k, v); | |
| if (this.map.size > this.cap) { | |
| const first = this.map.keys().next().value; | |
| if (first !== undefined) this.map.delete(first); | |
| } | |
| } | |
| clear() { | |
| this.map.clear(); | |
| } | |
| } | |
| function suggestionKey(prefix: string, suffix: string, lang: string | null): string { | |
| const p = prefix.length > CACHE_TAIL ? prefix.slice(-CACHE_TAIL) : prefix; | |
| const s = suffix.length > CACHE_HEAD ? suffix.slice(0, CACHE_HEAD) : suffix; | |
| return `${lang ?? ""}${p} |