use solely extmarks
Some checks failed
NeoVim tests / plenary-tests (push) Failing after 10s

This commit is contained in:
Jonathan Apodaca 2025-02-26 23:36:00 -07:00
parent 9db70c4c10
commit 407347bfbc
3 changed files with 113 additions and 146 deletions

View File

@ -80,7 +80,7 @@ function Buffer:autocmd(event, opts)
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.buf }))
end
--- @param markup string
function Buffer:render(markup) return self.renderer:render(markup) end
--- @param tree Tree
function Buffer:render(tree) return self.renderer:render(tree) end
return Buffer

View File

@ -8,15 +8,14 @@ local TagMetaTable = {}
--------------------------------------------------------------------------------
-- Renderer class
--------------------------------------------------------------------------------
--- @alias RendererHighlight { group: string; start: [number, number]; stop: [number, number ] }
--- @alias RendererExtmark { id?: number; start: [number, number]; stop: [number, number]; opts: any; tag: any }
--- @class Renderer
--- @field bufnr number
--- @field ns number
--- @field changedtick number
--- @field old { lines: string[]; hls: RendererHighlight[] }
--- @field curr { lines: string[]; hls: RendererHighlight[] }
--- @field tag_infos { tag:Tag[]; start0: [number, number]; stop0: [number, number] }[]
--- @field old { lines: string[]; extmarks: RendererExtmark[] }
--- @field curr { lines: string[]; extmarks: RendererExtmark[] }
local Renderer = {}
Renderer.__index = Renderer
@ -49,14 +48,16 @@ end
function Renderer.new(bufnr)
if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end
if vim.b[bufnr]._renderer_ns == nil then vim.b[bufnr]._renderer_ns = vim.api.nvim_create_namespace '' end
if vim.b[bufnr]._renderer_ns == nil then
vim.b[bufnr]._renderer_ns = vim.api.nvim_create_namespace('my.renderer:' .. tostring(bufnr))
end
local self = setmetatable({
bufnr = bufnr,
ns = vim.b[bufnr]._renderer_ns,
changedtick = 0,
old = { lines = {}, hls = {} },
curr = { lines = {}, hls = {} },
old = { lines = {}, extmarks = {} },
curr = { lines = {}, extmarks = {} },
}, Renderer)
return self
end
@ -129,74 +130,45 @@ function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_
function Renderer:render(tree)
local changedtick = vim.b[self.bufnr].changedtick
if changedtick ~= self.changedtick then
self.curr = { lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false), hls = {} }
self.curr = { lines = vim.api.nvim_buf_get_lines(self.bufnr, 0, -1, false) }
self.changedtick = changedtick
end
--- @type RendererHighlight[]
local hls = {}
--- @type { tag:Tag[]; start0: [number, number]; stop0: [number, number] }[]
local tag_infos = {}
--- @type RendererExtmark[]
local extmarks = {}
--- @type string[]
local lines = Renderer.markup_to_lines {
tree = tree,
on_tag = function(tag, start0, stop0)
table.insert(tag_infos, { tag = tag, start0 = start0, stop0 = stop0 })
local function attr_num(nm)
if not tag.attributes[nm] then return end
return tonumber(tag.attributes[nm])
end
local function attr_bool(nm)
if not tag.attributes[nm] then return end
return tag.attributes[nm] and true or false
end
if tag.name == 'text' then
local group = tag.attributes.hl
if type(group) == 'string' then
table.insert(hls, { group = group, start = start0, stop = stop0 })
local local_exists = #vim.tbl_keys(vim.api.nvim_get_hl(self.ns, { name = group })) > 0
local global_exists = #vim.tbl_keys(vim.api.nvim_get_hl(0, { name = group }))
local exists = local_exists or global_exists
if not exists or attr_bool 'hl:force' then
vim.api.nvim_set_hl_ns(self.ns)
vim.api.nvim_set_hl(self.ns, group, {
fg = tag.attributes['hl:fg'],
bg = tag.attributes['hl:bg'],
sp = tag.attributes['hl:sp'],
blend = attr_num 'hl:blend',
bold = attr_bool 'hl:bold',
standout = attr_bool 'hl:standout',
underline = attr_bool 'hl:underline',
undercurl = attr_bool 'hl:undercurl',
underdouble = attr_bool 'hl:underdouble',
underdotted = attr_bool 'hl:underdotted',
underdashed = attr_bool 'hl:underdashed',
strikethrough = attr_bool 'hl:strikethrough',
italic = attr_bool 'hl:italic',
reverse = attr_bool 'hl:reverse',
nocombine = attr_bool 'hl:nocombine',
link = tag.attributes['hl:link'],
default = attr_bool 'hl:default',
ctermfg = attr_num 'hl:ctermfg',
ctermbg = attr_num 'hl:ctermbg',
cterm = tag.attributes['hl:cterm'],
force = attr_bool 'hl:force',
})
local hl = tag.attributes.hl
if type(hl) == 'string' then
tag.attributes.extmark = tag.attributes.extmark or {}
tag.attributes.extmark.hl_group = hl
end
local extmark = tag.attributes.extmark
-- Force creating an extmark if there are key handlers. To accurately
-- sense the bounds of the text, we need an extmark:
if tag.attributes.on_key or tag.attributes.on_typed then extmark = extmark or {} end
if extmark then
table.insert(extmarks, {
start = start0,
stop = stop0,
opts = extmark,
tag = tag,
})
end
end
end,
}
self.old = self.curr
self.curr = { lines = lines, hls = hls }
self.tag_infos = tag_infos
self.curr = { lines = lines, extmarks = extmarks }
self:_reconcile()
end
@ -236,6 +208,9 @@ function Renderer:_reconcile()
local line_changes = utils.levenshtein(self.old.lines, self.curr.lines)
self.old = self.curr
--
-- Step 1: morph the text to the desired state:
--
self:_log { line_changes = line_changes }
for _, line_change in ipairs(line_changes) do
local lnum0 = line_change.index - 1
@ -266,30 +241,87 @@ function Renderer:_reconcile()
-- No change
end
end
self.changedtick = vim.b[self.bufnr].changedtick
--
-- Step 2: reconcile extmarks:
--
-- Clear current extmarks:
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
-- Set current extmarks:
for _, extmark in ipairs(self.curr.extmarks) do
extmark.id = vim.api.nvim_buf_set_extmark(
self.bufnr,
self.ns,
extmark.start[1],
extmark.start[2],
vim.tbl_extend('force', {
id = extmark.id,
end_row = extmark.stop[1],
end_col = extmark.stop[2],
}, extmark.opts)
)
end
--
-- Step 3: setup and updated on_key handler:
--
vim.on_key(nil, self.ns)
vim.on_key(function(key, typed)
-- Discard if not in the current buffer:
if vim.api.nvim_get_current_buf() ~= self.bufnr then return end
if
-- do not capture keys in the wrong buffer:
vim.api.nvim_get_current_buf() ~= self.bufnr
-- do not capture keys in COMMAND mode:
or vim.startswith(vim.api.nvim_get_mode().mode, 'c')
then
return
end
-- find the tag with the smallest intersection that contains the cursor:
local pos0 = vim.api.nvim_win_get_cursor(0)
pos0[1] = pos0[1] - 1 -- make it actually 0-based
local cursor_line0, cursor_col0 = unpack(pos0)
-- Find the tag that contains the cursor: If the tag is listening for the
-- key that just fired, call its callback:
for _, tag_info in ipairs(self.tag_infos) do
local tag = tag_info.tag
local start0, stop0 = tag_info.start0, tag_info.stop0
if
-- line:
start0[1] <= cursor_line0
and stop0[1] >= cursor_line0
-- column:
and start0[2] <= cursor_col0
and stop0[2] > cursor_col0
then
-- The cursor (block) occupies **two** extmark spaces: one for it's left
-- edge, and one for it's right. We need to do our own intersection test,
-- because the NeoVim API is over-inclusive in what it returns:
--- @type { id: number; row0: number; col0: number; details: any }[]
local intersecting_extmarks = vim
.iter(vim.api.nvim_buf_get_extmarks(self.bufnr, self.ns, pos0, pos0, { details = true, overlap = true }))
:map(function(ext)
local id, row0, col0, details = unpack(ext)
return { id = id, row0 = row0, col0 = col0, details = details }
end)
:filter(function(ext)
if ext.details.end_row ~= nil and ext.details.end_col ~= nil then
return cursor_line0 >= ext.row0
and cursor_col0 >= ext.col0
and cursor_line0 <= ext.details.end_row
and cursor_col0 < ext.details.end_col
else
return true
end
end)
:totable()
-- When we set the extmarks in the step above, we captured the IDs of the
-- created extmarks in self.curr.extmarks, which also has which tag each
-- extmark is associated with. Cross-reference with that list to get a list
-- of tags that we need to fire events for:
local matching_tags = vim
.iter(intersecting_extmarks)
:map(function(ext)
for _, extmark_cache in ipairs(self.curr.extmarks) do
if extmark_cache.id == ext.id then return extmark_cache.tag end
end
end)
:totable()
-- Check the attributes for each matching_tag and fire events if they are
-- listening:
for _, tag in ipairs(matching_tags) do
-- is the tag listening?
if tag.attributes.on_key and type(tag.attributes.on_key[key]) == 'function' then
-- key:
@ -299,20 +331,7 @@ function Renderer:_reconcile()
return tag.attributes.on_typed[typed]()
end
end
end
end, self.ns)
-- vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.curr.lines)
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
for _, hl in ipairs(self.curr.hls) do
(vim.hl or vim.highlight).range(self.bufnr, self.ns, hl.group, hl.start, hl.stop, {
inclusive = false,
priority = (vim.hl or vim.highlight).priorities.user,
regtype = 'v',
})
end
self.changedtick = vim.b[self.bufnr].changedtick
end
return Renderer

View File

@ -1,59 +1,7 @@
local Renderer = require 'u.renderer'
--- @param markup string
local function parse(markup)
-- call private method:
return (Renderer --[[@as any]])._parse_markup(markup)
end
describe('Renderer', function()
it('_parse_markup: empty string', function()
local nodes = parse [[]]
assert.are.same({}, nodes)
end)
it('_parse_markup: only string', function()
local nodes = parse [[The quick brown fox jumps over the lazy dog.]]
assert.are.same({
{ kind = 'text', value = 'The quick brown fox jumps over the lazy dog.' },
}, nodes)
end)
it('_parse_markup: &lt;', function()
local nodes = parse [[&lt;t value="bleh" />]]
assert.are.same({
{ kind = 'text', value = '<t value="bleh" />' },
}, nodes)
end)
it('_parse_markup: empty tag', function()
local nodes = parse [[</>]]
assert.are.same({ { kind = 'tag', name = '', attributes = {} } }, nodes)
end)
it('_parse_markup: tag', function()
local nodes = parse [[<t value="Hello" />]]
assert.are.same({
{
kind = 'tag',
name = 't',
attributes = {
value = 'Hello',
},
},
}, nodes)
end)
it('_parse_markup: attributes with quotes', function()
local nodes = parse [[<t value="Hello \"there\"" />]]
assert.are.same({
{
kind = 'tag',
name = 't',
attributes = {
value = 'Hello "there"',
},
},
}, nodes)
it('markup_to_string', function()
assert.are.equal(true, false) -- TODO
end)
end)