From 6baaef085f4991a7971308f0c75f5c2edd11f4dc Mon Sep 17 00:00:00 2001 From: amzxyz Date: Tue, 20 Jan 2026 16:38:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=90=88=E5=B9=B68=E4=B8=AA=E6=8C=89?= =?UTF-8?q?=E9=94=AE=E5=A4=84=E7=90=86=E5=99=A8lua=E4=B8=BA=E4=B8=80?= =?UTF-8?q?=E4=B8=AA=EF=BC=8CKP=E5=B0=8F=E9=94=AE=E7=9B=98=E3=80=81?= =?UTF-8?q?=E5=AD=97=E6=AF=8D=E9=80=89=E8=AF=8D=E3=80=81=E7=AC=A6=E5=8F=B7?= =?UTF-8?q?=E5=BF=AB=E6=89=93=E3=80=81=E8=B6=85=E5=BC=BA=E5=88=86=E8=AF=8D?= =?UTF-8?q?=E3=80=81=E9=87=8D=E5=A4=8D=E9=99=90=E5=88=B6=E3=80=81=E9=80=80?= =?UTF-8?q?=E6=A0=BC=E9=99=90=E5=88=B6=E3=80=81=E5=A3=B0=E8=B0=83=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E3=80=81=E4=BB=A5=E8=AF=8D=E5=AE=9A=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom/wanxiang_pro.schema.yaml | 11 +- lua/backspace_limit.lua | 63 --- lua/kp_number_processor.lua | 172 -------- lua/letter_selector.lua | 89 ----- lua/limit_repeated.lua | 61 --- lua/quick_symbol_text.lua | 137 ------- lua/select_character.lua | 41 -- lua/super_processor.lua | 667 ++++++++++++++++++++++++++++++++ lua/super_segmentation.lua | 250 ------------ lua/tone_fallback.lua | 140 ------- wanxiang.schema.yaml | 10 +- wanxiang_t9.schema.yaml | 3 - 12 files changed, 671 insertions(+), 973 deletions(-) delete mode 100644 lua/backspace_limit.lua delete mode 100644 lua/kp_number_processor.lua delete mode 100644 lua/letter_selector.lua delete mode 100644 lua/limit_repeated.lua delete mode 100644 lua/quick_symbol_text.lua delete mode 100644 lua/select_character.lua create mode 100644 lua/super_processor.lua delete mode 100644 lua/super_segmentation.lua delete mode 100644 lua/tone_fallback.lua diff --git a/custom/wanxiang_pro.schema.yaml b/custom/wanxiang_pro.schema.yaml index d83d9c6..1b2b032 100644 --- a/custom/wanxiang_pro.schema.yaml +++ b/custom/wanxiang_pro.schema.yaml @@ -54,17 +54,10 @@ switches: # 输入引擎 engine: processors: - - lua_processor@*select_character #以词定字,默认左中括号上屏一个词的前一个字,右中括号上屏一个词的后一个字 + - lua_processor@*super_processor #KP小键盘、字母选词、符号快打、超强分词、重复限制、退格限制、声调回退、以词定字 - lua_processor@*partial_commit #通过ctrl+1~0局部提交10个字以内的句子的前几个字(一般为正确的前几个)使用时要遵循合理的分词结构能促进后续编码打出正确的词汇 - - lua_processor@*letter_selector #在N模式R模式下输入数字被视为编码,那么如何上屏呢,现在除了方向键还提供qwertyuio对照1-9来选词 - - lua_processor@*tone_fallback #声调辅助回退,当你输入声调数字错误时,继续输入正确的而不用回退删除 - lua_processor@*super_sequence*P #手动排序,高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+l 移除位移 ctrl+p 置顶 - - lua_processor@*quick_symbol_text #快符引导以及重复上屏,配合quick_symbol_text顶层配置清单定义扩展按键 - lua_processor@*super_tips #超级提示模块:表情、简码、翻译、化学式、等等靠你想象 - - lua_processor@*limit_repeated #用于限制最大候选长度以及最大重复输入声母编码长度,避免性能异常 - - lua_processor@*backspace_limit #防止连续 Backspace 在编码为空时删除已上屏内容 - - lua_processor@*kp_number_processor #管理主键盘小键盘的数字处理逻辑,有输入中数字不上屏和数字一直不上屏设置可选 - - lua_processor@*super_segmentation #通过双击分词符号触发重新分词,并在持续输入分词符号时,能在预设方式之间循环,用于应对类似自然码:必输 必须是 为相同编码导致的必输前置的问题 - ascii_composer #处理英文模式及中英文切换 - recognizer #与 matcher 搭配,处理符合特定规则的输入码,如网址、反查等 tags - key_binder #在特定条件下将按键绑定到其他按键,如重定义逗号、句号为候选翻页、开关快捷键等 @@ -106,7 +99,7 @@ engine: - lua_filter@*super_filter #comment前,相关功能见Lua文件 - lua_filter@*super_english #comment前,负责英文方案及中英混输中英文单词格式化,语句流,自动加空格等策略 - lua_filter@*super_comment_preedit #OpenCC前,超级注释模块、超级preedit,支持错词提示、辅助码显示,部件组字读音注释,有声调、无声调全拼编码的转换,支持个性化配置和关闭相应的功能,详情搜索super_comment_preedit进行详细配置 - - lua_filter@*super_replacer #用来替代OpenCC的处理器 + - lua_filter@*super_replacer #OpenCC替代器,更灵活的处理方式,更自由的自定义方式 - lua_filter@*super_sequence*F #手动排序,对高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+0 移除位移 - uniquifier #去重 diff --git a/lua/backspace_limit.lua b/lua/backspace_limit.lua deleted file mode 100644 index a24c396..0000000 --- a/lua/backspace_limit.lua +++ /dev/null @@ -1,63 +0,0 @@ --- backspace_limiter.lua --- 防止连续 Backspace 在编码为空时删除已上屏内容,虽然我更推荐拍下esc。 --- 这个功能依赖按键事件的处理,运行逻辑的问题在手机上无法得到好的效果,其中macOS特非常特殊,它的按键事件等同于手机逻辑,因此手机和Mac都屏蔽了这一功能 --- @author amzxyz -local M = {} -local ACCEPT, PASS = 1, 2 - --- 引入移动设备检测模块 -local wanxiang = require("wanxiang") - --- 状态标志说明: --- env.prev_input_len: 上一次按键前的输入长度 --- env.bs_sequence: 当前是否处于连续 Backspace 序列中 - -function M.init(env) - env.prev_input_len = -1 -- 初始化为无效值 - env.bs_sequence = false -end - -function M.func(key, env) - local ctx = env.engine.context - local kc = key.keycode - -- 这里嵌入一段记录按键的逻辑,给英文空格使用 - if not key:release() and ctx.composition:empty() then - -- 检测:回车 (0xff0d, 0xff8d) 或 空格 (0x20) - if kc == 0xff0d or kc == 0xff8d or kc == 0x20 then - -- 发送信号:刚才发生了空闲换行或空格,打断英文连贯性 - ctx:set_property("english_spacing", "true") - end - end --嵌入结束 - -- 非 Backspace 键或按键释放事件:重置状态 - if kc ~= 0xFF08 or key:release() then - env.bs_sequence = false - env.prev_input_len = -1 - return PASS - end - - -- 获取当前输入长度 - local current_len = #ctx.input - - -- 处于连续 Backspace 序列中 - if env.bs_sequence then - -- 移动设备由于运行逻辑的问题不能实现友好的逻辑 - if wanxiang.is_mobile_device() then - return PASS -- 直接放行 - -- PC设备保持原有逻辑:长度1变0时拦截 - else - if env.prev_input_len == 1 and current_len == 0 then - return ACCEPT -- 拦截:PC设备上从1变为0的情况 - end - end - -- 更新状态 - env.prev_input_len = current_len - return PASS - end - -- 开始新的 Backspace 序列 - env.bs_sequence = true - env.prev_input_len = current_len - -- 首次按键总是允许 - return PASS -end - -return M \ No newline at end of file diff --git a/lua/kp_number_processor.lua b/lua/kp_number_processor.lua deleted file mode 100644 index 7017482..0000000 --- a/lua/kp_number_processor.lua +++ /dev/null @@ -1,172 +0,0 @@ --- https://github.com/amzxyz/rime_wanxiang --- 万象家族 lua,小键盘行为控制: --- - 小键盘数字:根据 kp_number_mode 决定 “参与编码 / 直接上屏” --- - 主键盘数字:在有候选菜单时,用于选第 n 个候选 --- --- 用法示例(schema.yaml): --- engine: --- processors: --- - lua_processor@*kp_number_processor --- # 小键盘模式(可省略,默认 auto) --- # auto : 空闲时直接上屏,输入中参与编码 --- # compose : 无论是否在输入中,小键盘都参与编码(不直接上屏) --- kp_number_mode: auto - - -local wanxiang = require("wanxiang") - --- 小键盘键码映射 -local KP = { - [0xFFB1] = 1, -- KP_1 - [0xFFB2] = 2, - [0xFFB3] = 3, - [0xFFB4] = 4, - [0xFFB5] = 5, - [0xFFB6] = 6, - [0xFFB7] = 7, - [0xFFB8] = 8, - [0xFFB9] = 9, - [0xFFB0] = 0, -- KP_0 -} -local P = {} - --- [调试工具] 最小化日志打印 (如需调试请取消注释) --- local function log_info(msg) --- log.info("kp_number: " .. tostring(msg)) --- end - --- 检查当前输入+数字是否匹配命令模式 -local function is_function_code_after_digit(env, context, digit_char) - if not context or not digit_char or digit_char == "" then return false end - local code = context.input or "" - local s = code .. digit_char - - local pats = env.function_patterns - if not pats then return false end - - for _, pat in ipairs(pats) do - -- Lua pattern 匹配 - if s:match(pat) then return true end - end - return false -end - ----@param env Env -function P.init(env) - local engine = env.engine - local config = engine.schema.config - local context = engine.context - - env.page_size = config:get_int("menu/page_size") or 6 - local m = config:get_string("kp_number_mode") or "auto" - if m ~= "auto" and m ~= "compose" then m = "auto" end - env.kp_mode = m - - env.context = context - env.is_composing = context:is_composing() - env.has_menu = context:has_menu() - - -- 从 wanxiang 模块加载并转译正则 - -- 这一步会自动处理 YAML 正则到 Lua 模式的所有转换 - env.function_patterns = wanxiang.load_regex_patterns(config, "recognizer/patterns") - - -- log_info("Loaded " .. #(env.function_patterns or {}) .. " patterns.") - env.kp_update_connection = context.update_notifier:connect(function(ctx) - env.context = ctx - env.is_composing = ctx:is_composing() - env.has_menu = ctx:has_menu() - end) -end ----@param env Env -function P.fini(env) - if env.kp_update_connection then - env.kp_update_connection:disconnect() - env.kp_update_connection = nil - end - env.context = nil - env.is_composing = nil - env.has_menu = nil - env.function_patterns = nil -end - ----@param key KeyEvent ----@param env Env ----@return ProcessResult -function P.func(key, env) - if key:release() then return wanxiang.RIME_PROCESS_RESULTS.kNoop end - - local context = env.context or env.engine.context - local mode = env.kp_mode or "auto" - local page_sz = env.page_size - - -- 1) 小键盘数字处理 - local kp_num = KP[key.keycode] - if kp_num ~= nil then - if key:ctrl() or key:alt() or key:super() or key:shift() then - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - local ch = tostring(kp_num) - - -- 如果匹配到正则(如网址、反查),则拦截,强制作为编码输入 - if is_function_code_after_digit(env, context, ch) then - if context.push_input then context:push_input(ch) - else context.input = (context.input or "") .. ch end - return wanxiang.RIME_PROCESS_RESULTS.kAccepted - end - - -- 正常数字逻辑 - if mode == "auto" then - if env.is_composing then - if context.push_input then context:push_input(ch) - else context.input = (context.input or "") .. ch end - else - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - else -- compose - if context.push_input then context:push_input(ch) - else context.input = (context.input or "") .. ch end - end - return wanxiang.RIME_PROCESS_RESULTS.kAccepted - end - - -- 2) 主键盘数字处理 - local r = key:repr() or "" - if r:match("^[0-9]$") then - if key:ctrl() or key:alt() or key:super() then - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - if is_function_code_after_digit(env, context, r) then - if context.push_input then context:push_input(r) - else context.input = (context.input or "") .. r end - return wanxiang.RIME_PROCESS_RESULTS.kAccepted - end - - if env.has_menu then - local d = tonumber(r) - if d == 0 then d = 10 end - if d and d >= 1 and d <= page_sz then - local composition = context.composition - if composition and not composition:empty() then - local seg = composition:back() - local menu = seg and seg.menu - if menu and not menu:empty() then - local sel_index = seg.selected_index or 0 - local page_start = math.floor(sel_index / page_sz) * page_sz - local index = page_start + (d - 1) - if index < menu:candidate_count() then - if context:select(index) then - return wanxiang.RIME_PROCESS_RESULTS.kAccepted - end - end - end - end - end - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - end - - return wanxiang.RIME_PROCESS_RESULTS.kNoop -end - -return P \ No newline at end of file diff --git a/lua/letter_selector.lua b/lua/letter_selector.lua deleted file mode 100644 index 7c5f2e9..0000000 --- a/lua/letter_selector.lua +++ /dev/null @@ -1,89 +0,0 @@ --- @amzxyz https://github.com/amzxyz/rime_wanxiang --- 功能:仅在特定前缀或者tag模式下,按 qwertyuio 选择第 1~9 个候选 - -local wanxiang = require("wanxiang") - -local M = {} - --- 键码映射:q w e r t y u i o → 1..9 -local KEY2IDX = { - [0x71] = 1, -- q - [0x77] = 2, -- w - [0x65] = 3, -- e - [0x72] = 4, -- r - [0x74] = 5, -- t - [0x79] = 6, -- y - [0x75] = 7, -- u - [0x69] = 8, -- i - [0x6F] = 9, -- o -} - --- 判断是否在命令模式 -local function is_function_mode_active(context) - if not context or not context.composition or context.composition:empty() then - return false - end - local seg = context.composition:back() - if not seg then return false end - return seg:has_tag("number") or seg:has_tag("Ndate") -end - --- 缓存命令模式的状态,避免每次按键都计算 -local function on_update(env, ctx) - env._fn_active = is_function_mode_active(ctx) -end - -function M.init(env) - env._fn_active = false - env._upd_conn = env.engine.context.update_notifier:connect(function(ctx) - on_update(env, ctx) - end) -end - -function M.fini(env) - if env._upd_conn then - env._upd_conn:disconnect() - env._upd_conn = nil - end -end - -local function handle_key(key_event, env) - -- 只处理按下;有修饰键则忽略 - if key_event:release() or key_event:ctrl() or key_event:alt() or key_event:super() then - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - local idx = KEY2IDX[key_event.keycode] - if not idx then - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - local context = env.engine.context - if not env._fn_active then - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - if not context or not context.composition or context.composition:empty() then - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - local seg = context.composition:back() - if not seg or not seg.menu then - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - -- 准备最多 9 个候选 - local count = seg.menu:prepare(9) - if idx < 1 or idx > count then - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - -- 选择:候选索引从 0 开始 - context:select(idx - 1) - return wanxiang.RIME_PROCESS_RESULTS.kAccepted -end - -function M.func(key_event, env) - return handle_key(key_event, env) -end - -return M diff --git a/lua/limit_repeated.lua b/lua/limit_repeated.lua deleted file mode 100644 index adc9e1d..0000000 --- a/lua/limit_repeated.lua +++ /dev/null @@ -1,61 +0,0 @@ --- 用于限制最大候选数量以及重复最大输入编码,防止卡顿性能异常 ---@amzxyz ---https://github.com/amzxyz -local M = {} -local ACCEPT, PASS = 1, 2 - -local MAX_REPEAT = 8 -- 连续重复输入声母上限 -local MAX_SEGMENTS = 40 -- 允许的最大“分段”数 -local INITIALS = "[bpmfdtnlgkhjqxrzcsywiu]" - --- 计算末尾重复 -local function tail_rep(s) - local last, n = s:sub(-1), 1 - for i = #s - 1, 1, -1 do - if s:sub(i, i) == last then n = n + 1 else break end - end - return last, n -end - --- 在候选栏最后一个 segment 加提示 -local function prompt(ctx, msg) - local comp = ctx.composition - if comp and not comp:empty() then comp:back().prompt = msg end -end - -function M.func(key, env) - local ctx, kc = env.engine.context, key.keycode - -- 先拿到“上一轮”高亮候选的 preedit 及段数 - local cand = ctx:get_selected_candidate() - local preedit = cand and (cand.preedit or cand:get_genuine().preedit) or "" - local segs = 1 - for _ in preedit:gmatch("[%'%s]") do segs = segs + 1 end - -- 本次按键字符(只关心字母 / 分隔符) - local ch - if kc >= 0x61 and kc <= 0x7A then -- a~z - ch = string.char(kc) - elseif kc == 0x27 then -- ' - ch = "'" - elseif kc == 0x20 then -- space - ch = " " - end - -- ① 连续声母限制:第 MAX_REPEAT 个同声母直接拦截 - if ch and kc >= 0x61 and kc <= 0x7A then - local nxt = ctx.input .. ch - local last, rep_n = tail_rep(nxt) - if last:match(INITIALS) and rep_n > MAX_REPEAT then - prompt(ctx, " 〔已超最大重复声母〕") - return ACCEPT - end - end - -- ② 分段限制:第 MAX_SEGMENTS 段拦截 - local segs_after = segs - if ch == "'" or ch == " " then segs_after = segs + 1 end - if segs_after >= MAX_SEGMENTS and kc >= 0x61 and kc <= 0x7A then - prompt(ctx, " 〔已超最大输入长度〕") - return ACCEPT - end - return PASS -end - -return M \ No newline at end of file diff --git a/lua/quick_symbol_text.lua b/lua/quick_symbol_text.lua deleted file mode 100644 index 2675536..0000000 --- a/lua/quick_symbol_text.lua +++ /dev/null @@ -1,137 +0,0 @@ --- 欢迎使用万象拼音方案(quick_symbol_text) --- @amzxyz --- https://github.com/amzxyz/rime_wanxiang --- 触发:由 schema.yaml -> quick_symbol_text/trigger 加载(默认 ^([a-z])/$ ) --- a/、b/ ... 单字母触发预设编码自动上屏;值可设为 "repeat" 实现重复上屏上一条提交内容 --- custom>schema>lua 合并键值(仅合并单字母 a-z 键) - -local wanxiang = require("wanxiang") - --- 读取 symkey -local function load_mapping_from_config(config) - local symbol_map = {} - local ok_map, map = pcall(function() return config:get_map("quick_symbol_text/symkey") end) - if not ok_map or not map then return symbol_map end - local ok_keys, keys = pcall(function() return map:keys() end) - if not ok_keys or not keys then return symbol_map end - for _, key in ipairs(keys) do - local v = config:get_string("quick_symbol_text/symkey/" .. key) - if v ~= nil then - symbol_map[string.lower(tostring(key))] = v - end - end - return symbol_map -end - --- 读取 trigger -local function load_trigger_from_config(config) - local default_pat = "^([a-z])/$" - if not config then return default_pat end - local ok, s = pcall(function() return config:get_string("quick_symbol_text/trigger") end) - if ok and type(s) == "string" and #s > 0 then return s end - return default_pat -end - --- 默认单字母映射 -local default_mapping = { - q = ":", - w = "?", - e = "(", - r = ")", - t = "~", - y = "·", - u = "『", - i = "』", - o = "〖", - p = "〗", - a = "!", - s = "……", - d = "、", - f = "“", - g = "”", - h = "‘", - j = "’", - k = "【", - l = "】", - z = "。", - x = "?", - c = "!", - v = "——", - b = "%", - n = "《", - m = "》", -} - -local function init(env) - local config = env.engine.schema.config - env.single_symbol_pattern = load_trigger_from_config(config) - - -- 默认表 - env.mapping = {} - for k, v in pairs(default_mapping) do - if #k == 1 and k:match("^[a-z]$") then env.mapping[k] = v end - end - -- 覆盖(仅单字母) - local custom = load_mapping_from_config(config) - for k, v in pairs(custom) do - local key = tostring(k):lower() - if #key == 1 and key:match("^[a-z]$") then - env.mapping[key] = v -- ""=禁用;"repeat"=特殊语义 - end - end - - env.last_commit_text = "欢迎使用万象拼音!" - - -- 记录上屏文本(供 repeat) - env.quick_symbol_text_commit_notifier = - env.engine.context.commit_notifier:connect(function(ctx) - local t = ctx:get_commit_text() - if t ~= "" then env.last_commit_text = t end - end) - - -- 命中触发则上屏并清空 - env.quick_symbol_text_update_notifier = - env.engine.context.update_notifier:connect(function(context) - local input = context.input or "" - local key = string.match(input, env.single_symbol_pattern) - if not key then return end - key = string.lower(key) - local symbol = env.mapping[key] - if symbol == nil or symbol == "" then return end -- 未配置/禁用 - if type(symbol) == "string" and symbol:lower() == "repeat" then - if env.last_commit_text ~= "" then - env.engine:commit_text(env.last_commit_text) - context:clear() - end - else - env.engine:commit_text(symbol) - context:clear() - end - end) -end - -local function fini(env) - if env.quick_symbol_text_commit_notifier then - env.quick_symbol_text_commit_notifier:disconnect() - env.quick_symbol_text_commit_notifier = nil - end - if env.quick_symbol_text_update_notifier then - env.quick_symbol_text_update_notifier:disconnect() - env.quick_symbol_text_update_notifier = nil - end -end - --- 命中时吃键,避免后续流程处理 -local function processor(key_event, env) - local input = env.engine.context.input or "" - local key = string.match(input, env.single_symbol_pattern) - if key then - key = string.lower(key) - local symbol = env.mapping[key] - if symbol ~= nil and symbol ~= "" then - return wanxiang.RIME_PROCESS_RESULTS.kAccepted - end - end - return wanxiang.RIME_PROCESS_RESULTS.kNoop -end -return { init = init, fini = fini, func = processor } \ No newline at end of file diff --git a/lua/select_character.lua b/lua/select_character.lua deleted file mode 100644 index 356a61a..0000000 --- a/lua/select_character.lua +++ /dev/null @@ -1,41 +0,0 @@ --- 以词定字 - -local wanxiang = require("wanxiang") - -local select = {} - -function select.init(env) - local config = env.engine.schema.config - select.first_key = config:get_string('key_binder/select_first_character') - select.last_key = config:get_string('key_binder/select_last_character') -end - -function select.func(key, env) - local engine = env.engine - local context = env.engine.context - - if - not key:release() - and (context:is_composing() or context:has_menu()) - and (select.first_key or select.last_key) - then - local text = context.input - if context:get_selected_candidate() then - text = context:get_selected_candidate().text - end - if utf8.len(text) > 1 then - if (key:repr() == select.first_key) then - engine:commit_text(text:sub(1, utf8.offset(text, 2) - 1)) - context:clear() - return wanxiang.RIME_PROCESS_RESULTS.kAccepted - elseif (key:repr() == select.last_key) then - engine:commit_text(text:sub(utf8.offset(text, -1))) - context:clear() - return wanxiang.RIME_PROCESS_RESULTS.kAccepted - end - end - end - return wanxiang.RIME_PROCESS_RESULTS.kNoop -end - -return select diff --git a/lua/super_processor.lua b/lua/super_processor.lua new file mode 100644 index 0000000..e30d613 --- /dev/null +++ b/lua/super_processor.lua @@ -0,0 +1,667 @@ +-- lua/super_processor.lua +-- @amzxyz +-- https://github.com/amzxyz/rime_wanxiang +-- 全能按键处理器:整合 KP小键盘、字母选词、符号快打、超强分词、重复限制、退格限制、声调回退、以词定字 +-- +-- 用法: 在 schema.yaml 中 engine/processors 列表添加 - lua_processor@*super_processor + +local wanxiang = require("wanxiang") +local M = {} + +local K_REJECT, K_ACCEPT, K_NOOP = 0, 1, 2 + +-- 1. 全局常量定义 (Constants) + +-- [KpNumber] 小键盘键码映射 +local KP_MAP = { + [0xFFB1] = 1, [0xFFB2] = 2, [0xFFB3] = 3, + [0xFFB4] = 4, [0xFFB5] = 5, [0xFFB6] = 6, + [0xFFB7] = 7, [0xFFB8] = 8, [0xFFB9] = 9, + [0xFFB0] = 0, +} + +-- [LetterSelector] 字母选词键码映射 (qwert...) +local LETTER_SEL_MAP = { + [0x71] = 1, [0x77] = 2, [0x65] = 3, [0x72] = 4, [0x74] = 5, + [0x79] = 6, [0x75] = 7, [0x69] = 8, [0x6F] = 9, [0x70] = 10, +} + +-- [QuickSymbol] 默认符号映射表 +local SYMBOL_DEFAULT = { + q=":", w="?", e="(", r=")", t="~", y="·", u="『", i="』", o="〖", p="〗", + a="!", s="……", d="、", f="“", g="”", h="‘", j="’", k="【", l="】", + z="。", x="?", c="!", v="——", b="%", n="《", m="》" +} + +-- [LimitRepeated] 重复限制配置 +local MAX_REPEAT = 8 +local MAX_SEGMENTS = 40 +local INITIALS = "[bpmfdtnlgkhjqxrzcsywiu]" + +-- [SuperSegmentation] 分词模式配置 +local SEG_PATTERNS = { + [3] = { all = { {2,1}, {1,2} } }, + [4] = { all = { {2,2}, {1,3}, {3,1} } }, + [5] = { all = { {2,3}, {3,2} } }, + [6] = { all = { {2,2,2}, {3,3} } }, + [7] = { all = { {2,2,3}, {2,3,2}, {3,2,2} } }, + [8] = { all = { {2,2,2,2}, {2,3,3}, {3,2,3}, {3,3,2} } }, + [10] = { all = { {2,2,2,2,2} } }, +} + +-- 2. 核心辅助函数 (Utilities) + +-- 字符串转义 +local function escp(ch) return ch:gsub("(%W)","%%%1") end + +-- 数组求和 +local function sum(a) local s=0; for _,v in ipairs(a) do s=s+v end; return s end + +-- 表键生成 +local function key_of(a) return table.concat(a, ",") end + +-- 列表查找索引 +local function find_idx(list, key) for i,t in ipairs(list) do if key_of(t)==key then return i end end end + +-- 统计末尾指定字符数量 +local function count_trailing(s, ch) local n=0; for i=#s,1,-1 do if s:sub(i,i)==ch then n=n+1 else break end end; return n end + +-- 移除末尾指定字符 +local function strip_trailing(s, ch) return (s:gsub(escp(ch).."+$","")) end + +-- 移除分隔符 (自动和手动) +local function strip_delims(s, md, ad) + if md and md~="" then s = s:gsub(escp(md),"") end + if ad and ad~="" then s = s:gsub(escp(ad),"") end + return s +end + +-- 根据分组重构字符串 +local function build_by_groups(core, ch_manual, groups) + if not groups or #groups==0 or sum(groups)~=#core then return core end + local out, i = {}, 1 + for gi,g in ipairs(groups) do + out[#out+1] = core:sub(i, i+g-1); i = i + g + if gi < #groups then out[#out+1] = ch_manual end + end + return table.concat(out) +end + +-- 从字符串解析分段长度 +local function lens_from_string(s, md, ad) + if not s or s=="" then return nil end + local segs, buf = {}, {} + local function flush() if #buf>0 then segs[#segs+1]=table.concat(buf); buf={} end end + for i=1,#s do + local c=s:sub(i,i) + if c==md or c==ad or c==" " then flush() + else + local b=string.byte(c) + if b and ((b>=65 and b<=90) or (b>=97 and b<=122)) then buf[#buf+1]=string.char(b):lower() end + end + end + flush() + if #segs==0 then return nil end + local L={}; for _,seg in ipairs(segs) do L[#L+1]=#seg end + return L +end + +-- 获取缓存的分段长度 +local function get_cached_lens(env, ctx, md, ad) + local L = env.seg_last_preedit_lens + if L and type(L)=="table" and #L>0 then return L end + local seg = ctx.composition:back() + local cand = seg and seg:get_selected_candidate() or nil + return lens_from_string(cand and cand.preedit or nil, md, ad) +end + +-- 增强版 UTF-8 长度计算 (Super Segmentation 使用) +local function ulen(s) + if not s or s == "" then return 0 end + if utf8 and utf8.len then + local ok, n = pcall(utf8.len, s) + if ok and n then return n end + end + local n = 0 + if utf8 and utf8.codes then + for _ in utf8.codes(s) do n = n + 1 end + return n + end + return #s +end + +-- 检查数字后是否紧跟功能编码 (KpNumber 使用) +local function is_function_code_after_digit(env, context, digit_char) + if not context or not digit_char or digit_char == "" then return false end + local code = context.input or "" + local s = code .. digit_char + local pats = env.kp_func_patterns + if not pats then return false end + for _, pat in ipairs(pats) do + if s:match(pat) then return true end + end + return false +end + +-- 计算尾部重复字符数 (LimitRepeated 使用) +local function tail_rep(s) + local last, n = s:sub(-1), 1 + for i = #s - 1, 1, -1 do + if s:sub(i, i) == last then n = n + 1 else break end + end + return last, n +end + +-- 设置候选框提示 (LimitRepeated 使用) +local function prompt(ctx, msg) + local comp = ctx.composition + if comp and not comp:empty() then comp:back().prompt = msg end +end + +-- 压缩连续声调 (ToneFallback 使用) +local function compress_runs_keep_last(text) + local changed = false + local out = text:gsub('([7890])([7890]+)', function(_, tail) + changed = true + return tail:sub(-1) + end) + return out, changed +end + +-- 3. 初始化与资源管理 (Init & Fini) + +function M.init(env) + local engine = env.engine + local config = engine.schema.config + local context = engine.context + + -- [1] 配置加载 (按功能模块分类) + + -- [BackspaceLimit] + env.bs_prev_len = -1 + env.bs_sequence = false + + -- [KpNumber] 小键盘 + env.kp_page_size = config:get_int("menu/page_size") or 6 + local m = config:get_string("kp_number_mode") or "auto" + env.kp_mode = (m == "auto" or m == "compose") and m or "auto" + env.kp_func_patterns = wanxiang.load_regex_patterns(config, "recognizer/patterns") + + -- [LetterSelector] 字母选词状态位 + env.ls_active = false + + -- [ToneFallback] 声调容错 + env.tone_state = "idle" + env.lookup_key = config:get_string('wanxiang_lookup/key') or '`' + + -- [QuickSymbol] 符号快打 + env.qs_trigger = "^([a-z])/$" + if config then + local ok, s = pcall(function() return config:get_string("quick_symbol_text/trigger") end) + if ok and type(s)=="string" and #s>0 then env.qs_trigger = s end + end + env.qs_mapping = {} + for k, v in pairs(SYMBOL_DEFAULT) do env.qs_mapping[k] = v end + local ok_map, map = pcall(function() return config:get_map("quick_symbol_text/symkey") end) + if ok_map and map then + local ok_keys, keys = pcall(function() return map:keys() end) + if ok_keys and keys then + for _, key in ipairs(keys) do + local v = config:get_string("quick_symbol_text/symkey/" .. key) + if v then env.qs_mapping[string.lower(tostring(key))] = v end + end + end + end + env.qs_last_commit = "欢迎使用万象拼音!" + + -- [SelectCharacter] 以词定字 + env.sc_first_key = config:get_string('key_binder/select_first_character') + env.sc_last_key = config:get_string('key_binder/select_last_character') + + -- [SuperSegmentation] 超强分词 + local delim = config:get_string("speller/delimiter") or " '" + env.seg_auto_delim = delim:sub(1,1) + env.seg_manual_delim = delim:sub(2,2) + env.seg_core = nil + env.seg_start_idx = nil + env.seg_N = nil + env.seg_base = nil + + -- [2] 统一 Update Notifier (状态缓存与自动处理) + + env.conn_update = context.update_notifier:connect(function(ctx) + -- A. [ToneFallback] 执行声调压缩 + local t_state = env.tone_state or "idle" + env.tone_state = "idle" + + local input = ctx.input or "" + if t_state == "compress" and input ~= "" then + local caret = (ctx.caret_pos ~= nil) and ctx.caret_pos or #input + if caret < 0 then caret = 0 end + if caret > #input then caret = #input end + + local left = (caret > 0) and input:sub(1, caret) or "" + local left_new, changed = compress_runs_keep_last(left) + + if changed then + if caret > 0 then ctx:pop_input(caret) end + if #left_new > 0 then ctx:push_input(left_new) end + end + end + + -- B. [SuperSegmentation] 缓存数据 + local seg = ctx.composition:back() + local cand = seg and seg:get_selected_candidate() or nil + local pre = cand and cand.preedit or nil + env.seg_last_preedit_lens = lens_from_string(pre, env.seg_manual_delim, env.seg_auto_delim) + env.seg_last_input_caret = input + env.seg_last_caret_pos = ctx.caret_pos + + -- C. [LetterSelector] 缓存激活状态 + env.ls_active = false + if not ctx.composition:empty() then + local s = ctx.composition:back() + if s and (s:has_tag("number") or s:has_tag("Ndate")) then + env.ls_active = true + end + end + + -- D. [KpNumber] 缓存状态 + env.kp_is_composing = ctx:is_composing() + env.kp_has_menu = ctx:has_menu() + + -- E. [QuickSymbol] 自动上屏逻辑 + local qkey = string.match(input, env.qs_trigger) + if qkey then + qkey = string.lower(qkey) + local symbol = env.qs_mapping[qkey] + if symbol and symbol ~= "" then + if type(symbol)=="string" and symbol:lower()=="repeat" then + if env.qs_last_commit ~= "" then + engine:commit_text(env.qs_last_commit) + ctx:clear() + end + else + engine:commit_text(symbol) + ctx:clear() + end + end + end + end) + -- [3] 统一 Commit Notifier (记录上屏) + env.conn_commit = context.commit_notifier:connect(function(ctx) + local t = ctx:get_commit_text() + if t ~= "" then env.qs_last_commit = t end + end) +end + +function M.fini(env) + if env.conn_update then env.conn_update:disconnect(); env.conn_update = nil end + if env.conn_commit then env.conn_commit:disconnect(); env.conn_commit = nil end + env.memory = nil +end + +-- 4. 逻辑分发处理 (Handlers) + +-- [QuickSymbol] 拦截触发键,防止进入 Speller +local function handle_quick_symbol_intercept(key, env, ctx) + local input = ctx.input or "" + local matched = string.match(input, env.qs_trigger) + if matched then + local k = string.lower(matched) + if env.qs_mapping[k] and env.qs_mapping[k] ~= "" then + return true -- Accepted + end + end + return false +end + +-- [SuperSegmentation] 处理分词符 ' +local function handle_segmentation(key, env, ctx) + if key.keycode ~= string.byte(env.seg_manual_delim) then + env.seg_core, env.seg_start_idx, env.seg_N, env.seg_base = nil, nil, nil, nil + return false + end + if ctx.composition:empty() then return false end + + local last_input = env.seg_last_input_caret or ctx.input or "" + local last_caret = env.seg_last_caret_pos + if not last_caret or last_caret ~= ulen(last_input) then + env.seg_core, env.seg_start_idx, env.seg_N, env.seg_base = nil, nil, nil, nil + return false + end + + local md = env.seg_manual_delim + local before = ctx.input or "" + local after = before .. md + local tlen = count_trailing(after, md) + local head = strip_trailing(after, md) + local core = strip_delims(head, md, env.seg_auto_delim) + local N = #core + local conf = SEG_PATTERNS[N] + -- 大于 10 码动态构建分词:在2、3码之间循环 + if N > 10 then + local groups_2 = {} + for i = 1, math.floor(N / 2) do table.insert(groups_2, 2) end + if N % 2 ~= 0 then table.insert(groups_2, N % 2) end + + local groups_3 = {} + for i = 1, math.floor(N / 3) do table.insert(groups_3, 3) end + if N % 3 ~= 0 then table.insert(groups_3, N % 3) end + + conf = { all = { groups_2, groups_3 } } + end + if env.seg_core ~= core or env.seg_N ~= N then + env.seg_core = core + env.seg_N = N + env.seg_start_idx = nil + env.seg_base = nil + end + + if env.seg_base == nil then env.seg_base = head end + + if conf and env.seg_start_idx == nil then + local start_idx = 0 + local L = get_cached_lens(env, ctx, md, env.seg_auto_delim) + if not (L and sum(L)==N) then L = lens_from_string(head, md, env.seg_auto_delim) end + if L and sum(L)==N then + local idx = find_idx(conf.all, key_of(L)) + if idx then start_idx = idx end + end + env.seg_start_idx = start_idx + end + + if tlen == 1 then + ctx.input = after + return true + end + + if not conf then + ctx.input = after + return true + end + + local m = #conf.all + local k = tlen - 1 + + local function restore() + ctx.input = (env.seg_base or head) .. md + env.seg_core, env.seg_start_idx, env.seg_N, env.seg_base = nil, nil, nil, nil + env.seg_core = core; env.seg_N = N + end + + if env.seg_start_idx and env.seg_start_idx ~= 0 then + local cycle_len = m + local r = k % cycle_len + if r == 0 then restore(); return true end + local idx = ((env.seg_start_idx - 1 + r) % m) + 1 + local rebuilt = build_by_groups(core, md, conf.all[idx]) + ctx.input = rebuilt .. md:rep(tlen) + return true + else + local cycle_len = m + 1 + local r = k % cycle_len + if r == 0 then restore(); return true end + local idx = ((r - 1) % m) + 1 + local rebuilt = build_by_groups(core, md, conf.all[idx]) + ctx.input = rebuilt .. md:rep(tlen) + return true + end +end + +-- [Backspace Limit] 退格限制 +local function handle_backspace(key, env, ctx) + local kc = key.keycode + if not key:release() and ctx.composition:empty() then + if kc == 0xff0d or kc == 0xff8d or kc == 0x20 then + ctx:set_property("english_spacing", "true") --记下来按键状态给英文自动加空格用 + end + end + + if kc ~= 0xFF08 or key:release() then + env.bs_sequence = false + env.bs_prev_len = -1 + return false + end + + local cur_len = ctx.input and #ctx.input or 0 + if env.bs_sequence then + if not wanxiang.is_mobile_device() then + if env.bs_prev_len == 1 and cur_len == 0 then + return true + end + end + env.bs_prev_len = cur_len + return false + end + env.bs_sequence = true + env.bs_prev_len = cur_len + return false +end + +-- [Limit Repeated] 重复输入限制 +local function handle_limit_repeat(key, env, ctx) + local kc = key.keycode + if not (kc >= 0x61 and kc <= 0x7A) then return false end + + local cand = ctx:get_selected_candidate() + local preedit = cand and (cand.preedit or cand:get_genuine().preedit) or "" + local segs = 1 + for _ in preedit:gmatch("[%'%s]") do segs = segs + 1 end + + local ch = string.char(kc) + local input = ctx.input or "" + local nxt = input .. ch + local last, rep_n = tail_rep(nxt) + + if last:match(INITIALS) and rep_n > MAX_REPEAT then + prompt(ctx, " 〔已超最大重复声母〕") + return true + end + + if segs >= MAX_SEGMENTS then + prompt(ctx, " 〔已超最大输入长度〕") + return true + end + return false +end + +-- [Letter Selector] 字母选词 +local function handle_letter_select(key, env, ctx) + if not env.ls_active then return false end + if key:ctrl() or key:alt() or key:super() then return false end + local idx = LETTER_SEL_MAP[key.keycode] + if not idx then return false end + + if ctx.composition:empty() then return false end + local seg = ctx.composition:back() + if not seg or not seg.menu then return false end + + local count = seg.menu:prepare(9) + if idx < 1 or idx > count then return false end + + ctx:select(idx - 1) + return true +end + +-- [Select Character] 以词定字逻辑 (New!) +local function handle_select_character(key, env, ctx) + -- 1. 检查配置是否存在 + if not (env.sc_first_key or env.sc_last_key) then return false end + + -- 2. 状态检查:必须在输入中或有候选菜单 + if not (ctx:is_composing() or ctx:has_menu()) then return false end + + -- 3. 键值匹配 + local repr = key:repr() + local is_first = (repr == env.sc_first_key) + local is_last = (repr == env.sc_last_key) + if not (is_first or is_last) then return false end + + -- 4. 获取当前选中的候选词或输入 + local text = ctx.input + local cand = ctx:get_selected_candidate() + if cand then text = cand.text end + + -- 5. 执行上屏 + if utf8.len(text) > 1 then + if is_first then + -- 上屏第一个字 (sub: 1 到 第二个字偏移量-1) + env.engine:commit_text(text:sub(1, utf8.offset(text, 2) - 1)) + ctx:clear() + return true -- Accepted + elseif is_last then + -- 上屏最后一个字 (sub: 最后一个字偏移量) + env.engine:commit_text(text:sub(utf8.offset(text, -1))) + ctx:clear() + return true -- Accepted + end + end + return false +end + +-- [KpNumber & ToneFallback] 数字键综合逻辑 +local function handle_number_logic(key, env, ctx) + local kc = key.keycode + local input = ctx.input or "" + + -- A. 小键盘处理 (KpNumber) + local kp_num = KP_MAP[kc] + if kp_num ~= nil then + if key:ctrl() or key:alt() or key:super() or key:shift() then return false end + + -- ToneFallback: 小键盘必须跳过压缩 + env.tone_state = "skip" + + local ch = tostring(kp_num) + + -- 1. 正则拦截 + if is_function_code_after_digit(env, ctx, ch) then + if ctx.push_input then ctx:push_input(ch) else ctx.input = input .. ch end + return true + end + -- 2. 模式处理 + if env.kp_mode == "auto" then + if env.kp_is_composing then + if ctx.push_input then ctx:push_input(ch) else ctx.input = input .. ch end + else + return false -- Noop + end + else -- compose mode + if ctx.push_input then ctx:push_input(ch) else ctx.input = input .. ch end + end + return true + end + + -- B. 主键盘数字 + local r = key:repr() or "" + if r:match("^[0-9]$") then + if key:ctrl() or key:alt() or key:super() then return false end + + -- ToneFallback: 标记回退意图 + local is_func_mode = false + if wanxiang.is_function_mode_active then + is_func_mode = wanxiang.is_function_mode_active(ctx) + end + -- 如果是反查模式 OR 功能模式,状态设为 idle (不回退) + if input:find(env.lookup_key, 1, true) or is_func_mode then + env.tone_state = "idle" + -- 这里不 return,因为数字键可能还有“正则拦截”或“候选选词”的任务 + else + env.tone_state = "compress" + local caret = (ctx.caret_pos ~= nil) and ctx.caret_pos or #input + if caret > #input then caret = #input end + local left = (caret > 0) and input:sub(1, caret) or "" + local _, changed = compress_runs_keep_last(left) + if changed then return true end + end + + -- 正则拦截 + if is_function_code_after_digit(env, ctx, r) then + if ctx.push_input then ctx:push_input(r) else ctx.input = input .. r end + return true + end + + -- 候选选词 + if env.kp_has_menu then + local d = tonumber(r) + if d == 0 then d = 10 end + if d and d >= 1 and d <= env.kp_page_size then + local comp = ctx.composition + if comp and not comp:empty() then + local seg = comp:back() + local menu = seg and seg.menu + if menu and not menu:empty() then + local sel_index = seg.selected_index or 0 + local page_start = math.floor(sel_index / env.kp_page_size) * env.kp_page_size + local index = page_start + (d - 1) + if index < menu:candidate_count() then + if ctx:select(index) then return true end + end + end + end + end + return false + end + else + env.tone_state = "idle" + end + return false +end + +-- 5. 主入口函数 (Main Logic Flow) +function M.func(key, env) + local ctx = env.engine.context + + -- 1. 优先处理按键释放 + if key:release() then + handle_backspace(key, env, ctx) + return K_NOOP + end + + local kc = key.keycode + + -- 2. QuickSymbol 拦截 (a-z + /) + if handle_quick_symbol_intercept(key, env, ctx) then + return K_ACCEPT + end + + -- 3. Backspace 退格防止删除已上屏内容 + if kc == 0xFF08 then + if handle_backspace(key, env, ctx) then return K_ACCEPT end + end + + -- 4. Select Character 以词定字 (New!) + -- 它的优先级很高,因为是针对当前候选的操作 + -- 但必须在 Backspace 之后,防止误操作 + if handle_select_character(key, env, ctx) then + return K_ACCEPT + end + + -- 5. 分词符 ' [SuperSegmentation] 处理分词符 ' + if kc == 0x27 then + if handle_segmentation(key, env, ctx) then return K_ACCEPT end + end + + -- 6. 字母键 (a-z)[Limit Repeated] 重复输入限制 + if kc >= 0x61 and kc <= 0x7A then + if handle_limit_repeat(key, env, ctx) then return K_ACCEPT end + end + + -- 7. (q-o + 特定 Tag)[Letter Selector] 字母选词 + if env.ls_active and (LETTER_SEL_MAP[kc] ~= nil) then + if handle_letter_select(key, env, ctx) then return K_ACCEPT end + end + + -- 8. 数字键 (小键盘 + 声调 + 选词)[KpNumber & ToneFallback] 数字键综合逻辑 + if (kc >= 0xFFB0 and kc <= 0xFFB9) or (kc >= 0x30 and kc <= 0x39) then + if handle_number_logic(key, env, ctx) then return K_ACCEPT end + else + -- 非数字键,重置声调状态 + env.tone_state = "idle" + end + + return K_NOOP +end + +return M \ No newline at end of file diff --git a/lua/super_segmentation.lua b/lua/super_segmentation.lua deleted file mode 100644 index 8bfda7f..0000000 --- a/lua/super_segmentation.lua +++ /dev/null @@ -1,250 +0,0 @@ --- super_segmentation.lua ---@amzxyz https://github.com/amzxyz/rime_wanxiang --- 规则: --- 1) 第 1 个 ':仅记录“现场”(baseline_head=当前整段输入,含你之前的手动分隔),记录起点索引,不重建 --- 2) 第 2 个 ' 起:开始循环 --- - 命中起点 s:只循环 s 后面的 m-1 个形态(跳过 s 本身) --- - 未命中:从 all[1] 开始循环 m 个形态 --- 3) 走完一圈:恢复到 baseline_head,并尾部只保留 1 个 ' --- 4) 支持 N=3..8(可扩展 PATTERNS) --- 5) 使用 update_notifier 预缓存可见分段,避免移动端“晚一拍” - -local K_REJECT, K_ACCEPT, K_NOOP = 0, 1, 2 -local M = {} - --- ---------- utils ---------- -local function escp(ch) return ch:gsub("(%W)","%%%1") end -local function sum(a) local s=0; for _,v in ipairs(a) do s=s+v end; return s end -local function key_of(a) return table.concat(a, ",") end -local function find_idx(list, key) for i,t in ipairs(list) do if key_of(t)==key then return i end end end -local function count_trailing(s, ch) local n=0; for i=#s,1,-1 do if s:sub(i,i)==ch then n=n+1 else break end end; return n end -local function strip_trailing(s, ch) return (s:gsub(escp(ch).."+$","")) end - --- 去掉手动与自动分隔符,得到“纯编码” -local function strip_delims(s, md, ad) - if md and md~="" then s = s:gsub(escp(md),"") end - if ad and ad~="" then s = s:gsub(escp(ad),"") end - return s -end - --- 依据分组把 core 插入手动分隔符重建 -local function build_by_groups(core, ch_manual, groups) - if not groups or #groups==0 or sum(groups)~=#core then return core end - local out, i = {}, 1 - for gi,g in ipairs(groups) do - out[#out+1] = core:sub(i, i+g-1); i = i + g - if gi < #groups then out[#out+1] = ch_manual end - end - return table.concat(out) -end - --- 从字符串解析分段长度(空格或 ' 都视为可见分隔) -local function lens_from_string(s, md, ad) - if not s or s=="" then return nil end - local segs, buf = {}, {} - local function flush() if #buf>0 then segs[#segs+1]=table.concat(buf); buf={} end end - for i=1,#s do - local c=s:sub(i,i) - if c==md or c==ad or c==" " then - flush() - else - local b=string.byte(c) - if b and ((b>=65 and b<=90) or (b>=97 and b<=122)) then - buf[#buf+1]=string.char(b):lower() - end - end - end - flush() - if #segs==0 then return nil end - local L={}; for _,seg in ipairs(segs) do L[#L+1]=#seg end - return L -end - --- —— 缓存读取:优先用通知器缓存的 lens,其次现场计算 —— -local function get_cached_lens(env, ctx, md, ad) - local L = env._last_preedit_lens - if L and type(L)=="table" and #L>0 then return L end - local seg = ctx.composition:back() - local cand = seg and seg:get_selected_candidate() or nil - return lens_from_string(cand and cand.preedit or nil, md, ad) -end - --- ---------- patterns ---------- -local PATTERNS = { - [3] = { all = { {2,1}, {1,2} } }, - [4] = { all = { {2,2}, {1,3}, {3,1} } }, - [5] = { all = { {2,3}, {3,2} } }, - [6] = { all = { {2,2,2}, {3,3} } }, - [7] = { all = { {2,2,3}, {2,3,2}, {3,2,2} } }, - [8] = { all = { {2,2,2,2}, {2,3,3}, {3,2,3}, {3,3,2} } }, - [10] = { all = { {2,2,2,2,2} } }, - [12] = { all = { {2,2,2,2,2,2} } }, -} - --- ---------- session state ---------- -local function reset_session(env) - env._ss_core_letters = nil -- 纯编码(去分隔) - env._ss_start_idx = nil -- 起点索引(1..m),未命中则 0 - env._ss_N = nil - env._ss_baseline_head = nil -- 基线:包含你之前的手动分隔/空格 -end -local function ulen(s) - if not s or s == "" then return 0 end - if utf8 and utf8.len then - local ok, n = pcall(utf8.len, s) - if ok and n then return n end - end - -- 兜底:简单按 UTF-8 码点数 - local n = 0 - if utf8 and utf8.codes then - for _ in utf8.codes(s) do n = n + 1 end - return n - end - -- 再兜底:直接 #s(有误差,但总比没有好) - return #s -end -function M.init(env) - local cfg = env.engine.schema.config - local delimiter = cfg:get_string("speller/delimiter") or " '" - if #delimiter < 2 then delimiter = " '" end - env.auto_delim = delimiter:sub(1,1) -- 通常空格 - env.manual_delim = delimiter:sub(2,2) -- 通常单引号 - - -- 缓存最新一帧的可见分段与输入 - env._upd_conn = env.engine.context.update_notifier:connect(function(ctx) - local seg = ctx.composition:back() - local cand = seg and seg:get_selected_candidate() or nil - local pre = cand and cand.preedit or nil - env._last_preedit_lens = lens_from_string(pre, env.manual_delim, env.auto_delim) - env._last_input_head = ctx.input - env._last_input_for_caret = ctx.input - env._last_caret_pos = ctx.caret_pos - end) - - reset_session(env) -end - -function M.fini(env) - if env._upd_conn then env._upd_conn:disconnect(); env._upd_conn=nil end -end - --- ---------- main ---------- -function M.func(key_event, env) - if key_event:release() then return K_NOOP end - - local ctx = env.engine.context - if ctx.composition:empty() then return K_NOOP end - - local md = env.manual_delim or "'" - local ad = env.auto_delim or " " - - -- 只处理手动分隔符键 - if key_event.keycode ~= string.byte(md) then - reset_session(env); return K_NOOP - end - --用「上一帧」的光标位置判断是不是在中间编辑 - do - local last_input = env._last_input_for_caret or ctx.input or "" - local last_caret = env._last_caret_pos - - local total_len = ulen(last_input) - -- 只有「上一帧光标在末尾」我们才认定在玩超分段 - if not last_caret or last_caret ~= total_len then - -- 上一帧光标不在末尾:说明用户在中间编辑,这次 ' 交给默认逻辑 - reset_session(env) - return K_NOOP - end - end - -- 把这次 ' 并入输入,统计尾部 ' 数 - local before = ctx.input or "" - local after = before .. md - local tlen = count_trailing(after, md) - - -- 去掉末尾 ' 串,得到 head(本次按键前的完整输入)与 core(纯编码) - local head = strip_trailing(after, md) - local core = strip_delims(head, md, ad) - local N = #core - local conf = PATTERNS[N] - - -- 若核心/长度变化,重置会话 - if env._ss_core_letters ~= core or env._ss_N ~= N then - env._ss_core_letters = core - env._ss_N = N - env._ss_start_idx = nil - env._ss_baseline_head = nil - end - - -- 只要本轮还没记过,就立刻记录“基线 + 起点”(无论 tlen==1 还是 tlen>=2) - if env._ss_baseline_head == nil then - env._ss_baseline_head = head -- 保留你原有的空格或手动 ' - end - if conf and env._ss_start_idx == nil then - local start_idx = 0 - -- 先用缓存的可见分段;不行就直接用 head 切分(可避免“23 又走到 23'”的伪步骤) - local L = get_cached_lens(env, ctx, md, ad) - if not (L and sum(L)==N) then - L = lens_from_string(head, md, ad) - end - if L and sum(L)==N then - local idx = find_idx(conf.all, key_of(L)) - if idx then start_idx = idx end - end - env._ss_start_idx = start_idx - end - - -- 第 1 个 ' :仅记录,不重建 - if tlen == 1 then - ctx.input = after - return K_ACCEPT - end - - -- 第 2 个 ' 起:循环(若无该长度配置,直接接纳输入) - if not conf then - ctx.input = after - return K_ACCEPT - end - - local m = #conf.all - local k = tlen - 1 -- 从第二个 ' 开始计数 - - -- 恢复:回到第一拍记录的 baseline(保留空格/已有 '),尾部只留 1 个 ' - local function restore() - local baseline = env._ss_baseline_head or head - ctx.input = baseline .. md - reset_session(env) - env._ss_core_letters = core - env._ss_N = N - end - - if env._ss_start_idx and env._ss_start_idx ~= 0 then - -- 命中起点:只循环后续 m-1 个形态,跳过当前形态 - local variants_count = m - 1 - local cycle_len = variants_count + 1 - local r = k % cycle_len - if r == 0 then - restore(); return K_ACCEPT - else - local idx = ((env._ss_start_idx - 1 + r) % m) + 1 -- 跳过起点本身 - local groups = conf.all[idx] - local rebuilt = build_by_groups(core, md, groups) - ctx.input = rebuilt .. md:rep(tlen) - return K_ACCEPT - end - else - -- 未命中起点:从 all[1] 开始循环 m 个形态 - local variants_count = m - local cycle_len = variants_count + 1 - local r = k % cycle_len - if r == 0 then - restore(); return K_ACCEPT - else - local idx = ((r - 1) % m) + 1 - local groups = conf.all[idx] - local rebuilt = build_by_groups(core, md, groups) - ctx.input = rebuilt .. md:rep(tlen) - return K_ACCEPT - end - end -end - -return { init = M.init, fini = M.fini, func = M.func } diff --git a/lua/tone_fallback.lua b/lua/tone_fallback.lua deleted file mode 100644 index 380dbb0..0000000 --- a/lua/tone_fallback.lua +++ /dev/null @@ -1,140 +0,0 @@ --- 欢迎使用万象拼音方案 --- @amzxyz --- https://github.com/amzxyz/rime_wanxiang --- 用来在声调辅助的时候当你输入 2 个数字的时候自动将声调替换为第二个数字, --- 也就是说你发现输入错误声调你可以手动轮巡输入而不用回退删除直接输入下一个即可 --- 兼容小键盘输入中不回退 - -local wanxiang = require("wanxiang") - --- 将目标字符的连续段压缩为“最后一个字符” -local function compress_runs_keep_last(text) - local changed = false - local out = text:gsub('([7890])([7890]+)', function(_, tail) - changed = true - return tail:sub(-1) - end) - return out, changed -end - -local function should_ignore(ctx) - return wanxiang.is_function_mode_active(ctx) or ctx.input == "" -end - --- 小键盘 keycode 判定(0xFFB0 ~ 0xFFB9) -local function is_kp_digit_keycode(kc) - return kc >= 0xFFB0 and kc <= 0xFFB9 -end - ----@class Env ----@field tone_state "idle"|"skip"|"compress" ----@field tone_fallback_update_connection Connection|nil - -local P = {} - -function P.init(env) - env.tone_state = "idle" - local config = env.engine.schema.config - env.lookup_key = config:get_string('wanxiang_lookup/key') or '`' - local ctx = env.engine and env.engine.context - if not ctx or not ctx.update_notifier then return end - - env.tone_fallback_update_connection = ctx.update_notifier:connect(function(c) - if should_ignore(c) then - env.tone_state = "idle" - return - end - - -- 只在当前按键对应的一次更新里消费 state - local state = env.tone_state or "idle" - env.tone_state = "idle" -- 立刻消费,避免“下一次才生效” - - -- 小键盘数字:标记为 skip,本次直接不压缩 - if state == "skip" then - return - end - - -- 非压缩状态:直接不动 - if state ~= "compress" then - return - end - - -- 压缩逻辑 - local input = c.input - local caret = (c.caret_pos ~= nil) and c.caret_pos or #input - if caret < 0 then caret = 0 end - if caret > #input then caret = #input end - - -- 仅处理光标左侧;右侧保持不变 - local left = (caret > 0) and input:sub(1, caret) or "" - local right = (caret < #input) and input:sub(caret + 1) or "" - - local left_new, changed = compress_runs_keep_last(left) - if not changed then return end - - -- 只改左侧,避免干扰右侧;并精确设置 caret_pos - if caret > 0 then c:pop_input(caret) end - if #left_new > 0 then c:push_input(left_new) end - if c.caret_pos ~= nil then c.caret_pos = #left_new end - -- 右侧 right 不需处理,Rime 会保持不变 - end) -end - -function P.fini(env) - if env.tone_fallback_update_connection then - env.tone_fallback_update_connection:disconnect() - env.tone_fallback_update_connection = nil - end - env.tone_state = "idle" -end - ----@return ProcessResult -function P.func(key, env) - local ctx = env.engine.context - if should_ignore(ctx) then - env.tone_state = "idle" - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - -- 小键盘数字:标记这次按键为 skip,本轮 update_notifier 不压缩 - local kc = key.keycode - if is_kp_digit_keycode(kc) then - env.tone_state = "skip" - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - -- 主键盘数字 0–9:标记为 compress - local r = key:repr() or "" - if r:match("^[0-9]$") then - local input = ctx.input or "" - local caret = (ctx.caret_pos ~= nil) and ctx.caret_pos or #input - if caret < 0 then caret = 0 end - if caret > #input then caret = #input end - local left = (caret > 0) and input:sub(1, caret) or "" - - if left:find(env.lookup_key, 1, true) then - env.tone_state = "idle" - return wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - env.tone_state = "compress" - - -- 这里用“预测压缩是否会发生”来决定要不要告诉 Rime “我处理了这个按键” - local input = ctx.input or "" - local caret = (ctx.caret_pos ~= nil) and ctx.caret_pos or #input - if caret < 0 then caret = 0 end - if caret > #input then caret = #input end - - local left = (caret > 0) and input:sub(1, caret) or "" - local _, changed = compress_runs_keep_last(left) - - return changed and wanxiang.RIME_PROCESS_RESULTS.kAccepted - or wanxiang.RIME_PROCESS_RESULTS.kNoop - end - - -- 其它按键:不触发压缩,也不跳过 - env.tone_state = "idle" - return wanxiang.RIME_PROCESS_RESULTS.kNoop -end - -return P diff --git a/wanxiang.schema.yaml b/wanxiang.schema.yaml index 785e6c7..10aad15 100644 --- a/wanxiang.schema.yaml +++ b/wanxiang.schema.yaml @@ -49,16 +49,10 @@ switches: # 输入引擎 engine: processors: - - lua_processor@*select_character #以词定字,默认左中括号上屏一个词的前一个字,右中括号上屏一个词的后一个字 + - lua_processor@*super_processor #KP小键盘、字母选词、符号快打、超强分词、重复限制、退格限制、声调回退、以词定字 - lua_processor@*partial_commit #通过ctrl+1~0局部提交10个字以内的句子的前几个字(一般为正确的前几个)使用时要遵循合理的分词结构能促进后续编码打出正确的词汇 - - lua_processor@*letter_selector #在N模式R模式下输入数字被视为编码,那么如何上屏呢,现在除了方向键还提供qwertyuio对照1-9来选词 - - lua_processor@*quick_symbol_text #快符引导以及重复上屏,配合quick_symbol_text顶层配置清单定义扩展按键 - lua_processor@*super_tips #超级提示模块:表情、简码、翻译、化学式、等等靠你想象 - - lua_processor@*tone_fallback #声调辅助回退,当你输入声调数字错误时,继续输入正确的而不用回退删除 - lua_processor@*super_sequence*P #手动排序,高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+l 移除位移 ctrl+p 置顶 - - lua_processor@*limit_repeated #用于限制最大候选长度以及最大重复输入声母编码长度,避免性能异常 - - lua_processor@*backspace_limit #防止连续 Backspace 在编码为空时删除已上屏内容 - - lua_processor@*kp_number_processor #管理小键盘的处理逻辑,有输入中数字不上屏和数字一直不上屏设置可选 - ascii_composer #处理英文模式及中英文切换 - recognizer #与 matcher 搭配,处理符合特定规则的输入码,如网址、反查等 tags - key_binder #在特定条件下将按键绑定到其他按键,如重定义逗号、句号为候选翻页、开关快捷键等 @@ -100,7 +94,7 @@ engine: - lua_filter@*super_filter #comment前,相关功能见Lua文件 - lua_filter@*super_english #comment前,负责英文方案及中英混输中英文单词格式化,语句流,自动加空格等策略 - lua_filter@*super_comment_preedit #OpenCC前,超级注释模块、超级preedit,支持错词提示、辅助码显示,部件组字读音注释,有声调、无声调全拼编码的转换,支持个性化配置和关闭相应的功能,详情搜索super_comment_preedit进行详细配置 - - lua_filter@*super_replacer #用来替代OpenCC的处理器 + - lua_filter@*super_replacer #OpenCC替代器,更灵活的处理方式,更自由的自定义方式 - lua_filter@*super_sequence*F #手动排序,对高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+0 移除位移 - uniquifier # 去重 diff --git a/wanxiang_t9.schema.yaml b/wanxiang_t9.schema.yaml index f7972f0..6e9c172 100644 --- a/wanxiang_t9.schema.yaml +++ b/wanxiang_t9.schema.yaml @@ -48,10 +48,7 @@ engine: processors: - t9_processor #元书T9处理器 - lua_processor@*super_tips #超级提示模块:表情、简码、翻译、化学式、等等靠你想象 - - lua_processor@*partial_commit #通过ctrl+1~0局部提交10个字以内的句子的前几个字(一般为正确的前几个)使用时要遵循合理的分词结构能促进后续编码打出正确的词汇 - lua_processor@*super_sequence*P #手动排序,高亮候选 ctrl+j左移动 ctrl+k 右移动 ctrl+l 移除位移 ctrl+p 置顶 - - lua_processor@*limit_repeated #用于限制最大候选长度以及最大重复输入声母编码长度,避免性能异常 - - lua_processor@*backspace_limit #防止连续 Backspace 在编码为空时删除已上屏内容 - ascii_composer #处理英文模式及中英文切换 - recognizer #与 matcher 搭配,处理符合特定规则的输入码,如网址、反查等 tags - key_binder #在特定条件下将按键绑定到其他按键,如重定义逗号、句号为候选翻页、开关快捷键等