From c47f5b60d2f4a19b925a3c309c2bf806d86f1891 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Wed, 12 Apr 2023 05:14:15 -0700 Subject: [PATCH 1/3] feat: support for vim.ui.confirm --- README.md | 12 +-- lua/dressing/config.lua | 39 +++++++++ lua/dressing/confirm.lua | 176 +++++++++++++++++++++++++++++++++++++++ lua/dressing/patch.lua | 2 +- lua/dressing/util.lua | 25 +++++- tests/manual/confirm.lua | 14 ++++ 6 files changed, 259 insertions(+), 9 deletions(-) create mode 100644 lua/dressing/confirm.lua create mode 100644 tests/manual/confirm.lua diff --git a/README.md b/README.md index 21de002..86500bb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # Dressing.nvim -With the release of Neovim 0.6 we were given the start of extensible core UI -hooks ([vim.ui.select](https://github.com/neovim/neovim/pull/15771) and -[vim.ui.input](https://github.com/neovim/neovim/pull/15959)). They exist to -allow plugin authors to override them with improvements upon the default -behavior, so that's exactly what we're going to do. +Neovim has several utility functions under the `vim.ui.*` namespace for accepting input from the user. Dressing replaces the default functions with prettier implementations that use floating windows. It is a goal to match and not extend the core Neovim API. All options that core respects will be respected, and we will not accept any custom parameters or @@ -26,10 +22,14 @@ Neovim 0.7.0+ (for earlier versions, use the [nvim-0.5 branch](https://github.co ## Screenshots -`vim.input` replacement (handling a LSP rename) +`vim.input` (handling a LSP rename) ![Screenshot from 2021-12-09 17-36-16](https://user-images.githubusercontent.com/506791/145502533-3dc2f87d-95ea-422d-a318-12c0092f1bdf.png) +`vim.confirm` + +TODO + `vim.select` (telescope) ![Screenshot from 2021-12-02 19-46-01](https://user-images.githubusercontent.com/506791/144541916-4fa60c50-cadc-4f0f-b3c1-6307310e6e99.png) diff --git a/lua/dressing/config.lua b/lua/dressing/config.lua index fc3e208..6303371 100644 --- a/lua/dressing/config.lua +++ b/lua/dressing/config.lua @@ -158,6 +158,45 @@ local default_config = { -- Used to override format_item. See :help dressing-format format_item_override = {}, + -- see :help dressing_get_config + get_config = nil, + }, + confirm = { + -- Set to false to disable the vim.ui.confirm implementation + enabled = true, + + -- Title of the confirm window + title = "Confirm", + + -- Can be 'left', 'right', or 'center' + title_pos = "center", + + border = "rounded", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + width = nil, + -- min_width and max_width can be a list of mixed types. + -- min_width = {20, 0.2} means "the greater of 20 columns or 20% of total" + max_width = { 140, 0.9 }, + min_width = { 20, 0.2 }, + height = nil, + max_height = 0.9, + min_height = { 4, 0.2 }, + + buf_options = {}, + win_options = { + -- Window transparency (0-100) + winblend = 10, + -- Disable line wrapping + wrap = false, + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + -- see :help dressing_get_config get_config = nil, }, diff --git a/lua/dressing/confirm.lua b/lua/dressing/confirm.lua new file mode 100644 index 0000000..dc2418c --- /dev/null +++ b/lua/dressing/confirm.lua @@ -0,0 +1,176 @@ +local global_config = require("dressing.config") +local patch = require("dressing.patch") +local util = require("dressing.util") + +return function(message, opts, callback) + local config = global_config.get_mod_config("confirm", message, opts) + if not config.enabled then + return patch.original_mods.confirm(opts, callback) + end + vim.validate({ + message = { message, "s" }, + opts = { opts, "t" }, + callback = { callback, "f" }, + }) + vim.validate({ + choices = { opts.choices, "t", true }, + default = { opts.default, "n", true }, + type = { opts.type, "s", true }, + }) + vim.api.nvim_set_hl(0, "ConfirmCursor", { blend = 100, default = true }) + if not opts.choices or vim.tbl_isempty(opts.choices) then + opts.choices = { "&OK" } + end + if not opts.default then + opts.default = 1 + end + -- TODO this doesn't do anything yet + if not opts.type then + opts.type = "G" + else + opts.type = string.sub(opts.type, 1, 1) + end + + local bufnr = vim.api.nvim_create_buf(false, true) + vim.bo[bufnr].buftype = "nofile" + vim.bo[bufnr].bufhidden = "wipe" + vim.bo[bufnr].swapfile = false + local winid + + local guicursor = vim.o.guicursor + local function choose(idx) + local cb = callback + callback = function(_) end + if winid then + vim.api.nvim_win_close(winid, true) + end + vim.o.guicursor = guicursor + local choice = opts.choices[idx] + if choice then + choice = choice:gsub("&", "") + end + cb(idx, choice) + end + local function cancel() + choose(0) + end + + local clean_choices = {} + local choice_shortcut_idx = {} + for i, choice in ipairs(opts.choices) do + local idx = choice:find("&") + local key + if idx and idx < string.len(choice) then + table.insert(clean_choices, choice:sub(1, idx - 1) .. choice:sub(idx + 1)) + key = choice:sub(idx + 1, idx + 1) + table.insert(choice_shortcut_idx, idx) + else + key = choice:sub(1, 1) + table.insert(clean_choices, choice) + table.insert(choice_shortcut_idx, 1) + end + vim.keymap.set("n", key:lower(), function() + choose(i) + end, { buffer = bufnr }) + vim.keymap.set("n", key:upper(), function() + choose(i) + end, { buffer = bufnr }) + end + vim.keymap.set("n", "", cancel, { buffer = bufnr }) + vim.keymap.set("n", "", cancel, { buffer = bufnr }) + + local lines = vim.split(message, "\n") + local highlights = {} + table.insert(lines, "") + + -- Calculate the width of the choices if they are on a single line + local choices_width = 0 + for _, choice in ipairs(clean_choices) do + choices_width = choices_width + vim.api.nvim_strwidth(choice) + end + -- Make sure to account for spacing + choices_width = choices_width + #clean_choices - 1 + + local desired_width = choices_width + for _, line in ipairs(lines) do + local len = string.len(line) + if len > desired_width then + desired_width = len + end + end + + local width = util.calculate_width("editor", desired_width, config) + + if width < choices_width then + -- Render one choice per line + for i, choice in ipairs(clean_choices) do + table.insert(lines, choice) + table.insert(highlights, { "Keyword", #lines, choice_shortcut_idx[i] - 1 }) + end + else + -- Render all choices on a single line + local extra_spacing = width - choices_width + local line = "" + local num_dividers = #clean_choices - 1 + for i, choice in ipairs(clean_choices) do + if i > 1 then + line = line .. " " .. string.rep(" ", math.floor(extra_spacing / num_dividers)) + if extra_spacing % num_dividers >= i then + line = line .. " " + end + end + local col_start = line:len() - 1 + line = line .. choice + table.insert(highlights, { "Keyword", #lines + 1, col_start + choice_shortcut_idx[i] }) + end + table.insert(lines, line) + end + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, lines) + local ns = vim.api.nvim_create_namespace("confirm") + for _, hl in ipairs(highlights) do + local group, lnum, col_start, col_end = unpack(hl) + if not col_end then + col_end = col_start + 1 + end + vim.api.nvim_buf_add_highlight(bufnr, ns, group, lnum - 1, col_start, col_end) + end + + local height = util.calculate_height("editor", #lines, config) + local winopt = { + relative = "editor", + border = config.border, + zindex = config.zindex, + style = "minimal", + width = width, + height = height, + col = math.floor((util.get_editor_width() - width) / 2), + row = math.floor((util.get_editor_height() - height) / 2), + } + if vim.fn.has("nvim-0.9") == 1 and config.title ~= "" then + winopt.title = config.title + winopt.title_pos = config.title_pos + end + winopt = config.override(winopt) or winopt + winid = vim.api.nvim_open_win(bufnr, true, winopt) + for k, v in pairs(config.buf_options) do + vim.api.nvim_buf_set_option(bufnr, k, v) + end + for k, v in pairs(config.win_options) do + vim.api.nvim_win_set_option(winid, k, v) + end + vim.o.guicursor = "a:ConfirmCursor" + + vim.api.nvim_create_autocmd("BufLeave", { + buffer = bufnr, + callback = cancel, + once = true, + nested = true, + }) + vim.api.nvim_create_autocmd("WinLeave", { + callback = cancel, + once = true, + nested = true, + }) + vim.bo[bufnr].modifiable = false +end diff --git a/lua/dressing/patch.lua b/lua/dressing/patch.lua index 169747d..c05377c 100644 --- a/lua/dressing/patch.lua +++ b/lua/dressing/patch.lua @@ -1,4 +1,4 @@ -local all_modules = { "input", "select" } +local all_modules = { "confirm", "input", "select" } local M = {} diff --git a/lua/dressing/util.lua b/lua/dressing/util.lua index 8e9f639..8e9471f 100644 --- a/lua/dressing/util.lua +++ b/lua/dressing/util.lua @@ -42,9 +42,30 @@ local function calculate_dim(desired_size, size, min_size, max_size, total_size) return math.floor(ret) end +---@return integer +M.get_editor_width = function() + return vim.o.columns +end + +---@return integer +M.get_editor_height = function() + local editor_height = vim.o.lines - vim.o.cmdheight + -- Subtract 1 if tabline is visible + if vim.o.showtabline == 2 or (vim.o.showtabline == 1 and #vim.api.nvim_list_tabpages() > 1) then + editor_height = editor_height - 1 + end + -- Subtract 1 if statusline is visible + if + vim.o.laststatus >= 2 or (vim.o.laststatus == 1 and #vim.api.nvim_tabpage_list_wins(0) > 1) + then + editor_height = editor_height - 1 + end + return editor_height +end + local function get_max_width(relative, winid) if relative == "editor" then - return vim.o.columns + return M.get_editor_width() else return vim.api.nvim_win_get_width(winid or 0) end @@ -52,7 +73,7 @@ end local function get_max_height(relative, winid) if relative == "editor" then - return vim.o.lines - vim.o.cmdheight + return M.get_editor_height() else return vim.api.nvim_win_get_height(winid or 0) end diff --git a/tests/manual/confirm.lua b/tests/manual/confirm.lua new file mode 100644 index 0000000..2a3d475 --- /dev/null +++ b/tests/manual/confirm.lua @@ -0,0 +1,14 @@ +-- Run this test with :source % + +local function run_test() + vim.ui.confirm("Pick a fruit", { choices = { "Apple", "Banana", "O&range" } }, function(idx, text) + if idx then + vim.notify(string.format("selected '%d' (%s)", idx, text), vim.log.levels.INFO) + else + vim.notify("Canceled", vim.log.levels.INFO) + end + end) +end + +-- Replace this with the desired backend to test +run_test() From 87acc02fffaaea721f74a52b1fc7681cd6eb13da Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 12 Apr 2023 12:14:33 +0000 Subject: [PATCH 2/3] [docgen] Update README.md skip-checks: true --- README.md | 39 +++++++++++++++++++++++++++++++++++++++ doc/dressing.txt | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/README.md b/README.md index 86500bb..22b5117 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,45 @@ require('dressing').setup({ -- Used to override format_item. See :help dressing-format format_item_override = {}, + -- see :help dressing_get_config + get_config = nil, + }, + confirm = { + -- Set to false to disable the vim.ui.confirm implementation + enabled = true, + + -- Title of the confirm window + title = "Confirm", + + -- Can be 'left', 'right', or 'center' + title_pos = "center", + + border = "rounded", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + width = nil, + -- min_width and max_width can be a list of mixed types. + -- min_width = {20, 0.2} means "the greater of 20 columns or 20% of total" + max_width = { 140, 0.9 }, + min_width = { 20, 0.2 }, + height = nil, + max_height = 0.9, + min_height = { 4, 0.2 }, + + buf_options = {}, + win_options = { + -- Window transparency (0-100) + winblend = 10, + -- Disable line wrapping + wrap = false, + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + -- see :help dressing_get_config get_config = nil, }, diff --git a/doc/dressing.txt b/doc/dressing.txt index 4d6cdbb..2975cc9 100644 --- a/doc/dressing.txt +++ b/doc/dressing.txt @@ -165,6 +165,45 @@ Configure dressing.nvim by calling the setup() function. -- Used to override format_item. See :help dressing-format format_item_override = {}, + -- see :help dressing_get_config + get_config = nil, + }, + confirm = { + -- Set to false to disable the vim.ui.confirm implementation + enabled = true, + + -- Title of the confirm window + title = "Confirm", + + -- Can be 'left', 'right', or 'center' + title_pos = "center", + + border = "rounded", + + -- These can be integers or a float between 0 and 1 (e.g. 0.4 for 40%) + width = nil, + -- min_width and max_width can be a list of mixed types. + -- min_width = {20, 0.2} means "the greater of 20 columns or 20% of total" + max_width = { 140, 0.9 }, + min_width = { 20, 0.2 }, + height = nil, + max_height = 0.9, + min_height = { 4, 0.2 }, + + buf_options = {}, + win_options = { + -- Window transparency (0-100) + winblend = 10, + -- Disable line wrapping + wrap = false, + }, + + override = function(conf) + -- This is the config that will be passed to nvim_open_win. + -- Change values here to customize the layout + return conf + end, + -- see :help dressing_get_config get_config = nil, }, From 68348df383806233bb33ba09d940d55c41bb8f71 Mon Sep 17 00:00:00 2001 From: Steven Arcangeli Date: Wed, 12 Apr 2023 09:29:04 -0700 Subject: [PATCH 3/3] doc: add screenshot of vim.ui.confirm --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22b5117..da57b50 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ Neovim 0.7.0+ (for earlier versions, use the [nvim-0.5 branch](https://github.co `vim.confirm` -TODO +![Screenshot 2023-04-12 at 9 27 53 AM](https://user-images.githubusercontent.com/506791/231521989-3a4049ae-f56e-4864-b0df-4dc9702f14b1.png) `vim.select` (telescope)