Skip to content

Commit

Permalink
feat: allow Borders and popups to be resized (#180)
Browse files Browse the repository at this point in the history
* feat: allow `Border`s to be resized

* refactor: reduced overlap between `:new` and resizing function

- rename `Border:resize` to `Border:set_size`

- allow `Border:set_size` to be passed on option to create

  a window rather than resize

- remove overlapped parts from `Border:new` and use `Border:set_size`

  with `create_window` set to `true`

* stylua

* feat: move popup resizing PR to plenary

* stylua

* chore: refactoring

* refactor: use builtin nil-check for `create_window`

* refactor: factor out `__update_lines_get_win_config`

* fix: update `__update_lines...` name everywhere

* fix: variable scope issue

* refactor: rename aligning function

* refactor: rename to match vim's `popup_move`

* refactor: factor our positional config parsing to reuse in `popup.move`

* stylua

* chore: docs/add default height

* docs: comments

* fix: deepcopy `vim_options` to avoid changes to supplied variables

* refactor: rename `set_size` to `move`

- this then matches terminology for popups

* fix: update `Border` table values on `move`

* stylua

* refactor: calculate size before position

* stylua
  • Loading branch information
l-kershaw authored Sep 26, 2021
1 parent 03ac32a commit 8c6cc07
Show file tree
Hide file tree
Showing 2 changed files with 174 additions and 97 deletions.
206 changes: 130 additions & 76 deletions lua/plenary/popup/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ local Border = require "plenary.window.border"
local Window = require "plenary.window"
local utils = require "plenary.popup.utils"

local if_nil = vim.F.if_nil

local popup = {}

popup._pos_map = {
Expand All @@ -21,6 +23,9 @@ popup._pos_map = {
-- Keep track of hidden popups, so we can load them with popup.show()
popup._hidden = {}

-- Keep track of popup borders, so we don't have to pass them between functions
popup._borders = {}

local function dict_default(options, key, default)
if options[key] == nil then
return default[key]
Expand All @@ -32,7 +37,84 @@ end
-- Callbacks to be called later by popup.execute_callback
popup._callbacks = {}

-- Convert the positional {vim_options} to compatible neovim options and add them to {win_opts}
-- If an option is not given in {vim_options}, fall back to {default_opts}
local function add_position_config(win_opts, vim_options, default_opts)
default_opts = default_opts or {}

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

-- 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 = if_nil(vim_options.width, default_opts.width)
local height = if_nil(vim_options.height, default_opts.height)
win_opts.width = utils.bounded(width, vim_options.minwidth, vim_options.maxwidth)
win_opts.height = utils.bounded(height, vim_options.minheight, vim_options.maxheight)

if vim_options.line and vim_options.line ~= 0 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 - 1
end
else
win_opts.row = math.floor((vim.o.lines - win_opts.height) / 2)
end

if vim_options.col and vim_options.col ~= 0 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 - 1
end
else
win_opts.col = math.floor((vim.o.columns - win_opts.width) / 2)
end

-- pos
--
-- Using "topleft", "topright", "botleft", "botright" defines what corner of the popup "line"
-- and "col" are used for. When not set "topleft" behaviour is used.
-- Alternatively "center" can be used to position the popup in the center of the Neovim window,
-- in which case "line" and "col" are ignored.
if vim_options.pos then
if vim_options.pos == "center" then
vim_options.line = 0
vim_options.col = 0
win_opts.anchor = "NW"
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.
end

function popup.create(what, vim_options)
vim_options = vim.deepcopy(vim_options)

local bufnr
if type(what) == "number" then
bufnr = what
Expand Down Expand Up @@ -95,88 +177,23 @@ function popup.create(what, vim_options)
zindex = 50,
}

local win_opts = {}
win_opts.relative = "editor"

-- 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
vim_options.width = if_nil(vim_options.width, 1)
if type(what) == "number" then
height = vim.api.nvim_buf_line_count(what)
vim_options.height = vim.api.nvim_buf_line_count(what)
else
for _, v in ipairs(what) do
width = math.max(width, #v)
vim_options.width = math.max(vim_options.width, #v)
end
height = #what
vim_options.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)

-- pos
--
-- Using "topleft", "topright", "botleft", "botright" defines what corner of the popup "line"
-- and "col" are used for. When not set "topleft" behaviour is used.
-- Alternatively "center" can be used to position the popup in the center of the Neovim window,
-- in which case "line" and "col" are ignored.
if vim_options.pos then
if vim_options.pos == "center" then
vim_options.line = 0
vim_options.col = 0
win_opts.anchor = "NW"
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

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 and vim_options.line ~= 0 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 - 1
end
else
win_opts.row = math.floor((vim.o.lines - win_opts.height) / 2)
end

if vim_options.col and vim_options.col ~= 0 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 - 1
end
else
win_opts.col = math.floor((vim.o.columns - win_opts.width) / 2)
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.

local win_opts = {}
win_opts.relative = "editor"
win_opts.style = "minimal"

-- Add positional and sizing config to win_opts
add_position_config(win_opts, vim_options, { width = 1, height = 1 })

-- 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
Expand Down Expand Up @@ -210,11 +227,11 @@ function popup.create(what, vim_options)
win_opts.zindex = utils.bounded(zindex, 1, 32000)

-- noautocmd, undocumented vim default per https://github.com/vim/vim/issues/5737
win_opts.noautocmd = vim.F.if_nil(vim_options.noautocmd, true)
win_opts.noautocmd = if_nil(vim_options.noautocmd, true)

-- focusable,
-- vim popups are not focusable windows
win_opts.focusable = vim.F.if_nil(vim_options.focusable, false)
win_opts.focusable = if_nil(vim_options.focusable, false)

local win_id
if vim_options.hidden then
Expand Down Expand Up @@ -363,6 +380,7 @@ function popup.create(what, vim_options)
if should_show_border then
border_options.focusable = vim_options.border_focusable
border = Border:new(bufnr, win_id, win_opts, border_options)
popup._borders[win_id] = border
end

if vim_options.highlight then
Expand Down Expand Up @@ -417,6 +435,42 @@ function popup.create(what, vim_options)
}
end

-- Move popup with window id {win_id} to the position specified with {vim_options}.
-- {vim_options} may contain the following items that determine the popup position/size:
-- - line
-- - col
-- - height
-- - width
-- - maxheight/minheight
-- - maxwidth/minwidth
-- - pos
-- Unimplemented vim options here include: fixed
function popup.move(win_id, vim_options)
-- Create win_options
local win_opts = {}
win_opts.relative = "editor"

local current_pos = vim.api.nvim_win_get_position(win_id)
local default_opts = {
width = vim.api.nvim_win_get_width(win_id),
height = vim.api.nvim_win_get_height(win_id),
row = current_pos[1],
col = current_pos[2],
}

-- Add positional and sizing config to win_opts
add_position_config(win_opts, vim_options, default_opts)

-- Update content window
vim.api.nvim_win_set_config(win_id, win_opts)

-- Update border window (if present)
local border = popup._borders[win_id]
if border ~= nil then
border:move(win_opts, border._border_win_options)
end
end

function popup.execute_callback(bufnr)
if popup._callbacks[bufnr] then
local wrapper = popup._callbacks[bufnr]
Expand Down
65 changes: 44 additions & 21 deletions lua/plenary/window/border.lua
Original file line number Diff line number Diff line change
Expand Up @@ -159,11 +159,10 @@ function Border:change_title(new_title)
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
end

function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options)
assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'")

-- TODO: Probably can use just deep_extend, now that it's available
border_win_options = tbl.apply_defaults(border_win_options, {
-- Updates characters for border lines, and returns nvim_win_config
-- (generally used in conjunction with `move` or `new`)
function Border:__align_calc_config(content_win_options, border_win_options)
border_win_options = vim.tbl_deep_extend("keep", border_win_options, {
border_thickness = Border._default_thickness,

-- Border options, could be passed as a list?
Expand All @@ -177,22 +176,8 @@ function Border:new(content_bufnr, content_win_id, content_win_options, border_w
bot = "",
})

local obj = {}

obj.content_win_id = content_win_id
obj.content_win_options = content_win_options
obj._border_win_options = border_win_options

obj.bufnr = vim.api.nvim_create_buf(false, true)
assert(obj.bufnr, "Failed to create border buffer")
vim.api.nvim_buf_set_option(obj.bufnr, "bufhidden", "wipe")

obj.contents = Border._create_lines(content_win_options, border_win_options)
vim.api.nvim_buf_set_lines(obj.bufnr, 0, -1, false, obj.contents)

local thickness = border_win_options.border_thickness

obj.win_id = vim.api.nvim_open_win(obj.bufnr, false, {
local nvim_win_config = {
anchor = content_win_options.anchor,
relative = content_win_options.relative,
style = "minimal",
Expand All @@ -203,7 +188,45 @@ function Border:new(content_bufnr, content_win_id, content_win_options, border_w
zindex = content_win_options.zindex or 50,
noautocmd = content_win_options.noautocmd,
focusable = vim.F.if_nil(border_win_options.focusable, false),
})
}

-- Update border characters
self.contents = Border._create_lines(content_win_options, border_win_options)
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)

return nvim_win_config
end

-- Sets the size and position of the given Border.
-- Can be used to create a new window (with `create_window = true`)
-- or change an existing one
function Border:move(content_win_options, border_win_options)
self.content_win_options = content_win_options
self._border_win_options = border_win_options

-- Update lines in border buffer, and get config for border window
local nvim_win_config = self:__align_calc_config(content_win_options, border_win_options)

-- Set config for border window
vim.api.nvim_win_set_config(self.win_id, nvim_win_config)
end

function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options)
assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'")

local obj = {}

obj.content_win_id = content_win_id
obj.content_win_options = content_win_options
obj._border_win_options = border_win_options

obj.bufnr = vim.api.nvim_create_buf(false, true)
assert(obj.bufnr, "Failed to create border buffer")
vim.api.nvim_buf_set_option(obj.bufnr, "bufhidden", "wipe")

-- Create a border window and buffer, with border characters around the edge
local nvim_win_config = Border.__align_calc_config(obj, content_win_options, border_win_options)
obj.win_id = vim.api.nvim_open_win(obj.bufnr, false, nvim_win_config)

vim.cmd(
string.format(
Expand Down

0 comments on commit 8c6cc07

Please sign in to comment.