Skip to content

feat(char): CJK-aware f/t/F/T through flash.nvim char mode - #21

Open
fang2hou wants to merge 4 commits into
mainfrom
worktree/rapid-cloud-3f8e
Open

feat(char): CJK-aware f/t/F/T through flash.nvim char mode#21
fang2hou wants to merge 4 commits into
mainfrom
worktree/rapid-cloud-3f8e

Conversation

@fang2hou

@fang2hou fang2hou commented Aug 28, 2026

Copy link
Copy Markdown
Owner

Purpose

flash.nvim ships enhanced f/t/F/T motions (modes.char, on by default) and a //? label overlay (modes.search, off by default). This PR makes both flash-owned surfaces CJK-aware through one config group:

  • motions = { char = true }fv jumps to (Xiaohe-style v initial for zhong), ft to (kunrei-shiki tyuu) or (pinyin ti). Single-character matching; ;/,, counts, operator-pending stay native.
  • motions = { search = true } — with flash's modes.search.enabled = true, typing pinyin/romaji in //? puts flash-cjk's predictive labels on CJK matches; pressing a label char jumps (flash's own check_jump). <cr>/n/N keep native vim semantics.

User-owned triggers stay wrapper-style (flash-cjk.jump()/remote() to bind yourself) and share the same matching config (languages/priority/mixed_input); motions groups the flash-owned triggers. Both integrations follow the same mechanism: idempotent, pcall-safe wraps installed at setup()/build_opts().

Impact

  • lua/flash-cjk/patches.luachar_mode_patch() wraps flash.plugins.char.mode (per-motion pattern builder; t=\m.\ze<cjk>, T=\m<cjk>\zs., f/F bare; multi_line=false line anchor preserved). search_mode_patch() wraps flash.plugins.search.start: post-construction swap of state.pattern.mode to a mix compiler and state.labeler to flash-cjk's predictive labeler (multi-char queries need next-letter prediction — a typed continuation letter must never collide with an assigned label).
  • Native vim regex contract preserved on the / path: queries containing metacharacters (magic set \ . * [ ] ^ $ ~ plus the / delimiter) or non-ASCII bytes pass through verbatim — /.*, /^func untouched; flash's operator-pending \%<line>l\%<col>c. cmdline rewrite relies on the same passthrough. Trade-off: punctuation CJK classes in / cover only non-meta keys (, ; : ' "); the s-jump keeps the full set.
  • lua/flash-cjk/config.lua / init.luamotions = { char = true, search = true }; normalize_motions validates known flags as booleans and drops unknown fields (forward compatibility); non-booleans error ("false" is truthy in Lua), a rejected setup leaves config untouched.
  • tests/run.lua — char + search sections: pattern forms, passthrough set, idempotence, gates, validation, predictive-labeler collision check, real Char.jump flow.
  • tests/e2e/repro.lua + scenario.lua — 9 char checks and 13 search checks through the REAL flows (char: prefed Char.jump; search: a coroutine-driven cmdline harness feeding real //? keys through CmdlineEnter/Changed/check_jump, including a real label-char jump landing the cursor on 梯).
  • README (en/zh/ja/ko) + ARCHITECTURE.md — motions group docs, requirements, trade-offs.

Not touched: char max_length=1, char labeler/actions, the Rust matcher path (both wraps are vim-regex Search; parity enforced by the existing e2e phase diff).

Context

Verified reading map against the repo data: v (flypy zh→v singlepin) and t (ja tyuu); t (zhcn ti); n/z (ja). Both wraps were prototyped against real flash.nvim before implementation. Known inherited constraints, documented in the READMEs: char matching is single-character only; flash's clever-f repeat means a repeated motion letter cycles matches; / label matching needs flash's modes.search.enabled.

Risks

  • Behavior change for users with flash char mode enabled (default on): single letters also match CJK readings — opt out via motions = { char = false }.
  • The wraps target flash.plugins.char.mode / flash.plugins.search.start; upstream renames no-op via pcall and native behavior is preserved.
  • The / overlay intentionally does not rewrite the search register: after a plain <cr>, n/N search literally. Labels are the CJK carrier.

Testing

  • mise run check — pass
  • mise run test — pass (lua suite 221/0 — 53 new assertions across char+search; cargo 43; rust↔vim cross-validation)
  • mise run e2eE2E PASSED: 9 char + 13 search checks green in both the rust and vim-regex phases, parity identical, server lifecycle clean.

Wrap flash.plugins.char.mode idempotently: the single typed character
is compiled through the mix mode, so pinyin/romaji/romanization first
letters and CJK punctuation classes match CJK characters in flash's
enhanced char motions. t/T keep native before/after landing positions
and the multi_line line anchor; max_length, labeler, actions and the
Rust matcher path stay untouched (char mode is vim-regex only).
Opt out with setup({ char = false }).
…char

The char integration flag moves from a top-level boolean into the
motions group -- the config surface for integrations on entry points
flash.nvim itself owns (modes.char today; flash-owned surfaces such as
the / search mode land here later). Matching-semantic options stay
top-level. Non-boolean flags are rejected ("false" is truthy in Lua
and would silently enable the integration); a rejected setup leaves
the config untouched.
Deep-extending arbitrary user fields into config.motions persisted
unknown keys (typos look accepted) and left no room for future
table-shaped motion surfaces. normalize_motions mirrors the languages
precedent instead: known flags are validated booleans, unknown fields
are dropped (forward compatibility) -- only normalized values merge.
@fang2hou
fang2hou force-pushed the worktree/rapid-cloud-3f8e branch from 898c7df to 937a6ce Compare August 28, 2026 08:56
Wrap flash.plugins.search.start idempotently: after the native state
is created, the pattern mode is swapped for a mix-mode compiler and
the default labeler for flash-cjk's predictive one (multi-char queries
need next-letter prediction so a continuation letter never collides
with an assigned label -- flash's check_jump treats the extending char
as a label press).

Native vim regex keeps its contract: queries containing metacharacters
(the magic set plus the search delimiter) or non-ASCII bytes pass
through verbatim, so /.* and /^func are untouched; flash's
operator-pending position-pattern rewrite relies on the same
passthrough. <cr>, n and N stay native -- labels are the CJK carrier.
Requires flash's own modes.search.enabled; opt out with
setup({ motions = { search = false } }).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant