Files
msime-windows/engine/contracts/tests/windows_ipc_contract.cpp
Lu Fan(SonnyCalcr) c0a9d371f0 feat(server,windows,engine): 横排候选时左右方向键移动高亮 (#1080)
- 快捷键页「翻页方式」新增「左 / 右(横排时移动候选项)」,与「上 / 下」各自独立,默认关闭,
  只在候选窗为横排时生效。配置项 general.candidate_horizontal_arrow_navigation 已写入出厂模板。
- 光标在拼音串尾时右键移动高亮(到页末翻页),左键往回移高亮,高亮回到第一个候选后再按才移动光标;
  光标在串中间时右键先把光标移回串尾,再按才移动高亮。Ctrl+左/右与 UILess 宿主不变。
- 新增 worker 消息 HorizontalArrowSelectionChanged(39)和按键标记 FanyImePipeFlags::HorizontalArrowSelection:
  开关生效时 TSF 把候选态下的左右键交给 Server 决定,移光标时 Server 用 CompositionRestored 回光标,
  移高亮时回 MoveSelection*,TSF 不再自己预测光标。旧 DLL 忽略新消息,保持原行为。

Closes #779
Closes #895
2026-10-19 02:11:25 +08:00

470 lines
32 KiB
C++
Raw Permalink 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.
#include "../date_time_input.h"
#include "../direct_helpcode.h"
#include "../enumeration_comma.h"
#include "../ipc_negotiation.h"
#include "../mid_sentence_helpcode.h"
#include "../trilingual_input.h"
#include "../temporary_japanese_input.h"
#include "../uiless_page.h"
#include "../v_mode_input.h"
#include "../voice_composition_pipe.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <iostream>
#define CHECK(expression) \
do \
{ \
if (!(expression)) \
{ \
std::cerr << #expression << " at " << __LINE__ << '\n'; \
return 1; \
} \
} while (false)
int main()
{
// Wire bytes, not just matching C++ declarations: existing v1 DLLs write
// these offsets on both x86 and x64. Changes must not move UTF-16 data.
std::array<unsigned char, 304> bytes{};
bytes[0] = 10; // ClientHello
bytes[8] = 7; // client id
FanyImeNamedpipeData legacy{};
std::memcpy(&legacy, bytes.data(), bytes.size());
CHECK(legacy.client_id == 7);
CHECK(FanyImePipeEventType::InputStateAction == 17);
CHECK(FanyImeProtocol::InputStateShortcuts == (1u << 7));
CHECK(FanyImeProtocol::TemporaryJapaneseLongVowel == (1u << 8));
const auto rCapabilities = FanyImeProtocol::Capabilities | FanyImeProtocol::TemporaryJapaneseLongVowel;
const auto rHello = FanyImeProtocol::Hello(7, 123, rCapabilities);
CHECK((FanyImeProtocol::Negotiate(rHello).capabilities & FanyImeProtocol::TemporaryJapaneseLongVowel) == 0);
CHECK((FanyImeProtocol::Negotiate(rHello, rCapabilities).capabilities &
FanyImeProtocol::TemporaryJapaneseLongVowel) != 0);
CHECK((FanyImeProtocol::Negotiate(FanyImeProtocol::Hello(7, 124), rCapabilities).capabilities &
FanyImeProtocol::TemporaryJapaneseLongVowel) == 0);
CHECK(FanyImeWorkerReplyType::TemporaryJapanesePolicyChanged == 36);
CHECK(FanyImeWorkerReplyType::MaxKnown == 39);
for (wchar_t payload = L'0'; payload <= L'3'; ++payload)
{
CHECK(FanyImeTemporaryJapanese::IsValidPayload(payload));
CHECK(FanyImeTemporaryJapanese::ToPayload(FanyImeTemporaryJapanese::FromPayload(payload)) == payload);
}
CHECK(!FanyImeTemporaryJapanese::IsValidPayload(L'4'));
CHECK(!FanyImeTemporaryJapanese::IsValidPayload(L'/'));
CHECK(FanyImePipeEventType::HideCaretState == 16);
CHECK(FanyImePipeEventType::HideCaretState != FanyImePipeEventType::HideCandidateWnd);
CHECK(!FanyImePipeEventType::IsRouteDeactivation(FanyImePipeEventType::HideCaretState));
CHECK(FanyImeProtocol::CaretStateIndicator == (1u << 5));
CHECK((FanyImeProtocol::CaretStateIndicator & FanyImeProtocol::RequiredCapabilities) == 0);
// CapsLockEdge keeps the VK_CAPITAL value the first Servers matched on.
CHECK(FanyImeCaretStateTrigger::UserToggle == 0);
CHECK(FanyImeCaretStateTrigger::CapsLockEdge == 0x14);
CHECK(FanyImeCaretStateTrigger::FocusEntered != FanyImeCaretStateTrigger::UserToggle);
CHECK(FanyImeCaretStateTrigger::FocusEntered != FanyImeCaretStateTrigger::CapsLockEdge);
CHECK(FanyImeProtocol::Negotiate(legacy).legacy);
CHECK(FanyImeProtocol::Negotiate(legacy).accepted);
const auto capsOffSnapshot = FanyImePipeFlags::EncodeImeSwitchCapsLockSnapshot(false);
const auto capsOnSnapshot = FanyImePipeFlags::EncodeImeSwitchCapsLockSnapshot(true);
CHECK((FanyImePipeFlags::ImeSwitchCapsSnapshotPresent & FanyImePipeFlags::UiLess) == 0);
CHECK((FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled & FanyImePipeFlags::UiLess) == 0);
CHECK((FanyImePipeFlags::ImeSwitchCapsSnapshotPresent & 0xffu) == 0);
CHECK((FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled & 0xffu) == 0);
CHECK(FanyImePipeFlags::HasImeSwitchCapsLockSnapshot(capsOffSnapshot));
CHECK(!FanyImePipeFlags::ImeSwitchCapsLockSnapshotEnabled(capsOffSnapshot));
CHECK(FanyImePipeFlags::HasImeSwitchCapsLockSnapshot(capsOnSnapshot));
CHECK(FanyImePipeFlags::ImeSwitchCapsLockSnapshotEnabled(capsOnSnapshot));
CHECK(!FanyImePipeFlags::HasImeSwitchCapsLockSnapshot(0)); // old DLL packet
CHECK(!FanyImePipeFlags::ImeSwitchCapsLockSnapshotEnabled(FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled));
CHECK(FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(capsOffSnapshot).has_value());
CHECK(!*FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(capsOffSnapshot));
CHECK(*FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(capsOnSnapshot));
CHECK(!FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(0).has_value());
CHECK(
!FanyImePipeFlags::DecodeImeSwitchCapsLockSnapshot(FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled).has_value());
auto hello = FanyImeProtocol::Hello(7, 19);
auto result = FanyImeProtocol::Negotiate(hello);
CHECK(result.accepted && !result.legacy);
auto reply = FanyImeProtocol::Reply(hello, result);
CHECK(FanyImeProtocol::AcceptReply(reply, 19));
CHECK(!FanyImeProtocol::AcceptReply(reply, 18)); // stale reconnect ACK
CHECK(!FanyImeProtocol::AcceptReply(reply, 0));
const auto shortcutCapabilities = FanyImeProtocol::Capabilities | FanyImeProtocol::CharacterSetShortcut;
const auto shortcutHello = FanyImeProtocol::Hello(7, 20, shortcutCapabilities);
const auto oldServer = FanyImeProtocol::Negotiate(shortcutHello);
CHECK(oldServer.accepted);
CHECK((oldServer.capabilities & FanyImeProtocol::CharacterSetShortcut) == 0);
const auto newServer = FanyImeProtocol::Negotiate(shortcutHello, shortcutCapabilities);
CHECK(newServer.accepted);
CHECK((newServer.capabilities & FanyImeProtocol::CharacterSetShortcut) != 0);
const auto shortcutReply = FanyImeProtocol::Reply(shortcutHello, newServer);
CHECK(FanyImeProtocol::AcceptReply(shortcutReply, 20));
CHECK(FanyImeProtocol::ReplyCapabilities(shortcutReply) == shortcutCapabilities);
CHECK((FanyImeProtocol::Negotiate(hello, shortcutCapabilities).capabilities &
FanyImeProtocol::CharacterSetShortcut) == 0); // old client/new server
CHECK(FanyImeProtocol::IsCharacterSetShortcut('F', 3));
CHECK(FanyImeProtocol::IsCharacterSetShortcut('F', 0x80000003u));
for (unsigned modifiers = 0; modifiers < 8; ++modifiers)
CHECK(FanyImeProtocol::IsCharacterSetShortcut('F', modifiers) == (modifiers == 3));
CHECK(!FanyImeProtocol::IsCharacterSetShortcut('E', 3));
// Backspace retraction is negotiated separately: an old Server must keep
// its current behavior, and an old DLL must never receive CompositionRestored.
const auto restoreCapabilities = FanyImeProtocol::Capabilities | FanyImeProtocol::CompositionRestore;
const auto restoreHello = FanyImeProtocol::Hello(7, 21, restoreCapabilities);
const auto oldServerForRestore = FanyImeProtocol::Negotiate(restoreHello);
CHECK(oldServerForRestore.accepted);
CHECK((oldServerForRestore.capabilities & FanyImeProtocol::CompositionRestore) == 0);
const auto newServerForRestore = FanyImeProtocol::Negotiate(restoreHello, restoreCapabilities);
CHECK(newServerForRestore.accepted);
CHECK((newServerForRestore.capabilities & FanyImeProtocol::CompositionRestore) != 0);
CHECK(FanyImeProtocol::AcceptReply(FanyImeProtocol::Reply(restoreHello, newServerForRestore), 21));
CHECK(FanyImeProtocol::ReplyCapabilities(FanyImeProtocol::Reply(restoreHello, newServerForRestore)) ==
restoreCapabilities);
CHECK((FanyImeProtocol::Negotiate(hello, restoreCapabilities).capabilities & FanyImeProtocol::CompositionRestore) ==
0); // old client/new server
CHECK(FanyImeReplyType::CompositionRestored == 14);
CHECK(FanyImeReplyType::TrilingualCycle == 15);
CHECK(FanyImeReplyType::MaxKnown == FanyImeReplyType::TrilingualCycle);
CHECK(FanyImeReplyType::TransportUnavailable > FanyImeReplyType::MaxKnown);
const auto cycleCapabilities = FanyImeProtocol::Capabilities | FanyImeProtocol::TrilingualCycle;
const auto cycleHello = FanyImeProtocol::Hello(7, 22, cycleCapabilities);
const auto oldServerForCycle = FanyImeProtocol::Negotiate(cycleHello);
CHECK(oldServerForCycle.accepted);
CHECK((oldServerForCycle.capabilities & FanyImeProtocol::TrilingualCycle) == 0);
const auto newServerForCycle = FanyImeProtocol::Negotiate(cycleHello, cycleCapabilities);
CHECK(newServerForCycle.accepted && !newServerForCycle.legacy);
CHECK((newServerForCycle.capabilities & FanyImeProtocol::TrilingualCycle) != 0);
CHECK(FanyImeProtocol::AcceptReply(FanyImeProtocol::Reply(cycleHello, newServerForCycle), 22));
CHECK((FanyImeProtocol::Negotiate(hello, cycleCapabilities).capabilities & FanyImeProtocol::TrilingualCycle) == 0);
CHECK((FanyImeProtocol::TrilingualCycle & FanyImeProtocol::RequiredCapabilities) == 0);
{
using FanyImeTrilingualInput::Mode;
CHECK(FanyImeTrilingualInput::Next(true, false) == Mode::Japanese);
CHECK(FanyImeTrilingualInput::Next(true, true) == Mode::English);
CHECK(FanyImeTrilingualInput::Next(false, true) == Mode::Chinese);
CHECK(FanyImeTrilingualInput::Next(false, false) == Mode::Chinese);
// A plain VK_SHIFT is the binary toggle; only the explicit request bit cycles.
CHECK(!FanyImeTrilingualInput::IsCycleRequest(0x10, 0));
CHECK(!FanyImeTrilingualInput::IsCycleRequest(0x10, FanyImePipeFlags::UiLess));
for (unsigned modifiers = 0; modifiers < 8; ++modifiers)
{
const auto request = FanyImeTrilingualInput::EncodeCycleRequest(true, false) | modifiers;
CHECK(FanyImeTrilingualInput::IsCycleRequest(0x10, request) == (modifiers == 0));
CHECK(!FanyImeTrilingualInput::IsCycleRequest(0x09, request));
}
CHECK(FanyImeTrilingualInput::IsCycleRequest(0x10, FanyImeTrilingualInput::EncodeCycleRequest(false, true) |
FanyImePipeFlags::UiLess));
CHECK(!FanyImeTrilingualInput::IsCycleRequest(0x20, FanyImeTrilingualInput::EncodeCycleRequest(true, true)));
// The request bits stay clear of the Shift/Ctrl/Alt bits and every pipe flag.
CHECK((FanyImeTrilingualInput::CycleRequestMask & 7u) == 0);
CHECK((FanyImeTrilingualInput::CycleRequestMask &
(FanyImePipeFlags::UiLess | FanyImePipeFlags::ImeSwitchCapsSnapshotPresent |
FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled)) == 0);
for (const bool ime : {false, true})
{
for (const bool japanese : {false, true})
{
const auto request = FanyImeTrilingualInput::EncodeCycleRequest(ime, japanese);
CHECK(FanyImeTrilingualInput::CycleRequestImeEnabled(request) == ime);
CHECK(FanyImeTrilingualInput::CycleRequestJapanese(request) == japanese);
// The requester's state, not a Server global, picks the destination.
CHECK(FanyImeTrilingualInput::Destination(ime, japanese, true, !japanese) ==
FanyImeTrilingualInput::Next(ime, japanese));
}
}
// Turned off while queued: answer as the binary toggle.
CHECK(FanyImeTrilingualInput::Destination(true, true, false, false) == Mode::English);
CHECK(FanyImeTrilingualInput::Destination(true, false, false, true) == Mode::English);
CHECK(FanyImeTrilingualInput::Destination(false, true, false, false) == Mode::Chinese);
CHECK(FanyImeTrilingualInput::Destination(false, false, false, true) == Mode::Japanese);
Mode destination = Mode::Chinese;
std::wstring text;
CHECK(FanyImeTrilingualInput::ParsePayload(FanyImeTrilingualInput::BuildPayload(Mode::Japanese, L"你好"),
destination, text));
CHECK(destination == Mode::Japanese && text == L"你好");
CHECK(FanyImeTrilingualInput::ParsePayload(L"2\t", destination, text));
CHECK(destination == Mode::English && text.empty());
CHECK(FanyImeTrilingualInput::ParsePayload(L"0\tかな\tword", destination, text));
CHECK(destination == Mode::Chinese && text == L"かな\tword");
for (const auto *invalid : {L"", L"0", L"3\ttext", L"00\ttext", L"1|text", L"\ttext"})
{
CHECK(!FanyImeTrilingualInput::ParsePayload(invalid, destination, text));
}
const std::wstring embeddedNull(L"1\ttext\0tail", 11);
CHECK(!FanyImeTrilingualInput::ParsePayload(embeddedNull, destination, text));
}
hello.wch += 1;
result = FanyImeProtocol::Negotiate(hello);
CHECK(!result.accepted);
CHECK(!FanyImeProtocol::AcceptReply(FanyImeProtocol::Reply(hello, result), 19));
hello = FanyImeProtocol::Hello(7, 19);
hello.point[1] |= 1u << 20; // client requires an unknown capability
CHECK(!FanyImeProtocol::Negotiate(hello).accepted);
hello = FanyImeProtocol::Hello(7, 19);
hello.modifiers_down |= 1u << 20; // unknown optional capabilities are ignored
CHECK(FanyImeProtocol::Negotiate(hello).accepted);
hello.modifiers_down &= ~FanyImeProtocol::FocusEpochs;
CHECK(!FanyImeProtocol::Negotiate(hello).accepted);
hello = FanyImeProtocol::Hello(7, 19);
hello.keycode ^= 1;
CHECK(!FanyImeProtocol::Negotiate(hello).accepted);
hello = FanyImeProtocol::Hello(7, 0);
CHECK(!FanyImeProtocol::Negotiate(hello).accepted);
FanyImeNamedpipeDataToTsf old_server{};
old_server.msg_type = FanyImeReplyType::PipeReady;
old_server.request_id = 19;
CHECK(!FanyImeProtocol::AcceptReply(old_server, 19));
// Corrupt/missing negotiated capabilities cannot authorize the new DLL.
reply.candidate_string[2] = 0;
CHECK(!FanyImeProtocol::AcceptReply(reply, 19));
CHECK(FanyImeWorkerReplyType::SwitchToEn == FanyImeWorkerReplyType::SwitchToEnglish);
CHECK(FanyImeWorkerReplyType::CommitCandidate == FanyImeWorkerReplyType::CommitCurCandidate);
CHECK(FanyImeWorkerReplyType::CommitCandidateAndContinue == 27);
CHECK(FanyImeWorkerReplyType::MidSentenceHelpcodeChanged == 28);
CHECK(FanyImeWorkerReplyType::MidSentenceHelpcodeSemicolonChanged == 29);
CHECK(FanyImeWorkerReplyType::DirectHelpcodeChanged == 30);
CHECK(FanyImeWorkerReplyType::MidSentenceHelpcodeUppercaseChanged == 31);
CHECK(FanyImeWorkerReplyType::VModeChanged == 32);
CHECK(FanyImeWorkerReplyType::TrilingualCycleChanged == 33);
CHECK(FanyImeWorkerReplyType::AiContextUploadChanged == 34);
CHECK(FanyImeWorkerReplyType::UiLessCandidatesChanged == 35);
CHECK(FanyImeWorkerReplyType::TemporaryJapanesePolicyChanged == 36);
CHECK(FanyImeWorkerReplyType::EndHelpcodeTriggersChanged == 37);
CHECK(FanyImeWorkerReplyType::EnumerationCommaKeysChanged == 38);
CHECK(FanyImeWorkerReplyType::HorizontalArrowSelectionChanged == 39);
CHECK(FanyImeWorkerReplyType::MaxKnown == FanyImeWorkerReplyType::HorizontalArrowSelectionChanged);
// EnumerationCommaKeysChanged 的载荷:\ 和 / 各一位,两个都没勾按只有 \ 发。
CHECK(FanyImeEnumerationComma::PayloadFor(true, false) == L'1' &&
FanyImeEnumerationComma::PayloadFor(false, true) == L'2' &&
FanyImeEnumerationComma::PayloadFor(true, true) == L'3' &&
FanyImeEnumerationComma::PayloadFor(false, false) == L'1');
CHECK(FanyImeEnumerationComma::KeysFromPayload(L'3') ==
(FanyImeEnumerationComma::kBackslash | FanyImeEnumerationComma::kSlash) &&
!FanyImeEnumerationComma::IsValidPayload(L'0') && !FanyImeEnumerationComma::IsValidPayload(L'4') &&
FanyImeEnumerationComma::KeysFromPayload(L'x') == FanyImeEnumerationComma::kBackslash);
{
const auto full = FanyImeUiLessPage::Parse(FanyImeUiLessPage::Format(L"ni hao", L"你好,拟好", 1, 42));
CHECK(full.preedit == L"ni hao" && full.page == L"你好,拟好" && full.selection == 1 && full.generation == 42);
// A Server predating the generation field.
CHECK(full.hasPage && !FanyImeUiLessPage::Parse(L"ni hao").hasPage);
const auto legacy = FanyImeUiLessPage::Parse(L"\t你好,拟好\t1");
CHECK(legacy.page == L"你好,拟好" && legacy.selection == 1 && legacy.generation == 0);
// A DLL predating the field reads selection with _wtoi, which must stop at the third tab.
const std::wstring formatted = FanyImeUiLessPage::Format(L"", L"你好", 3, 7);
CHECK(std::wcstol(formatted.c_str() + formatted.find(L'\t', 1) + 1, nullptr, 10) == 3);
CHECK(FanyImeUiLessPage::IsCurrent(0, 9) && FanyImeUiLessPage::IsCurrent(9, 9));
CHECK(FanyImeUiLessPage::IsCurrent(10, 9) && !FanyImeUiLessPage::IsCurrent(8, 9));
}
// The sensitive-field flag stays clear of Shift/Ctrl/Alt, every pipe flag and the cycle request bits.
CHECK((FanyImePipeFlags::SensitiveField & 7u) == 0);
CHECK((FanyImePipeFlags::SensitiveField &
(FanyImePipeFlags::UiLess | FanyImePipeFlags::ImeSwitchCapsSnapshotPresent |
FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled | FanyImeTrilingualInput::CycleRequestMask)) == 0);
// Same for the horizontal arrow selection flag, which also stays clear of the sensitive-field bit.
CHECK((FanyImePipeFlags::HorizontalArrowSelection & 7u) == 0);
CHECK((FanyImePipeFlags::HorizontalArrowSelection &
(FanyImePipeFlags::UiLess | FanyImePipeFlags::ImeSwitchCapsSnapshotPresent |
FanyImePipeFlags::ImeSwitchCapsSnapshotEnabled | FanyImeTrilingualInput::CycleRequestMask |
FanyImePipeFlags::SensitiveField)) == 0);
// V 模式的形状规则,TSF(WCHAR)与 Server(char)共用。
{
using FanyImeVModeInput::Trigger;
const auto v_at = [](const std::wstring &text, std::size_t caret, wchar_t ch, Trigger trigger) {
return FanyImeVModeInput::AcceptsAt(text.data(), text.size(), caret, ch, trigger);
};
const auto v_end = [&](const std::wstring &text, wchar_t ch, Trigger trigger) {
return v_at(text, text.size(), ch, trigger);
};
CHECK(v_end(L"V", L'1', Trigger::UppercaseOnly) && v_end(L"V", L'(', Trigger::UppercaseOnly));
CHECK(!v_end(L"V", L'+', Trigger::UppercaseOnly) && !v_end(L"V", L'.', Trigger::AnyCase));
CHECK(v_end(L"V1", L'+', Trigger::UppercaseOnly) && v_end(L"V1+2", L'*', Trigger::AnyCase));
CHECK(v_end(L"V12", L'.', Trigger::AnyCase) && v_end(L"V(1", L')', Trigger::AnyCase));
CHECK(!v_end(L"v", L'1', Trigger::UppercaseOnly) && v_end(L"v", L'1', Trigger::AnyCase));
CHECK(!v_end(L"V", L'1', Trigger::Off) && !v_end(L"vi", L'1', Trigger::AnyCase));
CHECK(!v_end(L"V1", L'a', Trigger::AnyCase) && !v_end(L"T1", L'1', Trigger::AnyCase));
// 光标在中间:不能把运算符插到 V 和第一个数字之间。
CHECK(!v_at(L"V12", 1, L'+', Trigger::AnyCase) && v_at(L"V12", 1, L'3', Trigger::AnyCase) &&
v_at(L"V12", 2, L'+', Trigger::AnyCase) && !v_at(L"V12", 0, L'3', Trigger::AnyCase));
CHECK(!FanyImeVModeInput::IsComposition(L"V", 1, Trigger::UppercaseOnly) &&
!FanyImeVModeInput::IsComposition(L"v", 1, Trigger::AnyCase) &&
FanyImeVModeInput::IsComposition(L"v1", 2, Trigger::AnyCase) &&
!FanyImeVModeInput::IsComposition(L"v1", 2, Trigger::UppercaseOnly) &&
FanyImeVModeInput::IsComposition("V1a", 3, Trigger::UppercaseOnly) &&
!FanyImeVModeInput::IsComposition("vip", 3, Trigger::AnyCase));
CHECK(FanyImeVModeInput::TriggerFromPayload(L'2') == Trigger::AnyCase &&
FanyImeVModeInput::TriggerFromPayload(L'1') == Trigger::UppercaseOnly &&
FanyImeVModeInput::TriggerFromPayload(L'x') == Trigger::Off &&
FanyImeVModeInput::PayloadFromTrigger(Trigger::UppercaseOnly) == L'1');
}
// 直接辅助码的 / 与 ; 形状规则,TSF(WCHAR)与引擎(char)共用。
const auto slash_at = [](const std::wstring &text, std::size_t caret) {
return FanyImeDirectHelpcode::AcceptsSlashAt(text.data(), text.size(), caret);
};
CHECK(slash_at(L"uiab", 4) && slash_at(L"x;ab", 4) && !slash_at(L"uia", 3) && !slash_at(L"ui'b", 4));
CHECK(!slash_at(L"uiab/", 4) && slash_at(L"uiabui", 4) && !slash_at(L"uiab", 5));
CHECK(FanyImeDirectHelpcode::AcceptsSemicolonFinalAt("uiax", 4, 4) &&
!FanyImeDirectHelpcode::AcceptsSemicolonFinalAt("uix;", 4, 4) &&
!FanyImeDirectHelpcode::AcceptsSemicolonFinalAt("", 0, 0));
// DirectHelpcodeChanged 的载荷:开关和 / 勾没勾。旧 DLL 认的 "0"/"1" 意思不变。
CHECK(FanyImeDirectHelpcode::PayloadFor(false, true) == L'0' &&
FanyImeDirectHelpcode::PayloadFor(true, true) == L'1' &&
FanyImeDirectHelpcode::PayloadFor(true, false) == L'2');
CHECK(FanyImeDirectHelpcode::EnabledFromPayload(L'2') && !FanyImeDirectHelpcode::SlashFromPayload(L'2') &&
FanyImeDirectHelpcode::SlashFromPayload(L'1') && !FanyImeDirectHelpcode::EnabledFromPayload(L'0') &&
!FanyImeDirectHelpcode::IsValidPayload(L'3'));
// TSF(WCHAR)与引擎(char)共用同一条句中辅助码形状规则。
const auto accepts = [](const std::wstring &text) {
return FanyImeMidSentenceHelpcode::AcceptsMarker(text.data(), text.size());
};
CHECK(accepts(L"ulpb") && !accepts(L"ulp") && !accepts(L"") && !accepts(L"ulpb'"));
CHECK(!accepts(L"ulpb`") && !accepts(L"ulpb`x") && !accepts(L"ulpb`xY") && !accepts(L"ulpb`xi"));
CHECK(accepts(L"ulpb`xih") && !accepts(L"ulpb`xihf") && accepts(L"ulpb`xYih") && accepts(L"ulpb`xihfa"));
CHECK(FanyImeMidSentenceHelpcode::AcceptsMarker("ni'hc", 5) &&
!FanyImeMidSentenceHelpcode::AcceptsMarker("ni'h", 4));
// 光标移回句中:只看光标前的部分,光标后已是这个音节的段时不收。
const auto accepts_at = [](const std::wstring &text, std::size_t caret) {
return FanyImeMidSentenceHelpcode::AcceptsMarkerAt(text.data(), text.size(), caret);
};
CHECK(accepts_at(L"ulpbih", 2) && accepts_at(L"ulpbih", 4) && accepts_at(L"ulpbih", 6));
CHECK(!accepts_at(L"ulpbih", 0) && !accepts_at(L"ulpbih", 3) && !accepts_at(L"ulpbih", 7));
CHECK(!accepts_at(L"ul`xpb", 2) && accepts_at(L"ul`xpb", 6) && !accepts_at(L"ul`xpb", 3));
// 大写触发:完整音节后的大写字母自己开一段(≡ 反引号 + 这个字母),紧跟的大写字母是第二码。
const auto upper_marker = [](const std::wstring &text) {
return FanyImeMidSentenceHelpcode::AcceptsMarker(text.data(), text.size(), true);
};
CHECK(!upper_marker(L"ulX") && upper_marker(L"ulXpb") && !upper_marker(L"ulXp") && upper_marker(L"ulXYpb"));
// 关着时大写字母只是普通的一个键:ulXpb 是 5 键(奇数),ulXpbi 是 6 键。
CHECK(!accepts(L"ulXpb") && accepts(L"ulXpbi"));
const auto upper_at = [](const std::wstring &text, std::size_t caret) {
return FanyImeMidSentenceHelpcode::AcceptsMarkerAt(text.data(), text.size(), caret, true);
};
CHECK(!upper_at(L"ulXpb", 2) && upper_at(L"ulXpb", 5) && upper_at(L"ulpb", 2));
const auto second_at = [](const std::wstring &text, std::size_t caret, bool upper) {
return FanyImeMidSentenceHelpcode::AcceptsSecondCodeAt(text.data(), text.size(), caret, upper);
};
CHECK(second_at(L"ulX", 3, true) && !second_at(L"ulXY", 4, true) && !second_at(L"ulX", 3, false));
CHECK(second_at(L"ul`x", 4, false) && !second_at(L"ul`xY", 5, false) && !second_at(L"ul", 2, true));
const auto semicolon_at = [](const std::wstring &text, bool upper) {
return FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt(text.data(), text.size(), text.size(), upper);
};
// 关着时与原来的规则一致(只看最后一个 ');开着时大写段不算键:nihkXb 里 b 后面接 ; 是 b; 音节。
CHECK(semicolon_at(L"nihkb", false) && !semicolon_at(L"nihkXb", false) && semicolon_at(L"nihkXb", true));
CHECK(semicolon_at(L"ni'b", false) && !semicolon_at(L"nihk", true));
// 末尾辅助码(间接):触发键记成 |,后面至多两码、大小写都算;只在输入串末尾、完整音节之后能开段。
using FanyImeMidSentenceHelpcode::UppercaseTriggers;
const auto end_at = [](const std::wstring &text, std::size_t caret, UppercaseTriggers triggers) {
return FanyImeMidSentenceHelpcode::AcceptsEndMarkerAt(text.data(), text.size(), caret, triggers);
};
CHECK(end_at(L"ulpb", 4, {}) && !end_at(L"ulp", 3, {}) && !end_at(L"ulpb", 2, {}) && !end_at(L"", 0, {}));
CHECK(!end_at(L"ulpb|", 5, {}) && !end_at(L"ulpb|xy", 7, {}) && end_at(L"ul`xpb", 6, {}));
// 末尾段之后这一节从零数起:再敲两个键才又是完整音节。
CHECK(!accepts(L"ulpb|xy") && !accepts(L"ulpb|xyi") && accepts(L"ulpb|xyih"));
// 末尾大写触发:段首大写,第二码大小写都算;与句中大写触发互斥,都给时按句中。
const UppercaseTriggers end_upper(false, true);
CHECK(!FanyImeMidSentenceHelpcode::AcceptsMarker(L"ulpbXy", 6, end_upper) &&
FanyImeMidSentenceHelpcode::AcceptsMarker(L"ulpbXyih", 8, end_upper) &&
FanyImeMidSentenceHelpcode::AcceptsMarker(L"ulpbXy", 6, false));
CHECK(!UppercaseTriggers(true, true).end && UppercaseTriggers(true, true).mid);
const auto end_blocks = [](const std::wstring &text, UppercaseTriggers triggers) {
std::wstring kinds;
FanyImeMidSentenceHelpcode::ScanBlocks(
text.data(), text.size(), triggers,
[&](std::size_t begin, std::size_t end, std::size_t, FanyImeMidSentenceHelpcode::BlockKind kind) {
kinds += kind == FanyImeMidSentenceHelpcode::BlockKind::End ? L'E' : L'M';
kinds += std::to_wstring(begin) + L"-" + std::to_wstring(end) + L";";
});
return kinds;
};
CHECK(end_blocks(L"ulpb|xy", {}) == L"E4-7;" && end_blocks(L"ulpb|x", {}) == L"E4-6;");
CHECK(end_blocks(L"ul`xpb|XY", {}) == L"M2-4;E6-9;" && end_blocks(L"ulpbXy", end_upper) == L"E4-6;");
CHECK(end_blocks(L"ulpbXY", true) == L"M4-6;" && end_blocks(L"ulpbXy", {}) == L"");
// 句中段的第二码只认句中段。
CHECK(!FanyImeMidSentenceHelpcode::AcceptsSecondCodeAt(L"ulpb|x", 6, 6, {}) &&
!FanyImeMidSentenceHelpcode::AcceptsSecondCodeAt(L"ulpbX", 5, 5, end_upper));
// 末尾段后面的 ; 不是韵母;末尾段之后另起的一节照常数奇偶。
CHECK(!semicolon_at(L"ulpb|xy", false) && !semicolon_at(L"ulpb|x", false) && semicolon_at(L"ulpb|xyi", false));
CHECK(!FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt(L"ulpbXy", 6, 6, end_upper) &&
FanyImeMidSentenceHelpcode::AcceptsSemicolonFinalAt(L"ulpbXyi", 7, 7, end_upper));
// EndHelpcodeTriggersChanged 的载荷。
CHECK(FanyImeEndHelpcode::PayloadFor<wchar_t>(false, false, false) == L'0' &&
FanyImeEndHelpcode::PayloadFor<wchar_t>(true, true, true) == L'7' &&
FanyImeEndHelpcode::PayloadFor<wchar_t>(false, true, false) == L'2');
CHECK(FanyImeEndHelpcode::FlagsFromPayload(L'5') ==
(FanyImeEndHelpcode::kBacktick | FanyImeEndHelpcode::kUppercase) &&
!FanyImeEndHelpcode::IsValidPayload(L'8') && FanyImeEndHelpcode::FlagsFromPayload(L'x') == 0);
// Shift+T 指定日期时间的形状规则,TSF(WCHAR)、Server 与引擎(char)共用。
const auto date_time_at = [](const std::wstring &text, std::size_t caret, wchar_t ch) {
return FanyImeDateTimeInput::AcceptsAt(text.data(), text.size(), caret, ch);
};
const auto date_time_end = [&](const std::wstring &text, wchar_t ch) {
return date_time_at(text, text.size(), ch);
};
CHECK(date_time_end(L"T", L'2') && date_time_end(L"T2024122", L'5') && date_time_end(L"T20241225", L'1'));
CHECK(date_time_end(L"T2024122514", L':') && date_time_end(L"T2024122514:30", L':'));
CHECK(!date_time_end(L"T2024122514:30:00", L'1') && !date_time_end(L"T20241225", L':'));
CHECK(date_time_end(L"T2024", L'/') && date_time_end(L"T2024/1", L'2') && date_time_end(L"T2024/12", L'/'));
CHECK(!date_time_end(L"T2024/12/25", L'1') && !date_time_end(L"T2024/12/25", L'/'));
CHECK(date_time_end(L"T12", L'/') && date_time_end(L"T9", L':') && date_time_end(L"T9:0", L'5'));
CHECK(!date_time_end(L"T123", L'/') && !date_time_end(L"T123", L':') && !date_time_end(L"T12/25", L':'));
CHECK(!date_time_end(L"T", L'/') && !date_time_end(L"T", L':') && !date_time_end(L"Trq", L'1'));
CHECK(!date_time_end(L"U12", L'3') && !date_time_end(L"T2024", L'a') && !date_time_end(L"T", L'!'));
// 光标在中间:插入后的整串仍要能接成某种形状;光标不能停在开头的 T 前面。
CHECK(!date_time_at(L"T2024/12", 1, L'1') && date_time_at(L"T202412", 5, L'1') && !date_time_at(L"T2", 0, L'1'));
CHECK(FanyImeDateTimeInput::MatchComplete("20241225", 8).shape == FanyImeDateTimeInput::Shape::Date);
CHECK(FanyImeDateTimeInput::MatchComplete("2024122514", 10).shape == FanyImeDateTimeInput::Shape::DateTime);
CHECK(FanyImeDateTimeInput::MatchComplete("2024122514:30:05", 16).group_count == 3);
CHECK(FanyImeDateTimeInput::MatchComplete("2024/1/5", 8).shape == FanyImeDateTimeInput::Shape::YearMonthDay);
CHECK(FanyImeDateTimeInput::MatchComplete("2024/12", 7).shape == FanyImeDateTimeInput::Shape::YearMonth);
CHECK(FanyImeDateTimeInput::MatchComplete("12/25", 5).shape == FanyImeDateTimeInput::Shape::MonthDay);
CHECK(FanyImeDateTimeInput::MatchComplete("9:05", 4).shape == FanyImeDateTimeInput::Shape::Time);
CHECK(FanyImeDateTimeInput::MatchComplete("1430", 4).shape == FanyImeDateTimeInput::Shape::None);
CHECK(FanyImeDateTimeInput::MatchComplete("12/", 3).shape == FanyImeDateTimeInput::Shape::None);
CHECK(FanyImeDateTimeInput::MatchComplete("9:5", 3).shape == FanyImeDateTimeInput::Shape::None);
static_assert(FanyImeDateTimeInput::AcceptsAt("T2024", 5, 5, '/'), "the date shape must be usable at compile time");
const std::wstring voice(1000, L'x');
const auto frames = FanyImeVoiceCompositionPipe::EncodeSnapshot(voice, 7);
CHECK(FanyImeVoiceCompositionPipe::AssembleFrames(frames) == voice);
auto incomplete = frames;
incomplete.erase(incomplete.begin());
CHECK(FanyImeVoiceCompositionPipe::AssembleFrames(incomplete).empty());
// A middle frame legally carries flags 0, so an unterminated packet must be rejected on the chunk, not accepted
// because data[0] happens to be a NUL.
std::array<wchar_t, FanyImeVoiceCompositionPipe::kPacketChars> packet{};
packet.fill(L'x');
packet[0] = 0; // middle frame
packet[1] = 7; // generation
CHECK(!FanyImeVoiceCompositionPipe::ParseFrame(packet.data()).valid);
packet[FanyImeVoiceCompositionPipe::kPacketChars - 1] = 0;
const auto middle = FanyImeVoiceCompositionPipe::ParseFrame(packet.data());
CHECK(middle.valid && !middle.first && !middle.last && middle.generation == 7);
CHECK(middle.chunk == std::wstring(FanyImeVoiceCompositionPipe::kMaxChunkChars, L'x'));
const auto blank = FanyImeVoiceCompositionPipe::EncodeSnapshot(std::wstring(), 7);
CHECK(blank.size() == 1);
std::array<wchar_t, FanyImeVoiceCompositionPipe::kPacketChars> header{};
std::copy(blank[0].begin(), blank[0].end(), header.begin());
const auto empty = FanyImeVoiceCompositionPipe::ParseFrame(header.data());
CHECK(empty.valid && empty.first && empty.last && empty.chunk.empty());
// Statistics frames: fixed 28-byte header plus 24-byte events; the Go stats
// tool mirrors these numbers in internal/frames, so both sides must change
// together.
CHECK(FANY_IME_STATS_MAGIC == 0x54415453);
CHECK(FANY_IME_STATS_VERSION == 1);
CHECK(sizeof(FanyImeStatsBatchHeader) == 28);
CHECK(sizeof(FanyImeStatsEvent) == 24);
CHECK(offsetof(FanyImeStatsEvent, timestamp_utc_ft) == 0);
CHECK(offsetof(FanyImeStatsEvent, cjk) == 8);
CHECK(sizeof(FanyImeStatsBatchHeader) + 256 * sizeof(FanyImeStatsEvent) <= FANY_IME_STATS_MAX_FRAME_BYTES);
std::cout << "Windows IPC layout, negotiation, upgrade, voice and stats contracts passed\n";
}