-- The MIT License (MIT) -- -- Copyright © 2026 Scott E. Graves -- -- 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("", 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", "", M.cancel, map_opts) vim.keymap.set("n", "", M.confirm, map_opts) vim.keymap.set("i", "", M.cancel, map_opts) vim.keymap.set("i", "", M.confirm, map_opts) vim.keymap.set("i", "", M.history_previous, map_opts) vim.keymap.set("i", "", 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", "", 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 "" or "" 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