aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/tohtml.lua
diff options
context:
space:
mode:
authorJosh Rahm <joshuarahm@gmail.com>2024-11-19 22:57:13 +0000
committerJosh Rahm <joshuarahm@gmail.com>2024-11-19 22:57:13 +0000
commit9be89f131f87608f224f0ee06d199fcd09d32176 (patch)
tree11022dcfa9e08cb4ac5581b16734196128688d48 /runtime/lua/tohtml.lua
parentff7ed8f586589d620a806c3758fac4a47a8e7e15 (diff)
parent88085c2e80a7e3ac29aabb6b5420377eed99b8b6 (diff)
downloadrneovim-9be89f131f87608f224f0ee06d199fcd09d32176.tar.gz
rneovim-9be89f131f87608f224f0ee06d199fcd09d32176.tar.bz2
rneovim-9be89f131f87608f224f0ee06d199fcd09d32176.zip
Merge remote-tracking branch 'upstream/master' into mix_20240309
Diffstat (limited to 'runtime/lua/tohtml.lua')
-rw-r--r--runtime/lua/tohtml.lua207
1 files changed, 136 insertions, 71 deletions
diff --git a/runtime/lua/tohtml.lua b/runtime/lua/tohtml.lua
index 505de720ba..ed42b28725 100644
--- a/runtime/lua/tohtml.lua
+++ b/runtime/lua/tohtml.lua
@@ -1,6 +1,6 @@
--- @brief
---<pre>help
----:TOhtml {file} *:TOhtml*
+---:[range]TOhtml {file} *:TOhtml*
---Converts the buffer shown in the current window to HTML, opens the generated
---HTML in a new split window, and saves its contents to {file}. If {file} is not
---given, a temporary file (created by |tempname()|) is used.
@@ -40,13 +40,14 @@
--- @field winid integer
--- @field bufnr integer
--- @field width integer
---- @field buflen integer
+--- @field start integer
+--- @field end_ integer
--- @class (private) vim.tohtml.styletable
--- @field [integer] vim.tohtml.line (integer: (1-index, exclusive))
--- @class (private) vim.tohtml.line
---- @field virt_lines {[integer]:{[1]:string,[2]:integer}[]}
+--- @field virt_lines {[integer]:[string,integer][]}
--- @field pre_text string[][]
--- @field hide? boolean
--- @field [integer] vim.tohtml.cell? (integer: (1-index, exclusive))
@@ -57,6 +58,24 @@
--- @field [3] any[][] virt_text
--- @field [4] any[][] overlay_text
+--- @type string[]
+local notifications = {}
+
+---@param msg string
+local function notify(msg)
+ if #notifications == 0 then
+ vim.schedule(function()
+ if #notifications > 1 then
+ vim.notify(('TOhtml: %s (+ %d more warnings)'):format(notifications[1], #notifications - 1))
+ elseif #notifications == 1 then
+ vim.notify('TOhtml: ' .. notifications[1])
+ end
+ notifications = {}
+ end)
+ end
+ table.insert(notifications, msg)
+end
+
local HIDE_ID = -1
-- stylua: ignore start
local cterm_8_to_hex={
@@ -168,6 +187,8 @@ local background_color_cache = nil
--- @type string?
local foreground_color_cache = nil
+local len = vim.api.nvim_strwidth
+
--- @see https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands
--- @param color "background"|"foreground"|integer
--- @return string?
@@ -215,7 +236,7 @@ local function cterm_to_hex(colorstr)
if hex then
cterm_color_cache[color] = hex
else
- vim.notify_once("Info(TOhtml): Couldn't get terminal colors, using fallback")
+ notify("Couldn't get terminal colors, using fallback")
local t_Co = tonumber(vim.api.nvim_eval('&t_Co'))
if t_Co <= 8 then
cterm_color_cache = cterm_8_to_hex
@@ -241,7 +262,7 @@ local function get_background_color()
end
local hex = try_query_terminal_color('background')
if not hex or not hex:match('#%x%x%x%x%x%x') then
- vim.notify_once("Info(TOhtml): Couldn't get terminal background colors, using fallback")
+ notify("Couldn't get terminal background colors, using fallback")
hex = vim.o.background == 'light' and '#ffffff' or '#000000'
end
background_color_cache = hex
@@ -259,7 +280,7 @@ local function get_foreground_color()
end
local hex = try_query_terminal_color('foreground')
if not hex or not hex:match('#%x%x%x%x%x%x') then
- vim.notify_once("Info(TOhtml): Couldn't get terminal foreground colors, using fallback")
+ notify("Couldn't get terminal foreground colors, using fallback")
hex = vim.o.background == 'light' and '#000000' or '#ffffff'
end
foreground_color_cache = hex
@@ -292,9 +313,12 @@ local function style_line_insert_virt_text(style_line, col, val)
end
--- @param state vim.tohtml.state
---- @param hl string|integer|nil
+--- @param hl string|integer|string[]|integer[]?
--- @return nil|integer
local function register_hl(state, hl)
+ if type(hl) == 'table' then
+ hl = hl[#hl]
+ end
if type(hl) == 'nil' then
return
elseif type(hl) == 'string' then
@@ -370,7 +394,7 @@ end
--- @param state vim.tohtml.state
local function styletable_syntax(state)
- for row = 1, state.buflen do
+ for row = state.start, state.end_ do
local prev_id = 0
local prev_col = nil
for col = 1, #vim.fn.getline(row) + 1 do
@@ -390,7 +414,7 @@ end
--- @param state vim.tohtml.state
local function styletable_diff(state)
local styletable = state.style
- for row = 1, state.buflen do
+ for row = state.start, state.end_ do
local style_line = styletable[row]
local filler = vim.fn.diff_filler(row)
if filler ~= 0 then
@@ -400,7 +424,7 @@ local function styletable_diff(state)
{ { fill:rep(state.width), register_hl(state, 'DiffDelete') } }
)
end
- if row == state.buflen + 1 then
+ if row == state.end_ + 1 then
break
end
local prev_id = 0
@@ -442,7 +466,9 @@ local function styletable_treesitter(state)
if not query then
return
end
- for capture, node, metadata in query:iter_captures(root, buf_highlighter.bufnr, 0, state.buflen) do
+ for capture, node, metadata in
+ query:iter_captures(root, buf_highlighter.bufnr, state.start - 1, state.end_)
+ do
local srow, scol, erow, ecol = node:range()
--- @diagnostic disable-next-line: invisible
local c = q._query.captures[capture]
@@ -458,7 +484,7 @@ local function styletable_treesitter(state)
end
--- @param state vim.tohtml.state
---- @param extmark {[1]:integer,[2]:integer,[3]:integer,[4]:vim.api.keyset.set_extmark|any}
+--- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any]
--- @param namespaces table<integer,string>
local function _styletable_extmarks_highlight(state, extmark, namespaces)
if not extmark[4].hl_group then
@@ -467,7 +493,7 @@ local function _styletable_extmarks_highlight(state, extmark, namespaces)
---TODO(altermo) LSP semantic tokens (and some other extmarks) are only
---generated in visible lines, and not in the whole buffer.
if (namespaces[extmark[4].ns_id] or ''):find('vim_lsp_semantic_tokens') then
- vim.notify_once('Info(TOhtml): lsp semantic tokens are not supported, HTML may be incorrect')
+ notify('lsp semantic tokens are not supported, HTML may be incorrect')
return
end
local srow, scol, erow, ecol =
@@ -480,18 +506,28 @@ local function _styletable_extmarks_highlight(state, extmark, namespaces)
end
--- @param state vim.tohtml.state
---- @param extmark {[1]:integer,[2]:integer,[3]:integer,[4]:vim.api.keyset.set_extmark|any}
-local function _styletable_extmarks_virt_text(state, extmark)
+--- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any]
+--- @param namespaces table<integer,string>
+local function _styletable_extmarks_virt_text(state, extmark, namespaces)
if not extmark[4].virt_text then
return
end
+ ---TODO(altermo) LSP semantic tokens (and some other extmarks) are only
+ ---generated in visible lines, and not in the whole buffer.
+ if (namespaces[extmark[4].ns_id] or ''):find('vim_lsp_inlayhint') then
+ notify('lsp inlay hints are not supported, HTML may be incorrect')
+ return
+ end
local styletable = state.style
--- @type integer,integer
local row, col = extmark[2], extmark[3]
if
- extmark[4].virt_text_pos == 'inline'
- or extmark[4].virt_text_pos == 'eol'
- or extmark[4].virt_text_pos == 'overlay'
+ row < vim.api.nvim_buf_line_count(state.bufnr)
+ and (
+ extmark[4].virt_text_pos == 'inline'
+ or extmark[4].virt_text_pos == 'eol'
+ or extmark[4].virt_text_pos == 'overlay'
+ )
then
if extmark[4].virt_text_pos == 'eol' then
style_line_insert_virt_text(styletable[row + 1], #vim.fn.getline(row + 1) + 1, { ' ' })
@@ -510,7 +546,7 @@ local function _styletable_extmarks_virt_text(state, extmark)
else
style_line_insert_virt_text(styletable[row + 1], col + 1, { i[1], hlid })
end
- virt_text_len = virt_text_len + #i[1]
+ virt_text_len = virt_text_len + len(i[1])
end
if extmark[4].virt_text_pos == 'overlay' then
styletable_insert_range(state, row + 1, col + 1, row + 1, col + virt_text_len + 1, HIDE_ID)
@@ -521,17 +557,15 @@ local function _styletable_extmarks_virt_text(state, extmark)
hl_mode = 'blend',
hl_group = 'combine',
}
- for opt, val in ipairs(not_supported) do
+ for opt, val in pairs(not_supported) do
if extmark[4][opt] == val then
- vim.notify_once(
- ('Info(TOhtml): extmark.%s="%s" is not supported, HTML may be incorrect'):format(opt, val)
- )
+ notify(('extmark.%s="%s" is not supported, HTML may be incorrect'):format(opt, val))
end
end
end
--- @param state vim.tohtml.state
---- @param extmark {[1]:integer,[2]:integer,[3]:integer,[4]:vim.api.keyset.set_extmark|any}
+--- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any]
local function _styletable_extmarks_virt_lines(state, extmark)
---TODO(altermo) if the fold start is equal to virt_line start then the fold hides the virt_line
if not extmark[4].virt_lines then
@@ -552,7 +586,7 @@ local function _styletable_extmarks_virt_lines(state, extmark)
end
--- @param state vim.tohtml.state
---- @param extmark {[1]:integer,[2]:integer,[3]:integer,[4]:vim.api.keyset.set_extmark|any}
+--- @param extmark [integer, integer, integer, vim.api.keyset.set_extmark|any]
local function _styletable_extmarks_conceal(state, extmark)
if not extmark[4].conceal or state.opt.conceallevel == 0 then
return
@@ -586,7 +620,7 @@ local function styletable_extmarks(state)
_styletable_extmarks_conceal(state, v)
end
for _, v in ipairs(extmarks) do
- _styletable_extmarks_virt_text(state, v)
+ _styletable_extmarks_virt_text(state, v, namespaces)
end
for _, v in ipairs(extmarks) do
_styletable_extmarks_virt_lines(state, v)
@@ -597,7 +631,7 @@ end
local function styletable_folds(state)
local styletable = state.style
local has_folded = false
- for row = 1, state.buflen do
+ for row = state.start, state.end_ do
if vim.fn.foldclosed(row) > 0 then
has_folded = true
styletable[row].hide = true
@@ -611,18 +645,16 @@ local function styletable_folds(state)
end
end
if has_folded and type(({ pcall(vim.api.nvim_eval, vim.o.foldtext) })[2]) == 'table' then
- vim.notify_once(
- 'Info(TOhtml): foldtext returning a table is half supported, HTML may be incorrect'
- )
+ notify('foldtext returning a table with highlights is not supported, HTML may be incorrect')
end
end
--- @param state vim.tohtml.state
local function styletable_conceal(state)
local bufnr = state.bufnr
- vim.api.nvim_buf_call(bufnr, function()
- for row = 1, state.buflen do
- --- @type table<integer,{[1]:integer,[2]:integer,[3]:string}>
+ vim._with({ buf = bufnr }, function()
+ for row = state.start, state.end_ do
+ --- @type table<integer,[integer,integer,string]>
local conceals = {}
local line_len_exclusive = #vim.fn.getline(row) + 1
for col = 1, line_len_exclusive do
@@ -738,8 +770,8 @@ local function styletable_statuscolumn(state)
if foldcolumn:match('^auto') then
local max = tonumber(foldcolumn:match('^%w-:(%d)')) or 1
local maxfold = 0
- vim.api.nvim_buf_call(state.bufnr, function()
- for row = 1, vim.api.nvim_buf_line_count(state.bufnr) do
+ vim._with({ buf = state.bufnr }, function()
+ for row = state.start, state.end_ do
local foldlevel = vim.fn.foldlevel(row)
if foldlevel > maxfold then
maxfold = foldlevel
@@ -754,12 +786,12 @@ local function styletable_statuscolumn(state)
--- @type table<integer,any>
local statuses = {}
- for row = 1, state.buflen do
+ for row = state.start, state.end_ do
local status = vim.api.nvim_eval_statusline(
statuscolumn,
{ winid = state.winid, use_statuscol_lnum = row, highlights = true }
)
- local width = vim.api.nvim_strwidth(status.str)
+ local width = len(status.str)
if width > minwidth then
minwidth = width
end
@@ -774,7 +806,7 @@ local function styletable_statuscolumn(state)
for k, v in ipairs(hls) do
local text = str:sub(v.start + 1, hls[k + 1] and hls[k + 1].start or nil)
if k == #hls then
- text = text .. (' '):rep(minwidth - vim.api.nvim_strwidth(str))
+ text = text .. (' '):rep(minwidth - len(str))
end
if text ~= '' then
local hlid = register_hl(state, v.group)
@@ -794,7 +826,6 @@ local function styletable_listchars(state)
local function utf8_sub(str, i, j)
return vim.fn.strcharpart(str, i - 1, j and j - i + 1 or nil)
end
- local len = vim.api.nvim_strwidth
--- @type table<string,string>
local listchars = vim.opt_local.listchars:get()
local ids = setmetatable({}, {
@@ -805,7 +836,7 @@ local function styletable_listchars(state)
})
if listchars.eol then
- for row = 1, state.buflen do
+ for row = state.start, state.end_ do
local style_line = state.style[row]
style_line_insert_overlay_char(
style_line,
@@ -972,6 +1003,7 @@ local function extend_style(out, state)
--TODO(altermo) use local namespace (instead of global 0)
local fg = vim.fn.synIDattr(hlid, 'fg#')
local bg = vim.fn.synIDattr(hlid, 'bg#')
+ local sp = vim.fn.synIDattr(hlid, 'sp#')
local decor_line = {}
if vim.fn.synIDattr(hlid, 'underline') ~= '' then
table.insert(decor_line, 'underline')
@@ -989,6 +1021,8 @@ local function extend_style(out, state)
['font-weight'] = vim.fn.synIDattr(hlid, 'bold') ~= '' and 'bold' or nil,
['text-decoration-line'] = not vim.tbl_isempty(decor_line) and table.concat(decor_line, ' ')
or nil,
+ -- TODO(ribru17): fallback to displayed text color if sp not set
+ ['text-decoration-color'] = sp ~= '' and cterm_to_hex(sp) or nil,
--TODO(altermo) if strikethrough and undercurl then the strikethrough becomes wavy
['text-decoration-style'] = vim.fn.synIDattr(hlid, 'undercurl') ~= '' and 'wavy' or nil,
}
@@ -1099,16 +1133,22 @@ end
local function extend_pre(out, state)
local styletable = state.style
table.insert(out, '<pre>')
+ local out_start = #out
local hide_count = 0
--- @type integer[]
local stack = {}
+ local before = ''
+ local after = ''
local function loop(row)
+ local inside = row <= state.end_ and row >= state.start
local style_line = styletable[row]
if style_line.hide and (styletable[row - 1] or {}).hide then
return
end
- _extend_virt_lines(out, state, row)
+ if inside then
+ _extend_virt_lines(out, state, row)
+ end
--Possible improvement (altermo):
--Instead of looping over all the buffer characters per line,
--why not loop over all the style_line cells,
@@ -1118,8 +1158,16 @@ local function extend_pre(out, state)
end
local line = vim.api.nvim_buf_get_lines(state.bufnr, row - 1, row, false)[1] or ''
local s = ''
- s = s .. _pre_text_to_html(state, row)
- for col = 1, #line + 1 do
+ if inside then
+ s = s .. _pre_text_to_html(state, row)
+ end
+ local true_line_len = #line + 1
+ for k in pairs(style_line) do
+ if type(k) == 'number' and k > true_line_len then
+ true_line_len = k
+ end
+ end
+ for col = 1, true_line_len do
local cell = style_line[col]
--- @type table?
local char
@@ -1159,18 +1207,18 @@ local function extend_pre(out, state)
end
end
- if cell[3] then
+ if cell[3] and inside then
s = s .. _virt_text_to_html(state, cell)
end
char = cell[4][#cell[4]]
end
- if col == #line + 1 and not char then
+ if col == true_line_len and not char then
break
end
- if hide_count == 0 then
+ if hide_count == 0 and inside then
s = s
.. _char_to_html(
state,
@@ -1179,12 +1227,20 @@ local function extend_pre(out, state)
)
end
end
- table.insert(out, s)
+ if row > state.end_ + 1 then
+ after = after .. s
+ elseif row < state.start then
+ before = s .. before
+ else
+ table.insert(out, s)
+ end
end
- for row = 1, state.buflen + 1 do
+ for row = 1, vim.api.nvim_buf_line_count(state.bufnr) + 1 do
loop(row)
end
+ out[out_start] = out[out_start] .. before
+ out[#out] = out[#out] .. after
assert(#stack == 0, 'an open HTML tag was never closed')
table.insert(out, '</pre>')
end
@@ -1216,6 +1272,7 @@ local function global_state_to_state(winid, global_state)
if not width or width < 1 then
width = vim.api.nvim_win_get_width(winid)
end
+ local range = opt.range or { 1, vim.api.nvim_buf_line_count(bufnr) }
local state = setmetatable({
winid = winid == 0 and vim.api.nvim_get_current_win() or winid,
opt = vim.wo[winid],
@@ -1223,7 +1280,8 @@ local function global_state_to_state(winid, global_state)
bufnr = bufnr,
tabstop = (' '):rep(vim.bo[bufnr].tabstop),
width = width,
- buflen = vim.api.nvim_buf_line_count(bufnr),
+ start = range[1],
+ end_ = range[2],
}, { __index = global_state })
return state --[[@as vim.tohtml.state]]
end
@@ -1235,9 +1293,25 @@ local function opt_to_global_state(opt, title)
local fonts = {}
if opt.font then
fonts = type(opt.font) == 'string' and { opt.font } or opt.font --[[@as (string[])]]
+ for i, v in pairs(fonts) do
+ fonts[i] = ('"%s"'):format(v)
+ end
elseif vim.o.guifont:match('^[^:]+') then
- table.insert(fonts, vim.o.guifont:match('^[^:]+'))
+ -- Example:
+ -- Input: "Font,Escape\,comma, Ignore space after comma"
+ -- Output: { "Font","Escape,comma","Ignore space after comma" }
+ local prev = ''
+ for name in vim.gsplit(vim.o.guifont:match('^[^:]+'), ',', { trimempty = true }) do
+ if vim.endswith(name, '\\') then
+ prev = prev .. vim.trim(name:sub(1, -2) .. ',')
+ elseif vim.trim(name) ~= '' then
+ table.insert(fonts, ('"%s%s"'):format(prev, vim.trim(name)))
+ prev = ''
+ end
+ end
end
+ -- Generic family names (monospace here) must not be quoted
+ -- because the browser recognizes them as font families.
table.insert(fonts, 'monospace')
--- @type vim.tohtml.state.global
local state = {
@@ -1266,7 +1340,7 @@ local styletable_funcs = {
--- @param state vim.tohtml.state
local function state_generate_style(state)
- vim.api.nvim_win_call(state.winid, function()
+ vim._with({ win = state.winid }, function()
for _, fn in ipairs(styletable_funcs) do
--- @type string?
local cond
@@ -1282,35 +1356,22 @@ local function state_generate_style(state)
end)
end
---- @param winid integer[]|integer
+--- @param winid integer
--- @param opt? vim.tohtml.opt
--- @return string[]
local function win_to_html(winid, opt)
- if type(winid) == 'number' then
- winid = { winid }
- end
- --- @cast winid integer[]
- assert(#winid > 0, 'no window specified')
opt = opt or {}
- local title = table.concat(
- vim.tbl_map(vim.api.nvim_buf_get_name, vim.tbl_map(vim.api.nvim_win_get_buf, winid)),
- ','
- )
+ local title = vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(winid))
+
local global_state = opt_to_global_state(opt, title)
- --- @type vim.tohtml.state[]
- local states = {}
- for _, i in ipairs(winid) do
- local state = global_state_to_state(i, global_state)
- state_generate_style(state)
- table.insert(states, state)
- end
+ local state = global_state_to_state(winid, global_state)
+ state_generate_style(state)
+
local html = {}
extend_html(html, function()
extend_head(html, global_state)
extend_body(html, function()
- for _, state in ipairs(states) do
- extend_pre(html, state)
- end
+ extend_pre(html, state)
end)
end)
return html
@@ -1337,6 +1398,10 @@ local M = {}
--- infinitely.
--- (default: 'textwidth' if non-zero or window width otherwise)
--- @field width? integer
+---
+--- Range of rows to use.
+--- (default: entire buffer)
+--- @field range? integer[]
--- Converts the buffer shown in the window {winid} to HTML and returns the output as a list of string.
--- @param winid? integer Window to convert (defaults to current window)