undo-glow.nvim is a Neovim plugin that adds beautiful visual feedback to your edits. See exactly what changed when you undo, redo, paste, search, or perform any text operation.
Note
This plugin requires manual setupβno keymaps are created automatically. See the Quick Start guide below to get started in minutes!
- Visual feedback for all text operations - Undo, redo, paste, search, comments, and more
- Beautiful animations - Smooth fades, pulses, bounces, and 10+ other effects
- Zero dependencies - Uses only Neovim's native APIs
- Highly customizable - Change colors, duration, and animation styles per action
- Works with your favorite plugins - Built-in support for yanky.nvim, substitute.nvim, flash.nvim
undo.mov
redo.mp4
yank.mov
paste.mp4
search.mov
comment.mp4
beacon.mp4
Using lazy.nvim:
{
"y3owk1n/undo-glow.nvim",
version = "*", -- use stable releases
opts = {
-- your configuration (see Quick Start below)
}
}For other package managers, call setup() manually:
require("undo-glow").setup({
animation = {
enabled = true,
duration = 300,
}
})Here's a complete, ready-to-use configuration that covers the most common use cases:
{
"y3owk1n/undo-glow.nvim",
event = { "VeryLazy" },
---@type UndoGlow.Config
opts = {
animation = {
enabled = true,
duration = 300,
animation_type = "zoom",
window_scoped = true,
},
highlights = {
undo = {
hl_color = { bg = "#693232" }, -- Dark muted red
},
redo = {
hl_color = { bg = "#2F4640" }, -- Dark muted green
},
yank = {
hl_color = { bg = "#7A683A" }, -- Dark muted yellow
},
paste = {
hl_color = { bg = "#325B5B" }, -- Dark muted cyan
},
search = {
hl_color = { bg = "#5C475C" }, -- Dark muted purple
},
comment = {
hl_color = { bg = "#7A5A3D" }, -- Dark muted orange
},
cursor = {
hl_color = { bg = "#793D54" }, -- Dark muted pink
},
},
priority = 2048 * 3,
},
keys = {
{
"u",
function()
require("undo-glow").undo()
end,
mode = "n",
desc = "Undo with highlight",
noremap = true,
},
{
"U",
function()
require("undo-glow").redo()
end,
mode = "n",
desc = "Redo with highlight",
noremap = true,
},
{
"p",
function()
require("undo-glow").paste_below()
end,
mode = "n",
desc = "Paste below with highlight",
noremap = true,
},
{
"P",
function()
require("undo-glow").paste_above()
end,
mode = "n",
desc = "Paste above with highlight",
noremap = true,
},
{
"n",
function()
require("undo-glow").search_next({
animation = {
animation_type = "strobe",
},
})
end,
mode = "n",
desc = "Search next with highlight",
noremap = true,
},
{
"N",
function()
require("undo-glow").search_prev({
animation = {
animation_type = "strobe",
},
})
end,
mode = "n",
desc = "Search prev with highlight",
noremap = true,
},
{
"*",
function()
require("undo-glow").search_star({
animation = {
animation_type = "strobe",
},
})
end,
mode = "n",
desc = "Search star with highlight",
noremap = true,
},
{
"#",
function()
require("undo-glow").search_hash({
animation = {
animation_type = "strobe",
},
})
end,
mode = "n",
desc = "Search hash with highlight",
noremap = true,
},
{
"gc",
function()
-- This is an implementation to preserve the cursor position
local pos = vim.fn.getpos(".")
vim.schedule(function()
vim.fn.setpos(".", pos)
end)
return require("undo-glow").comment()
end,
mode = { "n", "x" },
desc = "Toggle comment with highlight",
expr = true,
noremap = true,
},
{
"gc",
function()
require("undo-glow").comment_textobject()
end,
mode = "o",
desc = "Comment textobject with highlight",
noremap = true,
},
{
"gcc",
function()
return require("undo-glow").comment_line()
end,
mode = "n",
desc = "Toggle comment line with highlight",
expr = true,
noremap = true,
},
},
init = function()
vim.api.nvim_create_autocmd("TextYankPost", {
desc = "Highlight when yanking (copying) text",
callback = function()
require("undo-glow").yank()
end,
})
-- This only handles neovim instance and do not highlight when switching panes in tmux
vim.api.nvim_create_autocmd("CursorMoved", {
desc = "Highlight when cursor moved significantly",
callback = function()
require("undo-glow").cursor_moved({
animation = {
animation_type = "slide",
},
})
end,
})
-- This will handle highlights when focus gained, including switching panes in tmux
vim.api.nvim_create_autocmd("FocusGained", {
desc = "Highlight when focus gained",
callback = function()
---@type UndoGlow.CommandOpts
local opts = {
animation = {
animation_type = "slide",
},
}
opts = require("undo-glow.utils").merge_command_opts("UgCursor", opts)
local pos = require("undo-glow.utils").get_current_cursor_row()
require("undo-glow").highlight_region(vim.tbl_extend("force", opts, {
s_row = pos.s_row,
s_col = pos.s_col,
e_row = pos.e_row,
e_col = pos.e_col,
force_edge = opts.force_edge == nil and true or opts.force_edge,
}))
end,
})
vim.api.nvim_create_autocmd("CmdlineLeave", {
desc = "Highlight when search cmdline leave",
callback = function()
require("undo-glow").search_cmd({
animation = {
animation_type = "fade",
},
})
end,
})
end,
},That's it! You now have beautiful visual feedback for all your edits. π
Tip
Want to customize colors, animations, or add more features? Check out the Configuration Guide below.
The main settings you'll want to customize:
opts = {
animation = {
enabled = true, -- Turn animations on/off
duration = 300, -- How long highlights last (milliseconds)
animation_type = "fade", -- Animation style (see options below)
},
highlights = {
undo = { hl_color = { bg = "#FF5555" } }, -- Red for undo
redo = { hl_color = { bg = "#50FA7B" } }, -- Green for redo
-- ... customize other operations
},
}Choose from 11 built-in animation styles:
"fade"- Smooth fade out (default)"pulse"- Breathing effect"zoom"- Brief brightness increase"slide"- Moves right before fading"blink"- Rapid on/off toggle"strobe"- Rapid color changes"jitter"- Shaky/vibrating effect"spring"- Overshoots then settles"rainbow"- Cycles through colors"desaturate"- Gradually mutes colors"fade_reverse"- Smooth fade in
Static highlight cleared after duration.
animation-none.mov
Gradually decreases opacity.
animation-fade.mov
Gradually increases opacity.
animation-fade-reverse.mov
Toggles highlight on and off.
animation-blink.mov
Rhythmic breathing effect.
animation-pulse.mov
Rapid shaking/vibrating.
animation-jitter.mov
Overshoots then settles.
animation-spring.mov
Gradually reduces saturation.
animation-desaturate.mov
Rapid color toggles.
animation-strobe.mov
Brief brightness increase.
animation-zoom.mov
Cycles through hues smoothly.
animation-raindow.mov
Moves right before fading.
animation-slide.mov
Three ways to set colors:
-- 1. Direct color (hex code)
highlights = {
undo = { hl_color = { bg = "#FF5555" } }
}
-- 2. Link to existing highlight group
highlights = {
undo = { hl = "Cursor" }
}
-- 3. Use vim.api.nvim_set_hl (in your config)
vim.api.nvim_set_hl(0, "UgUndo", { bg = "#FF5555" })Customize animations for specific operations:
keys = {
-- Use "zoom" for searches
{
"n",
function()
require("undo-glow").search_next({
animation = { animation_type = "zoom" }
})
end,
desc = "Search next"
},
-- Use "pulse" for undo
{
"u",
function()
require("undo-glow").undo({
animation = { animation_type = "pulse" }
})
end,
desc = "Undo"
},
}Show where your cursor lands after big jumps:
init = function()
vim.api.nvim_create_autocmd("CursorMoved", {
callback = function()
require("undo-glow").cursor_moved({
animation = { animation_type = "slide" }
}, {
steps_to_trigger = 10, -- Jump threshold
ignored_ft = { "mason", "lazy" }, -- Skip these filetypes
})
end,
})
endVisual feedback when toggling comments:
keys = {
{
"gc",
function()
local pos = vim.fn.getpos(".")
vim.schedule(function() vim.fn.setpos(".", pos) end)
return require("undo-glow").comment()
end,
mode = { "n", "x" },
expr = true,
desc = "Toggle comment"
},
{
"gcc",
function()
return require("undo-glow").comment_line()
end,
expr = true,
desc = "Comment line"
},
}-- Turn off yanky's built-in highlights
require("yanky").setup({
highlight = {
on_put = false,
on_yank = false,
}
})
-- Add undo-glow highlights
vim.keymap.set("n", "p", function()
return require("undo-glow").yanky_put("YankyPutAfter")
end, { expr = true, desc = "Paste below" })-- Turn off substitute's highlights
require("substitute").setup({
highlight_substituted_text = { enabled = false }
})
-- Add undo-glow highlights
vim.keymap.set("n", "s", function()
require("undo-glow").substitute_action(require("substitute").operator)
end, { desc = "Substitute" })-- Highlight cursor after jumping
vim.keymap.set({ "n", "x", "o" }, "s", function()
require("undo-glow").flash_jump()
end, { desc = "Flash jump" })Default colors (customize these in your config):
| Group | Default Color | Purpose |
|---|---|---|
UgUndo |
#FF5555 (red) |
Undo operations |
UgRedo |
#50FA7B (green) |
Redo operations |
UgYank |
#F1FA8C (yellow) |
Yank/copy |
UgPaste |
#8BE9FD (cyan) |
Paste |
UgSearch |
#BD93F9 (purple) |
Search |
UgComment |
#FFB86C (orange) |
Comments |
UgCursor |
#FF79C6 (magenta) |
Cursor movement |
Run the health check if something isn't working:
:checkhealth undo-glowCommon issues:
- Animations not showing? Make sure
animation.enabled = true - Wrong colors? Check if your theme is overriding the highlight groups
- Performance issues? Try increasing
debounce_delayor disabling animations
- Recipes - More configuration examples
- Advanced Documentation - APIs, custom animations, performance tuning
- GitHub Issues - Report bugs or request features
Note
For advanced users only! If you're happy with the basic setup above, you don't need to read this section.
The sections below cover advanced topics like creating custom animations, performance tuning, and extending the plugin with hooks and APIs.
π Table of Contents
Complete reference for all available options:
{
animation = {
enabled = false,
duration = 100, -- milliseconds
animation_type = "fade", -- or custom function
fps = 120, -- frames per second
easing = "in_out_cubic", -- or custom function
window_scoped = false, -- experimental: restrict to active window
},
fallback_for_transparency = {
bg = "#000000", -- fallback when transparent
fg = "#FFFFFF",
},
highlights = {
undo = {
hl = "UgUndo", -- highlight group name
hl_color = { bg = "#FF5555" }
},
-- ... other operations
},
priority = 4096, -- extmark priority
performance = {
color_cache_size = 1000,
debounce_delay = 50,
animation_skip_unchanged = true,
},
logging = {
level = "INFO", -- TRACE, DEBUG, INFO, WARN, ERROR, OFF
notify = true, -- show in notifications
file = false, -- write to log file
file_path = nil, -- custom log path
},
}Optimize the plugin for your machine:
performance = {
color_cache_size = 1000, -- Higher = faster, more memory
}- Fast machines: Increase to 2000+
- Slow machines: Decrease to 500
performance = {
debounce_delay = 50, -- milliseconds
}- Responsive: Lower values (25-50ms)
- Performance: Higher values (100-200ms)
performance = {
animation_skip_unchanged = true, -- Skip redundant frames
}Set to false only for debugging.
Configure detailed logging for debugging:
logging = {
level = "DEBUG", -- Show detailed info
notify = true, -- Display in Neovim
file = true, -- Write to file
file_path = "/tmp/undo-glow.log",
}Log Levels:
TRACE- Everything (very verbose)DEBUG- Detailed debuggingINFO- General info (default)WARN- Warnings onlyERROR- Errors onlyOFF- No logging
Create your own highlight commands:
Automatically detect and highlight changed text:
function my_custom_action()
require("undo-glow").highlight_changes({
hlgroup = "UgUndo",
animation = { animation_type = "pulse" }
})
-- Your action that modifies text
vim.cmd("normal! diw")
end
vim.keymap.set("n", "<leader>x", my_custom_action)Highlight exact coordinates:
function highlight_current_word()
local pos = vim.fn.getpos(".")
local word_start = vim.fn.searchpos("\\<", "bn", pos[2])[2]
local word_end = vim.fn.searchpos("\\>", "n", pos[2])[2]
require("undo-glow").highlight_region({
hlgroup = "UgSearch",
s_row = pos[2] - 1,
s_col = word_start - 1,
e_row = pos[2] - 1,
e_col = word_end,
})
endFor plugin developers and power users who want to extend functionality:
Intercept and modify plugin behavior:
local api = require("undo-glow.api")
-- Run before any highlight operation
api.register_hook("pre_highlight", function(data)
print("About to highlight:", data.operation)
-- Modify the highlight color
if data.operation == "undo" then
data.hl_color = { bg = "#FF0000" } -- Override the background color
-- data.hlgroup = "TermCursor" -- Use other group
-- Or set the highlight group directly:
-- vim.api.nvim_set_hl(0, "UgUndo", { bg = "#FF0000" })
end
end, 100) -- priority (higher = runs first)Available Hooks:
on_config_change- Configuration updatespre_highlight/post_highlight- All highlight operationspre_animation/post_animation- Animation lifecycleon_error- Error handlingpre_highlight_setup/post_highlight_setup- Highlight group creation
Hook Data Modifications:
data.hl_color- Override the highlight color (takes precedence over config)data.hlgroup- Change the highlight group used- Other fields like
data.operationare read-only
Subscribe to plugin events:
local api = require("undo-glow.api")
-- Track command usage
api.subscribe("command_executed", function(data)
print("Command:", data.command)
print("Operation:", data.opts.operation)
end)
-- Monitor configuration changes
api.subscribe("config_changed", function(data)
print("Config updated!")
print("Changes:", vim.inspect(data.changes))
end)
-- Handle errors
api.subscribe("log_message", function(data)
if data.level == "ERROR" then
print("Error:", data.message)
end
end)Available Events:
command_executed- Command operationsconfig_changed/config_error- Configuration lifecyclebuffer_changed- Text modificationslog_message- Logging eventscolor_conversion/color_cache_hit- Color processing
Change settings at runtime:
local api = require("undo-glow.api")
-- Build and apply new configuration
api.config_builder()
:animation({
enabled = true,
duration = 500,
animation_type = "spring"
})
:performance({
debounce_delay = 100
})
:build() -- Applies immediately
-- Listen for changes
api.subscribe("config_changed", function(data)
print("New config:", vim.inspect(data.new_config))
end)Customize behavior per operation type:
local api = require("undo-glow.api")
-- Different animations for different operations
api.register_hook("pre_animation", function(data)
local search_ops = { "search_next", "search_prev", "search_star", "search_hash" }
if vim.tbl_contains(search_ops, data.operation) then
data.animation_type = "rainbow"
elseif data.operation == "cursor_moved" then
data.animation_type = "spring"
elseif data.operation == "undo" then
data.animation_type = "pulse"
end
end)
-- Different colors per operation
api.register_hook("pre_highlight", function(data)
if data.operation == "undo" then
data.hl_color = { bg = "#4A90E2" }
elseif data.operation == "search_next" then
data.hl_color = { bg = "#50C878" }
end
end)Available Operations:
undo,redo- Undo/redoyank- Copypaste_below,paste_above- Pastesearch_next,search_prev,search_star,search_hash,search_cmd- Searchcomment,comment_textobject,comment_line- Commentscursor_moved- Cursor movementyanky_paste,substitute_paste- Plugin integrations
Create your own animation effects:
local api = require("undo-glow.api")
-- Register custom animation
api.register_animation("my_bounce", function(opts)
-- Step 1: Create extmark for highlighting (REQUIRED!)
local extmark_opts = require("undo-glow.utils").create_extmark_opts({
bufnr = opts.bufnr,
hlgroup = opts.hlgroup,
s_row = opts.coordinates.s_row,
s_col = opts.coordinates.s_col,
e_row = opts.coordinates.e_row,
e_col = opts.coordinates.e_col,
priority = require("undo-glow.config").config.priority,
force_edge = opts.state.force_edge,
window_scoped = opts.state.animation.window_scoped,
})
-- Step 2: Set the extmark
local extmark_id = vim.api.nvim_buf_set_extmark(
opts.bufnr,
opts.ns,
opts.coordinates.s_row,
opts.coordinates.s_col,
extmark_opts
)
-- Step 3: Add to extmark list
table.insert(opts.extmark_ids, extmark_id)
-- Step 4: Animate
require("undo-glow.animation").animate_start(opts, function(progress)
local bounce = math.abs(math.sin(progress * math.pi * 4))
return {
bg = string.format("#%02X%02X%02X",
math.floor(255 * bounce),
math.floor(100 * (1 - bounce)),
math.floor(50 * bounce)
)
}
end)
end)
-- Use it
require("undo-glow").setup({
animation = {
enabled = true,
animation_type = "my_bounce"
}
})Create custom easing for smooth animations:
-- Built-in easings
require("undo-glow").setup({
animation = {
easing = "in_out_cubic" -- or any other built-in
}
})Available Easings:
linear, in_quad, out_quad, in_out_quad, in_cubic, out_cubic, in_out_cubic, in_quart, out_quart, in_sine, out_sine, in_expo, out_expo, in_circ, out_circ, in_elastic, out_elastic, in_back, out_back, in_bounce, out_bounce
local function my_easing(opts)
-- opts.time is progress (0 to 1)
-- Return integer between 0 and opts.duration
return math.floor(opts.time * opts.time * opts.duration)
end
require("undo-glow").setup({
animation = {
easing = my_easing
}
})local api = require("undo-glow.api")
api.register_hook("post_highlight", function(data)
if data.operation == "undo" then
vim.fn.system("afplay /System/Library/Sounds/Blow.aiff &")
elseif data.operation == "redo" then
vim.fn.system("afplay /System/Library/Sounds/Glass.aiff &")
end
end)local api = require("undo-glow.api")
local stats = { undo = 0, redo = 0 }
api.subscribe("command_executed", function(data)
if data.command == "undo" then
stats.undo = stats.undo + 1
elseif data.command == "redo" then
stats.redo = stats.redo + 1
end
print(string.format("Undo: %d, Redo: %d", stats.undo, stats.redo))
end)local api = require("undo-glow.api")
api.register_hook("pre_highlight", function(data)
local ft = vim.bo.filetype
if ft == "lua" then
data.hl_color = { bg = "#4A90E2" } -- Blue for Lua
elseif ft == "python" then
data.hl_color = { bg = "#3776AB" } -- Python blue
elseif ft == "javascript" then
data.hl_color = { bg = "#F7DF1E" } -- JS yellow
end
end)local api = require("undo-glow.api")
api.register_hook("post_highlight", function(data)
if data.operation == "yank" and package.loaded.gitsigns then
-- Refresh git signs after yank
require("gitsigns").refresh()
end
end)Contributions are welcome! Please:
- Read the documentation carefully
- Check existing issues before creating new ones
- Test your changes thoroughly
- Follow the existing code style
Why choose undo-glow.nvim?
- β Fully configurable animations with custom easings
- β Exposed APIs for plugin developers
- β Per-operation configuration for colors and animations
- β Non-intrusive - no automatic keymaps
- β Library potential - use as foundation for other plugins
- β Thoroughly tested - core functionality tested
- β Easy plugin integration - seamless interop
- β Window-specific highlighting - works with splits
MIT License - see LICENSE file for details.