mirror of
https://github.com/folke/snacks.nvim
synced 2025-07-07 21:25:11 +00:00
This commit is contained in:
parent
1bbd47973d
commit
6ea4fa72dc
7 changed files with 807 additions and 365 deletions
|
@ -4,7 +4,8 @@ local M = {}
|
||||||
---@param buf number
|
---@param buf number
|
||||||
---@param opts? snacks.image.Opts|{src?: string}
|
---@param opts? snacks.image.Opts|{src?: string}
|
||||||
function M.attach(buf, opts)
|
function M.attach(buf, opts)
|
||||||
local file = opts and opts.src or vim.api.nvim_buf_get_name(buf)
|
opts = opts or {}
|
||||||
|
local file = opts.src or vim.api.nvim_buf_get_name(buf)
|
||||||
if not Snacks.image.supports(file) then
|
if not Snacks.image.supports(file) then
|
||||||
local lines = {} ---@type string[]
|
local lines = {} ---@type string[]
|
||||||
lines[#lines + 1] = "# Image viewer"
|
lines[#lines + 1] = "# Image viewer"
|
||||||
|
@ -28,6 +29,8 @@ function M.attach(buf, opts)
|
||||||
modified = false,
|
modified = false,
|
||||||
swapfile = false,
|
swapfile = false,
|
||||||
})
|
})
|
||||||
|
opts.conceal = true
|
||||||
|
opts.auto_resize = true
|
||||||
return Snacks.image.placement.new(buf, file, opts)
|
return Snacks.image.placement.new(buf, file, opts)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,6 +12,7 @@ local uv = vim.uv or vim.loop
|
||||||
|
|
||||||
---@class snacks.image.convert.Opts
|
---@class snacks.image.convert.Opts
|
||||||
---@field src string
|
---@field src string
|
||||||
|
---@field on_done? fun(convert: snacks.image.Convert)
|
||||||
|
|
||||||
---@class snacks.image.meta
|
---@class snacks.image.meta
|
||||||
---@field src string
|
---@field src string
|
||||||
|
@ -36,7 +37,7 @@ local uv = vim.uv or vim.loop
|
||||||
---@field proc? snacks.spawn.Proc
|
---@field proc? snacks.spawn.Proc
|
||||||
|
|
||||||
---@class snacks.image.cmd
|
---@class snacks.image.cmd
|
||||||
---@field cmd (fun(step: snacks.image.step):(snacks.image.Proc|snacks.image.Proc[])?)|snacks.image.Proc|snacks.image.Proc[]
|
---@field cmd (fun(step: snacks.image.step):(snacks.image.Proc|snacks.image.Proc[]))|snacks.image.Proc|snacks.image.Proc[]
|
||||||
---@field ft? string
|
---@field ft? string
|
||||||
---@field file? fun(convert: snacks.image.Convert, meta: snacks.image.meta): string
|
---@field file? fun(convert: snacks.image.Convert, meta: snacks.image.meta): string
|
||||||
---@field depends? string[]
|
---@field depends? string[]
|
||||||
|
@ -62,14 +63,6 @@ local commands = {
|
||||||
return M.is_uri(src) and convert:tmpfile("data") or src
|
return M.is_uri(src) and convert:tmpfile("data") or src
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
cache = {
|
|
||||||
file = function(convert, ctx)
|
|
||||||
return convert:tmpfile(convert:ft(ctx.src))
|
|
||||||
end,
|
|
||||||
cmd = function(step)
|
|
||||||
uv.fs_copyfile(step.meta.src, step.file)
|
|
||||||
end,
|
|
||||||
},
|
|
||||||
typ = {
|
typ = {
|
||||||
ft = "pdf",
|
ft = "pdf",
|
||||||
cmd = {
|
cmd = {
|
||||||
|
@ -98,13 +91,13 @@ local commands = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
on_done = function(step)
|
on_done = function(step)
|
||||||
local pdf = assert(step.meta.pdf, "No pdf file")
|
local pdf = assert(step.meta.pdf, "No pdf file") --[[@as string]]
|
||||||
if uv.fs_stat(pdf) then
|
if uv.fs_stat(pdf) then
|
||||||
uv.fs_rename(pdf, step.file)
|
uv.fs_rename(pdf, step.file)
|
||||||
end
|
end
|
||||||
end,
|
end,
|
||||||
on_error = function(step)
|
on_error = function(step)
|
||||||
local pdf = assert(step.meta.pdf, "No pdf file")
|
local pdf = assert(step.meta.pdf, "No pdf file") --[[@as string]]
|
||||||
if step.meta.pdf and vim.fn.getfsize(pdf) > 0 then
|
if step.meta.pdf and vim.fn.getfsize(pdf) > 0 then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
@ -191,6 +184,40 @@ local commands = {
|
||||||
}
|
}
|
||||||
|
|
||||||
local have = {} ---@type table<string, boolean>
|
local have = {} ---@type table<string, boolean>
|
||||||
|
local proc_queue = {} ---@type snacks.spawn.Proc[]
|
||||||
|
local proc_running = 0 ---@type number
|
||||||
|
local MAX_PROCS = 3
|
||||||
|
|
||||||
|
---@param proc? snacks.spawn.Proc
|
||||||
|
local function schedule(proc)
|
||||||
|
if proc then
|
||||||
|
table.insert(proc_queue, proc)
|
||||||
|
else
|
||||||
|
proc_running = proc_running - 1
|
||||||
|
end
|
||||||
|
-- Snacks.notify("proc_running: " .. proc_running .. "\nproc_queue: " .. #proc_queue, { id = "proc_running" })
|
||||||
|
if proc_running < MAX_PROCS and #proc_queue > 0 then
|
||||||
|
proc_running = proc_running + 1
|
||||||
|
proc = table.remove(proc_queue, 1)
|
||||||
|
proc:run()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param step snacks.image.step
|
||||||
|
local function get_cmd(step)
|
||||||
|
local cmd = step.cmd.cmd
|
||||||
|
cmd = type(cmd) == "function" and cmd(step) or cmd
|
||||||
|
local cmds = cmd.cmd and { cmd } or cmd
|
||||||
|
---@cast cmds snacks.image.Proc[]
|
||||||
|
for _, c in ipairs(cmds) do
|
||||||
|
if have[c.cmd] == nil then
|
||||||
|
have[c.cmd] = vim.fn.executable(c.cmd) == 1
|
||||||
|
end
|
||||||
|
if have[c.cmd] then
|
||||||
|
return c
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@class snacks.image.Convert
|
---@class snacks.image.Convert
|
||||||
---@field opts snacks.image.convert.Opts
|
---@field opts snacks.image.convert.Opts
|
||||||
|
@ -201,6 +228,7 @@ local have = {} ---@type table<string, boolean>
|
||||||
---@field steps snacks.image.step[]
|
---@field steps snacks.image.step[]
|
||||||
---@field _done? boolean
|
---@field _done? boolean
|
||||||
---@field _err? string
|
---@field _err? string
|
||||||
|
---@field _step number
|
||||||
---@field tpl_data table<string, string>
|
---@field tpl_data table<string, string>
|
||||||
local Convert = {}
|
local Convert = {}
|
||||||
Convert.__index = Convert
|
Convert.__index = Convert
|
||||||
|
@ -212,6 +240,7 @@ function Convert.new(opts)
|
||||||
opts.src = M.norm(opts.src)
|
opts.src = M.norm(opts.src)
|
||||||
self.opts = opts
|
self.opts = opts
|
||||||
self.src = opts.src
|
self.src = opts.src
|
||||||
|
self._step = 0
|
||||||
local base = vim.fn.fnamemodify(opts.src, ":t:r")
|
local base = vim.fn.fnamemodify(opts.src, ":t:r")
|
||||||
if M.is_uri(self.opts.src) then
|
if M.is_uri(self.opts.src) then
|
||||||
base = self.opts.src:gsub("%?.*", ""):match("^%w%w+://(.*)$") or base
|
base = self.opts.src:gsub("%?.*", ""):match("^%w%w+://(.*)$") or base
|
||||||
|
@ -228,12 +257,9 @@ function Convert.new(opts)
|
||||||
return self
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@return snacks.image.step?
|
||||||
function Convert:current()
|
function Convert:current()
|
||||||
for _, step in ipairs(self.steps) do
|
return self.steps[self._step]
|
||||||
if not step.done then
|
|
||||||
return step
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Convert:ready()
|
function Convert:ready()
|
||||||
|
@ -298,113 +324,126 @@ function Convert:resolve()
|
||||||
self.file = self.meta.src
|
self.file = self.meta.src
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param cb fun(convert: snacks.image.Convert)
|
---@param err? string
|
||||||
function Convert:run(cb)
|
function Convert:on_step(err)
|
||||||
if #self.steps == 0 then
|
local step = assert(self:current(), "No current step")
|
||||||
self._done = true
|
step.done = true
|
||||||
return cb(self)
|
step.err = err
|
||||||
|
if self.aborted then
|
||||||
|
return self:on_done()
|
||||||
|
end
|
||||||
|
if step and err and step.cmd.on_error and step.cmd.on_error(step) then
|
||||||
|
-- keep going
|
||||||
|
elseif err then
|
||||||
|
self._err = err
|
||||||
|
return self:on_done()
|
||||||
|
end
|
||||||
|
if step and step.cmd.on_done then
|
||||||
|
step.cmd.on_done(step)
|
||||||
end
|
end
|
||||||
|
|
||||||
local s = 0
|
if self._step < #self.steps then
|
||||||
local next ---@type fun()
|
self:step()
|
||||||
|
else
|
||||||
|
self:on_done()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@param step? snacks.image.step
|
-- Called when all steps are done or when an error occurs
|
||||||
---@param err? string
|
function Convert:on_done()
|
||||||
local function done(step, err)
|
local step = self:current()
|
||||||
if step then
|
self._done = true
|
||||||
step.done = true
|
if self._err and Snacks.image.config.convert.notify then
|
||||||
step.err = err
|
local title = step and ("Conversion failed at step `%s`"):format(step.name) or "Conversion failed"
|
||||||
|
if step and step.proc then
|
||||||
|
step.proc:debug({ title = title })
|
||||||
|
else
|
||||||
|
Snacks.notify.error("# " .. title .. "\n" .. self._err, { title = "Snacks Image" })
|
||||||
end
|
end
|
||||||
if step and err and step.cmd.on_error and step.cmd.on_error(step) then
|
end
|
||||||
-- keep going
|
if self.opts.on_done then
|
||||||
elseif err then
|
self.opts.on_done(self)
|
||||||
if Snacks.image.config.convert.notify then
|
end
|
||||||
local title = step and ("Conversion failed at step `%s`"):format(step.name) or "Conversion failed"
|
end
|
||||||
if step and step.proc then
|
|
||||||
step.proc:debug({ title = title })
|
function Convert:abort()
|
||||||
else
|
if self.aborted then
|
||||||
Snacks.notify.error("# " .. title .. "\n" .. err, { title = "Snacks Image" })
|
return
|
||||||
end
|
end
|
||||||
end
|
if self:done() then
|
||||||
self._err = err
|
return
|
||||||
self._done = true
|
end
|
||||||
return cb(self)
|
self.aborted = true
|
||||||
|
self._err = "Aborted"
|
||||||
|
for _, step in ipairs(self.steps) do
|
||||||
|
if step.proc then
|
||||||
|
step.proc:kill()
|
||||||
end
|
end
|
||||||
if step and step.cmd.on_done then
|
end
|
||||||
step.cmd.on_done(step)
|
end
|
||||||
|
|
||||||
|
function Convert:step()
|
||||||
|
self._step = self._step + 1
|
||||||
|
assert(self._step <= #self.steps, "No more steps")
|
||||||
|
|
||||||
|
local step = self.steps[self._step]
|
||||||
|
step.done = step.done or (uv.fs_stat(step.file) ~= nil)
|
||||||
|
if step.done then
|
||||||
|
return self:on_step()
|
||||||
|
end
|
||||||
|
|
||||||
|
local cmd = get_cmd(step)
|
||||||
|
if not cmd then
|
||||||
|
return self:on_step("No command available")
|
||||||
|
end
|
||||||
|
|
||||||
|
local args = type(cmd.args) == "function" and cmd.args() or cmd.args
|
||||||
|
---@cast args (number|string)[]
|
||||||
|
args = vim.deepcopy(args)
|
||||||
|
|
||||||
|
local data = vim.tbl_extend("keep", {
|
||||||
|
file = step.file,
|
||||||
|
basename = vim.fs.basename(step.file),
|
||||||
|
name = vim.fn.fnamemodify(step.file, ":t:r"),
|
||||||
|
dirname = vim.fs.dirname(step.meta.src),
|
||||||
|
src = step.meta.src,
|
||||||
|
}, self.tpl_data)
|
||||||
|
|
||||||
|
for a, arg in ipairs(args) do
|
||||||
|
if type(arg) == "string" then
|
||||||
|
args[a] = Snacks.picker.util.tpl(arg, data)
|
||||||
end
|
end
|
||||||
if s == #self.steps then
|
end
|
||||||
self._done = true
|
|
||||||
return cb(self)
|
step.proc = Spawn.new({
|
||||||
end
|
run = false,
|
||||||
next()
|
debug = Snacks.image.config.debug.convert,
|
||||||
|
cwd = cmd.cwd and Snacks.picker.util.tpl(cmd.cwd, data) or nil,
|
||||||
|
cmd = cmd.cmd,
|
||||||
|
args = args,
|
||||||
|
on_exit = function(proc, err)
|
||||||
|
schedule()
|
||||||
|
local out = vim.trim(proc:out() .. "\n" .. proc:err())
|
||||||
|
vim.schedule(function()
|
||||||
|
self:on_step(err and out or nil)
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
schedule(step.proc)
|
||||||
|
end
|
||||||
|
|
||||||
|
function Convert:run()
|
||||||
|
if #self.steps == 0 then
|
||||||
|
return self:on_done()
|
||||||
end
|
end
|
||||||
|
|
||||||
if not M.is_uri(self.src) and vim.fn.filereadable(self.src) == 0 then
|
if not M.is_uri(self.src) and vim.fn.filereadable(self.src) == 0 then
|
||||||
local f = M.is_uri(self.src) and self.src or vim.fn.fnamemodify(self.src, ":p:~")
|
local f = M.is_uri(self.src) and self.src or vim.fn.fnamemodify(self.src, ":p:~")
|
||||||
done(nil, ("File not found\n- `%s`"):format(f))
|
self._err = ("File not found\n- `%s`"):format(f)
|
||||||
return
|
return self:on_done()
|
||||||
end
|
end
|
||||||
|
|
||||||
next = function()
|
self:step()
|
||||||
s = s + 1
|
|
||||||
assert(s <= #self.steps, "No more steps")
|
|
||||||
local step = self.steps[s]
|
|
||||||
step.done = step.done or (uv.fs_stat(step.file) ~= nil)
|
|
||||||
if step.done then
|
|
||||||
return done(step)
|
|
||||||
end
|
|
||||||
|
|
||||||
local cmd = step.cmd.cmd
|
|
||||||
if type(cmd) == "function" then
|
|
||||||
local ok, c = pcall(cmd, step)
|
|
||||||
if ok and c then
|
|
||||||
cmd = c
|
|
||||||
else
|
|
||||||
return done(step, not ok and (c or "error") or nil)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local cmds = cmd.cmd and { cmd } or cmd
|
|
||||||
---@cast cmds snacks.image.Proc[]
|
|
||||||
for _, c in ipairs(cmds) do
|
|
||||||
if have[c.cmd] == nil then
|
|
||||||
have[c.cmd] = vim.fn.executable(c.cmd) == 1
|
|
||||||
end
|
|
||||||
if have[c.cmd] then
|
|
||||||
local args = type(c.args) == "function" and c.args() or c.args
|
|
||||||
---@cast args (number|string)[]
|
|
||||||
args = vim.deepcopy(args)
|
|
||||||
local data = vim.tbl_extend("keep", {
|
|
||||||
file = step.file,
|
|
||||||
basename = vim.fs.basename(step.file),
|
|
||||||
name = vim.fn.fnamemodify(step.file, ":t:r"),
|
|
||||||
dirname = vim.fs.dirname(step.meta.src),
|
|
||||||
src = step.meta.src,
|
|
||||||
}, self.tpl_data)
|
|
||||||
for a, arg in ipairs(args) do
|
|
||||||
if type(arg) == "string" then
|
|
||||||
args[a] = Snacks.picker.util.tpl(arg, data)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
step.proc = Spawn.new({
|
|
||||||
debug = Snacks.image.config.debug.convert,
|
|
||||||
cwd = c.cwd and Snacks.picker.util.tpl(c.cwd, data) or nil,
|
|
||||||
cmd = c.cmd,
|
|
||||||
args = args,
|
|
||||||
on_exit = function(proc, err)
|
|
||||||
local out = vim.trim(proc:out() .. "\n" .. proc:err())
|
|
||||||
vim.schedule(function()
|
|
||||||
done(step, err and out or nil)
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return done(step, "No command available")
|
|
||||||
end
|
|
||||||
next()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param src string
|
---@param src string
|
||||||
|
|
|
@ -3,6 +3,7 @@ local M = {}
|
||||||
|
|
||||||
---@alias TSMatch {node:TSNode, meta:vim.treesitter.query.TSMetadata}
|
---@alias TSMatch {node:TSNode, meta:vim.treesitter.query.TSMetadata}
|
||||||
---@alias snacks.image.transform fun(match: snacks.image.match, ctx: snacks.image.ctx)
|
---@alias snacks.image.transform fun(match: snacks.image.match, ctx: snacks.image.ctx)
|
||||||
|
---@alias snacks.image.find fun(matches: snacks.image.match[])
|
||||||
|
|
||||||
---@class snacks.image.Hover
|
---@class snacks.image.Hover
|
||||||
---@field img snacks.image.Placement
|
---@field img snacks.image.Placement
|
||||||
|
@ -24,9 +25,12 @@ local M = {}
|
||||||
---@field content? string
|
---@field content? string
|
||||||
---@field ext? string
|
---@field ext? string
|
||||||
---@field range? Range4
|
---@field range? Range4
|
||||||
|
---@field lang string
|
||||||
|
---@field type snacks.image.Type
|
||||||
|
|
||||||
local META_EXT = "image.ext"
|
local META_EXT = "image.ext"
|
||||||
local META_SRC = "image.src"
|
local META_SRC = "image.src"
|
||||||
|
local META_TYPE = "image.type"
|
||||||
local META_IGNORE = "image.ignore"
|
local META_IGNORE = "image.ignore"
|
||||||
local META_LANG = "image.lang"
|
local META_LANG = "image.lang"
|
||||||
|
|
||||||
|
@ -48,11 +52,10 @@ M.transforms = {
|
||||||
}, { indent = true, prefix = "$" })
|
}, { indent = true, prefix = "$" })
|
||||||
end,
|
end,
|
||||||
latex = function(img, ctx)
|
latex = function(img, ctx)
|
||||||
if not img.content then
|
if not (img.content and img.ext == "math.tex") then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local fg = Snacks.util.color("SnacksImageMath") or "#000000"
|
local fg = Snacks.util.color("SnacksImageMath") or "#000000"
|
||||||
img.ext = "math.tex"
|
|
||||||
local content = vim.trim(img.content or "")
|
local content = vim.trim(img.content or "")
|
||||||
content = content:gsub("^%$+`?", ""):gsub("`?%$+$", "")
|
content = content:gsub("^%$+`?", ""):gsub("`?%$+$", "")
|
||||||
content = content:gsub("^\\[%[%(]", ""):gsub("\\[%]%)]$", "")
|
content = content:gsub("^\\[%[%(]", ""):gsub("\\[%]%)]$", "")
|
||||||
|
@ -61,16 +64,16 @@ M.transforms = {
|
||||||
end
|
end
|
||||||
local packages = { "xcolor" }
|
local packages = { "xcolor" }
|
||||||
vim.list_extend(packages, Snacks.image.config.math.latex.packages)
|
vim.list_extend(packages, Snacks.image.config.math.latex.packages)
|
||||||
for _, line in ipairs(vim.api.nvim_buf_get_lines(ctx.buf, 0, -1, false)) do
|
vim.list_extend(packages, M.get_packages(ctx.buf))
|
||||||
if line:find("\\usepackage") then
|
|
||||||
for _, p in ipairs(vim.split(line:match("{(.-)}") or "", ",%s*")) do
|
|
||||||
if not vim.tbl_contains(packages, p) then
|
|
||||||
packages[#packages + 1] = p
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
table.sort(packages)
|
table.sort(packages)
|
||||||
|
local seen = {} ---@type table<string, boolean>
|
||||||
|
packages = vim.tbl_filter(function(p)
|
||||||
|
if seen[p] then
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
seen[p] = true
|
||||||
|
return true
|
||||||
|
end, packages)
|
||||||
img.content = Snacks.picker.util.tpl(Snacks.image.config.math.latex.tpl, {
|
img.content = Snacks.picker.util.tpl(Snacks.image.config.math.latex.tpl, {
|
||||||
font_size = Snacks.image.config.math.latex.font_size or "large",
|
font_size = Snacks.image.config.math.latex.font_size or "large",
|
||||||
packages = table.concat(packages, ", "),
|
packages = table.concat(packages, ", "),
|
||||||
|
@ -87,21 +90,58 @@ M.TS_ASYNC = (vim.treesitter.languagetree or {})._async_parse ~= nil
|
||||||
local hover ---@type snacks.image.Hover?
|
local hover ---@type snacks.image.Hover?
|
||||||
local uv = vim.uv or vim.loop
|
local uv = vim.uv or vim.loop
|
||||||
local dir_cache = {} ---@type table<string, boolean>
|
local dir_cache = {} ---@type table<string, boolean>
|
||||||
|
local buf_cache = {} ---@type table<number,{tick: number, [string]:any}>
|
||||||
|
|
||||||
|
---@param buf number
|
||||||
|
---@param key string
|
||||||
|
---@param fn fun():any
|
||||||
|
function M._cache(buf, key, fn)
|
||||||
|
if buf_cache[buf] and buf_cache[buf].tick ~= vim.api.nvim_buf_get_changedtick(buf) then
|
||||||
|
buf_cache[buf] = nil
|
||||||
|
end
|
||||||
|
buf_cache[buf] = buf_cache[buf] or { tick = vim.api.nvim_buf_get_changedtick(buf) }
|
||||||
|
if buf_cache[buf][key] == nil then
|
||||||
|
buf_cache[buf][key] = fn()
|
||||||
|
end
|
||||||
|
return buf_cache[buf][key]
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf number
|
||||||
|
function M.get_packages(buf)
|
||||||
|
if vim.bo[buf].filetype ~= "tex" then
|
||||||
|
return {}
|
||||||
|
end
|
||||||
|
return M._cache(buf, "packages", function()
|
||||||
|
local ret = {} ---@type string[]
|
||||||
|
for _, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
|
||||||
|
if line:find("\\usepackage", 1, true) then
|
||||||
|
for _, p in ipairs(vim.split(line:match("{(.-)}") or "", ",%s*")) do
|
||||||
|
if not vim.tbl_contains(ret, p) then
|
||||||
|
ret[#ret + 1] = p
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
---@param buf number
|
---@param buf number
|
||||||
function M.get_header(buf)
|
function M.get_header(buf)
|
||||||
local header = {} ---@type string[]
|
return M._cache(buf, "header", function()
|
||||||
local in_header = false
|
local header = {} ---@type string[]
|
||||||
for _, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
|
local in_header = false
|
||||||
if line:find("snacks:%s*header%s*start") then
|
for _, line in ipairs(vim.api.nvim_buf_get_lines(buf, 0, -1, false)) do
|
||||||
in_header = true
|
if line:find("snacks:%s*header%s*start") then
|
||||||
elseif line:find("snacks:%s*header%s*end") then
|
in_header = true
|
||||||
in_header = false
|
elseif line:find("snacks:%s*header%s*end") then
|
||||||
elseif in_header then
|
in_header = false
|
||||||
header[#header + 1] = line
|
elseif in_header then
|
||||||
|
header[#header + 1] = line
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
return table.concat(header, "\n")
|
||||||
return table.concat(header, "\n")
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param str string
|
---@param str string
|
||||||
|
@ -152,13 +192,35 @@ function M.resolve(buf, src)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param buf number
|
---@param buf number
|
||||||
---@param from? number
|
---@param cb snacks.image.find
|
||||||
---@param to? number
|
function M.find_visible(buf, cb)
|
||||||
function M.find(buf, from, to)
|
local ret = {} ---@type table<string,snacks.image.match>
|
||||||
|
local wins = vim.fn.win_findbuf(buf)
|
||||||
|
local count = #wins
|
||||||
|
for _, win in ipairs(wins) do
|
||||||
|
local info = vim.fn.getwininfo(win)[1]
|
||||||
|
M.find(buf, function(mathes)
|
||||||
|
for _, i in ipairs(mathes) do
|
||||||
|
ret[i.id] = i
|
||||||
|
end
|
||||||
|
count = count - 1
|
||||||
|
if count == 0 and cb then
|
||||||
|
cb(vim.tbl_values(ret))
|
||||||
|
end
|
||||||
|
end, { from = math.max(info.topline - 1, 1), to = info.botline })
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param buf number
|
||||||
|
---@param cb snacks.image.find
|
||||||
|
---@param opts? {from?: number, to?: number}
|
||||||
|
function M.find(buf, cb, opts)
|
||||||
local ok, parser = pcall(vim.treesitter.get_parser, buf)
|
local ok, parser = pcall(vim.treesitter.get_parser, buf)
|
||||||
if not ok or not parser then
|
if not ok or not parser then
|
||||||
return {}
|
return cb({})
|
||||||
end
|
end
|
||||||
|
opts = opts or {}
|
||||||
|
local from, to = opts.from, opts.to
|
||||||
parser:parse(from and to and { from, to } or true)
|
parser:parse(from and to and { from, to } or true)
|
||||||
local ret = {} ---@type snacks.image.match[]
|
local ret = {} ---@type snacks.image.match[]
|
||||||
parser:for_each_tree(function(tstree, tree)
|
parser:for_each_tree(function(tstree, tree)
|
||||||
|
@ -169,7 +231,7 @@ function M.find(buf, from, to)
|
||||||
if not query then
|
if not query then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
for _, match, meta in query:iter_matches(tstree:root(), buf, from and from - 1 or nil, to and to - 1 or nil) do
|
for _, match, meta in query:iter_matches(tstree:root(), buf, from and from - 1 or nil, to) do
|
||||||
if not meta[META_IGNORE] then
|
if not meta[META_IGNORE] then
|
||||||
---@type snacks.image.ctx
|
---@type snacks.image.ctx
|
||||||
local ctx = {
|
local ctx = {
|
||||||
|
@ -190,7 +252,7 @@ function M.find(buf, from, to)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
return ret
|
cb(ret)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param ctx snacks.image.ctx
|
---@param ctx snacks.image.ctx
|
||||||
|
@ -198,23 +260,31 @@ function M._img(ctx)
|
||||||
ctx.pos = ctx.pos or ctx.src or ctx.content
|
ctx.pos = ctx.pos or ctx.src or ctx.content
|
||||||
assert(ctx.pos, "no image node")
|
assert(ctx.pos, "no image node")
|
||||||
|
|
||||||
local range = vim.treesitter.get_range(ctx.pos.node, ctx.buf, ctx.pos.meta)
|
local range6 = vim.treesitter.get_range(ctx.pos.node, ctx.buf, ctx.pos.meta)
|
||||||
local lines = vim.api.nvim_buf_get_lines(ctx.buf, range[1], range[4] + 1, false)
|
local range = { range6[1], range6[2], range6[4], range6[5] } ---@type Range4
|
||||||
while #lines > 0 and vim.trim(lines[#lines]) == "" do
|
if range[3] > 0 and range[4] == 0 then
|
||||||
table.remove(lines)
|
range[3] = range[3] - 1
|
||||||
|
local line = vim.api.nvim_buf_get_lines(ctx.buf, range[3], range[3] + 1, false)[1]
|
||||||
|
range[4] = #line
|
||||||
end
|
end
|
||||||
---@type snacks.image.match
|
---@type snacks.image.match
|
||||||
local img = {
|
local img = {
|
||||||
ext = ctx.meta[META_EXT],
|
ext = ctx.meta[META_EXT],
|
||||||
src = ctx.meta[META_SRC],
|
src = ctx.meta[META_SRC],
|
||||||
|
lang = ctx.lang,
|
||||||
id = ctx.pos.node:id(),
|
id = ctx.pos.node:id(),
|
||||||
range = { range[1] + 1, range[2], range[4] + 1, range[5] },
|
range = { range[1] + 1, range[2], range[3] + 1, range[4] },
|
||||||
pos = {
|
pos = { range[1] + 1, range[2] },
|
||||||
range[1] + #lines,
|
type = "image",
|
||||||
math.min(range[2], range[5]),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
img.pos[1] = math.min(img.pos[1], vim.api.nvim_buf_line_count(ctx.buf))
|
if ctx.meta[META_TYPE] then
|
||||||
|
img.type = ctx.meta[META_TYPE]
|
||||||
|
elseif img.ext then
|
||||||
|
img.type = img.ext:match("^(%w+)%.") or img.type
|
||||||
|
end
|
||||||
|
if not Snacks.image.config.math.enabled and img.type == "math" then
|
||||||
|
return
|
||||||
|
end
|
||||||
if ctx.src then
|
if ctx.src then
|
||||||
img.src = vim.treesitter.get_node_text(ctx.src.node, ctx.buf, { metadata = ctx.src.meta })
|
img.src = vim.treesitter.get_node_text(ctx.src.node, ctx.buf, { metadata = ctx.src.meta })
|
||||||
end
|
end
|
||||||
|
@ -230,9 +300,6 @@ function M._img(ctx)
|
||||||
if img.src then
|
if img.src then
|
||||||
img.src = M.resolve(ctx.buf, img.src)
|
img.src = M.resolve(ctx.buf, img.src)
|
||||||
end
|
end
|
||||||
if not Snacks.image.config.math.enabled and img.ext and img.ext:find("math") then
|
|
||||||
return
|
|
||||||
end
|
|
||||||
if img.content and not img.src then
|
if img.content and not img.src then
|
||||||
local root = Snacks.image.config.cache
|
local root = Snacks.image.config.cache
|
||||||
vim.fn.mkdir(root, "p")
|
vim.fn.mkdir(root, "p")
|
||||||
|
@ -255,21 +322,23 @@ function M.hover_close()
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Get the image at the cursor (if any)
|
--- Get the image at the cursor (if any)
|
||||||
---@return string? image_src, snacks.image.Pos? image_pos
|
---@param cb fun(image_src?:string, image_pos?: snacks.image.Pos)
|
||||||
function M.at_cursor()
|
function M.at_cursor(cb)
|
||||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
local imgs = M.find(vim.api.nvim_get_current_buf(), cursor[1], cursor[1] + 1)
|
M.find(vim.api.nvim_get_current_buf(), function(imgs)
|
||||||
for _, img in ipairs(imgs) do
|
for _, img in ipairs(imgs) do
|
||||||
local range = img.range
|
local range = img.range
|
||||||
if range then
|
if range then
|
||||||
if
|
if
|
||||||
(range[1] == range[3] and cursor[2] >= range[2] and cursor[2] <= range[4])
|
(range[1] == range[3] and cursor[2] >= range[2] and cursor[2] <= range[4])
|
||||||
or (range[1] ~= range[3] and cursor[1] >= range[1] and cursor[1] <= range[3])
|
or (range[1] ~= range[3] and cursor[1] >= range[1] and cursor[1] <= range[3])
|
||||||
then
|
then
|
||||||
return img.src, img.pos
|
return cb(img.src, img.pos)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
cb()
|
||||||
|
end, { from = cursor[1], to = cursor[1] + 1 })
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.hover()
|
function M.hover()
|
||||||
|
@ -284,90 +353,54 @@ function M.hover()
|
||||||
M.hover_close()
|
M.hover_close()
|
||||||
end
|
end
|
||||||
|
|
||||||
local src = M.at_cursor()
|
M.at_cursor(function(src)
|
||||||
if not src then
|
if not src then
|
||||||
return M.hover_close()
|
return M.hover_close()
|
||||||
end
|
|
||||||
|
|
||||||
if hover and hover.img.img.src ~= src then
|
|
||||||
M.hover_close()
|
|
||||||
elseif hover then
|
|
||||||
hover.img:update()
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
local win = Snacks.win(Snacks.win.resolve(Snacks.image.config.doc, "snacks_image", {
|
|
||||||
show = false,
|
|
||||||
enter = false,
|
|
||||||
}))
|
|
||||||
win:open_buf()
|
|
||||||
local updated = false
|
|
||||||
local o = Snacks.config.merge({}, Snacks.image.config.doc, {
|
|
||||||
on_update_pre = function()
|
|
||||||
if hover and not updated then
|
|
||||||
updated = true
|
|
||||||
local loc = hover.img:state().loc
|
|
||||||
win.opts.width = loc.width
|
|
||||||
win.opts.height = loc.height
|
|
||||||
win:show()
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
inline = false,
|
|
||||||
})
|
|
||||||
hover = {
|
|
||||||
win = win,
|
|
||||||
buf = current_buf,
|
|
||||||
img = Snacks.image.placement.new(win.buf, src, o),
|
|
||||||
}
|
|
||||||
vim.api.nvim_create_autocmd({ "BufWritePost", "CursorMoved", "ModeChanged", "BufLeave" }, {
|
|
||||||
group = vim.api.nvim_create_augroup("snacks.image.hover", { clear = true }),
|
|
||||||
callback = function()
|
|
||||||
if not hover then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
M.hover()
|
|
||||||
if not hover then
|
|
||||||
return true
|
|
||||||
end
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
---@param buf number
|
|
||||||
function M.inline(buf)
|
|
||||||
local imgs = {} ---@type table<string, snacks.image.Placement>
|
|
||||||
return function()
|
|
||||||
local found = {} ---@type table<string, boolean>
|
|
||||||
for _, i in ipairs(M.find(buf)) do
|
|
||||||
local img = imgs[i.id] ---@type snacks.image.Placement?
|
|
||||||
if img and img.img.src ~= i.src then
|
|
||||||
img:close()
|
|
||||||
img = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
if not img then
|
|
||||||
img = Snacks.image.placement.new(
|
|
||||||
buf,
|
|
||||||
i.src,
|
|
||||||
Snacks.config.merge({}, Snacks.image.config.doc, {
|
|
||||||
pos = i.pos,
|
|
||||||
range = i.range,
|
|
||||||
inline = true,
|
|
||||||
})
|
|
||||||
)
|
|
||||||
imgs[i.id] = img
|
|
||||||
else
|
|
||||||
img:update()
|
|
||||||
end
|
|
||||||
found[i.id] = true
|
|
||||||
end
|
end
|
||||||
for nid, img in pairs(imgs) do
|
|
||||||
if not found[nid] then
|
if hover and hover.img.img.src ~= src then
|
||||||
img:close()
|
M.hover_close()
|
||||||
imgs[nid] = nil
|
elseif hover then
|
||||||
end
|
hover.img:update()
|
||||||
|
return
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
local win = Snacks.win(Snacks.win.resolve(Snacks.image.config.doc, "snacks_image", {
|
||||||
|
show = false,
|
||||||
|
enter = false,
|
||||||
|
}))
|
||||||
|
win:open_buf()
|
||||||
|
local updated = false
|
||||||
|
local o = Snacks.config.merge({}, Snacks.image.config.doc, {
|
||||||
|
on_update_pre = function()
|
||||||
|
if hover and not updated then
|
||||||
|
updated = true
|
||||||
|
local loc = hover.img:state().loc
|
||||||
|
win.opts.width = loc.width
|
||||||
|
win.opts.height = loc.height
|
||||||
|
win:show()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
inline = false,
|
||||||
|
})
|
||||||
|
hover = {
|
||||||
|
win = win,
|
||||||
|
buf = current_buf,
|
||||||
|
img = Snacks.image.placement.new(win.buf, src, o),
|
||||||
|
}
|
||||||
|
vim.api.nvim_create_autocmd({ "BufWritePost", "CursorMoved", "ModeChanged", "BufLeave" }, {
|
||||||
|
group = vim.api.nvim_create_augroup("snacks.image.hover", { clear = true }),
|
||||||
|
callback = function()
|
||||||
|
if not hover then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
M.hover()
|
||||||
|
if not hover then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param buf number
|
---@param buf number
|
||||||
|
@ -383,24 +416,17 @@ function M.attach(buf)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local group = vim.api.nvim_create_augroup("snacks.image.doc." .. buf, { clear = true })
|
|
||||||
|
|
||||||
local update = inline and M.inline(buf) or M.hover
|
|
||||||
|
|
||||||
if inline then
|
if inline then
|
||||||
vim.api.nvim_create_autocmd("BufWritePost", {
|
Snacks.image.inline.new(buf)
|
||||||
group = group,
|
|
||||||
buffer = buf,
|
|
||||||
callback = vim.schedule_wrap(update),
|
|
||||||
})
|
|
||||||
else
|
else
|
||||||
|
local group = vim.api.nvim_create_augroup("snacks.image.doc." .. buf, { clear = true })
|
||||||
vim.api.nvim_create_autocmd({ "CursorMoved" }, {
|
vim.api.nvim_create_autocmd({ "CursorMoved" }, {
|
||||||
group = group,
|
group = group,
|
||||||
buffer = buf,
|
buffer = buf,
|
||||||
callback = vim.schedule_wrap(update),
|
callback = vim.schedule_wrap(M.hover),
|
||||||
})
|
})
|
||||||
|
vim.schedule(M.hover)
|
||||||
end
|
end
|
||||||
vim.schedule(update)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
@ -4,20 +4,47 @@
|
||||||
---@field id number image id. unique per nvim instance and file
|
---@field id number image id. unique per nvim instance and file
|
||||||
---@field sent? boolean image data is sent
|
---@field sent? boolean image data is sent
|
||||||
---@field placements table<number, snacks.image.Placement> image placements
|
---@field placements table<number, snacks.image.Placement> image placements
|
||||||
---@field augroup number
|
|
||||||
---@field info? snacks.image.Info
|
---@field info? snacks.image.Info
|
||||||
---@field _convert? snacks.image.Convert
|
---@field _convert? snacks.image.Convert
|
||||||
|
---@field fsize? number
|
||||||
local M = {}
|
local M = {}
|
||||||
M.__index = M
|
M.__index = M
|
||||||
|
|
||||||
local NVIM_ID_BITS = 10
|
local NVIM_ID_BITS = 10
|
||||||
local CHUNK_SIZE = 4096
|
local CHUNK_SIZE = 4096
|
||||||
|
local MAX_FSIZE = 200 * 1024 * 1024 -- 200MB
|
||||||
local _id = 30
|
local _id = 30
|
||||||
local _pid = 0
|
local _pid = 10
|
||||||
local nvim_id = 0
|
local nvim_id = 0
|
||||||
local uv = vim.uv or vim.loop
|
local uv = vim.uv or vim.loop
|
||||||
local images = {} ---@type table<string, snacks.Image>
|
local images = {} ---@type table<string, snacks.Image>
|
||||||
local terminal = Snacks.image.terminal
|
local terminal = Snacks.image.terminal
|
||||||
|
local lru = {} ---@type {img:snacks.Image, used:number}[]
|
||||||
|
local lru_fsize = 0
|
||||||
|
|
||||||
|
---@param img snacks.Image
|
||||||
|
local function use(img)
|
||||||
|
if img.fsize == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local now = os.time()
|
||||||
|
for _, v in ipairs(lru) do
|
||||||
|
if v.img == img then
|
||||||
|
v.used = now
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
table.sort(lru, function(a, b)
|
||||||
|
return a.used > b.used
|
||||||
|
end)
|
||||||
|
while lru_fsize >= MAX_FSIZE and #lru > 0 do
|
||||||
|
local i = table.remove(lru).img
|
||||||
|
i.sent = false
|
||||||
|
lru_fsize = lru_fsize - (i.fsize or 0)
|
||||||
|
end
|
||||||
|
lru_fsize = lru_fsize + (img.fsize or 0)
|
||||||
|
table.insert(lru, { img = img, used = now })
|
||||||
|
end
|
||||||
|
|
||||||
---@param src string
|
---@param src string
|
||||||
function M.new(src)
|
function M.new(src)
|
||||||
|
@ -38,7 +65,6 @@ function M.new(src)
|
||||||
-- interleave the nvim id and the image id
|
-- interleave the nvim id and the image id
|
||||||
self.id = bit.bor(bit.lshift(nvim_id, 24 - NVIM_ID_BITS), _id)
|
self.id = bit.bor(bit.lshift(nvim_id, 24 - NVIM_ID_BITS), _id)
|
||||||
self.placements = {}
|
self.placements = {}
|
||||||
self.augroup = vim.api.nvim_create_augroup("snacks.image." .. self.id, { clear = true })
|
|
||||||
|
|
||||||
self:run()
|
self:run()
|
||||||
if self:ready() then
|
if self:ready() then
|
||||||
|
@ -50,12 +76,18 @@ end
|
||||||
|
|
||||||
function M:on_ready()
|
function M:on_ready()
|
||||||
if not self.sent then
|
if not self.sent then
|
||||||
|
self.fsize = vim.fn.getfsize(self.file)
|
||||||
self.info = self._convert and self._convert.meta.info or nil
|
self.info = self._convert and self._convert.meta.info or nil
|
||||||
|
if self.info and self.info.size then
|
||||||
|
-- ghostty uses the decoded rgba size to calculate the fsize
|
||||||
|
self.fsize = (self.info.size.width * 4 + 1) * self.info.size.height
|
||||||
|
end
|
||||||
self:send()
|
self:send()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function M:on_send()
|
function M:on_send()
|
||||||
|
use(self)
|
||||||
for _, placement in pairs(self.placements) do
|
for _, placement in pairs(self.placements) do
|
||||||
placement:update()
|
placement:update()
|
||||||
end
|
end
|
||||||
|
@ -82,23 +114,26 @@ function M:run()
|
||||||
if not self._convert then
|
if not self._convert then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
self._convert:run(function(convert)
|
self._convert:run()
|
||||||
if convert:error() then
|
|
||||||
vim.schedule(function()
|
|
||||||
for _, p in pairs(self.placements) do
|
|
||||||
p:error()
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
else
|
|
||||||
vim.schedule(function()
|
|
||||||
self:on_ready()
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M:convert()
|
function M:convert()
|
||||||
self._convert = Snacks.image.convert.convert({ src = self.src })
|
self._convert = Snacks.image.convert.convert({
|
||||||
|
src = self.src,
|
||||||
|
on_done = function(convert)
|
||||||
|
if convert:error() then
|
||||||
|
vim.schedule(function()
|
||||||
|
for _, p in pairs(self.placements) do
|
||||||
|
p:error()
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
vim.schedule(function()
|
||||||
|
self:on_ready()
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
return self._convert.file
|
return self._convert.file
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -148,15 +183,16 @@ end
|
||||||
|
|
||||||
---@param placement snacks.image.Placement
|
---@param placement snacks.image.Placement
|
||||||
function M:place(placement)
|
function M:place(placement)
|
||||||
for pid, p in pairs(self.placements) do
|
if not placement.id then
|
||||||
if p == placement then
|
_pid = _pid + 1
|
||||||
placement.id = pid
|
placement.id = _pid
|
||||||
return pid
|
end
|
||||||
end
|
self.placements[placement.id] = placement
|
||||||
|
if self.sent then
|
||||||
|
use(self)
|
||||||
|
elseif self:ready() then
|
||||||
|
self:send()
|
||||||
end
|
end
|
||||||
_pid = _pid + 1
|
|
||||||
placement.id = _pid
|
|
||||||
self.placements[_pid] = placement
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param pid? number
|
---@param pid? number
|
||||||
|
@ -170,8 +206,6 @@ function M:del(pid)
|
||||||
|
|
||||||
if not next(self.placements) then
|
if not next(self.placements) then
|
||||||
terminal.request({ a = "d", d = "i", i = self.id })
|
terminal.request({ a = "d", d = "i", i = self.id })
|
||||||
self.sent = false
|
|
||||||
pcall(vim.api.nvim_del_autocmd_by_id, self.augroup)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -6,10 +6,11 @@
|
||||||
---@field buf snacks.image.buf
|
---@field buf snacks.image.buf
|
||||||
---@field doc snacks.image.doc
|
---@field doc snacks.image.doc
|
||||||
---@field convert snacks.image.convert
|
---@field convert snacks.image.convert
|
||||||
|
---@field inline snacks.image.inline
|
||||||
local M = setmetatable({}, {
|
local M = setmetatable({}, {
|
||||||
---@param M snacks.image
|
---@param M snacks.image
|
||||||
__index = function(M, k)
|
__index = function(M, k)
|
||||||
if vim.tbl_contains({ "terminal", "image", "placement", "util", "doc", "buf", "convert" }, k) then
|
if vim.tbl_contains({ "terminal", "image", "placement", "util", "doc", "buf", "convert", "inline" }, k) then
|
||||||
M[k] = require("snacks.image." .. k)
|
M[k] = require("snacks.image." .. k)
|
||||||
end
|
end
|
||||||
return rawget(M, k)
|
return rawget(M, k)
|
||||||
|
@ -24,6 +25,7 @@ M.meta = {
|
||||||
---@alias snacks.image.Size {width: number, height: number}
|
---@alias snacks.image.Size {width: number, height: number}
|
||||||
---@alias snacks.image.Pos {[1]: number, [2]: number}
|
---@alias snacks.image.Pos {[1]: number, [2]: number}
|
||||||
---@alias snacks.image.Loc snacks.image.Pos|snacks.image.Size|{zindex?: number}
|
---@alias snacks.image.Loc snacks.image.Pos|snacks.image.Size|{zindex?: number}
|
||||||
|
---@alias snacks.image.Type "image"|"math"|"chart"
|
||||||
|
|
||||||
---@class snacks.image.Env
|
---@class snacks.image.Env
|
||||||
---@field name string
|
---@field name string
|
||||||
|
@ -78,7 +80,13 @@ local defaults = {
|
||||||
max_width = 80,
|
max_width = 80,
|
||||||
max_height = 40,
|
max_height = 40,
|
||||||
-- Set to `true`, to conceal the image text when rendering inline.
|
-- Set to `true`, to conceal the image text when rendering inline.
|
||||||
conceal = false, -- (experimental)
|
-- (experimental)
|
||||||
|
---@param lang string tree-sitter language
|
||||||
|
---@param type snacks.image.Type image type
|
||||||
|
conceal = function(lang, type)
|
||||||
|
-- only conceal math expressions
|
||||||
|
return type == "math"
|
||||||
|
end,
|
||||||
},
|
},
|
||||||
img_dirs = { "img", "images", "assets", "static", "public", "media", "attachments" },
|
img_dirs = { "img", "images", "assets", "static", "public", "media", "attachments" },
|
||||||
-- window options applied to windows displaying image buffers
|
-- window options applied to windows displaying image buffers
|
||||||
|
@ -101,6 +109,13 @@ local defaults = {
|
||||||
placement = false,
|
placement = false,
|
||||||
},
|
},
|
||||||
env = {},
|
env = {},
|
||||||
|
-- icons used to show where an inline image is located that is
|
||||||
|
-- rendered below the text.
|
||||||
|
icons = {
|
||||||
|
math = " ",
|
||||||
|
chart = " ",
|
||||||
|
image = " ",
|
||||||
|
},
|
||||||
---@class snacks.image.convert.Config
|
---@class snacks.image.convert.Config
|
||||||
convert = {
|
convert = {
|
||||||
notify = true, -- show a notification on error
|
notify = true, -- show a notification on error
|
||||||
|
@ -137,7 +152,7 @@ local defaults = {
|
||||||
-- but you can add more packages here. Useful for markdown documents.
|
-- but you can add more packages here. Useful for markdown documents.
|
||||||
packages = { "amsmath", "amssymb", "amsfonts", "amscd", "mathtools" },
|
packages = { "amsmath", "amssymb", "amsfonts", "amscd", "mathtools" },
|
||||||
tpl = [[
|
tpl = [[
|
||||||
\documentclass[preview,border=2pt,varwidth,12pt]{standalone}
|
\documentclass[preview,border=0pt,varwidth,12pt]{standalone}
|
||||||
\usepackage{${packages}}
|
\usepackage{${packages}}
|
||||||
\begin{document}
|
\begin{document}
|
||||||
${header}
|
${header}
|
||||||
|
@ -162,6 +177,7 @@ Snacks.config.style("snacks_image", {
|
||||||
|
|
||||||
Snacks.util.set_hl({
|
Snacks.util.set_hl({
|
||||||
Spinner = "Special",
|
Spinner = "Special",
|
||||||
|
Anchor = "Special",
|
||||||
Loading = "NonText",
|
Loading = "NonText",
|
||||||
Math = { fg = Snacks.util.color({ "@markup.math.latex", "Special", "Normal" }) },
|
Math = { fg = Snacks.util.color({ "@markup.math.latex", "Special", "Normal" }) },
|
||||||
}, { prefix = "SnacksImage", default = true })
|
}, { prefix = "SnacksImage", default = true })
|
||||||
|
@ -169,6 +185,7 @@ Snacks.util.set_hl({
|
||||||
---@class snacks.image.Opts
|
---@class snacks.image.Opts
|
||||||
---@field pos? snacks.image.Pos (row, col) (1,0)-indexed. defaults to the top-left corner
|
---@field pos? snacks.image.Pos (row, col) (1,0)-indexed. defaults to the top-left corner
|
||||||
---@field range? Range4
|
---@field range? Range4
|
||||||
|
---@field conceal? boolean
|
||||||
---@field inline? boolean render the image inline in the buffer
|
---@field inline? boolean render the image inline in the buffer
|
||||||
---@field width? number
|
---@field width? number
|
||||||
---@field min_width? number
|
---@field min_width? number
|
||||||
|
@ -178,6 +195,8 @@ Snacks.util.set_hl({
|
||||||
---@field max_height? number
|
---@field max_height? number
|
||||||
---@field on_update? fun(placement: snacks.image.Placement)
|
---@field on_update? fun(placement: snacks.image.Placement)
|
||||||
---@field on_update_pre? fun(placement: snacks.image.Placement)
|
---@field on_update_pre? fun(placement: snacks.image.Placement)
|
||||||
|
---@field type? snacks.image.Type
|
||||||
|
---@field auto_resize? boolean
|
||||||
|
|
||||||
local did_setup = false
|
local did_setup = false
|
||||||
|
|
||||||
|
@ -220,6 +239,22 @@ function M.setup(ev)
|
||||||
did_setup = true
|
did_setup = true
|
||||||
local group = vim.api.nvim_create_augroup("snacks.image", { clear = true })
|
local group = vim.api.nvim_create_augroup("snacks.image", { clear = true })
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
|
||||||
|
group = group,
|
||||||
|
callback = function(e)
|
||||||
|
vim.schedule(function()
|
||||||
|
Snacks.image.placement.clean(e.buf)
|
||||||
|
end)
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.api.nvim_create_autocmd({ "ExitPre" }, {
|
||||||
|
group = group,
|
||||||
|
once = true,
|
||||||
|
callback = function()
|
||||||
|
Snacks.image.placement.clean()
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
|
||||||
if M.config.formats and #M.config.formats > 0 then
|
if M.config.formats and #M.config.formats > 0 then
|
||||||
vim.api.nvim_create_autocmd("BufReadCmd", {
|
vim.api.nvim_create_autocmd("BufReadCmd", {
|
||||||
pattern = "*." .. table.concat(M.config.formats, ",*."),
|
pattern = "*." .. table.concat(M.config.formats, ",*."),
|
||||||
|
|
146
lua/snacks/image/inline.lua
Normal file
146
lua/snacks/image/inline.lua
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
---@class snacks.image.inline
|
||||||
|
---@field buf number
|
||||||
|
---@field imgs table<number, snacks.image.Placement>
|
||||||
|
---@field idx table<number, snacks.image.Placement>
|
||||||
|
local M = {}
|
||||||
|
M.__index = M
|
||||||
|
|
||||||
|
function M.new(buf)
|
||||||
|
local self = setmetatable({}, M)
|
||||||
|
self.buf = buf
|
||||||
|
self.imgs = {}
|
||||||
|
self.idx = {}
|
||||||
|
local group = vim.api.nvim_create_augroup("snacks.image.inline." .. buf, { clear = true })
|
||||||
|
|
||||||
|
local update = Snacks.util.debounce(function()
|
||||||
|
self:update()
|
||||||
|
end, { ms = 100 })
|
||||||
|
|
||||||
|
vim.api.nvim_create_autocmd({ "BufWritePost", "WinScrolled" }, {
|
||||||
|
group = group,
|
||||||
|
buffer = buf,
|
||||||
|
callback = vim.schedule_wrap(update),
|
||||||
|
})
|
||||||
|
vim.api.nvim_create_autocmd({ "ModeChanged", "CursorMoved" }, {
|
||||||
|
group = group,
|
||||||
|
buffer = buf,
|
||||||
|
callback = function(ev)
|
||||||
|
if ev.buf == self.buf and ev.buf == vim.api.nvim_get_current_buf() then
|
||||||
|
self:conceal()
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
vim.schedule(update)
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
|
||||||
|
function M:conceal()
|
||||||
|
local mode = vim.fn.mode()
|
||||||
|
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local by_mode = not mode:find("[nc]")
|
||||||
|
local hide = by_mode and self:visible() or self:get(cursor[1], cursor[1])
|
||||||
|
for _, img in pairs(self.imgs) do
|
||||||
|
img:show()
|
||||||
|
end
|
||||||
|
for _, img in pairs(hide) do
|
||||||
|
if by_mode or img.opts.conceal then
|
||||||
|
img:hide()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function M:visible()
|
||||||
|
local ret = {} ---@type table<number, snacks.image.Placement>
|
||||||
|
for _, win in ipairs(vim.fn.win_findbuf(self.buf)) do
|
||||||
|
local info = vim.fn.getwininfo(win)[1]
|
||||||
|
for k, v in pairs(self:get(math.max(info.topline - 1, 1), info.botline)) do
|
||||||
|
ret[k] = v
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param from number 1-indexed inclusive
|
||||||
|
---@param to number 1-indexed inclusive
|
||||||
|
function M:get(from, to)
|
||||||
|
local ret = {} ---@type table<number, snacks.image.Placement>
|
||||||
|
local marks = vim.api.nvim_buf_get_extmarks(self.buf, Snacks.image.placement.ns, { from - 1, 0 }, { to - 1, -1 }, {
|
||||||
|
overlap = true,
|
||||||
|
hl_name = false,
|
||||||
|
})
|
||||||
|
for _, m in ipairs(marks) do
|
||||||
|
local p = self.idx[m[1]] ---@type snacks.image.Placement?
|
||||||
|
if p and not self.imgs[p.id] then
|
||||||
|
self.idx[m[1]] = nil
|
||||||
|
p = nil
|
||||||
|
end
|
||||||
|
if p then
|
||||||
|
ret[p.id] = p
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return ret
|
||||||
|
end
|
||||||
|
|
||||||
|
function M:update()
|
||||||
|
local conceal = Snacks.image.config.doc.conceal
|
||||||
|
conceal = type(conceal) ~= "function" and function()
|
||||||
|
return conceal
|
||||||
|
end or conceal
|
||||||
|
Snacks.image.doc.find_visible(self.buf, function(imgs)
|
||||||
|
local visible = self:visible()
|
||||||
|
local stats = { new = 0, del = 0, update = 0 }
|
||||||
|
for _, i in ipairs(imgs) do
|
||||||
|
local img ---@type snacks.image.Placement?
|
||||||
|
for v, o in pairs(visible) do
|
||||||
|
if o.img.src == i.src then
|
||||||
|
img = o
|
||||||
|
visible[v] = nil
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not img then
|
||||||
|
stats.new = stats.new + 1
|
||||||
|
img = Snacks.image.placement.new(
|
||||||
|
self.buf,
|
||||||
|
i.src,
|
||||||
|
Snacks.config.merge({}, Snacks.image.config.doc, {
|
||||||
|
pos = i.pos,
|
||||||
|
range = i.range,
|
||||||
|
inline = true,
|
||||||
|
conceal = conceal(i.lang, i.type),
|
||||||
|
type = i.type,
|
||||||
|
---@param p snacks.image.Placement
|
||||||
|
on_update = function(p)
|
||||||
|
for _, eid in ipairs(p.eids) do
|
||||||
|
self.idx[eid] = p
|
||||||
|
end
|
||||||
|
end,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
for _, eid in ipairs(img.eids) do
|
||||||
|
self.idx[eid] = img
|
||||||
|
end
|
||||||
|
self.imgs[img.id] = img
|
||||||
|
else
|
||||||
|
stats.update = stats.update + 1
|
||||||
|
img.opts.pos = i.pos
|
||||||
|
img.opts.range = i.range
|
||||||
|
img:update()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
for _, img in pairs(visible) do
|
||||||
|
stats.del = stats.del + 1
|
||||||
|
img:close()
|
||||||
|
self.imgs[img.id] = nil
|
||||||
|
end
|
||||||
|
for k, v in pairs(stats) do
|
||||||
|
stats[k] = v > 0 and v or nil
|
||||||
|
end
|
||||||
|
Snacks.notify(
|
||||||
|
vim.inspect({ all = vim.tbl_count(self.imgs), stats = stats }),
|
||||||
|
{ ft = "lua", id = "snacks.image.inline" }
|
||||||
|
)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
|
@ -5,16 +5,24 @@
|
||||||
---@field buf number
|
---@field buf number
|
||||||
---@field opts snacks.image.Opts
|
---@field opts snacks.image.Opts
|
||||||
---@field augroup number
|
---@field augroup number
|
||||||
|
---@field hidden? boolean
|
||||||
---@field closed? boolean
|
---@field closed? boolean
|
||||||
---@field extmark_id? number
|
---@field type? snacks.image.Type
|
||||||
---@field _loc? snacks.image.Loc
|
---@field _loc? snacks.image.Loc
|
||||||
---@field _state? snacks.image.State
|
---@field _state? snacks.image.State
|
||||||
|
---@field eids number[]
|
||||||
|
---@field _extmarks? snacks.image.Extmark[]
|
||||||
local M = {}
|
local M = {}
|
||||||
M.__index = M
|
M.__index = M
|
||||||
|
|
||||||
|
---@alias snacks.image.Extmark vim.api.keyset.set_extmark|{row:number, col:number}
|
||||||
|
|
||||||
local terminal = Snacks.image.terminal
|
local terminal = Snacks.image.terminal
|
||||||
local uv = vim.uv or vim.loop
|
local uv = vim.uv or vim.loop
|
||||||
|
local ns = vim.api.nvim_create_namespace("snacks.image")
|
||||||
|
M.ns = ns
|
||||||
local PLACEHOLDER = vim.fn.nr2char(0x10EEEE)
|
local PLACEHOLDER = vim.fn.nr2char(0x10EEEE)
|
||||||
|
local placements = {} ---@type table<number, table<number, snacks.image.Placement>>
|
||||||
-- stylua: ignore
|
-- stylua: ignore
|
||||||
local diacritics = vim.split( "0305,030D,030E,0310,0312,033D,033E,033F,0346,034A,034B,034C,0350,0351,0352,0357,035B,0363,0364,0365,0366,0367,0368,0369,036A,036B,036C,036D,036E,036F,0483,0484,0485,0486,0487,0592,0593,0594,0595,0597,0598,0599,059C,059D,059E,059F,05A0,05A1,05A8,05A9,05AB,05AC,05AF,05C4,0610,0611,0612,0613,0614,0615,0616,0617,0657,0658,0659,065A,065B,065D,065E,06D6,06D7,06D8,06D9,06DA,06DB,06DC,06DF,06E0,06E1,06E2,06E4,06E7,06E8,06EB,06EC,0730,0732,0733,0735,0736,073A,073D,073F,0740,0741,0743,0745,0747,0749,074A,07EB,07EC,07ED,07EE,07EF,07F0,07F1,07F3,0816,0817,0818,0819,081B,081C,081D,081E,081F,0820,0821,0822,0823,0825,0826,0827,0829,082A,082B,082C,082D,0951,0953,0954,0F82,0F83,0F86,0F87,135D,135E,135F,17DD,193A,1A17,1A75,1A76,1A77,1A78,1A79,1A7A,1A7B,1A7C,1B6B,1B6D,1B6E,1B6F,1B70,1B71,1B72,1B73,1CD0,1CD1,1CD2,1CDA,1CDB,1CE0,1DC0,1DC1,1DC3,1DC4,1DC5,1DC6,1DC7,1DC8,1DC9,1DCB,1DCC,1DD1,1DD2,1DD3,1DD4,1DD5,1DD6,1DD7,1DD8,1DD9,1DDA,1DDB,1DDC,1DDD,1DDE,1DDF,1DE0,1DE1,1DE2,1DE3,1DE4,1DE5,1DE6,1DFE,20D0,20D1,20D4,20D5,20D6,20D7,20DB,20DC,20E1,20E7,20E9,20F0,2CEF,2CF0,2CF1,2DE0,2DE1,2DE2,2DE3,2DE4,2DE5,2DE6,2DE7,2DE8,2DE9,2DEA,2DEB,2DEC,2DED,2DEE,2DEF,2DF0,2DF1,2DF2,2DF3,2DF4,2DF5,2DF6,2DF7,2DF8,2DF9,2DFA,2DFB,2DFC,2DFD,2DFE,2DFF,A66F,A67C,A67D,A6F0,A6F1,A8E0,A8E1,A8E2,A8E3,A8E4,A8E5,A8E6,A8E7,A8E8,A8E9,A8EA,A8EB,A8EC,A8ED,A8EE,A8EF,A8F0,A8F1,AAB0,AAB2,AAB3,AAB7,AAB8,AABE,AABF,AAC1,FE20,FE21,FE22,FE23,FE24,FE25,FE26,10A0F,10A38,1D185,1D186,1D187,1D188,1D189,1D1AA,1D1AB,1D1AC,1D1AD,1D242,1D243,1D244", ",")
|
local diacritics = vim.split( "0305,030D,030E,0310,0312,033D,033E,033F,0346,034A,034B,034C,0350,0351,0352,0357,035B,0363,0364,0365,0366,0367,0368,0369,036A,036B,036C,036D,036E,036F,0483,0484,0485,0486,0487,0592,0593,0594,0595,0597,0598,0599,059C,059D,059E,059F,05A0,05A1,05A8,05A9,05AB,05AC,05AF,05C4,0610,0611,0612,0613,0614,0615,0616,0617,0657,0658,0659,065A,065B,065D,065E,06D6,06D7,06D8,06D9,06DA,06DB,06DC,06DF,06E0,06E1,06E2,06E4,06E7,06E8,06EB,06EC,0730,0732,0733,0735,0736,073A,073D,073F,0740,0741,0743,0745,0747,0749,074A,07EB,07EC,07ED,07EE,07EF,07F0,07F1,07F3,0816,0817,0818,0819,081B,081C,081D,081E,081F,0820,0821,0822,0823,0825,0826,0827,0829,082A,082B,082C,082D,0951,0953,0954,0F82,0F83,0F86,0F87,135D,135E,135F,17DD,193A,1A17,1A75,1A76,1A77,1A78,1A79,1A7A,1A7B,1A7C,1B6B,1B6D,1B6E,1B6F,1B70,1B71,1B72,1B73,1CD0,1CD1,1CD2,1CDA,1CDB,1CE0,1DC0,1DC1,1DC3,1DC4,1DC5,1DC6,1DC7,1DC8,1DC9,1DCB,1DCC,1DD1,1DD2,1DD3,1DD4,1DD5,1DD6,1DD7,1DD8,1DD9,1DDA,1DDB,1DDC,1DDD,1DDE,1DDF,1DE0,1DE1,1DE2,1DE3,1DE4,1DE5,1DE6,1DFE,20D0,20D1,20D4,20D5,20D6,20D7,20DB,20DC,20E1,20E7,20E9,20F0,2CEF,2CF0,2CF1,2DE0,2DE1,2DE2,2DE3,2DE4,2DE5,2DE6,2DE7,2DE8,2DE9,2DEA,2DEB,2DEC,2DED,2DEE,2DEF,2DF0,2DF1,2DF2,2DF3,2DF4,2DF5,2DF6,2DF7,2DF8,2DF9,2DFA,2DFB,2DFC,2DFD,2DFE,2DFF,A66F,A67C,A67D,A6F0,A6F1,A8E0,A8E1,A8E2,A8E3,A8E4,A8E5,A8E6,A8E7,A8E8,A8E9,A8EA,A8EB,A8EC,A8ED,A8EE,A8EF,A8F0,A8F1,AAB0,AAB2,AAB3,AAB7,AAB8,AABE,AABF,AAC1,FE20,FE21,FE22,FE23,FE24,FE25,FE26,10A0F,10A38,1D185,1D186,1D187,1D188,1D189,1D1AA,1D1AB,1D1AC,1D1AD,1D242,1D243,1D244", ",")
|
||||||
---@type table<number, string>
|
---@type table<number, string>
|
||||||
|
@ -26,6 +34,18 @@ setmetatable(positions, {
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
---@param buf? number
|
||||||
|
---@param id? number
|
||||||
|
function M.clean(buf, id)
|
||||||
|
for _, b in ipairs(buf and { buf } or vim.tbl_keys(placements)) do
|
||||||
|
for _, p in ipairs(id and { placements[b][id] } or vim.tbl_values(placements[b] or {})) do
|
||||||
|
if p then
|
||||||
|
p:close()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
---@param buf number
|
---@param buf number
|
||||||
---@param opts? snacks.image.Opts
|
---@param opts? snacks.image.Opts
|
||||||
function M.new(buf, src, opts)
|
function M.new(buf, src, opts)
|
||||||
|
@ -37,45 +57,32 @@ function M.new(buf, src, opts)
|
||||||
self.img = Snacks.image.image.new(src)
|
self.img = Snacks.image.image.new(src)
|
||||||
self.img:place(self)
|
self.img:place(self)
|
||||||
self.opts = opts or {}
|
self.opts = opts or {}
|
||||||
|
self.opts.pos = self.opts.pos or { 1, 0 }
|
||||||
self.buf = buf
|
self.buf = buf
|
||||||
self.ns = vim.api.nvim_create_namespace("snacks.image." .. self.id)
|
|
||||||
self.augroup = vim.api.nvim_create_augroup("snacks.image." .. self.id, { clear = true })
|
self.augroup = vim.api.nvim_create_augroup("snacks.image." .. self.id, { clear = true })
|
||||||
|
self.eids = {}
|
||||||
|
|
||||||
vim.api.nvim_create_autocmd({ "BufWinEnter", "WinEnter", "BufWinLeave", "BufEnter" }, {
|
if self.opts.auto_resize then
|
||||||
group = self.augroup,
|
vim.api.nvim_create_autocmd({ "BufWinEnter", "WinEnter", "BufWinLeave", "BufEnter" }, {
|
||||||
buffer = self.buf,
|
group = self.augroup,
|
||||||
callback = function()
|
buffer = self.buf,
|
||||||
vim.schedule(function()
|
callback = function()
|
||||||
self:update()
|
vim.schedule(function()
|
||||||
end)
|
self:update()
|
||||||
end,
|
end)
|
||||||
})
|
end,
|
||||||
vim.api.nvim_create_autocmd({ "WinClosed", "WinNew", "WinEnter", "WinResized" }, {
|
})
|
||||||
group = self.augroup,
|
vim.api.nvim_create_autocmd({ "WinClosed", "WinNew", "WinEnter", "WinResized" }, {
|
||||||
callback = function()
|
group = self.augroup,
|
||||||
vim.schedule(function()
|
callback = function()
|
||||||
self:update()
|
vim.schedule(function()
|
||||||
end)
|
self:update()
|
||||||
end,
|
end)
|
||||||
})
|
end,
|
||||||
|
})
|
||||||
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
|
end
|
||||||
group = self.augroup,
|
placements[self.buf] = placements[self.buf] or {}
|
||||||
buffer = self.buf,
|
placements[self.buf][self.id] = self
|
||||||
once = true,
|
|
||||||
callback = function()
|
|
||||||
vim.schedule(function()
|
|
||||||
self:close()
|
|
||||||
end)
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
vim.api.nvim_create_autocmd({ "ExitPre" }, {
|
|
||||||
group = self.augroup,
|
|
||||||
once = true,
|
|
||||||
callback = function()
|
|
||||||
self:close()
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
|
|
||||||
if self:ready() then
|
if self:ready() then
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
|
@ -83,6 +90,14 @@ function M.new(buf, src, opts)
|
||||||
end)
|
end)
|
||||||
elseif self.img:failed() then
|
elseif self.img:failed() then
|
||||||
self:error()
|
self:error()
|
||||||
|
elseif self.opts.inline then
|
||||||
|
-- temporary extmark so that we can keep track of unloaded images in the buffer
|
||||||
|
self:_render({
|
||||||
|
{
|
||||||
|
row = self.opts.pos[1] - 1,
|
||||||
|
col = self.opts.pos[2],
|
||||||
|
},
|
||||||
|
})
|
||||||
else
|
else
|
||||||
self:progress()
|
self:progress()
|
||||||
end
|
end
|
||||||
|
@ -145,8 +160,8 @@ function M:progress()
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
vim.api.nvim_buf_clear_namespace(self.buf, self.ns, 0, -1)
|
vim.api.nvim_buf_clear_namespace(self.buf, ns, 0, -1)
|
||||||
vim.api.nvim_buf_set_extmark(self.buf, self.ns, 0, 0, {
|
vim.api.nvim_buf_set_extmark(self.buf, ns, 0, 0, {
|
||||||
virt_text = {
|
virt_text = {
|
||||||
{ Snacks.util.spinner(), "SnacksImageSpinner" },
|
{ Snacks.util.spinner(), "SnacksImageSpinner" },
|
||||||
{ " " },
|
{ " " },
|
||||||
|
@ -169,12 +184,22 @@ function M:close()
|
||||||
if self.closed then
|
if self.closed then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
placements[self.buf][self.id] = nil
|
||||||
self.closed = true
|
self.closed = true
|
||||||
|
self:del()
|
||||||
self:debug("close")
|
self:debug("close")
|
||||||
self:hide()
|
|
||||||
pcall(vim.api.nvim_del_augroup_by_id, self.augroup)
|
pcall(vim.api.nvim_del_augroup_by_id, self.augroup)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function M:del()
|
||||||
|
self.img:del(self.id)
|
||||||
|
if vim.api.nvim_buf_is_valid(self.buf) then
|
||||||
|
for _, eid in ipairs(self.eids) do
|
||||||
|
vim.api.nvim_buf_del_extmark(self.buf, ns, eid)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
--- Renders the unicode placeholder grid in the buffer
|
--- Renders the unicode placeholder grid in the buffer
|
||||||
---@param loc snacks.image.Loc
|
---@param loc snacks.image.Loc
|
||||||
function M:render_grid(loc)
|
function M:render_grid(loc)
|
||||||
|
@ -183,10 +208,11 @@ function M:render_grid(loc)
|
||||||
[hl] = {
|
[hl] = {
|
||||||
fg = self.img.id,
|
fg = self.img.id,
|
||||||
sp = self.id,
|
sp = self.id,
|
||||||
bg = Snacks.image.config.debug.placement and "#FF007C" or nil,
|
bg = Snacks.image.config.debug.placement and "#FF007C" or "none",
|
||||||
|
nocombine = true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
local lines = {} ---@type string[]
|
local img = {} ---@type string[]
|
||||||
local height = math.min(#diacritics, loc.height)
|
local height = math.min(#diacritics, loc.height)
|
||||||
local width = math.min(#diacritics, loc.width)
|
local width = math.min(#diacritics, loc.width)
|
||||||
for r = 1, height do
|
for r = 1, height do
|
||||||
|
@ -197,56 +223,158 @@ function M:render_grid(loc)
|
||||||
line[#line + 1] = positions[r]
|
line[#line + 1] = positions[r]
|
||||||
line[#line + 1] = positions[c]
|
line[#line + 1] = positions[c]
|
||||||
end
|
end
|
||||||
lines[#lines + 1] = table.concat(line)
|
img[#img + 1] = table.concat(line)
|
||||||
end
|
end
|
||||||
|
|
||||||
if self.opts.inline then
|
local range = self.opts.range or { loc[1], loc[2], loc[1], loc[2] }
|
||||||
local padding = string.rep(" ", loc[2])
|
local lines = vim.api.nvim_buf_get_lines(self.buf, range[1] - 1, range[3], false)
|
||||||
vim.api.nvim_buf_clear_namespace(self.buf, self.ns, 0, -1)
|
local text_width = 0
|
||||||
local start_row, start_col = loc[1] - 1, loc[2]
|
for _, line in ipairs(lines) do
|
||||||
local end_row, end_col ---@type number?, number?
|
text_width = math.max(text_width, vim.api.nvim_strwidth(line))
|
||||||
local conceal = Snacks.image.config.doc.conceal and " " or nil
|
end
|
||||||
if self.opts.range and conceal then
|
local offset = range[2]
|
||||||
start_row, start_col = self.opts.range[1] - 1, self.opts.range[2]
|
local has_after = lines[#lines]:sub(range[4] + 1):find("%S") ~= nil
|
||||||
end_row, end_col = self.opts.range[3] - 1, self.opts.range[4]
|
local conceal = self.opts.conceal and "" or nil
|
||||||
|
local extmarks = {} ---@type snacks.image.Extmark[]
|
||||||
|
|
||||||
|
-- we can overlay the image if the text is multiline,
|
||||||
|
-- or the text has nothing after the image
|
||||||
|
-- and the text is not wrapped or the text fits the window width
|
||||||
|
local can_overlay = (#lines > 1 or not has_after)
|
||||||
|
for _, win in ipairs(can_overlay and self:wins() or {}) do
|
||||||
|
if vim.wo[win].wrap then
|
||||||
|
local info = vim.fn.getwininfo(win)[1]
|
||||||
|
if info.width - info.textoff < text_width then
|
||||||
|
can_overlay = false
|
||||||
|
break
|
||||||
|
end
|
||||||
end
|
end
|
||||||
self.extmark_id = vim.api.nvim_buf_set_extmark(self.buf, self.ns, start_row, start_col, {
|
end
|
||||||
end_row = end_row,
|
-- can_overlay = false
|
||||||
end_col = end_col,
|
|
||||||
|
if height == 1 and #lines == 1 then
|
||||||
|
-- render inline
|
||||||
|
self:_render({
|
||||||
|
{
|
||||||
|
row = range[1] - 1,
|
||||||
|
col = range[2],
|
||||||
|
end_row = range[3] - 1,
|
||||||
|
end_col = range[4],
|
||||||
|
conceal = conceal,
|
||||||
|
invalidate = vim.fn.has("nvim-0.10") == 1 and true or nil,
|
||||||
|
virt_text_pos = "inline",
|
||||||
|
virt_text = { { img[1], hl } },
|
||||||
|
virt_text_hide = true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
elseif can_overlay then
|
||||||
|
if conceal then
|
||||||
|
-- conceal and overlay on the first line
|
||||||
|
extmarks[#extmarks + 1] = {
|
||||||
|
row = range[1] - 1,
|
||||||
|
col = range[2],
|
||||||
|
end_row = range[3] - 1,
|
||||||
|
end_col = range[4],
|
||||||
|
conceal = conceal,
|
||||||
|
virt_text_pos = "overlay",
|
||||||
|
virt_text = { { table.remove(img, 1), hl } },
|
||||||
|
virt_text_hide = false,
|
||||||
|
virt_text_win_col = offset,
|
||||||
|
}
|
||||||
|
-- overlay over the other lines
|
||||||
|
for i = 1, math.min(#img, #lines - 1) do
|
||||||
|
extmarks[#extmarks + 1] = {
|
||||||
|
row = range[1] - 1 + i,
|
||||||
|
col = 0,
|
||||||
|
virt_text_pos = "overlay",
|
||||||
|
virt_text = { { table.remove(img, 1), hl } },
|
||||||
|
virt_text_hide = false,
|
||||||
|
virt_text_win_col = offset,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #img > 0 then
|
||||||
|
-- add additional virtual lines if there are more lines to render
|
||||||
|
local padding = string.rep(" ", offset)
|
||||||
|
extmarks[#extmarks + 1] = {
|
||||||
|
row = range[3] - 1,
|
||||||
|
col = 0,
|
||||||
|
---@param l string
|
||||||
|
virt_lines = vim.tbl_map(function(l)
|
||||||
|
return { { padding }, { l, hl } }
|
||||||
|
end, img),
|
||||||
|
virt_text_hide = false,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
self:_render(extmarks)
|
||||||
|
else
|
||||||
|
local icon = Snacks.image.config.icons[self.opts.type or "image"] or Snacks.image.config.icons.image
|
||||||
|
-- render below in virtual lines
|
||||||
|
extmarks[#extmarks + 1] = {
|
||||||
|
row = range[1] - 1,
|
||||||
|
col = range[2],
|
||||||
|
end_row = range[3] - 1,
|
||||||
|
end_col = range[4],
|
||||||
conceal = conceal,
|
conceal = conceal,
|
||||||
id = self.extmark_id,
|
virt_text = { { icon, "SnacksImageAnchor" } },
|
||||||
|
virt_text_pos = "inline",
|
||||||
|
virt_text_hide = false,
|
||||||
---@param l string
|
---@param l string
|
||||||
virt_lines = vim.tbl_map(function(l)
|
virt_lines = vim.tbl_map(function(l)
|
||||||
return { { padding }, { l, hl } }
|
return { { l, hl } }
|
||||||
end, lines),
|
end, img),
|
||||||
strict = false,
|
}
|
||||||
invalidate = vim.fn.has("nvim-0.10") == 1 and true or nil,
|
self:_render(extmarks)
|
||||||
})
|
|
||||||
else
|
|
||||||
vim.bo[self.buf].modifiable = true
|
|
||||||
vim.api.nvim_buf_set_lines(self.buf, 0, -1, false, lines)
|
|
||||||
vim.bo[self.buf].modifiable = false
|
|
||||||
vim.bo[self.buf].modified = false
|
|
||||||
for r = 1, #lines do
|
|
||||||
vim.api.nvim_buf_set_extmark(self.buf, self.ns, r - 1, 0, {
|
|
||||||
end_col = #lines[r],
|
|
||||||
hl_group = hl,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function M:hide()
|
---@param extmarks snacks.image.Extmark[]
|
||||||
if vim.api.nvim_buf_is_valid(self.buf) then
|
function M:_render(extmarks)
|
||||||
vim.api.nvim_buf_clear_namespace(self.buf, self.ns, 0, -1)
|
for _, e in ipairs(extmarks) do
|
||||||
|
e.undo_restore = false
|
||||||
|
e.strict = false
|
||||||
|
if self.hidden then
|
||||||
|
e.virt_text = nil
|
||||||
|
e.conceal = nil
|
||||||
|
if e.virt_lines then
|
||||||
|
e.virt_lines = vim.tbl_map(function(l)
|
||||||
|
return { { "" } }
|
||||||
|
end, e.virt_lines)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
self.img:del(self.id)
|
local eids = {} ---@type number[]
|
||||||
|
for _, extmark in ipairs(extmarks) do
|
||||||
|
local row, col = extmark.row, extmark.col
|
||||||
|
extmark.row, extmark.col, extmark.id = nil, nil, table.remove(self.eids, 1)
|
||||||
|
table.insert(eids, vim.api.nvim_buf_set_extmark(self.buf, ns, row, col, extmark))
|
||||||
|
end
|
||||||
|
for _, eid in ipairs(self.eids) do
|
||||||
|
vim.api.nvim_buf_del_extmark(self.buf, ns, eid)
|
||||||
|
end
|
||||||
|
self.eids = eids
|
||||||
|
end
|
||||||
|
|
||||||
|
function M:hide()
|
||||||
|
if self.hidden or not self:ready() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.hidden = true
|
||||||
|
self:update()
|
||||||
|
end
|
||||||
|
|
||||||
|
function M:show()
|
||||||
|
if not self.hidden or not self:ready() then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
self.hidden = false
|
||||||
|
self:update()
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param state snacks.image.State
|
---@param state snacks.image.State
|
||||||
function M:render_fallback(state)
|
function M:render_fallback(state)
|
||||||
if not self.opts.inline then
|
if not self.opts.inline then
|
||||||
vim.api.nvim_buf_clear_namespace(self.buf, self.ns, 0, -1)
|
vim.api.nvim_buf_clear_namespace(self.buf, ns, 0, -1)
|
||||||
end
|
end
|
||||||
for _, win in ipairs(state.wins) do
|
for _, win in ipairs(state.wins) do
|
||||||
self:debug("render_fallback", win)
|
self:debug("render_fallback", win)
|
||||||
|
@ -299,10 +427,29 @@ function M:state()
|
||||||
local size = Snacks.image.util.fit(self.img.file, { width = width, height = height }, { info = self.img.info })
|
local size = Snacks.image.util.fit(self.img.file, { width = width, height = height }, { info = self.img.info })
|
||||||
|
|
||||||
local pos = self.opts.pos or { 1, 0 }
|
local pos = self.opts.pos or { 1, 0 }
|
||||||
|
|
||||||
|
local function is_inline()
|
||||||
|
local range = self.opts.range or { pos[1], pos[2], pos[1], pos[2] }
|
||||||
|
if range[1] == range[3] then
|
||||||
|
local line = vim.api.nvim_buf_get_lines(self.buf, range[1] - 1, range[1], false)[1] or ""
|
||||||
|
local has_before = line:sub(1, range[2]):find("%S") ~= nil
|
||||||
|
local has_after = line:sub(range[4] + 1):find("%S") ~= nil
|
||||||
|
return has_before or has_after
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- scale down to fit inline
|
||||||
|
if size.height <= 2 and is_inline() then
|
||||||
|
size.width = math.ceil(size.width / size.height) + 2
|
||||||
|
size.height = 1
|
||||||
|
end
|
||||||
|
|
||||||
---@class snacks.image.State
|
---@class snacks.image.State
|
||||||
|
---@field hidden boolean
|
||||||
---@field loc snacks.image.Loc
|
---@field loc snacks.image.Loc
|
||||||
---@field wins number[]
|
---@field wins number[]
|
||||||
return {
|
return {
|
||||||
|
hidden = self.hidden or false,
|
||||||
loc = {
|
loc = {
|
||||||
pos[1],
|
pos[1],
|
||||||
pos[2],
|
pos[2],
|
||||||
|
@ -313,11 +460,23 @@ function M:state()
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function M:valid()
|
||||||
|
return self.buf
|
||||||
|
and vim.api.nvim_buf_is_valid(self.buf)
|
||||||
|
and self:ready()
|
||||||
|
and self.opts.pos[1] <= vim.api.nvim_buf_line_count(self.buf)
|
||||||
|
end
|
||||||
|
|
||||||
function M:update()
|
function M:update()
|
||||||
if not self:ready() then
|
if not self:ready() then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if not self:valid() then
|
||||||
|
self:del()
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if self.opts.on_update_pre then
|
if self.opts.on_update_pre then
|
||||||
self.opts.on_update_pre(self)
|
self.opts.on_update_pre(self)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue