You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

409 lines
9.6 KiB

local nui,_ = pcall(require, "nui.input")
if not nui then return end
local M = { fn = {} }
local fn = {}
local state = {
query = '',
history = nil,
idx_hist = 0,
hooks = {},
cmdline = {},
user_opts = {},
prompt_length = 0,
prompt_content = ''
}
local defaults = {
cmdline = {
enable_keymaps = true,
smart_history = true,
prompt = ': '
},
popup = {
position = {
row = '10%',
col = '50%',
},
size = {
width = '60%',
},
border = {
style = 'rounded',
text = {
top="",
top_align="left"
}
},
win_options = {
winhighlight = 'Normal:Normal,FloatBorder:FloatBorder',
},
},
hooks = {
before_mount = function(input)
end,
after_mount = function(input)
end,
set_keymaps = function(imap, feedkeys)
end
}
}
M.inp = nil
fn.check_nvim = function()
if vim.fn.has('nvim-0.7') == 1 then
fn.map = function(lhs, rhs)
vim.keymap.set('i', lhs, rhs, { buffer = M.inp.bufnr, noremap = true })
end
fn.nmap = function(lhs, rhs)
vim.keymap.set('n', lhs, rhs, { buffer = M.inp.bufnr, noremap = true })
end
else
fn.map = function(lhs, rhs)
if type(rhs) == 'string' then
vim.api.nvim_buf_set_keymap(M.inp.bufnr, 'i', lhs, rhs, { noremap = true })
else
M.inp:map('i', lhs, rhs, { noremap = true }, true)
end
end
fn.nmap = function(lhs, rhs)
M.inp:map('n', lhs, rhs, { noremap = true }, true)
end
end
end
M.setup = function(config, input_opts, callback)
config = config or {}
input_opts = input_opts or {}
state.user_opts = config
defaults.cmdline.prompt = input_opts.prompt or ": "
local popup_options = fn.merge(defaults.popup, config.popup)
state.hooks = fn.merge(defaults.hooks, config.hooks)
state.cmdline = fn.merge(defaults.cmdline, config.cmdline)
state.prompt_length = state.cmdline.prompt:len()
state.prompt_content = state.cmdline.prompt
return {
popup = popup_options,
input = {
prompt = state.cmdline.prompt,
default_value = input_opts.default_value,
on_change = fn.on_change(),
on_close = function() fn.reset_history() end,
on_submit = callback
}
}
end
M.open = function(opts, popup_opts, callback)
local pop_opts = defaults.popup
state.user_opts = {}
if popup_opts ~= nil or popup_opts == {} then
pop_opts = popup_opts
end
local state_user_opts = fn.merge(state.user_opts, pop_opts)
local ui = M.setup(state_user_opts, opts, callback)
fn.check_nvim()
M.inp = require('nui.input')(ui.popup, ui.input)
state.hooks.before_mount(M.inp)
M.inp:mount()
vim.bo.omnifunc = 'v:lua._fine_cmdline_omnifunc'
if state.cmdline.enable_keymaps then
fn.keymaps()
end
if vim.fn.has('nvim-0.7') == 0 then
fn.map('<BS>', function() fn.prompt_backspace(state.prompt_length) end)
end
state.hooks.set_keymaps(fn.map, fn.feedkeys)
state.hooks.after_mount(M.inp)
end
fn.on_change = function()
local prev_hist_idx = 0
return function(value)
if prev_hist_idx == state.idx_hist then
state.query = value
return
end
if value == '' then
return
end
prev_hist_idx = state.idx_hist
end
end
fn.keymaps = function()
fn.map('<Esc>', M.fn.close)
fn.map('<C-c>', M.fn.close)
fn.nmap('<Esc>', M.fn.close)
fn.nmap('<C-c>', M.fn.close)
fn.map('<Tab>', M.fn.complete_or_next_item)
fn.map('<S-Tab>', M.fn.stop_complete_or_previous_item)
if state.cmdline.smart_history then
fn.map('<Up>', M.fn.up_search_history)
fn.map('<Down>', M.fn.down_search_history)
else
fn.map('<Up>', M.fn.up_history)
fn.map('<Down>', M.fn.down_history)
end
end
M.fn.close = function()
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-e>')
else
fn.feedkeys('<Space>')
vim.defer_fn(function()
local ok = pcall(M.inp.input_props.on_close)
if not ok then
pcall(vim.api.nvim_win_close, M.inp.winid, true)
pcall(vim.api.nvim_buf_delete, M.inp.bufnr, { force = true })
end
end, 3)
end
end
M.fn.up_search_history = function()
if vim.fn.pumvisible() == 1 then return end
local prompt = state.prompt_length
local line = vim.fn.getline('.')
local user_input = line:sub(prompt + 1, vim.fn.col('.'))
if line:len() == prompt then
M.fn.up_history()
return
end
fn.cmd_history()
local idx = state.idx_hist == 0 and 1 or (state.idx_hist + 1)
while (state.history[idx]) do
local cmd = state.history[idx]
if vim.startswith(cmd, state.query) then
state.idx_hist = idx
fn.replace_line(cmd)
return
end
idx = idx + 1
end
state.idx_hist = 1
if user_input ~= state.query then
fn.replace_line(state.query)
end
end
M.fn.down_search_history = function()
if vim.fn.pumvisible() == 1 then return end
local prompt = state.prompt_length
local line = vim.fn.getline('.')
local user_input = line:sub(prompt + 1, vim.fn.col('.'))
if line:len() == prompt then
M.fn.down_history()
return
end
fn.cmd_history()
local idx = state.idx_hist == 0 and #state.history or (state.idx_hist - 1)
while (state.history[idx]) do
local cmd = state.history[idx]
if vim.startswith(cmd, state.query) then
state.idx_hist = idx
fn.replace_line(cmd)
return
end
idx = idx - 1
end
state.idx_hist = #state.history
if user_input ~= state.query then
fn.replace_line(state.query)
end
end
M.fn.up_history = function()
if vim.fn.pumvisible() == 1 then return end
fn.cmd_history()
state.idx_hist = state.idx_hist + 1
local cmd = state.history[state.idx_hist]
if not cmd then
state.idx_hist = 0
return
end
fn.replace_line(cmd)
end
M.fn.down_history = function()
if vim.fn.pumvisible() == 1 then return end
fn.cmd_history()
state.idx_hist = state.idx_hist - 1
local cmd = state.history[state.idx_hist]
if not cmd then
state.idx_hist = 0
return
end
fn.replace_line(cmd)
end
M.fn.complete_or_next_item = function()
state.uses_completion = true
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-n>')
else
fn.feedkeys('<C-x><C-o>')
end
end
M.fn.stop_complete_or_previous_item = function()
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-p>')
else
fn.feedkeys('<C-x><C-z>')
end
end
M.fn.next_item = function()
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-n>')
end
end
M.fn.previous_item = function()
if vim.fn.pumvisible() == 1 then
fn.feedkeys('<C-p>')
end
end
M.omnifunc = function(start, base)
local prompt_length = state.prompt_length
local line = vim.fn.getline('.')
local input = line:sub(prompt_length + 1)
if start == 1 then
local split = vim.split(input, ' ')
local last_word = split[#split]
local len = #line - #last_word
for i = #split - 1, 1, -1 do
local word = split[i]
if vim.endswith(word, [[\\]]) then
break
elseif vim.endswith(word, [[\]]) then
len = len - #word - 1
else
break
end
end
return len
end
return vim.api.nvim_buf_call(vim.fn.bufnr('#'), function()
return vim.fn.getcompletion(input .. base, 'file')
end)
end
fn.replace_line = function(cmd)
vim.cmd('normal! V"_c')
vim.api.nvim_buf_set_lines(
M.inp.bufnr,
vim.fn.line('.') - 1,
vim.fn.line('.'),
true,
{ state.prompt_content .. cmd }
)
vim.api.nvim_win_set_cursor(
M.inp.winid,
{ vim.fn.line('$'), vim.fn.getline('.'):len() }
)
end
fn.cmd_history = function()
if state.history then return end
local history_string = vim.fn.execute('history cmd')
local history_list = vim.split(history_string, '\n')
local results = {}
for i = #history_list, 3, -1 do
local item = history_list[i]
local _, finish = string.find(item, '%d+ +')
table.insert(results, string.sub(item, finish + 1))
end
state.history = results
end
fn.reset_history = function()
state.idx_hist = 0
state.history = nil
state.query = ''
end
fn.merge = function(defaults, override)
return vim.tbl_deep_extend(
'force',
{},
defaults,
override or {}
)
end
fn.feedkeys = function(keys)
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes(keys, true, true, true),
'n',
true
)
end
fn.prompt_backspace = function(prompt)
local cursor = vim.api.nvim_win_get_cursor(0)
local line = cursor[1]
local col = cursor[2]
if col ~= prompt then
local completion = vim.fn.pumvisible() == 1 and state.uses_completion
if completion then fn.feedkeys('<C-x><C-z>') end
vim.api.nvim_buf_set_text(0, line - 1, col - 1, line - 1, col, { '' })
vim.api.nvim_win_set_cursor(0, { line, col - 1 })
if completion then fn.feedkeys('<C-x><C-o>') end
end
end
_fine_cmdline_omnifunc = M.omnifunc
return M