From f44c1555c325eebe49793ffd829b208ab9baf9c6 Mon Sep 17 00:00:00 2001 From: Sasha Gurevich Date: Sun, 14 Jun 2026 15:29:34 +0300 Subject: [PATCH] feat: added toggle backwards command --- README.md | 20 ++++++++++++----- doc/toggle.nvim.txt | 29 +++++++++++++++++------- lua/toggle/init.lua | 10 +++++++-- lua/toggle/mapping.lua | 12 ++++++++++ lua/toggle/replacer.lua | 20 ++++++++--------- test/mapping_spec.lua | 20 +++++++++++++++++ test/test_files/test_2.txt | 6 +++++ test/toggle_spec.lua | 45 ++++++++++++++++++++++++++++++++++++++ 8 files changed, 136 insertions(+), 26 deletions(-) create mode 100644 test/test_files/test_2.txt diff --git a/README.md b/README.md index 931a2e2..b2c82e2 100644 --- a/README.md +++ b/README.md @@ -43,20 +43,28 @@ require('toggle').setup({ ### Keymap example ```lua --- v for visual mode, if needed vim.keymap.set({ 'n', 'v' }, 't', require('toggle').toggle, { desc = 'Toggle word' }) +vim.keymap.set({ 'n', 'v' }, 'T', function() require('toggle').toggle(true) end, { desc = 'Toggle word (backward)' }) ``` ## How it works -1. Gets the **word** under the cursor and checks for a mapping → replaces with `ciw` / `ciW` -2. Falls back to the single **character** under the cursor → replaces with `r` -3. Also works for visual mode, in such case selection is being checked -4. Also checks if end of word matches anything in mappings and toggles it +`toggle.toggle()` checks replacers in this order (first match wins): + +1. Visual selection +2. `cWORD` (`ciW`) +3. `cword` (`ciw`) +4. End-of-word match (`ce`) for cases like `goto_prev` → `goto_next` +5. Single character (`r`) + +Pass `true` to toggle backwards in the mapping cycle: + +```lua +require('toggle').toggle(true) +``` Mappings are circular: for a group like `{ 'public', 'protected', 'private' }`, each value cycles to the next, and the last wraps to the first. ## Default mappings See [lua/toggle/defaults.lua](lua/toggle/defaults.lua) for the full list of built-in toggle pairs. - diff --git a/doc/toggle.nvim.txt b/doc/toggle.nvim.txt index 7d72fa6..a0fba56 100644 --- a/doc/toggle.nvim.txt +++ b/doc/toggle.nvim.txt @@ -83,22 +83,30 @@ mappings ~ ============================================================================== 4. USAGE *toggle-usage* -Place your cursor on a word and call the toggle function. +Place your cursor on text and call the toggle function. -The plugin performs two checks: +The plugin checks replacers in this order (first match wins): - 1. It reads the word under the cursor (||). If a mapping exists, - it replaces the word using `ciw`. - 2. If no word mapping is found, it reads the single character under the - cursor. If a mapping exists, it replaces the character using `r`. + 1. Visual selection (`c` in visual mode) + 2. WORD under cursor (||), replaced with `ciW` + 3. Word under cursor (||), replaced with `ciw` + 4. End-of-word suffix under cursor, replaced with `ce` + 5. Single character under cursor, replaced with `r` Example keymap: >lua - vim.keymap.set('n', 't', require('toggle').toggle, { + vim.keymap.set({ 'n', 'v' }, 't', require('toggle').toggle, { desc = 'Toggle word under cursor', }) < +Backward toggle example: >lua + + vim.keymap.set({ 'n', 'v' }, 'T', function() + require('toggle').toggle(true) + end, { desc = 'Toggle word backward' }) +< + ============================================================================== 5. FUNCTIONS *toggle-functions* @@ -108,10 +116,15 @@ toggle.setup({opts}) Can be called multiple times to reconfigure mappings. *toggle.toggle()* -toggle.toggle() +toggle.toggle([{toggle_backwards}]) Toggle the word or character under the cursor. See |toggle-usage| for details on the matching behavior. + Parameters: ~ + {toggle_backwards} (`boolean`, optional) + When `true`, cycles mappings backward (previous value in a circular + group) instead of forward. + ============================================================================== 6. DEFAULT MAPPINGS *toggle-defaults* diff --git a/lua/toggle/init.lua b/lua/toggle/init.lua index 1b637d3..03df1a0 100644 --- a/lua/toggle/init.lua +++ b/lua/toggle/init.lua @@ -35,7 +35,7 @@ function M.setup(opts) end end -function M.toggle() +function M.toggle(toggle_backwards) local replacers = { replacer.__visual_mode_replacer, replacer.__get_cWORD_replacer, @@ -44,12 +44,18 @@ function M.toggle() replacer.__get_character_replacer, } + local mapper = mapping.__get_mapping + + if toggle_backwards then + mapper = mapping.__get_previous_mapping + end + local current_cursor_position = vim.fn.getcurpos() for _, get_replacer in ipairs(replacers) do local r = get_replacer() if r.can_handle() then - r.replace() + r.replace(mapper) if config.keep_cursor_position then vim.fn.setpos('.', current_cursor_position) end diff --git a/lua/toggle/mapping.lua b/lua/toggle/mapping.lua index 5a4f842..8532acd 100644 --- a/lua/toggle/mapping.lua +++ b/lua/toggle/mapping.lua @@ -10,6 +10,18 @@ M.__get_mapping = function(word) return mapping[word] end +M.__get_previous_mapping = function(word) + -- cycle till the original word and get previous one + local next_word = word + while next_word ~= mapping[next_word] do + next_word = mapping[next_word] + + if mapping[next_word] == word then + return next_word + end + end +end + M.__register = function(list) for i, value in ipairs(list or {}) do if i == #list then diff --git a/lua/toggle/replacer.lua b/lua/toggle/replacer.lua index f3cce87..c62999d 100644 --- a/lua/toggle/replacer.lua +++ b/lua/toggle/replacer.lua @@ -10,8 +10,8 @@ M.__get_cword_replacer = function() return mapping.__has_mapping(word) end, - replace = function() - vim.api.nvim_command('normal! ciw' .. mapping.__get_mapping(word)) + replace = function(get_mapping) + vim.api.nvim_command('normal! ciw' .. get_mapping(word)) end, } end @@ -23,8 +23,8 @@ M.__get_cWORD_replacer = function() return mapping.__has_mapping(word) end, - replace = function() - vim.api.nvim_command('normal! ciW' .. mapping.__get_mapping(word)) + replace = function(get_mapping) + vim.api.nvim_command('normal! ciW' .. get_mapping(word)) end, } end @@ -39,8 +39,8 @@ M.__get_character_replacer = function() return mapping.__has_mapping(character) end, - replace = function() - vim.api.nvim_command('normal! r' .. mapping.__get_mapping(character)) + replace = function(get_mapping) + vim.api.nvim_command('normal! r' .. get_mapping(character)) end, } end @@ -60,8 +60,8 @@ M.__get_end_of_word_replacer = function() return mapping.__has_mapping(end_of_word_under_cursor) end, - replace = function() - vim.api.nvim_command('normal! ce' .. mapping.__get_mapping(end_of_word_under_cursor)) + replace = function(get_mapping) + vim.api.nvim_command('normal! ce' .. get_mapping(end_of_word_under_cursor)) end, } end @@ -85,8 +85,8 @@ M.__visual_mode_replacer = function() return is_visual_mode() and mapping.__has_mapping(selected_text) end, - replace = function() - vim.api.nvim_command('norm! c' .. mapping.__get_mapping(selected_text)) + replace = function(get_mapping) + vim.api.nvim_command('norm! c' .. get_mapping(selected_text)) end, } end diff --git a/test/mapping_spec.lua b/test/mapping_spec.lua index 7f40fdf..4e3879e 100644 --- a/test/mapping_spec.lua +++ b/test/mapping_spec.lua @@ -54,4 +54,24 @@ describe('mapping', function() it('__has_mapping - nil', function() assert(mapping.__has_mapping(nil) == false) end) + + it('__get_previous_mapping - existing mapping', function() + mapping.__reset() + mapping.__register({ 'foo', 'bar' }) + assert(mapping.__get_previous_mapping('foo') == 'bar') + assert(mapping.__get_previous_mapping('bar') == 'foo') + end) + + it('__get_previous_mapping - chain of mappings', function() + mapping.__reset() + mapping.__register({ 'foo', 'bar', 'zoo' }) + assert(mapping.__get_previous_mapping('foo') == 'zoo') + assert(mapping.__get_previous_mapping('bar') == 'foo') + assert(mapping.__get_previous_mapping('zoo') == 'bar') + end) + + it('__get_previous_mapping - non-existing mapping', function() + mapping.__reset() + assert(mapping.__get_previous_mapping('non-existing') == nil) + end) end) diff --git a/test/test_files/test_2.txt b/test/test_files/test_2.txt new file mode 100644 index 0000000..230d2d3 --- /dev/null +++ b/test/test_files/test_2.txt @@ -0,0 +1,6 @@ +false +false, +< +goto_prev +thisisalongwordwithprotectedanotherwordinside +kaboomza diff --git a/test/toggle_spec.lua b/test/toggle_spec.lua index c230cd9..a568430 100644 --- a/test/toggle_spec.lua +++ b/test/toggle_spec.lua @@ -69,4 +69,49 @@ describe('toggle', function() assert(vim.fn.expand('') == 'goto_prev') end) end) + + it('toggle (backwards) - cword', function() + open_file_at('test/test_files/test_2.txt', 1, 1, function() + toggle.toggle(true) + assert(vim.fn.expand('') == 'true') + toggle.toggle(true) + assert(vim.fn.expand('') == 'false') + end) + end) + + it('toggle (backwards) - cWORD', function() + open_file_at('test/test_files/test_2.txt', 2, 1, function() + toggle.toggle(true) + assert(vim.fn.expand('') == 'true') + toggle.toggle(true) + assert(vim.fn.expand('') == 'false') + end) + end) + + it('toggle (backwards) - char', function() + open_file_at('test/test_files/test_2.txt', 3, 1, function() + toggle.toggle(true) + assert(get_character_under_cursor() == '>') + toggle.toggle(true) + assert(get_character_under_cursor() == '<') + end) + end) + + it('toggle (backwards) - end of word', function() + open_file_at('test/test_files/test_2.txt', 4, 5, function() + toggle.toggle(true) + assert(vim.fn.expand('') == 'goto_next') + toggle.toggle(true) + assert(vim.fn.expand('') == 'goto_prev') + end) + end) + + it('toggle (backwards) - no match', function() + open_file_at('test/test_files/test_2.txt', 6, 1, function() + toggle.toggle(true) + assert(vim.fn.expand('') == 'kaboomza') + toggle.toggle(true) + assert(vim.fn.expand('') == 'kaboomza') + end) + end) end)