A Neovim plugin to quickly swap (switch, change)
a word (string) under the cursor or a pattern in the current line.
For example, if the cursor is on enable
it will switch to disable
and vice
versa (see Features).
Note
The former name of this plugin was nvim-opposites
. Work in progress. 🚀
This plugin is a bit over-engineered, but I'm having a lot of fun programming
it and testing things out.
In Germany we would say: "Mit Kanonen auf Spatzen schießen". 😉
Other similar or better plugins are:
Notes to breaking changes can be found in the
Table of Contents:
- Features
- Requirements
- Installation
- Usage
- Modules: opposites, chains, cases, todos
- Configuration: Default Options
- Notes
‼️ Breaking Changes- Todo
- Switches between opposite words (see opposites).
- e.g.
true
->false
- Adapts the capitalization of the replaced word.
- e.g.
true
,True
,tRUe
,TRUE
->false
,False
,fALse
,FALSE
.
- e.g.
- e.g.
- Switches through word chains (see chains).
- e.g.
foo
->bar
->baz
->foo
- Adapts the capitalization of the replaced word.
- e.g.
⚠️ Switches between naming conventions (see cases).- e.g.
foo_bar
->fooBar
->FooBar
->foo_bar
- e.g.
- Switches through todo states (see todos).
- e.g.
- [ ] foo
->- [x] foo
- e.g.
If several results are found, the user is asked which result to switch to.
- Neovim >= 0.10
return {
'tigion/swap.nvim',
keys = {
{ '<Leader>i', function() require('swap').switch() end, desc = 'Swap word' },
-- { '<Leader>I', function() require('swap').opposites.switch() end, desc = 'Swap to opposite word' },
-- { '<Leader>I', function() require('swap').chains.switch() end, desc = 'Swap to next word' },
-- { '<Leader>I', function() require('swap').cases.switch() end, desc = 'Swap naming convention' },
-- { '<Leader>I', function() require('swap').cases.switch('pascal') end, desc = 'Swap to PascalCase' },
-- { '<Leader>I', function() require('swap').todos.switch() end, desc = 'Swap todo state' },
},
---@module 'swap'
---@type swap.Config
opts = {},
}
With the future Neovim 0.12, there will be a built-in vim.pack
plugin manager.
It is still under development.
vim.pack.add({
'https://github.com/tigion/swap.nvim',
-- { src = 'https://github.com/tigion/swap.nvim', version = 'main' },
})
-- Use `setup()` for your own user configuration in the `{}` table.
require('swap').setup({})
-- Add a key mapping to switch something.
vim.keymap.set('n', '<Leader>i', require('swap').switch, { desc = 'Swap word' })
Function | Description | Module |
---|---|---|
require('swap').switch() |
Uses all allowed modules (config) | |
require('swap').opposites.switch() |
Switches between opposite words | opposites |
require('swap').chains.switch() |
Switches through word chains | chains |
require('swap').cases.switch() |
Switches between naming conventions | cases |
require('swap').cases.switch('<case_id>') |
Switches to the given naming convention | cases |
require('swap').todos.switch() |
Switches through todo states | todos |
Call the functions directly or use them in a key mapping.
vim.keymap.set('n', '<Leader>i', require('swap').switch, { desc = 'Swap word' })
See the configuration section for the available default options and the modules section for configuration examples.
Call require(‘swap’).switch()
to change the word (string) under the cursor or
the pattern in the current line to one of the allowed modules in
the all.modules
table.
Example:
opts = {
all = {
-- modules = { 'opposites', 'todos' }, -- defaults
modules = { 'opposites', 'chains', 'cases', 'todos' },
},
}
Module | Description |
---|---|
opposites | Switches between opposite words |
chains | Switches through word chains |
cases | Switches between naming conventions |
todos | Switches through todo states |
Call require('swap').opposites.switch()
to switch to the opposite word
or string under the cursor. The found string can also be a part of a word.
For more own defined words, add them to the words
or words_by_ft
table in
the opposites
part of the swap.Config
table.
If use_default_words
and use_default_words_by_ft
is set to false
, only
the user defined words will be used.
Example:
opts = {
opposites = {
words = { -- Default opposite words.
['angel'] = 'devil', -- Adds a new one.
['yes'] = 'ja', -- Replaces the default `['yes'] = 'no'`.
['min'] = nil, -- Removes a default.
},
words_by_ft = { -- File type specific opposite words.
['lua'] = {
['=='] = '~=', -- Replaces the default `['=='] = '!='` for lua files.
},
['sql'] = {
['asc'] = 'desc', -- Adds a new for SQL files.
},
},
},
}
Note
Flexible word recognition can be used to avoid having to configure every variant of capitalization. Activated by default. See Case Sensitive Mask.
Tip
It doesn't have to be opposites words that are exchanged (e.g. ['Vim'] = 'Neovim'
).
Call require(‘opposites’).chains.switch()
to switch to the next word or
string in a word chain under the cursor. The found string can also be a part of
a word.
Examples:
Monday
->Tuesday
->Wednesday
-> ... ->Sunday
->Monday
foo
->bar
->baz
->qux
->foo
The word chains are defined in the words
and words_by_ft
tables in
the chains
part of the swap.Config
table.
Example:
opts = {
chains = {
words = { -- Default word chains.
{ 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' },
{ 'foo', 'bar', 'baz', 'qux' },
},
words_by_ft = { -- File type specific word chains.
asciidoc = {
{ '[NOTE]', '[TIP]', '[IMPORTANT]', '[WARNING]', '[CAUTION]' }, -- AsciiDoc admonitions (block)
{ 'NOTE:', 'TIP:', 'IMPORTANT:', 'WARNING:', 'CAUTION:' }, -- AsciiDoc admonitions (line)
},
markdown = {
{ '[!NOTE]', '[!TIP]', '[!IMPORTANT]', '[!WARNING]', '[!CAUTION]' }, -- Markdown (GitHub) alerts
},
},
},
}
Rules:
- The word chains must be at least 2 words long.
- The word chains should not contain the same word more than once.
Note
Flexible word recognition can be used to avoid having to configure every variant of capitalization. Activated by default. See Case Sensitive Mask.
Warning
This feature is experimental and work in progress. The word identification is very limited (see Limits).
Call require('swap').cases.switch()
to switch to the next case type of the
word under the cursor.
Example:
foo_bar
→FOO_BAR
→foo-bar
→FOO-BAR
→fooBar
→FooBar
→foo_bar
Supported case types are:
snake_case
,SCREAMING_SNAKE_CASE
kebab-case
,SCREAMING-KEBAB-CASE
camelCase
PascalCase
The allowed case types and the switch order can be configured in the types
table in the cases
part of the swap.Config
table.
Example:
opts = {
cases = {
types = {
'snake', -- snake_case
'screaming_snake', -- SCREAMING_SNAKE_CASE
'kebab', -- kebab-case
'screaming_kebab', -- SCREAMING-KEBAB-CASE
'camel', -- camelCase
'pascal', -- PascalCase
},
},
}
Tip
With a given case type id in require('swap').cases.switch('<case_id>')
you
can also directly switch to an case type. The supported case type ids
are: snake
, screaming_snake
, kebab
, screaming_kebab
, camel
and
pascal
.
Example with require('swap').cases.switch('pascal')
:
- foo_bar -> FooBar
- Identifies only words with alphanumeric characters, underscores and dashes
(
a-zA-Z0-9_-
). - Word parts must start with a letter.
- Numbers are only allowed at the end of the word parts.
- Underscores and dashes are only allowed between the word parts.
- Words must be at least 2 parts long.
- No mixed case types.
- No support of abbreviations in capital letters for camelCase and PascalCase
(e.g. ✅
fooJson
, ❌fooJSON
, ✅userId
, ❌userID
).
Examples:
- ✅
foo_bar
,foo_bar1
,foo_bar_baz
- ❌
foo
,foo_1bar
,_foo_bar
,foo_bar_
,foo_bar-baz
,foo_bar_Baz
Call require('swap').todos.switch()
to switch through the todo states.
Supported default todo syntax:
- [ ] foo
with the states[ ]
,[x]
Supported filetype specific todo syntax:
- Markdown Task-Lists:
- [ ] foo
with the states[ ]
,[x]
- AsciiDoc Checklist:
* [ ] foo
with the states[ ]
,[x]
([*]
) - Org Mode Checkboxes:
- [ ] foo
with the states[ ]
,[-]
,[X]
([x]
)
Rules:
- The cursor can be anywhere in the line.
- The first match is used.
- The filetype specific todo syntax have priority over the default todo syntax.
In lazy.nvim, use the table opts = {}
for your own configuration. For other
plugin manager, call the setup function require('swap').setup({})
with the
provided options in {}
directly.
Show annotations and descriptions
---@alias swap.ConfigModule
--- | 'opposites'
--- | 'cases'
--- | 'chains'
--- | 'todos'
---@alias swap.ConfigOppositesWords table<string, string>
---@alias swap.ConfigOppositesWordsByFt table<string, swap.ConfigOppositesWords>
---@alias swap.ConfigChainsWords string[][]
---@alias swap.ConfigChainsWordsByFt table<string, swap.ConfigChainsWords>
---@alias swap.ConfigCasesId
--- | 'snake' snake_case
--- | 'screaming_snake' SCREAMING_SNAKE_CASE
--- | 'kebab' kebab-case
--- | 'screaming_kebab' SCREAMING-KEBAB-CASE
--- | 'camel' camelCase
--- | 'pascal' PascalCase
---@alias swap.ConfigCasesTypes swap.ConfigCasesId[]
---@class swap.ConfigAll
---@field modules? swap.ConfigModule[] The default modules to use.
---@class swap.ConfigOpposites
---@field use_case_sensitive_mask? boolean Whether to use a case sensitive mask.
---@field use_default_words? boolean Whether to use the default opposites.
---@field use_default_words_by_ft? boolean Whether to use the default opposites by file type.
---@field words? swap.ConfigOppositesWords The words with their opposite words.
---@field words_by_ft? swap.ConfigOppositesWordsByFt The file type specific words with their opposite words.
---@class swap.ConfigChains
---@field use_case_sensitive_mask? boolean Whether to use a case sensitive mask.
---@field words? swap.ConfigChainsWords The word chains to search for.
---@field words_by_ft? swap.ConfigChainsWordsByFt The file type specific word chains to search for.
---@class swap.ConfigCases
---@field types? swap.ConfigCasesTypes The allowed case types to parse.
---@class swap.ConfigNotify
---@field found? boolean Whether to notify when a word is found.
---@field not_found? boolean Whether to notify when no word is found.
---@class swap.Config
---@field max_line_length? integer The maximum line length to search.
---@field ignore_overlapping_matches? boolean Whether to ignore overlapping matches.
---@field all? swap.ConfigAll The options for all modules.
---@field opposites? swap.ConfigOpposites The options for the opposites.
---@field cases? swap.ConfigCases The options for the cases.
---@field chains? swap.ConfigChains The options for the chains.
---@field notify? swap.ConfigNotify The notifications to show.
---@type swap.Config
local defaults = {
max_line_length = 1000,
ignore_overlapping_matches = true,
all = {
modules = { 'opposites', 'todos' },
},
opposites = {
use_case_sensitive_mask = true,
use_default_words = true,
use_default_words_by_ft = true,
words = {
['enable'] = 'disable',
['true'] = 'false',
['yes'] = 'no',
['on'] = 'off',
['and'] = 'or',
['left'] = 'right',
['up'] = 'down',
['min'] = 'max',
['=='] = '!=',
['<='] = '>=',
['<'] = '>',
},
words_by_ft = {
['lua'] = {
['=='] = '~=',
},
['sql'] = {
['asc'] = 'desc',
},
},
},
chains = {
use_case_sensitive_mask = true,
words = {},
words_by_ft = {},
},
cases = {
types = {
'snake',
'screaming_snake',
'kebab',
'screaming_kebab',
'camel',
'pascal',
},
},
notify = {
found = false,
not_found = true,
},
}
Flexible word recognition can be used to avoid having to configure every variant of capitalization. This means that variants with capital letters are also found for configured lower-case words and the replaced opposite word adapts the capitalization.
Rules:
- If the found word is uppercase, the mask is upper case.
- If the found word is lowercase, the mask is lower case.
- If the found word is mixed case, the mask is a string to represent the case. Longer words are masked at the end with lower case letters.
Deactivate this behavior by setting use_case_sensitive_mask = false
in the
module options.
Important
If a configured word or his opposite word contains capital letters, then for this words no mask is used.
Example with ['enable'] = 'disable'
:
- found:
enable
,Enable
,EnAbLe
andENABLE
- replaced with:
disable
,Disable
,diSAble
andDISABLE
Example with ['enable'] = 'Disable'
:
- found:
enable
- replaced with:
Disable
By default, overlapping matches are ignored. This means that for the word
foofoo
, if the cursor is in the middle foo
of the word foofoofoo
, only
the first foofoo
is found and the second foofoo
is ignored.
If you want to not ignore overlapping matches, set the option
opts.ignore_overlapping_matches
to false
(default is true
).
-
2025-07-03: The name has changed.
- The repo name has changed from
nvim-opposites
toswap.nvim
. - The plugin module name has changed from
opposites
toswap
.
- The repo name has changed from
-
2025-06-24: The functions have changed.
- The default behavior of
require('opposites').switch()
is now to switch to a supported variant. require('opposites').opposites.switch()
is now only for switching to the opposite word.require('opposites').cases.next()
is nowrequire('opposites').cases.switch()
- See the Usage section.
- The default behavior of
-
2025-06-19: The configuration has changed.
- Options for the opposites are now in the
opposites
table. - The
opposites
andopposites_by_ft
tables are now renamed towords
andwords_by_ft
. - See the Configuration section.
- Options for the opposites are now in the
- Cases: Add support for underscore prefixes and suffixes like
_foo
,__foo_bar__
or__fooBar
. - Add some tests.
- Limit and check the user configuration.
- Change the plugin name to
swap.nvim
. - Switch todo states.
- Support word chains like
{ 'foo', 'bar', 'baz' }
. - Refactoring of the code for separate modules like
opposites
andcases
. - Switch naming conventions (case types).
- Use
vim.ui.select
instead ofvim.fn.inputlist
. - Refactoring of the first quickly written code.
- Adapt the capitalization of the words to reduce words like
true
,True
,tRUe
andTRUE
. - Add file type specific opposites.