Skip to content

Commit 8c6cc07

Browse files
authored
feat: allow Borders and popups to be resized (#180)
* 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
1 parent 03ac32a commit 8c6cc07

2 files changed

Lines changed: 174 additions & 97 deletions

File tree

lua/plenary/popup/init.lua

Lines changed: 130 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ local Border = require "plenary.window.border"
99
local Window = require "plenary.window"
1010
local utils = require "plenary.popup.utils"
1111

12+
local if_nil = vim.F.if_nil
13+
1214
local popup = {}
1315

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

26+
-- Keep track of popup borders, so we don't have to pass them between functions
27+
popup._borders = {}
28+
2429
local function dict_default(options, key, default)
2530
if options[key] == nil then
2631
return default[key]
@@ -32,7 +37,84 @@ end
3237
-- Callbacks to be called later by popup.execute_callback
3338
popup._callbacks = {}
3439

40+
-- Convert the positional {vim_options} to compatible neovim options and add them to {win_opts}
41+
-- If an option is not given in {vim_options}, fall back to {default_opts}
42+
local function add_position_config(win_opts, vim_options, default_opts)
43+
default_opts = default_opts or {}
44+
45+
local cursor_relative_pos = function(pos_str, dim)
46+
assert(string.find(pos_str, "^cursor"), "Invalid value for " .. dim)
47+
win_opts.relative = "cursor"
48+
local line = 0
49+
if (pos_str):match "cursor%+(%d+)" then
50+
line = line + tonumber((pos_str):match "cursor%+(%d+)")
51+
elseif (pos_str):match "cursor%-(%d+)" then
52+
line = line - tonumber((pos_str):match "cursor%-(%d+)")
53+
end
54+
return line
55+
end
56+
57+
-- Feels like maxheight, minheight, maxwidth, minwidth will all be related
58+
--
59+
-- maxheight Maximum height of the contents, excluding border and padding.
60+
-- minheight Minimum height of the contents, excluding border and padding.
61+
-- maxwidth Maximum width of the contents, excluding border, padding and scrollbar.
62+
-- minwidth Minimum width of the contents, excluding border, padding and scrollbar.
63+
local width = if_nil(vim_options.width, default_opts.width)
64+
local height = if_nil(vim_options.height, default_opts.height)
65+
win_opts.width = utils.bounded(width, vim_options.minwidth, vim_options.maxwidth)
66+
win_opts.height = utils.bounded(height, vim_options.minheight, vim_options.maxheight)
67+
68+
if vim_options.line and vim_options.line ~= 0 then
69+
if type(vim_options.line) == "string" then
70+
win_opts.row = cursor_relative_pos(vim_options.line, "row")
71+
else
72+
win_opts.row = vim_options.line - 1
73+
end
74+
else
75+
win_opts.row = math.floor((vim.o.lines - win_opts.height) / 2)
76+
end
77+
78+
if vim_options.col and vim_options.col ~= 0 then
79+
if type(vim_options.col) == "string" then
80+
win_opts.col = cursor_relative_pos(vim_options.col, "col")
81+
else
82+
win_opts.col = vim_options.col - 1
83+
end
84+
else
85+
win_opts.col = math.floor((vim.o.columns - win_opts.width) / 2)
86+
end
87+
88+
-- pos
89+
--
90+
-- Using "topleft", "topright", "botleft", "botright" defines what corner of the popup "line"
91+
-- and "col" are used for. When not set "topleft" behaviour is used.
92+
-- Alternatively "center" can be used to position the popup in the center of the Neovim window,
93+
-- in which case "line" and "col" are ignored.
94+
if vim_options.pos then
95+
if vim_options.pos == "center" then
96+
vim_options.line = 0
97+
vim_options.col = 0
98+
win_opts.anchor = "NW"
99+
else
100+
win_opts.anchor = popup._pos_map[vim_options.pos]
101+
end
102+
else
103+
win_opts.anchor = "NW" -- This is the default, but makes `posinvert` easier to implement
104+
end
105+
106+
-- , fixed When FALSE (the default), and:
107+
-- , - "pos" is "botleft" or "topleft", and
108+
-- , - "wrap" is off, and
109+
-- , - the popup would be truncated at the right edge of
110+
-- , the screen, then
111+
-- , the popup is moved to the left so as to fit the
112+
-- , contents on the screen. Set to TRUE to disable this.
113+
end
114+
35115
function popup.create(what, vim_options)
116+
vim_options = vim.deepcopy(vim_options)
117+
36118
local bufnr
37119
if type(what) == "number" then
38120
bufnr = what
@@ -95,88 +177,23 @@ function popup.create(what, vim_options)
95177
zindex = 50,
96178
}
97179

98-
local win_opts = {}
99-
win_opts.relative = "editor"
100-
101-
-- Feels like maxheight, minheight, maxwidth, minwidth will all be related
102-
--
103-
-- maxheight Maximum height of the contents, excluding border and padding.
104-
-- minheight Minimum height of the contents, excluding border and padding.
105-
-- maxwidth Maximum width of the contents, excluding border, padding and scrollbar.
106-
-- minwidth Minimum width of the contents, excluding border, padding and scrollbar.
107-
local width = vim_options.width or 1
108-
local height
180+
vim_options.width = if_nil(vim_options.width, 1)
109181
if type(what) == "number" then
110-
height = vim.api.nvim_buf_line_count(what)
182+
vim_options.height = vim.api.nvim_buf_line_count(what)
111183
else
112184
for _, v in ipairs(what) do
113-
width = math.max(width, #v)
185+
vim_options.width = math.max(vim_options.width, #v)
114186
end
115-
height = #what
187+
vim_options.height = #what
116188
end
117-
win_opts.width = utils.bounded(width, vim_options.minwidth, vim_options.maxwidth)
118-
win_opts.height = utils.bounded(height, vim_options.minheight, vim_options.maxheight)
119-
120-
-- pos
121-
--
122-
-- Using "topleft", "topright", "botleft", "botright" defines what corner of the popup "line"
123-
-- and "col" are used for. When not set "topleft" behaviour is used.
124-
-- Alternatively "center" can be used to position the popup in the center of the Neovim window,
125-
-- in which case "line" and "col" are ignored.
126-
if vim_options.pos then
127-
if vim_options.pos == "center" then
128-
vim_options.line = 0
129-
vim_options.col = 0
130-
win_opts.anchor = "NW"
131-
else
132-
win_opts.anchor = popup._pos_map[vim_options.pos]
133-
end
134-
else
135-
win_opts.anchor = "NW" -- This is the default, but makes `posinvert` easier to implement
136-
end
137-
138-
local cursor_relative_pos = function(pos_str, dim)
139-
assert(string.find(pos_str, "^cursor"), "Invalid value for " .. dim)
140-
win_opts.relative = "cursor"
141-
local line = 0
142-
if (pos_str):match "cursor%+(%d+)" then
143-
line = line + tonumber((pos_str):match "cursor%+(%d+)")
144-
elseif (pos_str):match "cursor%-(%d+)" then
145-
line = line - tonumber((pos_str):match "cursor%-(%d+)")
146-
end
147-
return line
148-
end
149-
150-
if vim_options.line and vim_options.line ~= 0 then
151-
if type(vim_options.line) == "string" then
152-
win_opts.row = cursor_relative_pos(vim_options.line, "row")
153-
else
154-
win_opts.row = vim_options.line - 1
155-
end
156-
else
157-
win_opts.row = math.floor((vim.o.lines - win_opts.height) / 2)
158-
end
159-
160-
if vim_options.col and vim_options.col ~= 0 then
161-
if type(vim_options.col) == "string" then
162-
win_opts.col = cursor_relative_pos(vim_options.col, "col")
163-
else
164-
win_opts.col = vim_options.col - 1
165-
end
166-
else
167-
win_opts.col = math.floor((vim.o.columns - win_opts.width) / 2)
168-
end
169-
170-
-- , fixed When FALSE (the default), and:
171-
-- , - "pos" is "botleft" or "topleft", and
172-
-- , - "wrap" is off, and
173-
-- , - the popup would be truncated at the right edge of
174-
-- , the screen, then
175-
-- , the popup is moved to the left so as to fit the
176-
-- , contents on the screen. Set to TRUE to disable this.
177189

190+
local win_opts = {}
191+
win_opts.relative = "editor"
178192
win_opts.style = "minimal"
179193

194+
-- Add positional and sizing config to win_opts
195+
add_position_config(win_opts, vim_options, { width = 1, height = 1 })
196+
180197
-- posinvert, When FALSE the value of "pos" is always used. When
181198
-- , TRUE (the default) and the popup does not fit
182199
-- , vertically and there is more space on the other side
@@ -210,11 +227,11 @@ function popup.create(what, vim_options)
210227
win_opts.zindex = utils.bounded(zindex, 1, 32000)
211228

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

215232
-- focusable,
216233
-- vim popups are not focusable windows
217-
win_opts.focusable = vim.F.if_nil(vim_options.focusable, false)
234+
win_opts.focusable = if_nil(vim_options.focusable, false)
218235

219236
local win_id
220237
if vim_options.hidden then
@@ -363,6 +380,7 @@ function popup.create(what, vim_options)
363380
if should_show_border then
364381
border_options.focusable = vim_options.border_focusable
365382
border = Border:new(bufnr, win_id, win_opts, border_options)
383+
popup._borders[win_id] = border
366384
end
367385

368386
if vim_options.highlight then
@@ -417,6 +435,42 @@ function popup.create(what, vim_options)
417435
}
418436
end
419437

438+
-- Move popup with window id {win_id} to the position specified with {vim_options}.
439+
-- {vim_options} may contain the following items that determine the popup position/size:
440+
-- - line
441+
-- - col
442+
-- - height
443+
-- - width
444+
-- - maxheight/minheight
445+
-- - maxwidth/minwidth
446+
-- - pos
447+
-- Unimplemented vim options here include: fixed
448+
function popup.move(win_id, vim_options)
449+
-- Create win_options
450+
local win_opts = {}
451+
win_opts.relative = "editor"
452+
453+
local current_pos = vim.api.nvim_win_get_position(win_id)
454+
local default_opts = {
455+
width = vim.api.nvim_win_get_width(win_id),
456+
height = vim.api.nvim_win_get_height(win_id),
457+
row = current_pos[1],
458+
col = current_pos[2],
459+
}
460+
461+
-- Add positional and sizing config to win_opts
462+
add_position_config(win_opts, vim_options, default_opts)
463+
464+
-- Update content window
465+
vim.api.nvim_win_set_config(win_id, win_opts)
466+
467+
-- Update border window (if present)
468+
local border = popup._borders[win_id]
469+
if border ~= nil then
470+
border:move(win_opts, border._border_win_options)
471+
end
472+
end
473+
420474
function popup.execute_callback(bufnr)
421475
if popup._callbacks[bufnr] then
422476
local wrapper = popup._callbacks[bufnr]

lua/plenary/window/border.lua

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,10 @@ function Border:change_title(new_title)
159159
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
160160
end
161161

162-
function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options)
163-
assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'")
164-
165-
-- TODO: Probably can use just deep_extend, now that it's available
166-
border_win_options = tbl.apply_defaults(border_win_options, {
162+
-- Updates characters for border lines, and returns nvim_win_config
163+
-- (generally used in conjunction with `move` or `new`)
164+
function Border:__align_calc_config(content_win_options, border_win_options)
165+
border_win_options = vim.tbl_deep_extend("keep", border_win_options, {
167166
border_thickness = Border._default_thickness,
168167

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

180-
local obj = {}
181-
182-
obj.content_win_id = content_win_id
183-
obj.content_win_options = content_win_options
184-
obj._border_win_options = border_win_options
185-
186-
obj.bufnr = vim.api.nvim_create_buf(false, true)
187-
assert(obj.bufnr, "Failed to create border buffer")
188-
vim.api.nvim_buf_set_option(obj.bufnr, "bufhidden", "wipe")
189-
190-
obj.contents = Border._create_lines(content_win_options, border_win_options)
191-
vim.api.nvim_buf_set_lines(obj.bufnr, 0, -1, false, obj.contents)
192-
193179
local thickness = border_win_options.border_thickness
194-
195-
obj.win_id = vim.api.nvim_open_win(obj.bufnr, false, {
180+
local nvim_win_config = {
196181
anchor = content_win_options.anchor,
197182
relative = content_win_options.relative,
198183
style = "minimal",
@@ -203,7 +188,45 @@ function Border:new(content_bufnr, content_win_id, content_win_options, border_w
203188
zindex = content_win_options.zindex or 50,
204189
noautocmd = content_win_options.noautocmd,
205190
focusable = vim.F.if_nil(border_win_options.focusable, false),
206-
})
191+
}
192+
193+
-- Update border characters
194+
self.contents = Border._create_lines(content_win_options, border_win_options)
195+
vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, self.contents)
196+
197+
return nvim_win_config
198+
end
199+
200+
-- Sets the size and position of the given Border.
201+
-- Can be used to create a new window (with `create_window = true`)
202+
-- or change an existing one
203+
function Border:move(content_win_options, border_win_options)
204+
self.content_win_options = content_win_options
205+
self._border_win_options = border_win_options
206+
207+
-- Update lines in border buffer, and get config for border window
208+
local nvim_win_config = self:__align_calc_config(content_win_options, border_win_options)
209+
210+
-- Set config for border window
211+
vim.api.nvim_win_set_config(self.win_id, nvim_win_config)
212+
end
213+
214+
function Border:new(content_bufnr, content_win_id, content_win_options, border_win_options)
215+
assert(type(content_win_id) == "number", "Must supply a valid win_id. It's possible you forgot to call with ':'")
216+
217+
local obj = {}
218+
219+
obj.content_win_id = content_win_id
220+
obj.content_win_options = content_win_options
221+
obj._border_win_options = border_win_options
222+
223+
obj.bufnr = vim.api.nvim_create_buf(false, true)
224+
assert(obj.bufnr, "Failed to create border buffer")
225+
vim.api.nvim_buf_set_option(obj.bufnr, "bufhidden", "wipe")
226+
227+
-- Create a border window and buffer, with border characters around the edge
228+
local nvim_win_config = Border.__align_calc_config(obj, content_win_options, border_win_options)
229+
obj.win_id = vim.api.nvim_open_win(obj.bufnr, false, nvim_win_config)
207230

208231
vim.cmd(
209232
string.format(

0 commit comments

Comments
 (0)