@@ -9,6 +9,8 @@ local Border = require "plenary.window.border"
99local Window = require " plenary.window"
1010local utils = require " plenary.popup.utils"
1111
12+ local if_nil = vim .F .if_nil
13+
1214local popup = {}
1315
1416popup ._pos_map = {
@@ -21,6 +23,9 @@ popup._pos_map = {
2123-- Keep track of hidden popups, so we can load them with popup.show()
2224popup ._hidden = {}
2325
26+ -- Keep track of popup borders, so we don't have to pass them between functions
27+ popup ._borders = {}
28+
2429local function dict_default (options , key , default )
2530 if options [key ] == nil then
2631 return default [key ]
3237-- Callbacks to be called later by popup.execute_callback
3338popup ._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+
35115function 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 }
418436end
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+
420474function popup .execute_callback (bufnr )
421475 if popup ._callbacks [bufnr ] then
422476 local wrapper = popup ._callbacks [bufnr ]
0 commit comments