Meh I'll figure out submodules later
This commit is contained in:
parent
4ca9d44a90
commit
8cb281f436
352 changed files with 66107 additions and 0 deletions
207
.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/README.md
Normal file
207
.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/README.md
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
# Menu
|
||||
|
||||
`Menu` is abstraction layer on top of `Popup`.
|
||||
|
||||
```lua
|
||||
local Menu = require("nui.menu")
|
||||
local event = require("nui.utils.autocmd").event
|
||||
|
||||
local popup_options = {
|
||||
relative = "cursor",
|
||||
position = {
|
||||
row = 1,
|
||||
col = 0,
|
||||
},
|
||||
border = {
|
||||
style = "rounded",
|
||||
text = {
|
||||
top = "[Choose Item]",
|
||||
top_align = "center",
|
||||
},
|
||||
},
|
||||
win_options = {
|
||||
winhighlight = "Normal:Normal",
|
||||
}
|
||||
}
|
||||
|
||||
local menu = Menu(popup_options, {
|
||||
lines = {
|
||||
Menu.separator("Group One"),
|
||||
Menu.item("Item 1"),
|
||||
Menu.item("Item 2"),
|
||||
Menu.separator("Group Two", {
|
||||
char = "-",
|
||||
text_align = "right",
|
||||
}),
|
||||
Menu.item("Item 3"),
|
||||
Menu.item("Item 4"),
|
||||
},
|
||||
max_width = 20,
|
||||
keymap = {
|
||||
focus_next = { "j", "<Down>", "<Tab>" },
|
||||
focus_prev = { "k", "<Up>", "<S-Tab>" },
|
||||
close = { "<Esc>", "<C-c>" },
|
||||
submit = { "<CR>", "<Space>" },
|
||||
},
|
||||
on_close = function()
|
||||
print("CLOSED")
|
||||
end,
|
||||
on_submit = function(item)
|
||||
print("SUBMITTED", vim.inspect(item))
|
||||
end,
|
||||
})
|
||||
```
|
||||
|
||||
You can manipulate the associated buffer and window using the
|
||||
`split.bufnr` and `split.winid` properties.
|
||||
|
||||
**NOTE**: the first argument accepts options for `nui.popup` component.
|
||||
|
||||
## Options
|
||||
|
||||
### `lines`
|
||||
|
||||
**Type:** `table`
|
||||
|
||||
List of menu items.
|
||||
|
||||
**`Menu.item(content, data?)`**
|
||||
|
||||
`Menu.item` is used to create an item object for the `Menu`.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type |
|
||||
| --------- | -------------------------------- |
|
||||
| `content` | `string` / `NuiText` / `NuiLine` |
|
||||
| `data` | `table` / `nil` |
|
||||
|
||||
**Example**
|
||||
|
||||
```lua
|
||||
Menu.item("One") --> { text = "One" }
|
||||
|
||||
Menu.item("Two", { id = 2 }) --> { id = 2, text = "Two" }
|
||||
```
|
||||
|
||||
This is what you get as the argument of `on_submit` callback function.
|
||||
You can include whatever you want in the item object.
|
||||
|
||||
**`Menu.separator(content?, options?)`**
|
||||
|
||||
`Menu.separator` is used to create a menu item that can't be focused.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type |
|
||||
| --------- | ---------------------------------------------------------------------------------- |
|
||||
| `content` | `string` / `NuiText` / `NuiLine` / `nil` |
|
||||
| `options` | `{ char?: string\|NuiText, text_align?: "'left'"\|"'center'"\|"'right'" }` / `nil` |
|
||||
|
||||
You can just use `Menu.item` only and implement `Menu.separator`'s behavior
|
||||
by providing a custom `should_skip_item` function.
|
||||
|
||||
### `prepare_item`
|
||||
|
||||
**Type:** `function`
|
||||
|
||||
_Signature:_ `prepare_item(item)`
|
||||
|
||||
If provided, this function is used for preparing each menu item.
|
||||
|
||||
The return value should be a `NuiLine` object or `string` or a list containing either of them.
|
||||
|
||||
If return value is `nil`, that node will not be rendered.
|
||||
|
||||
### `should_skip_item`
|
||||
|
||||
**Type:** `function`
|
||||
|
||||
_Signature:_ `should_skip_item(item)`
|
||||
|
||||
If provided, this function is used to determine if an item should be
|
||||
skipped when focusing previous/next item.
|
||||
|
||||
The return value should be `boolean`.
|
||||
|
||||
By default, items created by `Menu.separator` are skipped.
|
||||
|
||||
### `max_height`
|
||||
|
||||
**Type:** `number`
|
||||
|
||||
Maximum height of the menu.
|
||||
|
||||
### `min_height`
|
||||
|
||||
**Type:** `number`
|
||||
|
||||
Minimum height of the menu.
|
||||
|
||||
### `max_width`
|
||||
|
||||
**Type:** `number`
|
||||
|
||||
Maximum width of the menu.
|
||||
|
||||
### `min_width`
|
||||
|
||||
**Type:** `number`
|
||||
|
||||
Minimum width of the menu.
|
||||
|
||||
### `keymap`
|
||||
|
||||
**Type:** `table`
|
||||
|
||||
Key mappings for the menu.
|
||||
|
||||
**Example**
|
||||
|
||||
```lua
|
||||
keymap = {
|
||||
close = { "<Esc>", "<C-c>" },
|
||||
focus_next = { "j", "<Down>", "<Tab>" },
|
||||
focus_prev = { "k", "<Up>", "<S-Tab>" },
|
||||
submit = { "<CR>" },
|
||||
},
|
||||
```
|
||||
|
||||
### `on_change`
|
||||
|
||||
**Type:** `function`
|
||||
|
||||
_Signature:_ `on_change(item, menu) -> nil`
|
||||
|
||||
Callback function, called when menu item is focused.
|
||||
|
||||
### `on_close`
|
||||
|
||||
**Type:** `function`
|
||||
|
||||
_Signature:_ `on_close() -> nil`
|
||||
|
||||
Callback function, called when menu is closed.
|
||||
|
||||
### `on_submit`
|
||||
|
||||
**Type:** `function`
|
||||
|
||||
_Signature:_ `on_submit(item) -> nil`
|
||||
|
||||
Callback function, called when menu is submitted.
|
||||
|
||||
## Methods
|
||||
|
||||
Methods from `nui.popup` are also available for `nui.menu`.
|
||||
|
||||
## Properties
|
||||
|
||||
### `menu.tree`
|
||||
|
||||
The underlying `NuiTree` object used for rendering the menu. You can use it to
|
||||
manipulate the menu items on-the-fly and access all the `NuiTree` methods.
|
||||
|
||||
## Wiki Page
|
||||
|
||||
You can find additional documentation/examples/guides/tips-n-tricks in [nui.menu wiki page](https://github.com/MunifTanjim/nui.nvim/wiki/nui.menu).
|
||||
377
.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/init.lua
Normal file
377
.config/nvim/pack/tree/start/nui.nvim/lua/nui/menu/init.lua
Normal file
|
|
@ -0,0 +1,377 @@
|
|||
local Line = require("nui.line")
|
||||
local Popup = require("nui.popup")
|
||||
local Text = require("nui.text")
|
||||
local Tree = require("nui.tree")
|
||||
local _ = require("nui.utils")._
|
||||
local defaults = require("nui.utils").defaults
|
||||
local is_type = require("nui.utils").is_type
|
||||
|
||||
local function calculate_initial_max_width(items)
|
||||
local max_width = 0
|
||||
|
||||
for _, item in ipairs(items) do
|
||||
local width = 0
|
||||
if is_type("string", item.text) then
|
||||
width = vim.api.nvim_strwidth(item.text)
|
||||
elseif is_type("table", item.text) and item.text.width then
|
||||
width = item.text:width()
|
||||
end
|
||||
|
||||
if max_width < width then
|
||||
max_width = width
|
||||
end
|
||||
end
|
||||
|
||||
return max_width
|
||||
end
|
||||
|
||||
local default_keymap = {
|
||||
close = { "<Esc>", "<C-c>" },
|
||||
focus_next = { "j", "<Down>", "<Tab>" },
|
||||
focus_prev = { "k", "<Up>", "<S-Tab>" },
|
||||
submit = { "<CR>" },
|
||||
}
|
||||
|
||||
---@param keymap table<_nui_menu_keymap_action, string|string[]>
|
||||
---@return table<_nui_menu_keymap_action, string[]>
|
||||
local function parse_keymap(keymap)
|
||||
local result = defaults(keymap, {})
|
||||
|
||||
for name, default_keys in pairs(default_keymap) do
|
||||
if is_type("nil", result[name]) then
|
||||
result[name] = default_keys
|
||||
elseif is_type("string", result[name]) then
|
||||
result[name] = { result[name] }
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
---@type nui_menu_should_skip_item
|
||||
local function default_should_skip_item(node)
|
||||
return node._type == "separator"
|
||||
end
|
||||
|
||||
---@param menu NuiMenu
|
||||
---@return nui_menu_prepare_item
|
||||
local function make_default_prepare_node(menu)
|
||||
local border = menu.border
|
||||
|
||||
local fallback_sep = {
|
||||
char = Text(is_type("table", border._.char) and border._.char.top or " "),
|
||||
text_align = is_type("table", border._.text) and border._.text.top_align or "left",
|
||||
}
|
||||
|
||||
-- luacov: disable
|
||||
if menu._.sep then
|
||||
-- @deprecated
|
||||
|
||||
if menu._.sep.char then
|
||||
fallback_sep.char = Text(menu._.sep.char)
|
||||
end
|
||||
|
||||
if menu._.sep.text_align then
|
||||
fallback_sep.text_align = menu._.sep.text_align
|
||||
end
|
||||
end
|
||||
-- luacov: enable
|
||||
|
||||
local max_width = menu._.size.width
|
||||
|
||||
---@type nui_menu_prepare_item
|
||||
local function default_prepare_node(node)
|
||||
---@type NuiText|NuiLine
|
||||
local content = is_type("string", node.text) and Text(node.text) or node.text
|
||||
|
||||
if node._type == "item" then
|
||||
if content:width() > max_width then
|
||||
if is_type("function", content.set) then
|
||||
---@cast content NuiText
|
||||
_.truncate_nui_text(content, max_width)
|
||||
else
|
||||
---@cast content NuiLine
|
||||
_.truncate_nui_line(content, max_width)
|
||||
end
|
||||
end
|
||||
|
||||
local line = Line()
|
||||
|
||||
line:append(content)
|
||||
|
||||
return line
|
||||
end
|
||||
|
||||
if node._type == "separator" then
|
||||
local sep_char = Text(defaults(node._char, fallback_sep.char))
|
||||
local sep_text_align = defaults(node._text_align, fallback_sep.text_align)
|
||||
|
||||
local sep_max_width = max_width - sep_char:width() * 2
|
||||
|
||||
if content:width() > sep_max_width then
|
||||
if content._texts then
|
||||
---@cast content NuiLine
|
||||
_.truncate_nui_line(content, sep_max_width)
|
||||
else
|
||||
---@cast content NuiText
|
||||
_.truncate_nui_text(content, sep_max_width)
|
||||
end
|
||||
end
|
||||
|
||||
local left_gap_width, right_gap_width =
|
||||
_.calculate_gap_width(defaults(sep_text_align, "center"), sep_max_width, content:width())
|
||||
|
||||
local line = Line()
|
||||
|
||||
line:append(Text(sep_char))
|
||||
|
||||
if left_gap_width > 0 then
|
||||
line:append(Text(sep_char):set(string.rep(sep_char:content(), left_gap_width)))
|
||||
end
|
||||
|
||||
line:append(content)
|
||||
|
||||
if right_gap_width > 0 then
|
||||
line:append(Text(sep_char):set(string.rep(sep_char:content(), right_gap_width)))
|
||||
end
|
||||
|
||||
line:append(Text(sep_char))
|
||||
|
||||
return line
|
||||
end
|
||||
end
|
||||
|
||||
return default_prepare_node
|
||||
end
|
||||
|
||||
---@param menu NuiMenu
|
||||
---@param direction "'next'" | "'prev'"
|
||||
---@param current_linenr nil | number
|
||||
local function focus_item(menu, direction, current_linenr)
|
||||
local curr_linenr = current_linenr or vim.api.nvim_win_get_cursor(menu.winid)[1]
|
||||
|
||||
local next_linenr = nil
|
||||
|
||||
if direction == "next" then
|
||||
if curr_linenr == #menu.tree:get_nodes() then
|
||||
next_linenr = 1
|
||||
else
|
||||
next_linenr = curr_linenr + 1
|
||||
end
|
||||
elseif direction == "prev" then
|
||||
if curr_linenr == 1 then
|
||||
next_linenr = #menu.tree:get_nodes()
|
||||
else
|
||||
next_linenr = curr_linenr - 1
|
||||
end
|
||||
end
|
||||
|
||||
local next_node = menu.tree:get_node(next_linenr)
|
||||
|
||||
if menu._.should_skip_item(next_node) then
|
||||
return focus_item(menu, direction, next_linenr)
|
||||
end
|
||||
|
||||
if next_linenr then
|
||||
vim.api.nvim_win_set_cursor(menu.winid, { next_linenr, 0 })
|
||||
menu._.on_change(next_node)
|
||||
end
|
||||
end
|
||||
|
||||
---@alias nui_menu_prepare_item nui_tree_prepare_node
|
||||
---@alias nui_menu_should_skip_item fun(node: NuiTree.Node): boolean
|
||||
|
||||
---@alias _nui_menu_keymap_action 'close'|'focus_next'|'focus_prev'|'submit'
|
||||
|
||||
---@class nui_menu_internal: nui_popup_internal
|
||||
---@field items NuiTree.Node[]
|
||||
---@field keymap table<_nui_menu_keymap_action, string[]>
|
||||
---@field sep { char?: string|NuiText, text_align?: nui_t_text_align } # deprecated
|
||||
---@field prepare_item nui_menu_prepare_item
|
||||
---@field should_skip_item nui_menu_should_skip_item
|
||||
---@field on_change fun(item: NuiTree.Node): nil
|
||||
|
||||
---@class nui_menu_options
|
||||
---@field lines NuiTree.Node[]
|
||||
---@field prepare_item? nui_tree_prepare_node
|
||||
---@field should_skip_item? nui_menu_should_skip_item
|
||||
---@field max_height? integer
|
||||
---@field min_height? integer
|
||||
---@field max_width? integer
|
||||
---@field min_width? integer
|
||||
---@field keymap? table<_nui_menu_keymap_action, string|string[]>
|
||||
---@field on_change? fun(item: NuiTree.Node, menu: NuiMenu): nil
|
||||
---@field on_close? fun(): nil
|
||||
---@field on_submit? fun(item: NuiTree.Node): nil
|
||||
|
||||
---@class NuiMenu: NuiPopup
|
||||
---@field private _ nui_menu_internal
|
||||
local Menu = Popup:extend("NuiMenu")
|
||||
|
||||
---@param content? string|NuiText|NuiLine
|
||||
---@param options? { char?: string|NuiText, text_align?: nui_t_text_align }
|
||||
---@return NuiTree.Node
|
||||
function Menu.separator(content, options)
|
||||
options = options or {}
|
||||
return Tree.Node({
|
||||
_id = tostring(math.random()),
|
||||
_type = "separator",
|
||||
_char = options.char,
|
||||
_text_align = options.text_align,
|
||||
text = defaults(content, ""),
|
||||
})
|
||||
end
|
||||
|
||||
---@param content string|NuiText|NuiLine
|
||||
---@param data? table
|
||||
---@return NuiTree.Node
|
||||
function Menu.item(content, data)
|
||||
if not data then
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
if is_type("table", content) and content.text then
|
||||
---@cast content table
|
||||
data = content
|
||||
else
|
||||
data = { text = content }
|
||||
end
|
||||
else
|
||||
data.text = content
|
||||
end
|
||||
|
||||
data._type = "item"
|
||||
data._id = data.id or tostring(math.random())
|
||||
|
||||
return Tree.Node(data)
|
||||
end
|
||||
|
||||
---@param popup_options nui_popup_options
|
||||
---@param options nui_menu_options
|
||||
function Menu:init(popup_options, options)
|
||||
local max_width = calculate_initial_max_width(options.lines)
|
||||
|
||||
local width = math.max(math.min(max_width, defaults(options.max_width, 256)), defaults(options.min_width, 4))
|
||||
local height = math.max(math.min(#options.lines, defaults(options.max_height, 256)), defaults(options.min_height, 1))
|
||||
|
||||
---@type nui_popup_options
|
||||
popup_options = vim.tbl_deep_extend("force", {
|
||||
enter = true,
|
||||
size = {
|
||||
width = width,
|
||||
height = height,
|
||||
},
|
||||
win_options = {
|
||||
cursorline = true,
|
||||
scrolloff = 1,
|
||||
sidescrolloff = 0,
|
||||
},
|
||||
zindex = 60,
|
||||
}, popup_options)
|
||||
|
||||
Menu.super.init(self, popup_options)
|
||||
|
||||
self._.items = options.lines
|
||||
self._.keymap = parse_keymap(options.keymap)
|
||||
|
||||
---@param node NuiTree.Node
|
||||
self._.on_change = function(node)
|
||||
if options.on_change then
|
||||
options.on_change(node, self)
|
||||
end
|
||||
end
|
||||
|
||||
---@deprecated
|
||||
self._.sep = options.separator
|
||||
|
||||
self._.should_skip_item = defaults(options.should_skip_item, default_should_skip_item)
|
||||
self._.prepare_item = defaults(options.prepare_item, self._.prepare_item)
|
||||
|
||||
self.menu_props = {}
|
||||
|
||||
local props = self.menu_props
|
||||
|
||||
props.on_submit = function()
|
||||
local item = self.tree:get_node()
|
||||
|
||||
self:unmount()
|
||||
|
||||
if options.on_submit then
|
||||
options.on_submit(item)
|
||||
end
|
||||
end
|
||||
|
||||
props.on_close = function()
|
||||
self:unmount()
|
||||
|
||||
if options.on_close then
|
||||
options.on_close()
|
||||
end
|
||||
end
|
||||
|
||||
props.on_focus_next = function()
|
||||
focus_item(self, "next")
|
||||
end
|
||||
|
||||
props.on_focus_prev = function()
|
||||
focus_item(self, "prev")
|
||||
end
|
||||
end
|
||||
|
||||
---@param config? nui_layout_options
|
||||
function Menu:update_layout(config)
|
||||
Menu.super.update_layout(self, config)
|
||||
|
||||
self._.prepare_item = defaults(self._.prepare_item, make_default_prepare_node(self))
|
||||
end
|
||||
|
||||
function Menu:mount()
|
||||
Menu.super.mount(self)
|
||||
|
||||
local props = self.menu_props
|
||||
|
||||
for _, key in pairs(self._.keymap.focus_next) do
|
||||
self:map("n", key, props.on_focus_next, { noremap = true, nowait = true })
|
||||
end
|
||||
|
||||
for _, key in pairs(self._.keymap.focus_prev) do
|
||||
self:map("n", key, props.on_focus_prev, { noremap = true, nowait = true })
|
||||
end
|
||||
|
||||
for _, key in pairs(self._.keymap.close) do
|
||||
self:map("n", key, props.on_close, { noremap = true, nowait = true })
|
||||
end
|
||||
|
||||
for _, key in pairs(self._.keymap.submit) do
|
||||
self:map("n", key, props.on_submit, { noremap = true, nowait = true })
|
||||
end
|
||||
|
||||
self.tree = Tree({
|
||||
winid = self.winid,
|
||||
ns_id = self.ns_id,
|
||||
nodes = self._.items,
|
||||
get_node_id = function(node)
|
||||
return node._id
|
||||
end,
|
||||
prepare_node = self._.prepare_item,
|
||||
})
|
||||
|
||||
---@deprecated
|
||||
self._tree = self.tree
|
||||
|
||||
self.tree:render()
|
||||
|
||||
-- focus first item
|
||||
for linenr = 1, #self.tree:get_nodes() do
|
||||
local node, target_linenr = self.tree:get_node(linenr)
|
||||
if not self._.should_skip_item(node) then
|
||||
vim.api.nvim_win_set_cursor(self.winid, { target_linenr, 0 })
|
||||
self._.on_change(node)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@alias NuiMenu.constructor fun(popup_options: nui_popup_options, options: nui_menu_options): NuiMenu
|
||||
---@type NuiMenu|NuiMenu.constructor
|
||||
local NuiMenu = Menu
|
||||
|
||||
return NuiMenu
|
||||
Loading…
Add table
Add a link
Reference in a new issue