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->Mondayfoo->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 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_CASEkebab-case,SCREAMING-KEBAB-CASEcamelCasePascalCase
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 hyphens
(
a-zA-Z0-9_-). - Word parts must start with a letter.
- Numbers are only allowed within the word parts and not at the beginning.
- Underscores and hyphens are only allowed between the word parts.
- Underscores are allowed as prefix and/or suffix.
- Words must be at least 2 parts long.
- No mixed case types.
- Limited support of abbreviations in capital letters for camelCase and PascalCase,
but it's only one-way
(e.g.
fooJSON->foo_json->fooJson,HTTPRequest->http_request->HttpRequest). - Some inaccurate edge cases (e.g.
a_b_c->aBC->a_bc).
Examples:
- ✅
foo_bar,foo_bar1,foo_bar_baz - ✅
_foo_bar,__foo_bar,__foo_bar__ - ❌
foo,foo_1bar,foo_bar-baz,foo_bar_Baz
Call require('swap').todos.switch() to switch through the todo states.
Supported default todo syntax:
- [ ] foowith the states[ ],[x]
Supported filetype specific todo syntax:
- Markdown Task-Lists:
- [ ] foowith the states[ ],[x] - AsciiDoc Checklist:
* [ ] foowith the states[ ],[x]([*]) - Org Mode Checkboxes:
- [ ] foowith 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,EnAbLeandENABLE - replaced with:
disable,Disable,diSAbleandDISABLE
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-oppositestoswap.nvim. - The plugin module name has changed from
oppositestoswap.
- 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
oppositestable. - The
oppositesandopposites_by_fttables are now renamed towordsandwords_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
oppositesandcases. - Switch naming conventions (case types).
- Use
vim.ui.selectinstead ofvim.fn.inputlist. - Refactoring of the first quickly written code.
- Adapt the capitalization of the words to reduce words like
true,True,tRUeandTRUE. - Add file type specific opposites.