feat(docs): table rendering

Render tables in both `:help` and generated HTML.

Use NBSP (nonbreaking space) char in empty table cells.
This commit is contained in:
Justin M. Keyes committed 2026-10-15 17:43:50 +02:00
1 parent 2accbdfd42
commit 6e4ab11798
6 files changed
+161 -69

No files matched your search

+15 -18
View File
@@ -109,18 +109,17 @@ The Nvim C API defines custom types for all function parameters. Some are just
typedefs around C99 standard types, others are Nvim-defined data structures.
Basic types ~
>
API Type C type
------------------------------------------------------------------------
API Type C type ~
Nil
Boolean bool
Integer (signed 64-bit integer) int64_t
Float (IEEE 754 double precision) double
String {char* data, size_t size} struct
Array kvec
Dict (msgpack: map) kvec
Object any of the above
<
Boolean bool
Integer (signed 64-bit integer) int64_t
Float (IEEE 754 double precision) double
String {char* data, size_t size} struct
Array kvec
Dict (msgpack: map) kvec
Object any of the above
Note:
- Empty Array is accepted as a valid Dictionary parameter.
- Functions cannot cross RPC boundaries. But API functions (e.g.
@@ -136,13 +135,11 @@ Special types (msgpack EXT) ~
The EXT object data is the (integer) object handle. The EXT type codes given
in the |api-metadata| `types` key are stable: they will not change and are
thus forward-compatible.
>
EXT Type C type Data
------------------------------------------------------------------------
Buffer enum value kObjectTypeBuffer |bufnr()|
Window enum value kObjectTypeWindow |window-ID|
Tabpage enum value kObjectTypeTabpage internal handle
<
EXT Type C type Data ~
Buffer enum value kObjectTypeBuffer |bufnr()|
Window enum value kObjectTypeWindow |window-ID|
Tabpage enum value kObjectTypeTabpage internal handle
*api-indexing*
Most of the API uses 0-based indices, and ranges are end-exclusive. For the
+45 -46
View File
@@ -29,7 +29,7 @@ Tag Char Insert-mode action ~
|i_CTRL-D| CTRL-D delete one shiftwidth of indent in the current
line
|i_CTRL-E| CTRL-E insert the character which is below the cursor
CTRL-F not used (but by default it's in 'cinkeys' to
  CTRL-F not used (but by default it's in 'cinkeys' to
re-indent the current line)
|i_CTRL-G_j| CTRL-G CTRL-J line down, to column where inserting started
|i_CTRL-G_j| CTRL-G j line down, to column where inserting started
@@ -71,7 +71,7 @@ Tag Char Insert-mode action ~
|i_CTRL-R_CTRL-P| CTRL-R CTRL-P {register}
insert the contents of a register literally
and fix indent.
CTRL-S not used or used for terminal control flow
  CTRL-S not used or used for terminal control flow
|i_CTRL-T| CTRL-T insert one shiftwidth of indent in current
line
|i_CTRL-U| CTRL-U delete all entered characters in the current
@@ -88,24 +88,18 @@ Tag Char Insert-mode action ~
|i_CTRL-[| CTRL-[ same as <Esc>
|i_CTRL-\_CTRL-N| CTRL-\ CTRL-N go to Normal mode
|i_CTRL-\_CTRL-G| CTRL-\ CTRL-G go to Normal mode
CTRL-\ a - z reserved for extensions
CTRL-\ others not used
  CTRL-\ a - z reserved for extensions
  CTRL-\ others not used
|i_CTRL-]| CTRL-] trigger abbreviation
|i_CTRL-^| CTRL-^ toggle use of |:lmap| mappings
|i_CTRL-_| CTRL-_ When 'allowrevins' set: toggle 'revins'
<Space> to '~' not used, except '0' and '^' followed by
  <Space> to '~' not used, except '0' and '^' followed by
CTRL-D
|i_0_CTRL-D| 0 CTRL-D delete all indent in the current line
|i_^_CTRL-D| ^ CTRL-D delete all indent in the current line, restore
it in the next line
|i_<Del>| <Del> delete character under the cursor
Meta characters (0x80 to 0xff, 128 to 255)
not used
  Meta characters (0x80 to 0xff, 128 to 255) not used
|i_<Left>| <Left> cursor one character left
|i_<S-Left>| <S-Left> cursor one word left
|i_<C-Left>| <C-Left> cursor one word left
@@ -137,6 +131,8 @@ Tag Char Insert-mode action ~
commands in CTRL-X submode *i_CTRL-X_index*
Tag Char CTRL-X submode action ~
------------------------------------------------------------------------------ ~
|i_CTRL-X_CTRL-D| CTRL-X CTRL-D complete defined identifiers
|i_CTRL-X_CTRL-E| CTRL-X CTRL-E scroll up
|i_CTRL-X_CTRL-F| CTRL-X CTRL-F complete file names
@@ -158,17 +154,19 @@ commands in CTRL-X submode *i_CTRL-X_index*
commands in completion mode (see |popupmenu-keys|)
|complete_CTRL-E| CTRL-E stop completion and go back to original text
|complete_CTRL-Y| CTRL-Y accept selected match and stop completion
CTRL-L insert one character from the current match
<CR> insert currently selected match
<BS> delete one character and redo search
CTRL-H same as <BS>
<Up> select the previous match
<Down> select the next match
<PageUp> select a match several entries back
<PageDown> select a match several entries forward
other stop completion and insert the typed character
Tag Char Completion-mode action ~
------------------------------------------------------------------------------ ~
|complete_CTRL-E| CTRL-E stop completion and go back to original text
|complete_CTRL-Y| CTRL-Y accept selected match and stop completion
  CTRL-L insert one character from the current match
  <CR> insert currently selected match
  <BS> delete one character and redo search
  CTRL-H same as <BS>
  <Up> select the previous match
  <Down> select the next match
  <PageUp> select a match several entries back
  <PageDown> select a match several entries forward
  other stop completion and insert the typed character
==============================================================================
2. Normal mode *normal-index*
@@ -184,7 +182,7 @@ note: 1 = cursor movement command; 2 = can be undone/redone
Tag Char Note Normal-mode action ~
------------------------------------------------------------------------------ ~
CTRL-@ not used
  CTRL-@ not used
|CTRL-A| CTRL-A 2 add N to number at/after cursor
|CTRL-B| CTRL-B 1 scroll N screens Backwards
|CTRL-C| CTRL-C interrupt current (search) command
@@ -199,7 +197,7 @@ Tag Char Note Normal-mode action ~
|<NL>| <NL> 1 same as "j"
|<S-NL>| <S-NL> 1 same as CTRL-F
|CTRL-J| CTRL-J 1 same as "j"
CTRL-K not used
  CTRL-K not used
|CTRL-L| CTRL-L redraw screen
|<CR>| <CR> 1 cursor to the first CHAR N lines lower
|<S-CR>| <S-CR> 1 same as CTRL-F
@@ -207,9 +205,9 @@ Tag Char Note Normal-mode action ~
|CTRL-N| CTRL-N 1 same as "j"
|CTRL-O| CTRL-O 1 go to N older entry in jump list
|CTRL-P| CTRL-P 1 same as "k"
CTRL-Q not used, or used for terminal control flow
  CTRL-Q not used, or used for terminal control flow
|CTRL-R| CTRL-R 2 redo changes which were undone with 'u'
CTRL-S not used, or used for terminal control flow
  CTRL-S not used, or used for terminal control flow
|CTRL-T| CTRL-T jump to N older Tag in tag list
|CTRL-U| CTRL-U scroll N lines Upwards (default: half a
screen)
@@ -218,18 +216,17 @@ Tag Char Note Normal-mode action ~
|CTRL-X| CTRL-X 2 subtract N from number at/after cursor
|CTRL-Y| CTRL-Y scroll N lines downwards
|CTRL-Z| CTRL-Z suspend program (or start new shell)
CTRL-[ <Esc> not used
  CTRL-[ <Esc> not used
|CTRL-\_CTRL-N| CTRL-\ CTRL-N go to Normal mode (no-op)
|CTRL-\_CTRL-G| CTRL-\ CTRL-G go to Normal mode (no-op)
CTRL-\ a - z reserved for extensions
CTRL-\ others not used
  CTRL-\ a - z reserved for extensions
  CTRL-\ others not used
|CTRL-]| CTRL-] :ta to ident under cursor
|CTRL-^| CTRL-^ edit Nth alternate file (equivalent to
":e #N")
|CTRL-<Tab>| CTRL-<Tab> same as `g<Tab>` : go to last accessed tab
page
CTRL-_ not used
  CTRL-_ not used
|<Space>| <Space> 1 same as "l"
|!| !{motion}{filter}
2 filter Nmove text through the {filter}
@@ -358,7 +355,7 @@ Tag Char Note Normal-mode action ~
|ZZ| ZZ write if buffer changed and close window
|ZQ| ZQ close window without writing
|[| [{char} square bracket command (see |[| below)
\ not used
  \ not used
|]| ]{char} square bracket command (see |]| below)
|^| ^ 1 cursor to the first CHAR of the line
|_| _ 1 cursor to the first CHAR N - 1 lines lower
@@ -523,7 +520,7 @@ Tag Command Normal-mode action ~
|CTRL-W_CTRL-C| CTRL-W CTRL-C no-op
|CTRL-W_CTRL-D| CTRL-W CTRL-D same as "CTRL-W d"
|CTRL-W_CTRL-F| CTRL-W CTRL-F same as "CTRL-W f"
CTRL-W CTRL-G same as "CTRL-W g .."
  CTRL-W CTRL-G same as "CTRL-W g .."
|CTRL-W_CTRL-H| CTRL-W CTRL-H same as "CTRL-W h"
|CTRL-W_CTRL-I| CTRL-W CTRL-I same as "CTRL-W i"
|CTRL-W_CTRL-J| CTRL-W CTRL-J same as "CTRL-W j"
@@ -662,7 +659,6 @@ Tag Char Note Normal-mode action ~
|[z| [z 1 move to start of open fold
|[{| [{ 1 cursor N times back to unmatched '{'
|[<MiddleMouse>| [<MiddleMouse> 2 same as "[p"
|]_CTRL-D| ] CTRL-D jump to first #define found in current and
included files matching the word under the
cursor, start searching at cursor position
@@ -802,7 +798,7 @@ Tag Char Note Normal-mode action ~
non-blank character instead
|g<Home>| g<Home> 1 same as "g0"
|g<LeftMouse>| g<LeftMouse> ":tag" on the keyword at mouse-click
g<MiddleMouse> same as <C-MiddleMouse>
  g<MiddleMouse> same as <C-MiddleMouse>
|g<RightMouse>| g<RightMouse> same as <C-RightMouse>
|g<Tab>| g<Tab> go to last accessed tabpage
|g<Up>| g<Up> 1 same as "gk"
@@ -1019,7 +1015,7 @@ file names, tags, commands etc. as appropriate.
Tag Command Command-line editing-mode action ~
------------------------------------------------------------------------------ ~
CTRL-@ not used
  CTRL-@ not used
|c_CTRL-A| CTRL-A do completion on the pattern in front of the
cursor and insert all matches
|c_CTRL-B| CTRL-B cursor to begin of command-line
@@ -1051,7 +1047,7 @@ Tag Command Command-line editing-mode action ~
|c_CTRL-N| CTRL-N after using 'wildchar' with multiple matches:
go to next match, otherwise: recall older
command-line from history.
CTRL-O not used
  CTRL-O not used
|c_CTRL-P| CTRL-P after using 'wildchar' with multiple matches:
go to previous match, otherwise: recall older
command-line from history.
@@ -1064,29 +1060,28 @@ Tag Command Command-line editing-mode action ~
|c_CTRL-R_CTRL-O| CTRL-R CTRL-O {regname}
insert the contents of a register or object
under the cursor literally
CTRL-S not used, or used for terminal control flow
  CTRL-S not used, or used for terminal control flow
|c_CTRL-T| CTRL-T previous match when 'incsearch' is active
|c_CTRL-U| CTRL-U remove all characters between the cursor
and beginning of command-line
|c_CTRL-V| CTRL-V insert next non-digit literally, insert three
digit decimal number as a single byte.
|c_CTRL-W| CTRL-W delete the word in front of the cursor
CTRL-X not used (reserved for completion)
CTRL-Y copy (yank) modeless selection
CTRL-Z not used (reserved for suspend)
  CTRL-X not used (reserved for completion)
  CTRL-Y copy (yank) modeless selection
  CTRL-Z not used (reserved for suspend)
|c_<Esc>| <Esc> abandon command-line without executing it
|c_CTRL-[| CTRL-[ same as <Esc>
|c_CTRL-\_CTRL-N| CTRL-\ CTRL-N go to Normal mode, abandon command-line
|c_CTRL-\_CTRL-G| CTRL-\ CTRL-G go to Normal mode, abandon command-line
CTRL-\ a - d reserved for extensions
  CTRL-\ a - d reserved for extensions
|c_CTRL-\_e| CTRL-\ e {expr} replace the command line with the result of
{expr}
CTRL-\ f - z reserved for extensions
CTRL-\ others not used
  CTRL-\ f - z reserved for extensions
  CTRL-\ others not used
|c_CTRL-]| CTRL-] trigger abbreviation
|c_CTRL-^| CTRL-^ toggle use of |:lmap| mappings
|c_<Del>| <Del> delete the character under the cursor
|c_<Left>| <Left> cursor left
|c_<S-Left>| <S-Left> cursor one word left
|c_<C-Left>| <C-Left> cursor one word left
@@ -1108,6 +1103,8 @@ Tag Command Command-line editing-mode action ~
commands in wildmenu mode (see 'wildmenu')
Char Wildmenu action ~
------------------------------------------------------------------------------ ~
<Up> move up to parent
<Down> move down to submenu
<Left> select the previous match
@@ -1119,6 +1116,8 @@ commands in wildmenu mode (see 'wildmenu')
commands in wildmenu mode with 'wildoptions' set to "pum"
Char Wildmenu action ~
------------------------------------------------------------------------------ ~
<PageUp> select a match several entries back
<PageDown> select a match several entries forward
+1 -1
View File
@@ -1199,7 +1199,7 @@ Context-sensitive completion on the command-line:
file
|:wN| :wN[ext][!] [file] write file and edit previous file
in current window in new window ~
in current window in new window action ~
|:argument| :argu[ment] N :sar[gument] N edit file N
|:next| :n[ext] :sn[ext] edit next file
|:next_f| :n[ext] {arglist} :sn[ext] {arglist} define new arg list
-4
View File
@@ -18,8 +18,6 @@ clientserver option.
The following command line arguments are available:
argument meaning ~
--remote [+{cmd}] {file} ... *--remote*
Open the file list in a remote Vim. When
there is no Vim server, execute locally.
@@ -93,8 +91,6 @@ Vim supports additional functionality in clientserver that's not yet
implemented in Nvim. In particular, none of the "wait" variants are supported
yet. The following command line arguments are not yet available:
argument meaning ~
--remote-wait [+{cmd}] {file} ... *--remote-wait*
Not yet supported by Nvim.
As --remote, but wait for files to complete
+10
View File
@@ -12,6 +12,16 @@
(column_heading
(heading) @markup.heading.4)
; Table header: block starting with a tab-separated column_heading (see tree-sitter-vimdoc README).
(block
.
(line
(column_heading
(heading) @markup.underline
(#match? @markup.underline "\\S\\t")))
.
(line))
(column_heading
(delimiter) @markup.heading.4
(#set! conceal ""))
+90
View File
@@ -339,6 +339,52 @@ local function trim_indent(s)
return (vim.text.indent(0, s, { expandtab = 8 }))
end
--- Splits the named children of `node` into cells (tab-delimited).
---@param node TSNode
---@param bufnr integer
---@return TSNode[][]
local function get_table_cells(node, bufnr)
local cells = { {} } ---@type TSNode[][]
local prev ---@type TSNode?
for child in node:iter_children() do
if child:named() then
local sr, sc = child:range()
local _, _, pr, pc = (prev or child):range()
if prev and table.concat(vim.api.nvim_buf_get_text(bufnr, pr, pc, sr, sc, {})):find('\t') then
cells[#cells + 1] = {}
end
table.insert(cells[#cells], child)
prev = child
end
end
return cells
end
--- Gets the rows (header first) of `block` if it is a table, else nil. Each row is a list of cells,
--- each cell a list of nodes. See "Tables" in the tree-sitter-vimdoc README.
---@param block TSNode
---@param bufnr integer
---@return TSNode[][][]?
local function get_table(block, bufnr)
local hdr = block:named_child(0) and block:named_child(0):named_child(0)
if not hdr or hdr:type() ~= 'column_heading' then
return nil
end
-- (column_heading (heading …) (delimiter))
local rows = { get_table_cells(assert(hdr:named_child(0)), bufnr) } ---@type TSNode[][][]
for i = 1, block:named_child_count() - 1 do
local line = assert(block:named_child(i))
local is_sep = vim.treesitter.get_node_text(line, bufnr):match('^[%s%-=~]+$')
local row = not is_sep and get_table_cells(line, bufnr)
if row and #row == 1 and #rows > 1 then
vim.list_extend(rows[#rows][#rows[#rows]], row[1]) -- Wrapped line continues last cell.
elseif row then
rows[#rows + 1] = row
end
end
return #rows[1] > 1 and #rows > 1 and rows or nil
end
--- Gets raw buffer text in the node's range (+/- an offset), as a newline-delimited string.
---@param node TSNode
---@param bufnr integer
@@ -596,6 +642,27 @@ local function ts_node_to_html(root, level, lang_tree, headings, opt, stats)
-- Parent kind (string).
local parent = root:parent() and root:parent():type() or nil
local table_rows = node_name == 'block' and get_table(root, opt.buf)
if table_rows then
local trs = {} ---@type string[]
for i, row in ipairs(table_rows) do
local tag = i == 1 and 'th' or 'td'
local cells = {} ---@type string[]
for _, cell in ipairs(row) do
local s = ''
for _, node in ipairs(cell) do
s = s .. ts_node_to_html(node, level + 1, lang_tree, headings, opt, stats)
end
cells[#cells + 1] = ('<%s>%s</%s>'):format(tag, trim(s), tag)
end
trs[#trs + 1] = ('<tr>%s</tr>'):format(table.concat(cells))
end
return ('<table class="help-table">\n<thead>%s</thead>\n<tbody>\n%s\n</tbody>\n</table>\n'):format(
trs[1],
table.concat(trs, '\n', 2)
)
end
local text = ''
local trimmed ---@type string
if root:named_child_count() == 0 or node_name == 'ERROR' then
@@ -899,6 +966,29 @@ local function ts_node_to_typ(root, level, lang_tree, opt, stats)
-- Parent kind (string).
local parent = root:parent() and root:parent():type() or nil
local table_rows = node_name == 'block' and get_table(root, opt.buf)
if table_rows then
-- Typst fills a flat cell list row by row, so pad every row to the widest row.
local ncols = 0
for _, row in ipairs(table_rows) do
ncols = math.max(ncols, #row)
end
local trs = {} ---@type string[]
for i, row in ipairs(table_rows) do
local cells = {} ---@type string[]
for j = 1, ncols do
local s = ''
for _, node in ipairs(row[j] or {}) do
s = s .. ts_node_to_typ(node, level + 1, lang_tree, opt, stats)
end
cells[#cells + 1] = ('[%s]'):format(trim(s))
end
local tr = table.concat(cells, ', ')
trs[#trs + 1] = i == 1 and ('table.header(%s),'):format(tr) or ('%s,'):format(tr)
end
return ('#table(columns: %d,\n %s\n)\n'):format(ncols, table.concat(trs, '\n '))
end
local text = ''
local trimmed ---@type string
if root:named_child_count() == 0 or node_name == 'ERROR' then