fix(keymap): Don't run the Enter that commits an input-method candidate

WebKit sends the Enter (or Space) that commits a candidate after compositionend,
with isComposing already false and keyCode 229. Handlers that checked isComposing
alone ran it: dialogs confirmed and shortcuts fired while typing Chinese or
Japanese. isImeKey() checks both, and every keydown handler that skipped
composition uses it.
This commit is contained in:
l0ng-ai
2026-10-27 17:16:46 +08:00
parent 2c576418cb
commit f73c0a6b1d
10 changed files with 26 additions and 9 deletions
+2 -1
View File
@@ -6,6 +6,7 @@ import { sshLabel } from "./model";
import { SourceDialog } from "./SourceDialog";
import { closeDialog, useDatabase } from "./store";
import styles from "./Database.module.css";
import { isImeKey } from "@/platform/ime";
const ObjectDialogs = lazyComponent(() => import("./ObjectDialogs").then((m) => m.ObjectDialogs));
const ExportDialog = lazyComponent(() => import("./ExportDialog").then((m) => m.ExportDialog));
@@ -138,7 +139,7 @@ function ParamsDialog({
<div
className={styles.form}
onKeyDown={(e) => {
if (e.key !== "Enter" || e.nativeEvent.isComposing) return;
if (e.key !== "Enter" || isImeKey(e.nativeEvent)) return;
e.preventDefault();
done(values);
}}
+2 -1
View File
@@ -23,6 +23,7 @@ import { colorVar, connectRemedy, fileSourceName, newSourceId, parseSourceUrl, s
import { closeDialog, loadDatabases, loadSchemas, saveSource, useDatabase } from "./store";
import { VENDORS, sourceUrl, vendorById, vendorOf } from "./vendors";
import styles from "./Database.module.css";
import { isImeKey } from "@/platform/ime";
type Tab = "general" | "options" | "ssh" | "schemas" | "advanced";
@@ -284,7 +285,7 @@ function NamePicker({
aria-label={`Add ${title} pattern`}
onChange={(e) => setPattern(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
if (e.key === "Enter" && !isImeKey(e.nativeEvent)) {
e.preventDefault();
add();
}
+2 -1
View File
@@ -33,6 +33,7 @@ import { closeBreakpointPopover, useOverlays } from "./popoverStore";
import { VariablesView, type VarRoot } from "./VariablesView";
import { pushHistory, readHistory } from "./expressionHistory";
import styles from "./Overlays.module.css";
import { isImeKey } from "@/platform/ime";
export function DebuggerOverlays() {
return (
@@ -710,7 +711,7 @@ function EvaluateDialogInner({ initial }: { initial: string }) {
});
};
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.nativeEvent.isComposing) return;
if (isImeKey(e.nativeEvent)) return;
const el = e.currentTarget;
if (e.key === "Enter" && e.shiftKey && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
+2 -1
View File
@@ -21,6 +21,7 @@ import {
useMacros,
type MacroStep,
} from "./model";
import { isImeKey } from "@/platform/ime";
/** Records typing and editor keys of every text editor while a macro records. */
const recorder = [
@@ -31,7 +32,7 @@ const recorder = [
EditorView.domEventHandlers({
keydown(e) {
// Keys the IDE keymap ran as actions were recorded as those actions.
if (e.defaultPrevented || e.isComposing || !useMacros.getState().recording) return false;
if (e.defaultPrevented || isImeKey(e) || !useMacros.getState().recording) return false;
const key = editorKeyName(e);
if (key) recordStep({ kind: "key", key });
return false;
@@ -57,6 +57,7 @@ import {
widenSearchEverywhere,
} from "./store";
import styles from "./SearchEverywhere.module.css";
import { isImeKey } from "@/platform/ime";
type SeScope = "allPlaces" | ProjectFileSet;
@@ -315,7 +316,7 @@ function SearchEverywhereDialog() {
};
const onKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.nativeEvent.isComposing) return;
if (isImeKey(e.nativeEvent)) return;
if (e.key === "Tab") {
e.preventDefault();
e.stopPropagation();
+2 -1
View File
@@ -33,6 +33,7 @@ import { trackRuntimeSettings } from "./runtime";
import { installActionProxies, installSettingsHost } from "./window/bridge";
import { AssignShortcutOverlay, showAssignShortcut } from "./keymap/AssignShortcut";
import { settingsSearchEntries } from "./searchIndex";
import { isImeKey } from "@/platform/ime";
export { settingsWindowParent } from "./window/bridge";
export const SettingsWindow = lazyComponent(() => import("./window/SettingsWindow").then((m) => m.SettingsWindow));
@@ -165,7 +166,7 @@ function listenLaunch(): () => void {
/** Esc and ⌘W cancel, like closing the window; ⌘, stays here. */
function installWindowKeys(): () => void {
const onKey = (e: KeyboardEvent) => {
if (e.defaultPrevented || e.isComposing) return;
if (e.defaultPrevented || isImeKey(e)) return;
const mod = e.metaKey || e.ctrlKey;
if ((e.key === "Escape" && !isPopupOpen()) || (mod && !e.altKey && !e.shiftKey && e.key.toLowerCase() === "w")) {
e.preventDefault();
+2 -1
View File
@@ -18,6 +18,7 @@ import { executeAction } from "@/platform/actions";
import { effectiveBindings, strokeOf } from "@/platform/keymap";
import { workflowSettings } from "@/platform/workflowSettings";
import { terminalRuntime } from "./loader";
import { isImeKey } from "@/platform/ime";
export type TerminalKeyResult =
| { kind: "send"; data: string }
@@ -128,7 +129,7 @@ export function isAllowedIdeShortcut(stroke: string, bindings: ReadonlyArray<rea
export function installTerminalKeys(target: Window = window): () => void {
const onKeyDown = (e: KeyboardEvent) => {
const rt = terminalRuntime();
if (e.isComposing || !rt) return;
if (isImeKey(e) || !rt) return;
// The terminal find bar: Escape closes it (instead of "focus editor"), ⌘F reselects the query.
if ((e.target as Element | null)?.closest?.("[data-terminal-find]")) {
const plain = !e.ctrlKey && !e.altKey && !e.shiftKey;
+8
View File
@@ -0,0 +1,8 @@
/**
* True while an input method is composing, including the key that ends it: WebKit sends the
* Enter / Space that commits a candidate after `compositionend`, with `isComposing` already
* false and keyCode 229, so a handler checking `isComposing` alone runs that Enter.
*/
export function isImeKey(e: KeyboardEvent): boolean {
return e.isComposing || e.keyCode === 229;
}
+2 -1
View File
@@ -18,6 +18,7 @@ import { intellijWindows } from "./keymaps/intellij-windows";
import { vscodeMacos } from "./keymaps/vscode-macos";
import { vscodeWindows } from "./keymaps/vscode-windows";
import { usePlatform } from "./platformInfo";
import { isImeKey } from "./ime";
export type Keymap = Record<string, string[]>;
@@ -439,7 +440,7 @@ export function installKeyboardDispatcher(target: Window = window, now: () => nu
if (runHeldGesture(e)) return reset();
resetTap();
otherKeyAt = e.key === "Escape" || e.key === "Tab" ? -Infinity : now();
if (e.isComposing) return;
if (isImeKey(e)) return;
const stroke = strokeOf(e);
if (!stroke) return;
const chord = pending.length > 0;
+2 -1
View File
@@ -8,6 +8,7 @@ import { FOCUSABLE, onWindowDeactivate, openLayer, trapTab } from "./popupState"
import { useTx } from "@/platform/i18n";
import { selectableText } from "./selectableText";
import styles from "./Dialog.module.css";
import { isImeKey } from "@/platform/ime";
/**
* `default`: filled accent button, triggered by Enter. `cancel`: Esc / close
@@ -256,7 +257,7 @@ function DialogInner({
onCloseRef.current();
} else if (e.key === "Tab") {
trapTab(e, ref.current);
} else if (e.key === "Enter" && !e.defaultPrevented && !e.isComposing && !e.shiftKey && !e.altKey) {
} else if (e.key === "Enter" && !e.defaultPrevented && !isImeKey(e) && !e.shiftKey && !e.altKey) {
const t = e.target instanceof Element ? e.target : null;
const mod = e.metaKey || e.ctrlKey;
if (t && t !== document.body && !ref.current.contains(t)) return;