Meh I'll figure out submodules later

This commit is contained in:
mustard 2025-09-16 01:01:02 +02:00
parent 4ca9d44a90
commit 8cb281f436
352 changed files with 66107 additions and 0 deletions

View file

@ -0,0 +1,674 @@
# Popup
Popup is an abstraction layer on top of window.
Creates a new popup object (but does not mount it immediately).
**Examples**
```lua
local Popup = require("nui.popup")
local popup = Popup({
position = "50%",
size = {
width = 80,
height = 40,
},
enter = true,
focusable = true,
zindex = 50,
relative = "editor",
border = {
padding = {
top = 2,
bottom = 2,
left = 3,
right = 3,
},
style = "rounded",
text = {
top = " I am top title ",
top_align = "center",
bottom = "I am bottom title",
bottom_align = "left",
},
},
buf_options = {
modifiable = true,
readonly = false,
},
win_options = {
winblend = 10,
winhighlight = "Normal:Normal,FloatBorder:FloatBorder",
},
})
```
You can manipulate the associated buffer and window using the
`split.bufnr` and `split.winid` properties.
## Options
### `border`
**Type:** `table`
Contains all border related options.
#### `border.padding`
**Type:** `table`
Controls the popup padding.
**Examples**
It can be a list (`table`) with number of cells for top, right, bottom and left.
The order behaves like [CSS](https://developer.mozilla.org/en-US/docs/Web/CSS) padding.
```lua
border = {
-- `1` for top/bottom and `2` for left/right
padding = { 1, 2 },
},
```
You can also use a map (`table`) to set padding at specific side:
```lua
border = {
-- `1` for top, `2` for left, `0` for other sides
padding = {
top = 1,
left = 2,
},
},
```
#### `border.style`
**Type:** `string` or `table`
Controls the styling of the border.
**Examples**
Can be one of the pre-defined styles: `"double"`, `"none"`, `"rounded"`, `"shadow"`, `"single"`, `"solid"` or `"default"`.
```lua
border = {
style = "double",
},
```
List (`table`) of characters starting from the top-left corner and then clockwise:
```lua
border = {
style = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" },
},
```
Map (`table`) with named characters:
```lua
border = {
style = {
top_left = "╭", top = "─", top_right = "╮",
left = "│", right = "│",
bottom_left = "╰", bottom = "─", bottom_right = "╯",
},
},
```
If you don't need all these options, you can also pass the value of `border.style` to `border`
directly.
To set the highlight group for all the border characters, use the `win_options.winhighlight`
option and include the name of highlight group for `FloatBorder`.
**Examples**
```lua
win_options = {
winhighlight = "Normal:Normal,FloatBorder:SpecialChar",
},
```
To set the highlight group for individual border character, you can use `NuiText` or a tuple
with `(char, hl_group)`.
**Examples**
```lua
border = {
style = { { [[/]], "SpecialChar" }, [[─]], NuiText([[\]], "SpecialChar"), [[│]] },
},
```
#### `border.text`
**Type:** `table`
Text displayed on the border (as title/footnote).
| Key | Type | Description |
| ---------------- | -------------------------------------------- | ---------------------------- |
| `"top"` | `string` / `NuiLine` / `NuiText` | top border text |
| `"top_align"` | `"left"` / `"right"`/ `"center"` _(default)_ | top border text alignment |
| `"bottom"` | `string` / `NuiLine` / `NuiText` | bottom border text |
| `"bottom_align"` | `"left"` / `"right"`/ `"center"` _(default)_ | bottom border text alignment |
`"top"` and `"bottom"` also supports list of `(text, hl_group)` tuples, just like the native popup.
**Examples**
```lua
border = {
text = {
top = "Popup Title",
top_align = "center",
},
},
```
---
### `ns_id`
**Type:** `number` or `string`
Namespace id (`number`) or name (`string`).
---
### `anchor`
**Type:** `"NW"` / `"NE"` / `"SW"` / `"SE"`
Decides which corner of the popup to place at `position`.
---
### `relative`
**Type:** `string` or `table`
This option affects how `position` and `size` are calculated.
**Examples**
Relative to cursor on current window:
```lua
relative = "cursor",
```
Relative to the current editor screen:
```lua
relative = "editor",
```
Relative to the current window (_default_):
```lua
relative = "win",
```
Relative to the window with specific id:
```lua
relative = {
type = "win",
winid = 5,
},
```
Relative to the buffer position:
```lua
relative = {
type = "buf",
-- zero-indexed
position = {
row = 5,
col = 5,
},
},
```
---
### `position`
**Type:** `number` or `percentage string` or `table`
Position is calculated from the top-left corner.
If `position` is `number` or `percentage string`, it applies to both `row` and `col`.
Or you can pass a table to set them separately.
For `percentage string`, position is calculated according to the option `relative`.
If `relative` is set to `"buf"` or `"cursor"`, `percentage string` is not allowed.
**Examples**
```lua
position = 50,
```
```lua
position = "50%",
```
```lua
position = {
row = 30,
col = 20,
},
```
```lua
position = {
row = "20%",
col = "50%",
},
```
---
### `size`
**Type:** `number` or `percentage string` or `table`
Determines the size of the popup.
If `size` is `number` or `percentage string`, it applies to both `width` and `height`.
You can also pass a table to set them separately.
For `percentage string`, `size` is calculated according to the option `relative`.
If `relative` is set to `"buf"` or `"cursor"`, window size is considered.
Decimal `number` in `(0,1)` range is treated similar to `percentage string`. For
example: `0.5` is same as `"50%"`.
**Examples**
```lua
size = 50,
```
```lua
size = "50%",
```
```lua
size = 0.5,
```
```lua
size = {
width = 80,
height = 40,
},
```
```lua
size = {
width = "80%",
height = 0.6,
},
```
---
### `enter`
**Type:** `boolean`
If `true`, the popup is entered immediately after mount.
**Examples**
```lua
enter = true,
```
---
### `focusable`
**Type:** `boolean`
If `false`, the popup can not be entered by user actions (wincmds, mouse events).
**Examples**
```lua
focusable = true,
```
---
### `zindex`
**Type:** `number`
Sets the order of the popup on z-axis.
Popup with higher the `zindex` goes on top of popups with lower `zindex`.
**Examples**
```lua
zindex = 50,
```
---
### `buf_options`
**Type:** `table`
Contains all buffer related options (check `:h options | /local to buffer`).
**Examples**
```lua
buf_options = {
modifiable = false,
readonly = true,
},
```
---
### `win_options`
**Type:** `table`
Contains all window related options (check `:h options | /local to window`).
**Examples**
```lua
win_options = {
winblend = 10,
winhighlight = "Normal:Normal,FloatBorder:FloatBorder",
},
```
---
### `bufnr`
**Type:** `number`
You can pass `bufnr` of an existing buffer to display it on the popup.
**Examples:**
```lua
bufnr = vim.api.nvim_get_current_buf(),
```
## Methods
### `popup:mount`
_Signature:_ `popup:mount()`
Mounts the popup.
**Examples**
```lua
popup:mount()
```
---
### `popup:unmount`
_Signature:_ `popup:unmount()`
Unmounts the popup.
**Examples**
```lua
popup:unmount()
```
---
### `popup:hide`
_Signature:_ `popup:hide()`
Hides the popup window. Preserves the buffer (related content, autocmds and keymaps).
---
### `popup:show`
_Signature:_ `popup:show()`
Shows the hidden popup window.
---
### `popup:map`
_Signature:_ `popup:map(mode, key, handler, opts) -> nil`
Sets keymap for the popup.
**Parameters**
| Name | Type | Description |
| --------- | --------------------- | --------------------------------------------------------------------------- |
| `mode` | `string` | check `:h :map-modes` |
| `key` | `string` | key for the mapping |
| `handler` | `string` / `function` | handler for the mapping |
| `opts` | `table` | check `:h :map-arguments` (including `remap`/`noremap`, excluding `buffer`) |
**Examples**
```lua
local ok = popup:map("n", "<esc>", function(bufnr)
print("ESC pressed in Normal mode!")
end, { noremap = true })
```
---
### `popup:unmap`
_Signature:_ `popup:unmap(mode, key) -> nil`
Deletes keymap for the popup.
**Parameters**
| Name | Type | Description |
| ------ | ------------- | --------------------- |
| `mode` | `"n"` / `"i"` | check `:h :map-modes` |
| `key` | `string` | key for the mapping |
**Examples**
```lua
local ok = popup:unmap("n", "<esc>")
```
---
### `popup:on`
_Signature:_ `popup:on(event, handler, options)`
Defines `autocmd` to run on specific events for this popup.
**Parameters**
| Name | Type | Description |
| --------- | --------------------- | ------------------------------------------ |
| `event` | `string[]` / `string` | check `:h events` |
| `handler` | `function` | handler function for event |
| `options` | `table` | keys `once`, `nested` and values `boolean` |
**Examples**
```lua
local event = require("nui.utils.autocmd").event
popup:on({ event.BufLeave }, function()
popup:unmount()
end, { once = true })
```
`event` can be expressed as any of the followings:
```lua
{ event.BufLeave, event.BufDelete }
-- or
{ event.BufLeave, "BufDelete" }
-- or
event.BufLeave
-- or
"BufLeave"
-- or
"BufLeave,BufDelete"
```
---
### `popup:off`
_Signature:_ `popup:off(event)`
Removes `autocmd` defined with `popup:on({ ... })`
**Parameters**
| Name | Type | Description |
| ------- | --------------------- | ----------------- |
| `event` | `string[]` / `string` | check `:h events` |
**Examples**
```lua
popup:off("*")
```
---
### `popup:update_layout`
_Signature:_ `popup:update_layout(config)`
Sets the layout of the popup. You can use this method to change popup's
size or position after it's mounted.
**Parameters**
`config` is a `table` having the following keys:
| Key | Type |
| ---------- | --------------------------------- |
| `anchor` | `"NW"` / `"NE"` / `"SW"` / `"SE"` |
| `relative` | `string` / `table` |
| `position` | `string` / `table` |
| `size` | `string` / `table` |
They are the same options used for popup initialization.
**Examples**
```lua
popup:update_layout({
relative = "win",
size = {
width = 80,
height = 40,
},
position = {
row = 30,
col = 20,
},
})
```
---
### `popup.border:set_highlight`
_Signature:_ `popup.border:set_highlight(highlight: string) -> nil`
Sets border highlight.
**Parameters**
| Name | Type | Description |
| ----------- | -------- | -------------------- |
| `highlight` | `string` | highlight group name |
**Examples**
```lua
popup.border:set_highlight("SpecialChar")
```
---
### `popup.border:set_style`
_Signature:_ `popup.border:set_style(style: string|table) -> nil`
Sets border style.
**Parameters**
| Name | Type | Description |
| ------- | ------------------ | ------------ |
| `style` | `string` / `table` | border style |
This `style` parameter is exactly the same as popup option `border.style`.
You'll need to call `popup:update_layout()` after this for the change to render on screen.
**Examples**
```lua
popup.border:set_style("rounded")
popup:update_layout()
```
---
### `popup.border:set_text`
_Signature:_ `popup.border:set_text(edge, text, align)`
Sets border text.
**Parameters**
| Name | Type |
| ------- | ------------------------------------------- |
| `edge` | `"top"` / `"bottom"` / `"left"` / `"right"` |
| `text` | `string` |
| `align` | `"left"` / `"right"`/ `"center"` |
**Examples**
```lua
popup.border:set_text("bottom", "[Progress: 42%]", "right")
```
## Wiki Page
You can find additional documentation/examples/guides/tips-n-tricks in [nui.popup wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.popup).

View file

@ -0,0 +1,746 @@
---@diagnostic disable: invisible
local Object = require("nui.object")
local Line = require("nui.line")
local Text = require("nui.text")
local _ = require("nui.utils")._
local is_type = require("nui.utils").is_type
local has_nvim_0_5_1 = vim.fn.has("nvim-0.5.1") == 1
local has_nvim_0_11_0 = _.feature.v0_11
local index_name = {
"top_left",
"top",
"top_right",
"right",
"bottom_right",
"bottom",
"bottom_left",
"left",
}
---@param border _nui_popup_border_style_list
---@return _nui_popup_border_style_map
local function to_border_map(border)
local count = vim.tbl_count(border) --[[@as integer]]
if count < 8 then
-- fillup all 8 characters
for i = count + 1, 8 do
local fallback_index = i % count
local char = border[fallback_index == 0 and count or fallback_index]
if type(char) == "table" then
char = char.content and Text(char) or vim.deepcopy(char)
end
border[i] = char
end
end
---@type _nui_popup_border_style_map
local named_border = {}
for index, name in ipairs(index_name) do
named_border[name] = border[index]
end
return named_border
end
---@param char _nui_popup_border_style_map
---@return _nui_popup_border_internal_char
local function normalize_char_map(char)
if not char or type(char) == "string" then
return char
end
for position, item in pairs(char) do
if type(item) == "string" then
char[position] = Text(item, "FloatBorder")
elseif not item.content then
char[position] = Text(item[1], item[2] or "FloatBorder")
elseif item.extmark then
item.extmark.hl_group = item.extmark.hl_group or "FloatBorder"
else
item.extmark = { hl_group = "FloatBorder" }
end
end
return char --[[@as _nui_popup_border_internal_char]]
end
---@param char? NuiText
---@return boolean
local function is_empty_char(char)
return not char or 0 == char:width()
end
---@param text? _nui_popup_border_option_text_value
---@return nil|NuiLine|NuiText
local function normalize_border_text(text)
if not text then
return text
end
if type(text) == "string" then
return Text(text, "FloatTitle")
end
if text.content then
for _, text_chunk in ipairs(text._texts or { text }) do
text_chunk.extmark = vim.tbl_deep_extend("keep", text_chunk.extmark or {}, {
hl_group = "FloatTitle",
})
end
return text --[[@as NuiLine|NuiText]]
end
local line = Line()
for _, chunk in ipairs(text) do
if type(chunk) == "string" then
line:append(chunk, "FloatTitle")
else
line:append(chunk[1], chunk[2] or "FloatTitle")
end
end
return line
end
---@param internal nui_popup_border_internal
---@param popup_winhighlight? string
---@return nil|string
local function calculate_winhighlight(internal, popup_winhighlight)
if internal.type == "simple" then
return
end
local winhl = popup_winhighlight
-- @deprecated
if internal.highlight then
if not string.match(internal.highlight, ":") then
internal.highlight = "FloatBorder:" .. internal.highlight
end
winhl = internal.highlight
internal.highlight = nil
end
return winhl
end
---@param padding? nui_popup_border_option_padding
---@return nil|nui_popup_border_internal_padding
local function normalize_option_padding(padding)
if not padding then
return nil
end
if is_type("map", padding) then
---@cast padding _nui_popup_border_option_padding_map
return padding
end
local map = {}
---@cast padding _nui_popup_border_option_padding_list
map.top = padding[1] or 0
map.right = padding[2] or map.top
map.bottom = padding[3] or map.top
map.left = padding[4] or map.right
return map
end
---@param text? nui_popup_border_option_text
---@return nil|nui_popup_border_internal_text
local function normalize_option_text(text)
if not text then
return text
end
text.top = normalize_border_text(text.top)
text.bottom = normalize_border_text(text.bottom)
return text --[[@as nui_popup_border_internal_text]]
end
---@param edge 'top'|'bottom'
---@param text? NuiLine|NuiText
---@param align? nui_t_text_align
---@return NuiLine
local function calculate_buf_edge_line(internal, edge, text, align)
local char, size = internal.char, internal.size
local left_char = char[edge .. "_left"]
local mid_char = char[edge]
local right_char = char[edge .. "_right"]
if left_char:content() == "" then
left_char = Text(mid_char:content() == "" and char["left"] or mid_char)
end
if right_char:content() == "" then
right_char = Text(mid_char:content() == "" and char["right"] or mid_char)
end
local max_width = size.width - left_char:width() - right_char:width()
local content = Line()
if mid_char:width() == 0 then
content:append(string.rep(" ", max_width))
else
content:append(text or "")
end
_.truncate_nui_line(content, max_width)
local left_gap_width, right_gap_width = _.calculate_gap_width(align or "center", max_width, content:width())
local line = Line()
line:append(left_char)
if left_gap_width > 0 then
line:append(Text(mid_char):set(string.rep(mid_char:content(), left_gap_width)))
end
line:append(content)
if right_gap_width > 0 then
line:append(Text(mid_char):set(string.rep(mid_char:content(), right_gap_width)))
end
line:append(right_char)
return line
end
---@param internal nui_popup_border_internal
---@return nil|NuiLine[]
local function calculate_buf_lines(internal)
local char, size, text = internal.char, internal.size, internal.text or {}
if type(char) == "string" then
return nil
end
local left_char, right_char = char.left, char.right
local gap_length = size.width - left_char:width() - right_char:width()
---@type NuiLine[]
local lines = {}
table.insert(lines, calculate_buf_edge_line(internal, "top", text.top, text.top_align))
for _ = 1, size.height - 2 do
table.insert(
lines,
Line({
Text(left_char),
Text(string.rep(" ", gap_length)),
Text(right_char),
})
)
end
table.insert(lines, calculate_buf_edge_line(internal, "bottom", text.bottom, text.bottom_align))
return lines
end
local styles = {
bold = to_border_map({ "", "", "", "", "", "", "", "" }),
double = to_border_map({ "", "", "", "", "", "", "", "" }),
none = "none",
rounded = to_border_map({ "", "", "", "", "", "", "", "" }),
shadow = "shadow",
single = to_border_map({ "", "", "", "", "", "", "", "" }),
solid = to_border_map({ " ", " ", " ", " ", " ", " ", " ", " " }),
}
---@param style nui_popup_border_option_style
---@param prev_char_map? _nui_popup_border_internal_char
---@return _nui_popup_border_style_map
local function prepare_char_map(style, prev_char_map)
if type(style) == "string" then
if not styles[style] then
error("invalid border style name")
end
---@cast style _nui_popup_border_style_builtin
return vim.deepcopy(styles[style])
end
if is_type("list", style) then
---@cast style _nui_popup_border_style_list
return to_border_map(style)
end
---@cast style _nui_popup_border_style_map
return vim.tbl_extend("force", prev_char_map or {}, style)
end
---@param internal nui_popup_border_internal
---@return nui_popup_border_internal_size
local function calculate_size_delta(internal)
---@type nui_popup_border_internal_size
local delta = {
width = 0,
height = 0,
}
local char = internal.char
if type(char) == "table" then
if not is_empty_char(char.top) then
delta.height = delta.height + 1
end
if not is_empty_char(char.bottom) then
delta.height = delta.height + 1
end
if not is_empty_char(char.left) then
delta.width = delta.width + 1
end
if not is_empty_char(char.right) then
delta.width = delta.width + 1
end
end
local padding = internal.padding
if padding then
if padding.top then
delta.height = delta.height + padding.top
end
if padding.bottom then
delta.height = delta.height + padding.bottom
end
if padding.left then
delta.width = delta.width + padding.left
end
if padding.right then
delta.width = delta.width + padding.right
end
end
return delta
end
---@param border NuiPopupBorder
---@return nui_popup_border_internal_size
local function calculate_size(border)
---@type nui_popup_border_internal_size
local size = vim.deepcopy(border.popup._.size)
size.width = size.width + border._.size_delta.width
size.height = size.height + border._.size_delta.height
return size
end
---@param border NuiPopupBorder
---@return nui_popup_border_internal_position
local function calculate_position(border)
local position = vim.deepcopy(border.popup._.position)
position.col = position.col - math.floor(border._.size_delta.width / 2 + 0.5)
position.row = position.row - math.floor(border._.size_delta.height / 2 + 0.5)
return position
end
local function adjust_popup_win_config(border)
local internal = border._
local popup_position = {
row = 0,
col = 0,
}
local char = internal.char
if type(char) == "table" then
if not is_empty_char(char.top) then
popup_position.row = popup_position.row + 1
end
if not is_empty_char(char.left) then
popup_position.col = popup_position.col + 1
end
end
local padding = internal.padding
if padding then
if padding.top then
popup_position.row = popup_position.row + padding.top
end
if padding.left then
popup_position.col = popup_position.col + padding.left
end
end
local popup = border.popup
-- luacov: disable
if not has_nvim_0_5_1 then
popup.win_config.row = internal.position.row + popup_position.row
popup.win_config.col = internal.position.col + popup_position.col
return
end
-- luacov: enable
-- relative to the border window
popup.win_config.anchor = nil
popup.win_config.relative = "win"
popup.win_config.win = border.winid
popup.win_config.bufpos = nil
popup.win_config.row = popup_position.row
popup.win_config.col = popup_position.col
end
--luacheck: push no max line length
---@alias nui_t_text_align 'left'|'center'|'right'
---@alias nui_popup_border_internal_type 'simple'|'complex'
---@alias nui_popup_border_internal_position table<'row'|'col', number>
---@alias nui_popup_border_internal_size table<'height'|'width', number>
---@alias nui_popup_border_internal_padding _nui_popup_border_option_padding_map
---@alias nui_popup_border_internal_text { top?: NuiLine|NuiText, top_align?: nui_t_text_align, bottom?: NuiLine|NuiText, bottom_align?: nui_t_text_align }
---@alias _nui_popup_border_internal_char table<_nui_popup_border_style_map_position, NuiText>
---@alias _nui_popup_border_option_padding_list table<1|2|3|4, integer>
---@alias _nui_popup_border_option_padding_map table<'top'|'right'|'bottom'|'left', integer>
---@alias nui_popup_border_option_padding _nui_popup_border_option_padding_list|_nui_popup_border_option_padding_map
---@alias _nui_popup_border_style_char_tuple table<1|2, string>
---@alias _nui_popup_border_style_char string|_nui_popup_border_style_char_tuple|NuiText
---@alias _nui_popup_border_style_builtin 'double'|'none'|'rounded'|'shadow'|'single'|'solid'|'default'
---@alias _nui_popup_border_style_list table<1|2|3|4|5|6|7|8, _nui_popup_border_style_char>
---@alias _nui_popup_border_style_map_position 'top_left'|'top'|'top_right'|'right'|'bottom_right'|'bottom'|'bottom_left'|'left'
---@alias _nui_popup_border_style_map table<_nui_popup_border_style_map_position, _nui_popup_border_style_char>
---@alias nui_popup_border_option_style _nui_popup_border_style_builtin|_nui_popup_border_style_list|_nui_popup_border_style_map
---@alias _nui_popup_border_option_text_value string|NuiLine|NuiText|string[]|table<1|2, string>[]
---@alias nui_popup_border_option_text { top?: _nui_popup_border_option_text_value, top_align?: nui_t_text_align, bottom?: _nui_popup_border_option_text_value, bottom_align?: nui_t_text_align }
--luacheck: pop
---@class nui_popup_border_internal
---@field type nui_popup_border_internal_type
---@field style nui_popup_border_option_style
---@field char _nui_popup_border_internal_char
---@field padding? _nui_popup_border_option_padding_map
---@field position nui_popup_border_internal_position
---@field size nui_popup_border_internal_size
---@field size_delta nui_popup_border_internal_size
---@field text? nui_popup_border_internal_text
---@field lines? NuiLine[]
---@field winhighlight? string
---@class nui_popup_border_options
---@field padding? nui_popup_border_option_padding
---@field style? nui_popup_border_option_style
---@field text? nui_popup_border_option_text
---@class NuiPopupBorder
---@field bufnr integer
---@field private _ nui_popup_border_internal
---@field private popup NuiPopup
---@field win_config nui_popup_win_config
---@field winid number
local Border = Object("NuiPopupBorder")
---@param popup NuiPopup
---@param options nui_popup_border_options
function Border:init(popup, options)
self.popup = popup
self._ = {
---@deprecated
highlight = options.highlight,
padding = normalize_option_padding(options.padding),
text = normalize_option_text(options.text),
}
local internal = self._
if internal.text or internal.padding then
internal.type = "complex"
else
internal.type = "simple"
end
self:set_style(options.style or _.get_default_winborder())
internal.winhighlight = calculate_winhighlight(internal, self.popup._.win_options.winhighlight)
if internal.type == "simple" then
return self
end
self:_buf_create()
self.win_config = {
style = "minimal",
border = "none",
focusable = false,
zindex = self.popup.win_config.zindex,
anchor = self.popup.win_config.anchor,
}
if type(internal.char) == "string" then
self.win_config.border = internal.char
end
end
function Border:_open_window()
if self.winid or not self.bufnr then
return
end
self.win_config.noautocmd = true
self.winid = vim.api.nvim_open_win(self.bufnr, false, self.win_config)
self.win_config.noautocmd = nil
assert(self.winid, "failed to create border window")
if self._.winhighlight then
_.set_win_option(self.winid, "winhighlight", self._.winhighlight)
end
if self.popup._.win_options.winblend then
_.set_win_option(self.winid, "winblend", self.popup._.win_options.winblend)
end
adjust_popup_win_config(self)
vim.api.nvim_command("redraw")
end
function Border:_close_window()
if not self.winid then
return
end
if vim.api.nvim_win_is_valid(self.winid) then
vim.api.nvim_win_close(self.winid, true)
end
self.winid = nil
end
function Border:_buf_create()
if not self.bufnr or not vim.api.nvim_buf_is_valid(self.bufnr) then
self.bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[self.bufnr].modifiable = true
assert(self.bufnr, "failed to create border buffer")
end
end
function Border:mount()
local popup = self.popup
if not popup._.loading or popup._.mounted then
return
end
local internal = self._
if internal.type == "simple" then
return
end
self:_buf_create()
if internal.lines then
_.render_lines(internal.lines, self.bufnr, popup.ns_id, 1, #internal.lines)
end
self:_open_window()
end
function Border:unmount()
local popup = self.popup
if not popup._.loading or not popup._.mounted then
return
end
local internal = self._
if internal.type == "simple" then
return
end
if self.bufnr then
if vim.api.nvim_buf_is_valid(self.bufnr) then
_.clear_namespace(self.bufnr, self.popup.ns_id)
vim.api.nvim_buf_delete(self.bufnr, { force = true })
end
self.bufnr = nil
end
self:_close_window()
end
function Border:_relayout()
local internal = self._
if internal.type ~= "complex" then
return
end
if self.popup.win_config.anchor and self.popup.win_config.anchor ~= self.win_config.anchor then
self.win_config.anchor = self.popup.win_config.anchor
self.popup.win_config.anchor = nil
end
local position = self.popup._.position
self.win_config.relative = position.relative
self.win_config.win = position.relative == "win" and position.win or nil
self.win_config.bufpos = position.bufpos
internal.size = calculate_size(self)
self.win_config.width = internal.size.width
self.win_config.height = internal.size.height
internal.position = calculate_position(self)
self.win_config.row = internal.position.row
self.win_config.col = internal.position.col
internal.lines = calculate_buf_lines(internal)
if self.winid then
vim.api.nvim_win_set_config(self.winid, self.win_config)
end
if self.bufnr then
if internal.lines then
_.render_lines(internal.lines, self.bufnr, self.popup.ns_id, 1, #internal.lines)
end
end
adjust_popup_win_config(self)
vim.api.nvim_command("redraw")
end
---@param edge "'top'" | "'bottom'"
---@param text? nil|string|NuiLine|NuiText
---@param align? nil | "'left'" | "'center'" | "'right'"
function Border:set_text(edge, text, align)
local internal = self._
if not internal.text then
return
end
internal.text[edge] = normalize_border_text(text)
internal.text[edge .. "_align"] = align or internal.text[edge .. "_align"]
if not internal.lines then
return
end
local line = calculate_buf_edge_line(
internal,
edge,
internal.text[edge],
internal.text[edge .. "_align"] --[[@as nui_t_text_align]]
)
local linenr = edge == "top" and 1 or #internal.lines
internal.lines[linenr] = line
line:render(self.bufnr, self.popup.ns_id, linenr)
end
---@param highlight string highlight group
function Border:set_highlight(highlight)
local internal = self._
local winhighlight_data = _.parse_winhighlight(self.popup._.win_options.winhighlight)
winhighlight_data["FloatBorder"] = highlight
self.popup._.win_options.winhighlight = _.serialize_winhighlight(winhighlight_data)
if self.popup.winid then
_.set_win_option(self.popup.winid, "winhighlight", self.popup._.win_options.winhighlight)
end
internal.winhighlight = calculate_winhighlight(internal, self.popup._.win_options.winhighlight)
if self.winid then
_.set_win_option(self.winid, "winhighlight", internal.winhighlight)
end
end
---@param style nui_popup_border_option_style
function Border:set_style(style)
local internal = self._
if style == "default" then
style = _.get_default_winborder()
if style == "none" and internal.type == "complex" then
style = "single"
end
end
internal.style = style
local char = prepare_char_map(internal.style, internal.char)
local is_borderless = type(char) == "string"
if is_borderless then
if not internal.char then -- initial
if internal.text then
error("text not supported for style:" .. char)
end
elseif internal.type == "complex" then -- subsequent
error("cannot change from previous style to " .. char)
end
end
internal.char = normalize_char_map(char)
internal.size_delta = calculate_size_delta(internal)
end
---@param char_map _nui_popup_border_internal_char
---@return _nui_popup_border_style_char_tuple[]
local function to_tuple_list(char_map)
---@type _nui_popup_border_style_char_tuple[]
local border = {}
for index, name in ipairs(index_name) do
if not char_map[name] then
error(string.format("missing named border: %s", name))
end
local char = char_map[name]
border[index] = { char:content(), char.extmark.hl_group }
end
return border
end
---@return nil|_nui_popup_border_style_builtin|_nui_popup_border_style_char_tuple[]
function Border:get()
local internal = self._
if internal.type ~= "simple" then
if has_nvim_0_11_0 then
return "none"
end
return nil
end
if type(internal.char) == "string" then
return internal.char
end
return to_tuple_list(internal.char)
end
---@alias NuiPopupBorder.constructor fun(popup: NuiPopup, options: nui_popup_border_options): NuiPopupBorder
---@type NuiPopupBorder|NuiPopupBorder.constructor
local NuiPopupBorder = Border
return NuiPopupBorder

View file

@ -0,0 +1,426 @@
local Border = require("nui.popup.border")
local Object = require("nui.object")
local buf_storage = require("nui.utils.buf_storage")
local autocmd = require("nui.utils.autocmd")
local keymap = require("nui.utils.keymap")
local utils = require("nui.utils")
local _ = utils._
local defaults = utils.defaults
local is_type = utils.is_type
local layout_utils = require("nui.layout.utils")
local u = {
clear_namespace = _.clear_namespace,
get_next_id = _.get_next_id,
size = layout_utils.size,
position = layout_utils.position,
update_layout_config = layout_utils.update_layout_config,
}
-- luacov: disable
-- @deprecated
---@param opacity number
---@deprecated
local function calculate_winblend(opacity)
assert(0 <= opacity, "opacity must be equal or greater than 0")
assert(opacity <= 1, "opacity must be equal or lesser than 0")
return 100 - (opacity * 100)
end
-- luacov: enable
local function merge_default_options(options)
options.relative = defaults(options.relative, "win")
options.enter = defaults(options.enter, false)
options.zindex = defaults(options.zindex, 50)
options.buf_options = defaults(options.buf_options, {})
options.win_options = defaults(options.win_options, {})
options.border = defaults(options.border, _.get_default_winborder())
return options
end
local function normalize_options(options)
options = _.normalize_layout_options(options)
if is_type("string", options.border) then
options.border = {
style = options.border,
}
end
return options
end
--luacheck: push no max line length
---@alias nui_popup_internal_position { relative: "'cursor'"|"'editor'"|"'win'", win: number, bufpos?: number[], row: number, col: number }
---@alias nui_popup_internal_size { height: number, width: number }
---@alias nui_popup_win_config { focusable: boolean, style: "'minimal'", zindex: number, relative: "'cursor'"|"'editor'"|"'win'", win?: number, bufpos?: number[], row: number, col: number, width: number, height: number, border?: string|table, anchor?: nui_layout_option_anchor }
--luacheck: pop
---@class nui_popup_internal
---@field augroup table<'hide'|'unmount', string>
---@field buf_options table<string, any>
---@field layout table
---@field layout_ready boolean
---@field loading boolean
---@field mounted boolean
---@field position nui_popup_internal_position
---@field size nui_popup_internal_size
---@field unmanaged_bufnr? boolean
---@field win_config nui_popup_win_config
---@field win_enter boolean
---@field win_options table<string, any>
---@class nui_popup_options
---@field border? _nui_popup_border_style_builtin|nui_popup_border_options
---@field ns_id? string|integer
---@field anchor? nui_layout_option_anchor
---@field relative? nui_layout_option_relative_type|nui_layout_option_relative
---@field position? number|string|nui_layout_option_position
---@field size? number|string|nui_layout_option_size
---@field enter? boolean
---@field focusable? boolean
---@field zindex? integer
---@field buf_options? table<string, any>
---@field win_options? table<string, any>
---@field bufnr? integer
---@class NuiPopup
---@field border NuiPopupBorder
---@field bufnr integer
---@field ns_id integer
---@field private _ nui_popup_internal
---@field win_config nui_popup_win_config
---@field winid number
local Popup = Object("NuiPopup")
---@param options nui_popup_options
function Popup:init(options)
local id = u.get_next_id()
options = merge_default_options(options)
options = normalize_options(options)
self._ = {
id = id,
buf_options = options.buf_options,
layout = {},
layout_ready = false,
loading = false,
mounted = false,
win_enter = options.enter,
win_options = options.win_options,
win_config = {
focusable = options.focusable,
style = "minimal",
anchor = options.anchor,
zindex = options.zindex,
},
augroup = {
hide = string.format("%s_hide", id),
unmount = string.format("%s_unmount", id),
},
}
self.win_config = self._.win_config
self.ns_id = _.normalize_namespace_id(options.ns_id)
if options.bufnr then
self.bufnr = options.bufnr
self._.unmanaged_bufnr = true
else
self:_buf_create()
end
-- luacov: disable
-- @deprecated
if not self._.win_options.winblend and is_type("number", options.opacity) then
self._.win_options.winblend = calculate_winblend(options.opacity)
end
-- @deprecated
if not self._.win_options.winhighlight and not is_type("nil", options.highlight) then
self._.win_options.winhighlight = options.highlight
end
-- luacov: enable
self.border = Border(self, options.border)
self.win_config.border = self.border:get()
if options.position and options.size then
self:update_layout(options)
end
end
function Popup:_open_window()
if self.winid or not self.bufnr then
return
end
self.win_config.noautocmd = true
self.winid = vim.api.nvim_open_win(self.bufnr, self._.win_enter, self.win_config)
self.win_config.noautocmd = nil
vim.api.nvim_win_call(self.winid, function()
autocmd.exec("BufWinEnter", {
buffer = self.bufnr,
modeline = false,
})
end)
assert(self.winid, "failed to create popup window")
_.set_win_options(self.winid, self._.win_options)
end
function Popup:_close_window()
if not self.winid then
return
end
if vim.api.nvim_win_is_valid(self.winid) then
vim.api.nvim_win_close(self.winid, true)
end
self.winid = nil
end
function Popup:_buf_create()
if not self.bufnr then
self.bufnr = vim.api.nvim_create_buf(false, true)
assert(self.bufnr, "failed to create buffer")
end
end
function Popup:mount()
if not self._.layout_ready then
return error("layout is not ready")
end
if self._.loading or self._.mounted then
return
end
self._.loading = true
autocmd.create_group(self._.augroup.hide, { clear = true })
autocmd.create_group(self._.augroup.unmount, { clear = true })
autocmd.create("QuitPre", {
group = self._.augroup.unmount,
buffer = self.bufnr,
callback = vim.schedule_wrap(function()
self:unmount()
end),
}, self.bufnr)
autocmd.create("BufWinEnter", {
group = self._.augroup.unmount,
buffer = self.bufnr,
callback = function()
-- When two popup using the same buffer and both of them
-- are hidden, calling `:show` for one of them fires
-- `BufWinEnter` for both of them. And in that scenario
-- one of them will not have `self.winid`.
if self.winid then
-- @todo skip registering `WinClosed` multiple times
-- for the same popup
autocmd.create("WinClosed", {
group = self._.augroup.hide,
nested = true,
pattern = tostring(self.winid),
callback = function()
self:hide()
end,
}, self.bufnr)
end
end,
}, self.bufnr)
self.border:mount()
self:_buf_create()
_.set_buf_options(self.bufnr, self._.buf_options)
self:_open_window()
self._.loading = false
self._.mounted = true
end
function Popup:hide()
if self._.loading or not self._.mounted then
return
end
self._.loading = true
pcall(autocmd.delete_group, self._.augroup.hide)
self.border:_close_window()
self:_close_window()
self._.loading = false
end
function Popup:show()
if self._.loading then
return
end
if not self._.mounted then
return self:mount()
end
self._.loading = true
autocmd.create_group(self._.augroup.hide, { clear = true })
self.border:_open_window()
self:_open_window()
self._.loading = false
end
function Popup:_buf_destroy()
if self.bufnr then
if vim.api.nvim_buf_is_valid(self.bufnr) then
u.clear_namespace(self.bufnr, self.ns_id)
if not self._.unmanaged_bufnr then
vim.api.nvim_buf_delete(self.bufnr, { force = true })
end
end
buf_storage.cleanup(self.bufnr)
if not self._.unmanaged_bufnr then
self.bufnr = nil
end
end
end
function Popup:unmount()
if self._.loading or not self._.mounted then
return
end
self._.loading = true
pcall(autocmd.delete_group, self._.augroup.hide)
pcall(autocmd.delete_group, self._.augroup.unmount)
self.border:unmount()
self:_buf_destroy()
self:_close_window()
self._.loading = false
self._.mounted = false
end
-- set keymap for this popup window
---@param mode string check `:h :map-modes`
---@param key string|string[] key for the mapping
---@param handler string | fun(): nil handler for the mapping
---@param opts? table<"'expr'"|"'noremap'"|"'nowait'"|"'remap'"|"'script'"|"'silent'"|"'unique'", boolean>
---@return nil
function Popup:map(mode, key, handler, opts, ___force___)
if not self.bufnr then
error("popup buffer not found.")
end
return keymap.set(self.bufnr, mode, key, handler, opts, ___force___)
end
---@param mode string check `:h :map-modes`
---@param key string|string[] key for the mapping
---@return nil
function Popup:unmap(mode, key, ___force___)
if not self.bufnr then
error("popup buffer not found.")
end
return keymap._del(self.bufnr, mode, key, ___force___)
end
---@param event string | string[]
---@param handler string | function
---@param options? table<"'once'" | "'nested'", boolean>
function Popup:on(event, handler, options)
if not self.bufnr then
error("popup buffer not found.")
end
autocmd.buf.define(self.bufnr, event, handler, options)
end
---@param event? string | string[]
function Popup:off(event)
if not self.bufnr then
error("popup buffer not found.")
end
autocmd.buf.remove(self.bufnr, nil, event)
end
-- luacov: disable
-- @deprecated
-- Use `popup:update_layout`.
---@deprecated
function Popup:set_layout(config)
return self:update_layout(config)
end
-- luacov: enable
---@param config? nui_layout_options
function Popup:update_layout(config)
config = config or {}
u.update_layout_config(self._, config)
self.border:_relayout()
self._.layout_ready = true
if self.winid then
-- upstream issue: https://github.com/neovim/neovim/issues/20370
local win_config_style = self.win_config.style
---@diagnostic disable-next-line: assign-type-mismatch
self.win_config.style = ""
vim.api.nvim_win_set_config(self.winid, self.win_config)
self.win_config.style = win_config_style
end
end
-- luacov: disable
-- @deprecated
-- Use `popup:update_layout`.
---@deprecated
function Popup:set_size(size)
self:update_layout({ size = size })
end
-- luacov: enable
-- luacov: disable
-- @deprecated
-- Use `popup:update_layout`.
---@deprecated
function Popup:set_position(position, relative)
self:update_layout({ position = position, relative = relative })
end
-- luacov: enable
---@alias NuiPopup.constructor fun(options: nui_popup_options): NuiPopup
---@type NuiPopup|NuiPopup.constructor
local NuiPopup = Popup
return NuiPopup