Skip to content

Commit 9e3f4ff

Browse files
delphinusclaude
andcommitted
feat(fold): toggle folds and expandable regions with za / <CR> (#21)
Folds (callouts/details) and expandable regions (truncated code blocks and tables) could previously only be toggled by clicking. Bind `za` and `<CR>` in the preview buffer to toggle the block under the cursor, so the preview is usable without a mouse. - `za` toggles the fold/region under the cursor (no-op otherwise). - `<CR>` toggles the block under the cursor, otherwise closes the window when it is a close key (float/tab mode), and is a no-op in toggle mode. - Extract side-effect-free `fold_at` / `region_at` finders shared by the mouse and keyboard handlers; mouse behavior (incl. URL precedence) is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4af5c3e commit 9e3f4ff

6 files changed

Lines changed: 299 additions & 41 deletions

File tree

README.ja.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ Neovim 用の Markdown レンダリングエンジンです。生の Markdown
1212

1313
- **リッチなインライン書式** — 太字、取り消し線、インラインコード、リンク、Obsidian `==highlight==` をその場でレンダリング
1414
- **テーブル** — 罫線文字による描画、列アラインメント、比例サイズ調整、セル内インライン書式
15-
- **コールアウト & 折りたたみ** — GitHub / Obsidian のアラートタイプに対応。色付きボーダー・アイコン・クリックで折りたたみ切り替え
16-
- **コードブロック** — treesitter シンタックスハイライト付きフェンスコードブロック。省略時はクリックで展開
15+
- **コールアウト & 折りたたみ** — GitHub / Obsidian のアラートタイプに対応。色付きボーダー・アイコン・クリックまたは `za` / `<CR>` で折りたたみ切り替え
16+
- **コードブロック** — treesitter シンタックスハイライト付きフェンスコードブロック。省略時はクリックまたは `za` / `<CR>` で展開
1717
- **画像** — ローカルおよび Web 画像(PNG, JPEG, WebP, GIF, アニメーション GIF)をターミナルグラフィクスプロトコルでインライン表示
1818
- **動画** — ローカルおよび Web 動画(MP4, WebM, MOV, AVI, MKV, M4V)をアニメーションフレームとしてインライン再生
1919
- **Mermaid ダイアグラム** — 画像としてインライン表示
2020
- **CJK 対応ワードラップ** — JIS X 4051 禁則処理 + [BudouX](https://github.com/google/budoux)[budoux.lua](https://github.com/delphinus/budoux.lua) 経由)によるオプションのフレーズ分割
2121
- **クリック可能リンク** — マウスクリックで URL を開く。対応ターミナルでは OSC 8 ハイパーリンク
22-
- **`<details>` 対応**クリックで折りたたみ可能なセクション`open` 属性にも対応
22+
- **`<details>` 対応**クリックまたは `za` / `<CR>` で折りたたみ可能なセクション`open` 属性にも対応
2323
- **ライブラリ API** — レンダリングエンジンを自作プラグインからプログラム的に利用可能
2424

2525
<figure align="center">
@@ -144,6 +144,17 @@ vim.keymap.set("n", "<leader>md", "<Plug>(md-render-demo)", { desc = "Mar
144144
| `<Plug>(md-render-split)` | ウィンドウを分割してソースとレンダリング表示を並べる |
145145
| `<Plug>(md-render-demo)` | 対応する全 Markdown 記法のデモウィンドウを表示 |
146146

147+
### プレビュー内のキー
148+
149+
レンダリングされたプレビュー(フローティング・タブ・その場トグル)内では、以下のバッファローカルなキーが自動で設定されます:
150+
151+
| キー | 動作 |
152+
|---|---|
153+
| `za` | カーソル行の折りたたみ / 展開ブロックを切り替える(該当しない行では何もしない) |
154+
| `<CR>` | カーソル行の折りたたみ / 展開ブロックを切り替える。該当しなければウィンドウを閉じる(フローティング / タブモードのみ) |
155+
| `<LeftMouse>` | クリックで折りたたみ・展開を切り替え、リンクを開く |
156+
| `q` / `<Esc>` | ウィンドウを閉じる(フローティング / タブモードのみ) |
157+
147158
## コマンド
148159

149160
このプラグインが公開するユーザコマンドは `:MdRender` ひとつで、サブコマンドで動作モードを切り替えます。
@@ -178,7 +189,7 @@ vim.keymap.set("n", "<leader>md", "<Plug>(md-render-demo)", { desc = "Mar
178189
- 同じソースが複数のウィンドウで表示されている場合、切替はそのウィンドウだけに作用し、他のウィンドウの編集は次に当該ウィンドウをレンダーモードにした時点で反映されます。
179190
- カーソル位置は source line マップを介してソース ↔ レンダー間を往復します。
180191
- レンダーモードのウィンドウでは `number` / `relativenumber` / `list` が無効化されます。元の値はウィンドウに保存され、ソースに戻したときに復元されます。
181-
- レンダーモード中、`q` / `<Esc>` / `<CR>` は閉じる動作に**割り当てられません**。ソースに戻すには再度 `:MdRender toggle` を呼びます。`<LeftMouse>` は折りたたみ・展開・リンク open を引き続き処理します
192+
- レンダーモード中、`q` / `<Esc>` / `<CR>` は閉じる動作に**割り当てられません**。ソースに戻すには再度 `:MdRender toggle` を呼びます。`<LeftMouse>` / `za` / `<CR>` は折りたたみ・展開を引き続き切り替えます(リンク open `<LeftMouse>`)
182193

183194
### Insert モード連動の自動トグル(実験的)
184195

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ A Markdown rendering engine for Neovim. Transforms raw Markdown into richly high
1212

1313
- **Rich inline formatting** — bold, strikethrough, inline code, links, Obsidian `==highlight==`, all rendered in-place
1414
- **Tables** — box-drawing borders, column alignment, proportional sizing, and inline formatting within cells
15-
- **Callouts & folds** — GitHub and Obsidian alert types with colored borders, icons, and click-to-toggle folding
16-
- **Code blocks** — fenced blocks with treesitter syntax highlighting; expandable when truncated
15+
- **Callouts & folds** — GitHub and Obsidian alert types with colored borders, icons, and folding you can toggle by clicking or with `za` / `<CR>`
16+
- **Code blocks** — fenced blocks with treesitter syntax highlighting; expandable when truncated (click or `za` / `<CR>`)
1717
- **Images** — local and web images (PNG, JPEG, WebP, GIF, animated GIF) displayed inline via terminal graphics protocol
1818
- **Video** — local and web video (MP4, WebM, MOV, AVI, MKV, M4V) played as animated frames inline
1919
- **Mermaid diagrams** — rendered as images inline
2020
- **CJK-aware word wrapping** — JIS X 4051 kinsoku shori + optional [BudouX](https://github.com/google/budoux) phrase segmentation via [budoux.lua](https://github.com/delphinus/budoux.lua)
2121
- **Clickable links** — mouse click to open URLs; hover the mouse over a link to peek the full URL in a subtle floating window; OSC 8 hyperlink support for compatible terminals
22-
- **`<details>` support** — collapsible sections with click-to-toggle, respecting the `open` attribute
22+
- **`<details>` support** — collapsible sections you can toggle by clicking or with `za` / `<CR>`, respecting the `open` attribute
2323
- **Library API** — use the rendering engine programmatically from your own plugins
2424

2525
<figure align="center">
@@ -144,6 +144,17 @@ vim.keymap.set("n", "<leader>md", "<Plug>(md-render-demo)", { desc = "Mar
144144
| `<Plug>(md-render-split)` | Open a split showing source and rendered Markdown |
145145
| `<Plug>(md-render-demo)` | Show a demo window with all supported Markdown notations |
146146

147+
### In-preview keys
148+
149+
Inside a rendered preview (floating, tab, or in-place toggle), these buffer-local keys are set automatically:
150+
151+
| Key | Action |
152+
|---|---|
153+
| `za` | Toggle the fold / expandable region under the cursor (no-op elsewhere) |
154+
| `<CR>` | Toggle the fold / expandable region under the cursor; otherwise close the window (in floating / tab mode) |
155+
| `<LeftMouse>` | Toggle folds, expand regions, and open links by clicking |
156+
| `q` / `<Esc>` | Close the window (floating / tab mode only) |
157+
147158
## Commands
148159

149160
The plugin exposes a single `:MdRender` command with subcommands:
@@ -178,7 +189,7 @@ Behavior:
178189
- When the same source is shown in multiple windows, only the invoking window swaps; edits from other windows are reflected on the next toggle into render mode.
179190
- Cursor position round-trips between source and render via the source-line mapping.
180191
- `number`, `relativenumber`, and `list` are turned off on render-mode windows. The originals are stashed on the window and restored when toggling back to source.
181-
- Inside render mode, `q` / `<Esc>` / `<CR>` are **not** bound to close — call `:MdRender toggle` again to return to source mode. `<LeftMouse>` still toggles folds, expands regions, and opens links.
192+
- Inside render mode, `q` / `<Esc>` / `<CR>` are **not** bound to close — call `:MdRender toggle` again to return to source mode. `<LeftMouse>`, `za`, and `<CR>` still toggle folds and expand regions (and `<LeftMouse>` opens links).
182193

183194
### Auto-toggle on Insert mode (experimental)
184195

doc/md-render.jax

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ md-render.nvim は Neovim 用の Markdown レンダリングエンジンです
3939
`==highlight==`
4040
- テーブル(罫線文字による描画、列アラインメント、比例サイズ調整)
4141
- コールアウト & 折りたたみ(GitHub / Obsidian のアラートタイプに対応。色付き
42-
ボーダー・アイコン・クリック折りたたみ切り替え
43-
- コードブロック(treesitter シンタックスハイライト付き。省略時はクリックで展
44-
42+
ボーダー・アイコン・クリックまたは `za` / `<CR>` で折りたたみ切り替え
43+
- コードブロック(treesitter シンタックスハイライト付き。省略時はクリックまた
44+
`za` / `<CR>` で展開
4545
- 画像(PNG, JPEG, WebP, GIF, アニメーション GIF)をターミナルグラフィクスプロ
4646
トコルでインライン表示
4747
- 動画(MP4, WebM, MOV, AVI, MKV, M4V)をアニメーションフレームとしてインラ
@@ -51,7 +51,7 @@ md-render.nvim は Neovim 用の Markdown レンダリングエンジンです
5151
割)
5252
- クリック可能リンク(マウスクリックで URL を開く。マウスをリンクに乗せると
5353
完全な URL を控え目なフロートウィンドウに表示。OSC 8 ハイパーリンク対応)
54-
- `<details>` 対応(クリックで折りたたみ可能なセクション
54+
- `<details>` 対応(クリックまたは `za` / `<CR>` で折りたたみ可能なセクション
5555
- ライブラリ API(自作プラグインからプログラム的に利用可能)
5656

5757
==============================================================================
@@ -139,7 +139,10 @@ lazy.nvim の場合: >lua
139139
プレビューウィンドウ内では以下のキーが使えます:
140140

141141
`q` / `<Esc>` プレビューウィンドウを閉じる
142-
`<CR>` コールアウトの折りたたみ / 省略領域の展開
142+
`za` カーソル行のコールアウトの折りたたみ / 省略領域の
143+
展開を切り替える (該当しない行では何もしない)
144+
`<CR>` カーソル行のコールアウトの折りたたみ / 省略領域の
145+
展開を切り替える。該当しなければウィンドウを閉じる
143146
`<LeftMouse>` リンクのクリック、折りたたみ、領域展開
144147
`<MouseMove>` マウスをリンクに乗せると、エディタ下部の小さな
145148
フロートウィンドウに完全な URL を表示。
@@ -148,8 +151,9 @@ lazy.nvim の場合: >lua
148151

149152
|:MdRender-toggle| で開かれたレンダーモードのバッファでは `q` / `<Esc>` /
150153
`<CR>` は閉じる動作に**割り当てられません**。ソースに戻すには再度
151-
|:MdRender-toggle| を呼びます。`<LeftMouse>` は折りたたみ・展開・リンクを
152-
引き続き処理し、`<MouseMove>` の URL 表示も有効なままです。
154+
|:MdRender-toggle| を呼びます。`<LeftMouse>` / `za` / `<CR>` は折りたたみ・
155+
展開を引き続き切り替え (リンク open は `<LeftMouse>`)、`<MouseMove>` の URL
156+
表示も有効なままです。
153157

154158
==============================================================================
155159
コマンド *md-render-commands*
@@ -253,8 +257,8 @@ lazy.nvim の場合: >lua
253257
レンダーバッファ上で効くもの: ~
254258

255259
読み取り系操作: 移動 (hjkl, gg, G, ...)、検索 (/, ?)、yank (y)、
256-
ビジュアル選択、マーク、<LeftMouse>(折りたたみ・展開・リンク
257-
open)。
260+
ビジュアル選択、マーク、`za` / `<CR>`(折りたたみ・展開の切り替え)、
261+
<LeftMouse>(折りたたみ・展開・リンク open)。
258262

259263
`:w` / `:w!` は source バッファへ転送されるため、render モード
260264
を抜けずに保存できます。`:w other.md` や `:saveas` のような別名

doc/md-render.txt

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ Features: ~
3838
Obsidian`==highlight==`)
3939
- Tables with box-drawing borders, column alignment, and proportional sizing
4040
- Callouts and folds (GitHub / Obsidian alert types with colored borders,
41-
icons, and click-to-toggle folding)
41+
icons, and folding you can toggle by click or `za` / `<CR>`)
4242
- Code blocks with treesitter syntax highlighting; expandable when truncated
43+
(click or `za` / `<CR>`)
4344
- Images (PNG, JPEG, WebP, GIF, animated GIF) displayed inline via terminal
4445
graphics protocol
4546
- Video (MP4, WebM, MOV, AVI, MKV, M4V) played as animated frames inline
@@ -48,7 +49,7 @@ Features: ~
4849
segmentation)
4950
- Clickable links (mouse click to open URLs; mouse hover peeks the full URL
5051
in a subtle floating window; OSC 8 hyperlink support)
51-
- `<details>` support (collapsible sections with click-to-toggle)
52+
- `<details>` support (collapsible sections; toggle by click or `za` / `<CR>`)
5253
- Library API for programmatic use from other plugins
5354

5455
==============================================================================
@@ -138,7 +139,10 @@ Map them yourself: >lua
138139
Inside the preview window, the following keys are available:
139140

140141
`q` / `<Esc>` Close the preview window
141-
`<CR>` Toggle callout fold / expand truncated region
142+
`za` Toggle the callout fold / expandable region under the
143+
cursor (no-op when the cursor is not on one)
144+
`<CR>` Toggle the callout fold / expandable region under the
145+
cursor; otherwise close the preview window
142146
`<LeftMouse>` Click links, toggle folds, expand regions
143147
`<MouseMove>` Hover a link to peek its full URL in a small floating
144148
window at the bottom of the editor. Requires
@@ -147,8 +151,9 @@ Inside the preview window, the following keys are available:
147151

148152
Inside a render-mode buffer opened with |:MdRender-toggle|, `q` / `<Esc>` /
149153
`<CR>` are NOT bound to close — call |:MdRender-toggle| again to return to
150-
source mode. `<LeftMouse>` still toggles folds, expands regions, and
151-
opens links; `<MouseMove>` hover-peek also stays active.
154+
source mode. `<LeftMouse>`, `za`, and `<CR>` still toggle folds and expand
155+
regions (and `<LeftMouse>` opens links); `<MouseMove>` hover-peek also stays
156+
active.
152157

153158
==============================================================================
154159
COMMANDS *md-render-commands*
@@ -250,8 +255,8 @@ still registered as deprecated aliases — see |md-render-deprecated-commands|.
250255
What works on the render buffer: ~
251256

252257
Read-only operations: motion (hjkl, gg, G, ...), search (/, ?),
253-
yank (y), visual selection, marks, and <LeftMouse> (folds,
254-
expand, link open).
258+
yank (y), visual selection, marks, `za` / `<CR>` (toggle folds
259+
and expand regions), and <LeftMouse> (folds, expand, link open).
255260

256261
`:w` and `:w!` are forwarded to the source buffer so you can
257262
save without leaving render mode. Different filenames (e.g.

lua/md-render/display_utils.lua

Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -317,12 +317,70 @@ function M.setup_float_keymaps(buf, ns, win, content, close_handle, opts)
317317
end
318318

319319
local close_keys = opts.close_keys or { "q", "<Esc>", "<CR>" }
320+
local cr_is_close = vim.tbl_contains(close_keys, "<CR>")
320321
for _, key in ipairs(close_keys) do
321-
vim.api.nvim_buf_set_keymap(buf, "n", key, ":close<CR>", { noremap = true, silent = true })
322+
-- <CR> is bound below so it can toggle a block under the cursor first and
323+
-- fall back to closing only when the cursor is not on a foldable region.
324+
if key ~= "<CR>" then
325+
vim.api.nvim_buf_set_keymap(buf, "n", key, ":close<CR>", { noremap = true, silent = true })
326+
end
322327
end
323328

324329
UrlHover.attach(buf, ns, win)
325330

331+
-- Return the foldable callout header at the given 0-indexed rendered line, or nil.
332+
---@param line integer
333+
---@return MdRender.CalloutFold?
334+
local function fold_at(line)
335+
local cur_content = get_content()
336+
if on_fold_toggle and cur_content.callout_folds then
337+
for _, fold in ipairs(cur_content.callout_folds) do
338+
if fold.header_line == line then return fold end
339+
end
340+
end
341+
end
342+
343+
-- Return the expandable region containing the given 0-indexed rendered line, or nil.
344+
---@param line integer
345+
---@return MdRender.ExpandableRegion?
346+
local function region_at(line)
347+
local cur_content = get_content()
348+
if on_expand_toggle and cur_content.expandable_regions then
349+
for _, region in ipairs(cur_content.expandable_regions) do
350+
if line >= region.start_line and line <= region.end_line then return region end
351+
end
352+
end
353+
end
354+
355+
-- Toggle the foldable callout or expandable region under the cursor.
356+
-- Returns true if something was toggled.
357+
---@return boolean
358+
local function toggle_at_cursor()
359+
local line = vim.api.nvim_win_get_cursor(0)[1] - 1
360+
local fold = fold_at(line)
361+
if fold then
362+
on_fold_toggle(fold.source_line, not fold.collapsed)
363+
return true
364+
end
365+
local region = region_at(line)
366+
if region then
367+
on_expand_toggle(region.block_id, not region.expanded)
368+
return true
369+
end
370+
return false
371+
end
372+
373+
-- `za` toggles the block under the cursor (no-op when not on one). Overriding
374+
-- it buffer-locally also suppresses Vim's default "E490: No fold found".
375+
vim.keymap.set("n", "za", toggle_at_cursor, { buffer = buf, noremap = true, silent = true })
376+
377+
-- `<CR>` toggles the block under the cursor; otherwise it closes the window
378+
-- when it is a close key (float/tab mode), and is a no-op in toggle mode.
379+
vim.keymap.set("n", "<CR>", function()
380+
if toggle_at_cursor() then return end
381+
if cr_is_close then vim.cmd.close() end
382+
end, { buffer = buf, noremap = true, silent = true })
383+
326384
vim.keymap.set("n", "<LeftRelease>", function()
327385
local mouse = vim.fn.getmousepos()
328386
if mouse.winid == win then
@@ -383,25 +441,19 @@ function M.setup_float_keymaps(buf, ns, win, content, close_handle, opts)
383441
end
384442

385443
-- Check for foldable callout header click
386-
if on_fold_toggle and cur_content.callout_folds then
387-
for _, fold in ipairs(cur_content.callout_folds) do
388-
if fold.header_line == click_line then
389-
on_fold_toggle(fold.source_line, not fold.collapsed)
390-
return
391-
end
392-
end
444+
local fold = fold_at(click_line)
445+
if fold then
446+
on_fold_toggle(fold.source_line, not fold.collapsed)
447+
return
393448
end
394449

395450
-- Check for expandable region click (code blocks / tables)
396-
if on_expand_toggle and cur_content.expandable_regions then
397-
for _, region in ipairs(cur_content.expandable_regions) do
398-
if click_line >= region.start_line and click_line <= region.end_line then
399-
-- If click is on a URL, open it instead of toggling expansion
400-
if try_open_url() then return end
401-
on_expand_toggle(region.block_id, not region.expanded)
402-
return
403-
end
404-
end
451+
local region = region_at(click_line)
452+
if region then
453+
-- If click is on a URL, open it instead of toggling expansion
454+
if try_open_url() then return end
455+
on_expand_toggle(region.block_id, not region.expanded)
456+
return
405457
end
406458

407459
-- In OSC 8 terminals, the terminal handles link clicks natively.

0 commit comments

Comments
 (0)