- 快捷键页「翻页方式」新增「左 / 右(横排时移动候选项)」,与「上 / 下」各自独立,默认关闭, 只在候选窗为横排时生效。配置项 general.candidate_horizontal_arrow_navigation 已写入出厂模板。 - 光标在拼音串尾时右键移动高亮(到页末翻页),左键往回移高亮,高亮回到第一个候选后再按才移动光标; 光标在串中间时右键先把光标移回串尾,再按才移动高亮。Ctrl+左/右与 UILess 宿主不变。 - 新增 worker 消息 HorizontalArrowSelectionChanged(39)和按键标记 FanyImePipeFlags::HorizontalArrowSelection: 开关生效时 TSF 把候选态下的左右键交给 Server 决定,移光标时 Server 用 CompositionRestored 回光标, 移高亮时回 MoveSelection*,TSF 不再自己预测光标。旧 DLL 忽略新消息,保持原行为。 Closes #779 Closes #895
470 lines
32 KiB
C++
470 lines
32 KiB
C++
#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";
|
||
}
|