366 lines
9.7 KiB
Lua
366 lines
9.7 KiB
Lua
-- The MIT License (MIT)
|
|
--
|
|
-- Copyright © 2026 Scott E. Graves <scott.e.graves@protonmail.com>
|
|
--
|
|
-- Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
-- and associated documentation files (the “Software”), to deal in the Software without restriction,
|
|
-- including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
-- sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
|
-- furnished to do so, subject to the following conditions:
|
|
--
|
|
-- The above copyright notice and this permission notice shall be included in all copies or
|
|
-- substantial portions of the Software.
|
|
--
|
|
-- THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
|
|
-- BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
-- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
|
-- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
--
|
|
local M = {}
|
|
|
|
local namespace = vim.api.nvim_create_namespace("DarculaInput")
|
|
local history = {}
|
|
local queue = {}
|
|
local context
|
|
|
|
local function title(prompt)
|
|
prompt = vim.trim(prompt or "Input")
|
|
prompt = prompt:gsub(":$", "")
|
|
return " " .. prompt .. " "
|
|
end
|
|
|
|
local function editor_width(prompt, default)
|
|
local columns = vim.o.columns
|
|
local minimum = math.max(20, math.floor(columns * 0.2))
|
|
local maximum = math.min(140, math.floor(columns * 0.9), columns - 4)
|
|
local preferred = math.max(
|
|
40,
|
|
vim.api.nvim_strwidth(prompt) + 4,
|
|
vim.api.nvim_strwidth(default) + 2
|
|
)
|
|
|
|
minimum = math.min(minimum, maximum)
|
|
return math.max(minimum, math.min(preferred, maximum))
|
|
end
|
|
|
|
local function cursor_width(prompt, default)
|
|
local maximum = math.min(80, vim.o.columns - 2)
|
|
local preferred = math.max(
|
|
32,
|
|
vim.api.nvim_strwidth(title(prompt)),
|
|
vim.api.nvim_strwidth(default) + 2
|
|
)
|
|
return math.min(preferred, maximum)
|
|
end
|
|
|
|
local function window_config(opts, prompt, default)
|
|
if opts.scope == "cursor" or opts.scope == "line" then
|
|
local width = cursor_width(prompt, default)
|
|
local position = vim.fn.screenpos(0, vim.fn.line("."), vim.fn.col("."))
|
|
local screen_column = math.max(0, (position.curscol or position.col or 1) - 1)
|
|
local column = 0
|
|
if screen_column + width + 2 > vim.o.columns then
|
|
column = vim.o.columns - screen_column - width - 2
|
|
end
|
|
|
|
return {
|
|
relative = "cursor",
|
|
row = position.row == 1 and 1 or 0,
|
|
col = column,
|
|
width = width
|
|
}
|
|
end
|
|
|
|
local width = editor_width(prompt, default)
|
|
return {
|
|
relative = "editor",
|
|
row = math.max(0, math.floor((vim.o.lines - 3) / 2)),
|
|
col = math.max(0, math.floor((vim.o.columns - width) / 2)),
|
|
width = width
|
|
}
|
|
end
|
|
|
|
local function set_text(text)
|
|
if not context or not vim.api.nvim_buf_is_valid(context.bufnr) then
|
|
return
|
|
end
|
|
|
|
text = (text or ""):gsub("\n", " ")
|
|
vim.api.nvim_buf_set_lines(context.bufnr, 0, -1, false, {text})
|
|
vim.api.nvim_win_set_cursor(context.winid, {1, #text})
|
|
end
|
|
|
|
local function apply_highlight()
|
|
if not context or not context.opts.highlight then
|
|
return
|
|
end
|
|
|
|
local text = vim.api.nvim_buf_get_lines(context.bufnr, 0, 1, false)[1] or ""
|
|
local highlights
|
|
if type(context.opts.highlight) == "function" then
|
|
highlights = context.opts.highlight(text)
|
|
else
|
|
highlights = vim.fn[context.opts.highlight](text)
|
|
end
|
|
|
|
vim.api.nvim_buf_clear_namespace(context.bufnr, namespace, 0, -1)
|
|
for _, highlight in ipairs(highlights or {}) do
|
|
vim.api.nvim_buf_add_highlight(
|
|
context.bufnr,
|
|
namespace,
|
|
highlight[3],
|
|
0,
|
|
highlight[1],
|
|
highlight[2]
|
|
)
|
|
end
|
|
end
|
|
|
|
local function keep_one_line()
|
|
if not context or not vim.api.nvim_buf_is_valid(context.bufnr) then
|
|
return
|
|
end
|
|
|
|
local lines = vim.api.nvim_buf_get_lines(context.bufnr, 0, -1, false)
|
|
if #lines <= 1 then
|
|
return
|
|
end
|
|
|
|
while #lines > 1 do
|
|
if lines[1]:match("^%s*$") then
|
|
table.remove(lines, 1)
|
|
else
|
|
table.remove(lines)
|
|
end
|
|
end
|
|
vim.api.nvim_buf_set_lines(context.bufnr, 0, -1, false, lines)
|
|
end
|
|
|
|
local function close_completion()
|
|
if vim.fn.pumvisible() == 1 then
|
|
local key = vim.api.nvim_replace_termcodes("<C-e>", true, false, true)
|
|
vim.api.nvim_feedkeys(key, "n", false)
|
|
end
|
|
end
|
|
|
|
local function show_next()
|
|
if context or #queue == 0 then
|
|
return
|
|
end
|
|
|
|
local request = table.remove(queue, 1)
|
|
local opts = request.opts
|
|
local prompt = opts.prompt or "Input"
|
|
local default = (opts.default or ""):gsub("\n", " ")
|
|
local position = window_config(opts, prompt, default)
|
|
local bufnr = vim.api.nvim_create_buf(false, true)
|
|
local mode = vim.api.nvim_get_mode().mode:sub(1, 1)
|
|
local winid = vim.api.nvim_open_win(
|
|
bufnr,
|
|
true,
|
|
{
|
|
relative = position.relative,
|
|
row = position.row,
|
|
col = position.col,
|
|
width = position.width,
|
|
height = 1,
|
|
border = "rounded",
|
|
style = "minimal",
|
|
title = title(prompt),
|
|
title_pos = "left"
|
|
}
|
|
)
|
|
|
|
context = {
|
|
bufnr = bufnr,
|
|
callback = request.callback,
|
|
history_index = nil,
|
|
history_tip = nil,
|
|
mode = mode,
|
|
opts = opts,
|
|
winid = winid
|
|
}
|
|
|
|
vim.bo[bufnr].bufhidden = "wipe"
|
|
vim.bo[bufnr].filetype = "DarculaInput"
|
|
vim.bo[bufnr].swapfile = false
|
|
vim.b[bufnr].minicompletion_disable = true
|
|
vim.wo[winid].list = true
|
|
vim.wo[winid].listchars = "precedes:…,extends:…"
|
|
vim.wo[winid].sidescrolloff = 0
|
|
vim.wo[winid].winhighlight = "Title:Pmenu"
|
|
vim.wo[winid].wrap = false
|
|
|
|
set_text(default)
|
|
|
|
local map_opts = {buffer = bufnr, silent = true}
|
|
vim.keymap.set("n", "<Esc>", M.cancel, map_opts)
|
|
vim.keymap.set("n", "<CR>", M.confirm, map_opts)
|
|
vim.keymap.set("i", "<C-c>", M.cancel, map_opts)
|
|
vim.keymap.set("i", "<CR>", M.confirm, map_opts)
|
|
vim.keymap.set("i", "<Up>", M.history_previous, map_opts)
|
|
vim.keymap.set("i", "<Down>", M.history_next, map_opts)
|
|
|
|
local has_cmp, cmp = pcall(require, "cmp")
|
|
if has_cmp then
|
|
cmp.setup.buffer {
|
|
enabled = opts.completion ~= nil,
|
|
sources = {{name = "omni"}}
|
|
}
|
|
end
|
|
|
|
if opts.completion then
|
|
vim.bo[bufnr].completefunc = "v:lua.require'darcula.ui.input'.completefunc"
|
|
if not has_cmp or not pcall(require, "cmp_omni") then
|
|
vim.keymap.set("i", "<Tab>", M.trigger_completion, {
|
|
buffer = bufnr,
|
|
expr = true,
|
|
silent = true
|
|
})
|
|
end
|
|
end
|
|
|
|
vim.api.nvim_create_autocmd({"TextChanged", "TextChangedI"}, {
|
|
buffer = bufnr,
|
|
callback = function()
|
|
keep_one_line()
|
|
apply_highlight()
|
|
end
|
|
})
|
|
vim.api.nvim_create_autocmd("BufLeave", {
|
|
buffer = bufnr,
|
|
callback = function()
|
|
if context and context.bufnr == bufnr and not context.closing then
|
|
M.cancel()
|
|
end
|
|
end,
|
|
once = true
|
|
})
|
|
|
|
apply_highlight()
|
|
vim.cmd("startinsert!")
|
|
end
|
|
|
|
local function finish(value)
|
|
if not context or context.closing then
|
|
return
|
|
end
|
|
|
|
close_completion()
|
|
local finished = context
|
|
finished.closing = true
|
|
context = nil
|
|
|
|
if value ~= nil and history[#history] ~= value then
|
|
table.insert(history, value)
|
|
end
|
|
|
|
if vim.api.nvim_win_is_valid(finished.winid) then
|
|
vim.api.nvim_win_close(finished.winid, true)
|
|
end
|
|
|
|
if finished.mode ~= "i" then
|
|
vim.cmd("stopinsert")
|
|
end
|
|
|
|
vim.schedule(function()
|
|
vim.schedule(show_next)
|
|
finished.callback(value)
|
|
end)
|
|
end
|
|
|
|
function M.confirm()
|
|
if not context then
|
|
return
|
|
end
|
|
|
|
local text = vim.api.nvim_buf_get_lines(context.bufnr, 0, 1, false)[1] or ""
|
|
finish(text)
|
|
end
|
|
|
|
function M.cancel()
|
|
finish(context and context.opts.cancelreturn or nil)
|
|
end
|
|
|
|
function M.history_previous()
|
|
if not context or #history == 0 then
|
|
return
|
|
end
|
|
|
|
if context.history_index == nil then
|
|
context.history_tip =
|
|
vim.api.nvim_buf_get_lines(context.bufnr, 0, 1, false)[1] or ""
|
|
context.history_index = #history
|
|
elseif context.history_index > 1 then
|
|
context.history_index = context.history_index - 1
|
|
end
|
|
|
|
set_text(history[context.history_index])
|
|
end
|
|
|
|
function M.history_next()
|
|
if not context or not context.history_index then
|
|
return
|
|
end
|
|
|
|
if context.history_index == #history then
|
|
context.history_index = nil
|
|
set_text(context.history_tip)
|
|
return
|
|
end
|
|
|
|
context.history_index = context.history_index + 1
|
|
set_text(history[context.history_index])
|
|
end
|
|
|
|
function M.completefunc(find_start, base)
|
|
local completion = context and context.opts.completion
|
|
if not completion then
|
|
return find_start == 1 and 0 or {}
|
|
end
|
|
if find_start == 1 then
|
|
return 0
|
|
end
|
|
|
|
local pieces = vim.split(completion, ",", {plain = true})
|
|
if pieces[1] == "custom" or pieces[1] == "customlist" then
|
|
local func = pieces[2]
|
|
local result
|
|
if vim.startswith(func, "v:lua.") then
|
|
local lua_func = assert(loadstring("return " .. func:sub(7) .. "(...)"))
|
|
result = lua_func(base, base, vim.fn.strlen(base))
|
|
else
|
|
result = vim.fn[func](base, base, vim.fn.strlen(base))
|
|
end
|
|
return pieces[1] == "custom" and
|
|
vim.split(result, "\n", {plain = true, trimempty = true}) or
|
|
result
|
|
end
|
|
|
|
local ok, result = pcall(vim.fn.getcompletion, base, completion)
|
|
return ok and result or {}
|
|
end
|
|
|
|
function M.trigger_completion()
|
|
return vim.fn.pumvisible() == 1 and "<C-n>" or "<C-x><C-u>"
|
|
end
|
|
|
|
function M.input(opts, callback)
|
|
vim.validate("opts", opts, "table", true)
|
|
vim.validate("callback", callback, "function")
|
|
table.insert(queue, {opts = opts or {}, callback = callback})
|
|
|
|
if vim.v.vim_did_enter == 0 then
|
|
vim.schedule(show_next)
|
|
else
|
|
show_next()
|
|
end
|
|
end
|
|
|
|
function M.setup()
|
|
vim.ui.input = M.input
|
|
end
|
|
|
|
return M
|