Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kinsoku.nvim

CJK-aware hard wrapping for Neovim. UAX #14 line breaking plus kinsoku shori (禁則処理), wired to gq through 'formatexpr'. Markdown and MDX aware. No runtime dependencies.

類比授權規則(analogy-warranting rule,
AWR):陳述已知共有特徵與被推出特徵之間
所成立的邏輯關係。

Do you actually want hard wrapping?

Worth thirty seconds before you install anything.

If your complaint is that CJK looks wrong on screen — a line that stops short and leaves a wide gap at the right margin — you do not need this plugin. You need one option:

:setlocal nolinebreak

'linebreak' breaks only at 'breakat' characters, and 'breakat' is ASCII-only. A run of Chinese or Japanese is therefore a single unbreakable token: when it does not fit in the space left on the line, Neovim moves the whole run down and leaves the gap you are looking at. With 'linebreak' off, Neovim wraps at the exact screen column instead — which is what CJK typesetting does anyway. The cost is that Latin words can be cut mid-word.

That is a display setting. Nothing on disk changes.

This plugin does the opposite: it writes the line breaks into your file. Reach for it once you have decided you want hard-wrapped source — for line-level git diff on prose, for a 72-column plain-text convention, for mail. Neovim cannot produce that for CJK at all: the built-in formatter breaks only at whitespace, so gq on a Chinese paragraph does nothing whatsoever.

Read this first

Hard-wrapped CJK Markdown renders a spurious space at every wrap point unless your publishing pipeline removes segment breaks between CJK characters. CommonMark turns a soft line break into \n, and HTML collapses that to a space — between two Han characters, that is a visible gap that was never in your source.

This is not fixable in the editor. CSS Text Level 3 made the segment break transformation rules UA-defined in 2020; Firefox removes the break, Chrome and Safari make no guarantee.

Fix your pipeline before you start hard wrapping:

Ecosystem Plugin
remark / Astro / Next.js MDX remark-join-cjk-lines
markdown-it / VitePress markdown-it-cjk-breaks
Gatsby gatsby-remark-remove-cjk-breaks

With that in place, wrapping is invisible in your published output — which is exactly the guarantee this plugin is built around.

Why hard wrap?

Because soft wrap cannot be fixed from a plugin — only traded off, which is what nolinebreak above does.

Neovim's 'linebreak' breaks only at characters in 'breakat', and the manual is blunt about the limit:

This option lets you choose which characters might cause a line break if 'linebreak' is on. Only works for ASCII characters.

Chinese and Japanese have no inter-word spaces, so a CJK paragraph is one enormous unbreakable token. Mix in a Latin fragment and it becomes the only break candidate, which is why you get wrap points in the middle of a clause.

The decision lives in Neovim's C rendering code and there is no callback for it — no 'breakatfunc', nothing. Upstream attempts have stalled: vim/vim#6598 was closed unmerged in November 2024 after four years, and neovim/neovim#13967 has been open since 2021. 'formatexpr' is the one hook Lua can reach, and it drives hard wrapping.

Install

lazy.nvim

{ "wayne930242/kinsoku.nvim", ft = { "markdown", "mdx" }, opts = {} }

mini.deps

add("wayne930242/kinsoku.nvim")
require("kinsoku").setup()

Then wrap with gqip, gqq, or gq over a visual selection, exactly as you already do. Set 'textwidth' to whatever width you like.

Configuration

setup() is optional. These are the defaults:

require("kinsoku").setup({
  filetypes = { "markdown", "mdx" },  -- where to install 'formatexpr'
  fallback_width = 80,                -- used only when 'textwidth' is 0
})

That is the whole surface. The plugin sets a buffer-local 'formatexpr' and touches nothing else — 'textwidth', 'formatoptions', 'linebreak', 'wrap' and 'breakat' stay exactly as you configured them. If you already have t in 'formatoptions', wrapping while you type works too, through the same hook.

Where it breaks lines

The rule is narrower than "wherever UAX #14 permits", on purpose:

A break is placed only where re-joining the two lines reproduces the original text exactly.

Two boundaries survive that round trip — after a run of spaces, and between two East Asian Wide characters. Everything else is refused, because a rejoin would insert a space your reader would see:

Boundary Break? Why
| yes both wide; rejoin adds nothing
rule, | AWR yes after a space; rejoin puts it back
規則 | AWR no rejoin renders 規則 AWR
analogy- | warranting no rejoin renders analogy- warranting
られる | ——根拠 no em dash is Ambiguous width, not Wide

On top of that, kinsoku shori applies. Closing punctuation 。、)」 never starts a line, opening brackets 「(【 never end one, and —— …… are never split. Resolution is by push-out (追い出し) — a monospace grid cannot compress spacing, so pulling content back (追い込み) is not available.

Emoji are safe: ZWJ sequences, skin-tone modifiers, variation selectors, combining marks and flag pairs are never cut apart, because Neovim does not expose grapheme cluster segmentation to Lua and naive breaking would shred them.

What it will not touch

Fenced and indented code, front matter, tables, headings, thematic breaks, MDX import/export statements, JSX blocks, and directive markers are left byte-identical. Inside a line, code spans, link destinations, $…$ math, inline JSX and directive attribute blocks are never broken. Anything the scanner cannot classify is protected rather than reflowed, so the failure mode is "did not wrap", never "corrupted your file".

Wrapping is idempotent: running gq twice produces the same bytes as running it once.

Non-goals

  • Fixing soft wrap. Out of reach from Lua; the honest fix is a Neovim core patch.
  • 盤古之白 — inserting spaces between CJK and Latin. A text transformation orthogonal to line breaking, and contentious among CJK writers.
  • Korean-specific handling. Korean has inter-word spaces and already wraps correctly.
  • Thai, Lao, Khmer, Myanmar. These need dictionary segmentation. They fall back to breaking at spaces — the same as Neovim does today, so nothing regresses.

License

MIT

About

CJK-aware hard wrapping for Neovim. UAX #14 line breaking + kinsoku shori (禁則処理) via 'formatexpr'. Markdown/MDX aware. Zero dependencies.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages