Release v0.6.28: typo correction, partial sync and selection diagnostics
This commit is contained in:
+75
-7
@@ -73,6 +73,8 @@ class Transaction
|
||||
Clip snapshot;
|
||||
DWORD expected{};
|
||||
bool captured{};
|
||||
mutable bool invalidLogged{};
|
||||
unsigned copyNumber{};
|
||||
std::wstring adopted;
|
||||
|
||||
public:
|
||||
@@ -108,8 +110,18 @@ class Transaction
|
||||
}
|
||||
bool valid() const
|
||||
{
|
||||
return target && IsWindow(target) && GetForegroundWindow() == target &&
|
||||
app.inputVersion.load() == version;
|
||||
auto foreground = GetForegroundWindow();
|
||||
auto currentVersion = app.inputVersion.load();
|
||||
bool ok = target && IsWindow(target) && foreground == target && currentVersion == version;
|
||||
if (!ok && !invalidLogged)
|
||||
{
|
||||
invalidLogged = true;
|
||||
diagnosticEvent(L"Context invalid: target=" + std::to_wstring((uintptr_t)target) +
|
||||
L" alive=" + std::to_wstring(IsWindow(target)) + L" foreground=" +
|
||||
std::to_wstring((uintptr_t)foreground) + L" expectedVersion=" + std::to_wstring(version) +
|
||||
L" currentVersion=" + std::to_wstring(currentVersion));
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
void check()
|
||||
{
|
||||
@@ -144,10 +156,14 @@ class Transaction
|
||||
}
|
||||
std::wstring copy()
|
||||
{
|
||||
const auto started = GetTickCount64();
|
||||
const auto number = ++copyNumber;
|
||||
if (!valid())
|
||||
return {};
|
||||
write(textClip(L"\x2063KSwitcher-" + std::to_wstring(GetTickCount64())));
|
||||
DWORD before = expected;
|
||||
diagnosticEvent(L"Copy begin number=" + std::to_wstring(number) + L" sentinelSequence=" +
|
||||
std::to_wstring(before) + L" target=" + std::to_wstring((uintptr_t)target));
|
||||
if (!valid() || !chord(L'C', MOD_CONTROL))
|
||||
{
|
||||
diagnosticEvent(L"Copy failed: context changed or simulated Ctrl+C blocked");
|
||||
@@ -166,6 +182,10 @@ class Transaction
|
||||
DWORD now = GetClipboardSequenceNumber();
|
||||
if (now != changed)
|
||||
{
|
||||
diagnosticEvent(L"Copy sequence number=" + std::to_wstring(number) + L" from=" +
|
||||
std::to_wstring(changed) + L" to=" + std::to_wstring(now) + L" elapsedMs=" +
|
||||
std::to_wstring(GetTickCount64() - started) + L" owner=" +
|
||||
std::to_wstring((uintptr_t)GetClipboardOwner()));
|
||||
changedAgain |= changed != before;
|
||||
changed = now;
|
||||
stable = GetTickCount64();
|
||||
@@ -183,7 +203,8 @@ class Transaction
|
||||
return {};
|
||||
if (!chord(L'C', MOD_CONTROL))
|
||||
return {};
|
||||
diagnosticEvent(L"Copy retry: clipboard unchanged after Ctrl+C");
|
||||
diagnosticEvent(L"Copy retry: clipboard unchanged after Ctrl+C number=" + std::to_wstring(number) +
|
||||
L" attempt=" + std::to_wstring(attempts + 1) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started));
|
||||
++attempts;
|
||||
retryAt = GetTickCount64() + 250;
|
||||
}
|
||||
@@ -191,6 +212,10 @@ class Transaction
|
||||
}
|
||||
if (changed == before || !valid())
|
||||
{
|
||||
diagnosticEvent(L"Copy end number=" + std::to_wstring(number) + L" attempts=" +
|
||||
std::to_wstring(attempts) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started) +
|
||||
L" sequence=" + std::to_wstring(GetClipboardSequenceNumber()) + L" clipboardOpenBy=" +
|
||||
std::to_wstring((uintptr_t)GetOpenClipboardWindow()));
|
||||
diagnosticEvent(changed == before ? L"Copy failed: clipboard timeout (2000 ms)" :
|
||||
L"Copy failed: context changed");
|
||||
return {};
|
||||
@@ -201,6 +226,9 @@ class Transaction
|
||||
expected = changed;
|
||||
auto clip = readClipboard(app.window);
|
||||
auto text = clip.text();
|
||||
diagnosticEvent(L"Copy result number=" + std::to_wstring(number) + L" attempts=" +
|
||||
std::to_wstring(attempts) + L" elapsedMs=" + std::to_wstring(GetTickCount64() - started) +
|
||||
L" text=" + diagnosticText(text));
|
||||
adopted = text;
|
||||
check();
|
||||
for (auto &f : clip.formats)
|
||||
@@ -233,6 +261,27 @@ class Transaction
|
||||
const std::function<bool(const std::wstring &)> &suffixMatches = {})
|
||||
{
|
||||
// WebView word boundaries are only candidates; exact verification is mandatory.
|
||||
diagnosticEvent(L"Selection begin steps=" + std::to_wstring(count) + L" expected=" + diagnosticText(wordCandidate));
|
||||
GUITHREADINFO gui{sizeof(gui)};
|
||||
if (GetGUIThreadInfo(GetWindowThreadProcessId(target, nullptr), &gui))
|
||||
{
|
||||
wchar_t cls[128]{};
|
||||
GetClassNameW(gui.hwndFocus, cls, 128);
|
||||
std::wstring offsets = L"unavailable";
|
||||
if (_wcsicmp(cls, L"Edit") == 0)
|
||||
{
|
||||
DWORD_PTR range{};
|
||||
if (SendMessageTimeoutW(gui.hwndFocus, EM_GETSEL, 0, 0,
|
||||
SMTO_ABORTIFHUNG | SMTO_BLOCK, 25, &range) && (DWORD)range != 0xffffffff)
|
||||
offsets = std::to_wstring(LOWORD(range)) + L"," + std::to_wstring(HIWORD(range));
|
||||
}
|
||||
diagnosticEvent(L"Selection editor class=" + std::wstring(cls) + L" focus=" +
|
||||
std::to_wstring((uintptr_t)gui.hwndFocus) + L" caretWindow=" + std::to_wstring((uintptr_t)gui.hwndCaret) +
|
||||
L" caretRect=" + std::to_wstring(gui.rcCaret.left) + L"," + std::to_wstring(gui.rcCaret.top) +
|
||||
L"," + std::to_wstring(gui.rcCaret.right) + L"," + std::to_wstring(gui.rcCaret.bottom) +
|
||||
L" layout=" + std::to_wstring((uintptr_t)windowLayout(target)) + L" selectionOffsets=" + offsets);
|
||||
}
|
||||
else diagnosticEvent(L"Selection editor: focus/caret/selection unavailable");
|
||||
if (needsSelectionDelay(target) && !wordCandidate.empty() &&
|
||||
std::all_of(wordCandidate.begin(), wordCandidate.end(),
|
||||
[](wchar_t c) { return letter(c) || iswdigit(c); }))
|
||||
@@ -241,6 +290,7 @@ class Transaction
|
||||
return {};
|
||||
Sleep(25);
|
||||
auto text = copy();
|
||||
diagnosticEvent(L"Selection word-boundary match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text));
|
||||
if (valid() && matches(text))
|
||||
{
|
||||
diagnosticEvent(L"Selection: verified word boundary");
|
||||
@@ -251,6 +301,7 @@ class Transaction
|
||||
Sleep(25);
|
||||
}
|
||||
auto text = select(count) ? copy() : std::wstring{};
|
||||
diagnosticEvent(L"Selection initial match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text));
|
||||
// Chromium editors can apply only part of a burst of Shift+Left events.
|
||||
// Extend only a verified suffix; never replace a partial or unrelated range.
|
||||
if (valid() && !matches(text) && !text.empty() &&
|
||||
@@ -263,10 +314,11 @@ class Transaction
|
||||
{
|
||||
if (!valid() || !chord(VK_LEFT, MOD_SHIFT))
|
||||
return {};
|
||||
diagnosticEvent(L"Selection recovery Shift+Left sent=" + std::to_wstring(i + 1) + L" total=" + std::to_wstring(missing));
|
||||
Sleep(25);
|
||||
}
|
||||
text = copy();
|
||||
diagnosticEvent(L"Selection: suffix recovery match=" + std::to_wstring(matches(text)));
|
||||
diagnosticEvent(L"Selection: suffix recovery match=" + std::to_wstring(matches(text)) + L" copied=" + diagnosticText(text));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
@@ -532,6 +584,7 @@ void App::applySettings(const Settings &requested)
|
||||
run.reset();
|
||||
last.reset();
|
||||
++inputVersion;
|
||||
if (busy) diagnosticEvent(L"Input context changed: settings applied version=" + std::to_wstring(inputVersion.load()));
|
||||
}
|
||||
// Clear on the worker so the next check reloads edits to dictionary files.
|
||||
enqueue([this] { autoCorrector.configure({}, {}); });
|
||||
@@ -560,6 +613,7 @@ void App::reloadLayouts()
|
||||
run.reset();
|
||||
last.reset();
|
||||
++inputVersion;
|
||||
if (busy) diagnosticEvent(L"Input context changed: layouts reloaded version=" + std::to_wstring(inputVersion.load()));
|
||||
}
|
||||
registerHotkeys(settings);
|
||||
}
|
||||
@@ -589,6 +643,7 @@ void App::resetInput()
|
||||
{
|
||||
std::lock_guard lock(stateMutex);
|
||||
++inputVersion;
|
||||
if (busy) diagnosticEvent(L"Input context changed: reset version=" + std::to_wstring(inputVersion.load()));
|
||||
run.reset();
|
||||
last.reset();
|
||||
}
|
||||
@@ -606,6 +661,9 @@ void App::keyboard(const KBDLLHOOKSTRUCT &k, UINT mods)
|
||||
if (isHotkey(vk, mods))
|
||||
return;
|
||||
++inputVersion;
|
||||
if (busy) diagnosticEvent(L"Input context changed: keyboard vk=" + std::to_wstring(vk) +
|
||||
L" modifiers=" + std::to_wstring(mods) + L" injected=" + std::to_wstring((k.flags & LLKHF_INJECTED) != 0) +
|
||||
L" version=" + std::to_wstring(inputVersion.load()));
|
||||
last.reset();
|
||||
HWND fg = GetForegroundWindow();
|
||||
HKL layout = windowLayout(fg);
|
||||
@@ -839,7 +897,10 @@ LRESULT CALLBACK App::mouseProc(int code, WPARAM wp, LPARAM lp)
|
||||
// KSwitcher generates no mouse input. Remote clicks invalidate a typing
|
||||
// range just like local clicks, including while a transaction is pending.
|
||||
if (wp == WM_LBUTTONDOWN || wp == WM_RBUTTONDOWN || wp == WM_MBUTTONDOWN || wp == WM_MOUSEWHEEL)
|
||||
{
|
||||
if (current->busy) diagnosticEvent(L"Input context changed: mouse message=" + std::to_wstring(wp));
|
||||
current->resetInput();
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(nullptr, code, wp, lp);
|
||||
}
|
||||
@@ -1010,9 +1071,10 @@ void App::transform(int command, int snippet)
|
||||
!(command == 0 && reason == L"paste-sent-unverified")},
|
||||
{"elapsedMs", GetTickCount64() - start}, {"foregroundSame", GetForegroundWindow() == target},
|
||||
{"inputSame", app.inputVersion.load() == version}, {"original", bounded(text)},
|
||||
{"expected", bounded(result)}, {"actual", bounded(actual)},
|
||||
{"originalUnits", text.size()}, {"expectedUnits", result.size()}, {"actualUnits", actual.size()},
|
||||
{"textTruncated", text.size() > 4096 || result.size() > 4096 || actual.size() > 4096}};
|
||||
{"expected", bounded(verification == L"selection-mismatch" ? expected : result)}, {"actual", bounded(actual)},
|
||||
{"selectionExpected", bounded(expected)},
|
||||
{"originalUnits", text.size()}, {"expectedUnits", (verification == L"selection-mismatch" ? expected : result).size()}, {"actualUnits", actual.size()},
|
||||
{"textTruncated", text.size() > 4096 || result.size() > 4096 || actual.size() > 4096 || expected.size() > 4096}};
|
||||
diagnosticOperation(record.dump(-1, ' ', true, DiagnosticJson::error_handler_t::replace));
|
||||
}
|
||||
catch (...) {} // Logging must never abort a completed input transaction.
|
||||
@@ -1135,6 +1197,8 @@ void App::transform(int command, int snippet)
|
||||
if (newlines(text) != newlines(previous->result))
|
||||
{
|
||||
reason = L"undo-verification-mismatch";
|
||||
actual = text;
|
||||
verification = L"selection-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
return;
|
||||
@@ -1177,6 +1241,8 @@ void App::transform(int command, int snippet)
|
||||
std::to_wstring(recent.keys.size()) + L" match=" + std::to_wstring(hint));
|
||||
if (hint < 0)
|
||||
{
|
||||
actual = text;
|
||||
verification = L"selection-mismatch";
|
||||
reason = text.empty() ? L"recent-copy-empty" : L"recent-verification-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
@@ -1194,6 +1260,8 @@ void App::transform(int command, int snippet)
|
||||
{ return newlines(candidate) == newlines(previous->result); });
|
||||
if (newlines(text) != newlines(previous->result))
|
||||
{
|
||||
actual = text;
|
||||
verification = L"selection-mismatch";
|
||||
reason = L"previous-result-verification-mismatch";
|
||||
if (tx.valid())
|
||||
chord(VK_RIGHT);
|
||||
|
||||
Reference in New Issue
Block a user