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.
399 lines
9.3 KiB
399 lines
9.3 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',
|
|
},
|
|
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, callback)
|
|
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
|
|
|