diff --git a/server/MetasequoiaImeEngine b/server/MetasequoiaImeEngine index 6169c7de..15b24889 160000 --- a/server/MetasequoiaImeEngine +++ b/server/MetasequoiaImeEngine @@ -1 +1 @@ -Subproject commit 6169c7defbf766e2f774ef1962d925fe48cdb621 +Subproject commit 15b248894912c916650648d99abe1eab2cb888ae diff --git a/server/src/ipc/candidate_selection_policy.h b/server/src/ipc/candidate_selection_policy.h index 933354c6..2a7060df 100644 --- a/server/src/ipc/candidate_selection_policy.h +++ b/server/src/ipc/candidate_selection_policy.h @@ -24,7 +24,7 @@ constexpr bool ShouldEnterCreatingWord(CandidateSource source, bool continues_co // cloud: Chinese, cloud, AI, English, emoji, kaomoji // cloud only: Chinese, cloud, English, emoji, kaomoji // base: Chinese, English, emoji, kaomoji -inline void NormalizeMixedCandidateOrder(std::vector &items) +inline void NormalizeMixedCandidateOrder(std::vector &items, size_t local_prefix_slots = 1) { std::vector local_candidates; std::vector english_candidates; @@ -67,7 +67,7 @@ inline void NormalizeMixedCandidateOrder(std::vector &items) items.insert(items.begin() + offset, std::move(candidate)); }; - size_t slot = 1; + size_t slot = (std::min)(local_prefix_slots, items.size()); if (cloud_candidate) { insert_at(slot++, std::move(*cloud_candidate)); diff --git a/server/src/ipc/event_listener.cpp b/server/src/ipc/event_listener.cpp index a40565a7..96d7bf25 100644 --- a/server/src/ipc/event_listener.cpp +++ b/server/src/ipc/event_listener.cpp @@ -29,6 +29,7 @@ #include #include "global/globals.h" #include "MetasequoiaImeEngine/common/helpcode_utils.h" +#include "MetasequoiaImeEngine/japanese/romaji_converter.h" #include "MetasequoiaImeEngine/quanpin/quanpin_query.h" #include "MetasequoiaImeEngine/user_dictionary/user_dictionary_journal.h" #include "ipc/event_listener.h" @@ -2951,7 +2952,10 @@ void ApplyCloudCandidate(const std::string &candidate, const std::string &pinyin size_t insert_index = items.size() >= 1 ? 1 : 0; items.insert(items.begin() + insert_index, WordItem(pinyin, candidate, 1, CandidateSource::CloudSuggestion)); - FanyImeIpc::NormalizeMixedCandidateOrder(items); + const bool preserve_single_kana_pair = + g_inputSession->current_scheme_type() == SchemeType::JapaneseRomaji && + japanese::IsSingleKanaConversion(japanese::ConvertRomaji(g_inputSession->get_pinyin_sequence())); + FanyImeIpc::NormalizeMixedCandidateOrder(items, preserve_single_kana_pair ? 2 : 1); g_inputSession->cache_dynamic_candidate(cloud_query_state.cache_key, candidate, CandidateSource::CloudSuggestion); Global::cloud_candidate = {true, candidate, cloud_query_state.committed_pinyin}; diff --git a/server/src/session/engine_input_session.cpp b/server/src/session/engine_input_session.cpp index b85ab5b3..67d5a603 100644 --- a/server/src/session/engine_input_session.cpp +++ b/server/src/session/engine_input_session.cpp @@ -275,10 +275,15 @@ const std::string &EngineInputSession::get_pinyin_segmentation() const std::string EngineInputSession::get_pinyin_segmentation_with_cases() const { - if (is_wubi() || is_japanese()) + if (is_wubi()) { return request().raw_input; } + if (is_japanese()) + { + return request().raw_input_with_cases.empty() ? request().raw_input + : request().raw_input_with_cases; + } if (is_shuangpin() && GetConfiguredShuangpinPreeditMode() == "shuangpin") { std::string preedit = request().raw_segmentation.empty() ? request().raw_input : request().raw_segmentation; diff --git a/server/tests/src/test_engine_shuangpin_session.cpp b/server/tests/src/test_engine_shuangpin_session.cpp index f3f9c3b7..37c1cd0f 100644 --- a/server/tests/src/test_engine_shuangpin_session.cpp +++ b/server/tests/src/test_engine_shuangpin_session.cpp @@ -134,6 +134,21 @@ TEST_CASE(KaomojiMixedCandidateSitsRightAfterEmoji) REQUIRE_EQ(items[5].source, CandidateSource::Kaomoji); } +TEST_CASE(JapaneseSingleKanaPairStaysAheadOfCloudCandidate) +{ + std::vector items = { + WordItem("Ka", "か", 1000000, CandidateSource::Generated), + WordItem("Ka", "カ", 999999, CandidateSource::Generated), + WordItem("ka", "蚊", 1, CandidateSource::CloudSuggestion), + WordItem("ka", "科", 100, CandidateSource::Database), + }; + + FanyImeIpc::NormalizeMixedCandidateOrder(items, 2); + REQUIRE_EQ(items[0].word, std::string("か")); + REQUIRE_EQ(items[1].word, std::string("カ")); + REQUIRE_EQ(items[2].source, CandidateSource::CloudSuggestion); +} + TEST_CASE(EngineShuangpinAiCandidateConsumesFullRawInput) { EngineInputSession session(SchemeType::Shuangpin); diff --git a/server/tests/src/test_japanese_romaji.cpp b/server/tests/src/test_japanese_romaji.cpp index fef7d502..2e8a5bb8 100644 --- a/server/tests/src/test_japanese_romaji.cpp +++ b/server/tests/src/test_japanese_romaji.cpp @@ -4,6 +4,7 @@ #include "MetasequoiaImeEngine/japanese/japanese_matrix_search.h" #include "MetasequoiaImeEngine/providers/japanese_candidate_provider.h" #include "MetasequoiaImeEngine/schemes/japanese_romaji_scheme.h" +#include "src/session/engine_input_session.h" #include #include #include @@ -76,6 +77,19 @@ TEST_CASE(JapaneseRomajiSchemePreservesTypedCodeAndShowsKanaSegmentation) REQUIRE_EQ(request.segmentation, std::string("にほんご")); } +TEST_CASE(JapanesePreeditPreservesTypedCasesAcrossEngineAndCandidateUi) +{ + EngineInputSession session(SchemeType::JapaneseRomaji); + for (const char ch : std::string("NiHonGo")) + { + const char vk = static_cast(std::toupper(static_cast(ch))); + session.handle_key(static_cast(vk), 0, static_cast(ch)); + } + REQUIRE_EQ(session.get_pinyin_sequence(), std::string("nihongo")); + REQUIRE_EQ(session.get_pinyin_sequence_with_cases(), std::string("NiHonGo")); + REQUIRE_EQ(session.get_pinyin_segmentation_with_cases(), std::string("NiHonGo")); +} + TEST_CASE(JapaneseProviderCombinesGeneratedKanaAndSqliteCandidates) { const auto path = CreateJapaneseDatabase(); @@ -90,6 +104,17 @@ TEST_CASE(JapaneseProviderCombinesGeneratedKanaAndSqliteCandidates) REQUIRE(ContainsWord(kana_candidates, "にほんご")); REQUIRE(ContainsWord(kana_candidates, "ニホンゴ")); + QueryRequest single_kana; + single_kana.scheme = SchemeType::JapaneseRomaji; + single_kana.raw_input = "ka"; + single_kana.raw_input_with_cases = "Ka"; + single_kana.valid = true; + const auto single_kana_candidates = provider.query(single_kana); + REQUIRE(single_kana_candidates.size() >= 2); + REQUIRE_EQ(single_kana_candidates[0].word, std::string("か")); + REQUIRE_EQ(single_kana_candidates[1].word, std::string("カ")); + REQUIRE_EQ(single_kana_candidates[0].pinyin, std::string("Ka")); + QueryRequest direct; direct.scheme = SchemeType::JapaneseRomaji; direct.raw_input = "qa"; @@ -185,6 +210,22 @@ TEST_CASE(JapaneseMatrixSearchDecodesWholeSentenceWhenModelAvailable) found_kawaii = found_kawaii || lemma.surface == "可愛い" || lemma.reading == "かわいい" || lemma.surface == "かわいい"; REQUIRE(found_kawaii); + + const auto exact = decoder.ExactLemmas("にほん", 64); + const auto best_exact = decoder.ExactLemmas("にほん", 1); + REQUIRE(!exact.empty()); + REQUIRE(best_exact.size() == 1); + REQUIRE(best_exact.front().word_cost == exact.front().word_cost); + for (size_t index = 1; index < exact.size(); ++index) + REQUIRE(exact[index - 1].word_cost <= exact[index].word_cost); + + const auto short_prefix = decoder.PrefixLemmas("か", 32); + REQUIRE(short_prefix.size() == 32); + for (size_t index = 1; index < short_prefix.size(); ++index) + REQUIRE(short_prefix[index - 1].word_cost <= short_prefix[index].word_cost); + + const auto pending_consonant = search.Search("k", 12); + REQUIRE(!pending_consonant.empty()); } TEST_CASE(JapaneseSentenceDecoderReadsGeneratedMozcModelWhenAvailable)