├── .luarocks ├── default-lua-version.lua └── config-5.1.lua ├── .gitignore ├── stylua.toml ├── spec ├── init.lua ├── helper.lua └── better-n │ ├── lib │ └── keymap_spec.lua │ └── feature_spec.lua ├── .busted ├── nvim-better-n-scm-1.rockspec ├── LICENSE ├── lua ├── better-n │ ├── lib │ │ ├── keymap.lua │ │ └── enumerable.lua │ ├── config.lua │ ├── register.lua │ └── repeatable.lua └── better-n.lua └── README.md /.luarocks/default-lua-version.lua: -------------------------------------------------------------------------------- 1 | return "5.1" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .luarc.json 3 | lua_modules/ 4 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | line_endings = "Unix" 2 | indent_type = "Spaces" 3 | indent_width = 2 4 | no_call_parentheses = false 5 | -------------------------------------------------------------------------------- /spec/init.lua: -------------------------------------------------------------------------------- 1 | local cwd = vim.fn.getcwd() 2 | 3 | vim.opt.swapfile = false 4 | 5 | vim.api.nvim_command([[set rtp+=.]]) 6 | vim.api.nvim_command(string.format([[set rtp+=%s/lua_modules/lib/lua/5.1/,%s/spec/]], cwd, cwd)) 7 | vim.api.nvim_command([[packloadall]]) 8 | -------------------------------------------------------------------------------- /.busted: -------------------------------------------------------------------------------- 1 | return { 2 | _all = { 3 | coverage = false, 4 | lpath = "lua/?.lua;lua/?/init.lua;lua_modules/share/lua/5.1/?.lua;lua_modules/share/lua/5.1/?/init.lua;lua_modules/lib/lua/5.1/?/?.so;spec/?.lua;spec/init.lua;spec/?/init.lua spec/?.lua;spec/?/?.lua;", 5 | }, 6 | default = { 7 | verbose = true, 8 | }, 9 | tests = { 10 | verbose = true, 11 | }, 12 | } 13 | -------------------------------------------------------------------------------- /nvim-better-n-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | rockspec_format = "3.0" 2 | package = "nvim-better-n" 3 | version = "scm-1" 4 | 5 | dependencies = { 6 | "lua == 5.1", 7 | } 8 | test_dependencies = { 9 | "lua == 5.1", 10 | "busted", 11 | "nlua", 12 | } 13 | 14 | source = { 15 | url = "git://github.com/jonatan-branting/" .. package, 16 | } 17 | 18 | build = { 19 | type = "builtin", 20 | } 21 | 22 | -------------------------------------------------------------------------------- /.luarocks/config-5.1.lua: -------------------------------------------------------------------------------- 1 | lua_version = "5.1" 2 | variables = { 3 | LUA = "./lua_modules/bin/nlua", 4 | LUA_INCDIR = "/opt/homebrew/include/luajit-2.1", 5 | LUA_LUADIR = "./lua_modules/", 6 | LUA_BINDIR = "/opt/homebrew/include/luajit-2.1", 7 | } 8 | rocks_servers = { 9 | "https://nvim-neorocks.github.io/rocks-binaries/", 10 | "https://luarocks.org/", 11 | "https://luarocks.org/dev", 12 | } 13 | -------------------------------------------------------------------------------- /spec/helper.lua: -------------------------------------------------------------------------------- 1 | local M = {} 2 | 3 | function M.feed(text, feed_opts) 4 | feed_opts = feed_opts or "mtx" 5 | local to_feed = vim.api.nvim_replace_termcodes(text, true, false, true) 6 | 7 | vim.api.nvim_feedkeys(to_feed, feed_opts, false) 8 | end 9 | 10 | function M.setup_buffer(input, filetype) 11 | local buf = vim.api.nvim_create_buf(false, true) 12 | vim.api.nvim_buf_set_option(buf, "filetype", filetype) 13 | vim.api.nvim_command("buffer " .. buf) 14 | 15 | vim.api.nvim_buf_set_lines(0, 0, -1, true, input) 16 | 17 | return buf 18 | end 19 | 20 | function M.get_buf_lines() 21 | return vim.api.nvim_buf_get_lines(0, 0, vim.api.nvim_buf_line_count(0), false) 22 | end 23 | 24 | function M.script_path() 25 | local str = debug.getinfo(2, "S").source:sub(2) 26 | return str:match(("(.*%s)"):format("/")) 27 | end 28 | 29 | function M.t(str) 30 | return vim.api.nvim_replace_termcodes(str, true, true, true) 31 | end 32 | 33 | return M 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2022 Jonatan Branting 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the "Software"), 7 | to deal in the Software without restriction, including without limitation 8 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 9 | and/or sell copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 18 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 19 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 20 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 21 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /spec/better-n/lib/keymap_spec.lua: -------------------------------------------------------------------------------- 1 | local Keymap = require("better-n.lib.keymap") 2 | 3 | describe("__index", function() 4 | it("should return the correct mapping for a given key", function() 5 | vim.keymap.set("n", "", "test-keymap") 6 | local keymap = Keymap:new({ bufnr = nil, mode = "n" }) 7 | local mapping = keymap[""] 8 | 9 | assert.is_not_nil(mapping) 10 | assert.are.equal("", mapping.lhs) 11 | assert.are.equal("test-keymap", mapping.rhs) 12 | end) 13 | 14 | it("should return the correct mapping for a given key bound for a specific buffer", function() 15 | vim.keymap.set("n", "]]", "another-test-keymap", { buffer = 0 }) 16 | local keymap = Keymap:new({ bufnr = 0, mode = "n" }) 17 | local mapping = keymap["]]"] 18 | 19 | assert.is_not_nil(mapping) 20 | assert.are.equal("]]", mapping.lhs) 21 | assert.are.equal("another-test-keymap", mapping.rhs) 22 | end) 23 | 24 | it("should return nil for a non-existent key", function() 25 | local keymap = Keymap:new({ bufnr = 0, mode = "n" }) 26 | local mapping = keymap[""] 27 | 28 | assert.is_nil(mapping) 29 | end) 30 | end) 31 | -------------------------------------------------------------------------------- /lua/better-n/lib/keymap.lua: -------------------------------------------------------------------------------- 1 | local Enumerable = require("better-n.lib.enumerable") 2 | 3 | local Keymap = {} 4 | 5 | function Keymap:new(opts) 6 | local instance = { 7 | bufnr = opts.bufnr or 0, 8 | mode = opts.mode or "n", 9 | } 10 | 11 | local buffer_mappings = vim.api.nvim_buf_get_keymap(instance.bufnr, instance.mode) 12 | local global_mappings = vim.api.nvim_get_keymap(instance.mode) 13 | 14 | local mappings = Enumerable:new(buffer_mappings) 15 | for _, mapping in ipairs(global_mappings) do 16 | mappings:append(mapping) 17 | end 18 | 19 | instance.mappings = mappings 20 | 21 | setmetatable(instance, Keymap) 22 | 23 | return instance 24 | end 25 | 26 | Keymap.__index = function(self, key) 27 | -- Support accessing instance variables directly 28 | local class_value = rawget(self, key) 29 | if class_value ~= nil then 30 | return class_value 31 | end 32 | 33 | local keycode = vim.keycode(key) 34 | local mappings = rawget(self, "mappings") 35 | 36 | return mappings:find(function(mapping) 37 | -- Compare using keycodes to avoid casing issues 38 | return vim.keycode(mapping.lhs) == keycode 39 | end) 40 | end 41 | 42 | return Keymap 43 | 44 | -------------------------------------------------------------------------------- /lua/better-n.lua: -------------------------------------------------------------------------------- 1 | local Register = require("better-n.register") 2 | local Config = require("better-n.config") 3 | 4 | local M = {} 5 | 6 | function M.instance() 7 | if _G.better_n_register ~= nil then 8 | return _G.better_n_register 9 | end 10 | 11 | _G.better_n_register = Register:new() 12 | 13 | return _G.better_n_register 14 | end 15 | 16 | function M.setup(opts) 17 | if opts.mappings then 18 | vim.deprecate( 19 | "opts.mappings is deprecated", 20 | 'create mappings manually using `require("better-n").create({ next = ..., previous = ... })`', 21 | "HEAD", 22 | "nvim-better-n", 23 | false 24 | ) 25 | end 26 | if opts.callbacks then 27 | vim.deprecate( 28 | "opts.callbacks", 29 | "Use `vim.api.nvim_create_autocmd` to listen to the User event with pattern `BetterNMappingExecuted` instead", 30 | "HEAD", 31 | "nvim-better-n", 32 | false 33 | ) 34 | end 35 | local defaults = Config.get_default_config() 36 | local config = vim.tbl_deep_extend("force", defaults, opts) 37 | 38 | Config.apply_config(config) 39 | 40 | return M 41 | end 42 | 43 | function M.next() 44 | return M.instance():next() 45 | end 46 | 47 | function M.previous() 48 | return M.instance():previous() 49 | end 50 | 51 | function M.n() 52 | return M.next() 53 | end 54 | 55 | function M.shift_n() 56 | return M.previous() 57 | end 58 | 59 | function M.create(...) 60 | return M.instance():create(...) 61 | end 62 | 63 | return M 64 | -------------------------------------------------------------------------------- /spec/better-n/feature_spec.lua: -------------------------------------------------------------------------------- 1 | local t = require("../helper") 2 | local better_n = require("better-n") 3 | 4 | better_n.setup({}) 5 | vim.keymap.set({ "n", "x" }, "n", better_n.next, { nowait = true, expr = true }) 6 | vim.keymap.set({ "n", "x" }, "", better_n.previous, { nowait = true, expr = true }) 7 | 8 | describe("feature", function() 9 | before_each(function() 10 | t.setup_buffer({ 11 | " atest atext arow", 12 | " atest atext arow", 13 | }, "lua") 14 | end) 15 | 16 | it("can repeat registered movements", function() 17 | t.feed("fa") 18 | t.feed("n") 19 | t.feed("ciwTEXT") 20 | 21 | local result = t.get_buf_lines() 22 | 23 | assert.are.same({ 24 | " atest TEXT arow", 25 | " atest atext arow", 26 | }, result) 27 | end) 28 | 29 | it("does not interfere with f-movements", function() 30 | t.feed("fa") 31 | t.feed("n") 32 | t.feed("ciwTEXT") 33 | 34 | local result = t.get_buf_lines() 35 | 36 | assert.are.same({ 37 | " atest TEXT arow", 38 | " atest atext arow", 39 | }, result) 40 | end) 41 | 42 | it("does not interfere with the replaying of macros", function() 43 | t.feed("qq") 44 | 45 | t.feed("fa") 46 | t.feed("n") 47 | t.feed("ciwTEXT") 48 | 49 | t.feed("") 50 | t.feed("q") 51 | 52 | t.feed("j0") 53 | t.feed("@q") 54 | 55 | local result = t.get_buf_lines() 56 | 57 | assert.are.same({ 58 | " atest TEXT arow", 59 | " atest TEXT arow", 60 | }, result) 61 | end) 62 | 63 | it("does not interfere with the 'normal' command", function() 64 | t.feed(":1,2normal fanciwTEXT") 65 | 66 | local result = t.get_buf_lines() 67 | 68 | assert.are.same({ 69 | " atest TEXT arow", 70 | " atest TEXT arow", 71 | }, result) 72 | end) 73 | end) 74 | -------------------------------------------------------------------------------- /lua/better-n/config.lua: -------------------------------------------------------------------------------- 1 | local Config = {} 2 | local P = {} 3 | 4 | function P._setup_default_mappings() 5 | local better_n = require("better-n") 6 | 7 | local f = better_n.create({ initiate = "f", next = ";", previous = "," }) 8 | vim.keymap.set({ "n", "x" }, "f", f.passthrough, { expr = true, silent = true }) 9 | 10 | local F = better_n.create({ initiate = "F", next = ";", previous = "," }) 11 | vim.keymap.set({ "n", "x" }, "F", F.passthrough, { expr = true, silent = true }) 12 | 13 | local t = better_n.create({ initiate = "t", next = ";", previous = "," }) 14 | vim.keymap.set({ "n", "x" }, "t", t.passthrough, { expr = true, silent = true }) 15 | 16 | local T = better_n.create({ initiate = "T", next = ";", previous = "," }) 17 | vim.keymap.set({ "n", "x" }, "T", T.passthrough, { expr = true, silent = true }) 18 | 19 | local asterisk = better_n.create({ initiate = "*", next = "n", previous = "" }) 20 | vim.keymap.set({ "n", "x" }, "*", asterisk.passthrough, { expr = true, silent = true }) 21 | 22 | local hash = better_n.create({ initiate = "#", next = "n", previous = "" }) 23 | vim.keymap.set({ "n", "x" }, "#", hash.passthrough, { expr = true, silent = true }) 24 | 25 | vim.keymap.set({ "n", "x" }, "n", better_n.next, { expr = true, silent = true, nowait = true }) 26 | vim.keymap.set({ "n", "x" }, "", better_n.previous, { expr = true, silent = true, nowait = true }) 27 | end 28 | 29 | function P._setup_cmdline_mappings() 30 | local better_n = require("better-n") 31 | 32 | better_n.create({ id = "/", next = "n", previous = "" }) 33 | better_n.create({ id = "?", next = "n", previous = "" }) 34 | end 35 | 36 | function Config.get_default_config() 37 | return { 38 | disable_default_mappings = false, 39 | disable_cmdline_mappings = false, 40 | -- @deprecated 41 | callbacks = { 42 | mapping_executed = nil, 43 | }, 44 | -- @deprecated 45 | mappings = {}, 46 | } 47 | end 48 | 49 | function Config.apply_config(config) 50 | if not config.disable_cmdline_mappings then 51 | P._setup_cmdline_mappings() 52 | end 53 | 54 | if not config.disable_default_mappings then 55 | P._setup_default_mappings() 56 | end 57 | end 58 | 59 | return Config 60 | -------------------------------------------------------------------------------- /lua/better-n/register.lua: -------------------------------------------------------------------------------- 1 | local Repeatable = require("better-n.repeatable") 2 | 3 | local augroup = vim.api.nvim_create_augroup("BetterN", {}) 4 | 5 | local Register = {} 6 | 7 | function Register:new() 8 | local instance = { 9 | last_repeatable_id = nil, 10 | repeatables = {}, 11 | } 12 | 13 | setmetatable(instance, self) 14 | self.__index = self 15 | 16 | vim.api.nvim_create_autocmd("CmdlineLeave", { 17 | group = augroup, 18 | callback = function() 19 | local abort = vim.v.event.abort 20 | local cmdline_char = vim.fn.expand("") 21 | 22 | if not abort and instance.repeatables[cmdline_char] ~= nil then 23 | instance.last_repeatable_id = cmdline_char 24 | end 25 | end, 26 | }) 27 | 28 | vim.api.nvim_create_autocmd("User", { 29 | group = augroup, 30 | pattern = { "BetterNMappingExecuted" }, 31 | callback = function(args) 32 | local repeatable_id = args.data.repeatable_id 33 | 34 | instance.last_repeatable_id = repeatable_id 35 | end, 36 | }) 37 | 38 | return instance 39 | end 40 | 41 | function Register:create(opts) 42 | local repeatable = Repeatable:new({ 43 | register = self, 44 | bufnr = opts.bufnr or 0, 45 | next = opts.next, 46 | previous = opts.previous or opts.prev, 47 | passthrough = opts.initiate or opts.key or opts.next, 48 | mode = opts.mode or "n", 49 | id = opts.id, 50 | }) 51 | 52 | vim.keymap.set(repeatable.mode, repeatable.passthrough_key, repeatable.passthrough, { expr = true, silent = true }) 53 | vim.keymap.set(repeatable.mode, repeatable.next_key, repeatable.next, { expr = true, silent = true }) 54 | vim.keymap.set(repeatable.mode, repeatable.previous_key, repeatable.previous, { expr = true, silent = true }) 55 | 56 | self.repeatables[repeatable.id] = repeatable 57 | 58 | return repeatable 59 | end 60 | 61 | function Register:next() 62 | if self.last_repeatable_id == nil then 63 | return 64 | end 65 | 66 | return self.repeatables[self.last_repeatable_id]:next() 67 | end 68 | 69 | function Register:previous() 70 | if self.last_repeatable_id == nil then 71 | return 72 | end 73 | 74 | return self.repeatables[self.last_repeatable_id]:previous() 75 | end 76 | 77 | -- Workaround for # only working for array-based tables 78 | function Register:_num_repeatables() 79 | local count = 0 80 | for _ in pairs(self.repeatables) do 81 | count = count + 1 82 | end 83 | 84 | return count 85 | end 86 | 87 | return Register 88 | -------------------------------------------------------------------------------- /lua/better-n/lib/enumerable.lua: -------------------------------------------------------------------------------- 1 | local Enumerable = {} 2 | 3 | function Enumerable:new(items) 4 | local instance = { 5 | items = items, 6 | } 7 | 8 | setmetatable(instance, self) 9 | self.__index = self 10 | 11 | return instance 12 | end 13 | 14 | function Enumerable:append(item) 15 | table.insert(self.items, item) 16 | end 17 | 18 | function Enumerable:length() 19 | return #self.items 20 | end 21 | 22 | function Enumerable:contains(item) 23 | return vim.tbl_contains(self.items, item) 24 | end 25 | 26 | function Enumerable:any(func) 27 | if not func then 28 | return self:length() > 0 29 | end 30 | 31 | return self:find(func) ~= nil 32 | end 33 | 34 | function Enumerable:to_table() 35 | return self.items 36 | end 37 | 38 | function Enumerable:each(func_or_func_name) 39 | local func = nil 40 | if type(func_or_func_name) == "string" then 41 | func = function(item, ...) return item[func_or_func_name](item, ...) end 42 | else 43 | func = func_or_func_name 44 | end 45 | 46 | for i, item in ipairs(self.items) do 47 | func(item, i) 48 | end 49 | end 50 | 51 | function Enumerable:map(func_or_func_name) 52 | local mapped = {} 53 | 54 | local func = nil 55 | if type(func_or_func_name) == "string" then 56 | func = function(item, ...) return item[func_or_func_name](item, ...) end 57 | else 58 | func = func_or_func_name 59 | end 60 | 61 | for i, item in ipairs(self.items) do 62 | local result, _ = func(item, i) 63 | 64 | table.insert(mapped, result) 65 | end 66 | 67 | return Enumerable:new(mapped) 68 | end 69 | 70 | function Enumerable:filter(func) 71 | local filtered = {} 72 | 73 | for _, item in ipairs(self.items) do 74 | if func(item) then 75 | table.insert(filtered, item) 76 | end 77 | end 78 | 79 | return Enumerable:new(filtered) 80 | end 81 | 82 | function Enumerable:select(func) 83 | return self:filter(func) 84 | end 85 | 86 | function Enumerable:reject(func) 87 | local filtered = {} 88 | 89 | for _, item in ipairs(self.items) do 90 | if not func(item) then 91 | table.insert(filtered, item) 92 | end 93 | end 94 | 95 | return Enumerable:new(filtered) 96 | end 97 | 98 | function Enumerable:reduce(func, initial) 99 | local result = initial 100 | 101 | for _, item in ipairs(self.items) do 102 | result = func(result, item) 103 | end 104 | 105 | return result 106 | end 107 | 108 | function Enumerable:find(func) 109 | for _, item in ipairs(self.items) do 110 | if func(item) then 111 | return item 112 | end 113 | end 114 | end 115 | 116 | function Enumerable:last() 117 | return self.items[#self.items] 118 | end 119 | 120 | function Enumerable:first() 121 | return self.items[1] 122 | end 123 | 124 | function Enumerable:table() 125 | return self.items 126 | end 127 | 128 | return Enumerable 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nvim-better-n 2 | Repeat movement commands using `n` in the same vein that `.` repeats action commands. 3 | 4 |
5 | 6 |
7 | 8 | ## About 9 | `nvim-better-n` attempts address a problem with Vim, which is that almost every 10 | single binding is used by default, for (often) very niche actions. I want to be 11 | able to reuse convenient bindings for similar things, reducing both mental 12 | overhead as well as opening up more bindings, allowing Vim to be more 13 | ergonomic. 14 | 15 | It does this by rebinding `n` (which is a rather convenient binding), so that 16 | it used for multiple different movement commands, in the same vein `.` repeats 17 | action commands. 18 | 19 | For example, if we jump to the next hunk, using `]h`, we can repeat 20 | that command using `n`, allowing for far easier "scrolling" using that motion 21 | without coming up with a bind that is easier to repeat. 22 | 23 | Using this binding for that motion would, without this plugin, be rather 24 | cumbersome in the cases were you wanted to press it multiple times. 25 | 26 | It should also be noted that this frees up both `;`, and `,` for other actions, 27 | as `n` will instead handle their current task. 28 | 29 | ## Install 30 | Install as usual, using your favourite plugin manager. 31 | 32 | ```lua 33 | use "jonatan-branting/nvim-better-n" 34 | ``` 35 | 36 | ## Setup 37 | 38 | ```lua 39 | require("better-n").setup( 40 | { 41 | -- These are default values, which can be omitted. 42 | -- By default, the following mappings are made repeatable using `n` and ``: 43 | -- `f`, `F`, `t`, `T`, `*`, `#`, `/`, `?` 44 | disable_default_mappings = false, 45 | disable_cmdline_mappings = false, 46 | } 47 | ) 48 | 49 | vim.nvim_create_autocmd("User", { 50 | pattern = "BetterNMappingExecuted", 51 | callback = function(args) 52 | -- args.data.repeatable_id and args.data.mode are available here 53 | end 54 | }) 55 | 56 | -- You create repeatable mappings like this: 57 | local hunk_navigation = require("better-n").create( 58 | { 59 | next = require("gitsigns").next_hunk, 60 | prev = require("gitsigns").prev_hunk 61 | } 62 | ) 63 | 64 | vim.keymap.set({ "n", "x" }, "]h", hunk_navigation.next_key) 65 | vim.keymap.set({ "n", "x" }, "[h", hunk_navigation.previous_key) 66 | 67 | -- or 68 | 69 | vim.keymap.set({ "n", "x" }, "]h", hunk_navigation.next, { expr = true }) 70 | vim.keymap.set({ "n", "x" }, "[h", hunk_navigation.prev, { expr = true }) 71 | 72 | -- 73 | ``` 74 | 75 | ## Repeatable buffer-local mappings 76 | To make buffer-local mappings repeatable, you can wrap the mappings in a `FileType` autocommand. 77 | 78 | ```lua 79 | vim.api.nvim_create_autocmd( 80 | "FileType", 81 | { 82 | callback = function(args) 83 | local repeatable_square_brackets = require("better_n").create({ next = "]]", prev = "[[" }) 84 | 85 | vim.keymap.set("n", "]]", repeatable_square_brackets.next_key, { buffer = args.buf }) 86 | vim.keymap.set("n", "[[", repeatable_square_brackets.prev_key, { buffer = args.buf })) 87 | } 88 | ) 89 | ``` 90 | -------------------------------------------------------------------------------- /lua/better-n/repeatable.lua: -------------------------------------------------------------------------------- 1 | local Keymap = require("better-n.lib.keymap") 2 | 3 | local Repeatable = {} 4 | 5 | function Repeatable:new(opts) 6 | local instance = { 7 | register = opts.register or error("opts.register is required" .. vim.inspect(opts)), 8 | passthrough_action = opts.passthrough or error("opts.passthrough is required" .. vim.inspect(opts)), 9 | id = opts.id or opts.register:_num_repeatables(), 10 | mode = opts.mode, 11 | bufnr = opts.bufnr 12 | } 13 | 14 | setmetatable(instance, self) 15 | self.__index = self 16 | 17 | instance.passthrough_key = "(better-n-#" .. instance.id .. ")" 18 | instance.next_key = "(better-n-#" .. instance.id .. "-next)" 19 | instance.previous_key = "(better-n-#" .. instance.id .. "-previous)" 20 | 21 | local keymap = Keymap:new({bufnr = opts.bufnr, mode = instance.mode}) 22 | local next_action = opts.next or error("opts.next is required" .. vim.inspect(opts)) 23 | local previous_action = opts.previous or error("opts.previous or opts.prev is required" .. vim.inspect(opts)) 24 | 25 | -- Extract the actual action from the keymap if it's a string. 26 | -- This is more robust, and solves some remap issues that can otherwise occur. 27 | if type(next_action) == "string" then 28 | next_action = (keymap[next_action] or {}).rhs or next_action 29 | end 30 | 31 | if type(previous_action) == "string" then 32 | previous_action = (keymap[previous_action] or {}).rhs or previous_action 33 | end 34 | 35 | instance.next_action = next_action 36 | instance.previous_action = previous_action 37 | 38 | instance.next = function() 39 | return instance:_next() 40 | end 41 | instance.previous = function() 42 | return instance:_previous() 43 | end 44 | instance.passthrough = function() 45 | return instance:_passthrough() 46 | end 47 | 48 | instance.prev = instance.previous 49 | instance.prev_key = instance.previous_key 50 | 51 | return instance 52 | end 53 | 54 | function Repeatable:_next() 55 | vim.api.nvim_exec_autocmds("User", { 56 | pattern = { "BetterNNext", "BetterNMappingExecuted" }, 57 | data = { repeatable_id = self.id, key = self.id, mode = vim.fn.mode() }, 58 | }) 59 | 60 | if type(self.next_action) == "function" then 61 | return vim.schedule(self.next_action) 62 | else 63 | return vim.v.count1 .. self.next_action 64 | end 65 | end 66 | 67 | function Repeatable:_previous() 68 | vim.api.nvim_exec_autocmds("User", { 69 | pattern = { "BetterNPrevious", "BetterNMappingExecuted" }, 70 | data = { repeatable_id = self.id, key = self.id, mode = vim.fn.mode() }, 71 | }) 72 | 73 | if type(self.previous_action) == "function" then 74 | return vim.schedule(self.previous_action) 75 | else 76 | return vim.v.count1 .. self.previous_action 77 | end 78 | end 79 | 80 | function Repeatable:_passthrough() 81 | vim.api.nvim_exec_autocmds("User", { 82 | pattern = { "BetterNPassthrough", "BetterNMappingExecuted" }, 83 | data = { repeatable_id = self.id, key = self.id, mode = vim.fn.mode() }, 84 | }) 85 | 86 | if type(self.passthrough_action) == "function" then 87 | return vim.schedule(self.passthrough_action) 88 | else 89 | return vim.v.count1 .. self.passthrough_action 90 | end 91 | end 92 | 93 | return Repeatable 94 | --------------------------------------------------------------------------------