fix: deduplicate CodeMirror completion state
This commit is contained in:
@@ -13,12 +13,11 @@ type ConfigSourceCompletionItem = {
|
||||
};
|
||||
|
||||
export function toCodeMirrorCompletion(
|
||||
source: string,
|
||||
result: ConfigSourceCompletionResult | null,
|
||||
): CompletionResult | null {
|
||||
if (!result) return null;
|
||||
return {
|
||||
from: utf8ByteOffsetToUtf16(source, result.from),
|
||||
from: result.from,
|
||||
options: result.items.map((item) => ({
|
||||
label: item.label,
|
||||
type: item.kind,
|
||||
@@ -27,31 +26,3 @@ export function toCodeMirrorCompletion(
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function utf8ByteOffsetToUtf16(source: string, byteOffset: number): number {
|
||||
if (!Number.isSafeInteger(byteOffset) || byteOffset < 0) {
|
||||
throw new RangeError(
|
||||
"completion byte offset must be a non-negative integer",
|
||||
);
|
||||
}
|
||||
|
||||
let bytes = 0;
|
||||
let utf16 = 0;
|
||||
for (const character of source) {
|
||||
if (bytes === byteOffset) return utf16;
|
||||
const codePoint = character.codePointAt(0)!;
|
||||
bytes += codePoint <= 0x7f
|
||||
? 1
|
||||
: codePoint <= 0x7ff
|
||||
? 2
|
||||
: codePoint <= 0xffff
|
||||
? 3
|
||||
: 4;
|
||||
utf16 += character.length;
|
||||
if (bytes > byteOffset) {
|
||||
throw new RangeError("completion byte offset splits a UTF-8 code point");
|
||||
}
|
||||
}
|
||||
if (bytes === byteOffset) return utf16;
|
||||
throw new RangeError("completion byte offset is outside the source");
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ export class ConfigSourceToolchain {
|
||||
utf16Offset,
|
||||
explicit,
|
||||
});
|
||||
return toCodeMirrorCompletion(source, result);
|
||||
return toCodeMirrorCompletion(result);
|
||||
}
|
||||
format(source: string): Promise<string> {
|
||||
return this.#request({ kind: "format", source });
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
export const CODEMIRROR_VITE_DEDUPE = [
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/language",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
];
|
||||
@@ -1,6 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { autocompletion, type CompletionContext, type CompletionResult } from '@codemirror/autocomplete';
|
||||
import {
|
||||
autocompletion,
|
||||
completionKeymap,
|
||||
completionStatus,
|
||||
startCompletion,
|
||||
type CompletionContext,
|
||||
type CompletionResult,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection } from '@codemirror/view';
|
||||
@@ -69,6 +76,18 @@
|
||||
'.cm-tooltip-autocomplete > ul > li[aria-selected]': { background: 'var(--interactive-selected)', color: 'var(--text-strong)' },
|
||||
});
|
||||
|
||||
function scheduleCompletion(editor: EditorView) {
|
||||
queueMicrotask(() => {
|
||||
if (
|
||||
editor.hasFocus &&
|
||||
!editor.state.facet(EditorState.readOnly) &&
|
||||
completionStatus(editor.state) === null
|
||||
) {
|
||||
startCompletion(editor);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!host || untrack(() => view)) return;
|
||||
const initialValue = untrack(() => value);
|
||||
@@ -86,11 +105,16 @@
|
||||
highlightActiveLine(),
|
||||
decodal({ highlight: false }),
|
||||
syntaxHighlighting(syntaxTheme),
|
||||
...(handleComplete ? [autocompletion({ override: [async (context: CompletionContext) => {
|
||||
const doc = context.state.doc.toString();
|
||||
return await handleComplete(doc, context.pos, context.explicit);
|
||||
}] })] : []),
|
||||
keymap.of([]),
|
||||
autocompletion({
|
||||
override: [
|
||||
async (context: CompletionContext) => {
|
||||
if (!handleComplete) return null;
|
||||
const doc = context.state.doc.toString();
|
||||
return await handleComplete(doc, context.pos, context.explicit);
|
||||
},
|
||||
],
|
||||
}),
|
||||
keymap.of(completionKeymap),
|
||||
fixedSchemaWrapperCompartment.of(
|
||||
initialFixedSchemaWrapper ? fixedSchemaWrapperExtension() : [],
|
||||
),
|
||||
@@ -98,7 +122,16 @@
|
||||
EditorState.readOnly.of(initialReadonly),
|
||||
EditorView.editable.of(!initialReadonly),
|
||||
]),
|
||||
EditorView.domEventHandlers({
|
||||
focus: (_event, editor) => {
|
||||
scheduleCompletion(editor);
|
||||
return false;
|
||||
},
|
||||
}),
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (update.selectionSet && !update.docChanged) {
|
||||
scheduleCompletion(update.view);
|
||||
}
|
||||
if (update.docChanged) handleChange(update.state.doc.toString());
|
||||
}),
|
||||
theme,
|
||||
@@ -123,6 +156,7 @@
|
||||
EditorView.editable.of(!nextReadonly),
|
||||
]),
|
||||
});
|
||||
if (!nextReadonly) scheduleCompletion(editor);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { CODEMIRROR_VITE_DEDUPE } from "../../src/lib/workspace/config-source/vite-dedupe.ts";
|
||||
|
||||
declare const Deno: {
|
||||
test(name: string, fn: () => Promise<void> | void): void;
|
||||
readTextFile(path: URL): Promise<string>;
|
||||
@@ -7,6 +9,23 @@ function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
Deno.test("Vite deduplicates CodeMirror stateful packages", () => {
|
||||
for (
|
||||
const packageName of [
|
||||
"@codemirror/autocomplete",
|
||||
"@codemirror/language",
|
||||
"@codemirror/state",
|
||||
"@codemirror/view",
|
||||
"@lezer/common",
|
||||
]
|
||||
) {
|
||||
assert(
|
||||
CODEMIRROR_VITE_DEDUPE.includes(packageName),
|
||||
`Vite must deduplicate ${packageName}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("config editor snapshots Svelte proxies before cloning baselines", async () => {
|
||||
const source = await Deno.readTextFile(
|
||||
new URL(
|
||||
@@ -72,6 +91,13 @@ Deno.test("Decodal editor follows readonly prop changes after mount", async () =
|
||||
!source.includes("--border-subtle"),
|
||||
"CodeMirror theme must use workspace tokens that actually exist",
|
||||
);
|
||||
assert(
|
||||
source.includes("keymap.of(completionKeymap)") &&
|
||||
source.includes("startCompletion(editor)") &&
|
||||
source.includes("update.selectionSet && !update.docChanged") &&
|
||||
source.includes("completionStatus(editor.state) === null"),
|
||||
"completion should be explicitly available and start when an editable cursor moves into an empty schema position",
|
||||
);
|
||||
assert(
|
||||
source.includes("fixedSchemaWrapperCompartment.reconfigure") &&
|
||||
source.includes("fixedSchemaWrapperExtension()") &&
|
||||
|
||||
@@ -67,10 +67,9 @@ Deno.test("toolchain converts reactive-like proxies to plain Worker messages", a
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("toolchain adapts WASM completion items and byte offsets for CodeMirror", () => {
|
||||
const source = "let 名 = tru";
|
||||
const result = toCodeMirrorCompletion(source, {
|
||||
from: new TextEncoder().encode("let 名 = ").length,
|
||||
Deno.test("toolchain preserves WASM UTF-16 completion ranges for CodeMirror", () => {
|
||||
const result = toCodeMirrorCompletion({
|
||||
from: "let 名 = ".length,
|
||||
items: [
|
||||
{
|
||||
label: "true",
|
||||
@@ -84,7 +83,7 @@ Deno.test("toolchain adapts WASM completion items and byte offsets for CodeMirro
|
||||
assert(result !== null, "WASM completion should produce a CodeMirror result");
|
||||
assert(
|
||||
result.from === "let 名 = ".length,
|
||||
"byte offsets should become UTF-16 offsets",
|
||||
"WASM UTF-16 offsets should be preserved for CodeMirror",
|
||||
);
|
||||
assert(
|
||||
result.options.length === 1,
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import { CODEMIRROR_VITE_DEDUPE } from "./src/lib/workspace/config-source/vite-dedupe";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()],
|
||||
|
||||
resolve: {
|
||||
dedupe: CODEMIRROR_VITE_DEDUPE,
|
||||
},
|
||||
|
||||
server: {
|
||||
host: "localhost",
|
||||
port: 5173,
|
||||
|
||||
Reference in New Issue
Block a user