diff --git a/.cspell/words.txt b/.cspell/words.txt index 1507d5e..ea6adb8 100644 --- a/.cspell/words.txt +++ b/.cspell/words.txt @@ -102,6 +102,7 @@ ctermfg cursorcolumn cursorline cust_ +customlist cword dapui darcula @@ -169,6 +170,7 @@ gerrand getbufinfo getcmdline getcmdtype +getcompletion getcwd getreg gettype @@ -271,6 +273,7 @@ mfussenegger mhartington mhinz milewski +minicompletion_disable minipairs minipairs_cr minisnippets @@ -322,6 +325,7 @@ nvim nvim_feedkeys nvimrc olivercederborg +omni omnifunc omnisharp openai_params @@ -357,6 +361,7 @@ reflog reindent_linewise reineke relativenumber +require ringbuf rktjmp rustfmt @@ -390,11 +395,13 @@ softtabstop splitbelow splitright starlark +startswith statox statusline stdpath stevearc stlnc +strlen stroustrup struct strwidth @@ -423,6 +430,7 @@ to_vimscript topdelete tpope treesitter +trimempty truecolor tspunctbracket tspunctdelimiter diff --git a/nvim/lua/darcula/plugins.lua b/nvim/lua/darcula/plugins.lua index 66489bb..a66407f 100644 --- a/nvim/lua/darcula/plugins.lua +++ b/nvim/lua/darcula/plugins.lua @@ -110,7 +110,6 @@ vim.list_extend( {src = "https://github.com/rcarriga/nvim-dap-ui"}, {src = "https://github.com/rktjmp/shipwright.nvim"}, {src = "https://github.com/sgraves76/cppman.nvim"}, - {src = "https://github.com/stevearc/dressing.nvim"}, {src = "https://github.com/ten3roberts/window-picker.nvim"}, {src = "https://github.com/tpope/vim-dispatch"}, {src = "https://github.com/tpope/vim-fugitive"} diff --git a/nvim/lua/darcula/setup/500_dressing.lua b/nvim/lua/darcula/setup/500_input.lua similarity index 87% rename from nvim/lua/darcula/setup/500_dressing.lua rename to nvim/lua/darcula/setup/500_input.lua index fd790d7..470bf9b 100644 --- a/nvim/lua/darcula/setup/500_dressing.lua +++ b/nvim/lua/darcula/setup/500_input.lua @@ -17,14 +17,4 @@ -- 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. -- -require("dressing").setup { - input = { - relative = "editor", - win_options = { - winhighlight = "Title:Pmenu" - } - }, - select = { - relative = "editor" - } -} +require("darcula.ui.input").setup() diff --git a/nvim/lua/darcula/ui/input.lua b/nvim/lua/darcula/ui/input.lua new file mode 100644 index 0000000..ef7e952 --- /dev/null +++ b/nvim/lua/darcula/ui/input.lua @@ -0,0 +1,365 @@ +-- 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