Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 83 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,107 @@ Menu ui for neovim ( supports nested menus )

https://github.com/user-attachments/assets/89d96170-e039-4d3d-9640-0fdc3358a833

## Install
## Features

- LSP Actions menu
- mapleader which-key menu
- neo-tree support
- nvimtree support

## Installation

Install the plugin with your package manager:

[lazy.nvim](https://github.com/folke/lazy.nvim)

```lua
{ "nvzone/volt" , lazy = true },
{ "nvzone/menu" , lazy = true },
{
"nvzone/menu",
dependencies = { "nvzone/volt" },
opts = {
-- your configuration comes here
-- or leave it empty to use the default settings
},
}
```

Default settings:

```lua
opts = {
-- Overwrite filetype menu in handler(). If not provided, require("menus.ft." .. filetype) is tried to load the menu.
ft = {},
-- The default menu to open in handler(), if filetype specific menu is not found.
default_menu = "default",
-- Should we install default mappings? Default mappings presented below.
default_mappings = false,
-- Should the menu have a border?
border = false,
item_gap = 5,
},
```

## Usage

To open a menu, you can use:

```lua
require("menu").open(options, opts)
require("menu").open(items, opts)
```
- options is a table or string, if string then it will look at the table from menus* module of this repo
- opts : { mouse = true, border = false }"

Items is detected to be:
- a function, in which case it is called and it can return a string or a table.
- a string, in which case `require("menus." .. items)` is called. This may result in a function, which is then called.
- the end result has to be a table of menu items.

Opts has the following attributes:
- **mouse**: (`boolean`) When true, will create menu at cursor position.

Menu item has the following attributes:
- **name**: (`string`) The name of the item.
- **cmd**: (`string|fun():any`) The command to execute when item is selected.
- **items**: (`MenuItem[]|fun():MenuItem[]`) Submenu items or a function that returns the submenu items (required for submenu)
- **rtxt**: (`string`) Text to show on the right of the item (optional)
- **hl**: (`string`) The hightlight of the item.

The library provides a default handler:

```lua
require("menu").handler(opts)
```

The handler automatically chooses which menu to open depending on the current filetype.

Opts has the following attributes:
- **mouse**: (`boolean`)
- When menu is open and **mouse** is set to true, then menu will be closed and a new menu will be repened at new cursor location.
- When menu is open and **mouse** is set to false, menu will just be closed.

### For keyboard users

- Use `h` `l` to move between windows
- Use `q` to close the window
- Press the keybind defined for menu item or scroll to it and press enter, to execute it

### Examples
### Default mappings

Keyboard users can run the mapping when inside the menu, mouse users can click.

- Keyboard users can run the mapping when inside the menu, mouse users can click.
```lua
-- Keyboard users
vim.keymap.set("n", "<C-t>", function()
require("menu").open("default")
end, {})

-- mouse users + nvimtree users!
vim.keymap.set({ "n", "v" }, "<RightMouse>", function()
require('menu.utils').delete_old_menus()

vim.cmd.exec '"normal! \\<RightMouse>"'
vim.keymap.set("n", "<C-t>", function() require("menu").handler {mouse = false} end)
-- Mouse users
vim.keymap.set({ "n", "v" }, "<RightMouse>", function() require("menu").handler {mouse = true} end)
```

-- clicked buf
local buf = vim.api.nvim_win_get_buf(vim.fn.getmousepos().winid)
local options = vim.bo[buf].ft == "NvimTree" and "nvimtree" or "default"
Same settings in lazy.nvim specification:

require("menu").open(options, { mouse = true })
end, {})
```lua
keys = {
{ mode = "n", "<C-t>", function() require("menu").handler({ mouse = false }) end },
{ mode = "n", "<RightMouse>", function() require("menu").handler({ mouse = true }) end },
{ mode = "v", "<RightMouse>", function() require("menu").handler({ mouse = true }) end },
},
```

Check example of [defaults menu](https://github.com/NvChad/menu/blob/main/lua/menus/default.lua) to see know syntax of options table.
Expand Down
144 changes: 123 additions & 21 deletions lua/menu/init.lua
Original file line number Diff line number Diff line change
@@ -1,40 +1,77 @@
local M = {}
local api = vim.api
local state = require "menu.state"
local layout = require "menu.layout"
local ns = api.nvim_create_namespace "NvMenu"
local ns = vim.api.nvim_create_namespace "NvMenu"
local volt = require "volt"
local volt_events = require "volt.events"
local mappings = require "menu.mappings"
local utils = require "menu.utils"

---@class MenuItem
---@field name string
---@field cmd? string|fun():any
---@field items? MenuItem[]|fun():MenuItem[]
---@field rtxt? string
---@field hl? string

---@class MenuOpenOpts
---@field mouse? boolean
---@field nested? boolean

---@param items string|MenuItem[]|fun():MenuItem[]
---@param opts MenuOpenOpts
M.open = function(items, opts)
opts = opts or {}

local cur_buf = api.nvim_get_current_buf()
local cur_buf = vim.api.nvim_get_current_buf()

if vim.bo[cur_buf].ft ~= "NvMenu" then
state.old_data = {
buf = api.nvim_get_current_buf(),
win = api.nvim_get_current_win(),
cursor = api.nvim_win_get_cursor(0),
buf = vim.api.nvim_get_current_buf(),
win = vim.api.nvim_get_current_win(),
cursor = vim.api.nvim_win_get_cursor(0),
}
end

items = type(items) == "table" and items or require("menus." .. items)
local items_was = items
if type(items) == "function" then
items = items()
end
if type(items) == "string" then
items = require("menus." .. items)
if type(items) == "function" then
items = items()
end
end
assert(
type(items) == "table",
"Items has to be a table."
.. " type(items_was)="
.. type(items_was)
.. " debug.getinfo(items_was)="
.. vim.inspect(type(items_was) == "function" and debug.getinfo(items_was))
.. " type(items)="
.. type(items)
.. " vim.inspect(items)="
.. vim.inspect(items)
.. " vim.inspect(opts)="
.. vim.inspect(opts)
.. ". Most probably provided menus configuration is invalid."
)

if not state.config then
state.config = opts
end

local config = state.config

local buf = api.nvim_create_buf(false, true)
state.bufs[buf] = { items = items, item_gap = opts.item_gap or 5 }
local buf = vim.api.nvim_create_buf(false, true)
state.bufs[buf] = { items = items, item_gap = M.config.item_gap or 5 }
table.insert(state.bufids, buf)

local h = #items
local h = #items or 1
local bufv = state.bufs[buf]
bufv.w = require("menu.utils").get_width(items)
bufv.w = utils.get_width(items)
bufv.w = bufv.w + bufv.item_gap

local win_opts = {
Expand All @@ -54,22 +91,22 @@ M.open = function(items, opts)
if config.mouse then
local pos = vim.fn.getmousepos()
win_opts.win = pos.winid
win_opts.col = api.nvim_win_get_width(pos.winid) + 2
win_opts.col = vim.api.nvim_win_get_width(pos.winid) + M.config.nested_col
win_opts.row = pos.winrow - 2
else
win_opts.win = api.nvim_get_current_win()
win_opts.col = api.nvim_win_get_width(win_opts.win) + 2
win_opts.row = api.nvim_win_get_cursor(win_opts.win)[1] - 1
win_opts.win = vim.api.nvim_get_current_win()
win_opts.col = vim.api.nvim_win_get_width(win_opts.win) + M.config.nested_col
win_opts.row = vim.api.nvim_win_get_cursor(win_opts.win)[1] - 1
end
end

local win = api.nvim_open_win(buf, not config.mouse, win_opts)
local win = vim.api.nvim_open_win(buf, not config.mouse, win_opts)

volt.gen_data {
{ buf = buf, ns = ns, layout = layout },
}

if config.border then
if M.config.border then
vim.wo[win].winhl = "Normal:Normal,FloatBorder:LineNr"
else
vim.wo[win].winhl = "Normal:ExBlack2Bg,FloatBorder:ExBlack2Border"
Expand All @@ -84,13 +121,13 @@ M.open = function(items, opts)
state.bufs = {}
state.config = nil

if api.nvim_win_is_valid(state.old_data.win) then
api.nvim_set_current_win(state.old_data.win)
if vim.api.nvim_win_is_valid(state.old_data.win) then
vim.api.nvim_set_current_win(state.old_data.win)
vim.schedule(function()
local cursor_line = math.max(1,state.old_data.cursor[1])
local cursor_line = math.max(1, state.old_data.cursor[1])
local cursor_col = math.max(0, state.old_data.cursor[2])

api.nvim_win_set_cursor(state.old_data.win, { cursor_line, cursor_col })
vim.api.nvim_win_set_cursor(state.old_data.win, { cursor_line, cursor_col })
end)
end

Expand All @@ -107,4 +144,69 @@ M.open = function(items, opts)
end
end

M.delete_old_menus = utils.delete_old_menus

---@class MenuConfig
---@field ft? {string: string|MenuItem|fun():MenuItem}
---@field default_menu? string|MenuItem
---@field default_mappings? boolean
---@field border? boolean
---@field item_gap? integer
---@field nested_col? integer

---@class MenuConfig
M.config = {
ft = {},
default_menu = "default",
default_mappings = false,
border = false,
item_gap = 5,
nested_col = 2,
}

---@param args MenuConfig
M.setup = function(args)
M.config = vim.tbl_deep_extend("force", M.config, args or {})
if M.config.default_mappings then
vim.keymap.set("n", "<C-t>", function()
M.handler { mouse = false }
end)
vim.keymap.set({ "n", "v" }, "<RightMouse>", function()
M.handler { mouse = true }
end)
end
end

---@param opts MenuOpenOpts
M.handler = function(opts)
opts = opts or {}
local window = 0
if opts.mouse then
-- On second mouse click remove current manu and reopen it.
require("menu.utils").delete_old_menus()
vim.cmd.exec '"normal! \\<RightMouse>"'
window = vim.fn.getmousepos().winid
else
if #require("menu.state").bufids > 0 then
-- if a menu is already open, close it.
require("menu.utils").delete_old_menus()
return
end
end
local ft = vim.bo[vim.api.nvim_win_get_buf(window)].ft
-- First try user filetype overwrites.
local items = M.config.ft[ft]
if not items then
-- Then try filetype specific menus.
local ok, mod = pcall(require, "menus.ft." .. ft)
if ok then
items = mod
else
-- Fallback to defaults.
items = M.config.default_menu or "default"
end
end
M.open(items, opts)
end

return M
39 changes: 37 additions & 2 deletions lua/menus/default.lua
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
return {
local function insertafter(arr, after, elem)
local idx = 1
for i, v in ipairs(arr) do
if v.name:find(after) then
idx = i
break
end
end
table.insert(arr, idx, elem)
end

---@type MenuItem[]
local items = {

{
name = "Format Buffer",
Expand All @@ -25,7 +37,9 @@ return {
{
name = " Lsp Actions",
hl = "Exblue",
items = "lsp",
items = function()
return require "menus.lsp"
end,
},

{ name = "separator" },
Expand Down Expand Up @@ -83,3 +97,24 @@ return {
end,
},
}

local ok = require "which-key"
if ok then
insertafter(items, "Lsp Actions", {
name = " Which-key from leader",
hl = "Exblue",
items = function()
local subitems = require "menus.which-key"(vim.g.mapleader)[1].items
return type(subitems) == "function" and subitems() or subitems
end,
})
insertafter(items, "Lsp Actions", {
name = " Which-key all keys",
hl = "Exblue",
items = function()
return require "menus.which-key"()
end,
})
end

return items
File renamed without changes.
Loading