fix(windows-ime): 避免 TSF 预检导致文本重复插入 (#1021)

Co-authored-by: tonyzaca7755963 <tonyzaca7755963@users.noreply.github.com>
This commit is contained in:
Teariceandtonyzaca7755963 authored and GitHub committed 2026-09-07 08:29:07 +08:00
1 parent 823a6a14cd
commit 82aa33ffd6
3 files changed
+36 -32

No files matched your search

@@ -11,6 +11,7 @@ const launcherPath = join(scriptsDir, "windows-package-msvc.cmd");
const ciWorkflowPath = join(repoRoot, ".github", "workflows", "release-tauri.yml");
const imeBuildPath = join(scriptsDir, "windows-ime-build.ps1");
const imeInstallSmokePath = join(scriptsDir, "windows-ime-install-smoke.ps1");
const realAsrInsertionSmokePath = join(scriptsDir, "windows-real-asr-insertion-smoke.ps1");
const imeRegisterPath = join(scriptsDir, "windows-ime-register.ps1");
const imeUnregisterPath = join(scriptsDir, "windows-ime-unregister.ps1");
const imeSolutionPath = join(appRoot, "windows-ime", "OpenLessIme.sln");
@@ -26,6 +27,7 @@ const launcher = readFileSync(launcherPath, "utf8");
const ciWorkflow = readFileSync(ciWorkflowPath, "utf8");
const imeBuild = readFileSync(imeBuildPath, "utf8");
const imeInstallSmoke = readFileSync(imeInstallSmokePath, "utf8");
const realAsrInsertionSmoke = readFileSync(realAsrInsertionSmokePath, "utf8");
const imeRegister = readFileSync(imeRegisterPath, "utf8");
const imeUnregister = readFileSync(imeUnregisterPath, "utf8");
const imeSolution = readFileSync(imeSolutionPath, "utf8");
@@ -109,8 +111,11 @@ assert.match(imeTextService, /TF_ES_ASYNC \| TF_ES_READWRITE/, "IME should retry
assert.match(imeTextService, /WaitForSingleObject/, "IME pipe submit should wait for async edit-session completion");
assert.match(imeEditSession, /SetEvent/, "IME edit session should signal async completion back to the pipe submitter");
assert.match(imeEditSession, /Collapse\(edit_cookie, TF_ANCHOR_END\)/, "IME should collapse the committed range to its end after insertion");
assert.match(imeEditSession, /SetSelection\(edit_cookie, 1, &selection\)/, "IME should move the caret to the end of inserted text");
assert.match(imeEditSession, /TF_AE_END/, "IME should make the end of the committed text the active selection end");
assert.match(imeEditSession, /SetSelection\(edit_cookie, 1, &selection\)/, "IME should move the caret to the end of inserted text");
assert.match(imeEditSession, /TF_AE_END/, "IME should make the end of the committed text the active selection end");
const insertAtSelectionCalls = imeEditSession.match(/\bInsertTextAtSelection\s*\(/g) ?? [];
assert.equal(insertAtSelectionCalls.length, 1, "IME should submit the dictated text to the host exactly once");
assert.doesNotMatch(imeEditSession, /TF_IAS_QUERYONLY/, "IME should not preflight the full text through host text stores before committing it");
assert.match(wixFragment, /DirectoryRef Id="INSTALLDIR"/, "WiX fragment should install into the app directory");
assert.match(wixFragment, /Component Id="OpenLessImeDllX64Component"/, "WiX fragment should define the x64 TSF DLL component");
@@ -149,6 +154,7 @@ assert.match(imeInstallSmoke, /Join-ProcessArguments/, "install smoke should quo
assert.match(imeInstallSmoke, /\$commandLine = Join-ProcessArguments \$ArgumentList/, "install smoke should build a single quoted command line");
assert.match(imeInstallSmoke, /Start-Process -FilePath \$FilePath -ArgumentList \$commandLine/, "install smoke should pass a single quoted command line to Start-Process");
assert.match(imeInstallSmoke, /OpenLessImeSubmit/, "install smoke should preserve TSF backend context");
assert.match(realAsrInsertionSmoke, /\$targetTextForComparison -cne \$finalTextForComparison/, "real insertion smoke should reject duplicate or partial text in initially empty targets");
assert.match(imeInstallSmoke, /Software\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/, "install smoke should check x64 COM registration");
assert.match(imeInstallSmoke, /Software\\WOW6432Node\\Classes\\CLSID\\\{6B9F3F4F-5EE7-42D6-9C61-9F80B03A5D7D\}\\InprocServer32/, "install smoke should check x86 COM registration");
assert.match(imeInstallSmoke, /LanguageProfile\\0x00000804\\\{9B5F5E04-23F6-47DA-9A26-D221F6C3F02E\}/, "install smoke should check the TSF language profile");
@@ -1016,7 +1016,13 @@ try {
if ([string]::IsNullOrWhiteSpace($targetText)) {
throw "$Target readback is empty."
}
if (-not $targetText.Contains($latest.finalText)) {
$targetTextForComparison = $targetText.Replace("`r`n", "`n")
$finalTextForComparison = ([string]$latest.finalText).Replace("`r`n", "`n")
$targetStartsEmpty = $Target -in @("notepad", "browser", "win32edit")
if ($targetStartsEmpty -and $targetTextForComparison -cne $finalTextForComparison) {
throw "$Target readback does not exactly match latest finalText in an initially empty target; duplicate or partial insertion detected (expected length=$($finalTextForComparison.Length), actual length=$($targetTextForComparison.Length))."
}
if (-not $targetStartsEmpty -and -not $targetText.Contains($latest.finalText)) {
if ($targetText.Contains($clipboardSentinel)) {
throw "$Target readback contains the pre-dictation clipboard sentinel instead of latest finalText."
}
@@ -101,38 +101,30 @@ HRESULT OpenLessEditSession::InsertText(TfEditCookie edit_cookie) {
return hr;
}
ITfRange* query_range = nullptr;
if (cancellation_ && cancellation_->load()) {
insert_at_selection->Release();
return HRESULT_FROM_WIN32(ERROR_CANCELLED);
}
// Commit in a single call. Some Chromium-backed text stores surface a
// full-text QUERYONLY preflight as an edit and then duplicate the real commit.
ITfRange* committed_range = nullptr;
hr = insert_at_selection->InsertTextAtSelection(
edit_cookie, TF_IAS_QUERYONLY, text_.c_str(),
static_cast<LONG>(text_.size()), &query_range);
if (query_range != nullptr) {
query_range->Release();
query_range = nullptr;
}
if (SUCCEEDED(hr) && cancellation_ && cancellation_->load()) {
hr = HRESULT_FROM_WIN32(ERROR_CANCELLED);
}
if (SUCCEEDED(hr)) {
ITfRange* committed_range = nullptr;
hr = insert_at_selection->InsertTextAtSelection(
edit_cookie, 0, text_.c_str(), static_cast<LONG>(text_.size()),
&committed_range);
if (committed_range != nullptr) {
if (SUCCEEDED(hr)) {
const HRESULT collapse_hr =
committed_range->Collapse(edit_cookie, TF_ANCHOR_END);
if (SUCCEEDED(collapse_hr)) {
TF_SELECTION selection = {};
selection.range = committed_range;
selection.style.ase = TF_AE_END;
selection.style.fInterimChar = FALSE;
(void)context_->SetSelection(edit_cookie, 1, &selection);
}
edit_cookie, 0, text_.c_str(), static_cast<LONG>(text_.size()),
&committed_range);
if (committed_range != nullptr) {
if (SUCCEEDED(hr)) {
const HRESULT collapse_hr =
committed_range->Collapse(edit_cookie, TF_ANCHOR_END);
if (SUCCEEDED(collapse_hr)) {
TF_SELECTION selection = {};
selection.range = committed_range;
selection.style.ase = TF_AE_END;
selection.style.fInterimChar = FALSE;
(void)context_->SetSelection(edit_cookie, 1, &selection);
}
committed_range->Release();
}
committed_range->Release();
}
insert_at_selection->Release();