mirror of
https://github.com/folke/snacks.nvim
synced 2025-07-07 13:15:08 +00:00
298 lines
7.6 KiB
Lua
298 lines
7.6 KiB
Lua
---@class snacks.input
|
|
---@overload fun(opts: snacks.input.Opts, on_confirm: fun(value?: string)): snacks.win
|
|
local M = setmetatable({}, {
|
|
__call = function(M, ...)
|
|
return M.input(...)
|
|
end,
|
|
})
|
|
|
|
M.meta = {
|
|
desc = "Better `vim.ui.input`",
|
|
needs_setup = true,
|
|
}
|
|
|
|
---@alias snacks.input.Pos "left"|"title"|false
|
|
|
|
---@class snacks.input.Config
|
|
---@field enabled? boolean
|
|
---@field win? snacks.win.Config|{}
|
|
---@field icon? string
|
|
---@field icon_pos? snacks.input.Pos
|
|
---@field prompt_pos? snacks.input.Pos
|
|
local defaults = {
|
|
icon = " ",
|
|
icon_hl = "SnacksInputIcon",
|
|
icon_pos = "left",
|
|
prompt_pos = "title",
|
|
win = { style = "input" },
|
|
expand = true,
|
|
}
|
|
|
|
Snacks.util.set_hl({
|
|
Icon = "DiagnosticHint",
|
|
Normal = "Normal",
|
|
Border = "DiagnosticInfo",
|
|
Title = "DiagnosticInfo",
|
|
Prompt = "SnacksInputTitle",
|
|
}, { prefix = "SnacksInput", default = true })
|
|
|
|
Snacks.config.style("input", {
|
|
backdrop = false,
|
|
position = "float",
|
|
border = "rounded",
|
|
title_pos = "center",
|
|
height = 1,
|
|
width = 60,
|
|
relative = "editor",
|
|
noautocmd = true,
|
|
row = 2,
|
|
-- relative = "cursor",
|
|
-- row = -3,
|
|
-- col = 0,
|
|
wo = {
|
|
winhighlight = "NormalFloat:SnacksInputNormal,FloatBorder:SnacksInputBorder,FloatTitle:SnacksInputTitle",
|
|
cursorline = false,
|
|
},
|
|
bo = {
|
|
filetype = "snacks_input",
|
|
buftype = "prompt",
|
|
},
|
|
--- buffer local variables
|
|
b = {
|
|
completion = false, -- disable blink completions in input
|
|
},
|
|
keys = {
|
|
n_esc = { "<esc>", { "cmp_close", "cancel" }, mode = "n", expr = true },
|
|
i_esc = { "<esc>", { "cmp_close", "stopinsert" }, mode = "i", expr = true },
|
|
i_cr = { "<cr>", { "cmp_accept", "confirm" }, mode = { "i", "n" }, expr = true },
|
|
i_tab = { "<tab>", { "cmp_select_next", "cmp" }, mode = "i", expr = true },
|
|
i_ctrl_w = { "<c-w>", "<c-s-w>", mode = "i", expr = true },
|
|
i_up = { "<up>", { "hist_up" }, mode = { "i", "n" } },
|
|
i_down = { "<down>", { "hist_down" }, mode = { "i", "n" } },
|
|
q = "cancel",
|
|
},
|
|
})
|
|
|
|
local ui_input = vim.ui.input
|
|
|
|
---@class snacks.input.Opts: snacks.input.Config,{}
|
|
---@field prompt? string
|
|
---@field default? string
|
|
---@field completion? string
|
|
---@field highlight? fun()
|
|
|
|
---@class snacks.input.ctx
|
|
---@field opts? snacks.input.Opts
|
|
---@field win? snacks.win
|
|
local ctx = {}
|
|
|
|
---@param opts? snacks.input.Opts
|
|
---@param on_confirm fun(value?: string)
|
|
function M.input(opts, on_confirm)
|
|
assert(type(on_confirm) == "function", "`on_confirm` must be a function")
|
|
|
|
local history = require("snacks.picker.util.history").new("input", {
|
|
filter = function(value)
|
|
return value ~= ""
|
|
end,
|
|
})
|
|
|
|
local parent_win = vim.api.nvim_get_current_win()
|
|
local mode = vim.fn.mode()
|
|
|
|
---@param force? boolean
|
|
local function record(force)
|
|
if not ctx.win then
|
|
return
|
|
end
|
|
if not force and not history:is_current() then
|
|
return
|
|
end
|
|
local text = vim.trim(ctx.win:text())
|
|
if text == "" then
|
|
return
|
|
end
|
|
history:record(text)
|
|
end
|
|
|
|
local function confirm(value)
|
|
record()
|
|
ctx.win = nil
|
|
ctx.opts = nil
|
|
vim.cmd.stopinsert()
|
|
vim.schedule(function()
|
|
if vim.api.nvim_win_is_valid(parent_win) then
|
|
vim.api.nvim_set_current_win(parent_win)
|
|
if mode == "i" then
|
|
vim.cmd("startinsert")
|
|
end
|
|
end
|
|
on_confirm(value)
|
|
end)
|
|
end
|
|
|
|
opts = Snacks.config.get("input", defaults, opts) --[[@as snacks.input.Opts]]
|
|
opts.prompt = opts.prompt or "Input"
|
|
opts.prompt = vim.trim(opts.prompt)
|
|
opts.prompt = opts.prompt_pos == "title" and opts.prompt:gsub(":$", "") or opts.prompt
|
|
|
|
local title, statuscolumn = {}, {} ---@type string[], string[]
|
|
local function add(text, hl, pos)
|
|
if pos == "title" then
|
|
table.insert(title, { " " .. text, hl })
|
|
else
|
|
table.insert(statuscolumn, "%#" .. hl .. "#" .. text)
|
|
end
|
|
end
|
|
|
|
if opts.icon_pos and (opts.icon or "") ~= "" then
|
|
add(opts.icon, "SnacksInputIcon", opts.icon_pos)
|
|
end
|
|
add(opts.prompt, "SnacksInputTitle", opts.prompt_pos)
|
|
|
|
if next(title) then
|
|
table.insert(title, { " ", "SnacksInputTitle" })
|
|
end
|
|
|
|
---@param text? string
|
|
local function set(text)
|
|
text = text or ""
|
|
vim.api.nvim_buf_set_lines(ctx.win.buf, 0, -1, false, { text })
|
|
vim.api.nvim_win_set_cursor(ctx.win.win, { 1, #text })
|
|
end
|
|
|
|
opts.win = Snacks.win.resolve("input", opts.win, {
|
|
enter = true,
|
|
title = next(title) and title or nil,
|
|
bo = {
|
|
modifiable = true,
|
|
completefunc = "v:lua.Snacks.input.complete",
|
|
omnifunc = "v:lua.Snacks.input.complete",
|
|
},
|
|
wo = {
|
|
statuscolumn = next(statuscolumn) and " " .. table.concat(statuscolumn, " ") .. " " or " ",
|
|
},
|
|
actions = {
|
|
cancel = function(self)
|
|
confirm()
|
|
self:close()
|
|
end,
|
|
stopinsert = function()
|
|
vim.cmd("stopinsert")
|
|
end,
|
|
confirm = function(self)
|
|
confirm(self:text())
|
|
self:close()
|
|
end,
|
|
hist_up = function(self)
|
|
record()
|
|
set(history:prev())
|
|
end,
|
|
hist_down = function(self)
|
|
record()
|
|
set(history:next())
|
|
end,
|
|
cmp = function()
|
|
return vim.fn.pumvisible() == 0 and "<c-x><c-u>"
|
|
end,
|
|
cmp_close = function()
|
|
return vim.fn.pumvisible() == 1 and "<c-e>"
|
|
end,
|
|
cmp_accept = function()
|
|
return vim.fn.pumvisible() == 1 and "<c-y>"
|
|
end,
|
|
cmp_select_next = function()
|
|
return vim.fn.pumvisible() == 1 and "<c-n>"
|
|
end,
|
|
cmp_select_prev = function()
|
|
return vim.fn.pumvisible() == 1 and "<c-p>"
|
|
end,
|
|
},
|
|
})
|
|
|
|
local parent_zindex = vim.api.nvim_win_get_config(parent_win).zindex
|
|
opts.win.zindex = parent_zindex and parent_zindex + 1 or opts.win.zindex
|
|
|
|
local min_width = opts.win.width or 60
|
|
if opts.expand then
|
|
---@param self snacks.win
|
|
opts.win.width = function(self)
|
|
local w = type(min_width) == "function" and min_width(self) or min_width --[[@as number]]
|
|
return math.max(w, vim.api.nvim_strwidth(self:text()) + 5)
|
|
end
|
|
end
|
|
|
|
local win = Snacks.win(opts.win)
|
|
ctx = { opts = opts, win = win }
|
|
vim.fn.prompt_setprompt(win.buf, "")
|
|
if opts.default then
|
|
vim.api.nvim_buf_set_lines(win.buf, 0, -1, false, { opts.default })
|
|
end
|
|
|
|
vim.api.nvim_win_call(win.win, function()
|
|
vim.cmd("startinsert!")
|
|
end)
|
|
|
|
vim.fn.prompt_setcallback(win.buf, function(text)
|
|
confirm(text)
|
|
win:close()
|
|
end)
|
|
vim.fn.prompt_setinterrupt(win.buf, function()
|
|
confirm()
|
|
win:close()
|
|
end)
|
|
|
|
win:on({ "TextChangedI", "TextChanged" }, function()
|
|
if not win:valid() then
|
|
return
|
|
end
|
|
vim.bo[win.buf].modified = false
|
|
if opts.expand then
|
|
if vim.api.nvim_win_is_valid(parent_win) then
|
|
vim.api.nvim_win_call(parent_win, function()
|
|
win:update()
|
|
end)
|
|
end
|
|
vim.api.nvim_win_call(win.win, function()
|
|
vim.fn.winrestview({ leftcol = 0 })
|
|
end)
|
|
end
|
|
end, { buf = true })
|
|
return win
|
|
end
|
|
|
|
---@param findstart number
|
|
---@param base string
|
|
---@private
|
|
function M.complete(findstart, base)
|
|
local completion = ctx.opts.completion
|
|
if findstart == 1 then
|
|
return 0
|
|
end
|
|
if not completion then
|
|
return {}
|
|
end
|
|
local ok, results = pcall(vim.fn.getcompletion, base, completion)
|
|
return ok and results or {}
|
|
end
|
|
|
|
function M.enable()
|
|
vim.ui.input = M.input
|
|
end
|
|
|
|
function M.disable()
|
|
vim.ui.input = ui_input
|
|
end
|
|
|
|
---@private
|
|
function M.health()
|
|
if Snacks.config.get("input", defaults).enabled then
|
|
if vim.ui.input == M.input then
|
|
Snacks.health.ok("`vim.ui.input` is set to `Snacks.input`")
|
|
else
|
|
Snacks.health.error("`vim.ui.input` is not set to `Snacks.input`")
|
|
end
|
|
end
|
|
end
|
|
|
|
return M
|