From c58abba2f58f423f14b63015ad9dbde7b7dc9e33 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Tue, 10 Aug 2021 10:25:12 -0400 Subject: [PATCH 1/6] feat: Move all of popup into plenary --- lua/plenary/popup.lua | 209 ------------------ lua/plenary/popup/init.lua | 398 +++++++++++++++++++++++++++++++++++ lua/plenary/popup/utils.lua | 30 +++ tests/plenary/popup_spec.lua | 132 ++++++++++++ 4 files changed, 560 insertions(+), 209 deletions(-) delete mode 100644 lua/plenary/popup.lua create mode 100644 lua/plenary/popup/init.lua create mode 100644 lua/plenary/popup/utils.lua create mode 100644 tests/plenary/popup_spec.lua diff --git a/lua/plenary/popup.lua b/lua/plenary/popup.lua deleted file mode 100644 index bb53c7d4..00000000 --- a/lua/plenary/popup.lua +++ /dev/null @@ -1,209 +0,0 @@ ---- popup.lua ---- ---- Wrapper to make the popup api from vim in neovim. ---- Hope to get this part merged in at some point in the future. - -local vim = vim - -local Border = require "plenary.window.border" - -local popup = {} - -popup._pos_map = { - topleft = "NW", - topright = "NE", - botleft = "SW", - botright = "SE", -} - --- Keep track of hidden popups, so we can load them with popup.show() -popup._hidden = {} - -local function dict_default(options, key, default) - if options[key] == nil then - return default[key] - else - return options[key] - end -end - -function popup.popup_create(what, vim_options) - local bufnr - if type(what) == "number" then - bufnr = what - else - bufnr = vim.api.nvim_create_buf(false, true) - assert(bufnr, "Failed to create buffer") - - -- TODO: Handle list of lines - vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, { what }) - end - - local option_defaults = { - posinvert = true, - } - - local win_opts = {} - - if vim_options.line then - -- TODO: Need to handle "cursor", "cursor+1", ... - win_opts.row = vim_options.line - else - -- TODO: It says it needs to be "vertically cenetered"?... - -- wut is that. - win_opts.row = 0 - end - - if vim_options.col then - -- TODO: Need to handle "cursor", "cursor+1", ... - win_opts.col = vim_options.col - else - -- TODO: It says it needs to be "horizontally cenetered"?... - win_opts.col = 0 - end - - if vim_options.pos then - if vim_options.pos == "center" then - -- TODO: Do centering.. - else - win_opts.anchor = popup._pos_map[vim_options.pos] - end - end - - -- posinvert When FALSE the value of "pos" is always used. When - -- TRUE (the default) and the popup does not fit - -- vertically and there is more space on the other side - -- then the popup is placed on the other side of the - -- position indicated by "line". - if dict_default(vim_options, "posinvert", option_defaults) then - -- TODO: handle the invert thing - end - - -- fixed When FALSE (the default), and: - -- - "pos" is "botleft" or "topleft", and - -- - "wrap" is off, and - -- - the popup would be truncated at the right edge of - -- the screen, then - -- the popup is moved to the left so as to fit the - -- contents on the screen. Set to TRUE to disable this. - - win_opts.style = "minimal" - - -- Feels like maxheigh, minheight, maxwidth, minwidth will all be related - win_opts.height = 5 - win_opts.width = 25 - - -- textprop When present the popup is positioned next to a text - -- property with this name and will move when the text - -- property moves. Use an empty string to remove. See - -- |popup-textprop-pos|. - -- related: - -- textpropwin - -- textpropid - - -- border - local border_options = {} - if vim_options.border then - local b_top, b_rgight, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft - if vim_options.borderchars == nil then - b_top, b_rgight, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = { - "-", - "|", - "-", - "|", - "┌", - "┐", - "┘", - "└", - } - elseif #vim_options.borderchars == 1 then - -- TODO: Unpack 8 times cool to the same vars - print "..." - elseif #vim_options.borderchars == 2 then - -- TODO: Unpack to edges & corners - print "..." - elseif #vim_options.borderchars == 8 then - b_top, b_rgight, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = vim_options.borderhighlight - end - end - - win_opts.relative = "editor" - - local win_id - if vim_options.hidden then - assert(false, "I have not implemented this yet and don't know how") - else - win_id = vim.api.nvim_open_win(bufnr, true, win_opts) - end - - -- Moved, handled after since we need the window ID - if vim_options.moved then - if vim_options.moved == "any" then - vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id) - elseif vim_options.moved == "word" then - -- TODO: Handle word, WORD, expr, and the range functions... which seem hard? - end - else - vim.cmd(string.format("autocmd BufLeave ++once call nvim_win_close(%s, v:false)", bufnr, win_id)) - end - - if vim_options.time then - local timer = vim.loop.new_timer() - timer:start( - vim_options.time, - 0, - vim.schedule_wrap(function() - vim.api.nvim_close_win(win_id, false) - end) - ) - end - - -- Buffer Options - if vim_options.cursorline then - vim.api.nvim_win_set_option(0, "cursorline", true) - end - - -- vim.api.nvim_win_set_option(0, 'wrap', dict_default(vim_options, 'wrap', option_defaults)) - - -- ===== Not Implemented Options ===== - -- flip: not implemented at the time of writing - -- Mouse: - -- mousemoved: no idea how to do the things with the mouse, so it's an exercise for the reader. - -- drag: mouses are hard - -- resize: mouses are hard - -- close: mouses are hard - -- - -- scrollbar - -- scrollbarhighlight - -- thumbhighlight - -- - -- tabpage: seems useless - - -- Create border - - -- title - if vim_options.title then - border_options.title = vim_options.title - - if vim_options.border == 0 or vim_options.border == nil then - vim_options.border = 1 - border_options.width = 1 - end - end - - if vim_options.border then - Border:new(bufnr, win_id, win_opts, border_options) - end - - -- TODO: Perhaps there's a way to return an object that looks like a window id, - -- but actually has some extra metadata about it. - -- - -- This would make `hidden` a lot easier to manage - return win_id -end - -function popup.show(self, asdf) end - -popup.show = function() end - -return popup diff --git a/lua/plenary/popup/init.lua b/lua/plenary/popup/init.lua new file mode 100644 index 00000000..a39bbda5 --- /dev/null +++ b/lua/plenary/popup/init.lua @@ -0,0 +1,398 @@ +--- popup.lua +--- +--- Wrapper to make the popup api from vim in neovim. +--- Hope to get this part merged in at some point in the future. + +local Border = require "plenary.window.border" +local Window = require "plenary.window" +local utils = require "popup.utils" + +local popup = {} + +popup._pos_map = { + topleft = "NW", + topright = "NE", + botleft = "SW", + botright = "SE", +} + +-- Keep track of hidden popups, so we can load them with popup.show() +popup._hidden = {} + +local function dict_default(options, key, default) + if options[key] == nil then + return default[key] + else + return options[key] + end +end + +-- Callbacks to be called later by popup.execute_callback +popup._callbacks = {} + +function popup.create(what, vim_options) + local bufnr + if type(what) == "number" then + bufnr = what + else + bufnr = vim.api.nvim_create_buf(false, true) + assert(bufnr, "Failed to create buffer") + + vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") + + -- TODO: Handle list of lines + if type(what) == "string" then + what = { what } + else + assert(type(what) == "table", '"what" must be a table') + end + + -- padding List with numbers, defining the padding + -- above/right/below/left of the popup (similar to CSS). + -- An empty list uses a padding of 1 all around. The + -- padding goes around the text, inside any border. + -- Padding uses the 'wincolor' highlight. + -- Example: [1, 2, 1, 3] has 1 line of padding above, 2 + -- columns on the right, 1 line below and 3 columns on + -- the left. + if vim_options.padding then + local pad_top, pad_right, pad_below, pad_left + if vim.tbl_isempty(vim_options.padding) then + pad_top = 1 + pad_right = 1 + pad_below = 1 + pad_left = 1 + else + local padding = vim_options.padding + pad_top = padding[1] or 0 + pad_right = padding[2] or 0 + pad_below = padding[3] or 0 + pad_left = padding[4] or 0 + end + + local left_padding = string.rep(" ", pad_left) + local right_padding = string.rep(" ", pad_right) + for index = 1, #what do + what[index] = string.format("%s%s%s", left_padding, what[index], right_padding) + end + + for _ = 1, pad_top do + table.insert(what, 1, "") + end + + for _ = 1, pad_below do + table.insert(what, "") + end + end + + vim.api.nvim_buf_set_lines(bufnr, 0, -1, true, what) + end + + local option_defaults = { + posinvert = true, + } + + local win_opts = {} + win_opts.relative = "editor" + + local cursor_relative_pos = function(pos_str, dim) + assert(string.find(pos_str, "^cursor"), "Invalid value for " .. dim) + win_opts.relative = "cursor" + local line = 0 + if (pos_str):match "cursor%+(%d+)" then + line = line + tonumber((pos_str):match "cursor%+(%d+)") + elseif (pos_str):match "cursor%-(%d+)" then + line = line - tonumber((pos_str):match "cursor%-(%d+)") + end + return line + end + + if vim_options.line then + if type(vim_options.line) == "string" then + win_opts.row = cursor_relative_pos(vim_options.line, "row") + else + win_opts.row = vim_options.line + end + else + -- TODO: It says it needs to be "vertically cenetered"?... + -- wut is that. + win_opts.row = 0 + end + + if vim_options.col then + if type(vim_options.col) == "string" then + win_opts.col = cursor_relative_pos(vim_options.col, "col") + else + win_opts.col = vim_options.col + end + else + -- TODO: It says it needs to be "horizontally cenetered"?... + win_opts.col = 0 + end + + if vim_options.pos then + if vim_options.pos == "center" then + -- TODO: Do centering.. + else + win_opts.anchor = popup._pos_map[vim_options.pos] + end + else + win_opts.anchor = "NW" -- This is the default, but makes `posinvert` easier to implement + end + + -- , fixed When FALSE (the default), and: + -- , - "pos" is "botleft" or "topleft", and + -- , - "wrap" is off, and + -- , - the popup would be truncated at the right edge of + -- , the screen, then + -- , the popup is moved to the left so as to fit the + -- , contents on the screen. Set to TRUE to disable this. + + win_opts.style = "minimal" + + -- Feels like maxheight, minheight, maxwidth, minwidth will all be related + -- + -- maxheight Maximum height of the contents, excluding border and padding. + -- minheight Minimum height of the contents, excluding border and padding. + -- maxwidth Maximum width of the contents, excluding border, padding and scrollbar. + -- minwidth Minimum width of the contents, excluding border, padding and scrollbar. + local width = vim_options.width or 1 + local height + if type(what) == "number" then + height = vim.api.nvim_buf_line_count(what) + else + for _, v in ipairs(what) do + width = math.max(width, #v) + end + height = #what + end + win_opts.width = utils.bounded(width, vim_options.minwidth, vim_options.maxwidth) + win_opts.height = utils.bounded(height, vim_options.minheight, vim_options.maxheight) + + -- posinvert, When FALSE the value of "pos" is always used. When + -- , TRUE (the default) and the popup does not fit + -- , vertically and there is more space on the other side + -- , then the popup is placed on the other side of the + -- , position indicated by "line". + if dict_default(vim_options, "posinvert", option_defaults) then + if win_opts.anchor == "NW" or win_opts.anchor == "NE" then + if win_opts.row + win_opts.height > vim.o.lines and win_opts.row * 2 > vim.o.lines then + -- Don't know why, but this is how vim adjusts it + win_opts.row = win_opts.row - win_opts.height - 2 + end + elseif win_opts.anchor == "SW" or win_opts.anchor == "SE" then + if win_opts.row - win_opts.height < 0 and win_opts.row * 2 < vim.o.lines then + -- Don't know why, but this is how vim adjusts it + win_opts.row = win_opts.row + win_opts.height + 2 + end + end + end + + -- textprop, When present the popup is positioned next to a text + -- , property with this name and will move when the text + -- , property moves. Use an empty string to remove. See + -- , |popup-textprop-pos|. + -- related: + -- textpropwin + -- textpropid + + local win_id + if vim_options.hidden then + assert(false, "I have not implemented this yet and don't know how") + else + win_id = vim.api.nvim_open_win(bufnr, false, win_opts) + end + + -- Moved, handled after since we need the window ID + if vim_options.moved then + if vim_options.moved == "any" then + vim.lsp.util.close_preview_autocmd({ "CursorMoved", "CursorMovedI" }, win_id) + elseif vim_options.moved == "word" then + -- TODO: Handle word, WORD, expr, and the range functions... which seem hard? + end + else + local silent = false + vim.cmd( + string.format( + "autocmd BufDelete %s ++once ++nested :lua require('plenary.window').try_close(%s, true)", + (silent and "") or "", + bufnr, + win_id + ) + ) + end + + if vim_options.time then + local timer = vim.loop.new_timer() + timer:start( + vim_options.time, + 0, + vim.schedule_wrap(function() + Window.try_close(win_id, false) + end) + ) + end + + -- Buffer Options + if vim_options.cursorline then + vim.api.nvim_win_set_option(win_id, "cursorline", true) + end + + if vim_options.wrap ~= nil then + vim.api.nvim_win_set_option(win_id, "wrap", vim_options.wrap) + end + + -- ===== Not Implemented Options ===== + -- flip: not implemented at the time of writing + -- Mouse: + -- mousemoved: no idea how to do the things with the mouse, so it's an exercise for the reader. + -- drag: mouses are hard + -- resize: mouses are hard + -- close: mouses are hard + -- + -- scrollbar + -- scrollbarhighlight + -- thumbhighlight + + -- tabpage: seems useless + + -- Create border + + local should_show_border = nil + local border_options = {} + + -- border List with numbers, defining the border thickness + -- above/right/below/left of the popup (similar to CSS). + -- Only values of zero and non-zero are recognized. + -- An empty list uses a border all around. + if vim_options.border then + should_show_border = true + + if type(vim_options.border) == "boolean" or vim.tbl_isempty(vim_options.border) then + border_options.border_thickness = Border._default_thickness + elseif #vim_options.border == 4 then + border_options.border_thickness = { + top = utils.bounded(vim_options.border[1], 0, 1), + right = utils.bounded(vim_options.border[2], 0, 1), + bot = utils.bounded(vim_options.border[3], 0, 1), + left = utils.bounded(vim_options.border[4], 0, 1), + } + else + error(string.format("Invalid configuration for border: %s", vim.inspect(vim_options.border))) + end + elseif vim_options.border == false then + should_show_border = false + end + + if (should_show_border == nil or should_show_border) and vim_options.borderchars then + should_show_border = true + + -- borderchars List with characters, defining the character to use + -- for the top/right/bottom/left border. Optionally + -- followed by the character to use for the + -- topleft/topright/botright/botleft corner. + -- Example: ['-', '|', '-', '|', '┌', '┐', '┘', '└'] + -- When the list has one character it is used for all. + -- When the list has two characters the first is used for + -- the border lines, the second for the corners. + -- By default a double line is used all around when + -- 'encoding' is "utf-8" and 'ambiwidth' is "single", + -- otherwise ASCII characters are used. + + local b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft + if vim_options.borderchars == nil then + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + "═", "║", "═", "║", "╔", "╗", "╝", "╚" + elseif #vim_options.borderchars == 1 then + local b_char = vim_options.borderchars[1] + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + b_char, b_char, b_char, b_char, b_char, b_char, b_char, b_char + elseif #vim_options.borderchars == 2 then + local b_char = vim_options.borderchars[1] + local c_char = vim_options.borderchars[2] + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = + b_char, b_char, b_char, b_char, c_char, c_char, c_char, c_char + elseif #vim_options.borderchars == 8 then + b_top, b_right, b_bot, b_left, b_topleft, b_topright, b_botright, b_botleft = unpack(vim_options.borderchars) + else + error(string.format 'Not enough arguments for "borderchars"') + end + + border_options.top = b_top + border_options.bot = b_bot + border_options.right = b_right + border_options.left = b_left + border_options.topleft = b_topleft + border_options.topright = b_topright + border_options.botright = b_botright + border_options.botleft = b_botleft + end + + -- title + if vim_options.title then + -- TODO: Check out how title works with weird border combos. + border_options.title = vim_options.title + end + + local border = nil + if should_show_border then + border = Border:new(bufnr, win_id, win_opts, border_options) + end + + if vim_options.highlight then + vim.api.nvim_win_set_option(win_id, "winhl", string.format("Normal:%s", vim_options.highlight)) + end + + if vim_options.borderhighlight then + vim.api.nvim_win_set_option(border.win_id, "winhl", string.format("Normal:%s", vim_options.borderhighlight)) + end + + -- enter + local should_enter = vim_options.enter + if should_enter == nil then + should_enter = true + end + + if should_enter then + -- set focus after border creation so that it's properly placed (especially + -- in relative cursor layout) + vim.api.nvim_set_current_win(win_id) + end + + -- callback + if vim_options.callback then + popup._callbacks[bufnr] = function() + -- (jbyuki): Giving win_id is pointless here because it's closed right afterwards + -- but it might make more sense once hidden is implemented + local row, _ = unpack(vim.api.nvim_win_get_cursor(win_id)) + vim_options.callback(win_id, what[row]) + vim.api.nvim_win_close(win_id, true) + end + vim.api.nvim_buf_set_keymap( + bufnr, + "n", + "", + 'lua require"popup".execute_callback(' .. bufnr .. ")", + { noremap = true } + ) + end + + -- TODO: Perhaps there's a way to return an object that looks like a window id, + -- but actually has some extra metadata about it. + -- + -- This would make `hidden` a lot easier to manage + return win_id, { + win_id = win_id, + border = border, + } +end + +function popup.execute_callback(bufnr) + if popup._callbacks[bufnr] then + local wrapper = popup._callbacks[bufnr] + wrapper() + popup._callbacks[bufnr] = nil + end +end + +return popup diff --git a/lua/plenary/popup/utils.lua b/lua/plenary/popup/utils.lua new file mode 100644 index 00000000..18815d92 --- /dev/null +++ b/lua/plenary/popup/utils.lua @@ -0,0 +1,30 @@ + +local utils = {} + +utils.bounded = function(value, min, max) + min = min or 0 + max = max or math.huge + + if min then value = math.max(value, min) end + if max then value = math.min(value, max) end + + return value +end + +utils.apply_defaults = function(original, defaults) + if original == nil then + original = {} + end + + original = vim.deepcopy(original) + + for k, v in pairs(defaults) do + if original[k] == nil then + original[k] = v + end + end + + return original +end + +return utils diff --git a/tests/plenary/popup_spec.lua b/tests/plenary/popup_spec.lua new file mode 100644 index 00000000..fecf14ef --- /dev/null +++ b/tests/plenary/popup_spec.lua @@ -0,0 +1,132 @@ +local popup = require "plenary.popup" + +local eq = assert.are.same + +describe("plenary.popup", function() + before_each(function() + vim.cmd [[highlight PopupColor1 ctermbg=lightblue guibg=lightblue]] + vim.cmd [[highlight PopupColor2 ctermbg=lightcyan guibg=lightcyan]] + end) + + -- TODO: Probably want to clear all the popups between iterations + -- after_each(function() end) + + it("can create a very simple window", function() + local win_id = popup.create("hello there", { + line = 1, + col = 1, + width = 20, + }) + + local win_config = vim.api.nvim_win_get_config(win_id) + eq(20, win_config.width) + end) + + it("can apply a highlight", function() + local win_id = popup.create("hello there", { + highlight = "PopupColor1", + }) + + eq("Normal:PopupColor1", vim.api.nvim_win_get_option(win_id, "winhl")) + end) + + it("can create a border", function() + local win_id, config = popup.create("hello border", { + line = 2, + col = 3, + border = {}, + }) + + eq(true, vim.api.nvim_win_is_valid(win_id)) + + local border_id = config.border.win_id + assert(border_id, "Has a border win id") + eq(true, vim.api.nvim_win_is_valid(border_id)) + end) + + it("can do basic padding", function() + local win_id = popup.create("12345", { + line = 1, + col = 1, + padding = {}, + }) + + local bufnr = vim.api.nvim_win_get_buf(win_id) + eq({ "", " 12345 ", "" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) + end) + + it("can do padding and border", function() + local win_id, config = popup.create("hello border", { + line = 2, + col = 2, + border = {}, + padding = {}, + }) + + local bufnr = vim.api.nvim_win_get_buf(win_id) + eq({ "", " hello border ", "" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) + + local border_id = config.border.win_id + assert(border_id, "Has a border win id") + eq(true, vim.api.nvim_win_is_valid(border_id)) + end) + + describe("borderchars", function() + local test_border = function(name, borderchars, expected) + it(name, function() + local win_id, config = popup.create("all the plus signs", { + line = 8, + col = 55, + padding = { 0, 3, 0, 3 }, + borderchars = borderchars, + }) + + local border_id = config.border.win_id + local border_bufnr = vim.api.nvim_win_get_buf(border_id) + eq(expected, vim.api.nvim_buf_get_lines(border_bufnr, 0, -1, false)) + end) + end + + test_border("can support multiple border patterns", { "+" }, { + "++++++++++++++++++++++++++", + "+ +", + "++++++++++++++++++++++++++", + }) + + test_border("can support multiple patterns inside the borderchars", { "-", "+" }, { + "+------------------------+", + "- -", + "+------------------------+", + }) + end) + + describe("what", function() + it("can be an existing bufnr", function() + local bufnr = vim.api.nvim_create_buf(false, false) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { "pass bufnr 1", "pass bufnr 2" }) + local win_id = popup.create(bufnr, { + line = 8, + col = 55, + minwidth = 20, + }) + + eq(bufnr, vim.api.nvim_win_get_buf(win_id)) + eq({ "pass bufnr 1", "pass bufnr 2" }, vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)) + end) + end) + + describe("cursor", function() + pending("not yet tested", function() + popup.create({ "option 1", "option 2" }, { + line = "cursor+2", + col = "cursor+2", + border = { 1, 1, 1, 1 }, + enter = true, + cursorline = true, + callback = function(win_id, sel) + print(sel) + end, + }) + end) + end) +end) From 38fdd70b83b2edfa2b4145ca6ab49429b0d947bc Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Tue, 10 Aug 2021 10:38:10 -0400 Subject: [PATCH 2/6] fixup: Add lint that only certain plenary modules can be required --- lua/plenary/popup/init.lua | 2 +- tests/plenary/popup_requires_spec.lua | 35 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 tests/plenary/popup_requires_spec.lua diff --git a/lua/plenary/popup/init.lua b/lua/plenary/popup/init.lua index a39bbda5..235f824a 100644 --- a/lua/plenary/popup/init.lua +++ b/lua/plenary/popup/init.lua @@ -5,7 +5,7 @@ local Border = require "plenary.window.border" local Window = require "plenary.window" -local utils = require "popup.utils" +local utils = require "plenary.popup.utils" local popup = {} diff --git a/tests/plenary/popup_requires_spec.lua b/tests/plenary/popup_requires_spec.lua new file mode 100644 index 00000000..820b1b07 --- /dev/null +++ b/tests/plenary/popup_requires_spec.lua @@ -0,0 +1,35 @@ +local scandir = require "plenary.scandir" +local Path = require "plenary.path" + +local eq = assert.are.same + +describe("plenary.popup", function() + local allowed_imports = { + ["plenary.window"] = true, + ["plenary.window.border"] = true, + } + + local matches_any_import = function(line) + local matched = string.match(line, [[require "(.*)"]]) + if matched and not vim.startswith(matched, "plenary.popup") then + if not allowed_imports[matched] then + return true, string.format("Not an allowed import for popup: %s. Line: %s", matched, line) + end + end + return false, nil + end + + it("must not require anything other than Window and Border from plenary", function() + local result = scandir.scan_dir("./lua/plenary/popup", { depth = 1 }) + + for _, file in ipairs(result) do + local popup_file = Path:new(file) + local lines = popup_file:readlines() + + for _, line in ipairs(lines) do + local matches, msg = matches_any_import(line) + eq(false, matches, msg) + end + end + end) +end) From 32153da48fd5cd91fbbbca369fa2420162aafe5d Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Tue, 10 Aug 2021 10:52:24 -0400 Subject: [PATCH 3/6] fixup: stylua --- lua/plenary/popup/utils.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lua/plenary/popup/utils.lua b/lua/plenary/popup/utils.lua index 18815d92..e665e150 100644 --- a/lua/plenary/popup/utils.lua +++ b/lua/plenary/popup/utils.lua @@ -1,12 +1,15 @@ - local utils = {} utils.bounded = function(value, min, max) min = min or 0 max = max or math.huge - if min then value = math.max(value, min) end - if max then value = math.min(value, max) end + if min then + value = math.max(value, min) + end + if max then + value = math.min(value, max) + end return value end From e98918a023a27b08f012356e30327f10050fb334 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Wed, 11 Aug 2021 07:58:08 -0400 Subject: [PATCH 4/6] fixup: Update test to search for both types of requires --- tests/plenary/popup_requires_spec.lua | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/plenary/popup_requires_spec.lua b/tests/plenary/popup_requires_spec.lua index 820b1b07..2909f656 100644 --- a/tests/plenary/popup_requires_spec.lua +++ b/tests/plenary/popup_requires_spec.lua @@ -10,15 +10,22 @@ describe("plenary.popup", function() } local matches_any_import = function(line) - local matched = string.match(line, [[require "(.*)"]]) + local matched = string.match(line, [[require."(.*)"]]) if matched and not vim.startswith(matched, "plenary.popup") then if not allowed_imports[matched] then return true, string.format("Not an allowed import for popup: %s. Line: %s", matched, line) end end + return false, nil end + -- Tests to make sure that we're matching both types of requires + it("should match these kinds of patterns", function() + eq(true, matches_any_import [[local x = require "plenary.other"]]) + eq(true, matches_any_import [[local x = require("plenary.module").something]]) + end) + it("must not require anything other than Window and Border from plenary", function() local result = scandir.scan_dir("./lua/plenary/popup", { depth = 1 }) From ad109a199440971d2259bfef633805c4d2867704 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Wed, 11 Aug 2021 08:01:53 -0400 Subject: [PATCH 5/6] fixup: Add readme --- POPUP.md | 86 ++++++++++++++++++++++++++++++++++++++ lua/plenary/popup/init.lua | 2 + 2 files changed, 88 insertions(+) create mode 100644 POPUP.md diff --git a/POPUP.md b/POPUP.md new file mode 100644 index 00000000..771d5811 --- /dev/null +++ b/POPUP.md @@ -0,0 +1,86 @@ +# Popup tracking + +[WIP] An implementation of the Popup API from vim in Neovim. Hope to upstream +when complete + +## Goals + +Provide an API that is compatible with the vim `popup_*` APIs. After +stablization and any required features are merged into Neovim, we can upstream +this and expose the API in vimL to create better compatibility. + +## List of Neovim Features Required: + +- [ ] Add Z-index for floating windows + - [ ] When complete, we can add `zindex` parameter +- [ ] Key handlers (used for `popup_filter`) +- [ ] scrollbar for floating windows + - [ ] scrollbar + - [ ] scrollbarhighlight + - [ ] thumbhighlight + +Optional: + +- [ ] Add forced transparency to a floating window. + - Apparently overrides text? + - This is for the `mask` feature flag + + +Unlikely (due to technical difficulties): + +- [ ] Add `textprop` wrappers? + - textprop + - textpropwin + - textpropid +- [ ] "close" + - But this is mostly because I don't know how to use mouse APIs in nvim. If someone knows. please make an issue in the repo, and maybe we can get it sorted out. + +Unlikely (due to not sure if people are using): +- [ ] tabpage + +## Progress + +Suported Features: + +- [x] what + - string + - list of strings +- [x] popup_create-arguments + - [x] border + - [x] borderchars + - [x] col + - [x] cursorline + - [x] highlight + - [x] line + - [x] {max,min}{height,width} + - [?] moved + - [x] "any" + - [ ] "word" + - [ ] "WORD" + - [ ] "expr" + - [ ] (list options) + - [x] padding + - [?] pos + - Somewhat implemented. Doesn't work with borders though. + - [x] posinvert + - [x] time + - [x] title + - [x] wrap + +## All known unimplemented vim features at the moment + +- firstline +- hidden +- ~ pos +- fixed +- filter +- filtermode +- mapping +- callback +- mouse: + - mousemoved + - close + - drag + - resize + +- (not implemented in vim yet) flip diff --git a/lua/plenary/popup/init.lua b/lua/plenary/popup/init.lua index 235f824a..c94ceeac 100644 --- a/lua/plenary/popup/init.lua +++ b/lua/plenary/popup/init.lua @@ -2,6 +2,8 @@ --- --- Wrapper to make the popup api from vim in neovim. --- Hope to get this part merged in at some point in the future. +--- +--- Please make sure to update "POPUP.md" with any changes and/or notes. local Border = require "plenary.window.border" local Window = require "plenary.window" From ebc91967457963050ba8c2d514db7b9c0cc02038 Mon Sep 17 00:00:00 2001 From: TJ DeVries Date: Wed, 11 Aug 2021 08:03:05 -0400 Subject: [PATCH 6/6] fixup: Update readme --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index dfa2e234..13397109 100644 --- a/README.md +++ b/README.md @@ -213,10 +213,7 @@ Status: WIP ### plenary.popup -`popup_*` clone of Vim's commands. If it gets good enough, will submit PR to Neovim and write C wrappers -to provide compatibility layer for Neovim. - -Status: WIP +See [popup documentation](./POPUP.md) for both progress tracking and implemented APIs. ### plenary.window