fix: stabilize configuration editor completion
This commit is contained in:
@@ -20,6 +20,7 @@
|
|||||||
"@codemirror/language": "npm:@codemirror/language@6.12.4",
|
"@codemirror/language": "npm:@codemirror/language@6.12.4",
|
||||||
"@codemirror/state": "npm:@codemirror/state@6.7.1",
|
"@codemirror/state": "npm:@codemirror/state@6.7.1",
|
||||||
"@codemirror/view": "npm:@codemirror/view@6.43.8",
|
"@codemirror/view": "npm:@codemirror/view@6.43.8",
|
||||||
|
"@lezer/common": "npm:@lezer/common@1.5.2",
|
||||||
"@lezer/highlight": "npm:@lezer/highlight@1.2.3",
|
"@lezer/highlight": "npm:@lezer/highlight@1.2.3",
|
||||||
"decodal-codemirror": "npm:decodal-codemirror@0.3.0",
|
"decodal-codemirror": "npm:decodal-codemirror@0.3.0",
|
||||||
"clsx": "npm:clsx@2.1.1",
|
"clsx": "npm:clsx@2.1.1",
|
||||||
|
|||||||
Generated
+2
@@ -9,6 +9,7 @@
|
|||||||
"npm:@codemirror/state@6.7.1": "6.7.1",
|
"npm:@codemirror/state@6.7.1": "6.7.1",
|
||||||
"npm:@codemirror/view@6.43.8": "6.43.8",
|
"npm:@codemirror/view@6.43.8": "6.43.8",
|
||||||
"npm:@fontsource/ibm-plex-mono@5.3.0": "5.3.0",
|
"npm:@fontsource/ibm-plex-mono@5.3.0": "5.3.0",
|
||||||
|
"npm:@lezer/common@1.5.2": "1.5.2",
|
||||||
"npm:@lezer/highlight@1.2.3": "1.2.3",
|
"npm:@lezer/highlight@1.2.3": "1.2.3",
|
||||||
"npm:@sveltejs/adapter-static@3.0.9": "3.0.9_@sveltejs+kit@2.49.4__@sveltejs+vite-plugin-svelte@6.2.1___svelte@5.45.6___vite@7.2.7____yaml@2.9.0___yaml@2.9.0__svelte@5.45.6__typescript@5.9.3__vite@7.2.7___yaml@2.9.0__yaml@2.9.0_yaml@2.9.0",
|
"npm:@sveltejs/adapter-static@3.0.9": "3.0.9_@sveltejs+kit@2.49.4__@sveltejs+vite-plugin-svelte@6.2.1___svelte@5.45.6___vite@7.2.7____yaml@2.9.0___yaml@2.9.0__svelte@5.45.6__typescript@5.9.3__vite@7.2.7___yaml@2.9.0__yaml@2.9.0_yaml@2.9.0",
|
||||||
"npm:@sveltejs/kit@2.49.4": "2.49.4_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.45.6__vite@7.2.7___yaml@2.9.0__yaml@2.9.0_svelte@5.45.6_typescript@5.9.3_vite@7.2.7__yaml@2.9.0_yaml@2.9.0",
|
"npm:@sveltejs/kit@2.49.4": "2.49.4_@sveltejs+vite-plugin-svelte@6.2.1__svelte@5.45.6__vite@7.2.7___yaml@2.9.0__yaml@2.9.0_svelte@5.45.6_typescript@5.9.3_vite@7.2.7__yaml@2.9.0_yaml@2.9.0",
|
||||||
@@ -1026,6 +1027,7 @@
|
|||||||
"npm:@codemirror/language@6.12.4",
|
"npm:@codemirror/language@6.12.4",
|
||||||
"npm:@codemirror/state@6.7.1",
|
"npm:@codemirror/state@6.7.1",
|
||||||
"npm:@codemirror/view@6.43.8",
|
"npm:@codemirror/view@6.43.8",
|
||||||
|
"npm:@lezer/common@1.5.2",
|
||||||
"npm:@lezer/highlight@1.2.3",
|
"npm:@lezer/highlight@1.2.3",
|
||||||
"npm:@sveltejs/adapter-static@3.0.9",
|
"npm:@sveltejs/adapter-static@3.0.9",
|
||||||
"npm:@sveltejs/kit@2.49.4",
|
"npm:@sveltejs/kit@2.49.4",
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ type ConfigSourceCompletionItem = {
|
|||||||
priority: number;
|
priority: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function shouldStartCompletionAfterTyping(
|
||||||
|
insertedText: string,
|
||||||
|
): boolean {
|
||||||
|
return /\S/u.test(insertedText);
|
||||||
|
}
|
||||||
|
|
||||||
export function toCodeMirrorCompletion(
|
export function toCodeMirrorCompletion(
|
||||||
result: ConfigSourceCompletionResult | null,
|
result: ConfigSourceCompletionResult | null,
|
||||||
): CompletionResult | null {
|
): CompletionResult | null {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { untrack } from 'svelte';
|
import { untrack } from 'svelte';
|
||||||
import {
|
import {
|
||||||
autocompletion,
|
autocompletion,
|
||||||
|
closeCompletion,
|
||||||
completionKeymap,
|
completionKeymap,
|
||||||
completionStatus,
|
completionStatus,
|
||||||
startCompletion,
|
startCompletion,
|
||||||
@@ -10,9 +11,10 @@
|
|||||||
} from '@codemirror/autocomplete';
|
} from '@codemirror/autocomplete';
|
||||||
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
|
||||||
import { Compartment, EditorState } from '@codemirror/state';
|
import { Compartment, EditorState } from '@codemirror/state';
|
||||||
import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection } from '@codemirror/view';
|
import { EditorView, keymap, lineNumbers, highlightActiveLine, drawSelection, type ViewUpdate } from '@codemirror/view';
|
||||||
import { tags } from '@lezer/highlight';
|
import { tags } from '@lezer/highlight';
|
||||||
import { decodal } from 'decodal-codemirror';
|
import { decodal } from 'decodal-codemirror';
|
||||||
|
import { shouldStartCompletionAfterTyping } from '$lib/workspace/config-source/completion.ts';
|
||||||
import {
|
import {
|
||||||
fixedSchemaWrapperExtension,
|
fixedSchemaWrapperExtension,
|
||||||
moveSelectionIntoFixedWrapper,
|
moveSelectionIntoFixedWrapper,
|
||||||
@@ -88,6 +90,27 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function typedText(update: ViewUpdate): string | null {
|
||||||
|
let foundTyping = false;
|
||||||
|
let insertedText = '';
|
||||||
|
for (const transaction of update.transactions) {
|
||||||
|
if (!transaction.isUserEvent('input.type')) continue;
|
||||||
|
foundTyping = true;
|
||||||
|
transaction.changes.iterChanges((_fromA, _toA, _fromB, _toB, inserted) => {
|
||||||
|
insertedText += inserted.toString();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return foundTyping ? insertedText : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissCompletion(editor: EditorView) {
|
||||||
|
queueMicrotask(() => {
|
||||||
|
if (completionStatus(editor.state) !== null) closeCompletion(editor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const completionKeymapWithoutEnter = completionKeymap.filter((binding) => binding.key !== 'Enter');
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!host || untrack(() => view)) return;
|
if (!host || untrack(() => view)) return;
|
||||||
const initialValue = untrack(() => value);
|
const initialValue = untrack(() => value);
|
||||||
@@ -106,6 +129,7 @@
|
|||||||
decodal({ highlight: false }),
|
decodal({ highlight: false }),
|
||||||
syntaxHighlighting(syntaxTheme),
|
syntaxHighlighting(syntaxTheme),
|
||||||
autocompletion({
|
autocompletion({
|
||||||
|
activateOnTyping: false,
|
||||||
override: [
|
override: [
|
||||||
async (context: CompletionContext) => {
|
async (context: CompletionContext) => {
|
||||||
if (!handleComplete) return null;
|
if (!handleComplete) return null;
|
||||||
@@ -114,7 +138,7 @@
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
keymap.of(completionKeymap),
|
keymap.of(completionKeymapWithoutEnter),
|
||||||
fixedSchemaWrapperCompartment.of(
|
fixedSchemaWrapperCompartment.of(
|
||||||
initialFixedSchemaWrapper ? fixedSchemaWrapperExtension() : [],
|
initialFixedSchemaWrapper ? fixedSchemaWrapperExtension() : [],
|
||||||
),
|
),
|
||||||
@@ -122,17 +146,18 @@
|
|||||||
EditorState.readOnly.of(initialReadonly),
|
EditorState.readOnly.of(initialReadonly),
|
||||||
EditorView.editable.of(!initialReadonly),
|
EditorView.editable.of(!initialReadonly),
|
||||||
]),
|
]),
|
||||||
EditorView.domEventHandlers({
|
|
||||||
focus: (_event, editor) => {
|
|
||||||
scheduleCompletion(editor);
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (update.selectionSet && !update.docChanged) {
|
if (update.docChanged) {
|
||||||
|
handleChange(update.state.doc.toString());
|
||||||
|
const insertedText = typedText(update);
|
||||||
|
if (insertedText !== null) {
|
||||||
|
if (shouldStartCompletionAfterTyping(insertedText)) {
|
||||||
scheduleCompletion(update.view);
|
scheduleCompletion(update.view);
|
||||||
|
} else {
|
||||||
|
dismissCompletion(update.view);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (update.docChanged) handleChange(update.state.doc.toString());
|
|
||||||
}),
|
}),
|
||||||
theme,
|
theme,
|
||||||
],
|
],
|
||||||
@@ -156,7 +181,6 @@
|
|||||||
EditorView.editable.of(!nextReadonly),
|
EditorView.editable.of(!nextReadonly),
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
if (!nextReadonly) scheduleCompletion(editor);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import denoConfig from "../../deno.json" with { type: "json" };
|
||||||
import { CODEMIRROR_VITE_DEDUPE } from "../../src/lib/workspace/config-source/vite-dedupe.ts";
|
import { CODEMIRROR_VITE_DEDUPE } from "../../src/lib/workspace/config-source/vite-dedupe.ts";
|
||||||
|
|
||||||
declare const Deno: {
|
declare const Deno: {
|
||||||
@@ -24,6 +25,13 @@ Deno.test("Vite deduplicates CodeMirror stateful packages", () => {
|
|||||||
`Vite must deduplicate ${packageName}`,
|
`Vite must deduplicate ${packageName}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const packageName of CODEMIRROR_VITE_DEDUPE) {
|
||||||
|
assert(
|
||||||
|
packageName in (denoConfig.imports ?? {}),
|
||||||
|
`${packageName} must be a direct dependency so Vite can deduplicate it`,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test("config editor snapshots Svelte proxies before cloning baselines", async () => {
|
Deno.test("config editor snapshots Svelte proxies before cloning baselines", async () => {
|
||||||
@@ -92,11 +100,15 @@ Deno.test("Decodal editor follows readonly prop changes after mount", async () =
|
|||||||
"CodeMirror theme must use workspace tokens that actually exist",
|
"CodeMirror theme must use workspace tokens that actually exist",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
source.includes("keymap.of(completionKeymap)") &&
|
source.includes("keymap.of(completionKeymapWithoutEnter)") &&
|
||||||
|
source.includes("binding.key !== 'Enter'") &&
|
||||||
|
source.includes("activateOnTyping: false") &&
|
||||||
|
source.includes("shouldStartCompletionAfterTyping(insertedText)") &&
|
||||||
source.includes("startCompletion(editor)") &&
|
source.includes("startCompletion(editor)") &&
|
||||||
source.includes("update.selectionSet && !update.docChanged") &&
|
!source.includes("EditorView.domEventHandlers") &&
|
||||||
|
!source.includes("update.selectionSet") &&
|
||||||
source.includes("completionStatus(editor.state) === null"),
|
source.includes("completionStatus(editor.state) === null"),
|
||||||
"completion should be explicitly available and start when an editable cursor moves into an empty schema position",
|
"completion should start only after non-whitespace typing, without using focus, cursor movement, Space, or Enter",
|
||||||
);
|
);
|
||||||
assert(
|
assert(
|
||||||
source.includes("fixedSchemaWrapperCompartment.reconfigure") &&
|
source.includes("fixedSchemaWrapperCompartment.reconfigure") &&
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ declare const Deno: {
|
|||||||
readTextFile(path: URL): Promise<string>;
|
readTextFile(path: URL): Promise<string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
import { toCodeMirrorCompletion } from "../../src/lib/workspace/config-source/completion.ts";
|
import {
|
||||||
|
shouldStartCompletionAfterTyping,
|
||||||
|
toCodeMirrorCompletion,
|
||||||
|
} from "../../src/lib/workspace/config-source/completion.ts";
|
||||||
import { jsonWorkerMessage } from "../../src/lib/workspace/config-source/toolchain-message.ts";
|
import { jsonWorkerMessage } from "../../src/lib/workspace/config-source/toolchain-message.ts";
|
||||||
|
|
||||||
function assert(condition: unknown, message: string): asserts condition {
|
function assert(condition: unknown, message: string): asserts condition {
|
||||||
@@ -67,6 +70,25 @@ Deno.test("toolchain converts reactive-like proxies to plain Worker messages", a
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("completion starts only after non-whitespace typing", () => {
|
||||||
|
assert(
|
||||||
|
!shouldStartCompletionAfterTyping(" "),
|
||||||
|
"Space should not start completion",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!shouldStartCompletionAfterTyping("\n"),
|
||||||
|
"Enter should not start completion",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
!shouldStartCompletionAfterTyping("\t"),
|
||||||
|
"other whitespace should not start completion",
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
shouldStartCompletionAfterTyping("p"),
|
||||||
|
"non-whitespace typing should start completion",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
Deno.test("toolchain preserves WASM UTF-16 completion ranges for CodeMirror", () => {
|
Deno.test("toolchain preserves WASM UTF-16 completion ranges for CodeMirror", () => {
|
||||||
const result = toCodeMirrorCompletion({
|
const result = toCodeMirrorCompletion({
|
||||||
from: "let 名 = ".length,
|
from: "let 名 = ".length,
|
||||||
|
|||||||
Reference in New Issue
Block a user