Files
Todo-List/scripts/check-ime-guard.mjs
2026-09-12 13:55:57 +08:00

91 lines
3.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 回归UI 文件里所有 Enter 与 Escape 按键处理都必须有 IME 守卫。
//
// 历史 bugcommand-palette.js 的 onKey 在 key === 'Enter' 时直接执行命令,
// 没检查 `e.isComposing || e.keyCode === 229` —— CJK 用户用输入法上屏
// 候选词时按 Enter会被误判为「确认命令」意外触发高亮项。
//
// 同样问题适用于 EscapeCJK/JP/KR 用户在 IME 组合中按 Esc 是「取消 IME 组合」,
// 不是「关闭弹窗 / 取消编辑」。无守卫会让 Esc 同时关掉 IME 和上层 UI。
//
// 修复:所有 UI 文件的 Enter / Esc 分支都要叠加 `!isImeComposing(e)` 或
// `if (isImeComposing(e)) return;`,由 src/utils/keymap.js 提供统一 helper。
//
// 静态分析覆盖feedback.js / conflict-dialog.js / diff-preview-dialog.js /
// import-dialog.js / settings-dialog.js / task-list.js / command-palette.js。
//
// 用法node scripts/check-ime-guard.mjs
import { readFileSync } from 'node:fs';
import { ok, bad, printSummary } from './_lib/check.mjs';
const FILES = [
'src/ui/feedback.js',
'src/ui/conflict-dialog.js',
'src/ui/diff-preview-dialog.js',
'src/ui/import-dialog.js',
'src/ui/settings-dialog.js',
'src/ui/task-list.js',
'src/ui/command-palette.js',
'src/ui/sidebar.js',
'src/ui/toolbar.js',
];
// 用 indexOf 代替 regex —— 一些 Node / shell 组合下 regex 字面量会被静默改写。
function hasImeImport(src) {
const idx = src.indexOf('isImeComposing');
if (idx < 0) return false;
const before = src.lastIndexOf('import', idx);
if (before < 0) return false;
const after = src.indexOf('keymap.js', idx);
if (after < 0 || after - idx > 200) return false;
return true;
}
// 通用:扫某种 key 的所有出现位置,检查其后窗口内是否有 IME 守卫。
function findBareKeys(src, key) {
const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`e\\.key\\s*===\\s*['"]${escaped}['"]`, 'g');
const matches = [...src.matchAll(re)];
const bare = [];
for (const em of matches) {
const pos = em.index;
const window = src.slice(Math.max(0, pos - 50), pos + 400);
const guarded = /!\s*isImeComposing\s*\(\s*e\s*\)/.test(window)
|| /isImeComposing\s*\(\s*e\s*\)\s*\)?\s*return/.test(window)
// `if (e.key === 'X' && !isImeComposing(e))` —— 守卫可能在条件之后
|| new RegExp(`e\\.key\\s*===\\s*['"]${escaped}['"][^)]*&&\\s*!\\s*isImeComposing`).test(window);
if (!guarded) {
const line = src.slice(0, pos).split('\n').length;
bare.push({ line, snippet: src.slice(pos, pos + 80).replace(/\n/g, ' ') });
}
}
return bare;
}
for (const file of FILES) {
const src = readFileSync(file, 'utf8');
if (hasImeImport(src)) {
ok(`${file}: 引入 isImeComposing helper`);
} else {
bad(`${file}: 引入 isImeComposing helper`, '未 import isImeComposing —— Enter/Esc 守卫可能漏写');
}
for (const key of ['Enter', 'Escape']) {
const bare = findBareKeys(src, key);
if (bare.length === 0) {
ok(`${file}: 所有 ${key} 分支均已守卫(共 0 个未守卫)`);
} else {
for (const b of bare) {
const reason = key === 'Enter'
? 'CJK 用户上屏候选词会被误判为确认'
: 'CJK 用户按 Esc 取消 IME 组合会被误判为关闭弹窗/取消编辑';
bad(`${file}:${b.line}${key} 分支无 IME 守卫`,
`附近代码:${b.snippet}…… ${reason}`);
}
}
}
}
printSummary('check-ime-guard');