Iron-E / nvim-libmodal

Create new "modes" for Neovim!
Other
118 stars 7 forks source link

What's the best way to expose the mode keymaps table to my consumers? #35

Closed mawkler closed 8 months ago

mawkler commented 8 months ago

Hi again! I'm trying to expose the keymaps table to the consumers of my plugin. Here's a simplified example:

local options = {
local libmodal = require('libmodal')

local M = {}

local options = {
  mode_keymaps = {
    n = function() vim.notify('foo') end,
    u = vim.cmd.undo, -- THIS DOESN'T CURRENTLY WORK
  }
}

vim.keymap.set('n', 'M', function()
  require('libmodal').mode.enter('Mode 1', mode_keymaps)
end)

function M.setup(opts)
  options = vim.tbl_deep_extend('force', options, opts or {})
end

The problem is the line u = vim.cmd.undo. Since libmodal passes the current mode as an agrument, that argument gets passed to vim.cmd.undo, causing the following error:

...ocal/share/nvim/lazy/nvim-libmodal/lua/libmodal/Mode.lua:125: invalid key: input

I could solve this by requiring my users to wrap each callback in a function like this:

u = function() vim.cmd.undo() end

But it feels like an ugly solution. I would prefer if I could abstract away libmodal. So I tried creating a function that iterates over all the mappings and wraps them with a function:

local function clean_mappings(mappings)
  local new_mappings = {}
  for keymap, fn in pairs(mappings) do
    new_mappings[keymap] = function() fn() end
  end

  return new_mappings
end

This solves the agove problem. However, I also want to use a mapping to switch mode with libmodal.mode.switch. That means that the Mode type argument passed to switches returned function gets lost.

Do you have any suggestions as to how I could solve this? Or is there any way for libmodal to determine whether it should or shouldn't pass an argument to the callback when calling it?

Iron-E commented 8 months ago

You might be able to use something like this:


--- Checks if `fn` is a callback, or a "mode callback" (meaning it uses `self`)
--- @param fn function
--- @return boolean
local function is_mode_callback(fn)
    local info = debug.getinfo(fn, 'Su')
    return
        info.nparams == 1 -- takes one parameter
        and info.what == 'Lua' -- is a lua function
        and not info.source:find '^@vim' -- is not a builtin
end

Unfortunately, this kind of thing seems prevalent when it comes to callbacks. For example:

-- Template from `:h nvim_create_autocmd`
vim.api.nvim_create_autocmd({"BufEnter", "BufWinEnter"}, {
  pattern = {"*.c", "*.h"},
  callback = vim.cmd.undo, -- ← this also doesn't work
})

I could provide a helper, e.g.

function libmodal.mode.fn(f, ...)
    local args = { ... }
    return function()
        f(unpack(args))
    end
end

And then the example becomes:

local fn = libmodal.mode.fn
local options = {
  mode_keymaps = {
    n = function() vim.notify('foo') end,
    u = fn(vim.cmd.undo), 
    e = fn(vim.cmd.edit, 'foo'), -- you can also pass args
  }
}
mawkler commented 8 months ago

Ok thank you for your response. I guess that I could expose my own fn and tell users to use that 🙂

Iron-E commented 8 months ago

I've merged #36 which adds libmodal.mode.map, a namespace including fn (the utility we discussed) and also switch (the utility implemented previously, just soft-moved to create emphasis that it can only be used as a mapping).

Iron-E commented 8 months ago

I'll close this for now, but if anything else comes up let me know :)