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('', 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('', M.fn.close) fn.map('', M.fn.close) fn.nmap('', M.fn.close) fn.nmap('', M.fn.close) fn.map('', M.fn.complete_or_next_item) fn.map('', M.fn.stop_complete_or_previous_item) if state.cmdline.smart_history then fn.map('', M.fn.up_search_history) fn.map('', M.fn.down_search_history) else fn.map('', M.fn.up_history) fn.map('', M.fn.down_history) end end M.fn.close = function() if vim.fn.pumvisible() == 1 then fn.feedkeys('') else fn.feedkeys('') 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('') else fn.feedkeys('') end end M.fn.stop_complete_or_previous_item = function() if vim.fn.pumvisible() == 1 then fn.feedkeys('') else fn.feedkeys('') end end M.fn.next_item = function() if vim.fn.pumvisible() == 1 then fn.feedkeys('') end end M.fn.previous_item = function() if vim.fn.pumvisible() == 1 then fn.feedkeys('') 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('') 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('') end end end _fine_cmdline_omnifunc = M.omnifunc return M