Reactivity. Right in your neovim.
- Performant:
reactive.nvim
uses neovim events to apply highlights (ModeChanged
for mode changes,WinEnter
, andBufWinEnter
for coloring active/inactive windows), your input isn't monitored at all. - Window highlights: apply highlights only for a current window. Utilizes
'winhighlight'
neovim-specific option. (read more:h 'winhighlight'
). - Highlights: apply/change global highlights on mode changes.
- Highly customizable: you can customize literally any mode, even very specific one like
niI
(triggered when you press Ctrl + o in insert mode) - Specificity and priority systems: if you are coming from FrontEnd, you probably already understand what these term mean. In
Reactive
every mode has its specificity depending on its length. More of this below. - Presets: define your own presets or use builtin ones.
- Modes: apply different highlights for different modes.
- Operators: you can apply your highlights and window highlights on any operator like 'd', 'c', 'y' and others (all operators supported).
- Custom operators: you can even apply highlights on your custom operators! Always wanted to highlight a cursor line (or whatever), when using a specific external plugin's operator? Now it's possible.
- Extendable: other plugin creators (especially theme ones) can use
reactive.nvim
to add dynamic highlights to their plugins.
reactive
is a plugin that brings interactivity in your Neovim experience. It allows you to apply window-local or global highlights on mode changes or on window entering/leaving. If you're a plugin developer and considering usingreactive
as a dependency, you can make your plugin (especially if it's a colortheme) much more reactive and more pleasant to use. Read extending reactive for more on that.
reactive-modes.mp4
reactive-operators.mp4
reactive-windows.mp4
You can see how a cursor color and a cursor line color change when I switch modes/operators: Green for "insert" mode, blue for "change" operator, red for "delete" operator, orange for "yank" operator, violet for "visual" mode, cyan for "replace" mode. Moreover, when you switch windows you can see how those colors changed for inactive window.
reactive.nvim.mp4
If you watched closely, you could see how
reactive
works withtelepath.nvim
. Now you will know which window you jumped in and what your current operator is.
reactive
is in its early stages and some fields with their values may change in favor of convenience in the future, but this should not stop you
from trying this plugin out. Breaking changes (if any) won't happen suddenly and unexpectedly, if they don't break the core behavior of a plugin. In other cases,
they will be marked deprecated
and you'll be notified in your Neovim console.
Neovim version: >= 0.7.0
- With
lazy.nvim
:
{ 'rasulomaroff/reactive.nvim' }
- With
packer.nvim
use { 'rasulomaroff/reactive.nvim' }
To quickly understand what you can do with this plugin, just use built-in presets, but I encourage you to build your preset and not rely on built-in ones, which can be changed in the future. For the full spec look here.
require('reactive').setup {
builtin = {
cursorline = true,
cursor = true,
modemsg = true
}
}
Alternatively, you can add your own preset:
require('reactive').add_preset {
-- your preset configuration here
}
You don't need to call the setup
function to initialize reactive
. It will be initialized as soon as you require it.
The setup
function is only needed when you want to configure presets that some other plugins added or load their presets.
For the full spec look here.
require('reactive').setup {
configs = {
-- a key here is a preset name, a value can be a boolean (if false, presets will be disabled)
-- or a table that overwrites preset's values
presetone = {
modes = {
i = {
winhl = {
-- overwrites StatusLine highlight group in insert mode
StatusLine = { bg = '', fg = '' }
}
}
}
},
-- disables `presettwo`
presettwo = false
}
}
Loading presets that you or other plugin authors added to the folder (reactive/presets/yourpresetname.lua
).
More on that here
require('reactive').setup {
load = { 'yourpresetname' } -- you can also use a string
}
Reactive has the following commands you can use:
ReactiveStart
- startsreactive
by initializing listeners, such as events onModeChanged
,WinEnter
etc.ReactiveStop
- removes listeners.ReactiveToggle
- toggles state between 2 commands above.Reactive enable <preset>
- enables selected preset.Reactive disable <preset>
- disables selected preset.Reactive toggle <preset>
- toggles selected preset.
This is a table you're passing to the setup
method.
Property | Type | Description |
---|---|---|
builtin | table<string, boolean | Preset> |
This field is there for demonstration purposes, but you can actually use it if the colors meet your needs. The key is a preset name and a value is either a boolean or a table of preset fields you want to overwrite. Take a look at the Preset Spec. |
configs | table<string, boolean | Preset> |
The key is a preset name. You can enable lazy presets here by passing true as a value or disable some by passing false . You can also customize a preset by passing a table with fields you want to overwrite. Take a look at the Preset Spec. |
load | string or table<string> |
This is a shortcut for the load_preset method which allows you to load presets from the reactive/presets/ directory. You can either put a name of a preset to this field or pass a table with preset names that should be loaded. Read about it here. |
The name
field is required.
Property | Type | Description |
---|---|---|
name | string |
This is your preset's name. It should be unique across other presets. |
lazy | boolean |
This property is meant to be used by other plugin developers. By making your preset lazy you can delay its usage till a user decides to activate it. |
priority | number |
You can set a priority of any preset, if you faced conflicting preset highlights, for example. It's not recommended to set this field, if you are a plugin developer. |
skip | fun(): boolean or { winhl?: fun(): boolean, hl?: fun(): boolean } |
This function will be called on every mode change, so that you can define when your preset shouldn't be applied. It should return true, if you want to skip applying highlights. You can also pass a table with functions, if you want to disable only window highlights or highlights. |
init | fun() |
This function will be called once when a preset inits. |
modes | table<string, ModeConfig> |
This is a table where a key is a mode (check :h mode() for understanding all the modes Neovim has), and a value is a ModeConfig specification. |
static | StaticConfig |
Static highlights are applied when there're no such highlights in the modes field. |
Example of a preset:
local my_preset = {
name = 'my-preset',
skip = function()
return vim.api.nvim_buf_get_option(0, 'buftype') == ''
end,
modes = {
n = {
-- normal mode configuration
},
i = {
-- insert mode configuration
}
}
}
Mode config is a table, containing following field and values:
Property | Type |
---|---|
winhl | table<string, table> |
hl | table<string, table> |
operators | table<string, ModeConfig> - but without operators field |
opfuncs | table<string, ModeConfig> - but without operators and opfuncs fields |
exact | boolean or { winhl?: boolean, hl?: boolean } |
frozen | boolean or { winhl?: boolean, hl?: boolean } |
from | fun(modes: { from: string, to: string }) |
to | fun(modes: { from: string, to: string }) |
type: table<string, table>
Table of window-local highlights, that will be applied in a specific mode/operator/opfunc.
A key in this table is a name of a highlight group and value - its value. The same you set with vim.api.nvim_set_hl(0, name, value)
.
Example:
winhl = {
StatusLine = { fg = '#000000', bg = '#ffffff' }
}
type: table<string, table>
Table of highlights, that will be applied in a specific mode/operator/opfunc. Very similar to winhl
, but these highlights are set globally (in every window).
For example, Cursor
hl group cannot be highlighted window-locally, so you can set it here.
Example:
hl = {
Cursor = { fg = '#00ff00', bg = '#ff00ff' }
}
type: ModeConfig
Note
This field can only be used inside operator-pending modes, like no
, nov
, noV
, and no\x16
.
This field allows you to configure operators as you configure modes. This is a table where a key is an operator ('d', 'y' or any other valid one, check :h operator
to see all existing operators),
and a value is a ModeConfig
spec. Highlights that you specify in this table will be prioritized over those from modes
.
Example:
operators = {
d = {
winhl = {
-- if the `no` mode has StatusLine highlight, it will be overwritten by this one below
StatusLine = { fg = 'yourcolor', bg = 'yourcolor' }
}
}
}
type: ModeConfig
Note
This field can only be used inside the g@
operator and is experimental.
You can apply highlights depending on your custom operators. Spec here will be the same as in the modes
and operator
fields.
Read more about using custom operators here.
type: fun(modes: { from: string, to: string })
Note
This field can only be used inside modes, not inside operators/opfuncs.
A callback that will be executed when neovim goes into another mode from this one.
Example:
modes = {
i = {
from = function(modes)
-- callback that will be executed every time you leave an insert mode
end
}
}
type: fun(modes: { from: string, to: string })
Note
This field can only be used inside modes, not inside operators/opfuncs.
A callback that will be executed when neovim goes into this mode.
modes = {
n = {
to = function(modes)
-- callback that will be executed every time you enter a normal mode
end
}
}
type: boolean
or { winhl?: boolean, hl?: boolean }
Note
This field will be checked only for the exact mode triggered. For example, if a triggered mode is Rv
, reactive
will check the exact
field only
for Rv
mode, not for R
. If you would like to stop mode propagation, look at the frozen
field.
You don't really need to use this field if the mode you're configuring consists of one letter. It allows you not to apply highlights from modes
that are less specific than triggered one. For example, if you set this is as true
for niI
mode and that mode is triggered, reactive
won't
apply highlights from ni
and n
modes. (It does it by default due to mode propagation).
You can also pass a table identifying which field you want to be exact.
type: boolean
or { winhl?: boolean, hl?: boolean }
This field tells reactive
to stop propagation of mode highlights. Can be a boolean value to stop propagation of highlights completely or a table which
specifies which highlights should be stopped. So, for example you specified frozen = true
for the n
(normal) mode, that means that reactive
won't
apply normal mode's highlights to any further n*
mode, like no
, niI
, niR
etc. You can read more about the mode propagation here.
A complete example of a preset can look like the following:
local my_preset = {
name = 'my-preset',
init = function()
-- making our cursor to use `MyCursor` highlight group
vim.opt.guicursor:append 'MyCursor'
end,
modes = {
n = {
winhl = {
-- we use `winhl` because we want to highlight CursorLine only in a current window, not in all of them
-- if you want to change global highlights, use the `hl` field instead.
CursorLine = { bg = '#21202e' }
},
hl = {
MyCursor = { bg = '#FFFFFF' }
},
},
no = {
-- You can also specify winhl and hl that will be applied with every operator
winhl = {},
hl = {},
operators = {
d = {
winhl = {
CursorLine = { bg = '#450a0a' }
},
hl = {
MyCursor = { bg = '#fca5a5' }
}
},
y = {
winhl = {
CursorLine = { bg = '#422006' },
},
hl = {
MyCursor = { bg = '#fdba74' }
}
}
}
},
i = {
winhl = {
CursorLine = { bg = '#042f2e' }
},
hl = {
MyCursor = { bg = '#5eead4' }
}
}
}
}
Note
Highlights from the static
field will be only applied when there're no alternatives for them in the modes
field.
Example:
{
modes = {
-- here is your modes config
},
static = {
hl = {
-- put your `fallback` highlights here
},
winhl = {
active = {
-- put your highlights for a current window here
},
inactive = {
-- put your highlights for non-current windows here
}
},
}
}
There are some useful shortcuts if you want to match all visual or select modes. Unfortunately, visual
mode, visual-line
mode, and visual-block
mode start from different
characters and cannot be match just by typing v
as other modes can be, for example:
insert
mode -i
(matches all insert modes)replace
-R
(matches all replace modes)
Given that, to match all visual modes you need to use the following config:
modes = {
-- \x16 here stands for visual block mode, which you can cause by pressing CTRL + v
[{ 'v', 'V', '\x16' }] = {
-- your config here
}
}
For select modes:
modes = {
[{ 's', 'S', '\x13' }] = {
-- your config here
}
}
If you're a plugin developer, please read this. Want to load a preset from other plugin? - Proceed to the 3rd step.
If you don't want to use the add_preset
method for some reasons, for example your preset is giant or you have several ones, then it is completely fine. For that reason there's a more clean way to do it:
- Create a file(s) with a desired preset name(s) inside
reactive/presets/yourpresetname.lua
directory. You should put this directory where Neovim will be able to access it. The best way to it is just to put it inside yourlua/
directory so the full path will look like thislua/reactive/presets/yourpresetname.lua
. - Return a preset from that file.
lua/reactive/presets/statusline.lua
return {
name = 'statusline', -- this field is required and should match your file name
modes = {
i = {
hl = {
StatusLine = {}
}
}
}
}
- Load this preset either using the
setup
method or theload_preset
method.
require('reactive').setup {
load = 'statusline' -- you can also use a table if you want to load several presets like that { 'statusline', 'another' }
}
or
require('reactive').load_preset 'statusline'
Important
This feature is experimental. Consider possible further changes.
You can apply highlights on custom operators the way you do for built-in operators.
This is a config for delete
operator.
operators = {
d = {}
}
As you may or may not know, all custom operators in Neovim utilize g@
operator. Documentation says this operator(g@) calls a function set with the 'operatorfunc' option.
So, to specify a custom operator you need to use g@
operator. Let's say we want to highlight StatusLine
dark violet when substitute.nvim
triggers its substitute
operator.
From its documentation we see that we should set this mapping to get it work:
vim.keymap.set("n", "s", require('substitute').operator, { noremap = true })
To also tell reactive
that this operator is triggered, we need to change this mapping to look like this:
vim.keymap.set("n", "s", function()
-- this way reactive will know that substitute's operator is triggered
-- the string you pass into this function should be unique across other custom operators
-- this method should called BEFORE the actual function
require('reactive').set_opfunc 's'
require('substitute').operator()
end, { noremap = true })
Warning
You can't pass any string to the set_opfunc
function since that string will eventually be decoded to be used in a highlight group's name.
Do NOT put any symbols there, except .
and @
. Also try to make the string short. As already been said, the string you pass into this
function should be unique across other custom operators.
Now to highlight StatusLine
when that operator is triggered, you Lua code will look like this:
operators = {
['g@'] = {
-- don't forget you can also set highlights for each custom operator, that will be applied
-- every time the `g@` operator is triggered
winhl = {},
hl = {},
opfuncs = {
-- 's' here is that unique name we passed into the `set_opfunc` method in the mapping
s = {
winhl = {
StatusLine = { bg = '#5b21b6' }
}
}
}
}
}
You can apply the same config for several modes at once by specifying a key as an array. This also works for operators and operator functions (opfuncs). Example:
{
modes = {
[{ 'n', 'i', 'v' }] = {
-- shared mode config for a normal, insert and visual mode
},
no = {
operators = {
[{ 'd', 'c' }] = {
-- shared mode config for delete and change operators
},
['g@'] = {
opfuncs = {
[{ 'firstoperator', 'secondoperator' }] = {
-- shared mode config for your custom operators
}
}
}
}
}
}
}
First of all, take a look at this picture. This is how mode propagation works.
As you can see, when some mode containing 2 or more characters is triggered, it starts propagating from the first character, and then continues
all the way up to the exact match. When propagating, reactive
will start to form what's called a Snapshot
. It's a table containing all of window and global highlights
that must be applied now.
Why
You're possibly thinking right now: "Why do we ever need it?". Well, because if you think how modes in Vim/Neovim are implemented, you'll notice that they're made in a very clever and thoughtful way.
Let's take the Rv
mode from the example above. Documentation says it is a 'Virtual Replace' mode, triggered by gR
keys. If you use this mode,
you'll notice that it's very similar to the usual 'Replace' mode. It obviously has some differences, but the first letter in Rv
is R
, identifying
that it's a Replace
mode at first, but a bit specific.
Now let's take nov
mode (Operator-pending, forced charwise mode). The propagation sequence here will be n
-> no
-> nov
.
n
->no
- when you're in the operator-pending mode, you're still kind of partially in the normal mode as well, because nothing has happenend yet and Neovim is waiting for your next action.no
->nov
- this is still the operator-pending mode, but now more specific. It's forced to act charwise.
This is why we need these fields. If you look at that picture again, you can imagine that frozen
field removes the blue arrow. For example, if you specify
frozen = true
for the n
mode, nov
mode won't get highlights from n
mode (but will from no
). But if you only want to get highlights from the triggered mode with
no propagation at all, specifying exact = true
for a mode will do what you want. The exact
field is only checked for the whole mode match (if a triggered mode is no
, the
exact
field is only checked for the no
mode, not for the n
).
You may think that this is a performance issue, since there's no sense in first forming a table with highlights and then erase everything if the last mode is exact
and
you will be right. This is why reactive
goes backwards from niI
, then ni
, finally n
. If niI
has exact
option, reactive
will stop looking further.
Now we have mode propagation, modes, operators, custom operators. How do we know which highlights should be applied? This is when specificity and priority step in. Specificity depends on a mode length, meaning that the longer the mode is, the more specific it is.
Let's say we have such a config:
modes = {
n = {
winhl = {
WinBar = { ... }
StatusLine = { ... }
}
},
no = {
winhl = {
WinBar = { ... }
}
}
}
If the operator-pending
mode is triggered, which is the equivalent to no
, the no
's mode length is 2, whereas the n
's is 1. That means that a resulted Snapshot
will contain WinBar
highlight from the no
mode and StatusLine
highlight
from the n
mode. Thus, no
is more specific than n
. Highlights from the n
mode will be applied just because the no
mode has n
letter at the first character.
Priority is intended to solve highlight conflicts between modes
| operators
| operator functions
.
They are listed below in order of priority:
- Operator functions
- Operators
- Modes
- Static
Meaning that highlights from operator functions will be prioritized over those from operators, whereas highlights from operators will be prioritized over
those from modes. The last colors that will be applied are those from the static
field.
Be aware that Specificity is still the most important factor. Highlights from a more specific mode will still overwrite those from a less specific one, even if they were
applied from an operator or an operator function.
The process of extending reactive
is made as straightforward as possible. If your plugin wants to use reactive
, just require it and add your preset.
require('reactive').add_preset {
name = 'your preset name',
-- other fields
-- do not forget about `lazy` field, if you want to delegate your preset activation to a user
}
Alternatively, you can create your preset file at reactive/presets/yourpresetname.lua
and then load it:
- Put your preset into a file, eg
reactive/presets/test.lua
return {
name = 'test', -- should be the same as the file name
modes = {
i = {
winhl = {},
hl = {}
}
},
-- other fields
-- do not forget about `lazy` field, if you want to delegate your preset activation to a user
}
- Load it from any other file that's executed:
require('reactive').load_preset 'test'
Then, if a user wants to configure your preset, they can do that from the setup
function:
require('reactive').setup {
configs = {
['your preset name'] = {
-- here they can put custom configuration
},
-- alternatively, to disable your plugin they can specify a boolean value
['your preset name'] = false -- or true, if your plugin is lazy and a user action is required to enable it
}
}