Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 19 additions & 9 deletions lua/image/image.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local log = require("image/utils/logger").within("image")
local renderer = require("image/renderer")
local transform_cache = require("image/utils/transform_cache")
local utils = require("image/utils")

-- { ["buf:row:col"]: { id, height } }
Expand All @@ -12,6 +13,12 @@ local Image = {
}
Image.__index = Image

local get_source_signature = function(path)
local source = transform_cache.source_identity(path)
if not source then return nil end
return table.concat({ source.path, source.mtime, tostring(source.size) }, "\0")
end

---@param template Image
---@param global_state State
---@return Image
Expand Down Expand Up @@ -54,11 +61,12 @@ function Image:render(geometry)
-- be left in place
if vim.fn.getcmdwintype() ~= "" then return end

-- track last_modified and wipe cache
local current_last_modified = vim.fn.getftime(self.original_path)
log.debug(("timestamp: %s, last_modified: %s"):format(current_last_modified, self.last_modified))
if self.last_modified ~= current_last_modified then
self.last_modified = current_last_modified
-- track source changes and wipe cache
local current_source_signature = get_source_signature(self.original_path)
local source_changed = self.source_signature ~= current_source_signature
log.debug("source changed", { changed = source_changed })
if source_changed then
self.source_signature = current_source_signature
self.resize_hash = nil
self.crop_hash = nil
self.transform_key = nil
Expand Down Expand Up @@ -277,16 +285,19 @@ local from_file = function(path, options, state)
return nil
end

-- bypass processing if already processed
local current_source_signature = get_source_signature(absolute_original_path)

-- bypass processing if an unchanged source was already processed
for _, instance in pairs(state.images) do
if instance.original_path == absolute_original_path then
if instance.original_path == absolute_original_path and instance.source_signature == current_source_signature then
local clone = createImage({
id = opts.id or utils.random.id(),
path = instance.path,
resized_path = instance.path,
cropped_path = instance.path,
original_path = instance.original_path,
source_format = instance.source_format,
source_signature = current_source_signature,
image_width = instance.image_width,
image_height = instance.image_height,
max_width_window_percentage = instance.max_width_window_percentage,
Expand All @@ -313,7 +324,6 @@ local from_file = function(path, options, state)
crop_hash = nil,
resize_hash = nil,
namespace = opts.namespace or nil,
last_modified = vim.fn.getftime(absolute_original_path),
}, state)
log.debug(("cloned image %s from %s"):format(clone.id, instance.id))
return clone
Expand All @@ -335,6 +345,7 @@ local from_file = function(path, options, state)
cropped_path = source_path,
original_path = absolute_original_path,
source_format = source_format,
source_signature = current_source_signature,
image_width = dimensions.width,
image_height = dimensions.height,
max_width_window_percentage = opts.max_width_window_percentage,
Expand All @@ -361,7 +372,6 @@ local from_file = function(path, options, state)
crop_hash = nil,
resize_hash = nil,
namespace = opts.namespace or nil,
last_modified = vim.fn.getftime(absolute_original_path),
}, state)

return instance
Expand Down
2 changes: 1 addition & 1 deletion lua/types.lua
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@
---@field hue fun(self: Image, hue: number)
---@field namespace? string
---@field extmark? { id: number, row: number, col: number }
---@field last_modified? number
---@field source_signature? string
---@field has_extmark_moved fun (self:Image): (boolean, number?, number?)
---@field ignore_global_max_size? boolean
---@field render_offset_top? number
Expand Down
110 changes: 110 additions & 0 deletions tests/core/image_refresh_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
local unload_image_modules = function()
for module_name in pairs(package.loaded) do
if module_name == "image" or module_name:match("^image/") then package.loaded[module_name] = nil end
end
end

describe("image source refresh", function()
local original_uv
local source_path
local clear_calls
local cache_clear_calls
local dimension_calls
local render_calls

before_each(function()
unload_image_modules()
original_uv = vim.uv
if not vim.uv then vim.uv = vim.loop end

clear_calls = 0
cache_clear_calls = 0
dimension_calls = 0
render_calls = 0
package.loaded["image/renderer"] = {
render = function(image)
render_calls = render_calls + 1
image.global_state.images[image.id] = image
image.is_rendered = true
return true
end,
clear_cache_for_path = function()
cache_clear_calls = cache_clear_calls + 1
end,
}

source_path = vim.fn.tempname()
local file = assert(io.open(source_path, "wb"))
file:write("\x89PNG\r\n\x1A\nfirst")
file:close()
end)

after_each(function()
vim.uv = original_uv
if source_path then vim.fn.delete(source_path) end
unload_image_modules()
end)

it("refreshes a changed source even when the second-resolution mtime is unchanged", function()
local image_module = require("image/image")
local state
state = {
enabled = true,
rendering_paused = false,
images = {},
extmarks_namespace = vim.api.nvim_create_namespace("image-refresh-test"),
processor = {
get_format = function()
return "png"
end,
get_dimensions = function()
dimension_calls = dimension_calls + 1
return { width = 2, height = 2 }
end,
},
backend = {
clear = function(id, shallow)
clear_calls = clear_calls + 1
local image = state.images[id]
if image then
image.is_rendered = false
if not shallow then state.images[id] = nil end
end
end,
},
}

local image = image_module.from_file(source_path, nil, state)
image:render()
assert.are.same(1, dimension_calls)
assert.are.same(1, render_calls)
assert.are.same(0, clear_calls)

local original_mtime_seconds = vim.fn.getftime(source_path)
local original_stat = assert(vim.uv.fs_stat(source_path))
local file = assert(io.open(source_path, "wb"))
file:write("\x89PNG\r\n\x1A\nother")
file:close()
local updated_stat = assert(vim.uv.fs_stat(source_path))
assert.are.same(original_stat.size, updated_stat.size)
local changed_mtime_nsec = original_stat.mtime.nsec == 250000000 and 750000000 or 250000000
assert.is_true(
vim.uv.fs_utime(source_path, original_stat.atime.sec, original_stat.mtime.sec + changed_mtime_nsec / 1000000000)
)
local changed_stat = assert(vim.uv.fs_stat(source_path))
assert.are.same(original_stat.mtime.sec, changed_stat.mtime.sec)
assert.are.same(false, original_stat.mtime.nsec == changed_stat.mtime.nsec)
assert.are.same(original_mtime_seconds, vim.fn.getftime(source_path))

local reloaded_image = image_module.from_file(source_path, nil, state)
assert.are.same(false, image == reloaded_image)
assert.are.same(false, image.source_signature == reloaded_image.source_signature)
assert.are.same(2, dimension_calls)

image:render()
assert.are.same(3, dimension_calls)
assert.are.same(2, render_calls)
assert.are.same(1, clear_calls)
assert.are.same(1, cache_clear_calls)
end)
end)