feat: pasting image within buffer (#331)
Signed-off-by: Aaron Pham <contact@aarnphm.xyz>
This commit is contained in:
parent
46a621e9de
commit
c635f73748
17
README.md
17
README.md
@ -32,6 +32,21 @@ Install `avante.nvim` using [lazy.nvim](https://github.com/folke/lazy.nvim):
|
||||
"MunifTanjim/nui.nvim",
|
||||
--- The below dependencies are optional,
|
||||
"nvim-tree/nvim-web-devicons", -- or echasnovski/mini.icons
|
||||
{
|
||||
-- support for image pasting
|
||||
"HakonHarnes/img-clip.nvim",
|
||||
event = "VeryLazy",
|
||||
opts = {
|
||||
-- recommended settings
|
||||
default = {
|
||||
embed_image_as_base64 = false,
|
||||
prompt_for_file_name = false,
|
||||
drag_and_drop = {
|
||||
insert_mode = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
-- Make sure to setup it properly if you have lazy=true
|
||||
'MeanderingProgrammer/render-markdown.nvim',
|
||||
@ -241,7 +256,7 @@ We would like to express our heartfelt gratitude to the contributors of the foll
|
||||
| --- | --- | --- | --- |
|
||||
| [git-conflict.nvim](https://github.com/akinsho/git-conflict.nvim) | No License | Diff comparison functionality | https://github.com/yetone/avante.nvim/blob/main/lua/avante/diff.lua |
|
||||
| [ChatGPT.nvim](https://github.com/jackMort/ChatGPT.nvim) | Apache 2.0 License | Calculation of tokens count | https://github.com/yetone/avante.nvim/blob/main/lua/avante/utils/tokens.lua |
|
||||
| [img-clip.nvim](https://github.com/HakonHarnes/img-clip.nvim) | MIT License | Clipboard image support | https://github.com/yetone/avante.nvim/blob/main/lua/avante/clipboard/init.lua |
|
||||
| [img-clip.nvim](https://github.com/HakonHarnes/img-clip.nvim) | MIT License | Clipboard image support | https://github.com/yetone/avante.nvim/blob/main/lua/avante/clipboard.lua |
|
||||
|
||||
The high quality and ingenuity of these projects' source code have been immensely beneficial throughout our development process. We extend our sincere thanks and respect to the authors and contributors of these projects. It is the selfless dedication of the open-source community that drives projects like avante.nvim forward.
|
||||
|
||||
|
26
after/plugin/avante.lua
Normal file
26
after/plugin/avante.lua
Normal file
@ -0,0 +1,26 @@
|
||||
--- NOTE: We will override vim.paste if img-clip.nvim is available to work with avante.nvim internal logic paste
|
||||
|
||||
local Clipboard = require("avante.clipboard")
|
||||
local Config = require("avante.config")
|
||||
|
||||
if Config.support_paste_image() then
|
||||
vim.paste = (function(overriden)
|
||||
---@param lines string[]
|
||||
---@param phase -1|1|2|3
|
||||
return function(lines, phase)
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local filetype = vim.api.nvim_get_option_value("filetype", { buf = bufnr })
|
||||
if filetype ~= "AvanteInput" then
|
||||
return overriden(lines, phase)
|
||||
end
|
||||
|
||||
---@type string
|
||||
local line = lines[1]
|
||||
|
||||
local ok = Clipboard.paste_image(line)
|
||||
if not ok then
|
||||
return overriden(lines, phase)
|
||||
end
|
||||
end
|
||||
end)(vim.paste)
|
||||
end
|
74
lua/avante/clipboard.lua
Normal file
74
lua/avante/clipboard.lua
Normal file
@ -0,0 +1,74 @@
|
||||
---NOTE: this module is inspired by https://github.com/HakonHarnes/img-clip.nvim/tree/main
|
||||
---@see https://github.com/ekickx/clipboard-image.nvim/blob/main/lua/clipboard-image/paste.lua
|
||||
|
||||
local Path = require("plenary.path")
|
||||
local Utils = require("avante.utils")
|
||||
local Config = require("avante.config")
|
||||
---@module "img-clip"
|
||||
local ImgClip = nil
|
||||
|
||||
---@class AvanteClipboard
|
||||
---@field get_base64_content fun(filepath: string): string | nil
|
||||
---
|
||||
---@class avante.Clipboard: AvanteClipboard
|
||||
local M = {}
|
||||
|
||||
---@type Path
|
||||
local paste_directory = nil
|
||||
|
||||
---@return Path
|
||||
local function get_paste_directory()
|
||||
if paste_directory then
|
||||
return paste_directory
|
||||
end
|
||||
paste_directory = Path:new(Config.history.storage_path):joinpath("pasted_images")
|
||||
return paste_directory
|
||||
end
|
||||
|
||||
M.support_paste_image = Config.support_paste_image
|
||||
|
||||
M.setup = function()
|
||||
get_paste_directory()
|
||||
|
||||
if not paste_directory:exists() then
|
||||
paste_directory:mkdir({ parent = true })
|
||||
end
|
||||
|
||||
if M.support_paste_image() and ImgClip == nil then
|
||||
ImgClip = require("img-clip")
|
||||
end
|
||||
end
|
||||
|
||||
---@param line string
|
||||
M.paste_image = function(line)
|
||||
if not Config.support_paste_image() then
|
||||
return false
|
||||
end
|
||||
|
||||
return ImgClip.paste_image({
|
||||
dir_path = paste_directory:absolute(),
|
||||
prompt_for_file_name = false,
|
||||
filetypes = {
|
||||
AvanteInput = { url_encode_path = true, template = "\nimage: $FILE_PATH\n" },
|
||||
},
|
||||
}, line)
|
||||
end
|
||||
|
||||
---@param filepath string
|
||||
M.get_base64_content = function(filepath)
|
||||
local os_mapping = Utils.get_os_name()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
if os_mapping == "darwin" or os_mapping == "linux" then
|
||||
output = Utils.shell_run(("cat %s | base64 | tr -d '\n'"):format(filepath))
|
||||
if output.code == 0 then
|
||||
return output.stdout
|
||||
else
|
||||
error("Failed to convert image to base64")
|
||||
end
|
||||
else
|
||||
Utils.warn("Windows is not supported yet", { title = "Avante" })
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
@ -1,85 +0,0 @@
|
||||
local Utils = require("avante.utils")
|
||||
|
||||
---@class AvanteClipboard
|
||||
local M = {}
|
||||
|
||||
---@alias DarwinClipboardCommand "pngpaste" | "osascript"
|
||||
M.clip_cmd = nil
|
||||
|
||||
---@return DarwinClipboardCommand
|
||||
M.get_clip_cmd = function()
|
||||
if M.clip_cmd then
|
||||
return M.clip_cmd
|
||||
end
|
||||
if vim.fn.executable("pngpaste") == 1 then
|
||||
M.clip_cmd = "pngpaste"
|
||||
elseif vim.fn.executable("osascript") == 1 then
|
||||
M.clip_cmd = "osascript"
|
||||
end
|
||||
return M.clip_cmd
|
||||
end
|
||||
|
||||
M.has_content = function()
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
if cmd == "pngpaste" then
|
||||
output = Utils.shell_run("pngpaste -")
|
||||
return output.code == 0
|
||||
elseif cmd == "osascript" then
|
||||
output = Utils.shell_run("osascript -e 'clipboard info'")
|
||||
return output.code == 0 and output.stdout ~= nil and output.stdout:find("class PNGf") ~= nil
|
||||
end
|
||||
|
||||
Utils.warn("Failed to validate clipboard content", { title = "Avante" })
|
||||
return false
|
||||
end
|
||||
|
||||
M.save_content = function(filepath)
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
if cmd == "pngpaste" then
|
||||
output = Utils.shell_run(('pngpaste - > "%s"'):format(filepath))
|
||||
return output.code == 0
|
||||
elseif cmd == "osascript" then
|
||||
output = Utils.shell_run(
|
||||
string.format(
|
||||
[[osascript -e 'set theFile to (open for access POSIX file "%s" with write permission)' ]]
|
||||
.. [[-e 'try' -e 'write (the clipboard as «class PNGf») to theFile' -e 'end try' ]]
|
||||
.. [[-e 'close access theFile' -e 'do shell script "cat %s > %s"']],
|
||||
filepath,
|
||||
filepath,
|
||||
filepath
|
||||
)
|
||||
)
|
||||
return output.code == 0
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
M.get_base64_content = function()
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
if cmd == "pngpaste" then
|
||||
output = Utils.shell_run("pngpaste - | base64 | tr -d '\n'")
|
||||
if output.code == 0 then
|
||||
return output.stdout
|
||||
end
|
||||
elseif cmd == "osascript" then
|
||||
output = Utils.shell_run(
|
||||
[[osascript -e 'set theFile to (open for access POSIX file "/tmp/image.png" with write permission)' -e 'try' -e 'write (the clipboard as «class PNGf») to theFile' -e 'end try' -e 'close access theFile'; ]]
|
||||
.. [[cat /tmp/image.png | base64 | tr -d '\n']]
|
||||
)
|
||||
if output.code == 0 then
|
||||
return output.stdout
|
||||
end
|
||||
end
|
||||
error("Failed to get clipboard content")
|
||||
end
|
||||
|
||||
return M
|
@ -1,48 +0,0 @@
|
||||
---NOTE: this module is inspired by https://github.com/HakonHarnes/img-clip.nvim/tree/main
|
||||
---@see https://github.com/ekickx/clipboard-image.nvim/blob/main/lua/clipboard-image/paste.lua
|
||||
|
||||
local Path = require("plenary.path")
|
||||
local Utils = require("avante.utils")
|
||||
local Config = require("avante.config")
|
||||
|
||||
---@class AvanteClipboard
|
||||
---@field clip_cmd string
|
||||
---@field get_clip_cmd fun(): string
|
||||
---@field has_content fun(): boolean
|
||||
---@field get_base64_content fun(): string
|
||||
---@field save_content fun(filename: string): boolean
|
||||
---
|
||||
---@class avante.Clipboard: AvanteClipboard
|
||||
local M = {}
|
||||
|
||||
---@type Path
|
||||
local paste_directory = nil
|
||||
|
||||
---@return Path
|
||||
local function get_paste_directory()
|
||||
if paste_directory then
|
||||
return paste_directory
|
||||
end
|
||||
paste_directory = Path:new(Config.history.storage_path):joinpath("pasted_images")
|
||||
return paste_directory
|
||||
end
|
||||
|
||||
M.setup = function()
|
||||
get_paste_directory()
|
||||
|
||||
if not paste_directory:exists() then
|
||||
paste_directory:mkdir({ parent = true })
|
||||
end
|
||||
end
|
||||
|
||||
return setmetatable(M, {
|
||||
__index = function(t, k)
|
||||
local os_mapping = Utils.get_os_name()
|
||||
---@type AvanteClipboard
|
||||
local impl = require("avante.clipboard." .. os_mapping)
|
||||
if impl[k] ~= nil then
|
||||
return impl[k]
|
||||
end
|
||||
return t[k]
|
||||
end,
|
||||
})
|
@ -1,74 +0,0 @@
|
||||
local Utils = require("avante.utils")
|
||||
|
||||
---@class AvanteClipboard
|
||||
local M = {}
|
||||
|
||||
M.clip_cmd = nil
|
||||
|
||||
M.get_clip_cmd = function()
|
||||
if M.clip_cmd then
|
||||
return M.clip_cmd
|
||||
end
|
||||
-- Wayland
|
||||
if os.getenv("WAYLAND_DISPLAY") ~= nil and vim.fn.executable("wl-paste") == 1 then
|
||||
M.clip_cmd = "wl-paste"
|
||||
-- X11
|
||||
elseif os.getenv("DISPLAY") ~= nil and vim.fn.executable("xclip") == 1 then
|
||||
M.clip_cmd = "xclip"
|
||||
end
|
||||
return M.clip_cmd
|
||||
end
|
||||
|
||||
M.has_content = function()
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
-- X11
|
||||
if cmd == "xclip" then
|
||||
output = Utils.shell_run("xclip -selection clipboard -t TARGETS -o")
|
||||
return output.code == 0 and output.stdout:find("image/png") ~= nil
|
||||
elseif cmd == "wl-paste" then
|
||||
output = Utils.shell_run("wl-paste --list-types")
|
||||
return output.code == 0 and output.stdout:find("image/png") ~= nil
|
||||
end
|
||||
|
||||
Utils.warn("Failed to validate clipboard content", { title = "Avante" })
|
||||
return false
|
||||
end
|
||||
|
||||
M.save_content = function(filepath)
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
if cmd == "xclip" then
|
||||
output = Utils.shell_run(('xclip -selection clipboard -o -t image/png > "%s"'):format(filepath))
|
||||
return output.code == 0
|
||||
elseif cmd == "wl-paste" then
|
||||
output = Utils.shell_run(('wl-paste --type image/png > "%s"'):format(filepath))
|
||||
return output.code == 0
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
M.get_base64_content = function()
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
if cmd == "xclip" then
|
||||
output = Utils.shell_run("xclip -selection clipboard -o -t image/png | base64 | tr -d '\n'")
|
||||
if output.code == 0 then
|
||||
return output.stdout
|
||||
end
|
||||
elseif cmd == "osascript" then
|
||||
output = Utils.shell_run("wl-paste --type image/png | base64 | tr -d '\n'")
|
||||
if output.code == 0 then
|
||||
return output.stdout
|
||||
end
|
||||
end
|
||||
error("Failed to get clipboard content")
|
||||
end
|
||||
|
||||
return M
|
@ -1,67 +0,0 @@
|
||||
local Utils = require("avante.utils")
|
||||
|
||||
---@class AvanteClipboard
|
||||
local M = {}
|
||||
|
||||
M.clip_cmd = nil
|
||||
|
||||
M.get_clip_cmd = function()
|
||||
if M.clip_cmd then
|
||||
return M.clip_cmd
|
||||
end
|
||||
if (vim.fn.has("win32") > 0 or vim.fn.has("wsl") > 0) and vim.fn.executable("powershell.exe") then
|
||||
M.clip_cmd = "powershell.exe"
|
||||
end
|
||||
return M.clip_cmd
|
||||
end
|
||||
|
||||
M.has_content = function()
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
if cmd == "powershell.exe" then
|
||||
output =
|
||||
Utils.shell_run("Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Clipboard]::GetImage()")
|
||||
return output.code == 0 and output.stdout:find("Width") ~= nil
|
||||
end
|
||||
|
||||
Utils.warn("Failed to validate clipboard content", { title = "Avante" })
|
||||
return false
|
||||
end
|
||||
|
||||
M.save_content = function(filepath)
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
if cmd == "powershell.exe" then
|
||||
output = Utils.shell_run(
|
||||
("Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Clipboard]::GetImage().Save('%s')"):format(
|
||||
filepath
|
||||
)
|
||||
)
|
||||
return output.code == 0
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
M.get_base64_content = function()
|
||||
local cmd = M.get_clip_cmd()
|
||||
---@type vim.SystemCompleted
|
||||
local output
|
||||
|
||||
if cmd == "powershell.exe" then
|
||||
output = Utils.shell_run(
|
||||
[[Add-Type -AssemblyName System.Windows.Forms; $ms = New-Object System.IO.MemoryStream;]]
|
||||
.. [[ [System.Windows.Forms.Clipboard]::GetImage().Save($ms, [System.Drawing.Imaging.ImageFormat]::Png);]]
|
||||
.. [[ [System.Convert]::ToBase64String($ms.ToArray())]]
|
||||
)
|
||||
if output.code == 0 then
|
||||
return output.stdout:gsub("\r\n", ""):gsub("\n", ""):gsub("\r", "")
|
||||
end
|
||||
end
|
||||
error("Failed to get clipboard content")
|
||||
end
|
||||
|
||||
return M
|
@ -1,6 +1,8 @@
|
||||
---NOTE: user will be merged with defaults and
|
||||
---we add a default var_accessor for this table to config values.
|
||||
---
|
||||
|
||||
local Utils = require("avante.utils")
|
||||
|
||||
---@class avante.CoreConfig: avante.Config
|
||||
local M = {}
|
||||
|
||||
@ -65,7 +67,7 @@ M.defaults = {
|
||||
---1. auto_apply_diff_after_generation: Whether to automatically apply diff after LLM response.
|
||||
--- This would simulate similar behaviour to cursor. Default to false.
|
||||
---2. auto_set_highlight_group: Whether to automatically set the highlight group for the current line. Default to true.
|
||||
---3. support_paste_from_clipboard: Whether to support pasting image from clipboard. Note that we will override vim.paste for this. Default to false.
|
||||
---3. support_paste_from_clipboard: Whether to support pasting image from clipboard. This will be determined automatically based whether img-clip is available or not.
|
||||
behaviour = {
|
||||
auto_set_highlight_group = true,
|
||||
auto_apply_diff_after_generation = false,
|
||||
@ -151,7 +153,17 @@ M.hints = {}
|
||||
|
||||
---@param opts? avante.Config
|
||||
function M.setup(opts)
|
||||
M.options = vim.tbl_deep_extend("force", M.defaults, opts or {})
|
||||
M.options = vim.tbl_deep_extend(
|
||||
"force",
|
||||
M.defaults,
|
||||
opts or {},
|
||||
---@type avante.Config
|
||||
{
|
||||
behaviour = {
|
||||
support_paste_from_clipboard = M.support_paste_image(),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
M.diff = vim.tbl_deep_extend(
|
||||
"force",
|
||||
@ -168,6 +180,14 @@ function M.setup(opts)
|
||||
end
|
||||
end
|
||||
|
||||
M.support_paste_image = function()
|
||||
local supported = Utils.has("img-clip.nvim")
|
||||
if not supported then
|
||||
Utils.warn("img-clip.nvim is not installed. Pasting image will be disabled.", { once = true })
|
||||
end
|
||||
return supported
|
||||
end
|
||||
|
||||
---@param opts? avante.Config
|
||||
function M.override(opts)
|
||||
opts = opts or {}
|
||||
|
@ -96,11 +96,27 @@ M.stream = function(question, code_lang, code_content, selected_content_content,
|
||||
mode = mode or "planning"
|
||||
local provider = Config.provider
|
||||
|
||||
-- Check if the question contains an image path
|
||||
local image_path = nil
|
||||
local original_question = question
|
||||
if question:match("image: ") then
|
||||
local lines = vim.split(question, "\n")
|
||||
for i, line in ipairs(lines) do
|
||||
if line:match("^image: ") then
|
||||
image_path = line:gsub("^image: ", "")
|
||||
table.remove(lines, i)
|
||||
original_question = table.concat(lines, "\n")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@type AvantePromptOptions
|
||||
local code_opts = {
|
||||
base_prompt = mode == "planning" and planning_mode_prompt or editing_mode_prompt,
|
||||
system_prompt = system_prompt,
|
||||
question = question,
|
||||
question = original_question,
|
||||
image_path = image_path,
|
||||
code_lang = code_lang,
|
||||
code_content = code_content,
|
||||
selected_code_content = selected_content_content,
|
||||
@ -188,13 +204,6 @@ M.stream = function(question, code_lang, code_content, selected_content_content,
|
||||
on_complete("API request failed with status " .. result.status .. ". Body: " .. vim.inspect(result.body))
|
||||
end
|
||||
end)
|
||||
else
|
||||
vim.schedule(function()
|
||||
if not completed then
|
||||
completed = true
|
||||
on_complete(nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
active_job = nil
|
||||
end,
|
||||
|
@ -39,22 +39,22 @@ M.parse_message = function(opts)
|
||||
table.insert(message_content, selected_code_obj)
|
||||
end
|
||||
|
||||
table.insert(message_content, {
|
||||
type = "text",
|
||||
text = string.format("<question>%s</question>", opts.question),
|
||||
})
|
||||
|
||||
if Config.behaviour.support_paste_from_clipboard and Clipboard.has_content() then
|
||||
if Clipboard.support_paste_image() and opts.image_path then
|
||||
table.insert(message_content, {
|
||||
type = "image",
|
||||
source = {
|
||||
type = "base64",
|
||||
media_type = "image/png",
|
||||
data = Clipboard.get_base64_content(),
|
||||
data = Clipboard.get_base64_content(opts.image_path),
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
table.insert(message_content, {
|
||||
type = "text",
|
||||
text = string.format("<question>%s</question>", opts.question),
|
||||
})
|
||||
|
||||
local user_prompt = opts.base_prompt
|
||||
|
||||
local user_prompt_obj = {
|
||||
|
@ -12,6 +12,7 @@ local Dressing = require("avante.ui.dressing")
|
||||
---@field base_prompt AvanteBasePrompt
|
||||
---@field system_prompt AvanteSystemPrompt
|
||||
---@field question string
|
||||
---@field image_path? string
|
||||
---@field code_lang string
|
||||
---@field code_content string
|
||||
---@field selected_code_content? string
|
||||
|
@ -63,12 +63,12 @@ end
|
||||
M.parse_message = function(opts)
|
||||
---@type string | OpenAIMessage[]
|
||||
local user_content
|
||||
if Config.behaviour.support_paste_from_clipboard and Clipboard.has_content() then
|
||||
if Config.behaviour.support_paste_from_clipboard and opts.image_path then
|
||||
user_content = {}
|
||||
table.insert(user_content, {
|
||||
type = "image_url",
|
||||
image_url = {
|
||||
url = "data:image/png;base64," .. Clipboard.get_base64_content(),
|
||||
url = "data:image/png;base64," .. Clipboard.get_base64_content(opts.image_path),
|
||||
},
|
||||
})
|
||||
table.insert(user_content, { type = "text", text = M.get_user_message(opts) })
|
||||
|
Loading…
x
Reference in New Issue
Block a user