Persian/Arabic text is stored and edited in logical order in your buffer (normal, editable, searchable — nothing about the file changes). The problem is purely display: terminals lay out codepoints left-to-right in fixed cells, so Persian letters show up disconnected and in the wrong order.
rtl.nvim draws a corrected overlay on top of Persian text using Neovim extmarks:
- Shaping — picks the contextually correct glyph form (isolated / initial / medial / final) for each letter, using the standard Arabic joining rules and the Unicode Presentation Forms blocks.
- Reordering — reverses word/letter order within each detected Persian clause so it reads right-to-left, while embedded numbers keep their own left-to-right digit order and any English text is left untouched. Half-space (ZWNJ / U+200C) is handled transparently — letters on each side are shaped independently.
Your buffer content is never modified — only what's drawn on screen.
- Neovim ≥ 0.9 (uses
virt_text_pos = "overlay"extmarks) - A terminal font that includes glyphs for the Arabic Presentation Forms-A/B
Unicode blocks (
U+FB50–FDFF,U+FE70–FEFF) — e.g. Vazirmatn, Noto Naskh Arabic, Amiri. If your font is Latin-only, the overlay will show blank boxes/tofu even though the logic is correct — check this first.
{
"mavomen/rtl.nvim",
config = function()
require("rtl").setup({
auto_filetypes = { "markdown", "text" }, -- optional auto-enable
})
end,
}:RtlEnable " turn on overlay for current buffer
:RtlDisable " turn off
:RtlToggle " toggleOr from Lua: require("rtl").enable(), .disable(), .toggle(), .is_enabled().
:help rtl for full documentation.
require("rtl").setup({
auto_filetypes = { "markdown", "text" }, -- buffers to auto-enable on; {} = manual only
highlight = { fg = "#89b4fa" }, -- default: { link = "String" }; any nvim_set_hl() opts
})- Not a full UAX#9 bidi implementation. Reordering is done per detected "Persian clause" (a run of Persian letters/spaces/digits bounded by non-Arabic-script content). This handles the common case — a Persian sentence, possibly with embedded English words or numbers — correctly, but doesn't implement the full recursive embedding-level algorithm real bidi text needs for deeply nested mixed-direction text.
- No ligatures (e.g. lam-alef لا isn't merged into a single ligature glyph) — kept intentionally 1-to-1 so overlay column alignment stays simple and exact.
- No diacritics (tashkeel) handling yet — combining marks aren't currently treated as transparent for joining purposes.
- Overlay alignment assumes your font renders presentation-form glyphs at the same cell width as the base letters (true for most Arabic-support monospace fonts, but worth a visual check).
- Rendering is debounced at 40ms. Only the visible window is rendered for performance — scrolling or editing triggers a re-render of the newly visible lines.
Tests cover the shaping, bidi, and joining modules:
lua tests/run.luaNo external dependencies — runs on plain Lua 5.x.