refactor: ui lib (#1642)
This commit is contained in:
194
lua/avante/ui/confirm.lua
Normal file
194
lua/avante/ui/confirm.lua
Normal file
@@ -0,0 +1,194 @@
|
||||
local Popup = require("nui.popup")
|
||||
local NuiText = require("nui.text")
|
||||
local Highlights = require("avante.highlights")
|
||||
local Utils = require("avante.utils")
|
||||
|
||||
---@class avante.ui.Confirm
|
||||
---@field message string
|
||||
---@field callback fun(yes: boolean)
|
||||
---@field opts { container_winid: number }
|
||||
---@field _popup NuiPopup | nil
|
||||
local M = {}
|
||||
M.__index = M
|
||||
|
||||
---@param message string
|
||||
---@param callback fun(yes: boolean)
|
||||
---@param opts { container_winid: number }
|
||||
---@return avante.ui.Confirm
|
||||
function M:new(message, callback, opts)
|
||||
local this = setmetatable({}, M)
|
||||
this.message = message
|
||||
this.callback = callback
|
||||
this.opts = opts
|
||||
return this
|
||||
end
|
||||
|
||||
function M:open()
|
||||
local message = self.message
|
||||
local callback = self.callback
|
||||
local opts = self.opts
|
||||
|
||||
local focus_index = 2 -- 1 = Yes, 2 = No
|
||||
local yes_button_pos = { 23, 28 }
|
||||
local no_button_pos = { 33, 37 }
|
||||
|
||||
local BUTTON_NORMAL = Highlights.BUTTON_DEFAULT
|
||||
local BUTTON_FOCUS = Highlights.BUTTON_DEFAULT_HOVER
|
||||
|
||||
local button_line = string.rep(" ", 23) .. " Yes No "
|
||||
local button_line_num = 2 + #vim.split(message, "\n")
|
||||
local content = vim
|
||||
.iter({
|
||||
"",
|
||||
vim.tbl_map(function(line) return " " .. line end, vim.split(message, "\n")),
|
||||
"",
|
||||
button_line,
|
||||
"",
|
||||
})
|
||||
:flatten()
|
||||
:totable()
|
||||
local button_row = #content - 1
|
||||
|
||||
local container_winid = opts.container_winid or vim.api.nvim_get_current_win()
|
||||
local container_width = vim.api.nvim_win_get_width(container_winid)
|
||||
|
||||
local popup = Popup({
|
||||
relative = {
|
||||
type = "win",
|
||||
winid = container_winid,
|
||||
},
|
||||
position = {
|
||||
row = vim.o.lines - #content - 3,
|
||||
col = (container_width - 60) / 2,
|
||||
},
|
||||
size = { width = 60, height = #content + 3 },
|
||||
enter = true,
|
||||
focusable = true,
|
||||
border = {
|
||||
style = "rounded",
|
||||
text = { top = NuiText(" Confirmation ", Highlights.CONFIRM_TITLE) },
|
||||
},
|
||||
buf_options = {
|
||||
filetype = "avante-confirm",
|
||||
modifiable = false,
|
||||
readonly = true,
|
||||
},
|
||||
win_options = {
|
||||
winblend = 10,
|
||||
},
|
||||
})
|
||||
|
||||
local function focus_button(row)
|
||||
row = row or button_row
|
||||
if focus_index == 1 then
|
||||
vim.api.nvim_win_set_cursor(popup.winid, { row, yes_button_pos[1] })
|
||||
else
|
||||
vim.api.nvim_win_set_cursor(popup.winid, { row, no_button_pos[1] })
|
||||
end
|
||||
end
|
||||
|
||||
local function render_buttons()
|
||||
local yes_style = (focus_index == 1) and BUTTON_FOCUS or BUTTON_NORMAL
|
||||
local no_style = (focus_index == 2) and BUTTON_FOCUS or BUTTON_NORMAL
|
||||
|
||||
Utils.unlock_buf(popup.bufnr)
|
||||
vim.api.nvim_buf_set_lines(popup.bufnr, 0, -1, false, content)
|
||||
Utils.lock_buf(popup.bufnr)
|
||||
|
||||
vim.api.nvim_buf_add_highlight(popup.bufnr, 0, yes_style, button_line_num, yes_button_pos[1], yes_button_pos[2])
|
||||
vim.api.nvim_buf_add_highlight(popup.bufnr, 0, no_style, button_line_num, no_button_pos[1], no_button_pos[2])
|
||||
focus_button(button_line_num + 1)
|
||||
end
|
||||
|
||||
local function select_button()
|
||||
popup:unmount()
|
||||
callback(focus_index == 1)
|
||||
end
|
||||
|
||||
vim.keymap.set("n", "y", function()
|
||||
focus_index = 1
|
||||
render_buttons()
|
||||
select_button()
|
||||
end, { buffer = popup.bufnr })
|
||||
|
||||
vim.keymap.set("n", "n", function()
|
||||
focus_index = 2
|
||||
render_buttons()
|
||||
select_button()
|
||||
end, { buffer = popup.bufnr })
|
||||
|
||||
vim.keymap.set("n", "<Left>", function()
|
||||
focus_index = 1
|
||||
focus_button()
|
||||
end, { buffer = popup.bufnr })
|
||||
|
||||
vim.keymap.set("n", "<Right>", function()
|
||||
focus_index = 2
|
||||
focus_button()
|
||||
end, { buffer = popup.bufnr })
|
||||
|
||||
vim.keymap.set("n", "<Tab>", function()
|
||||
focus_index = (focus_index == 1) and 2 or 1
|
||||
focus_button()
|
||||
end, { buffer = popup.bufnr })
|
||||
|
||||
vim.keymap.set("n", "<S-Tab>", function()
|
||||
focus_index = (focus_index == 1) and 2 or 1
|
||||
focus_button()
|
||||
end, { buffer = popup.bufnr })
|
||||
|
||||
vim.keymap.set("n", "<CR>", function() select_button() end, { buffer = popup.bufnr })
|
||||
|
||||
vim.api.nvim_buf_set_keymap(popup.bufnr, "n", "<LeftMouse>", "", {
|
||||
callback = function()
|
||||
local pos = vim.fn.getmousepos()
|
||||
local row, col = pos["winrow"], pos["wincol"]
|
||||
if row == button_row then
|
||||
if col >= yes_button_pos[1] and col <= yes_button_pos[2] then
|
||||
focus_index = 1
|
||||
render_buttons()
|
||||
select_button()
|
||||
elseif col >= no_button_pos[1] and col <= no_button_pos[2] then
|
||||
focus_index = 2
|
||||
render_buttons()
|
||||
select_button()
|
||||
end
|
||||
end
|
||||
end,
|
||||
noremap = true,
|
||||
silent = true,
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("CursorMoved", {
|
||||
buffer = popup.bufnr,
|
||||
callback = function()
|
||||
local row, col = unpack(vim.api.nvim_win_get_cursor(0))
|
||||
if row == button_row then
|
||||
if col >= yes_button_pos[1] and col <= yes_button_pos[2] then
|
||||
focus_index = 1
|
||||
render_buttons()
|
||||
elseif col >= no_button_pos[1] and col <= no_button_pos[2] then
|
||||
focus_index = 2
|
||||
render_buttons()
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- popup:on(event.BufLeave, function() popup:unmount() end)
|
||||
|
||||
popup:mount()
|
||||
render_buttons()
|
||||
self._popup = popup
|
||||
end
|
||||
|
||||
function M:close()
|
||||
if self._popup then
|
||||
self._popup:unmount()
|
||||
self._popup = nil
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return M
|
||||
331
lua/avante/ui/prompt_input.lua
Normal file
331
lua/avante/ui/prompt_input.lua
Normal file
@@ -0,0 +1,331 @@
|
||||
local api = vim.api
|
||||
local fn = vim.fn
|
||||
local Config = require("avante.config")
|
||||
local Utils = require("avante.utils")
|
||||
|
||||
---@class avante.ui.PromptInput
|
||||
---@field bufnr integer | nil
|
||||
---@field winid integer | nil
|
||||
---@field win_opts table
|
||||
---@field shortcuts_hints_winid integer | nil
|
||||
---@field augroup integer | nil
|
||||
---@field start_insert boolean
|
||||
---@field submit_callback function | nil
|
||||
---@field cancel_callback function | nil
|
||||
---@field close_on_submit boolean
|
||||
---@field spinner_chars table
|
||||
---@field spinner_index integer
|
||||
---@field spinner_timer uv_timer_t | nil
|
||||
---@field spinner_active boolean
|
||||
---@field default_value string | nil
|
||||
local PromptInput = {}
|
||||
PromptInput.__index = PromptInput
|
||||
|
||||
---@class avante.ui.PromptInputOptions
|
||||
---@field start_insert? boolean
|
||||
---@field submit_callback? fun(input: string):nil
|
||||
---@field cancel_callback? fun():nil
|
||||
---@field close_on_submit? boolean
|
||||
---@field win_opts? table
|
||||
---@field default_value? string
|
||||
|
||||
---@param opts? avante.ui.PromptInputOptions
|
||||
function PromptInput:new(opts)
|
||||
opts = opts or {}
|
||||
local obj = setmetatable({}, PromptInput)
|
||||
obj.bufnr = nil
|
||||
obj.winid = nil
|
||||
obj.shortcuts_hints_winid = nil
|
||||
obj.augroup = api.nvim_create_augroup("PromptInput", { clear = true })
|
||||
obj.start_insert = opts.start_insert or false
|
||||
obj.submit_callback = opts.submit_callback
|
||||
obj.cancel_callback = opts.cancel_callback
|
||||
obj.close_on_submit = opts.close_on_submit or false
|
||||
obj.win_opts = opts.win_opts
|
||||
obj.default_value = opts.default_value
|
||||
obj.spinner_chars = {
|
||||
"⡀",
|
||||
"⠄",
|
||||
"⠂",
|
||||
"⠁",
|
||||
"⠈",
|
||||
"⠐",
|
||||
"⠠",
|
||||
"⢀",
|
||||
"⣀",
|
||||
"⢄",
|
||||
"⢂",
|
||||
"⢁",
|
||||
"⢈",
|
||||
"⢐",
|
||||
"⢠",
|
||||
"⣠",
|
||||
"⢤",
|
||||
"⢢",
|
||||
"⢡",
|
||||
"⢨",
|
||||
"⢰",
|
||||
"⣰",
|
||||
"⢴",
|
||||
"⢲",
|
||||
"⢱",
|
||||
"⢸",
|
||||
"⣸",
|
||||
"⢼",
|
||||
"⢺",
|
||||
"⢹",
|
||||
"⣹",
|
||||
"⢽",
|
||||
"⢻",
|
||||
"⣻",
|
||||
"⢿",
|
||||
"⣿",
|
||||
"⣶",
|
||||
"⣤",
|
||||
"⣀",
|
||||
}
|
||||
obj.spinner_index = 1
|
||||
obj.spinner_timer = nil
|
||||
obj.spinner_active = false
|
||||
return obj
|
||||
end
|
||||
|
||||
function PromptInput:open()
|
||||
self:close()
|
||||
|
||||
local bufnr = api.nvim_create_buf(false, true)
|
||||
self.bufnr = bufnr
|
||||
vim.bo[bufnr].filetype = "AvanteInput"
|
||||
Utils.mark_as_sidebar_buffer(bufnr)
|
||||
|
||||
local win_opts = vim.tbl_extend("force", {
|
||||
relative = "cursor",
|
||||
width = 40,
|
||||
height = 2,
|
||||
row = 1,
|
||||
col = 0,
|
||||
style = "minimal",
|
||||
border = Config.windows.edit.border,
|
||||
title = { { "Input", "FloatTitle" } },
|
||||
title_pos = "center",
|
||||
}, self.win_opts)
|
||||
|
||||
local winid = api.nvim_open_win(bufnr, true, win_opts)
|
||||
self.winid = winid
|
||||
|
||||
api.nvim_set_option_value("wrap", false, { win = winid })
|
||||
api.nvim_set_option_value("cursorline", true, { win = winid })
|
||||
api.nvim_set_option_value("modifiable", true, { buf = bufnr })
|
||||
|
||||
local default_value_lines = {}
|
||||
if self.default_value then default_value_lines = vim.split(self.default_value, "\n") end
|
||||
if #default_value_lines > 0 then
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, default_value_lines)
|
||||
api.nvim_win_set_cursor(winid, { #default_value_lines, #default_value_lines[#default_value_lines] })
|
||||
end
|
||||
|
||||
self:show_shortcuts_hints()
|
||||
|
||||
self:setup_keymaps()
|
||||
self:setup_autocmds()
|
||||
|
||||
if self.start_insert then vim.cmd([[startinsert!]]) end
|
||||
end
|
||||
|
||||
function PromptInput:close()
|
||||
if not self.bufnr then return end
|
||||
self:stop_spinner()
|
||||
self:close_shortcuts_hints()
|
||||
if api.nvim_get_mode().mode == "i" then vim.cmd([[stopinsert]]) end
|
||||
if self.winid and api.nvim_win_is_valid(self.winid) then
|
||||
api.nvim_win_close(self.winid, true)
|
||||
self.winid = nil
|
||||
end
|
||||
if self.bufnr and api.nvim_buf_is_valid(self.bufnr) then
|
||||
api.nvim_buf_delete(self.bufnr, { force = true })
|
||||
self.bufnr = nil
|
||||
end
|
||||
if self.augroup then
|
||||
api.nvim_del_augroup_by_id(self.augroup)
|
||||
self.augroup = nil
|
||||
end
|
||||
end
|
||||
|
||||
function PromptInput:cancel()
|
||||
self:close()
|
||||
if self.cancel_callback then self.cancel_callback() end
|
||||
end
|
||||
|
||||
function PromptInput:submit(input)
|
||||
if self.close_on_submit then self:close() end
|
||||
if self.submit_callback then self.submit_callback(input) end
|
||||
end
|
||||
|
||||
function PromptInput:show_shortcuts_hints()
|
||||
self:close_shortcuts_hints()
|
||||
|
||||
if not self.winid or not api.nvim_win_is_valid(self.winid) then return end
|
||||
|
||||
local win_width = api.nvim_win_get_width(self.winid)
|
||||
local win_height = api.nvim_win_get_height(self.winid)
|
||||
local buf_height = api.nvim_buf_line_count(self.bufnr)
|
||||
|
||||
local hint_text = (vim.fn.mode() ~= "i" and Config.mappings.submit.normal or Config.mappings.submit.insert)
|
||||
.. ": submit"
|
||||
|
||||
local display_text = hint_text
|
||||
|
||||
if self.spinner_active then
|
||||
local spinner = self.spinner_chars[self.spinner_index]
|
||||
display_text = spinner .. " " .. hint_text
|
||||
end
|
||||
|
||||
local buf = api.nvim_create_buf(false, true)
|
||||
api.nvim_buf_set_lines(buf, 0, -1, false, { display_text })
|
||||
vim.api.nvim_buf_add_highlight(buf, 0, "AvantePopupHint", 0, 0, -1)
|
||||
|
||||
local width = fn.strdisplaywidth(display_text)
|
||||
|
||||
local opts = {
|
||||
relative = "win",
|
||||
win = self.winid,
|
||||
width = width,
|
||||
height = 1,
|
||||
row = math.min(buf_height, win_height),
|
||||
col = math.max(win_width - width, 0),
|
||||
style = "minimal",
|
||||
border = "none",
|
||||
focusable = false,
|
||||
zindex = 100,
|
||||
}
|
||||
|
||||
self.shortcuts_hints_winid = api.nvim_open_win(buf, false, opts)
|
||||
end
|
||||
|
||||
function PromptInput:close_shortcuts_hints()
|
||||
if self.shortcuts_hints_winid and api.nvim_win_is_valid(self.shortcuts_hints_winid) then
|
||||
local buf = api.nvim_win_get_buf(self.shortcuts_hints_winid)
|
||||
api.nvim_win_close(self.shortcuts_hints_winid, true)
|
||||
api.nvim_buf_delete(buf, { force = true })
|
||||
self.shortcuts_hints_winid = nil
|
||||
end
|
||||
end
|
||||
|
||||
function PromptInput:start_spinner()
|
||||
self.spinner_active = true
|
||||
self.spinner_index = 1
|
||||
|
||||
if self.spinner_timer then
|
||||
self.spinner_timer:stop()
|
||||
self.spinner_timer:close()
|
||||
self.spinner_timer = nil
|
||||
end
|
||||
|
||||
self.spinner_timer = vim.loop.new_timer()
|
||||
local spinner_timer = self.spinner_timer
|
||||
|
||||
if self.spinner_timer then
|
||||
self.spinner_timer:start(0, 100, function()
|
||||
vim.schedule(function()
|
||||
if not self.spinner_active or spinner_timer ~= self.spinner_timer then return end
|
||||
self.spinner_index = (self.spinner_index % #self.spinner_chars) + 1
|
||||
self:show_shortcuts_hints()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function PromptInput:stop_spinner()
|
||||
self.spinner_active = false
|
||||
if self.spinner_timer then
|
||||
self.spinner_timer:stop()
|
||||
self.spinner_timer:close()
|
||||
self.spinner_timer = nil
|
||||
end
|
||||
self:show_shortcuts_hints()
|
||||
end
|
||||
|
||||
function PromptInput:setup_keymaps()
|
||||
local bufnr = self.bufnr
|
||||
|
||||
local function get_input()
|
||||
if not bufnr or not api.nvim_buf_is_valid(bufnr) then return "" end
|
||||
local lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
return lines[1] or ""
|
||||
end
|
||||
|
||||
vim.keymap.set(
|
||||
"i",
|
||||
Config.mappings.submit.insert,
|
||||
function() self:submit(get_input()) end,
|
||||
{ buffer = bufnr, noremap = true, silent = true }
|
||||
)
|
||||
|
||||
vim.keymap.set(
|
||||
"n",
|
||||
Config.mappings.submit.normal,
|
||||
function() self:submit(get_input()) end,
|
||||
{ buffer = bufnr, noremap = true, silent = true }
|
||||
)
|
||||
|
||||
vim.keymap.set("n", "<Esc>", function() self:cancel() end, { buffer = bufnr })
|
||||
vim.keymap.set("n", "q", function() self:cancel() end, { buffer = bufnr })
|
||||
end
|
||||
|
||||
function PromptInput:setup_autocmds()
|
||||
local bufnr = self.bufnr
|
||||
local group = self.augroup
|
||||
|
||||
api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
|
||||
group = group,
|
||||
buffer = bufnr,
|
||||
callback = function() self:show_shortcuts_hints() end,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd("ModeChanged", {
|
||||
group = group,
|
||||
pattern = "i:*",
|
||||
callback = function()
|
||||
local cur_buf = api.nvim_get_current_buf()
|
||||
if cur_buf == bufnr then self:show_shortcuts_hints() end
|
||||
end,
|
||||
})
|
||||
|
||||
api.nvim_create_autocmd("ModeChanged", {
|
||||
group = group,
|
||||
pattern = "*:i",
|
||||
callback = function()
|
||||
local cur_buf = api.nvim_get_current_buf()
|
||||
if cur_buf == bufnr then self:show_shortcuts_hints() end
|
||||
end,
|
||||
})
|
||||
|
||||
local quit_id, close_unfocus
|
||||
quit_id = api.nvim_create_autocmd("QuitPre", {
|
||||
group = group,
|
||||
buffer = bufnr,
|
||||
once = true,
|
||||
nested = true,
|
||||
callback = function()
|
||||
self:cancel()
|
||||
if not quit_id then
|
||||
api.nvim_del_autocmd(quit_id)
|
||||
quit_id = nil
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
close_unfocus = api.nvim_create_autocmd("WinLeave", {
|
||||
group = group,
|
||||
buffer = bufnr,
|
||||
callback = function()
|
||||
self:cancel()
|
||||
if close_unfocus then
|
||||
api.nvim_del_autocmd(close_unfocus)
|
||||
close_unfocus = nil
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return PromptInput
|
||||
Reference in New Issue
Block a user