This commit is contained in:
parent
9db70c4c10
commit
c2cc24aa4a
@ -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
|
||||
|
@ -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 = tag.attributes.extmark.hl_group or 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,53 +241,143 @@ 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 an 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)
|
||||
local pos_infos = self:get_pos_infos(pos0)
|
||||
|
||||
-- Check the attributes for each matching_tag and fire events if they are
|
||||
-- listening:
|
||||
for _, pos_info in ipairs(pos_infos) do
|
||||
local tag = pos_info.tag
|
||||
|
||||
-- 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
|
||||
-- is the tag listening?
|
||||
if tag.attributes.on_key and type(tag.attributes.on_key[key]) == 'function' then
|
||||
-- key:
|
||||
return tag.attributes.on_key[key]()
|
||||
local result = tag.attributes.on_key[key]()
|
||||
if result == '' then return '' end
|
||||
elseif tag.attributes.on_typed and type(tag.attributes.on_typed[typed]) == 'function' then
|
||||
-- typed:
|
||||
return tag.attributes.on_typed[typed]()
|
||||
end
|
||||
local result = tag.attributes.on_typed[typed]()
|
||||
if result == '' then return '' end
|
||||
end
|
||||
end
|
||||
end, self.ns)
|
||||
end
|
||||
|
||||
-- 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',
|
||||
})
|
||||
--- Returns pairs of extmarks and tags associate with said extmarks. The
|
||||
--- returned tags/extmarks are sorted smallest (innermost) to largest
|
||||
--- (outermost).
|
||||
---
|
||||
--- @private (private for now)
|
||||
--- @param pos0 [number; number]
|
||||
--- @return {
|
||||
--- extmark: { id: number; line0: number; col0: number; details: { end_row?: number; end_col?: number } };
|
||||
--- tag: Tag;
|
||||
--- }[]
|
||||
function Renderer:get_pos_infos(pos0)
|
||||
local cursor_line0, cursor_col0 = pos0[1], pos0[2]
|
||||
|
||||
-- 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; line0: 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, line0, col0, details = unpack(ext)
|
||||
return { id = id, line0 = line0, 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.line0
|
||||
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()
|
||||
|
||||
-- Sort the tags into smallest (inner) to largest (outer):
|
||||
table.sort(
|
||||
intersecting_extmarks,
|
||||
--- @param x1 { id: number; line0: number; col0: number; details: { end_row?: number; end_col?: number } }
|
||||
--- @param x2 { id: number; line0: number; col0: number; details: { end_row?: number; end_col?: number } }
|
||||
function(x1, x2)
|
||||
if
|
||||
x1.line0 == x2.line0
|
||||
and x1.col0 == x2.col0
|
||||
and x1.details.end_row == x2.details.end_row
|
||||
and x1.details.end_col == x2.details.end_col
|
||||
then
|
||||
return x1.id < x2.id
|
||||
end
|
||||
|
||||
self.changedtick = vim.b[self.bufnr].changedtick
|
||||
-- We only care about extmarks that are regions:
|
||||
if x1.details.end_row == nil or x2.details.end_row == nil then return x1.id < x2.id end
|
||||
|
||||
return x1.line0 >= x2.line0
|
||||
and x1.col0 >= x2.col0
|
||||
and x1.details.end_row <= x2.details.end_row
|
||||
and x1.details.end_row <= x2.details.end_row
|
||||
end
|
||||
)
|
||||
|
||||
-- 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:
|
||||
--- @type { extmark: { id: number; line0: number; col0: number; details: { end_row?: number; end_col?: number } }; tag: Tag }[]
|
||||
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 = ext, tag = extmark_cache.tag } end
|
||||
end
|
||||
end)
|
||||
:totable()
|
||||
|
||||
return matching_tags
|
||||
end
|
||||
|
||||
return Renderer
|
||||
|
@ -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: <', function()
|
||||
local nodes = parse [[<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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user