fix(editor): keep Enter and Tab with the IME during composition
This commit is contained in:
1 parent
b9ea54350d
commit
16491cc3dd
5 files changed
+216
No files matched your search
@@ -2210,6 +2210,10 @@ function runKeymapExtension(codeMirrorKeymap: (typeof import("@codemirror/view")
|
||||
}
|
||||
|
||||
function handleEnter(view: EditorViewType): boolean {
|
||||
// While an IME composition is active, Enter confirms the composition (the
|
||||
// candidate list); intercepting it here would accept a completion popup on
|
||||
// top of the composition instead of committing the typed text (issue #8029).
|
||||
if (isEditorComposing(view)) return false;
|
||||
clearPendingCompletionEnter();
|
||||
if (isBatchColumnSelectionCompletionActive(codeMirrorCompletionStatus?.(view.state) ?? null) && applySelectedBatchColumnSelection(view)) return true;
|
||||
// CodeMirror's default completion keymap is disabled so batch selection can
|
||||
@@ -2224,6 +2228,7 @@ function handleEnter(view: EditorViewType): boolean {
|
||||
selectFirstCompletion: codeMirrorSelectFirstCompletion,
|
||||
retryDelayMs: COMPLETION_TAB_RETRY_DELAY_MS,
|
||||
maxWaitMs: COMPLETION_ENTER_MAX_WAIT_MS,
|
||||
isComposing: () => isEditorComposing(view),
|
||||
onUnavailable: () => insertNewlineWithoutCompletion(view),
|
||||
onSettled: () => {
|
||||
if (cancelPendingCompletionEnter === cancelRetry) cancelPendingCompletionEnter = null;
|
||||
@@ -2276,6 +2281,7 @@ function acceptCompletionOrNextSnippetField(view: EditorViewType): boolean {
|
||||
// Any non-empty selection range means Tab is being used for block indent,
|
||||
// not word completion. A completion popup can still appear as a side effect
|
||||
// of the indent edit itself, so it must never hijack this or a following Tab.
|
||||
if (isEditorComposing(view)) return false;
|
||||
if (view.state.selection.ranges.every((range) => range.empty)) {
|
||||
const completionStatus = codeMirrorCompletionStatus?.(view.state) ?? null;
|
||||
if (isBatchColumnSelectionCompletionActive(completionStatus) && applySelectedBatchColumnSelection(view)) return true;
|
||||
@@ -2299,6 +2305,10 @@ function waitForCompletionTab(view: EditorViewType): boolean {
|
||||
|
||||
const retry = () => {
|
||||
pendingCompletionTabTimer = null;
|
||||
// The user started an IME composition while waiting for the pending
|
||||
// completion; Tab now belongs to the candidate list, so drop the queued
|
||||
// acceptance (and the normal-Tab fallback) instead of fighting the IME.
|
||||
if (isEditorComposing(view)) return;
|
||||
const selectionRanges = view.state.selection.ranges;
|
||||
if (view.state.doc !== initialDoc || selectionRanges.length !== initialSelectionRanges.length || selectionRanges.some((range, index) => !range.empty || range.anchor !== initialSelectionRanges[index]?.anchor || range.head !== initialSelectionRanges[index]?.head)) return;
|
||||
|
||||
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const editorSource = readFileSync(new URL("../QueryEditor.vue", import.meta.url), "utf8");
|
||||
|
||||
function functionSource(name: string, nextName: string) {
|
||||
const start = editorSource.indexOf(`function ${name}(`);
|
||||
const end = editorSource.indexOf(`function ${nextName}(`, start);
|
||||
expect(start).toBeGreaterThanOrEqual(0);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return editorSource.slice(start, end);
|
||||
}
|
||||
|
||||
// Regression test for #8029: while an IME composition is active, Enter and the
|
||||
// completion-accept shortcut belong to the IME candidate list. The acceptance
|
||||
// handlers gate on the same isEditorComposing helper the completion providers
|
||||
// already use, and the delayed retries must drop their queued acceptance once
|
||||
// a composition starts instead of accepting over (or after) it.
|
||||
describe("QueryEditor IME-safe completion acceptance", () => {
|
||||
it("gates the Enter handler on IME composition before any acceptance", () => {
|
||||
const source = functionSource("handleEnter", "clearPendingCompletionEnter");
|
||||
const composingGuard = source.indexOf("if (isEditorComposing(view)) return false;");
|
||||
const acceptCall = source.indexOf("codeMirrorAcceptCompletion?.(view)");
|
||||
|
||||
expect(composingGuard).toBeGreaterThanOrEqual(0);
|
||||
expect(acceptCall).toBeGreaterThan(composingGuard);
|
||||
});
|
||||
|
||||
it("gates the Tab completion-accept handler on IME composition", () => {
|
||||
const source = functionSource("acceptCompletionOrNextSnippetField", "clearPendingCompletionTab");
|
||||
const composingGuard = source.indexOf("if (isEditorComposing(view)) return false;");
|
||||
const selectionCheck = source.indexOf("view.state.selection.ranges.every");
|
||||
|
||||
expect(composingGuard).toBeGreaterThanOrEqual(0);
|
||||
expect(selectionCheck).toBeGreaterThan(composingGuard);
|
||||
});
|
||||
|
||||
it("drops the pending Tab acceptance retry when an IME composition starts", () => {
|
||||
const source = functionSource("waitForCompletionTab", "wordWrapExtension");
|
||||
const retry = source.indexOf("const retry = () => {");
|
||||
const composingGuard = source.indexOf("if (isEditorComposing(view)) return;", retry);
|
||||
|
||||
expect(retry).toBeGreaterThanOrEqual(0);
|
||||
expect(composingGuard).toBeGreaterThan(retry);
|
||||
});
|
||||
|
||||
it("passes an IME guard to the Enter acceptance retry helper", () => {
|
||||
const source = functionSource("handleEnter", "clearPendingCompletionEnter");
|
||||
expect(source.includes("isComposing: () => isEditorComposing(view)")).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user