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 }))
|
vim.api.nvim_create_autocmd(event, vim.tbl_extend('force', opts, { buffer = self.buf }))
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @param markup string
|
--- @param tree Tree
|
||||||
function Buffer:render(markup) return self.renderer:render(markup) end
|
function Buffer:render(tree) return self.renderer:render(tree) end
|
||||||
|
|
||||||
return Buffer
|
return Buffer
|
||||||
|
@ -8,15 +8,14 @@ local TagMetaTable = {}
|
|||||||
--------------------------------------------------------------------------------
|
--------------------------------------------------------------------------------
|
||||||
-- Renderer class
|
-- 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
|
--- @class Renderer
|
||||||
--- @field bufnr number
|
--- @field bufnr number
|
||||||
--- @field ns number
|
--- @field ns number
|
||||||
--- @field changedtick number
|
--- @field changedtick number
|
||||||
--- @field old { lines: string[]; hls: RendererHighlight[] }
|
--- @field old { lines: string[]; extmarks: RendererExtmark[] }
|
||||||
--- @field curr { lines: string[]; hls: RendererHighlight[] }
|
--- @field curr { lines: string[]; extmarks: RendererExtmark[] }
|
||||||
--- @field tag_infos { tag:Tag[]; start0: [number, number]; stop0: [number, number] }[]
|
|
||||||
local Renderer = {}
|
local Renderer = {}
|
||||||
Renderer.__index = Renderer
|
Renderer.__index = Renderer
|
||||||
|
|
||||||
@ -49,14 +48,16 @@ end
|
|||||||
function Renderer.new(bufnr)
|
function Renderer.new(bufnr)
|
||||||
if bufnr == nil then bufnr = vim.api.nvim_get_current_buf() end
|
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({
|
local self = setmetatable({
|
||||||
bufnr = bufnr,
|
bufnr = bufnr,
|
||||||
ns = vim.b[bufnr]._renderer_ns,
|
ns = vim.b[bufnr]._renderer_ns,
|
||||||
changedtick = 0,
|
changedtick = 0,
|
||||||
old = { lines = {}, hls = {} },
|
old = { lines = {}, extmarks = {} },
|
||||||
curr = { lines = {}, hls = {} },
|
curr = { lines = {}, extmarks = {} },
|
||||||
}, Renderer)
|
}, Renderer)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
@ -129,74 +130,45 @@ function Renderer.markup_to_string(opts) return table.concat(Renderer.markup_to_
|
|||||||
function Renderer:render(tree)
|
function Renderer:render(tree)
|
||||||
local changedtick = vim.b[self.bufnr].changedtick
|
local changedtick = vim.b[self.bufnr].changedtick
|
||||||
if changedtick ~= self.changedtick then
|
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
|
self.changedtick = changedtick
|
||||||
end
|
end
|
||||||
|
|
||||||
--- @type RendererHighlight[]
|
--- @type RendererExtmark[]
|
||||||
local hls = {}
|
local extmarks = {}
|
||||||
|
|
||||||
--- @type { tag:Tag[]; start0: [number, number]; stop0: [number, number] }[]
|
|
||||||
local tag_infos = {}
|
|
||||||
|
|
||||||
--- @type string[]
|
--- @type string[]
|
||||||
local lines = Renderer.markup_to_lines {
|
local lines = Renderer.markup_to_lines {
|
||||||
tree = tree,
|
tree = tree,
|
||||||
|
|
||||||
on_tag = function(tag, start0, stop0)
|
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
|
if tag.name == 'text' then
|
||||||
local group = tag.attributes.hl
|
local hl = tag.attributes.hl
|
||||||
if type(group) == 'string' then
|
if type(hl) == 'string' then
|
||||||
table.insert(hls, { group = group, start = start0, stop = stop0 })
|
tag.attributes.extmark = tag.attributes.extmark or {}
|
||||||
|
tag.attributes.extmark.hl_group = tag.attributes.extmark.hl_group or hl
|
||||||
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',
|
|
||||||
})
|
|
||||||
end
|
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
|
end
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.old = self.curr
|
self.old = self.curr
|
||||||
self.curr = { lines = lines, hls = hls }
|
self.curr = { lines = lines, extmarks = extmarks }
|
||||||
self.tag_infos = tag_infos
|
|
||||||
self:_reconcile()
|
self:_reconcile()
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -236,6 +208,9 @@ function Renderer:_reconcile()
|
|||||||
local line_changes = utils.levenshtein(self.old.lines, self.curr.lines)
|
local line_changes = utils.levenshtein(self.old.lines, self.curr.lines)
|
||||||
self.old = self.curr
|
self.old = self.curr
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Step 1: morph the text to the desired state:
|
||||||
|
--
|
||||||
self:_log { line_changes = line_changes }
|
self:_log { line_changes = line_changes }
|
||||||
for _, line_change in ipairs(line_changes) do
|
for _, line_change in ipairs(line_changes) do
|
||||||
local lnum0 = line_change.index - 1
|
local lnum0 = line_change.index - 1
|
||||||
@ -266,53 +241,143 @@ function Renderer:_reconcile()
|
|||||||
-- No change
|
-- No change
|
||||||
end
|
end
|
||||||
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(nil, self.ns)
|
||||||
vim.on_key(function(key, typed)
|
vim.on_key(function(key, typed)
|
||||||
-- Discard if not in the current buffer:
|
-- 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:
|
-- find the tag with the smallest intersection that contains the cursor:
|
||||||
local pos0 = vim.api.nvim_win_get_cursor(0)
|
local pos0 = vim.api.nvim_win_get_cursor(0)
|
||||||
pos0[1] = pos0[1] - 1 -- make it actually 0-based
|
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?
|
-- is the tag listening?
|
||||||
if tag.attributes.on_key and type(tag.attributes.on_key[key]) == 'function' then
|
if tag.attributes.on_key and type(tag.attributes.on_key[key]) == 'function' then
|
||||||
-- key:
|
-- 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
|
elseif tag.attributes.on_typed and type(tag.attributes.on_typed[typed]) == 'function' then
|
||||||
-- typed:
|
-- typed:
|
||||||
return tag.attributes.on_typed[typed]()
|
local result = tag.attributes.on_typed[typed]()
|
||||||
end
|
if result == '' then return '' end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end, self.ns)
|
end, self.ns)
|
||||||
|
end
|
||||||
|
|
||||||
-- vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.curr.lines)
|
--- Returns pairs of extmarks and tags associate with said extmarks. The
|
||||||
vim.api.nvim_buf_clear_namespace(self.bufnr, self.ns, 0, -1)
|
--- returned tags/extmarks are sorted smallest (innermost) to largest
|
||||||
for _, hl in ipairs(self.curr.hls) do
|
--- (outermost).
|
||||||
(vim.hl or vim.highlight).range(self.bufnr, self.ns, hl.group, hl.start, hl.stop, {
|
---
|
||||||
inclusive = false,
|
--- @private (private for now)
|
||||||
priority = (vim.hl or vim.highlight).priorities.user,
|
--- @param pos0 [number; number]
|
||||||
regtype = 'v',
|
--- @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
|
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
|
end
|
||||||
|
|
||||||
return Renderer
|
return Renderer
|
||||||
|
@ -1,59 +1,7 @@
|
|||||||
local Renderer = require 'u.renderer'
|
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()
|
describe('Renderer', function()
|
||||||
it('_parse_markup: empty string', function()
|
it('markup_to_string', function()
|
||||||
local nodes = parse [[]]
|
assert.are.equal(true, false) -- TODO
|
||||||
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)
|
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user