Files
msime-windows/engine/core/input_session_composition.cpp
T

907 lines
36 KiB
C++
Raw 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 "input_session.h"
#include "../common/helpcode_utils.h"
#include "../quanpin/quanpin_utils.h"
#include "../shuangpin/shuangpin_query.h"
#include "../shuangpin/shuangpin_utils.h"
#include "../japanese/romaji_converter.h"
#include <algorithm>
#include <cctype>
namespace metasequoia
{
namespace
{
std::string remove_delimiters(const std::string &segmented)
{
std::string normalized;
normalized.reserve(segmented.size());
for (const char ch : segmented)
{
if (ch != '\'')
{
normalized.push_back(ch);
}
}
return normalized;
}
void remove_consumed_leading_separators(std::string &raw_input, std::string &raw_input_with_cases)
{
size_t count = 0;
while (count < raw_input.size() && raw_input[count] == '\'')
{
++count;
}
raw_input.erase(0, count);
count = 0;
while (count < raw_input_with_cases.size() && raw_input_with_cases[count] == '\'')
{
++count;
}
raw_input_with_cases.erase(0, count);
}
// 整句落库的长度上限。词库里的短语本身就不超过这个音节数(对标
// quanpin::WordLatticeOptions::max_phrase_syllables),再长的整句只是这一次输入
// 的产物,落库除了撑大用户词库没有别的作用。
constexpr size_t kMaxLearnedSentenceSyllables = 7;
std::string normalize_canonical_pinyin_for_word(const std::string &pinyin, const std::string &word)
{
if (pinyin.empty())
{
return {};
}
const auto segments = quanpin::split_segments(pinyin);
if (segments.empty() || segments.size() != HelpcodeUtils::count_han_chars(word))
{
return {};
}
for (const auto &segment : segments)
{
if (segment.empty() || !quanpin::is_complete_pinyin_input(segment))
{
return {};
}
}
return quanpin::join_segments(segments);
}
std::string append_canonical_pinyin(const std::string &prefix, const std::string &suffix)
{
if (prefix.empty())
{
return suffix;
}
if (suffix.empty())
{
return {};
}
return prefix + "'" + suffix;
}
struct ShuangpinCompositionBase
{
std::string raw_input;
std::string raw_input_with_cases;
std::string effective_raw_input;
std::string effective_raw_input_with_cases;
size_t helpcode_length = 0;
};
ShuangpinCompositionBase ResolveShuangpinCompositionBase(const QueryRequest &request, const ShuangpinProfile &profile)
{
ShuangpinCompositionBase base{
request.raw_input, request.raw_input_with_cases.empty() ? request.raw_input : request.raw_input_with_cases};
base.effective_raw_input = shuangpin::remove_manual_delimiters(base.raw_input);
base.effective_raw_input_with_cases = shuangpin::remove_manual_delimiters(base.raw_input_with_cases);
if (!request.enable_shuangpin_helpcode || base.effective_raw_input.empty())
{
return base;
}
const auto has_complete_unseparated_base = [&](size_t helpcode_length) {
if (base.effective_raw_input.size() <= helpcode_length)
{
return false;
}
const size_t pure_length = base.effective_raw_input.size() - helpcode_length;
const size_t raw_prefix_length = shuangpin::raw_length_for_effective_prefix(base.raw_input, pure_length);
// An apostrophe immediately before the suffix makes that suffix a
// user-defined pinyin segment, not an auxiliary code.
if (raw_prefix_length < base.raw_input.size() && base.raw_input[raw_prefix_length] == '\'')
{
return false;
}
return shuangpin::is_complete_input(base.raw_input.substr(0, raw_prefix_length), profile);
};
if (shuangpin::detect_active_double_helpcode_length(base.raw_input, base.raw_input_with_cases, profile) == 2)
{
base.helpcode_length = 2;
return base;
}
if (base.effective_raw_input.size() % 2 == 1 && base.effective_raw_input.size() > 1)
{
if (has_complete_unseparated_base(1))
{
base.helpcode_length = 1;
}
}
return base;
}
bool HasActiveQuanpinHelpcode(const QueryRequest &request)
{
return request.enable_quanpin_helpcode &&
quanpin::detect_active_helpcode_length(request.raw_input, request.raw_input_with_cases) > 0;
}
std::string ResolveShuangpinCloudCacheKey(const QueryRequest &request, const ShuangpinProfile &profile)
{
const auto base = ResolveShuangpinCompositionBase(request, profile);
if (base.helpcode_length > 0 && base.effective_raw_input.size() >= base.helpcode_length)
{
const size_t base_length = base.effective_raw_input.size() - base.helpcode_length;
return base.raw_input.substr(0, shuangpin::raw_length_for_effective_prefix(base.raw_input, base_length));
}
return base.raw_input;
}
std::string ResolveQuanpinCloudCacheKey(const QueryRequest &request)
{
return quanpin::strip_active_helpcodes(request.raw_input, request.raw_input_with_cases);
}
// Same four-bit mapping as autocorrect_types_from_request (engine.cpp): the
// request only carries the two legacy bools, and either one on also enables
// deletion and insertion so the preedit rebuild sees the same correction space
// as the query.
unsigned QuanpinAutocorrectTypes(const QueryRequest &request)
{
const unsigned legacy =
(request.enable_quanpin_autocorrect_transposition ? quanpin::kAutocorrectTransposition : 0u) |
(request.enable_quanpin_autocorrect_neighbor ? quanpin::kAutocorrectNeighbor : 0u);
return legacy == 0 ? 0u : legacy | quanpin::kAutocorrectDeletion | quanpin::kAutocorrectInsertion;
}
std::string QuanpinLettersWithoutDelimiters(const std::string &text)
{
std::string letters;
letters.reserve(text.size());
for (const char ch : text)
{
if (ch != '\'')
{
letters.push_back(ch);
}
}
return letters;
}
// Folds letters for autocorrect display comparisons: lowercases and strips
// manual delimiters, and maps the u-umlaut style 'v' spelling onto 'u'. This
// equivalence is what classifies length-preserving spelling aliases (jv->ju,
// nue->nve) as "explainable by the cut" so the preedit can be rebuilt with
// separators; removing it would drop separators for alias-typed input. It only
// ever compares two internally derived strings, never gates a user-facing
// mark — the dictionary-side corrected_from contract lives in
// quanpin_dictionary.cpp and treats v/u as distinct letters on purpose.
std::string FoldQuanpinAutocorrectLetters(const std::string &text)
{
std::string folded;
folded.reserve(text.size());
for (const char ch : text)
{
if (ch == '\'')
{
continue;
}
const char lower = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
folded.push_back(lower == 'v' ? 'u' : lower);
}
return folded;
}
// Folded letters of a cut's syllable sequence, for comparing the BFS reading
// against the scheme segmentation reading.
std::string CutSyllableLetters(const quanpin::AutocorrectCut &cut)
{
std::string letters;
for (const auto &segment : cut.segments)
{
letters += segment.syllable;
}
return FoldQuanpinAutocorrectLetters(letters);
}
// Rebuilds the preedit from the cased input letters with separators at the raw
// spans the correction BFS reports. Letters (case included) are preserved
// verbatim; manual delimiters are replaced by the actual cut positions.
std::string RebuildQuanpinDisplayFromCut(const std::string &cased_input, const quanpin::AutocorrectCut &cut)
{
std::string display;
display.reserve(cased_input.size() + cut.segments.size());
size_t letter_index = 0;
size_t boundary_index = 0;
const size_t boundary_count = cut.segments.empty() ? 0 : cut.segments.size() - 1;
for (const char ch : cased_input)
{
if (ch == '\'')
{
continue;
}
display.push_back(ch);
++letter_index;
if (boundary_index < boundary_count &&
letter_index == cut.segments[boundary_index].start + cut.segments[boundary_index].raw_text.size())
{
display.push_back('\'');
++boundary_index;
}
}
return display;
}
// The preedit must always show the letters the user actually typed (PRD R5).
// Two layers can rewrite them into canonical pinyin: the scheme alias table
// (sahng -> shang, baked into raw_segmentation) and the dictionary correction
// BFS (shabg -> shang, which only re-separates). Both are rebuilt here from
// the raw letters with separators at the actual cut positions; when the BFS
// cannot explain a rewrite (length-changing aliases such as mihng -> ming) the
// raw letters are shown without separators. The rebuild is deliberately
// switch-independent: the alias layer rewrites letters regardless of the
// autocorrect switches, and AC1 only constrains the candidate list.
std::string BuildQuanpinAutocorrectDisplay(const QueryRequest &request)
{
const std::string &cased = request.raw_input_with_cases.empty() ? request.raw_input : request.raw_input_with_cases;
const std::string base = request.raw_segmentation.empty() ? cased : request.raw_segmentation;
if (request.raw_input.empty() || cased.empty())
{
return base;
}
const unsigned types = QuanpinAutocorrectTypes(request);
const std::string folded_input = FoldQuanpinAutocorrectLetters(cased);
const bool letters_rewritten = FoldQuanpinAutocorrectLetters(QuanpinLettersWithoutDelimiters(base)) != folded_input;
// Fast path: the scheme kept the typed letters and either no correction
// type is enabled or the input is already a complete pinyin spelling, so
// no correction interpretation exists to draw separators from.
if (!letters_rewritten && (types == 0 || quanpin::is_complete_pinyin_input(request.raw_input)))
{
return base;
}
// Jianpin guard, mirroring resolve_series_query in the dictionary layer: a
// legal syllable plus at most one trailing letter is user intent, never a
// typo. The deletion table explains shapes like "zheg" -> zheng, so without
// this guard the preedit would lose the scheme separators once the deletion
// bit rides along with the legacy switches.
if (quanpin::looks_like_syllable_with_jianpin_tail(request.raw_input))
{
// The typed spelling is legal, but the scheme alias table may still
// have re-segmented it into a reading with copied letters (dongua ->
// dong'gua, haoyonga -> hao'yong'ga). The dictionary ranks those
// readings against the exact one by frequency, yet the preedit must
// show the typed letters (PRD R5), so a rewritten base may never
// reach it verbatim.
return letters_rewritten ? QuanpinLettersWithoutDelimiters(cased) : base;
}
const auto cut = quanpin::autocorrect_cut_detail(folded_input, types);
// When the scheme rewrote the letters, the query resolved through the alias
// layer, so the preedit may only draw separators from the BFS when both
// layers explain the letters identically ("sahnghao" -> shang'hao).
// Otherwise the deletion bit would re-separate "sahng" as sa'hng while the
// candidates actually come from the alias reading shang -- the old code
// never noticed because the cut happened to be empty without it.
if (!cut.empty() && (!letters_rewritten || CutSyllableLetters(cut) == FoldQuanpinAutocorrectLetters(
QuanpinLettersWithoutDelimiters(base))))
{
return RebuildQuanpinDisplayFromCut(cased, cut);
}
// The BFS cannot explain the input (e.g. a length-changing alias such as
// mihng -> ming): fall back to the plain raw letters when the letters were
// rewritten, otherwise keep the scheme segmentation untouched.
return letters_rewritten ? QuanpinLettersWithoutDelimiters(cased) : base;
}
} // namespace
void InputSession::handle_engine_key(ImeKeyCode vk, ImeModifierMask modifiers_down, ImeCharacter wch)
{
engine_.handle_key(vk, modifiers_down, wch);
online_requests_.invalidate();
update_mixed_candidates();
}
void InputSession::recompute_candidates()
{
if (has_pending_pinyin_sequence_ || has_pending_pinyin_sequence_with_cases_)
{
apply_pending_sequence();
return;
}
engine_.handle_key(0, 0, 0);
update_mixed_candidates();
}
SchemeType InputSession::current_scheme_type() const
{
return engine_.current_scheme_type();
}
void InputSession::reset_state()
{
clear_pending_sequence();
reset_composition();
}
void InputSession::reset_cache()
{
engine_.reset_cache();
if (canonical_phrase_engine_)
canonical_phrase_engine_->reset_cache();
// 前缀候选是按文本缓存的,不跟着引擎缓存失效:只清缓存键,让下一次
// refresh_prefix_candidates 按新权重/选项重查;保留当前列表,避免 caret
// 激活期间出现空候选窗。
prefix_query_input_.clear();
}
void InputSession::reset_sentence_cache()
{
engine_.reset_sentence_cache();
if (canonical_phrase_engine_)
canonical_phrase_engine_->reset_sentence_cache();
// 前缀候选同样可能带整句,理由同 reset_cache。
prefix_query_input_.clear();
}
const std::vector<WordItem> &InputSession::get_candidates() const
{
return candidates();
}
bool InputSession::expand_initial_candidates()
{
return engine_.expand_initial_candidates();
}
std::optional<WordItem> InputSession::find_candidate(const std::string &key, const std::string &value)
{
return engine_.find_candidate(scheme(), key, value);
}
const QueryRequest &InputSession::request() const
{
return engine_.get_request();
}
const std::string &InputSession::get_pinyin_sequence() const
{
return request().raw_input;
}
const std::string &InputSession::get_pinyin_sequence_with_cases() const
{
return request().raw_input_with_cases.empty() ? request().raw_input : request().raw_input_with_cases;
}
const std::string &InputSession::get_pure_pinyin_sequence() const
{
return request().normalized_input;
}
const std::string &InputSession::get_pinyin_segmentation() const
{
return request().normalized_segmentation.empty() ? request().segmentation : request().normalized_segmentation;
}
std::string InputSession::get_pinyin_segmentation_with_cases() const
{
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() && shuangpin_preedit_uses_raw_)
{
std::string preedit = request().raw_segmentation.empty() ? request().raw_input : request().raw_segmentation;
if (!request().raw_input_with_cases.empty() && request().raw_input_with_cases.back() == '\'' &&
(preedit.empty() || preedit.back() != '\''))
{
preedit.push_back('\'');
}
return preedit;
}
if (current_scheme_type() == SchemeType::Quanpin)
{
return BuildQuanpinAutocorrectDisplay(request());
}
std::string preedit =
request().normalized_segmentation.empty() ? request().segmentation : request().normalized_segmentation;
if (!request().raw_input_with_cases.empty() && request().raw_input_with_cases.back() == '\'' &&
(preedit.empty() || preedit.back() != '\''))
{
preedit.push_back('\'');
}
return preedit;
}
std::string InputSession::get_quanpin() const
{
return request().normalized_input;
}
bool InputSession::is_all_complete_pure_pinyin() const
{
if (is_wubi())
{
return request().valid;
}
if (is_japanese())
{
return japanese::ConvertRomaji(request().raw_input).complete;
}
if (is_shuangpin())
{
const auto base = ResolveShuangpinCompositionBase(request(), shuangpin_profile_);
if (base.helpcode_length > 0 && base.effective_raw_input.size() >= base.helpcode_length)
{
const size_t base_length = base.effective_raw_input.size() - base.helpcode_length;
return shuangpin::is_complete_input(
base.raw_input.substr(0, shuangpin::raw_length_for_effective_prefix(base.raw_input, base_length)),
shuangpin_profile_);
}
return shuangpin::is_complete_input(base.raw_input, shuangpin_profile_);
}
const auto &segmentation =
request().normalized_segmentation.empty() ? request().segmentation : request().normalized_segmentation;
return !segmentation.empty() && quanpin::is_complete_pinyin_input(segmentation);
}
bool InputSession::reads_as_pinyin() const
{
if (has_active_helpcode())
return true;
std::string raw = remove_delimiters(request().raw_input);
std::transform(raw.begin(), raw.end(), raw.begin(),
[](unsigned char ch) { return static_cast<char>(std::tolower(ch)); });
if (raw.empty())
return false;
if (is_shuangpin())
{
// 双拼两键一个音节,打到一半时末尾会剩一个声母键。
return raw.size() == 1 || shuangpin::is_complete_input(raw, shuangpin_profile_) ||
shuangpin::is_complete_input(raw.substr(0, raw.size() - 1), shuangpin_profile_);
}
if (current_scheme_type() == SchemeType::Quanpin)
return !quanpin::cut_one_piece_min_segments(raw, false).empty();
return false;
}
bool InputSession::wubi_unique_four_code() const
{
// Both modes spell words rather than codes; a wubi code is not composed inside them. No host
// sets either one on a wubi session today, but the guard keeps the pair meaningful if one does.
if (dedicated_english_mode_ || local_input_mode_ != LocalInputMode::None)
{
return false;
}
// A code holding z is a wildcard guess, not a code the typist committed to: the "only
// candidate" behind it is one arbitrary letter away from several others, and committing it
// silently takes away the one thing the key exists for -- seeing what matched. Same reason
// mixed input never reaches here (no table row contains z, so its z codes stay empty).
if (engine_.get_request().raw_input.find('z') != std::string::npos)
{
return false;
}
// is_wubi() covers "this is a wubi session", and engine_.wubi_code_is_complete() is false for
// every other scheme.
if (!is_wubi() || !engine_.wubi_code_is_complete())
{
return false;
}
// Only the code's own rows count: the pinyin candidates mixed input appends are not the answer
// the auto-commit looks for. A code the table answered with exactly one row is genuinely unique.
return wubi_native_candidate_count() == 1;
}
bool InputSession::wubi_four_code_is_complete() const
{
// Same guards as wubi_unique_four_code minus the candidate count: hosts commit the first
// candidate on the next key whether or not the code has one candidate or many. The pinyin
// candidates mixed input appends do not make a code the table never answered complete.
if (dedicated_english_mode_ || local_input_mode_ != LocalInputMode::None)
{
return false;
}
// Wildcard codes neither auto-commit nor top-commit: see wubi_unique_four_code.
if (engine_.get_request().raw_input.find('z') != std::string::npos)
{
return false;
}
return is_wubi() && engine_.wubi_code_is_complete() && wubi_native_candidate_count() > 0;
}
bool InputSession::has_active_helpcode() const
{
if (is_wubi() || is_japanese())
{
return false;
}
if (is_shuangpin())
{
return ResolveShuangpinCompositionBase(request(), shuangpin_profile_).helpcode_length > 0;
}
return HasActiveQuanpinHelpcode(request());
}
void InputSession::set_pinyin_sequence(const std::string &pinyin_sequence)
{
pending_pinyin_sequence_ = pinyin_sequence;
has_pending_pinyin_sequence_ = true;
}
void InputSession::set_pinyin_sequence_with_cases(const std::string &pinyin_sequence)
{
pending_pinyin_sequence_with_cases_ = pinyin_sequence;
has_pending_pinyin_sequence_with_cases_ = true;
}
int InputSession::store_user_phrase(std::string pinyin, std::string word)
{
return engine_.create_word(std::move(pinyin), std::move(word));
}
int InputSession::store_user_phrase_from_canonical_pinyin(std::string pinyin, std::string word)
{
// Both quanpin and shuangpin ultimately share the canonical quanpin
// dictionary. Do not feed a complete quanpin key back through the active
// shuangpin profile a second time.
if (!canonical_phrase_engine_)
canonical_phrase_engine_ = std::make_unique<QuanpinEngine>(paths_);
return canonical_phrase_engine_->create_word_from_canonical_pinyin(std::move(pinyin), std::move(word));
}
// 整句候选(词格 CandidateSource::Generated、Google 解码器 CandidateSource::Fallback、
// 神经整句 NeuralDesktop / NeuralKeyboard)
// 是猜出来的,词库里没有它那一行,所以调频对它无效:update_weight_by_pinyin_and_word
// 改的是 SQLite 里已存在的行,找不到行就什么也不做,用户于是发现自己选多少次都提不上来。
// 选中即落库才是对的处理:存成用户词组之后,下次同样的输入它以 UserDatabase 候选出现,
// 按 quanpin::generated_sentence_insert_position 的约定排在两条整句之前,之后再选还能
// 走正常调频。
std::optional<std::string> InputSession::learn_sentence_candidate(const WordItem &selected)
{
// 本地模式(U/K/E/M/J/Y/R、日期时间)和英文模式也用 Generated 装自己的候选,
// 那些不是拼音整句,不能往拼音用户词库里塞。五笔码表候选也不进这里:混输追加的整句
// 候选自带 Quanpin 方案,按候选自己的方案判断,不看会话方案。
if (local_input_mode_ != LocalInputMode::None || dedicated_english_mode_ || is_japanese() ||
is_wubi_native_candidate(selected))
{
return std::nullopt;
}
// 读音必须完整且音节数与字数对得上,否则落库的是个读音残缺的词条。双拼的整句
// 候选同样带 canonical quanpin,这里统一按全拼键处理。
const std::string canonical = normalize_canonical_pinyin_for_word(selected.canonical_pinyin, selected.word);
if (canonical.empty() || quanpin::split_segments(canonical).size() > kMaxLearnedSentenceSyllables)
{
return std::nullopt;
}
if (store_user_phrase_from_canonical_pinyin(canonical, selected.word) != 0)
{
return "Unable to persist the selected sentence.";
}
return std::nullopt;
}
int InputSession::remove_candidate(std::string pinyin, std::string word, SchemeType scheme)
{
if (!is_wubi() && remove_delimiters(request().raw_input).size() == 1)
{
return -1;
}
return engine_.delete_by_pinyin_and_word(scheme, std::move(pinyin), std::move(word));
}
int InputSession::cache_dynamic_candidate(const std::string &pinyin, const std::string &word, CandidateSource source)
{
const int cache_result = engine_.cache_dynamic_candidate(pinyin, word, source);
(void)engine_.cache_dynamic_candidate_for_current_request(word, source);
return cache_result;
}
InputSession::SelectionTransition InputSession::advance_composition_after_selection(
const std::string &selected_pinyin, const std::string &selected_word, const std::string &selected_canonical_pinyin,
SchemeType selected_scheme)
{
SelectionTransition transition;
transition.selected_canonical_pinyin = selected_canonical_pinyin;
transition.wubi_native = selected_scheme == SchemeType::Wubi;
if (is_japanese())
{
transition.full_pure_pinyin = request().raw_input;
transition.current_segmentation = request().segmentation;
transition.current_segmentation_with_cases = request().raw_input_with_cases;
return transition;
}
if (transition.wubi_native)
{
transition.full_pure_pinyin = request().normalized_input;
transition.current_segmentation = request().normalized_input;
transition.current_segmentation_with_cases = request().raw_input;
return transition;
}
if (is_shuangpin())
{
const auto base = ResolveShuangpinCompositionBase(request(), shuangpin_profile_);
const size_t word_pinyin_length = HelpcodeUtils::count_han_chars(selected_word) * 2;
const size_t total_input_length = base.effective_raw_input.size();
transition.full_pure_pinyin =
base.helpcode_length > 0 && total_input_length >= base.helpcode_length
? base.effective_raw_input.substr(0, total_input_length - base.helpcode_length)
: base.effective_raw_input;
size_t consumed_length = remove_delimiters(selected_pinyin).size();
if (base.helpcode_length > 0)
{
const size_t rest_start =
shuangpin::raw_length_for_effective_prefix(base.raw_input_with_cases, word_pinyin_length);
transition.consumed_raw_input_with_cases = base.raw_input_with_cases.substr(0, rest_start);
const size_t required_length = word_pinyin_length + base.helpcode_length;
transition.continues_composition =
required_length < total_input_length && word_pinyin_length < total_input_length;
if (transition.continues_composition)
{
const size_t rest_end = shuangpin::raw_length_for_effective_prefix(
base.raw_input_with_cases, total_input_length - base.helpcode_length);
const std::string rest_pinyin_sequence = base.raw_input.substr(rest_start, rest_end - rest_start);
std::string normalized_rest = rest_pinyin_sequence;
std::string cased_rest = base.raw_input_with_cases.substr(rest_start, rest_end - rest_start);
remove_consumed_leading_separators(normalized_rest, cased_rest);
engine_.replace_shuangpin_raw_input(normalized_rest, cased_rest);
online_requests_.invalidate();
update_mixed_candidates();
}
}
else
{
if (consumed_length == 0 || consumed_length > base.effective_raw_input.size())
{
consumed_length = (std::min)(word_pinyin_length, base.effective_raw_input.size());
}
const size_t consumed_raw_length =
shuangpin::raw_length_for_effective_prefix(base.raw_input_with_cases, consumed_length);
transition.consumed_raw_input_with_cases = base.raw_input_with_cases.substr(0, consumed_raw_length);
transition.continues_composition = consumed_length < transition.full_pure_pinyin.size();
if (transition.continues_composition)
{
const std::string rest_pinyin_sequence =
base.raw_input.substr(consumed_raw_length, base.raw_input.size() - consumed_raw_length);
const std::string rest_pinyin_sequence_with_cases = base.raw_input_with_cases.substr(
consumed_raw_length, base.raw_input_with_cases.size() - consumed_raw_length);
std::string normalized_rest = rest_pinyin_sequence;
std::string cased_rest = rest_pinyin_sequence_with_cases;
remove_consumed_leading_separators(normalized_rest, cased_rest);
engine_.replace_shuangpin_raw_input(normalized_rest, cased_rest);
online_requests_.invalidate();
update_mixed_candidates();
}
}
transition.current_segmentation = get_pinyin_segmentation();
transition.current_segmentation_with_cases = get_pinyin_segmentation_with_cases();
return transition;
}
transition.full_pure_pinyin = request().normalized_input;
const std::string current_segmentation =
request().normalized_segmentation.empty() ? request().segmentation : request().normalized_segmentation;
const std::string current_segmentation_with_cases = get_pinyin_segmentation_with_cases();
const std::string selected_pure_pinyin = remove_delimiters(selected_pinyin);
const std::string raw_input_without_helpcodes =
quanpin::strip_active_helpcodes(request().raw_input, request().raw_input_with_cases);
const std::string raw_input_with_cases_without_helpcodes =
quanpin::strip_active_helpcodes_with_cases(request().raw_input, request().raw_input_with_cases);
size_t consumed_raw_length =
shuangpin::raw_length_for_effective_prefix(raw_input_with_cases_without_helpcodes, selected_pure_pinyin.size());
transition.consumed_raw_input_with_cases = raw_input_with_cases_without_helpcodes.substr(0, consumed_raw_length);
transition.continues_composition = !selected_pure_pinyin.empty() &&
selected_pure_pinyin.size() < transition.full_pure_pinyin.size() &&
consumed_raw_length < raw_input_without_helpcodes.size();
if (transition.continues_composition)
{
std::string rest_raw_input = raw_input_without_helpcodes.substr(consumed_raw_length);
std::string rest_raw_input_with_cases = raw_input_with_cases_without_helpcodes.substr(consumed_raw_length);
remove_consumed_leading_separators(rest_raw_input, rest_raw_input_with_cases);
engine_.replace_active_raw_input(rest_raw_input, rest_raw_input_with_cases);
online_requests_.invalidate();
update_mixed_candidates();
transition.current_segmentation = get_pinyin_segmentation();
transition.current_segmentation_with_cases = get_pinyin_segmentation_with_cases();
return transition;
}
transition.current_segmentation = current_segmentation;
transition.current_segmentation_with_cases = current_segmentation_with_cases;
return transition;
}
InputSession::CloudQueryState InputSession::get_cloud_query_state() const
{
CloudQueryState state;
if (is_japanese())
{
state.cache_key = request().raw_input;
state.committed_pinyin = request().raw_input;
state.should_query = !request().raw_input.empty();
state.query_text = state.should_query ? request().raw_input : std::string{};
return state;
}
if (is_wubi())
{
state.cache_key = request().normalized_input;
state.committed_pinyin = request().normalized_input;
return state;
}
if (is_shuangpin())
{
const auto base = ResolveShuangpinCompositionBase(request(), shuangpin_profile_);
state.cache_key = ResolveShuangpinCloudCacheKey(request(), shuangpin_profile_);
state.committed_pinyin = shuangpin::remove_manual_delimiters(state.cache_key);
if (has_active_helpcode())
{
return state;
}
const char last =
base.effective_raw_input_with_cases.empty() ? '\0' : base.effective_raw_input_with_cases.back();
const bool ends_with_input_key = (last >= 'a' && last <= 'z') || last == ';';
state.should_query =
ends_with_input_key && shuangpin::is_complete_input(base.effective_raw_input, shuangpin_profile_);
if (state.should_query)
{
// 云输入拿的是带撇号的全拼,但 ü 要换成 inputtools 认的写法:它和本地的
// Google 解码器一样只认 nue/lue,nve'dai'dong'wu 会被它拆成 nv + e,
// 「虐待动物」于是变成「女蛾黛动物」。committed_pinyin / cache_key 不受
// 影响,仍是双拼原串。
const std::string quanpin_segmentation =
shuangpin::normalize_input_with_delimiters(state.cache_key, shuangpin_profile_);
state.query_text = quanpin::to_google_spelling(quanpin_segmentation);
}
return state;
}
state.committed_pinyin = request().normalized_input;
state.cache_key = ResolveQuanpinCloudCacheKey(request());
if (has_active_helpcode())
{
return state;
}
state.should_query = !request().normalized_input.empty();
// 云端要按用户看到的分词来查:手动把 qi'e'huan 分成三段,就该带着音节边界发给云接口,
// 否则云端自行贪心断句会当成 qie'huan(切换)。normalized_segmentation 保留了用户的手动
// 撇号(cut_pinyin_with_corrections 先按撇号切分),既定住了分词,也让 to_google_spelling
// 能逐音节把 nve / lve 换成云端认的 nue / lue 写法。分词缺失时退回裸串。
const std::string &segmentation =
request().normalized_segmentation.empty() ? request().normalized_input : request().normalized_segmentation;
state.query_text = quanpin::to_google_spelling(segmentation);
return state;
}
InputSession::CreatingWordProgress InputSession::update_creating_word_progress(
const std::string &current_pinyin, const std::string &current_word, const std::string &selected_word,
const SelectionTransition &selection_transition) const
{
CreatingWordProgress progress;
if (selection_transition.wubi_native)
{
progress.pinyin = current_pinyin.empty() ? selection_transition.full_pure_pinyin : current_pinyin;
progress.word = current_word + selected_word;
progress.preedit = progress.word;
progress.completed = true;
progress.can_store = false;
return progress;
}
const std::string selected_canonical =
normalize_canonical_pinyin_for_word(selection_transition.selected_canonical_pinyin, selected_word);
const bool prior_parts_are_storeable = current_word.empty() || !current_pinyin.empty();
if (prior_parts_are_storeable && !selected_canonical.empty())
{
progress.pinyin = append_canonical_pinyin(current_pinyin, selected_canonical);
}
progress.word = current_word + selected_word;
progress.preedit = progress.word + selection_transition.current_segmentation_with_cases;
progress.completed = !selection_transition.continues_composition;
progress.can_store =
progress.completed && !normalize_canonical_pinyin_for_word(progress.pinyin, progress.word).empty();
return progress;
}
bool InputSession::is_shuangpin() const
{
return current_scheme_type() == SchemeType::Shuangpin;
}
bool InputSession::is_wubi() const
{
return current_scheme_type() == SchemeType::Wubi;
}
bool InputSession::is_wubi_native_candidate(const WordItem &item)
{
return item.scheme == SchemeType::Wubi;
}
std::size_t InputSession::wubi_native_candidate_count() const
{
return static_cast<std::size_t>(std::count_if(candidates().begin(), candidates().end(),
[](const WordItem &item) { return is_wubi_native_candidate(item); }));
}
bool InputSession::is_japanese() const
{
return current_scheme_type() == SchemeType::JapaneseRomaji;
}
void InputSession::clear_pending_sequence()
{
pending_pinyin_sequence_.clear();
pending_pinyin_sequence_with_cases_.clear();
has_pending_pinyin_sequence_ = false;
has_pending_pinyin_sequence_with_cases_ = false;
}
void InputSession::apply_pending_sequence()
{
caret_.reset();
const std::string raw_input = has_pending_pinyin_sequence_ ? pending_pinyin_sequence_ : request().raw_input;
const std::string raw_input_with_cases =
has_pending_pinyin_sequence_with_cases_ ? pending_pinyin_sequence_with_cases_ : raw_input;
switch (current_scheme_type())
{
case SchemeType::Shuangpin:
engine_.replace_shuangpin_raw_input(raw_input, raw_input_with_cases);
break;
case SchemeType::Quanpin:
engine_.replace_quanpin_raw_input(raw_input, raw_input_with_cases);
break;
case SchemeType::Wubi:
engine_.replace_wubi_raw_input(raw_input, raw_input_with_cases);
break;
case SchemeType::JapaneseRomaji:
engine_.replace_japanese_raw_input(raw_input, raw_input_with_cases);
break;
}
clear_pending_sequence();
online_requests_.invalidate();
update_mixed_candidates();
}
} // namespace metasequoia