Commit Graph
758 Commits
Author SHA1 Message Date
fanlusky 7d66b2556c feat(server,ui,ui-html): 候选翻译文本支持在皮肤中单独配色
外部皮肤 skin.toml 的 [candidate.dark] / [candidate.light] 新增 translation 键。
不写时沿用候选文字色叠 62% 不透明度;写了就按原值绘制(含选中行)。
D2D(CandidateList::Appearance::translationColor)、WebView2 注入 CSS
和设置页外部皮肤预览三处一致;预览顺带补上此前漏掉的 number 配色。
2026-09-26 23:38:37 +08:00
fanlusky c539d0cf2a feat(server,ui-html): 设置窗口驻留时长可配置,驻留期间降低内存占用
- 关闭设置窗口后隐藏驻留时,把 WebView2 内存目标设为 LOW 并 TrySuspend 冻结页面;
  重新打开时恢复 NORMAL、Resume 后再显示。本机驻留 60 秒后整棵进程树的工作集
  由约 408 MB 降到约 62 MB,重开仍是瞬间恢复。
- 新增配置项 appearance.settings_window_linger,可选 off / 1m / 5m / 10m / 30m /
  60m / forever,默认 10m。off 表示关闭即退出,forever 表示一直驻留。
- 同步出厂模板、开发配置、ime_config 读写校验和两个设置宿主的快照字段与写入分发;
  设置页“外观 → 界面渲染”新增“设置窗口后台驻留”下拉框。
- 新增测试覆盖模板默认值、缺键、非法值、setter 拒绝和逐值往返。
2026-09-26 22:57:18 +08:00
fanlusky 14815e2f43 perf(server,ui-html): 设置窗口冷启动从约 2.6 秒缩短到约 0.7 秒
- 设置页虚拟主机名改为 imesettings.example / candidate-skins.example:
  不带点的单标签主机名会先走一轮网络名称解析,导航后要空等约 2 秒才开始加载。
- 写 WebView2 用户数据目录和配置目录的 ACL 前先检查:已有 Users 完全控制(可继承)
  和 Medium 标签时跳过 SetNamedSecurityInfoW,避免每次启动都把 ACE 传播到整棵缓存树
  (本机约 120 ms 降到 1.5 ms)。
- 先显示窗口和 splash,再准备 WebView2。
- 显示前最多等 150 ms 取得配置里的主题,浅色主题不再先闪一下深色。
- 新增 METASEQUOIA_IME_SETTINGS_TRACE=1 冷启动打点,写入
  %TEMP%\metasequoia-settings-startup.log。
2026-09-26 22:57:18 +08:00
fanlusky 030f2d4db8 feat(server): D2D 悬浮工具栏跟随候选窗皮肤配色
此前 D2D 工具栏只按明暗切换默认配色,不看皮肤。新增 floating_toolbar_skin,
按 ui-html/webview2/ftb/<skin>(_light).html 给出背景、边框、图标、悬停、分隔线、
拖动条颜色与圆角;外部皮肤沿用其 base 皮肤,因为 toolbar.css 只作用于 WebView2。
2026-09-26 21:49:04 +08:00
fanlusky 8278c065c9 feat(server,ui-html): 新增内置皮肤秋桂,杨柳青 D2D 高亮对齐 WebView2
秋桂 Autumn osmanthus(autumn_osmanthus):暗色底 #7D929F、高亮 #F97D0A,
亮色底 #D6ECF0、高亮 #FFE399。高亮内缩于外框,首项顶部两角与尾项底部两角
和外框同为 10px;预编辑行隐藏时首项才贴顶。WebView2 候选窗与工具栏、D2D
候选窗、设置页卡片与预览一并提供。

ApplyCandidateFrame 只隐藏多余的候选行,:last-child 找不到最后一个可见行,
因此为其加上 .last-visible,杨柳青的 CSS 同步改用它,候选不满一页时尾项的
补边与贴边高亮才会生效。

杨柳青在 D2D 下改为直角、左右贴边的整行高亮,只在外框四角处用 9px 贴合,
行距、行内边距与预编辑行边距按 CSS 对齐。
2026-09-26 21:49:04 +08:00
Lu Fan(SonnyCalcr) 4375d80414 feat: 支持导入自定义辅助码,内置“加加”更名为“拼音加加” (#477)
* feat(engine): 支持 helpcodes/custom 下的自定义辅助码

custom 目录里的每个 .txt 都是一套方案,方案名为 custom/<文件名>。
文件开头的 "# name:" / "# name_en:" 注释给出中英文显示名,缺省用文件名;
兼容 BOM、CRLF 和全角冒号。方案名不能跳出 custom 目录。

* feat(engine): WebView 契约新增 openHelpcodeDirectory

设置页请求打开自定义辅助码目录,仅允许 settings 页发送。同步 ui-html/webview2/shared。

* feat(server): 设置页列出自定义辅助码并可打开其目录

配置快照带上 helpcode.custom_schemas 与 helpcode.custom_directory,每次都会重新扫描;
方案文件被删除或改名后回退到 lantian。两个设置宿主都会处理 openHelpcodeDirectory。

* feat(ui-html): 辅助码设置显示拼音加加与自定义方案

内置方案“加加”改名为“拼音加加”。自定义方案追加在内置方案之后;新增说明区,
提供“打开目录”和“刷新方案”两个按钮,刷新完成后弹出提示。

* feat(installer): 升级与迁移数据目录时保留自定义辅助码

升级清理时只清掉 helpcodes 里的内置表,保留 custom 子目录;更换数据目录时一并迁移;
安装时建出 helpcodes\custom。
2026-09-26 20:21:55 +08:00
fanlusky b3b3b3303c feat(windows): 焦点切换到另一个输入框时提示当前中英文状态
新增开关 general.caret_state_indicator_on_focus(默认关闭,需同时开启光标
状态提示)。

- DLL 把以下情况视为“换了输入框”:焦点窗口变化(Win32 编辑框、对话框、
  其他应用各有自己的窗口);焦点经过一段没有可编辑文档的间隙后落到可
  编辑文档上(Chromium、WPF 等单窗口宿主在字段之间就是先失焦再聚焦);
  焦点会话真正结束后重新开始。Chromium 打字时在两个可编辑文档之间直接
  替换文档管理器,不会触发。从 Win+. 面板返回不计入;线程焦点在真正的
  应用切换后返回计入。一次切换的多次回调由定时器合并为一次提示;组字
  过程中、以及没有光标的控件都保持静默。每次判断在 TSF 诊断日志里记录
  判定标志,不含任何文本
- 协议复用 IMESwitch:wch 改为 FanyImeCaretStateTrigger(UserToggle /
  CapsLockEdge / FocusEntered),CapsLockEdge 保持首发时的 VK_CAPITAL
  取值,Server 对未知值按 UserToggle 处理
- Server 在开关打开时显示当前的有效模式(Caps Lock 开启时为“英”);
  配置项同步写入两份模板和两个设置窗口宿主的读写映射
- 设置页在光标状态提示卡片中增加“切换输入框时提示”开关
2026-09-26 17:09:34 +08:00
fanlusky d99aaa0d7d feat(server): 光标状态提示补全左下方、正下方、右下方
提示位置原先只有左上方、正上方、右上方和“下方”四种,且“下方”在设置页
预览里居中显示,Server 却把它放在光标左侧。

- 新增 bottom-left(左下方)与 bottom-right(右下方)
- bottom 保留原配置值、改名为“正下方”,并改为与 top 对称地居中,
  使运行时位置与预览一致;已选“下方”的用户升级后不会被重置
- 合法位置与“是否在光标下方”的判断集中到 caret_state_indicator_policy.h,
  配置读取、写入与定位共用一份定义
- 同步两份配置模板的注释、设置页下拉项、aria 文案与预览样式,并补充测试
2026-09-26 16:00:59 +08:00
fanlusky 597d087580 fix(server): 光标状态提示仅由用户快捷键触发并收紧协议边界
- DLL 只在 Shift/Ctrl 切换、Shift+Space、Ctrl+.、Caps Lock 与
  Ctrl+Shift+F 处显式请求提示;Server 下发的模式同步、宿主转换模式
  同步和激活初始化不再弹出提示,移除 compartment 回调里的推断逻辑
- 恢复文档切换时候选窗的 OnKillThreadFocus,改用 COM 身份比较
- 能力位改名为 CaretStateIndicator,DLL 仅在 Server 确认后发送切换
  事件;Server 端这些事件只驱动提示,不再改写工具栏或状态快照
- 标点提示的中英模式由 DLL 随 PuncSwitch 携带
- UILess 宿主不显示提示,会话转为 UILess 时隐藏已显示的提示
- Ctrl+Shift+F 的锚点就地读取,不再覆盖 Global::Point,且只采信
  协商了该能力的客户端
- 撤回对所有包默认 point 的修改;提示窗口创建失败不再阻断 Server
  启动;提示内容改为结构化 CaretStateBadge
- Caps Lock 在同一按键重复进入 TestKeyDown 时只处理一次
2026-09-26 13:58:22 +08:00
fanlusky dc7cfa0354 Merge branch 'develop' into feat/issue-229-ime-status-indicator 2026-09-26 13:58:03 +08:00
Jia Sui 6506a7a07f fix(server): 退格删空 raw 不再吞掉已选词
输入 nihaoya、选「你好」、再敲一个字符后连按退格,删掉最后一个 raw 字符
的那一次会把造词态连同已选词一起清掉(尾部「raw 空即清」规则),用户看到
「你好」被直接删掉。这条规则的前提是「客户端会随空 raw 取消组合」,但协商
过 CompositionRestored 的客户端会应用帧、把组合留在「只有已选词」的状态
(与 R3 段删除一致),此时清态反而丢字。

- 新增 ShouldKeepCreatingWordAfterRawEmptied:仅协商过组合恢复、非 UILess
  且有快照时保留造词态与快照;普通退格与 Ctrl+Backspace 段删除共用该谓词。
- 普通退格删空 raw 时置 keep:帧里 word 非空,DLL 走
  _ApplyCreatingWordPayload 的「只有已选词」分支继续组合,不再 _HandleCancel;
  下一次退格由「raw 为空时退格只能是撤销」把最近一次选词撤回,而不是丢整句。
- 未协商与 UILess 客户端维持原行为(本地取消 + 清态),避免两侧状态分叉。
- 测试补 deleting_the_last_raw_character_keeps_the_created_word。
2026-09-25 15:59:05 +08:00
Jia Sui 9aba1a44ea refactor(server): 组合恢复帧统一走候选页发布助手
组合恢复帧的候选发布此前只在注释里按「退格撤销」解释,但条件挂在
composition_restored 上,实际覆盖的是每一帧:退格撤销、Ctrl+Backspace
段删除、Ctrl+方向段跳转。段删除与段跳转同样改了 raw / 光标前缀,旧代码
只发帧不重建页面,页面上留着按键前的候选与选择。

- 把发布逻辑抽成 PublishRestoredCompositionCandidates,注释写明它服务
  的是「每一帧组合恢复」而不是只有撤销,以及两条收起规则(raw 为空、
  前缀为空)各自的理由;抽取不改行为。
- 一次性高亮的消费随之落到助手内部,仍然每帧消费一次,不跨帧泄漏。
2026-09-25 09:46:05 +08:00
Jia Sui a596f8b1bc fix(server): 撤销重高亮只在候选页前缀未变时应用
选词快照记的候选绝对位置对应的是选词那一刻用户眼前的候选页,而撤销
重建的页面由恢复后的 raw 与光标前缀决定:选词之后用 Del 或光标编辑改
动过后缀时,重建页装着另一批候选,旧位置会把高亮落到不相干的项上。

- 新增 FanyImeIpc::NormalizeCandidatePagePrefix,按引擎的页身份把光标
  前缀折成小写(refresh_prefix_candidates 的同一套归一化)并夹到 raw 长度。
- 快照多带一个 selected_page_prefix,取在 advance_composition_after_selection
  之前(那之后引擎已经换成结算后的 raw,不再是用户看到的那一页)。
- 一次性交接从裸 int 改成 RestoredSelectionHighlight(位置 + 页前缀),
  HandleImeKey 重建页面后先比较前缀再套用位置,不一致就只发布新页不高亮。
- 测试更新位置交接用例并覆盖归一化谓词的空串、越界、大小写与改短后缀。
2026-09-25 09:43:38 +08:00
Jia Sui eb6c6f84ce fix(server): 造词态被退格清空时也回帧,避免 TSF 挂起烧满超时
答帧判定用的是按键之后的 creating_word.active,而退格删掉最后一个
raw 字符时尾部会清掉造词态与快照,形态随之消失:这样一次退格不发
帧,而 DLL 的 hold 只看按键前的镜像 word_for_creating_word,仍然
非空,于是白等满 50ms 才回退本地删除,正是本次改动想消除的那类等待。

- HandleImeKey 在 ApplyCompositionEditKey 之前抓一份客户端可见的
  形态,答帧条件改为「撤销过 或 按键前形态 或 按键后形态」,用新的
  ShouldAnswerRetreatBackspace 把这条不变量写成可测谓词。
- 该结局的 payload 是空 raw + 空 word,DLL 侧按 _ApplyCreatingWordPayload
  的空 word 分支立即 _HandleCancel,语义与超时后一致,只是不再等。
- 候选窗收起的判据从「raw 空且造词态还在」放宽为「raw 空」:造词态
  被这次退格终结时同样不能落入重建分支,否则会给空输入发布兜底假候选。
- 测试补 retreat_backspace_reply_survives_the_key_that_ends_the_word。
2026-09-25 09:37:57 +08:00
Jia Sui c06dc17ae1 feat(server,windows): 造词中的退格优先撤销最近一次选词
把"退格撤销上次选词"从旧的两条窄触发(剩余输入只剩一个字符、或光标
正好停在这个词后面)放宽为造词态下的默认行为,对齐 Rime/微信:只要
造词还活着且有可选回的快照,首个退格先撤销最近一次选词,剩余输入
长度与光标位置不再参与判定。

- 选后编辑锁(selected_before_editing):选词后又输入了新字符,退格
  继续删除这些新输入,而不是把选词从脚下抽走;只标记最新快照,与
  Rime 一致。空输入例外——没有字符可删时退格只能是撤销,否则会退回
  #35 的整句丢弃。
- 恢复改为前插:被选词消耗的拼音插回未删除的后缀之前,光标随恢复
  长度平移("输入变长、光标不动"),不再整串覆盖 raw 而丢掉后缀。
- 撤销后重新高亮:选词快照记下所选候选的绝对位置,撤销重建候选页
  后把高亮放回同一项(一次性消费),下次按键可直接改选,不用再找。
- 客户端契约拆成两层:DLL 只凭造词镜像(word_for_creating_word)
  挂起等待回复,看不到 Server 侧的快照与编辑锁,因此新增
  HasRetreatBackspaceShape 描述客户端可见形态,Server 对形态内每个
  退格都必须答帧(撤销、锁定删除或空历史空操作),否则挂起白烧满
  50ms 超时;ShouldRetreatCreatingWordSelection 只在真能改写状态时
  为真。
- 撤销改为纯状态操作,引擎序列、候选重建与光标前缀由尾部统一执行
  一次,替换原先每次撤销都额外跑一遍完整候选查询的 helper。
- DLL 侧:已消耗请求槽位的回复(或光标 0 处的 Normal 应答)之后
  丢弃 request id,让 worker 回退本地读取,而不是为已消失的槽位
  再烧一个 50ms 超时。

Signed-off-by: Jia Sui <jsfaint@gmail.com>
2026-09-24 20:52:12 +08:00
Lu Fan(SonnyCalcr) 2755c77192 Merge pull request #466 from fanlusky/feat/neural-sentence-association
feat: 神经网络整句联想与多来源整句开关
2026-09-24 14:53:55 +08:00
Jia Sui eac9fa72b5 feat(server): 固定排位候选词显示可配置的徽标
固定排位徽标此前硬编码为 📌,嫌吵的用户既关不掉也换不了。
新增两个配置项:candidate_fixed_badge 总开关与 candidate_fixed_badge_style 样式枚举(paperclip/pushpin/dot),设置页外观分组提供开关与下拉,改动后立即刷新候选页。
配置存枚举名而非字面 emoji,TOML 与日志里不出现图形字符;展示层白名单与配置层同宽,手改 TOML 写错只会退成无徽标。默认样式改为 📎,比实心红钉安静。
2026-09-24 14:32:10 +08:00
fanlusky 16877cb2d0 feat(server): 整句联想来源开关接入配置与设置页
新增 [association] 段的五个开关(词格、Google、神经 desktop / keyboard、去重补位),
同步写入出厂模板、工作配置和两个设置窗口宿主的读写映射,设置页输入分区加对应选项。
2026-09-24 14:03:43 +08:00
fanlusky 3929ec482b fix(server): 前缀选词后同步剩余 raw,避免光标被挪到串尾
HandleImeKey 在选词之前写入 raw_input_with_cases,前缀选词把 caret 归 0 后
它仍是选词前的整串,下一次编辑键里「raw 变了且 caret==0 → 光标移到串尾」的
判定误触发:DLL 光标停在后缀首,Server 跳到串尾,之后的插入与移动两侧分叉。
组合继续时统一同步剩余 raw;串尾造词流不受影响(caret 仍被夹到新串尾)。
2026-09-24 12:54:31 +08:00
Jia Sui 3821f3ede8 fix(server): 补回合并时丢失的测试用例花括号 2026-09-24 09:04:27 +08:00
Jia Sui 5b0be4e71d Merge branch 'develop' into feat/caret-driven-resegmentation 2026-09-24 08:45:54 +08:00
Jia Sui 54c41d34b2 fix(engine): reset_cache 连带失效 caret 前缀候选缓存 2026-09-23 21:32:59 +08:00
fanlusky 7b7b19f449 fix(wubi): 前缀提示不挡混合拼音、候选去重,修复顶字/自动上屏后快打丢字母
- 只有精确码行才算五笔表已回答,前缀提示不再阻止混合拼音回退;拼音无结果时保留提示
- 五笔前缀查询按词去重,保留精确码行
- HideCandidate 抑制改为推送成功记账、Hide 消费(带 1s 时限),不再被下一个按键撤销;
  四码唯一自动上屏路径同样记账
2026-09-23 15:41:04 +08:00
AaGMix 902989abc4 feat(server): 显示光标输入状态提示 2026-09-22 23:52:28 +08:00
Jia Sui 4f26f35ac3 fix(server): 光标移回串尾时从引擎重建候选页
RefreshCandidatePageUi 只重排旧 candidate_ui.items、不重读引擎,而结算读的
正是 items。回串尾时引擎已整串重算,页面却停在旧前缀候选:选词只消费前缀,
ni'hao'ya 回串尾得「你好ya」而非「你好呀」。提取
ResolveCaretArrowCandidatePublish(前缀空收起;门控启用含回串尾一律
PrepareCandidateList 重读并复活窗口;未启用维持只刷页面,R7/AC8 零差异)。
2026-09-22 18:49:37 +08:00
Jia Sui fceb0692cb fix(server): 造词帧的 caret 字段只发给协商过组合恢复的客户端
未协商的旧版 DLL 会把第 2 个制表符后的整个尾部当 display_preedit,
追加的 caret 字段会污染一次 inline preedit。提取
ShouldCreateWordFrameCarryCaret 谓词(协商 && caret<剩余串),
未协商时帧输出与旧 Server 字节一致;CompositionRestored 帧本就只在
协商路径产生,无需改。
2026-09-22 16:39:50 +08:00
Jia Sui 096c0ccb10 feat(server): 前缀重算接线与仅前缀选词结算
编辑键后按门控(协商 composition-restore 且非 UILess、非英文/特殊模式)
把 caret 喂给会话做前缀重解;caret 回到串尾显式清 caret_,串尾行为
与现状一致。前缀选词复用 advance_composition_after_selection 的头部
消费语义:剩余 raw 即后缀、caret 归 0,NeedToCreateWord 帧带契约既有的
可选 caret 字段(协议零新增);学习/调频/AI 上下文走既有链路。
空前缀时收起 Server 自有候选窗并守卫假候选兜底与 ShowCandidate。
门控谓词与边界补真值表单测;回归测试钉住「移回串尾恢复整串解码」。
2026-09-22 16:09:05 +08:00
Jia Sui 1cfba58eba feat(engine): 光标驱动的拼音前缀解码与候选重算
caret 从纯显示状态升级为查询管线输入:set_caret 量化到最后一个完整
音节单元边界(只消费 segment_raw_boundaries,#187 契约),前缀候选
独立解码、preedit 保持全串拼音风格;纯移动光标同样重算候选。
IInputScheme::set_raw_input 提升为接口方法以支持不动现场组合的
独立前缀查询。runtime_isolation 两处按 R3/R4 新语义改写(caret 在
音节中间 floor 到空前缀、候选为空是预期状态);新增 caret 前缀
会话单测(engine/tests + server CI 目标,隔离词库夹具,覆盖
全拼 floor、双拼贪心配对、五笔无单元模型、boundary=0 空候选)。
2026-09-22 14:56:22 +08:00
Jia Sui c6632e8d85 feat(engine): 五笔前缀逐键提示与调频接线
- 码表查询从精确匹配改为前缀匹配:精确等长行优先(保住一级/二级简码),
  前缀行按权重降序跟随,LIMIT 50 截断(引擎层不读 server 配置的分层边界)
- WubiCandidateProvider 注入 RuntimePaths:接通 update_weight / delete_by_key
  到 user_dictionary journal;新增 bump_wubi_weight(同码组 MAX+1、主库更新
  与 journal upsert 同事务,行缺失不造行)
- create_word / cache_dynamic_candidate 补「为何 no-op」注释(后续任务范畴)
- 两个 toml 的 wubi_schema 注释与实现对齐:当前仅支持 wubi86
- server 测试:前缀语义、LIMIT 截断、上界构造、调频+journal 回放、删词墓碑
2026-09-22 09:23:25 +08:00
Jia Sui cdfad7c04c fix(server): 顶字推送后的 HideCandidateWnd 不再清除余码组合
顶字提交让 DLL 结束旧组合时,TSF 必然发来 HideCandidateWnd;HideCandidate
处理器照常 ClearState 会把服务端刚重建的余码组合(如「数据」顶字后剩下的
x)一并抹掉,用户后续按键从空组合开始组词,服务端与 TSF 缓冲从此错位。

- HideCandidate 处理器核对顶字余码标记(client+epoch+raw 非空),
  匹配则跳过 ClearState 并重新请求显示候选窗
- 顶字推送前设置标记;HandleImeKey 开头复位,防推送失败后标记滞留
- 新增三点丢键定位探针(reader 收包 / worker 派发 / 与 DLL keydown-sent
  按 request_id 配对)与 stale-dropped 诊断日志
2026-09-22 09:23:25 +08:00
Jia Sui 536d0a95ef feat(windows): 五笔四码后继续输入不丢字(顶字)
四码(不论是否唯一)之后继续敲字母时,先上屏该码的首选候选,再把这个字母
保留为下一次组合的开头。根因是两侧对「组合里有几个字母」认知不一致:TSF 的
本地按键缓冲跟着敲键涨(raw 预编辑样式下 preedit 就是它),服务端的五笔 raw
被引擎裁在四码;空格提交时组合文本被整段替换成候选并清掉本地缓冲,多敲的
字母随之丢失。

- 新增 worker 消息 CommitCandidateAndContinue(27),payload "<consumed>\t<text>":
  TSF 按服务端给出的消费数裁**自己**的组合缓冲,而不是套用服务端按旧视图算好
  的剩余串——快打时用户已多敲的字母因此不会被覆盖
- TSF 处理序列全部复用既有原语:_InsertTextToComposition + _HandleCompleteCommitFirst
  真提交,_StartComposition + AddVirtualKey + _HandleCompositionInputWorker(NO_REQUEST_ID)
  重建余下字母的组合;无组合时退化为直写
- 服务端顶字不看自动上屏开关:开关只决定唯一码要不要多敲一键才上屏,
  不决定丢不丢输入;自动上屏那条路也改用同一消息,顺带收掉快打竞态
- 引擎新增 wubi_four_code_is_complete(完整四码、码表作答、不要求唯一;
  空码不算——没有候选可提交时不触发顶字)

已知限制:光标在组合中间时的改码行为维持现状;空码继续敲字母仍会被忽略。
2026-09-22 09:23:25 +08:00
Jia Sui 5d1a9a6cc2 feat(server): 五笔四码唯一自动上屏
五笔敲满四码且码表只答一个候选时不再需要空格:第四个字母落下即上屏,下一个
字母直接开新组合。行为无条件生效,不设配置开关:业界默认开启(微软五笔、
搜狗五笔、极点、Rime 主流五笔方案),唯一候选没有第二选择,确认空格是纯冗余;
而一条开关要牵动配置五处同步,换不来可形成预期的行为差异,不值。

- engine 只报告事实(wubi_unique_four_code:完整四码、码表作答、候选恰好一个),
  是否上屏由 Server 决定;判定收在 input_key_policy.h 的纯函数里,随 portable
  目标进 ASan/UBSan;组词(creating_word)激活时组合保持打开,唯一四码不会把
  正在拼的词截断上屏
- 提交复用 ProcessSelectionKey,与空格上屏同一套记账;但只能走 worker 管道推送
  CommitCandidate:字母键在默认 raw 预编辑样式下不读请求-回复管道,回一帧 Normal
  既不会上屏,还会在 TSF 侧留下永不释放的回复帧,而 Server 已按 Normal 清掉组合
- 上屏前按当前组合同步重建候选页:候选页异步发布,提交路径读的 ui.items /
  page_words 否则可能还停在第三码那一拍
- 非唯一四码不做顶字上屏:一个按键只能回一条管道消息,真顶字需要扩 IPC 协议
2026-09-22 09:23:24 +08:00
fanlusky 99a8a355c0 fix(engine): 云/AI 联想候选上屏后按手动分词落库
选中云或 AI 联想候选上屏后,再次输入同样的拼音却不出现该词。根因有二:

- create_word_from_canonical_pinyin 已按 canonical 键里的撇号校验过分段,却又调用
  do_validate 二次校验;do_validate 会先去掉撇号、再用贪心 correction 重新断句,把
  qi'e'huan 切成 qie'huan(音节数 3→2),与字数不符而静默拒绝入库。移除这次多余且
  有害的二次校验,分段以调用方给定的 canonical 键为准(do_validate 本身保持不变,
  造词/调频/删除三条旧路径不受影响)。
- 上屏时 AI/云候选自身不带 canonical_pinyin,落库用的是去掉音节边界的
  committed_pinyin,同样会被重新贪心断句。改用会话里带撇号的 normalized_segmentation
  作 canonical 键,经 create_word_from_canonical_pinyin 落库。
2026-09-22 01:13:11 +08:00
Jia Sui 566ff8b832 feat(engine): 新增加加辅助码(拼音加加),第六套辅助码方案
按笔顺拆前两个部件、各取读音首字母(妈 = 女 n + 马 m = nm),部件用加加俗称读音(亻=d、
扌=t、纟=j、忄=s、攵=f、夂=z、刂=l、彳=s);138 个独体/部首字取「本字声母 + 起笔」。码表
7968 条,覆盖《通用规范汉字表》7846 字,比小鹤少 422 个扩展区生僻字——缺码会让含这些字的
词组拿不到辅助码,是已知取舍。

编码对齐拼音加加 5.x 安装包内的 fzm.bin(双拼键位已换算为声母):6487 字取到两码,6349 字
逐字一致,138 字保留本表规则,覆盖外的 800 字按同一规则重建。来源与非官方性质写在 NOTICE.md
与随包的 THIRD_PARTY_NOTICES.txt。

- engine/contracts/assets 注册 helpcode_jiajia,assets.h 重新生成(5 → 6)
- 设置页两个下拉显示名「加加」,两份配置模板注释同步
- 两侧测试补支持列表;README / README.en.md / server/AGENTS.md 改「六套」

默认值不变(双拼 lantian、全拼 ziranma)。
2026-09-20 22:00:34 +08:00
fanlusky 94df9a7917 fix(stats): 修复统计功能审查发现的 8 个问题
- Server 退出时唤醒统计管道监听线程,避免从未有客户端连接时 join 永久阻塞
- DLL 发送批次时对 ERROR_FILE_NOT_FOUND 重试,覆盖服务端重建管道的窗口
- 新增 StatisticsEnabledChanged(26) 推送开关,DLL 侧在采集入口直接短路:
  关闭时不读 range、不分类、不开管道
- stats_meta 的 schema_version 写入正确的键名(此前键与值都是 "1")
- dropped_count 仅在实际发送的批次中消费,空批次不再吞掉丢弃计数
- _CaptureCompositionStats 改为遍历 composition range 的克隆,避免移动宿主的活动区间
- FanyImeStatsEvent 显式声明 reserved 尾部,不再把未初始化的填充字节写入管道
- 透传统计去重标记在抑制一次后即消费,避免同一 tick 内的连击被漏计
2026-09-20 20:34:50 +08:00
Jia Sui ffcd1aee28 feat: 输入统计(DLL 采集、Server 聚合存储与设置页展示)
- windows: 三上屏出口 + OnTestKeyDown 直通字符采集,修复半角数字恒零
- engine: 统计 Web 消息契约(statsRequest/statsResponse)
- server: 统计管道接收、聚合、SQLite 存储、保留策略与每日自动清理
- ui-html: 设置页「统计」模块(卡片、热图、明细、开关、清理)
- docs: PRIVACY / README 改为内置可选统计
2026-09-20 19:08:16 +08:00
fanlusky 1ea01d5e0a fix(quanpin): 备选切分保护位只拉首页外的候选,不覆盖调频名次
保护位原来对任何排在 index 1 之后的备选切分都生效,于是调频刚把 吉安 排到
自然第 5 位,它就因为成了 ji'an 组里权重最高的词、过了 1/100 门槛而被重新
钉回第 2 位。用户选一次就跳到第二,之后再怎么调也只能停在第二——固定槽位把
调频写进权重的结果整个覆盖掉了。

改成只对自然排序已经掉出首页(默认 page_size 6)的备选切分生效。西安 在出货
词库里的自然位置是 16,仍旧提升到第 2 位;调频过的 吉安 留在它自己挣来的位置
上,之后按 promote 一步步往上走。

夹具同步补候选:engine 与 server 的 xian 夹具原来只有三条主候选,备选切分自然
就在首页内,测不到提升这条路径。
2026-09-19 22:42:29 +08:00
fanlusky e32eeadec9 fix(engine): 调频比较集跨键合并,按权重而非显示顺序取基准
打 jian 时列表里混着另一种切分 ji'an 的词。吉安 在出货词库里权重是 1,压在同组的
积案(9420)/几案(5999) 底下,而 adjust_candidate_ranking 按音节数圈比较集,于是
吉安 只跟 ji'an 这一组比,可学权重封顶一万出头,永远追不上 见 的 3460998——用户
选多少次,这个候选都纹丝不动。

音节数这个尺度是 #400 加的,为的是挡住另一个 bug:显示列表里备选切分词会被保护位
顶到最前面,中点数学却假定列表按权重降序,于是拿 西鄂(w=6) 当基准把 写 写成 506。
根子在「显示顺序 != 权重顺序」,按音节数滤掉跨表行只是绕开了症状,代价是把每个候选
关进自己的组里。

改成在比较集上直接按权重降序排一次,不变式在源头恢复:任何行都不可能坐在高于自身
权重的位置上充当基准,西鄂 那条路被堵死;同时比较集不再按音节数切开,吉安 的基准
取到整个列表最重的 见,置顶一次即越过,promote 模式连选五次一路走到首位。写入仍只
落在 entry_key 的行上,#36 的护栏不变。

test_user_dictionary_journal 新增两条回归(跨键置顶、连续 promote 收敛到首位),
#400 的两条断言原样保留并仍然通过。
2026-09-19 22:14:57 +08:00
fanlusky 663f72301d fix(server): 独立上屏的整句候选也落库
上一个提交改的是 InputSession::commit(),而 Windows 前端根本不走那条路:选词由
ProcessSelectionKey 自己驱动。那里 CandidateSource::Generated 走提前返回,落库的
闸门 ShouldStoreEarlyReturnPhrase 要求造词前缀非空,也就是只认"先选前缀、再用整句
收尾"这一种形状;用户直接选一条完整的整句时 creating_word 没激活,条件为假,什么
也不做就返回了。Google 解码器那条整句(Fallback)不在提前返回的名单里,走普通
路径,但同样没有行可以调频。

因此拆出 ShouldStoreStandaloneSentence:整句候选独立上屏时就该落库,带 7 音节
上限(对标 quanpin::WordLatticeOptions::max_phrase_syllables)。接在造词前缀
后面的那一支由用户一段段选出来,维持原样不设限。

顺带修掉同一处的读音拼接:前缀为空时不能带上那个分隔符,'na'yi'tiao 这种前导
撇号会让整条读音作废。

原先有一条测试把这个缺陷钉住了(断言没有前缀就不落库),按新的意图翻过来。
2026-09-19 13:48:26 +08:00
fanlusky ccbaa3a631 fix(engine): 送进 Google 解码器前把 ü 换成 nue/lue/ju 写法
小鹤双拼 ntdddswu 出的是「女蛾黛动物」而不是「虐待动物」:nt 转成全拼后
是 nve(shuangpin_pinyin_set 把 nue 抹掉了,词库与词格那一侧统一用 v 形),
而本地 googlepinyinime-rev 的拼写表和 inputtools 云输入都不认 nve/lve/jv,
只有 nue/lue/ju/jue,也不做 v→ü 的转写。带分隔符的串照样送过去了,解码器
却把 nve 重新切成 nv + e,于是有了「女蛾」。

加一个 quanpin::to_google_spelling,按音节整体改写 nve→nue、lve→lue、
jv→ju 这一类拼写,只在两个 Google 解码器的边界上用:云输入的 query_text、
双拼与全拼的整句回退。词库查询和词格用的仍然是 canonical_syllable 那一侧的
v 形写法,没有动。nv'er、lv'se 这些 Google 也认的写法原样通过。
2026-09-19 12:29:01 +08:00
Lu Fan(SonnyCalcr) e1d53dd8f0 Merge pull request #426 from ouchihao/codex/fix-299-double-helpcode-cache
fix(engine): isolate ordered double-helpcode query caches
2026-09-18 21:34:50 +08:00
Ouchihao 0925937bfe fix(engine): isolate ordered double-helpcode query caches 2026-09-18 20:43:17 +08:00
Ouchihao 5e641fb5a0 fix(engine): accept yo as a complete shuangpin syllable 2026-09-18 20:35:14 +08:00
Lu Fan(SonnyCalcr) 4dea1cd12f Merge pull request #420 from metasequoiaime/feat/ctrl-arrows-segment-caret
feat(input): 组词内按分词的光标与删除(#35 / #187 / #347)
2026-09-18 12:35:52 +08:00
Jia Sui 809f700998 test(server): 覆盖微软双拼 ';' 尾键的分段边界
- nihkb;(你好病 = ni|hk|b;)断言 {0,2,4,6}:';' 归属它开始的音节
- nihcb; 断言 {0,2,3,5,6}:贪心最长配对是契约,'cb' 是有效微软编码
  (cou),按段删除跟随 preedit 显示的同一条切分

Refs #187
2026-09-18 08:59:19 +08:00
Jia Sui 2576986881 feat(windows): Ctrl+左右键按分词移动光标
组词中 Ctrl+Left/Right 按一个字的拼音(音节段)移动光标,
与 Ctrl+Backspace 的删除粒度共用同一套引擎分段边界。

- 闸门归纳为 SegmentEditFunction(code, modifiers):Ctrl-only + 组词存在,
  VK_BACK/LEFT/RIGHT 分别映射到 segment 函数;三处判据与延迟投影共用
- 新增 FUNCTION_MOVE_LEFT_SEGMENT / FUNCTION_MOVE_RIGHT_SEGMENT,
  编辑会话只按 CompositionRestored 的 caret 移动本地光标,不重建 buffer
- Server 用 segment_raw_boundaries() 取前/后边界,能力位与非 UiLess 时回包;
  非拼音/本地模式/UiLess/旧端仍按单字符 ±1 回退

Refs #187
2026-09-17 23:36:34 +08:00
Jia Sui b1ec3ab1e0 feat(server): Ctrl+Backspace 按分词删除前置拼音(#187)
组词中 Ctrl+Backspace 删除光标前的一个分词单元:剩余拼音非空时删
最后一个字的拼音(音节段),剩余拼音为空时删除最后一段已选分词。

engine:
- InputSession::segment_raw_boundaries() 作为按段删除/移动的唯一分段来源:
  全拼把纠错显示串的分隔符映回 raw 下标,双拼与 pinyin_segmentation 共用贪心谓词,
  2 字辅助码自成一段;非拼音方案与本地模式返回空表示无单位模型
server:
- IsSegmentBackspaceKey(Ctrl-only)、整段丢弃 / 边界截断 / 单字符回退三分支,
  截断点残留分隔符清理
- 复用 CompositionRestored 回包(含 caret),仅协商能力位且非 UiLess 时发送;
  新出现「只剩已选汉字、无拼音」的中间态
windows:
- 两处 Ctrl 闸门对组词中的 Ctrl+VK_BACK 放行,新增 FUNCTION_BACKSPACE_SEGMENT
  与 _HandleCompositionBackspaceSegment(读回包重建,失败回退单字符)
- 空 raw 状态渲染、终态清理 word_for_creating_word,延迟投影失效处理

Closes #187
2026-09-17 23:01:31 +08:00
fanlusky bc37ac9a9e feat(installer): 安装时可自定义数据目录位置
词库、用户配置和皮肤此前固定在 %LOCALAPPDATA%\metasequoiaime,整包几百 MB
全压在 C 盘。安装向导新增一页,允许把这部分数据放到任意盘。

程序本体仍固定装在 Program Files:MetasequoiaImeServer.exe 带 uiAccess=true,
只有在受信任目录里这个标志才生效。

运行期的权威来源是 HKLM\Software\Metasequoia\MetasequoiaIME 的 DataDir。
Server、TSF DLL 和引擎三个组件各自按同一顺序解析:METASEQUOIA_IME_DATA_DIR
环境变量 -> 该注册表值 -> %LOCALAPPDATA%\metasequoiaime。三处各持一份实现,
因为组件边界规则禁止 windows/ 与 server/ 互相 include。32 位 TSF DLL 用
KEY_WOW64_64KEY 读,避开 WOW64 重定向。

安全相关的两个约束:

- 安装器在数据目录里写 .metasequoiaime-data 标记。覆盖安装的清理和卸载的整目录
  删除只在看到这个标记(或目录就是历史默认位置)时才执行——用户可能把数据目录
  指到一个本来就有自己文件的文件夹。
- 选择页与 /DATADIR= 走同一套校验:拒绝相对路径、盘符根目录、不存在的盘、程序
  目录内部,以及 Windows/Program Files/各 AppData/用户主目录本身。

升级时改了位置,PrepareToInstall 在停掉输入法进程后用 robocopy /MOVE 搬运用户
拥有的数据(msime_user.db 及其 WAL/SHM/journal、config.toml、config.base.toml、
skins\),词库和前端资源由本次安装重新写入。跨卷移动目录 MoveFile 会失败,所以用
robocopy。用户词库搬运失败会中止安装:否则新版本对着空的 msime_user.db 启动,
ssPostInstall 的回放只记一句 skipped,用户的自造词就静悄悄没了。

测试侧新增 METASEQUOIA_IME_CONFIG_DIR,只改 config.toml 的位置而不搬词库。原先
靠重定向 LOCALAPPDATA 做隔离的办法在装了正式版的机器上已不再成立——注册表值
的优先级高于它。
2026-09-17 22:49:19 +08:00
fanlusky a898c3b11c feat(candidate): Ctrl+Enter 上屏候选右侧的译文
候选框开着且高亮候选带译文时,Ctrl+Enter 上屏那条译文而不是候选本身。
译文只有一条就直接上屏;有多条时把候选框整个换成这几条译义,空格/数字键
照常选一条上屏。

TSF 端原先在分类器最前面把所有 Ctrl 组合都还给应用,Ctrl+Enter 到不了
Server。现在在那条拦截之前先判一次:候选框开着才认,否则组合仍归应用。
认下来的键走既有的 FUNCTION_SERVER_CANDIDATE_KEY 通道,由 Server 决定
是上屏(CommitExactText)还是只开一个新候选页(NavigationIgnored)。

译文页复用同一份 candidate_ui.items,因此云/AI/英文/emoji/颜文字这些异步
回调和候选扩展都要在这个子模式下让路,否则会把译文冲掉。输入串全程没动,
退出时把存下来的 items 和页码原样放回去即可。译文页上的空格/数字键只回
Normal / OutofRange——SELECT_BY_NUMBER 的 TSF 路径只认这两种。
2026-09-17 20:21:56 +08:00
Jia Sui e64195731e feat(server): Backspace 删空剩余拼音后退选已选分词(#35)
已选分词是造词中间态:删空剩余拼音时不再整条 composition 丢弃,
而是退选最后一段并恢复其原始拼音与候选。

engine:
- SelectionTransition 暴露本次选择消费掉的原始输入(保留大小写与双拼按键)
engine/contracts:
- 新增回复类型 CompositionRestored=14 与能力位 CompositionRestore;
  MaxKnown 同步抬高,Server 按协商能力位门控发送,旧 DLL 不会收到未知帧
server:
- 造词中间态压 (consumed_raw, previous_creating_word) 快照,边界 Backspace 弹栈退选
- 任何清空 raw 的路径同时清 creating_word 与快照历史,避免跨 composition 泄漏
windows:
- _HandleCompositionBackspace 提示退选时读回包重建 keystroke buffer / word / 预编辑 / 光标
- NeedToCreateWord 与 CompositionRestored 共用载荷解析;旧 Server 与 UiLess 回退旧行为

Closes #35
2026-09-17 18:45:01 +08:00