Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP Integrations API Proposal (+ whichkey and mapper integrations) #31

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
testdata/
.DS_Store
connorgmeehan marked this conversation as resolved.
Show resolved Hide resolved
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
test:
nvim --headless --noplugin -u tests/minimal.lua -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal.lua'}"
test-head:
nvim --noplugin -u tests/minimal.lua
watch:
onchange "./lua/**/*.lua" -- make run
watch-test:
onchange "./**/*.(lua|scm)" -- make test

52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,58 @@ Has the same named fields as `keymapConfig`, with an additional field:

Sets a `string` prefix to be applied to all keymap inputs.

## Plugin Integrations

Nest.nvim can integrate with other plugins. See `lua/nest/integrations/example.lua` to build your own.

### nvim-mapper

Add `nest.enable(require('nest.integrations.mapper'))` before you run `applyKeymaps`.

#### Additional config

- `[3]|name string` A short name for the keymap or keymap group
- `[4]|description string` (optional) A longer description for the keymap or keymap group
- If not provided the `name` field will be used
- `uid string` (optional) A unique identifier for the nvim mapper config
- If not provided one will be generated from `name`
- `category string` (optional) A general category for the keymap group
- If not provided it will fallback to the `name` of the parent keymap group
- If no parent group the category will be `'unknown'`

#### Example
```lua
local nest = require('nest')
nest.enable(require('nest.integrations.mapper'))
nest.applyKeymaps {
{ '<leader>', {
{ 'f', '<cmd>Telescope find_files<cr>', 'Find Files'}, -- Minimal

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the example uses <leader>f for Telescope find_files, and in other spots (line 264 and original README) it uses <leader>ff
Suggestion: change it all to <leader>ff, just for consistency.

{ 'g', '<cmd>Telescope live_grep<cr>', 'Live Grep', 'Searches for a string within project files', uid = 'search_live_grep', category = 'search'}, -- Maximal
}},
}
```

### which-key.nvim

Add `nest.enable(require('nest.integrations.whichkey'))` before you run `applyKeymaps`.

#### Additional config

- `[3]|name string` A short name for the keymap or keymap group

#### Example
```lua
local nest = require('nest')
nest.enable(require('nest.integrations.whichkey'))
nest.applyKeymaps {
{ '<leader>', {
{ 'f', name = '+File', { -- Can be used as field
{ 'f', '<cmd>Telescope find_files<cr>', 'Find Files'}, -- Or as the 3rd element in table
}}
}},
}
```

## Planned Features

See issues and milestones
238 changes: 175 additions & 63 deletions lua/nest.lua
Original file line number Diff line number Diff line change
@@ -1,7 +1,53 @@
local module = {}

--[[
-- TYPES
--]]

--- Extra options that will be passed to nvim when binding keymaps
--- @class NestSettingsOptions
--- @field noremap boolean
--- @field silent boolean
--- @field expr boolean

--- Stores the current keymap state/settings including lhs/prefix
--- @class NestSettings
--- @field buffer boolean|number
--- @field prefix string
--- @field options NestSettingsOptions
--- @field mode string

--- Internal type for a node in a nest.nvim config, this is how the end-user will define their config
--- @class NestNode : NestSettings
--- @field [1] string|table<number, NestNode>
--- @field [2] string|function|table<number,NestNode>
--- @field [3] string|nil Name
--- @field name string|nil Name
--- @field [4] string|nil Description
--- @field description string|nil Description

--- Type definition for nest.nvim integration
--- @class NestIntegration
--- @field name string
--- @field on_init function|nil
--- @field handler function
--- @field on_complete function|nil

--- Paramater passed to handler of NestIntegration
--- @class NestIntegrationNode
--- @field lhs string
--- @field rhs table<number, NestNode>|string
--- @field name string
--- @field description string


--[[
-- UTILS
--]]

--- Defaults being applied to `applyKeymaps`
-- Can be modified to change defaults applied.
--- @type NestSettings
module.defaults = {
mode = 'n',
prefix = '',
Expand All @@ -24,6 +70,10 @@ module._getRhsExpr = function(index)
return vim.api.nvim_replace_termcodes(keys, true, true, true)
end

--- Converts a lua function to a string that can be called to execute the function
--- @param func function
--- @param expr boolean
--- @return string
local function functionToRhs(func, expr)
table.insert(rhsFns, func)

Expand All @@ -35,26 +85,17 @@ local function functionToRhs(func, expr)
end

local function copy(table)
local ret = {}

for key, value in pairs(table) do
ret[key] = value
end

return ret
return vim.deepcopy(table)
end

local function mergeTables(left, right)
local ret = copy(left)

for key, value in pairs(right) do
ret[key] = value
end

return ret
return vim.tbl_extend('force', left, right)
end

local function mergeOptions(left, right)
--- @param left NestSettings
--- @param right NestSettings
--- @return NestSettings
local function mergeSettings(left, right)
local ret = copy(left)

if right == nil then
Expand All @@ -80,63 +121,134 @@ local function mergeOptions(left, right)
return ret
end

--- Applies the given `keymapConfig`, creating nvim keymaps
module.applyKeymaps = function (config, presets)
local mergedPresets = mergeOptions(
presets or module.defaults,
config
)

local first = config[1]
--[[
-- INTEGRATIONS
--]]
-- Stores all the different handlers for the nest API
module.integrations = {}

-- Allows adding extra keymap integrations
--- @param integration NestIntegration
module.enable = function(integration)
if integration.name ~= nil then
module.integrations[integration.name] = integration
end
end

if type(first) == 'table' then
for _, it in ipairs(config) do
module.applyKeymaps(it, mergedPresets)
end

return
--- Default nest integration that binds keymaps
--- @type NestIntegration
local default_integration = {}
Copy link

@Pranav-Badrinathan Pranav-Badrinathan Jan 6, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

default_integration could be pulled out into it's own file, for consistency, because the other integrations are in their own files anyways.
Only reason to not would be if some user accidently nuked their integrations folder, having this in nest.lua would enable them to still run the barebones version of the plugin.
If someone deletes integrations, they can re-clone the rep. So my vote is on extract for consistency.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with splitting it into its own file but one of the selling points in the README.md was that it was a single lua file. Interested what @LionC thinks. On the other hand given that the vim.keymap api is merged it could be worth having two default integrations, one for the new API (no need to manage a table of lua functions) and one for the old API. Not sure if there's any benefit to doing this however.

Also for your last point, I don't think users will ever be deleting the contents of the integrations folder, it's just supposed internal to nest.nvim. If a user want's to add their own custom integration they can just create an object with the same fields as the example integration and pass it using nest.enable().

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...one of the selling points in the README.md was that it was a single lua file.

I completely missed that! This PR would also crush the 100 lines claim, though. Maybe 2 separate branches could serve different versions, one that's minimal and the other integrated. Though that certainly raises up the ante for maintaining this project quite a bit.

On the other hand given that the vim.keymap api is merged it could be worth having two default integrations, one for the new API (no need to manage a table of lua functions) and one for the old API. Not sure if there's any benefit to doing this however.

Oh wow! I had no idea they were doing this. It certainly would help pushing all the lifting onto the Nvim API itself.(also reduce lines of code, if that philosophy is still maintained).
And no, I don't think there would be a benefit maintaining for an older API. I mean, we could merge this PR as is and then modify it for new API, redirecting all who want the old API to the merge commit 🤷. Or if it is that troublesome, just have the old stuff be fallback. Adds bloat, but backwards compatibility.

OR (Ikr, too many ideas!) just split breaking stuff into different file with wrappers around the breaking stuff, and ask users to include whichever one fits their Nvim version.

Also for your last point, I don't think users will ever be deleting the contents of the integrations folder,...

Yeah, I know, I was just thinking of worst case scenarios. Just a couple of days ago packer kinda broke on me and would not delete the plugin folder when removing it, only it's contents. That led to it asking me repeatedly if I wanted to remove it, and not doing so, rinse and repeat. I got fed up, went to the plugins folder and did stuff that broke packer! So yeah.

default_integration.name = 'nest'
default_integration.handler = function (node, node_settings)
-- Skip tables (keymap groups)
if type(node.rhs) == 'table' then
return
end

for mode in string.gmatch(node_settings.mode, '.') do
local sanitizedMode = mode == '_'
and ''
or mode

if node_settings.buffer then
local buffer = (node_settings.buffer == true)
and 0
or node_settings.buffer

vim.api.nvim_buf_set_keymap(
buffer,
sanitizedMode,
node.lhs,
node.rhs,
node_settings.options
)
else
vim.api.nvim_set_keymap(
sanitizedMode,
node.lhs,
node.rhs,
node_settings.options
)
end
end
end
-- Bind default_integration keymap handler
module.enable(default_integration)

--[[
-- TRAVERSING CONFIG
--]]

--- @param node NestNode
--- @param settings NestSettings
module.traverse = function(node, settings, integrations)
local mergedSettings = mergeSettings(settings or module.defaults, node)

local first = node[1]

-- Top level of config, just traverse into each keymap/keymap group
if type(first) == 'table' then
for _, sub_node in ipairs(node) do
module.traverse(sub_node, mergedSettings, integrations)
end
return
end

-- First must be a string, append first to the prefix
mergedSettings.prefix = mergedSettings.prefix .. first
local second = node[2]

--- @type string|table<number, NestNode>
local rhs = type(second) == 'function'
and functionToRhs(second, mergedSettings.options.expr)
or second

-- Populate node.name and node.description if necessary
if node.name == nil and #node >= 3 then
node.name = node[3]
end
if node.description == nil and #node>=4 then
node.description = node[4]
end
node.lhs = mergedSettings.prefix
node.rhs = rhs

-- Pass current keymap node to all integrations
for _, integration in pairs(integrations) do
integration.handler(node, mergedSettings)
end

if type(rhs) == 'table' then
module.traverse(rhs, mergedSettings, integrations)
end
end

local second = config[2]

mergedPresets.prefix = mergedPresets.prefix .. first

if type(second) == 'table' then
module.applyKeymaps(second, mergedPresets)
--[[
-- ENTRY POINT
--]]

return
--- Applies the given `keymapConfig`, creating nvim keymaps
--- @param nest_config table<number, NestNode>
--- @param settings NestSettings
--- @param integrations table<number, NestIntegration> User can parse the nest config with a subset of integrations
module.applyKeymaps = function(nest_config, settings, integrations)
local ints = integrations or module.integrations
-- Run on init for each integration
for _, integration in pairs(ints) do
if integration.on_init ~= nil then
integration.on_init(nest_config, settings)
end
end

module.traverse(nest_config, settings, ints)

local rhs = type(second) == 'function'
and functionToRhs(second, mergedPresets.options.expr)
or second

for mode in string.gmatch(mergedPresets.mode, '.') do
local sanitizedMode = mode == '_'
and ''
or mode

if mergedPresets.buffer then
local buffer = (mergedPresets.buffer == true)
and 0
or mergedPresets.buffer

vim.api.nvim_buf_set_keymap(
buffer,
sanitizedMode,
mergedPresets.prefix,
rhs,
mergedPresets.options
)
else
vim.api.nvim_set_keymap(
sanitizedMode,
mergedPresets.prefix,
rhs,
mergedPresets.options
)
end
for _, integration in pairs(ints) do
if integration.on_complete ~= nil then
integration.on_complete()
end
end
end

return module
Loading