Files
msime-windows/engine/core/input_session_editing.cpp
T
fanlusky fae80660c1 feat(engine,server,windows,ui-html): 双拼句中辅助码
在完整音节后敲反引号,接着的字母筛这个音节上的字:第一码大小写均可,
紧跟的大写字母是第二码,之后可以接着打,一个输入里可以有多段。开关
helpcode.shuangpin_mid_sentence_helpcode 默认关闭。

约束在整句解码时生效,而不是解完再筛:
- Google 解码器在 add_char_qwerty 里送 extend_mtrx_nd 之前按约束过滤词条,
  锁住的字带着上下文重新整句解码;约束变化时整段重解,不污染增量搜索。
- kenlm 词格建图时按约束筛边,受约束的跨度不截断地查词库;神经重排只在
  词格 n-best 里挑,自动满足约束。
- 普通词条、云和 AI 候选按同一规则收尾校验;满足不了就不出整句。

反引号段在请求里换成手动分隔符,原串留给宿主回读与预编辑显示;选词推进
时剩余音节上的约束随原串保留。输入形状规则放在 contracts/mid_sentence_helpcode.h,
TSF、Server、引擎共用,新增 Worker opcode MidSentenceHelpcodeChanged = 28。
googlepinyinime-rev 已与上游解绑,AGENTS 与 UPSTREAM.md 同步说明。
2026-10-02 02:20:37 +08:00

265 lines
9.8 KiB
C++

#include "input_session.h"
#include "../shuangpin/shuangpin_query.h"
#include "../shuangpin/shuangpin_utils.h"
#include <algorithm>
#include <cctype>
namespace metasequoia
{
std::string InputSession::editing_text() const
{
if (dedicated_english_mode_)
return dedicated_english_preedit_;
if (local_input_mode_ == LocalInputMode::TemporaryJapanese)
return "R" + engine_.get_request().raw_input_with_cases;
if (local_input_mode_ != LocalInputMode::None)
return local_preedit_;
return get_pinyin_sequence_with_cases();
}
std::size_t InputSession::caret_position() const
{
const auto size = editing_text().size();
return std::min(caret_.value_or(size), size);
}
namespace
{
// Unit boundaries of a quanpin spelling in raw coordinates. The visible preedit
// (BuildQuanpinAutocorrectDisplay) is always rebuilt from the raw letters -- the
// autocorrect cut and the alias layer only move or add separators -- so a
// separator in the display marks where the next raw unit starts. A display that
// cannot explain the raw letters yields no boundaries and the caller falls back
// to single-character editing.
std::vector<std::size_t> QuanpinRawBoundaries(const std::string &raw, const std::string &display)
{
std::vector<std::size_t> boundaries;
std::vector<std::size_t> raw_letter_offsets;
raw_letter_offsets.reserve(raw.size());
for (std::size_t index = 0; index < raw.size(); ++index)
{
if (raw[index] != '\'')
{
raw_letter_offsets.push_back(index);
}
}
if (raw_letter_offsets.empty())
{
return boundaries;
}
boundaries.push_back(0);
std::size_t letters_seen = 0;
for (const char ch : display)
{
if (ch != '\'')
{
++letters_seen;
continue;
}
// The next unit starts at the raw offset of the next letter; a
// trailing separator has no next letter and starts no unit.
if (letters_seen < raw_letter_offsets.size())
{
boundaries.push_back(raw_letter_offsets[letters_seen]);
}
}
if (letters_seen != raw_letter_offsets.size())
{
// The display no longer corresponds letter-for-letter: refuse to map
// rather than delete an arbitrary span.
return {};
}
boundaries.push_back(raw.size());
boundaries.erase(std::unique(boundaries.begin(), boundaries.end()), boundaries.end());
return boundaries;
}
} // namespace
std::vector<std::size_t> InputSession::segment_raw_boundaries() const
{
// Local modes and the dedicated English scheme spell words, not syllables:
// the host treats these keys exactly like a plain Backspace (PRD R4).
if (dedicated_english_mode_ || local_input_mode_ != LocalInputMode::None)
{
return {};
}
const std::string raw_with_cases = get_pinyin_sequence_with_cases();
const std::string raw = get_pinyin_sequence();
if (raw_with_cases.empty())
{
return {};
}
if (current_scheme_type() == SchemeType::Shuangpin)
{
// 带句中辅助码的串没有逐音节的单元模型:按段删除、按段跳光标和光标前缀重算都退回逐字符。
if (shuangpin::has_mid_sentence_helpcode(raw_with_cases))
{
return {};
}
const std::size_t helpcode_length =
shuangpin::detect_active_double_helpcode_length(raw, raw_with_cases, shuangpin_profile_);
const std::string base =
helpcode_length > 0 ? shuangpin::trim_trailing_letters_preserve_delimiters(raw_with_cases, helpcode_length)
: raw_with_cases;
std::vector<std::size_t> boundaries = shuangpin::segment_raw_boundaries(base, shuangpin_profile_);
if (helpcode_length > 0 && !boundaries.empty())
{
// The active double helpcode is one editable unit of its own, the
// same boundary raw_segmentation draws before it.
boundaries.push_back(base.size());
boundaries.push_back(raw_with_cases.size());
std::sort(boundaries.begin(), boundaries.end());
boundaries.erase(std::unique(boundaries.begin(), boundaries.end()), boundaries.end());
}
return boundaries;
}
if (current_scheme_type() != SchemeType::Quanpin)
{
return {};
}
return QuanpinRawBoundaries(raw_with_cases, get_pinyin_segmentation_with_cases());
}
KeyResult InputSession::edit_at_caret(Command command)
{
auto text = editing_text();
auto caret = caret_position();
// Local-mode markers are commands, not editable payload. Backspace at the
// end of a marker-only composition retains the existing cancel behavior.
const std::size_t begin = local_input_mode_ == LocalInputMode::None ? 0 : 1;
switch (command)
{
case Command::MoveLeft:
caret = caret > begin ? caret - 1 : begin;
break;
case Command::MoveRight:
caret = std::min(caret + 1, text.size());
break;
case Command::MoveHome:
caret = begin;
break;
case Command::MoveEnd:
caret = text.size();
break;
case Command::Backspace:
if (caret <= begin)
return {true, std::nullopt, std::nullopt};
text.erase(--caret, 1);
return replace_editing_text(std::move(text), caret);
case Command::DeleteForward:
if (caret == text.size())
return {true, std::nullopt, std::nullopt};
text.erase(caret, 1);
return replace_editing_text(std::move(text), caret);
default:
return {};
}
caret_ = caret;
// Pure moves mutate the caret too, so they must re-evaluate the prefix boundary exactly
// like an insert/delete does; otherwise candidates() would keep serving the previous
// boundary's list until the next edit key (PRD R2: the boundary follows the caret).
if (!dedicated_english_mode_ && local_input_mode_ == LocalInputMode::None)
{
update_mixed_candidates();
}
return {true, std::nullopt, std::nullopt};
}
KeyResult InputSession::insert_at_caret(char character)
{
auto text = editing_text();
const auto caret = caret_position();
const bool lower = character >= 'a' && character <= 'z';
const bool upper = character >= 'A' && character <= 'Z';
bool accepted = lower || upper;
if (!dedicated_english_mode_)
{
switch (local_input_mode_)
{
case LocalInputMode::Unicode:
accepted = (character >= '0' && character <= '9') || (character >= 'a' && character <= 'f') ||
(character >= 'A' && character <= 'F') ||
(character == '+' && caret == 1 && text.find('+') == std::string::npos);
break;
case LocalInputMode::QuickPhrase:
accepted = lower;
break;
case LocalInputMode::DateTime:
accepted = false;
break;
case LocalInputMode::None:
accepted = lower || (upper && ((scheme() == SchemeType::Quanpin && quanpin_helpcode_enabled_) ||
(scheme() == SchemeType::Shuangpin && shuangpin_helpcode_enabled_)));
if (character == ';' && scheme() == SchemeType::Shuangpin &&
ShuangpinProfileUsesSemicolonFinal(shuangpin_profile_))
{
const auto separator = caret == 0 ? std::string::npos : text.rfind('\'', caret - 1);
const auto start = separator == std::string::npos ? 0 : separator + 1;
accepted = (caret - start) % 2 == 1;
}
if (character == '\'' && scheme() != SchemeType::Wubi)
accepted = caret > 0;
break;
case LocalInputMode::Emoji:
case LocalInputMode::Kaomoji:
case LocalInputMode::TemporaryJapanese:
accepted = accepted || character == '\'';
break;
default:
break;
}
}
if (!accepted)
return {};
if (character == '\'' && ((caret > 0 && text[caret - 1] == '\'') || text[caret] == '\''))
return {true, std::nullopt, std::nullopt};
text.insert(caret, 1, character);
return replace_editing_text(std::move(text), caret + 1);
}
KeyResult InputSession::replace_editing_text(std::string text, std::size_t caret)
{
std::optional<std::string> diagnostic;
if (dedicated_english_mode_)
{
dedicated_english_preedit_ = std::move(text);
update_dedicated_english_candidates();
}
else if (local_input_mode_ != LocalInputMode::None && local_input_mode_ != LocalInputMode::TemporaryJapanese)
{
local_preedit_ = std::move(text);
diagnostic = update_local_candidates();
}
else
{
const auto payload = local_input_mode_ == LocalInputMode::TemporaryJapanese ? text.substr(1) : text;
auto normalized = payload;
std::transform(normalized.begin(), normalized.end(), normalized.begin(),
[](unsigned char value) { return static_cast<char>(std::tolower(value)); });
set_pinyin_sequence(normalized);
set_pinyin_sequence_with_cases(payload);
apply_pending_sequence();
if (local_input_mode_ == LocalInputMode::TemporaryJapanese)
{
local_preedit_ = "R" + engine_.get_preedit();
local_candidates_ = engine_.get_candidates();
}
}
caret_ = caret;
online_requests_.invalidate();
discard_abandoned_phrase_progress();
// apply_pending_sequence() already refreshed candidates while the caret was cleared; now
// that it is back in place, re-evaluate the caret prefix boundary (a caret at the end of
// the text deactivates the prefix decode, mid-text re-decodes it).
if (!dedicated_english_mode_ && local_input_mode_ == LocalInputMode::None)
{
update_mixed_candidates();
}
return {true, std::nullopt, std::move(diagnostic)};
}
} // namespace metasequoia