Productivity and Utility Plugins
These plugins address everyday productivity needs — note-taking, refactoring helpers, undo visualization, multi-cursor operations, and other utilities that make daily editing smoother.
1. obsidian.nvim — Zettelkasten Note-Taking
Obsidian.nvim turns Neovim into a first-class Obsidian-compatible note editor with wiki links, completions, templates, and search:
lua/plugins/productivity.lua
return {
{
"epwalsh/obsidian.nvim",
version = "*",
ft = "markdown",
dependencies = { "nvim-lua/plenary.nvim" },
opts = {
workspaces = {
{
name = "personal",
path = "~/notes/personal",
},
{
name = "work",
path = "~/notes/work",
},
},
daily_notes = {
folder = "journal/daily",
date_format = "%Y-%m-%d",
alias_format = "%B %-d, %Y",
default_tags = { "daily-notes" },
template = "templates/daily.md",
},
completion = {
nvim_cmp = true,
min_chars = 2,
},
mappings = {
["gf"] = {
action = function()
return require("obsidian").util.gf_passthrough()
end,
opts = { noremap = false, expr = true, buffer = true },
},
["<cr>"] = {
action = function()
return require("obsidian").util.smart_action()
end,
opts = { buffer = true, expr = true },
},
["<C-Space>"] = {
action = function()
return require("obsidian").util.toggle_checkbox()
end,
opts = { buffer = true },
},
},
new_notes_location = "notes_subdir",
wiki_link_func = "use_alias_only",
preferred_link_style = "wiki",
disable_frontmatter = false,
note_frontmatter_func = function(note)
local out = { id = note.id, aliases = note.aliases, tags = note.tags }
if note.metadata ~= nil and not vim.tbl_isempty(note.metadata) then
for k, v in pairs(note.metadata) do
out[k] = v
end
end
return out
end,
templates = {
folder = "templates",
date_format = "%Y-%m-%d",
time_format = "%H:%M",
substitutions = {},
},
note_id_func = function(title)
local suffix = ""
if title ~= nil then
suffix = title:gsub(" ", "-"):gsub("[^A-Za-z0-9-]", ""):lower()
else
for _ = 1, 4 do suffix = suffix .. string.char(math.random(65, 90)) end
end
return tostring(os.time()) .. "-" .. suffix
end,
ui = {
enable = true,
update_debounce = 200,
max_file_length = 5000,
checkboxes = {
[" "] = { char = "", hl_group = "ObsidianTodo" },
["x"] = { char = "", hl_group = "ObsidianDone" },
[">"] = { char = "", hl_group = "ObsidianRightArrow" },
["~"] = { char = "", hl_group = "ObsidianTilde" },
["!"] = { char = "", hl_group = "ObsidianImportant" },
},
bullets = { char = "•", hl_group = "ObsidianBullet" },
external_link_icon = { char = "", hl_group = "ObsidianExtLinkIcon" },
reference_text = { hl_group = "ObsidianRefText" },
highlight_text = { hl_group = "ObsidianHighlightText" },
tags = { hl_group = "ObsidianTag" },
block_ids = { hl_group = "ObsidianBlockID" },
hl_groups = {
ObsidianTodo = { bold = true, fg = "#f78c6c" },
ObsidianDone = { bold = true, fg = "#89ddff" },
ObsidianRightArrow = { bold = true, fg = "#f78c6c" },
ObsidianTilde = { bold = true, fg = "#ff5370" },
ObsidianImportant = { bold = true, fg = "#d73128" },
ObsidianBullet = { bold = true, fg = "#89ddff" },
ObsidianRefText = { underline = true, fg = "#c792ea" },
ObsidianExtLinkIcon = { fg = "#c792ea" },
ObsidianTag = { italic = true, fg = "#89ddff" },
ObsidianBlockID = { italic = true, fg = "#89ddff" },
ObsidianHighlightText = { bg = "#75662e" },
},
},
},
keys = {
{ "<leader>on", "<cmd>ObsidianNew<cr>", desc = "New Obsidian note" },
{ "<leader>oo", "<cmd>ObsidianOpen<cr>", desc = "Open in Obsidian app" },
{ "<leader>of", "<cmd>ObsidianSearch<cr>", desc = "Search notes" },
{ "<leader>oq", "<cmd>ObsidianQuickSwitch<cr>", desc = "Quick switch note" },
{ "<leader>od", "<cmd>ObsidianDailies<cr>", desc = "Daily notes" },
{ "<leader>ot", "<cmd>ObsidianTemplate<cr>", desc = "Insert template" },
{ "<leader>ol", "<cmd>ObsidianLinks<cr>", desc = "Show links" },
{ "<leader>ob", "<cmd>ObsidianBacklinks<cr>", desc = "Show backlinks" },
{ "<leader>oL", "<cmd>ObsidianLinkNew<cr>", desc = "Link to new note" },
{ "<leader>op", "<cmd>ObsidianPasteImg<cr>", desc = "Paste image" },
{ "<leader>ox", "<cmd>ObsidianExtractNote<cr>", mode = "v", desc = "Extract to new note" },
},
},
}
2. inc-rename.nvim — Live Preview Rename
Shows a preview of renamed symbols as you type (before confirming):
{
"smjonas/inc-rename.nvim",
cmd = "IncRename",
config = function()
require("inc_rename").setup({
input_buffer_type = "dressing", -- use dressing.nvim for input
preview_empty_name = false,
show_message = true,
save_in_cmdline_history = true,
post_hook = nil,
})
vim.keymap.set("n", "<leader>lR", function()
return ":IncRename " .. vim.fn.expand("<cword>")
end, { expr = true, desc = "Rename (inc-rename)" })
end,
},
3. undotree — Visual Undo History
Visualize Neovim's branching undo tree:
{
"mbbill/undotree",
cmd = "UndotreeToggle",
keys = {
{ "<leader>uu", "<cmd>UndotreeToggle<cr>", desc = "Toggle undo tree" },
},
init = function()
vim.g.undotree_WindowLayout = 2 -- layout: 1-4
vim.g.undotree_SplitWidth = 35
vim.g.undotree_DiffpanelHeight = 10
vim.g.undotree_SetFocusWhenToggle = 1
vim.g.undotree_ShortIndicators = 0
vim.g.undotree_HelpLine = 1
vim.g.undotree_DiffAutoOpen = 1
vim.g.undotree_TreeNodeShape = "*"
vim.g.undotree_TreeVertShape = "|"
vim.g.undotree_TreeSplitShape = "/"
vim.g.undotree_TreeReturnShape = "\\"
vim.g.undotree_RelativeTimestamp = 1
end,
},
4. nvim-treesitter-context — Sticky Function Header
Shows the function/class context at the top of the screen when scrolled inside a long function:
{
"nvim-treesitter/nvim-treesitter-context",
event = { "BufReadPost", "BufNewFile" },
opts = {
enable = true,
max_lines = 3, -- max sticky context lines
min_window_height = 0,
line_numbers = true,
multiline_threshold = 20,
trim_scope = "outer",
mode = "cursor", -- cursor | topline
separator = nil,
zindex = 20,
on_attach = nil,
},
keys = {
{ "<leader>uc", "<cmd>TSContextToggle<cr>", desc = "Toggle treesitter context" },
{ "[C", function() require("treesitter-context").go_to_context() end, desc = "Go to context" },
},
},
5. persistence.nvim — Lightweight Session Management
{
"folke/persistence.nvim",
event = "BufReadPre",
opts = {
dir = vim.fn.expand(vim.fn.stdpath("state") .. "/sessions/"),
options = { "buffers", "curdir", "tabpages", "winsize", "help", "globals", "skiprtp" },
pre_save = nil,
save_empty = false,
},
keys = {
{ "<leader>qs", function() require("persistence").load() end, desc = "Restore session" },
{ "<leader>qS", function() require("persistence").select() end, desc = "Select session" },
{ "<leader>ql", function() require("persistence").load({ last = true }) end, desc = "Restore last session" },
{ "<leader>qd", function() require("persistence").stop() end, desc = "Don't save session" },
},
},
6. vim-visual-multi — Multiple Cursors
The most stable multi-cursor plugin for Neovim:
{
"mg979/vim-visual-multi",
event = { "BufReadPost", "BufNewFile" },
init = function()
vim.g.VM_maps = {
["Find Under"] = "<C-n>", -- select word, add cursor
["Find Subword Under"] = "<C-n>",
["Select All"] = "<C-A>",
["Add Cursor Down"] = "<C-Down>",
["Add Cursor Up"] = "<C-Up>",
["Undo"] = "u",
["Redo"] = "<C-r>",
}
vim.g.VM_theme = "ocean"
vim.g.VM_highlight_matches = "underline"
vim.g.VM_show_warnings = 0
vim.g.VM_silent_exit = 1
end,
},
Multi-cursor Usage
<C-n> → select word under cursor, add cursor to each next match
<C-n> ... → keep pressing to add more
<C-A> → select ALL occurrences at once
In multi-cursor mode:
n / N → go to next/prev match
q → skip this match
Q → remove cursor from this match
c → change all occurrences
7. neogen — Auto-generate Documentation Comments
{
"danymat/neogen",
dependencies = { "nvim-treesitter/nvim-treesitter" },
cmd = "Neogen",
keys = {
{ "<leader>cn", function() require("neogen").generate() end, desc = "Generate docstring" },
},
opts = {
snippet_engine = "luasnip",
enabled = true,
languages = {
lua = { template = { annotation_convention = "emmylua" } },
python = { template = { annotation_convention = "google_docstrings" } },
javascript = { template = { annotation_convention = "jsdoc" } },
typescript = { template = { annotation_convention = "tsdoc" } },
php = { template = { annotation_convention = "phpdoc" } },
},
},
},
Cursor on a function/class → :Neogen or <leader>cn
→ auto-generates:
Python: Google-style docstring with Args: Returns: sections
JS/TS: JSDoc /** @param */ block
PHP: PHPDoc /** @param @return */ block
Lua: EmmyLua ---@param ---@return annotations
8. vim-illuminate Alternatives: Highlight Word Plugins
-- mini.cursorword: highlight word under cursor everywhere
{
"echasnovski/mini.cursorword",
version = false,
event = { "BufReadPost", "BufNewFile" },
opts = { delay = 100 },
},
Plugin Quick Reference
| Plugin | Purpose | Key trigger |
|---|---|---|
obsidian.nvim | Zettelkasten notes | <leader>o prefix |
inc-rename | Live symbol rename | <leader>lR |
undotree | Visual undo history | <leader>uu |
treesitter-context | Sticky context header | auto-on |
persistence.nvim | Session save/restore | <leader>qs |
vim-visual-multi | Multi-cursor | <C-n> |
neogen | Docstring generation | <leader>cn |
todo-comments | TODO highlighting | ]t / [t |
spectre | Project-wide find/replace | <leader>sr |
twilight.nvim | Focus mode (dim inactive code) | <leader>tw |
zen-mode.nvim | Distraction-free writing | <leader>zz |
Zen Mode + Twilight — Focus Mode
{
"folke/zen-mode.nvim",
cmd = "ZenMode",
keys = { { "<leader>zz", "<cmd>ZenMode<cr>", desc = "Zen mode" } },
opts = {
window = {
backdrop = 0.95,
width = 100,
height = 1,
options = {},
},
plugins = {
options = { enabled = true, ruler = false, showcmd = false, laststatus = 0 },
twilight = { enabled = true },
gitsigns = { enabled = false },
tmux = { enabled = false },
},
},
},
{
"folke/twilight.nvim",
cmd = { "Twilight", "TwilightEnable", "TwilightDisable" },
keys = { { "<leader>tw", "<cmd>Twilight<cr>", desc = "Toggle twilight" } },
opts = {
dimming = { alpha = 0.25, color = { "Normal", "#ffffff" }, term_bg = "#000000", inactive = false },
context = 10,
treesitter = true,
expand = { "function", "method", "table", "if_statement" },
exclude = {},
},
},