image.nvim icon indicating copy to clipboard operation
image.nvim copied to clipboard

Telescope preview issue

Open miszo opened this issue 1 year ago • 12 comments

I'm trying to use your plugin to render previews in telescope. I've created a bufferpreviewer_maker like this:

local previewers = require('telescope.previewers')
local image_api = require('image')

local M = {}
local is_image_preview = false
local supported_images = { 'png', 'jpg', 'jpeg', 'heic', 'avif', 'gif', 'webp' }
local image = nil
local last_file_path = ''

local get_extension = function(filepath)
  local split_path = vim.split(filepath:lower(), '.', { plain = true })
  return split_path[#split_path]
end

local is_supported_image = function(filepath)
  local extension = get_extension(filepath)
  return vim.tbl_contains(supported_images, extension)
end

local delete_image = function()
  if not image then
    return
  end
  image:clear()
  is_image_preview = false
end

local create_image = function(filepath, winid, bufnr)
  image = image_api.from_file(filepath, { window = winid, buffer = bufnr })
  if not image then
    return
  end
  image:render()

  is_image_preview = true
end

M.buffer_previewer_maker = function(filepath, bufnr, opts)
  -- NOTE: Clear image when preview other file
  if is_image_preview and last_file_path ~= filepath then
    delete_image()
  end

  last_file_path = filepath

  local extension = get_extension(filepath)
  if is_supported_image(extension) then
    create_image(filepath, opts.winid, bufnr)
  else
    previewers.buffer_previewer_maker(filepath, bufnr, opts)
  end
end

M.teardown = function()
  if is_image_preview then
    delete_image()
  end
end

return M

And I have the problem when I try to preview the image for the first time – it doesn't render, it renders on the second attempt.

1st preview attempt 2nd preview attempt

I think that the problem is in the create_image function.

local create_image = function(filepath, winid, bufnr)
  image = image_api.from_file(filepath, { window = winid, buffer = bufnr })
  if not image then
    return
  end
  image:render()

  is_image_preview = true
end

Any ideas how to resolve the issue?

My env:

  • OS: macOS Sonoma Version 14.5 (23F79)
  • terminal: kitty 0.35.1
  • NeoVim: 0.10

Plugin setup:

  {
    'vhyrro/luarocks.nvim',
    priority = 1001, -- this plugin needs to run before anything else
    opts = {
      rocks = { 'magick' },
    },
  },
  {
    '3rd/image.nvim',
    dependencies = { 'luarocks.nvim' },
    config = function(_, opts)
      opts.integrations = opts.integrations or {}
      opts.integrations.markdown = opts.integrations.markdown or {}
      opts.integrations.markdown.only_render_image_at_cursor = true
      opts.hijack_file_patterns = opts.hijack_file_patterns or {}
      opts.hijack_file_patterns = { '*.png', '*.jpg', '*.jpeg', '*.gif', '*.webp', '*.avif', '*.heic' }
      opts.window_overlap_clear_enabled = true -- tried to remove or change it to false, the outcome was the same
      require('image').setup(opts)
    end,
  },

miszo avatar Jun 22 '24 17:06 miszo

I've added the echo statement in the create_image function

local create_image = function(filepath, winid, bufnr)
  image = image_api.from_file(filepath, { window = winid, buffer = bufnr })
  if not image then
    return
  end
  image:render()

  vim.api.nvim_echo({
    { 'image.is_rendered: ' .. tostring(image.is_rendered), nil },
  }, false, {})

  is_image_preview = true
end

And on the first try it prints image.is_rendered: false

CleanShot 2024-06-22 at 21 00 33@2x

But on the second try it returns the image.is_rendered: true

CleanShot 2024-06-22 at 21 02 26@2x

miszo avatar Jun 22 '24 19:06 miszo

I'm not sure whether it should be the actual solution, but I've wrapped it with the vim.defer_fn and it works every time 😅

local create_image = function(filepath, winid, bufnr)
  image = image_api.from_file(filepath, { window = winid, buffer = bufnr })
  if not image then
    return
  end
  vim.defer_fn(function()
    image:render()
  end, 0)

  is_image_preview = true
end

miszo avatar Jun 22 '24 19:06 miszo

@miszo I'd love to play around with this telescope previewer. Do you have a repo to install this as a telescope extension?

exosyphon avatar Jul 02 '24 13:07 exosyphon

@exosyphon, didn't go that deep to set up this as a telescope extension.

Here's the code for:

miszo avatar Jul 02 '24 13:07 miszo

Moved the code into a single file, based on example above, would be awesome if there was an extension for this tho:

The change filepath = string.gsub(filepath, " ", "%%20"):gsub("\\", "") was from @jugarpeupv: https://github.com/3rd/image.nvim/issues/183#issuecomment-2574790371

function telescope_image_preview()
    local supported_images = { "svg", "png", "jpg", "jpeg", "gif", "webp", "avif" }
    local from_entry = require("telescope.from_entry")
    local Path = require("plenary.path")
    local conf = require("telescope.config").values
    local Previewers = require("telescope.previewers")

    local previewers = require("telescope.previewers")
    local image_api = require("image")

    local is_image_preview = false
    local image = nil
    local last_file_path = ""

    local is_supported_image = function(filepath)
        local split_path = vim.split(filepath:lower(), ".", { plain = true })
        local extension = split_path[#split_path]
        return vim.tbl_contains(supported_images, extension)
    end

    local delete_image = function()
        if not image then
            return
        end

        image:clear()

        is_image_preview = false
    end

    local create_image = function(filepath, winid, bufnr)
        image = image_api.hijack_buffer(filepath, winid, bufnr)

        if not image then
            return
        end

        vim.schedule(function()
            image:render()
        end)

        is_image_preview = true
    end

    local function defaulter(f, default_opts)
        default_opts = default_opts or {}
        return {
            new = function(opts)
                if conf.preview == false and not opts.preview then
                    return false
                end
                opts.preview = type(opts.preview) ~= "table" and {} or opts.preview
                if type(conf.preview) == "table" then
                    for k, v in pairs(conf.preview) do
                        opts.preview[k] = vim.F.if_nil(opts.preview[k], v)
                    end
                end
                return f(opts)
            end,
            __call = function()
                local ok, err = pcall(f(default_opts))
                if not ok then
                    error(debug.traceback(err))
                end
            end,
        }
    end

    -- NOTE: Add teardown to cat previewer to clear image when close Telescope
    local file_previewer = defaulter(function(opts)
        opts = opts or {}
        local cwd = opts.cwd or vim.loop.cwd()
        return Previewers.new_buffer_previewer({
            title = "File Preview",
            dyn_title = function(_, entry)
                return Path:new(from_entry.path(entry, true)):normalize(cwd)
            end,

            get_buffer_by_name = function(_, entry)
                return from_entry.path(entry, true)
            end,

            define_preview = function(self, entry, _)
                local p = from_entry.path(entry, true)
                if p == nil or p == "" then
                    return
                end

                conf.buffer_previewer_maker(p, self.state.bufnr, {
                    bufname = self.state.bufname,
                    winid = self.state.winid,
                    preview = opts.preview,
                })
            end,

            teardown = function(_)
                if is_image_preview then
                    delete_image()
                end
            end,
        })
    end, {})

    local buffer_previewer_maker = function(filepath, bufnr, opts)
        -- NOTE: Clear image when preview other file
        if is_image_preview and last_file_path ~= filepath then
            delete_image()
        end

        last_file_path = filepath

        if is_supported_image(filepath) then
            filepath = string.gsub(filepath, " ", "%%20"):gsub("\\", "")
            create_image(filepath, opts.winid, bufnr)
        else
            previewers.buffer_previewer_maker(filepath, bufnr, opts)
        end
    end

    return { buffer_previewer_maker = buffer_previewer_maker, file_previewer = file_previewer.new }
end
local image_preview = telescope_image_preview()

require("telescope").setup({
    defaults = {
        file_previewer = image_preview.file_previewer,
        buffer_previewer_maker = image_preview.buffer_previewer_maker,
    },
    extensions = {
        file_browser = { hijack_netrw = true },
    },
})

sand4rt avatar Aug 12 '24 22:08 sand4rt

Nvim freezes somehow when i open the neotest summary. The require of image seems to be causing it (when i turn this off, neotest summary works):

local image_api = require("image")

@3rd @miszo any idea?

sand4rt avatar Aug 25 '24 09:08 sand4rt

Could be something going wrong with the decorations provider or auto-commands.

3rd avatar Aug 25 '24 23:08 3rd

Thanks for the reply, i turned my custom auto-commands and plugins off and nvim only crashes sometimes now in combination with local image_api = require("image").

Maybe it has something to do with: https://github.com/nvim-neotest/neotest/issues/441, i'll wait for that

sand4rt avatar Aug 26 '24 17:08 sand4rt

Thanks for the reply, i turned my custom auto-commands and plugins off and nvim only crashes sometimes now in combination with local image_api = require("image").

Maybe it has something to do with: https://github.com/nvim-neotest/neotest/issues/441, i'll wait for that

But did image.nvim ever render an image in the session? I think we shouldn't even set up the handlers and providers until an image needs to be rendered, I'll looking into it and make sure we don't!

3rd avatar Aug 26 '24 17:08 3rd

But did image.nvim ever render an image in the session

No it did not, thanks!

sand4rt avatar Aug 26 '24 18:08 sand4rt

Moved the code into a single file, based on example above, would be awesome if there was an extension for this tho:

function telescope_image_preview()
    local supported_images = { "svg", "png", "jpg", "jpeg", "gif", "webp", "avif" }
    local from_entry = require("telescope.from_entry")
    local Path = require("plenary.path")
    local conf = require("telescope.config").values
    local Previewers = require("telescope.previewers")

    local previewers = require("telescope.previewers")
    local image_api = require("image")

    local is_image_preview = false
    local image = nil
    local last_file_path = ""

    local is_supported_image = function(filepath)
        local split_path = vim.split(filepath:lower(), ".", { plain = true })
        local extension = split_path[#split_path]
        return vim.tbl_contains(supported_images, extension)
    end

    local delete_image = function()
        if not image then
            return
        end

        image:clear()

        is_image_preview = false
    end

    local create_image = function(filepath, winid, bufnr)
        image = image_api.hijack_buffer(filepath, winid, bufnr)

        if not image then
            return
        end

        vim.schedule(function()
            image:render()
        end)

        is_image_preview = true
    end

    local function defaulter(f, default_opts)
        default_opts = default_opts or {}
        return {
            new = function(opts)
                if conf.preview == false and not opts.preview then
                    return false
                end
                opts.preview = type(opts.preview) ~= "table" and {} or opts.preview
                if type(conf.preview) == "table" then
                    for k, v in pairs(conf.preview) do
                        opts.preview[k] = vim.F.if_nil(opts.preview[k], v)
                    end
                end
                return f(opts)
            end,
            __call = function()
                local ok, err = pcall(f(default_opts))
                if not ok then
                    error(debug.traceback(err))
                end
            end,
        }
    end

    -- NOTE: Add teardown to cat previewer to clear image when close Telescope
    local file_previewer = defaulter(function(opts)
        opts = opts or {}
        local cwd = opts.cwd or vim.loop.cwd()
        return Previewers.new_buffer_previewer({
            title = "File Preview",
            dyn_title = function(_, entry)
                return Path:new(from_entry.path(entry, true)):normalize(cwd)
            end,

            get_buffer_by_name = function(_, entry)
                return from_entry.path(entry, true)
            end,

            define_preview = function(self, entry, _)
                local p = from_entry.path(entry, true)
                if p == nil or p == "" then
                    return
                end

                conf.buffer_previewer_maker(p, self.state.bufnr, {
                    bufname = self.state.bufname,
                    winid = self.state.winid,
                    preview = opts.preview,
                })
            end,

            teardown = function(_)
                if is_image_preview then
                    delete_image()
                end
            end,
        })
    end, {})

    local buffer_previewer_maker = function(filepath, bufnr, opts)
        -- NOTE: Clear image when preview other file
        if is_image_preview and last_file_path ~= filepath then
            delete_image()
        end

        last_file_path = filepath

        if is_supported_image(filepath) then
            create_image(filepath, opts.winid, bufnr)
        else
            previewers.buffer_previewer_maker(filepath, bufnr, opts)
        end
    end

    return { buffer_previewer_maker = buffer_previewer_maker, file_previewer = file_previewer.new }
end
local image_preview = telescope_image_preview()

require("telescope").setup({
    defaults = {
        file_previewer = image_preview.file_previewer,
        buffer_previewer_maker = image_preview.buffer_previewer_maker,
    },
    extensions = {
        file_browser = { hijack_netrw = true },
    },
})

Is anyone else getting noticeable hang in the main loop when first selecting / rendering an image entry using this code?

I tried removing all Image:render() calls and replacing create_image with just local create_image = async.void(function(filepath, winid, bufnr) --image = image_api.hijack_buffer(filepath, winid, bufnr) -- calls image.render internally image = image_api.from_file(filepath, { window = winid, buffer = bufnr }) end) and there's still a freeze whenever selecting an image entry for the first time (Nothings rendered either, as expected). It doesn't happen if an image is already cached in state.images, so the freeze is due to image creation.

The freezing seems to happen in image.lua's from_file for me.

Am I misunderstanding how plenary's async.void can be used? Hows/is it possible to create the image objects outside the main loop to prevent freezing neovim? @3rd @sand4rt

Kimononono avatar Sep 08 '24 09:09 Kimononono

https://www.lua.org/pil/9.4.html#:~:text=However%2C%20unlike%20%22real%22%20multithreading%2C%20coroutines%20are%20non%20preemptive It would be nice to run the image processing on a different thread, but didn't get into that yet as we'll need a system to also abort/invalidate operations.

Edit: this is again a limitation of working with the rock, we'll have better alternatives that non-blocking, but we can do it in Lua as well if the magick rock will survive http://lua-users.org/wiki/ThreadsTutorial

3rd avatar Sep 08 '24 12:09 3rd

Since image.nvim "hijacks" a buffer, wouldn't it be possible to just bypass Telescope's check for "binary" files? if Telescope just renders the buffer normally, image.nvim should hijack it and display the image properly, without additional code 🤔 ?

https://github.com/nvim-telescope/telescope.nvim/blob/85922dde3767e01d42a08e750a773effbffaea3e/lua/telescope/previewers/buffer_previewer.lua#L224

 if (opts.ft == nil or opts.ft == "") and possible_binary then
          putils.set_preview_message(bufnr, opts.winid, "Binary cannot be previewed", opts.preview.msg_bg_fillchar)
          return
        end

carlos-algms avatar Nov 15 '24 17:11 carlos-algms

Moved the code into a single file, based on example above, would be awesome if there was an extension for this tho:

function telescope_image_preview()
    local supported_images = { "svg", "png", "jpg", "jpeg", "gif", "webp", "avif" }
    local from_entry = require("telescope.from_entry")
    local Path = require("plenary.path")
    local conf = require("telescope.config").values
    local Previewers = require("telescope.previewers")

    local previewers = require("telescope.previewers")
    local image_api = require("image")

    local is_image_preview = false
    local image = nil
    local last_file_path = ""

    local is_supported_image = function(filepath)
        local split_path = vim.split(filepath:lower(), ".", { plain = true })
        local extension = split_path[#split_path]
        return vim.tbl_contains(supported_images, extension)
    end

    local delete_image = function()
        if not image then
            return
        end

        image:clear()

        is_image_preview = false
    end

    local create_image = function(filepath, winid, bufnr)
        image = image_api.hijack_buffer(filepath, winid, bufnr)

        if not image then
            return
        end

        vim.schedule(function()
            image:render()
        end)

        is_image_preview = true
    end

    local function defaulter(f, default_opts)
        default_opts = default_opts or {}
        return {
            new = function(opts)
                if conf.preview == false and not opts.preview then
                    return false
                end
                opts.preview = type(opts.preview) ~= "table" and {} or opts.preview
                if type(conf.preview) == "table" then
                    for k, v in pairs(conf.preview) do
                        opts.preview[k] = vim.F.if_nil(opts.preview[k], v)
                    end
                end
                return f(opts)
            end,
            __call = function()
                local ok, err = pcall(f(default_opts))
                if not ok then
                    error(debug.traceback(err))
                end
            end,
        }
    end

    -- NOTE: Add teardown to cat previewer to clear image when close Telescope
    local file_previewer = defaulter(function(opts)
        opts = opts or {}
        local cwd = opts.cwd or vim.loop.cwd()
        return Previewers.new_buffer_previewer({
            title = "File Preview",
            dyn_title = function(_, entry)
                return Path:new(from_entry.path(entry, true)):normalize(cwd)
            end,

            get_buffer_by_name = function(_, entry)
                return from_entry.path(entry, true)
            end,

            define_preview = function(self, entry, _)
                local p = from_entry.path(entry, true)
                if p == nil or p == "" then
                    return
                end

                conf.buffer_previewer_maker(p, self.state.bufnr, {
                    bufname = self.state.bufname,
                    winid = self.state.winid,
                    preview = opts.preview,
                })
            end,

            teardown = function(_)
                if is_image_preview then
                    delete_image()
                end
            end,
        })
    end, {})

    local buffer_previewer_maker = function(filepath, bufnr, opts)
        -- NOTE: Clear image when preview other file
        if is_image_preview and last_file_path ~= filepath then
            delete_image()
        end

        last_file_path = filepath

        if is_supported_image(filepath) then
            create_image(filepath, opts.winid, bufnr)
        else
            previewers.buffer_previewer_maker(filepath, bufnr, opts)
        end
    end

    return { buffer_previewer_maker = buffer_previewer_maker, file_previewer = file_previewer.new }
end
local image_preview = telescope_image_preview()

require("telescope").setup({
    defaults = {
        file_previewer = image_preview.file_previewer,
        buffer_previewer_maker = image_preview.buffer_previewer_maker,
    },
    extensions = {
        file_browser = { hijack_netrw = true },
    },
})

Shouldn't it be vim.fn.getcwd()?

MuntasirSZN avatar Jan 06 '25 08:01 MuntasirSZN

Ey, thanks for the snippet code. I am having a problem though, when the .png file has white space it its filename, image.nvim is complaining, do you know how to solve it? i do not know how white space is handled, will have a look at source code but in case someone knows

image

jugarpeupv avatar Jan 07 '25 09:01 jugarpeupv

Ok i just found the solution

    if is_supported_image(filepath) then
      filepath = string.gsub(filepath, " ", "%%20"):gsub("\\", "")
      create_image(filepath, opts.winid, bufnr)
    else

jugarpeupv avatar Jan 07 '25 09:01 jugarpeupv

Ok i just found the solution

    if is_supported_image(filepath) then
      filepath = string.gsub(filepath, " ", "%%20"):gsub("\\", "")
      create_image(filepath, opts.winid, bufnr)
    else

Ey thanks!

MuntasirSZN avatar Jan 07 '25 09:01 MuntasirSZN

There is a minor problem with the script. If we go too fast in telescope, then the image will stay on top of the preview of the file.

MuntasirSZN avatar Jan 15 '25 01:01 MuntasirSZN

@miszo @MuntasirSZN @jugarpeupv

You guys ok if I push this into a plugin repo? would be nice a Telescope + image.nvim plugin

Seems functional enough

jcarlos7121 avatar Jan 23 '25 18:01 jcarlos7121

@jcarlos7121 Feel free, maybe we can close the issue? Because it is resolved, in my opinion, and the dedicated plugin would help people as well.

miszo avatar Jan 24 '25 09:01 miszo

Since image.nvim "hijacks" a buffer, wouldn't it be possible to just bypass Telescope's check for "binary" files? if Telescope just renders the buffer normally, image.nvim should hijack it and display the image properly, without additional code 🤔 ?

https://github.com/nvim-telescope/telescope.nvim/blob/85922dde3767e01d42a08e750a773effbffaea3e/lua/telescope/previewers/buffer_previewer.lua#L224

if (opts.ft == nil or opts.ft == "") and possible_binary then putils.set_preview_message(bufnr, opts.winid, "Binary cannot be previewed", opts.preview.msg_bg_fillchar) return end

@jcarlos7121 Before you create a plugin, please, consider my comment above. image.nvim can't hijack Telescope's preview because Telescope itself doesn't render the preview if it is a "binary" like file.

If we can bypass that, we won't need a new Plugin, as the hijack would do its thing.

carlos-algms avatar Jan 24 '25 15:01 carlos-algms

Anyone been able to fix(or reduce) lag when previewing images in telescope?

nxtkofi avatar Feb 11 '25 16:02 nxtkofi

carlos-algms wrote:

Since image.nvim "hijacks" a buffer, wouldn't it be possible to just bypass Telescope's check for "binary" files? if Telescope just renders the buffer normally, image.nvim should hijack it and display the image properly, without additional code 🤔 ?

https://github.com/nvim-telescope/telescope.nvim/blob/85922dde3767e01d42a08e750a773effbffaea3e/lua/telescope/previewers/buffer_previewer.lua#L224

if (opts.ft == nil or opts.ft == "") and possible_binary then putils.set_preview_message(bufnr, opts.winid, "Binary cannot be previewed", opts.preview.msg_bg_fillchar) return end

Good suggestion! Maybe something like this quick prototype in its place?

        -- if we still dont have a ft we need to display the binary message
        if (opts.ft == nil or opts.ft == "") and possible_binary then
          putils.set_preview_message(bufnr, opts.winid, "")
          local image_api = require("image")
          local image = image_api.from_file(filepath, { window = opts.winid, buf = bufnr })
          if image then
            vim.defer_fn(function()
              image:render()
            end, 0)
          end
          return
        end

That's just a quick test as I don't really am not very familiar with this extension, but it does successfully show e.g. jpeg images, while seemingly crashing when encountering gifs.

Additional limitations of the above:

  • It doesn't clear the previously shown image.
  • ~~It simultaneously displays some text version of the binary contents below the rendred images as well.~~ Update: Edited the example to get rid of that printed binary text in the background. Now it displays the error message background msg_bg_fillchar behind it instead as it's slightly less annoying. But still not ideal.

Obviously one should also verify that the file is in fact an image, and not just try to render an image from anything detected as a binary.

miyl avatar Mar 05 '25 21:03 miyl

Ey @miyl @jcarlos7121 @exosyphon @miszo @carlos-algms! Is this code still working for you guys?

For me it is not working anymore

I am pointing to commit 21909e3eb03bc738cce497f45602bf157b396672, since it seems a "bug" was introduced recently, i can load images well etc

Image

jugarpeupv avatar Aug 12 '25 14:08 jugarpeupv

Sorry @jugarpeupv, can't help you here. I've dropped usage of both this plugin and telescope in favor of the folke/snacks.nvim.

miszo avatar Aug 12 '25 14:08 miszo

Same, I dropped it in favour of Snacks, it just works ™️. Telescope was never meant to it, and I'm afraid it would require too many changes to achieve a proper "plugin" integration 😞

carlos-algms avatar Aug 12 '25 15:08 carlos-algms