Dossier - nvim-ultimate-dossier

Chapter 1: Introduction to Vim and Neovim

1.1 History and Philosophy

The Birth of Vi

To understand Vim and Neovim, we must first travel back to 1976, when Bill Joy created vi (visual) at the University of California, Berkeley. At the time, most text editors were line-oriented, meaning you could only edit one line at a time. Vi revolutionized text editing by allowing users to see and edit multiple lines simultaneously on their screen—a “visual” mode of editing that seems obvious today but was groundbreaking then.

Vi was built on top of ex, a line editor that itself descended from ed, the standard Unix text editor. This lineage explains why Vim still has an “Ex mode” and why many commands begin with a colon (:). These Ex commands are the DNA of vi, preserved through generations of evolution.

Vim: Vi IMproved

In 1991, Bram Moolenaar wanted to use vi on his Amiga computer but found no suitable port. So he created one, initially called “Vi IMitation.” As he added more features, the name evolved to “Vi IMproved”—Vim. The first public release came in November 1991 (Vim 1.14).

What started as a simple port became something far more powerful:

  • Multiple levels of undo (original vi only had one level)

  • Syntax highlighting for hundreds of languages

  • Split windows for viewing multiple files

  • Visual mode for selecting text

  • Plugin system for extensibility

  • Scripting language (VimScript/VimL) for customization

  • Extensive help system built into the editor

Over three decades, Vim became one of the most popular text editors in the world, beloved by programmers, system administrators, and writers. Bram Moolenaar maintained Vim until his passing in August 2023, leaving behind an incredible legacy.

The Neovim Revolution

In 2014, Thiago de Arruda started Neovim as a fork of Vim with ambitious goals:

  1. Refactor the codebase to make it more maintainable

  2. First-class support for embedding Neovim as a library

  3. Built-in terminal emulator

  4. Asynchronous job control

  5. Lua as a first-class scripting language

  6. Better default settings out of the box

  7. Modern development practices (continuous integration, better testing)

The Neovim project wasn’t born from animosity toward Vim, but from a desire to modernize the codebase while preserving what made Vim great. The motto became: “Vim, but better.”

The Philosophical Divide

Both Vim and Neovim share the same core philosophy:

Modal Editing: Unlike most editors where keys directly insert characters, Vim and Neovim separate editing into distinct modes. In Normal mode, keys are commands; in Insert mode, they insert text. This separation allows for powerful editing commands without endless keyboard shortcuts.

Composability: Commands are built from simple, composable parts. d means “delete,” w means “word,” so dw deletes a word. 3dw deletes three words. This grammar-like structure makes Vim incredibly powerful once you learn its vocabulary.

Text as Data: Vim treats text editing as data manipulation. Macros, registers, and repeatable operations make complex edits simple and automatable.

Where they diverge:

  • Vim prioritizes backward compatibility and stability. Changes are conservative, and existing scripts should continue working.

  • Neovim prioritizes modern features and developer experience. Breaking changes are acceptable if they lead to a better future.

Why This Matters

Understanding this history helps you appreciate both editors:

  • Vim is mature, stable, and available everywhere. If you SSH into a server, Vim is likely there.

  • Neovim is innovative, extensible, and has a thriving plugin ecosystem leveraging Lua and modern APIs.

Both are actively developed. Both are excellent. This book focuses more on Neovim because:

  1. It represents the future of modal editing

  2. Its Lua integration makes plugin development more accessible

  3. Its built-in LSP client brings IDE-like features natively

  4. Its architecture is cleaner for learning concepts

But everything you learn applies to both—they share 95% of their DNA.

1.2 Vim vs Neovim: Key Differences

Architectural Differences

Codebase

  • Vim: Original C codebase, over 30 years of accumulated code. Approximately 450,000 lines.

  • Neovim: Refactored C codebase with aggressive removal of legacy code. Cleaner separation of concerns.

Scripting Languages

  • Vim: VimScript (VimL) is the primary scripting language. Lua support added in Vim 8.2+ but secondary.

  • Neovim: Both VimScript and Lua are first-class. Lua is preferred and faster. Most new plugins use Lua.

Asynchronous Operations

  • Vim: Added async support in Vim 8.0 (2016) via jobs and channels.

  • Neovim: Built with async in mind from the start. Uses libuv for async I/O.

Embedding

  • Vim: Not designed to be embedded.

  • Neovim: Built as a library that can be embedded in other applications. Remote UI protocol allows GUI clients.

Feature Differences

Feature Vim Neovim
Built-in Terminal :terminal (Vim 8.1+) :terminal with better implementation
LSP Client Requires plugin Built-in (vim.lsp.*)
Tree-sitter Not available Built-in for better syntax highlighting
Lua API Limited Extensive (vim.api, vim.fn, vim.loop)
Configuration File ~/.vimrc or ~/.vim/vimrc ~/.config/nvim/init.vim or init.lua
Plugin Directory ~/.vim/ ~/.config/nvim/ or ~/.local/share/nvim/
Default Leader \ \ (but many use Space)
Line Numbers Off by default Off by default (but easy to enable)
Mouse Support Requires :set mouse=a Enabled by default
Clipboard Integration Requires configuration Better defaults
Floating Windows Popup windows (Vim 8.2+) Native floating windows with more features

Configuration Differences

File Locations

# Vim
~/.vimrc              # Main config file
~/.vim/               # Vim directory
~/.vim/autoload/      # Autoload scripts
~/.vim/plugin/        # Plugin files

# Neovim
~/.config/nvim/init.vim    # VimScript config
~/.config/nvim/init.lua    # Lua config (preferred)
~/.config/nvim/lua/        # Lua modules
~/.local/share/nvim/       # Data directory
~/.local/state/nvim/       # State files (undo, shada)

Sample Configurations

Vim (VimScript)

" ~/.vimrc
set number
set relativenumber
set expandtab
set shiftwidth=4
set tabstop=4

" Plugin management with vim-plug
call plug#begin('~/.vim/plugged')
Plug 'tpope/vim-fugitive'
Plug 'junegunn/fzf.vim'
call plug#end()

" Key mappings
nnoremap <leader>ff :Files<CR>
nnoremap <leader>fg :Rg<CR>

Neovim (VimScript)

" ~/.config/nvim/init.vim
set number
set relativenumber
set expandtab
set shiftwidth=4
set tabstop=4

" Plugin management with vim-plug
call plug#begin('~/.local/share/nvim/plugged')
Plug 'tpope/vim-fugitive'
Plug 'nvim-telescope/telescope.nvim'
call plug#end()

" Key mappings
nnoremap <leader>ff <cmd>Telescope find_files<cr>
nnoremap <leader>fg <cmd>Telescope live_grep<cr>

Neovim (Lua)


-- ~/.config/nvim/init.lua
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.expandtab = true
vim.opt.shiftwidth = 4
vim.opt.tabstop = 4


-- Plugin management with lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({
    "git", "clone", "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable", lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup({
  "tpope/vim-fugitive",
  {
    "nvim-telescope/telescope.nvim",
    dependencies = { "nvim-lua/plenary.nvim" }
  }
})


-- Key mappings
vim.keymap.set('n', '<leader>ff', '<cmd>Telescope find_files<cr>')
vim.keymap.set('n', '<leader>fg', '<cmd>Telescope live_grep<cr>')

API Differences

VimScript Function Call

" Vim and Neovim
:echo expand('%:p')
:call setline(1, 'Hello, World!')

Neovim Lua API


-- Neovim only
print(vim.fn.expand('%:p'))
vim.api.nvim_buf_set_lines(0, 0, 1, false, {'Hello, World!'})


-- Better performance with direct API calls
local current_file = vim.api.nvim_buf_get_name(0)

Performance Differences

Startup Time

  • Vim: Generally faster startup with minimal configuration

  • Neovim: Slightly slower due to Lua runtime initialization, but faster with optimized configs

Plugin Loading

  • Vim: Traditional runtime loading

  • Neovim: Lazy loading is more common and effective with modern plugin managers

Syntax Highlighting

  • Vim: Regex-based syntax highlighting

  • Neovim: Tree-sitter provides faster, more accurate highlighting

Default Behaviors

Neovim’s Better Defaults

" These are ON by default in Neovim but need to be set in Vim:
set autoindent          " Copy indent from current line when starting new line
set autoread            " Reload files changed outside of Vim
set backspace=indent,eol,start  " Backspace over everything in insert mode
set belloff=all         " Never ring the bell
set complete-=i         " Don't scan included files for completion
set display=lastline    " Show as much as possible of the last line
set encoding=utf-8      " Default encoding
set formatoptions=tcqj  " Better format options
set history=10000       " Longer command history
set hlsearch            " Highlight search matches
set incsearch           " Incremental search
set langnoremap         " Helps avoid mapping conflicts
set laststatus=2        " Always show status line
set listchars=tab:>\ ,trail:-,nbsp:+  " Better whitespace characters
set mouse=a             " Enable mouse support (not in Vim by default in terminals)
set nrformats=bin,hex   " Better number formats for <C-a>/<C-x>
set ruler               " Show cursor position
set sessionoptions-=options  " Don't save all options in sessions
set showcmd             " Show partial commands
set sidescroll=1        " Smoother horizontal scrolling
set smarttab            " Smart tab behavior
set tabpagemax=50       " More tab pages allowed
set tags=./tags;,tags   " Better tags file search
set ttyfast             " Faster terminal connection
set viminfo+=!          " Better viminfo
set wildmenu            " Command-line completion menu

When to Choose Which?

Choose Vim if:

  • You need maximum compatibility with older systems

  • You prefer conservative, battle-tested updates

  • You’re working on servers where Neovim isn’t installed

  • You have extensive VimScript customizations you don’t want to migrate

  • You prefer the traditional Unix philosophy of stability

Choose Neovim if:

  • You want modern IDE-like features (LSP, Tree-sitter)

  • You prefer Lua over VimScript for configuration

  • You want the latest features and active development

  • You’re building a new configuration from scratch

  • You appreciate better defaults out of the box

  • You want a more active plugin ecosystem

Use Both if:

  • You maintain systems with different editors installed

  • You want to learn both for maximum flexibility

  • You keep a shared configuration that works on both

Migration Path

If you’re coming from Vim to Neovim:

  1. Start with your existing Vim config: Neovim reads ~/.vimrc if ~/.config/nvim/init.vim doesn’t exist

  2. Gradually adopt Neovim features: Add LSP, Tree-sitter, Lua configs incrementally

  3. Use compatibility layers: Most Vim plugins work in Neovim unchanged

  4. Keep both installed: They can coexist peacefully


-- init.lua: Load your old vimrc first
vim.cmd('source ~/.vimrc')


-- Then add Neovim-specific features
require('neovim-specific-config')

1.3 Installation and Setup

Installing Vim

Linux

# Debian/Ubuntu
sudo apt update
sudo apt install vim

# For full features (Python, Ruby, etc.)
sudo apt install vim-gtk3  # or vim-gnome

# Fedora/RHEL/CentOS
sudo dnf install vim-enhanced

# Arch Linux
sudo pacman -S vim

# Check version and features
vim --version

macOS

# macOS comes with Vim, but it's often outdated
vim --version  # Check current version

# Install latest with Homebrew
brew install vim

# For GUI version (MacVim)
brew install macvim

Windows

  1. Download from vim.org

  2. Run the installer (gvim##.exe)

  3. Or use package managers:

# Chocolatey
choco install vim

# Scoop
scoop install vim

# Winget
winget install vim.vim

Building from Source

# Get the source
git clone https://github.com/vim/vim.git
cd vim/src

# Configure with desired features
./configure --with-features=huge \

            --enable-multibyte \

            --enable-python3interp=yes \

            --enable-rubyinterp=yes \

            --enable-luainterp=yes \

            --enable-perlinterp=yes \

            --enable-gui=gtk3 \

            --enable-cscope \

            --prefix=/usr/local

# Compile and install
make
sudo make install

Installing Neovim

Linux

# Ubuntu/Debian (stable version)
sudo apt install neovim

# For latest stable via PPA
sudo add-apt-repository ppa:neovim-ppa/stable
sudo apt update
sudo apt install neovim

# Fedora
sudo dnf install neovim

# Arch Linux
sudo pacman -S neovim

# AppImage (works on most Linux distros)
curl -LO https://github.com/neovim/neovim/releases/latest/download/nvim.appimage
chmod u+x nvim.appimage
./nvim.appimage

# Move to PATH
sudo mv nvim.appimage /usr/local/bin/nvim

macOS

# Homebrew
brew install neovim

# MacPorts
sudo port install neovim

Windows

# Chocolatey
choco install neovim

# Scoop
scoop install neovim

# Winget
winget install Neovim.Neovim

# Or download from GitHub releases
# https://github.com/neovim/neovim/releases

Building from Source

# Install dependencies (Ubuntu/Debian)
sudo apt install ninja-build gettext cmake unzip curl

# Clone repository
git clone https://github.com/neovim/neovim
cd neovim

# Checkout stable version (or use master for bleeding edge)
git checkout stable

# Build
make CMAKE_BUILD_TYPE=RelWithDebInfo
sudo make install

Verify Installation

# Check Neovim version
nvim --version

# Should show something like:
# NVIM v0.9.4
# Build type: Release
# LuaJIT 2.1.1692716794

Initial Configuration

Neovim Directory Structure

# Create Neovim config directory
mkdir -p ~/.config/nvim

# Create subdirectories for organization
mkdir -p ~/.config/nvim/lua
mkdir -p ~/.config/nvim/after/plugin
mkdir -p ~/.config/nvim/colors

First init.lua

Create ~/.config/nvim/init.lua:


-- Basic settings
vim.opt.number = true                -- Show line numbers
vim.opt.relativenumber = true        -- Show relative line numbers
vim.opt.mouse = 'a'                  -- Enable mouse support
vim.opt.ignorecase = true            -- Case insensitive search
vim.opt.smartcase = true             -- Unless search contains uppercase
vim.opt.hlsearch = false             -- Don't highlight all search matches
vim.opt.wrap = false                 -- Don't wrap lines
vim.opt.breakindent = true           -- Preserve indentation in wrapped text
vim.opt.tabstop = 4                  -- Tabs are 4 spaces
vim.opt.shiftwidth = 4               -- Indent by 4 spaces
vim.opt.expandtab = true             -- Use spaces instead of tabs
vim.opt.termguicolors = true         -- True color support


-- Set leader key to space
vim.g.mapleader = ' '
vim.g.maplocalleader = ' '


-- Basic keymaps
vim.keymap.set('n', '<leader>w', '<cmd>write<cr>', {desc = 'Save'})
vim.keymap.set('n', '<leader>q', '<cmd>quit<cr>', {desc = 'Quit'})


-- Navigate between windows
vim.keymap.set('n', '<C-h>', '<C-w>h')
vim.keymap.set('n', '<C-j>', '<C-w>j')
vim.keymap.set('n', '<C-k>', '<C-w>k')
vim.keymap.set('n', '<C-l>', '<C-w>l')

print("Neovim configuration loaded!")

Alternative: VimScript Configuration

Create ~/.config/nvim/init.vim:

" Basic settings
set number
set relativenumber
set mouse=a
set ignorecase
set smartcase
set nohlsearch
set nowrap
set breakindent
set tabstop=4
set shiftwidth=4
set expandtab
set termguicolors

" Set leader key
let mapleader = ' '
let maplocalleader = ' '

" Basic keymaps
nnoremap <leader>w :write<CR>
nnoremap <leader>q :quit<CR>

" Navigate between windows
nnoremap <C-h> <C-w>h
nnoremap <C-j> <C-w>j
nnoremap <C-k> <C-w>k
nnoremap <C-l> <C-w>l

echo "Neovim configuration loaded!"

Health Check

Neovim includes a health check system:

:checkhealth

This command checks:

  • Python provider setup

  • Ruby provider setup

  • Node.js provider setup

  • Clipboard integration

  • Terminal support

  • And more…

Fix any warnings or errors shown. Common issues:

# Install Python provider
pip3 install pynvim

# Install Node.js provider
npm install -g neovim

# Install Ruby provider
gem install neovim

Setting Up a Plugin Manager

We’ll use lazy.nvim, the modern plugin manager for Neovim.

Installing lazy.nvim

Add to your init.lua:


-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not (vim.uv or vim.loop).fs_stat(lazypath) then
  vim.fn.system({
    "git",
    "clone",
    "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable",
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)


-- Setup plugins
require("lazy").setup({

  -- Your plugins will go here
  {
    "folke/which-key.nvim",
    event = "VeryLazy",
    init = function()
      vim.o.timeout = true
      vim.o.timeoutlen = 300
    end,
    opts = {}
  },
  

  -- Colorscheme
  {
    "folke/tokyonight.nvim",
    lazy = false,
    priority = 1000,
    config = function()
      vim.cmd([[colorscheme tokyonight]])
    end,
  },
})

Managing Plugins

:Lazy              " Open lazy.nvim UI
:Lazy sync         " Install/update/clean plugins
:Lazy install      " Install missing plugins
:Lazy update       " Update plugins
:Lazy clean        " Remove unused plugins
:Lazy health       " Run health checks

Creating a Modular Configuration

For better organization, split your configuration:

File Structure: ~/.config/nvim/

├│── init.lua

├│── lua/

││ ├── config/

││ │ ├── options.lua

││ │ ├── keymaps.lua

││ │ └── autocmds.lua

││ └── plugins/

││ ├── init.lua

││ ├── telescope.lua

││ ├── treesitter.lua

││ └── lsp.lua

init.lua:


-- Load core configuration
require('config.options')
require('config.keymaps')
require('config.autocmds')


-- Bootstrap and load plugins
local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({
    "git", "clone", "--filter=blob:none",
    "https://github.com/folke/lazy.nvim.git",
    "--branch=stable", lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)

require("lazy").setup("plugins")

lua/config/options.lua:

local opt = vim.opt


-- Line numbers
opt.number = true
opt.relativenumber = true


-- Tabs and indentation
opt.tabstop = 4
opt.shiftwidth = 4
opt.expandtab = true
opt.autoindent = true


-- Search
opt.ignorecase = true
opt.smartcase = true
opt.hlsearch = false
opt.incsearch = true


-- Appearance
opt.termguicolors = true
opt.signcolumn = "yes"
opt.cursorline = true


-- Behavior
opt.mouse = "a"
opt.clipboard = "unnamedplus"
opt.undofile = true
opt.updatetime = 250
opt.timeoutlen = 300

lua/config/keymaps.lua:

local keymap = vim.keymap


-- Set leader
vim.g.mapleader = " "
vim.g.maplocalleader = " "


-- Save and quit
keymap.set("n", "<leader>w", "<cmd>write<cr>", { desc = "Save file" })
keymap.set("n", "<leader>q", "<cmd>quit<cr>", { desc = "Quit" })


-- Window navigation
keymap.set("n", "<C-h>", "<C-w>h", { desc = "Go to left window" })
keymap.set("n", "<C-j>", "<C-w>j", { desc = "Go to lower window" })
keymap.set("n", "<C-k>", "<C-w>k", { desc = "Go to upper window" })
keymap.set("n", "<C-l>", "<C-w>l", { desc = "Go to right window" })


-- Better visual mode indenting
keymap.set("v", "<", "<gv")
keymap.set("v", ">", ">gv")


-- Move lines up and down
keymap.set("n", "<A-j>", ":m .+1<cr>==", { desc = "Move line down" })
keymap.set("n", "<A-k>", ":m .-2<cr>==", { desc = "Move line up" })
keymap.set("v", "<A-j>", ":m '>+1<cr>gv=gv", { desc = "Move selection down" })
keymap.set("v", "<A-k>", ":m '<-2<cr>gv=gv", { desc = "Move selection up" })

1.4 First Steps: Understanding Modes

The most fundamental concept in Vim/Neovim is modal editing. Unlike traditional editors where every key press inserts a character, Vim has different modes for different tasks.

The Primary Modes

1. Normal Mode

This is the default mode when you open Vim. In Normal mode, keys are commands, not text input.

" You start here when opening Vim
" Press keys to execute commands:
j      " Move down one line
k      " Move up one line
dd     " Delete current line
yy     " Yank (copy) current line
p      " Paste after cursor
u      " Undo
<C-r>  " Redo (Ctrl+r)

To return to Normal mode from any other mode, press <Esc> or <C-[> (Control+[).

2. Insert Mode

This is where you actually type text, like in a normal editor.

Enter Insert mode from Normal mode:

i      " Insert before cursor
a      " Append after cursor
I      " Insert at beginning of line
A      " Append at end of line
o      " Open new line below
O      " Open new line above

In Insert mode, you can type normally. Return to Normal mode with <Esc>.

3. Visual Mode

For selecting text. There are three variants:

v      " Character-wise visual mode
V      " Line-wise visual mode
<C-v>  " Block-wise visual mode (Ctrl+v)

Once in Visual mode:

" Navigate to expand selection
h, j, k, l    " Expand in any direction
w, e, b       " Expand by word
$, 0          " Expand to line end/start

" Then perform operations
d      " Delete selection
y      " Yank (copy) selection
c      " Change selection (delete and enter insert mode)
>      " Indent selection
<      " Unindent selection

4. Command-Line Mode

For executing Ex commands, searches, and filters.

:      " Enter command-line mode for Ex commands
/      " Search forward
?      " Search backward

Examples:

:w              " Write (save) file
:q              " Quit
:wq             " Write and quit
:e file.txt     " Edit file.txt
:20             " Go to line 20
:%s/old/new/g   " Replace all 'old' with 'new'

5. Terminal Mode

Neovim’s built-in terminal emulator (also available in Vim 8.1+).

:terminal       " Open terminal in current window
:split | terminal  " Open terminal in horizontal split

In terminal mode:

<C-\><C-n>     " Return to Normal mode
i or a         " Return to Terminal mode from Normal mode

6. Replace Mode

Overwrites existing text instead of inserting.

R      " Enter Replace mode
<Esc>  " Return to Normal mode

Mode Indicators

Vim By default, Vim shows the current mode in the bottom-left:

– INSERT –

– VISUAL –

– REPLACE – (nothing shown in Normal mode)

Neovim Similar to Vim, but can be customized more easily.

To always see the mode in your statusline:

vim.opt.showmode = true  -- Default in both Vim and Neovim

The power of modal editing comes from composability. In Normal mode, you combine:

  1. Operators (actions)

  2. Motions (where to apply the action)

  3. Text objects (what to act upon)

Operators:

d    " Delete
c    " Change (delete and enter insert mode)
y    " Yank (copy)
>    " Indent
<    " Unindent
=    " Auto-indent

Motions:

w    " Next word
e    " End of word
b    " Beginning of word
$    " End of line
0    " Beginning of line
gg   " Top of file
G    " Bottom of file
}    " Next paragraph
{    " Previous paragraph

Text Objects:

iw   " Inner word
aw   " A word (includes surrounding space)
i"   " Inside double quotes
a"   " Around double quotes (includes quotes)
it   " Inside tag
at   " Around tag
ip   " Inner paragraph

Combining them:

dw    " Delete word
d$    " Delete to end of line
d2w   " Delete 2 words
di"   " Delete inside quotes
da"   " Delete around quotes (including quotes)
ciw   " Change inner word
yap   " Yank a paragraph
>ip   " Indent paragraph

The Grammar of Vim

Think of Vim commands as sentences:

[count] operator [count] motion/text-object

Examples:

3dw    " 3 times, delete a word
2dd    " 2 times, delete a line (dd = delete line)
d3w    " Delete 3 words (same as 3dw)
c2w    " Change 2 words
y5j    " Yank current line plus 5 lines down

Practice Exercise: Modal Workflow

Let’s walk through a common editing task:

Task: You have this text: The quick brown fox jumps over the lazy dog

You want to change “brown” to “red”:

  1. Navigate: Use /brown<Enter> to search for “brown”

  2. Change: Type ciw (change inner word)

  3. Insert: Type “red”

  4. Exit Insert: Press <Esc>

The full sequence: /brown<Enter>ciwred<Esc>

Another example: Delete everything inside parentheses:

Text:

print("Hello, World!")

Cursor on the line:

  1. Navigate to inside the parentheses: f( (find opening paren)

  2. Delete inside: di" (delete inside quotes)

Or more directly: f(ldi" (find paren, move right one char, delete inside quotes)

Or even simpler if cursor is anywhere on the line: /print<Enter>f"di"

Mode-Specific Best Practices

Normal Mode:

  • Stay in Normal mode as much as possible

  • Use motions instead of arrow keys

  • Learn to compose commands

  • Use . to repeat the last change

Insert Mode:

  • Get in, make your change, get out

  • Don’t use arrow keys (use <C-o> for one Normal mode command)

  • Use <C-w> to delete a word backward

  • Use <C-u> to delete to start of line

Visual Mode:

  • Use sparingly; often Normal mode commands are faster

  • Great for irregular selections

  • Perfect for block operations (<C-v>)

Command-Line Mode:

  • Use <Up> and <Down> to browse history

  • Use <C-r>" to paste from default register

  • Use <C-f> to open command-line window for editing

Your First Vim Session

Let’s put it all together. Open Neovim:

nvim practice.txt

Step 1: Enter Insert mode and type some text:

i
This is my first Neovim file.
I am learning modal editing.
It feels strange but powerful.
<Esc>

Step 2: Navigate (in Normal mode):

gg      " Go to top
j       " Down one line
$       " End of line
0       " Beginning of line
w       " Next word
b       " Previous word

Step 3: Make some edits:

" Go to the word 'strange'
/strange<Enter>

" Change it to 'different'
ciwdifferent<Esc>

" Go to end of second line and add text
j$aIt's starting to make sense.<Esc>

" Delete the third line
jdd

Step 4: Save and quit:

:wq

Understanding Why Modes Matter

Efficiency: Your fingers stay on the home row. No reaching for arrow keys, mouse, or complex Ctrl combinations.

Precision: Normal mode commands are surgical. You can express exactly what you want to do.

Repeatability: The . command repeats your last change. Combined with n (next search result), you can make identical edits across a file rapidly.

Composability: Learning 10 operators and 10 motions gives you 100 combinations, not 20 separate commands to memorize.

Example: Suppose you want to delete everything from your cursor to the next occurrence of “end”:

d/end<Enter>

This combines:

  • d (delete operator)

  • /end<Enter> (motion: to the next “end”)

No special “delete to search result” command needed—you just composed two concepts you already know.

Common Beginner Mistakes

1. Staying in Insert Mode Wrong approach: Enter Insert mode, use arrow keys to move, delete with Backspace. Right approach: Stay in Normal mode, use motions, enter Insert mode briefly.

2. Not Using Text Objects Wrong: dllll (delete and then press l four times) Right: dw (delete word)

3. Fighting the Modes Wrong: Getting frustrated that keys don’t type letters in Normal mode. Right: Embrace the modes. Normal is your home; Insert is a quick visit.

4. Ignoring the Dot Command Wrong: Manually repeating the same edit multiple times. Right: Make the edit once, then use . to repeat it.

5. Not Learning Motions Wrong: Using jjjjj to move down five lines. Right: Use 5j or relative line numbers with 5j.

Next Steps

Now that you understand modes:

  1. Practice switching between modes until it feels natural

  2. Learn the basic motions (h, j, k, l, w, e, b, $, 0)

  3. Learn the basic operators (d, c, y)

  4. Start combining them

  5. Gradually add text objects to your vocabulary

In the next chapters, we’ll dive deep into each of these concepts, building your Vim vocabulary until you’re thinking in Vim.


Chapter Summary:

In this chapter, you learned:

  • The history of vi, Vim, and Neovim

  • The philosophical differences between Vim and Neovim

  • Key architectural and feature differences

  • How to install and configure both editors

  • The fundamental concept of modal editing

  • The six primary modes and when to use each

  • How to think compositionally in Vim

  • Your first practical editing session

With this foundation, you’re ready to explore the powerful features that make Vim and Neovim beloved by millions of developers worldwide.


Chapter 2: Navigation Mastery

Navigation is the foundation of Vim efficiency. While other editors rely on arrow keys and mouse clicks, Vim provides a rich vocabulary of motion commands that keep your hands on the home row and your mind in a flow state. Master navigation, and everything else in Vim becomes exponentially more powerful.

2.1 Basic Navigations

The Home Row Movement: h, j, k, l

The most fundamental navigation commands:

h    " Move left (←)
j    " Move down (↓)
k    " Move up (↑)
l    " Move right (→)

Why these keys? When Bill Joy created vi, he used an ADM-3A terminal where these keys had arrow symbols on them. More importantly, they keep your fingers on the home row.

Mnemonic:

  • j looks like a down arrow

  • k is above j, so it goes up

  • h is on the left, goes left

  • l is on the right, goes right

Using counts:

5j    " Move down 5 lines
10k   " Move up 10 lines
3h    " Move left 3 characters
7l    " Move right 7 characters

Practice exercise: Open any file and navigate using only hjkl for 5 minutes. Resist the urge to use arrow keys.

Word-Wise Movement

Moving by words is much faster than character-by-character:

w     " Move to the beginning of the next word
W     " Move to the beginning of the next WORD (whitespace-separated)
e     " Move to the end of the current/next word
E     " Move to the end of the current/next WORD
b     " Move to the beginning of the previous word
B     " Move to the beginning of the previous WORD
ge    " Move to the end of the previous word
gE    " Move to the end of the previous WORD

Understanding word vs WORD:

# Example text:
my_variable = get_data(user.id)

# Using 'w' (word): stops at underscores and dots
# Cursor positions: my | _ | variable | = | get | _ | data | ( | user | . | id | )

# Using 'W' (WORD): stops only at whitespace
# Cursor positions: my_variable | = | get_data(user.id)

word: Delimited by non-keyword characters (punctuation, whitespace)

WORD: Delimited only by whitespace

Example with counts:

3w    " Move forward 3 words
2b    " Move backward 2 words
5e    " Move to the end of the 5th word forward

Practical use case:

Suppose you want to delete the next 3 words:

d3w   " Delete 3 words forward

Or change the next word:

cw    " Change word (delete word and enter insert mode)

Line-Wise Movement

0     " Move to the first column of the line (column 0)
^     " Move to the first non-blank character of the line
$     " Move to the end of the line
g_    " Move to the last non-blank character of the line

Example:

    print("Hello, World!")
#   ^                    $
#   |                    |
# First non-blank    End of line
0     " Takes you to the very beginning (before the spaces)
^     " Takes you to 'p' (first non-blank)
$     " Takes you after the exclamation mark
g_    " Takes you to the exclamation mark (last non-blank)

Combining with operators:

d$    " Delete from cursor to end of line
d0    " Delete from cursor to beginning of line
d^    " Delete from cursor to first non-blank character
c$    " Change from cursor to end of line (same as C)
y^    " Yank from cursor to first non-blank character

Line Number Movement

gg    " Go to the first line of the file
G     " Go to the last line of the file
{n}G  " Go to line number n (e.g., 42G goes to line 42)
:{n}  " Same as above (e.g., :42 goes to line 42)
{n}gg " Alternative to {n}G

Examples:

1G    " Go to line 1 (same as gg)
100G  " Go to line 100
:250  " Go to line 250

With relative line numbers enabled:


-- In your init.lua
vim.opt.relativenumber = true
vim.opt.number = true

Your screen shows: 3 ” some line 2 ” another line 1 ” yet another line 42 ” current line (absolute line number) 1 ” next line 2 ” line after that 3 ” three lines down

Now you can jump precisely:

5j    " Jump down 5 lines (you can see it's 5 lines away)
3k    " Jump up 3 lines

Percentage Movement

{n}%  " Go to n% through the file

Examples:

50%   " Go to the middle of the file
25%   " Go to 1/4 through the file
75%   " Go to 3/4 through the file

The special meaning of %:

When your cursor is on a bracket, parenthesis, or brace:

%     " Jump to the matching bracket/paren/brace

Example:

function example() {
  if (condition) {
    doSomething();
  }
}

With cursor on the opening { after example(), pressing % takes you to the matching } at the end of the function.

2.2 Jump Lists and Change Lists

Vim keeps track of where you’ve been and what you’ve changed. This creates a navigational history you can traverse.

Jump List

Every time you make a “jump” (movement that goes farther than one line), Vim records it.

What counts as a jump?

  • G, gg, {n}G (line number jumps)

  • / or ? (searching)

  • % (matching bracket)

  • ( or ) (sentence movement)

  • { or } (paragraph movement)

  • H, M, L (screen position jumps)

  • Opening a file

Jump list navigation:

<C-o>  " Jump to older position in jump list (go back)
<C-i>  " Jump to newer position in jump list (go forward)

Think of it like a web browser’s back/forward buttons.

Example workflow:

:e file1.txt     " Open file1
/search_term     " Search for something (creates a jump)
<C-o>            " Go back to previous position
gg               " Go to top (creates a jump)
<C-o>            " Go back
<C-o>            " Go back again
<C-i>            " Go forward

View the jump list:

:jumps

Output looks like: jump line col file/text 4 100 10 ~/file1.txt 3 42 5 ~/file2.txt 2 15 0 ~/file1.txt 1 88 12 ~/file3.txt >

The > indicates your current position.

Change List

Vim also tracks every position where you made a change.

g;    " Go to previous change position
g,    " Go to next change position

View the change list:

:changes

Practical use case:

You’re editing a function, make a change, then scroll around the file. To quickly return to where you were editing:

g;    " Jump back to last change

Jump to Definition (with tags)

If you have a tags file (generated by ctags or LSP):

<C-]>     " Jump to definition
<C-t>     " Jump back from definition
g<C-]>    " If multiple matches, show a list

" Preview definition without jumping
<C-w>}    " Open definition in preview window

Example workflow with LSP in Neovim:


-- In your LSP configuration
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, { desc = 'Go to definition' })
vim.keymap.set('n', 'gr', vim.lsp.buf.references, { desc = 'Go to references' })
vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, { desc = 'Go to implementation' })

Then:

gd      " Jump to definition (uses LSP)
<C-o>   " Jump back
gr      " See all references

2.3 Marks and Bookmarks

Marks are like bookmarks in your code. You can set a mark and instantly jump back to it.

Setting Marks

m{a-z}    " Set a mark in the current file (lowercase = file-local)
m{A-Z}    " Set a global mark (uppercase = across files)

Example:

ma    " Set mark 'a' at current cursor position
mA    " Set global mark 'A'

Jumping to Marks

'{mark}   " Jump to the line of {mark}
`{mark}   " Jump to the exact position (line and column) of {mark}

Examples:

'a    " Jump to line of mark a
`a    " Jump to exact position of mark a
'A    " Jump to line of global mark A
`A    " Jump to exact position of global mark A

Difference between ’ and `:

  • ' (single quote): Jumps to the first non-blank character of the line

  • ` (backtick): Jumps to the exact character position

Special Marks

Vim automatically sets some marks:

''    " Jump to position before the latest jump
``    " Jump to exact position before the latest jump
'.    " Jump to line of last change
`.    " Jump to exact position of last change
'^    " Jump to position where Insert mode was last stopped
`"    " Jump to position when last editing this file
`[    " Jump to beginning of last changed or yanked text
`]    " Jump to end of last changed or yanked text
`<    " Jump to beginning of last visual selection
`>    " Jump to end of last visual selection

Practical examples:

" You make an edit, then navigate away
" To return to where you were editing:
`.

" You paste some text
" To go to the beginning of what you just pasted:
`[

" To go to the end:
`]

" To select what you just pasted:
`[v`]

Viewing All Marks

:marks       " Show all marks
:marks aB    " Show only marks a and B

Deleting Marks

:delmarks a      " Delete mark a
:delmarks a-d    " Delete marks a, b, c, d
:delmarks!       " Delete all lowercase marks in current buffer

Practical Mark Usage

Use case 1: Comparing two sections of code

" At first section
ma

" Navigate to second section
/second_function<CR>

" Set another mark
mb

" Now you can jump between them
'a    " First section
'b    " Second section
'a    " Back to first

Use case 2: Refactoring across files

" In file1.txt
mA    " Set global mark A

" Open file2.txt
:e file2.txt

" Do some work...

" Jump back to file1.txt at mark A
`A

Use case 3: Quick navigation in a large file

" At the function you're working on
mf    " Mark 'f' for "function"

" At important variable declaration
mv    " Mark 'v' for "variable"

" At test section
mt    " Mark 't' for "test"

" Now jump around easily
'f    " Back to function
'v    " Check variable
't    " Check test
'f    " Back to function

2.4 Same-Line Navigation

f{char}   " Find next occurrence of {char} on current line
F{char}   " Find previous occurrence of {char} on current line
t{char}   " Till next {char} (cursor stops before the character)
T{char}   " Till previous {char}
;         " Repeat last f, t, F, or T command
,         " Repeat last f, t, F, or T command in opposite direction

Examples:

Line of text: const result = calculateTotal(price, quantity);

fc    " Find next 'c' → moves to 'c' in 'const'
;     " Repeat → moves to 'c' in 'calculate'
;     " Repeat → moves to 'c' in 'price'
,     " Reverse → moves back to 'c' in 'calculate'

fp    " Find next 'p' → moves to 'p' in 'price'
2fp   " Find 2nd occurrence of 'p' → moves to 'p' in 'price' (if first was earlier)

Using ‘t’ (till):

ct,   " Change till comma → deletes everything up to (but not including) comma
dt)   " Delete till closing paren

Line: print(“Hello, World!”)

Cursor at the start, type f" to find the quote, then dt" to delete till the closing quote: print(““)

Combining with Operators

This is where same-line navigation becomes powerful:

df,   " Delete from cursor through the next comma
dF,   " Delete from cursor back through the previous comma
ct.   " Change from cursor till (before) the next period
yf)   " Yank from cursor through the next closing paren

Real-world example:

Changing a function argument:

function example(oldParam, anotherParam, thirdParam) {

Cursor on ‘oldParam’, want to change it:

ciw   " Change inner word

Or if you want to delete everything up to the comma:

df,   " Delete through comma

Column Movement

{n}|  " Move to column n
0     " Move to column 0 (beginning of line)

Example:

50|   " Move to column 50

This is useful when working with formatted text or data files.

For more complex same-line searches, you can use:

/{pattern}\%{line}l

But typically f, F, t, T are sufficient for same-line navigation.

2.5 Screen Adjust

These commands reposition the screen without moving the cursor in the file:

zt    " Move current line to top of screen
zz    " Move current line to center of screen (mnemonic: 'z'oom to center)
zb    " Move current line to bottom of screen

Visual representation:

Before zt: 1 some line 2 another line 3 target line (cursor here) 4 next line 5 more lines

After zt: 3 target line (cursor here) ← now at top of screen 4 next line 5 more lines 6 … 7 …

Practical use:

You’re debugging and want to see what comes after a particular line:

/error_handler<CR>   " Find the error handler
zt                    " Put it at top of screen to see what follows

Or you want to see context before and after:

/critical_function<CR>
zz                     " Center it to see surrounding code

Screen Position Jumps

H     " Jump to the top (High) of the screen
M     " Jump to the middle (Middle) of the screen
L     " Jump to the bottom (Low) of the screen

With counts:

3H    " Jump to 3rd line from top of screen
5L    " Jump to 5th line from bottom of screen

Combining with operators:

dL    " Delete from cursor to bottom of screen
yH    " Yank from cursor to top of screen

2.6 Scroll

Smooth Scrolling

<C-e>  " Scroll down one line (Extra line appears at bottom)
<C-y>  " Scroll up one line (Yester line appears at top)
<C-d>  " Scroll down half a screen
<C-u>  " Scroll up half a screen
<C-f>  " Scroll forward one full screen (Forward)
<C-b>  " Scroll backward one full screen (Backward)

Mnemonic:

  • <C-e>: Extra line at bottom

  • <C-y>: Yester line at top

  • <C-d>: Down half page

  • <C-u>: Up half page

  • <C-f>: Forward full page

  • <C-b>: Backward full page

Customizing scroll amount:


-- In init.lua
vim.opt.scroll = 10  -- <C-d> and <C-u> scroll 10 lines instead of half-page

Smooth Scrolling Configuration

For a more pleasant scrolling experience:


-- Enable smooth scrolling in Neovim
vim.opt.scrolloff = 8        -- Keep 8 lines visible above/below cursor
vim.opt.sidescrolloff = 8    -- Keep 8 columns visible left/right of cursor

With scrolloff=8, the cursor never gets closer than 8 lines to the edge of the screen when scrolling.

Horizontal Scrolling

When wrap is off:

zh    " Scroll right (move screen to the left, revealing text on left)
zl    " Scroll left (move screen to the right, revealing text on right)
zH    " Scroll half a screen width right
zL    " Scroll half a screen width left

-- Disable line wrapping to use horizontal scroll
vim.opt.wrap = false

2.7 Paragraph and Section Movement

Paragraph Movement

{     " Jump to previous paragraph (or code block)
}     " Jump to next paragraph (or code block)

A “paragraph” is text separated by blank lines.

Example:

def function_one():
    print("First function")
    return True

def function_two():
    print("Second function")
    return False

def function_three():
    print("Third function")
    return None
{     " Jump to previous blank line (previous function)
}     " Jump to next blank line (next function)

Combining with operators:

d}    " Delete to end of paragraph
y{    " Yank to beginning of paragraph
c}    " Change to end of paragraph

Sentence Movement

(     " Jump to previous sentence
)     " Jump to next sentence

A sentence ends with ., !, or ? followed by whitespace or end of line.

Example: This is the first sentence. This is the second sentence. This is the third sentence.

)     " Next sentence
(     " Previous sentence

Useful in text editing:

das   " Delete a sentence
cis   " Change inner sentence

Section Movement

[[    " Jump to previous section (or function in some languages)
]]    " Jump to next section (or function in some languages)
[]    " Jump to previous end of section
][    " Jump to next end of section

What constitutes a “section” depends on the file type and syntax.

In C/C++: Sections are typically defined by { at column 0.

In Markdown: Sections are headings.

For more precise function navigation, use:

[m    " Jump to previous start of method (in some languages)
]m    " Jump to next start of method
[M    " Jump to previous end of method
]M    " Jump to next end of method

2.8 File Navigation and Buffer Jumping

The Alternate File

<C-^>  " Toggle between current and alternate file (same as :e #)

Example workflow:

:e file1.txt    " Open file1
:e file2.txt    " Open file2 (file1 becomes alternate)
<C-^>           " Toggle back to file1
<C-^>           " Toggle back to file2

This is incredibly useful for switching between header/implementation files, test files, etc.

Buffer Navigation

:ls           " List all buffers
:buffer {n}   " Go to buffer number n
:bnext        " Go to next buffer
:bprevious    " Go to previous buffer
:bfirst       " Go to first buffer
:blast        " Go to last buffer
:buffer {name} " Go to buffer by name (supports tab completion)

Shortcuts:

:bn    " Short for :bnext
:bp    " Short for :bprevious
:bf    " Short for :bfirst
:bl    " Short for :blast
:b#    " Go to alternate buffer (same as <C-^>)

Buffer by partial name:

:b fil<Tab>   " Tab completion to find buffer with 'fil' in name

Useful mappings:


-- In init.lua
vim.keymap.set('n', '[b', ':bprevious<CR>', { desc = 'Previous buffer' })
vim.keymap.set('n', ']b', ':bnext<CR>', { desc = 'Next buffer' })
vim.keymap.set('n', '<leader>bd', ':bdelete<CR>', { desc = 'Delete buffer' })
vim.keymap.set('n', '<leader>ba', ':%bd|e#|bd#<CR>', { desc = 'Delete all buffers except current' })

Window Navigation

When you have split windows:

<C-w>h    " Move to window on the left
<C-w>j    " Move to window below
<C-w>k    " Move to window above
<C-w>l    " Move to window on the right

<C-w>w    " Cycle through windows
<C-w>p    " Go to previous window
<C-w>t    " Go to top-left window
<C-w>b    " Go to bottom-right window

Better mappings (removes the Ctrl-w prefix):

vim.keymap.set('n', '<C-h>', '<C-w>h')
vim.keymap.set('n', '<C-j>', '<C-w>j')
vim.keymap.set('n', '<C-k>', '<C-w>k')
vim.keymap.set('n', '<C-l>', '<C-w>l')

Now you can use <C-h> directly instead of <C-w>h.

Tab Navigation

:tabnew       " Create new tab
:tabnext      " Go to next tab
:tabprevious  " Go to previous tab
:tabfirst     " Go to first tab
:tablast      " Go to last tab
:tabclose     " Close current tab
gt            " Go to next tab (in normal mode)
gT            " Go to previous tab (in normal mode)
{n}gt         " Go to tab number n

Better mappings:

vim.keymap.set('n', '<leader>tn', ':tabnew<CR>', { desc = 'New tab' })
vim.keymap.set('n', '<leader>tc', ':tabclose<CR>', { desc = 'Close tab' })
vim.keymap.set('n', '[t', ':tabprevious<CR>', { desc = 'Previous tab' })
vim.keymap.set('n', ']t', ':tabnext<CR>', { desc = 'Next tab' })

2.9 Search-Based Navigation

/pattern   " Search forward for pattern
?pattern   " Search backward for pattern
n          " Go to next match
N          " Go to previous match (opposite direction)

*          " Search forward for word under cursor
#          " Search backward for word under cursor
g*         " Search forward for partial word under cursor
g#         " Search backward for partial word under cursor

Example:

In a Python file, cursor on user:


*    " Finds next exact match of 'user'
#    " Finds previous exact match of 'user'
g*   " Finds 'user', 'username', 'user_id', etc.

Search Offset

You can specify where the cursor lands after a search:

/pattern/+2   " Search for pattern, land 2 lines below
/pattern/-3   " Search for pattern, land 3 lines above
/pattern/e    " Search for pattern, land at end of match
/pattern/e+1  " Search for pattern, land one char after end
/pattern/b    " Search for pattern, land at beginning (default)
/pattern/b+2  " Search for pattern, land 2 chars after beginning

Example:

/function/+1   " Find "function", move cursor 1 line down

This lands you inside the function body instead of on the function declaration.

Search Highlighting

:set hlsearch      " Highlight all search matches
:set nohlsearch    " Don't highlight search matches
:set incsearch     " Show matches as you type
:nohlsearch        " Temporarily turn off highlight (until next search)

Common mapping:


-- Clear search highlight with Esc in normal mode
vim.keymap.set('n', '<Esc>', ':nohlsearch<CR><Esc>', { silent = true })

Search History

/{pattern}   " Start a search
<Up>         " Previous search from history
<Down>       " Next search from history
/            " Start search, then press Up to cycle through history

View complete search history:

:history /    " or :history search

Case-Sensitive Search Control

:set ignorecase    " Case-insensitive search
:set smartcase     " Case-sensitive only if search contains uppercase

With these settings:

/test     " Matches: test, Test, TEST, TeSt
/Test     " Matches: Test only (because of uppercase)
/TEST     " Matches: TEST only

Force case sensitivity in a single search:

/\Cpattern    " Case-sensitive search (even with ignorecase on)
/\cpattern    " Case-insensitive search (even with noignorecase)

Count Matches

In Neovim/Vim 8.1+:

:%s/pattern//gn   " Count occurrences without replacing

This shows: X matches on Y lines

Or use a plugin:

" With vim-searchindex plugin (shows "match X of Y")
" Or in Neovim, configure:
vim.opt.shortmess:append("S")  -- Show search count in command line

Now when you search, you see: [1/5] (match 1 of 5).

2.10 Advanced Navigation Patterns

Pattern Matching in Navigation

You can use regex patterns in your navigation:

/\<word\>     " Search for exact word (word boundaries)
/\d\+         " Search for one or more digits
/^function    " Search for lines starting with "function"
/TODO\|FIXME  " Search for TODO or FIXME

Example - Navigate to next TODO:

/\cTODO       " Case-insensitive search for TODO
n             " Next TODO
n             " Next TODO
%     " Jump to matching parenthesis/bracket/brace
[{    " Jump to previous unmatched {
]}    " Jump to next unmatched }
[(    " Jump to previous unmatched (
])    " Jump to next unmatched )

Example:

function example() {
  if (condition) {
    doSomething();
  }
  return result;
}

Cursor inside doSomething():

[{    " Jump to opening { of if statement
[{    " Jump to opening { of function

Method/Function Navigation

Using built-in commands:

[m    " Jump to start of previous method/function
]m    " Jump to start of next method/function
[M    " Jump to end of previous method/function
]M    " Jump to end of next method/function

Better with plugins:

With nvim-treesitter-textobjects:

require'nvim-treesitter.configs'.setup {
  textobjects = {
    move = {
      enable = true,
      goto_next_start = {
        ["]m"] = "@function.outer",
        ["]]"] = "@class.outer",
      },
      goto_next_end = {
        ["]M"] = "@function.outer",
        ["]["] = "@class.outer",
      },
      goto_previous_start = {
        ["[m"] = "@function.outer",
        ["[["] = "@class.outer",
      },
      goto_previous_end = {
        ["[M"] = "@function.outer",
        ["[]"] = "@class.outer",
      },
    },
  },
}

Now ]m reliably jumps to the next function in any language Tree-sitter supports.

In Neovim with LSP:

vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, { desc = 'Previous diagnostic' })
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, { desc = 'Next diagnostic' })
vim.keymap.set('n', '[e', function()
  vim.diagnostic.goto_prev({ severity = vim.diagnostic.severity.ERROR })
end, { desc = 'Previous error' })
vim.keymap.set('n', ']e', function()
  vim.diagnostic.goto_next({ severity = vim.diagnostic.severity.ERROR })
end, { desc = 'Next error' })

Now you can jump between errors and warnings easily.

:cnext    " Go to next item in quickfix list
:cprevious " Go to previous item
:cfirst   " Go to first item
:clast    " Go to last item
:cnfile   " Go to first item in next file
:cpfile   " Go to first item in previous file

Shortcuts:

:cn    " :cnext
:cp    " :cprevious
:cf    " :cfirst
:cl    " :clast

Better mappings:

vim.keymap.set('n', '[q', ':cprevious<CR>', { desc = 'Previous quickfix' })
vim.keymap.set('n', ']q', ':cnext<CR>', { desc = 'Next quickfix' })
vim.keymap.set('n', '[Q', ':cfirst<CR>', { desc = 'First quickfix' })
vim.keymap.set('n', ']Q', ':clast<CR>', { desc = 'Last quickfix' })

Location List (Buffer-Local Quickfix)

:lnext      " Next location list item
:lprevious  " Previous location list item
:lfirst     " First location list item
:llast      " Last location list item

Mappings:

vim.keymap.set('n', '[l', ':lprevious<CR>', { desc = 'Previous location' })
vim.keymap.set('n', ']l', ':lnext<CR>', { desc = 'Next location' })

2.11 File Explorer Navigation

Netrw (Built-in File Explorer)

:Explore    " Open explorer in current window
:Sexplore   " Open explorer in horizontal split
:Vexplore   " Open explorer in vertical split
:Texplore   " Open explorer in new tab

Shortcuts:

:Ex     " :Explore
:Sex    " :Sexplore
:Vex    " :Vexplore

Inside Netrw:

<Enter>  " Open file/directory

-        " Go to parent directory
D        " Delete file
R        " Rename file
%        " Create new file
d        " Create new directory
i        " Change view style
s        " Change sort order

Better alternative: Use a modern file explorer plugin like nvim-tree or neo-tree.

nvim-tree example:

require("nvim-tree").setup()

vim.keymap.set('n', '<leader>e', ':NvimTreeToggle<CR>', { desc = 'Toggle file explorer' })

Then inside nvim-tree:

<Enter>  " Open file
h        " Collapse directory
l        " Expand directory
a        " Create file
d        " Delete file
r        " Rename file
y        " Copy name

2.12 Fuzzy Finding Navigation

Modern Neovim workflows heavily rely on fuzzy finders like Telescope.

Telescope.nvim

require('telescope').setup{}

local builtin = require('telescope.builtin')
vim.keymap.set('n', '<leader>ff', builtin.find_files, { desc = 'Find files' })
vim.keymap.set('n', '<leader>fg', builtin.live_grep, { desc = 'Live grep' })
vim.keymap.set('n', '<leader>fb', builtin.buffers, { desc = 'Find buffers' })
vim.keymap.set('n', '<leader>fh', builtin.help_tags, { desc = 'Help tags' })
vim.keymap.set('n', '<leader>fr', builtin.oldfiles, { desc = 'Recent files' })
vim.keymap.set('n', '<leader>fc', builtin.commands, { desc = 'Commands' })
vim.keymap.set('n', '<leader>fk', builtin.keymaps, { desc = 'Keymaps' })
vim.keymap.set('n', '<leader>fs', builtin.lsp_document_symbols, { desc = 'Document symbols' })
vim.keymap.set('n', '<leader>fw', builtin.lsp_workspace_symbols, { desc = 'Workspace symbols' })

Inside Telescope:

<C-n>    " Next item
<C-p>    " Previous item
<C-c>    " Close
<CR>     " Select
<C-x>    " Open in horizontal split
<C-v>    " Open in vertical split
<C-t>    " Open in new tab

This becomes your primary navigation method for:

  • Finding files by name

  • Searching file contents

  • Jumping to symbols/functions

  • Navigating buffers

  • Finding help

1. Think in Text Objects, Not Lines

Instead of:

jjjjdw   " Move down 4 lines, delete word

Think:

4jdw     " More intentional

Or better, if you’re targeting a specific word:

/word<CR>daw   " Search for word, delete it

2. Use Relative Line Numbers

vim.opt.relativenumber = true
vim.opt.number = true  -- Shows absolute number on current line

This lets you see exactly how many lines away things are.

3. Minimize Keystrokes

Instead of:

lllllll   " Move right 7 times

Use:

7l        " Move right 7 times

Or even better, if you’re targeting the end of line:

$         " End of line

Or a specific character:

fx        " Find next 'x'

4. Use Search for Long Distances

Instead of:

15j       " Move down 15 lines (requires counting)

Use:

/function_name<CR>   " Search for what you want

Search is often faster than counting lines.

5. Combine Navigation with Editing

Don’t navigate, then delete separately:

" Don't do this:
5j      " Navigate
dd      " Delete
" Do this instead:
d5j     " Delete down 5 lines

-- Quick jump to beginning/end of line
vim.keymap.set({'n', 'v'}, 'H', '^', { desc = 'Start of line' })
vim.keymap.set({'n', 'v'}, 'L', '$', { desc = 'End of line' })


-- Keep cursor centered when jumping
vim.keymap.set('n', 'n', 'nzzzv', { desc = 'Next search result' })
vim.keymap.set('n', 'N', 'Nzzzv', { desc = 'Previous search result' })
vim.keymap.set('n', '<C-d>', '<C-d>zz', { desc = 'Scroll down' })
vim.keymap.set('n', '<C-u>', '<C-u>zz', { desc = 'Scroll up' })


-- Join lines but keep cursor position
vim.keymap.set('n', 'J', 'mzJ`z', { desc = 'Join lines' })

7. Use Marks for Context Switching

When you’re about to navigate far away but want to come back:

ma              " Set mark 'a'
/some_pattern   " Navigate far away
...             " Do some work
'a              " Jump back

8. Learn the Quickfix Workflow

For navigating search results across files:

:vimgrep /pattern/ **/*.py   " Search all Python files
:copen                        " Open quickfix window

Then navigate with ]q and [q.

Exercise 1: Basic Movement

Open any code file and navigate using only these commands (no arrow keys!):

h j k l w e b 0 $ gg G

Goal: Navigate for 5 minutes without using arrow keys or mouse.

Exercise 2: Search Navigation

  1. Open a large file

  2. Search for a common word: /the<CR>

  3. Navigate through all occurrences using n and N

  4. Use * on a word and navigate through those occurrences

Exercise 3: Mark Usage

  1. Set mark a at the top of a function

  2. Navigate to the bottom of the file

  3. Set mark b

  4. Jump between them using 'a and 'b

  5. Try using `a and `b to see the difference

Exercise 4: Text Object Navigation

Practice these patterns:

di"    " Delete inside quotes
ci(    " Change inside parentheses
va}    " Visually select around braces
dap    " Delete a paragraph
cit    " Change inside HTML tag

Exercise 5: Combined Navigation

Delete from cursor to next occurrence of word “end”:

d/end<CR>

Change from cursor to the 3rd comma:

c3f,

Yank the current paragraph:

yap

2.15 Performance Tips

Lazy Redraw

When executing macros or complex operations:

vim.opt.lazyredraw = true  -- Don't redraw during macros

Efficient Searching

vim.opt.ignorecase = true
vim.opt.smartcase = true

This makes searching faster (case-insensitive) but still precise when needed.

Disable Mouse (for Faster Navigation Practice)

vim.opt.mouse = ""  -- Forces you to use keyboard navigation

Re-enable later:

vim.opt.mouse = "a"

Chapter Summary

In this chapter, you learned:

  • Basic navigation with hjkl, words, and lines

  • Jump lists and change lists for navigating your history

  • Marks for bookmarking positions

  • Same-line navigation with f, t, ;, and ,

  • Screen positioning with zt, zz, zb, H, M, L

  • Scrolling commands for viewing more code

  • Paragraph and section movement with {} and [[]]

  • Buffer, window, and tab navigation

  • Search-based navigation with /, ?, *, and #

  • Advanced patterns including LSP navigation and fuzzy finding

  • Best practices for efficient navigation

  • Practical exercises to build muscle memory

Navigation mastery is the foundation of Vim expertise. These commands become second nature with practice, and once they do, you’ll find yourself navigating code faster than you ever thought possible. In the next chapter, we’ll dive deep into editing fundamentals, where navigation combines with powerful text manipulation commands.


Chapter 3: Editing Fundamentals

Navigation gets you to the text you want to modify. Editing transforms it. In Vim, editing is where the philosophy of composability truly shines. You’ve already seen glimpses of it—operators like d, c, and y combined with motions. Now we’ll systematically explore the full editing vocabulary that makes Vim a text-editing language.

3.1 The Editing Grammar: Operators, Motions, and Text Objects

Vim’s editing commands follow a grammatical structure:

[count] [operator] [count] [motion/text-object]

Components:

  • Operator: The action to perform (d for delete, c for change, y for yank)

  • Motion: Where to apply the action (w for word, $ for end of line)

  • Text Object: What to act upon (iw for inner word, ap for a paragraph)

  • Count: How many times to repeat (optional)

Examples:

d2w      " Delete 2 words
c$       " Change to end of line
y3j      " Yank current line plus 3 below
2dd      " Delete 2 lines
3cw      " Change 3 words
gUiw     " Uppercase inner word

The Complete Operator Set

d        " Delete (cut)
c        " Change (delete and enter insert mode)
y        " Yank (copy)
~        " Toggle case
g~       " Toggle case (takes a motion)
gu       " Make lowercase
gU       " Make uppercase
>        " Indent right
<        " Indent left
=        " Auto-indent
!        " Filter through external program
gw       " Format text (wrap lines)
gq       " Format text (hard wrap)
g?       " ROT13 encode

Complete Motion Reference (Quick)

" Character-wise
h l 0 ^ $ f F t T ; , |

" Word-wise
w W e E b B ge gE

" Line-wise
j k + - _ gg G {n}G :{n}

" Paragraph/Section
{ } ( ) [[ ]] [] ][

" Search
/ ? n N * # g* g#

" Screen
H M L

" Marks
'{mark} `{mark}

Text Objects: The Inner and Around

Text objects are preceded by i (inner) or a (around):

iw    " Inner word (word only)
aw    " A word (word + trailing space)
iW    " Inner WORD
aW    " A WORD

is    " Inner sentence
as    " A sentence

ip    " Inner paragraph
ap    " A paragraph

i"    " Inner quotes (content between ")
a"    " Around quotes (includes quotes)
i'    " Inner single quotes
a'    " Around single quotes
i`    " Inner backticks
a`    " Around backticks

i(    " Inner parentheses (same as ib)
a(    " Around parentheses (same as ab)
i[    " Inner brackets
a[    " Around brackets
i{    " Inner braces (same as iB)
a{    " Around braces (same as aB)
i<    " Inner angle brackets
a<    " Around angle brackets

it    " Inner tag (HTML/XML)
at    " Around tag

Examples:

# Text: print("Hello, World!")
#       Cursor on 'H' in Hello

diw   " Delete 'Hello' → print(", World!")
daw   " Delete 'Hello, ' → print("World!")
di"   " Delete 'Hello, World!'print("")
da"   " Delete "Hello, World!"print()
di(   " Delete entire content → print()
da(   " Delete including parens → print
<!-- Text: <div class="container">Content here</div> -->
<!--       Cursor anywhere in 'Content here' -->

dit   " Delete 'Content here'
dat   " Delete <div class="container">Content here</div>
cit   " Change inner tag (delete content, enter insert mode)

Combining Operators and Text Objects

This is where Vim becomes incredibly powerful:

ciw     " Change inner word
dap     " Delete a paragraph
yit     " Yank inner HTML tag
gUaw    " Uppercase a word
>i{     " Indent inner braces
=a{     " Auto-indent a block
gqap    " Format a paragraph

Real-world example - Refactoring a function call:

result = calculate_total(price, quantity, tax_rate)

Cursor on calculate_total:

ciw     " Change function name
# Type new name
new_function<Esc>

Result:

result = new_function(price, quantity, tax_rate)

Or to change the first argument:

f(      " Find opening paren
w       " Move to first argument
ciw     " Change inner word

3.2 Basic Editing Commands

Insert and Append

i        " Insert before cursor
I        " Insert at beginning of line (first non-blank)
gi       " Insert at last insert position
gI       " Insert at column 1 (absolute beginning)

a        " Append after cursor
A        " Append at end of line

o        " Open new line below
O        " Open new line above

Examples:

    print("Hello")
#   ^cursor here

I        " Insert at beginning → cursor before 'p'
gI       " Insert at column 1 → cursor before indentation

Replace

r{char}  " Replace single character with {char}
R        " Enter Replace mode (like Insert, but overwrites)
gr{char} " Virtual replace (respects tabs)

Examples:

# Text: Hello World
#       ^cursor on H

rx       " Replace H with x → xello World
3r-      " Replace next 3 chars with - → ---lo World

Replace mode:

R        " Enter Replace mode
# Type 'Goodbye' (overwrites existing text)
<Esc>    " Exit Replace mode

Substitute

s        " Substitute character (delete char and enter insert mode)
S        " Substitute line (delete line and enter insert mode)

# With counts
3s       " Substitute 3 characters
2S       " Substitute 2 lines

Note: s is equivalent to cl (change letter), and S is equivalent to cc (change line).

Change

c{motion}   " Change (delete and enter insert mode)
cc          " Change entire line
C           " Change to end of line (same as c$)

# Examples
cw          " Change word
c2w         " Change 2 words
c$          " Change to end of line
c^          " Change to beginning of line
ciw         " Change inner word
cit         " Change inner tag
ci"         " Change inside quotes

Practical example - Changing a string:

const message = "Old message here";

Cursor on ‘O’ in “Old”:

ci"     " Change inside quotes
# Type new message
New message<Esc>

Result:

const message = "New message";

3.3 Delete Commands

Basic Deletion

x        " Delete character under cursor (like 'del' key)
X        " Delete character before cursor (like 'backspace')
d{motion}" Delete text defined by motion
dd       " Delete entire line
D        " Delete to end of line (same as d$)

# With counts
3x       " Delete 3 characters
2dd      " Delete 2 lines
d3w      " Delete 3 words

Delete with Motions

dw       " Delete word
de       " Delete to end of word
db       " Delete to beginning of word
d2w      " Delete 2 words
d$       " Delete to end of line
d^       " Delete to first non-blank character
d0       " Delete to beginning of line
dG       " Delete to end of file
dgg      " Delete to beginning of file
d}       " Delete to end of paragraph
d{       " Delete to beginning of paragraph

Delete with Text Objects

diw      " Delete inner word
daw      " Delete a word (including surrounding whitespace)
dis      " Delete inner sentence
das      " Delete a sentence
dip      " Delete inner paragraph
dap      " Delete a paragraph
di"      " Delete inside quotes
da"      " Delete around quotes (including quotes)
di(      " Delete inside parentheses
da(      " Delete around parentheses
dit      " Delete inner HTML tag
dat      " Delete around HTML tag
di{      " Delete inside braces
da{      " Delete around braces (including braces)

Practical examples:

def calculate(price, quantity):
    total = price * quantity
    return total

Cursor on calculate:

daw     " Delete 'calculate ' (function name with space)

Cursor inside the function body:

di{     " Delete all lines inside the function

Cursor on a parameter:

daw     " Delete the parameter and trailing comma/space

Delete to Character

df{char}  " Delete forward to and including {char}
dt{char}  " Delete forward till (before) {char}
dF{char}  " Delete backward to and including {char}
dT{char}  " Delete backward till (after) {char}

Example:

const result = getValue(param1, param2);

Cursor at start of line:

dt(      " Delete till opening paren → (param1, param2);
df)      " Delete through closing paren → ;

Delete Lines Matching Pattern

:g/pattern/d    " Delete all lines containing pattern
:g!/pattern/d   " Delete all lines NOT containing pattern
:v/pattern/d    " Same as above (v = inverse of g)

Examples:

:g/TODO/d       " Delete all lines with TODO
:g/^$/d         " Delete all empty lines
:g/^\s*$/d      " Delete all blank lines (including whitespace-only)
:v/import/d     " Delete all lines that don't contain 'import'

3.4 Copy (Yank) and Paste

Yank Commands

y{motion}   " Yank text defined by motion
yy          " Yank entire line (same as Y)
Y           " Yank entire line

# With counts
2yy         " Yank 2 lines
y3w         " Yank 3 words
y$          " Yank to end of line
yG          " Yank to end of file
ygg         " Yank to beginning of file

Yank with Text Objects

yiw      " Yank inner word
yaw      " Yank a word
yip      " Yank inner paragraph
yap      " Yank a paragraph
yi"      " Yank inside quotes
ya"      " Yank around quotes
yi(      " Yank inside parentheses
ya(      " Yank around parentheses
yit      " Yank inner HTML tag
yat      " Yank around HTML tag

Paste Commands

p        " Paste after cursor/below line
P        " Paste before cursor/above line
gp       " Paste and move cursor after pasted text
gP       " Paste before and move cursor after pasted text

# With counts
3p       " Paste 3 times
5P       " Paste 5 times above

Line-wise vs Character-wise paste:

If you yanked a full line with yy:

p        " Pastes below current line
P        " Pastes above current line

If you yanked characters/words with yw:

p        " Pastes after cursor
P        " Pastes before cursor

Paste from Different Registers

"ayy     " Yank line into register 'a'
"ap      " Paste from register 'a'
"bdiw    " Delete inner word into register 'b'
"bp      " Paste from register 'b'

"+y      " Yank to system clipboard
"+p      " Paste from system clipboard
"*y      " Yank to primary selection (X11)
"*p      " Paste from primary selection

Practical workflow:

" Yank function definition to register 'f'
"fyy

" Navigate elsewhere
/another_function<CR>

" Paste it
"fp

Paste in Insert Mode

<C-r>{register}    " Paste from register while in insert mode
<C-r>"             " Paste from default register
<C-r>+             " Paste from system clipboard
<C-r>0             " Paste from yank register (not delete)

Example:

i                  " Enter insert mode
<C-r>"             " Paste what was last yanked/deleted
<C-r>+             " Paste from system clipboard

Paste Over Selection

In visual mode:

v                  " Visual select
p                  " Paste, replacing selection

Important: After p in visual mode, the replaced text goes to the unnamed register. To avoid this:

" Use a specific register for yanking first
"ay        " Yank to register 'a'
v          " Visual select other text
"ap        " Paste from 'a', replacing selection

Or use the black hole register for deletion:

v          " Visual select
"_dP       " Delete to black hole, then paste

3.5 Undo and Redo

Basic Undo/Redo

u          " Undo last change
<C-r>      " Redo (undo the undo)
U          " Undo all changes on current line

# With counts
3u         " Undo 3 times
5<C-r>     " Redo 5 times

Undo Branches

Vim maintains a tree of changes, not just a linear history. If you undo and then make a new change, Vim doesn’t discard the undone changes—they become a branch.

g-         " Go to older text state
g+         " Go to newer text state
:earlier 5m    " Go to text state 5 minutes ago
:later 10s     " Go to text state 10 seconds later
:earlier 3     " Go back 3 changes
:later 2       " Go forward 2 changes

Time-based undo:

:earlier 1h    " Text as it was 1 hour ago
:earlier 30m   " 30 minutes ago
:earlier 2d    " 2 days ago
:later 5m      " 5 minutes later than current state

View Undo Tree

:undolist      " Show undo branches

Better visualization with plugin:


-- Using undotree plugin
vim.keymap.set('n', '<leader>u', ':UndotreeToggle<CR>', { desc = 'Toggle undo tree' })

Persistent Undo

Neovim can save undo history across sessions:

vim.opt.undofile = true
vim.opt.undodir = vim.fn.stdpath('cache') .. '/undo'

Now you can close Neovim, reopen a file, and still undo changes from previous sessions.

3.6 Repeat and Reverse

The Dot Command

The single most powerful editing command:

.          " Repeat last change

What counts as a “change”?

  • Any command that modified the buffer in normal mode

  • Everything typed in a single insert mode session

Examples:

# Change word:
ciw         " Change inner word
new_name<Esc>

# Move to another word and repeat:
w           " Move to next word
.           " Repeat the change → changes this word to 'new_name'

Delete line pattern:

dd          " Delete line
j           " Move down
.           " Delete next line
j.          " Move and delete
j.          " Move and delete

Append semicolon to multiple lines:

A;<Esc>     " Append semicolon and exit insert mode
j           " Move down
.           " Repeat (append semicolon)
j.j.j.      " Continue on multiple lines

Making Changes Repeatable

Structure your edits to be dot-repeatable:

Not ideal:

/target<CR>  " Search
dwi          " Delete word, enter insert
new_text<Esc>
n            " Next match
dwi          " Delete word again
new_text<Esc>  " Type same thing again

Better (repeatable):

/target<CR>  " Search
cwnew_text<Esc>  " Change word to 'new_text'
n            " Next match
.            " Repeat the change
n.n.n.       " Continue on other matches

Reverse Last f, t, F, T

;          " Repeat last f, t, F, or T
,          " Reverse last f, t, F, or T

Example:

const result = calculateTotal(price, quantity, tax);
f,         " Find first comma
.          " Not applicable (. doesn't work with motions)
;          " Find next comma
;          " Find next comma
,          " Go back to previous comma

But when combined with operators:

df,        " Delete to comma
;          " Find next comma
.          " Repeat delete to comma

Repeat Ex Commands

@:         " Repeat last Ex command
@@         " Repeat last @: (after first use)

Example:

:s/old/new/g    " Substitute in current line
j               " Move down
@:              " Repeat substitution
j@@             " Move down and repeat again

3.7 Visual Mode Editing

Entering Visual Mode

v          " Character-wise visual mode
V          " Line-wise visual mode
<C-v>      " Block-wise visual mode (column mode)
gv         " Reselect last visual selection

Visual Mode Operators

Once in visual mode, select text and apply operators:

d          " Delete selection
c          " Change selection
y          " Yank selection
~          " Toggle case
u          " Make lowercase
U          " Make uppercase
>          " Indent right
<          " Indent left
=          " Auto-indent
J          " Join lines
gq         " Format/wrap text

Example:

def calculate_total(price, quantity):
    subtotal = price * quantity
    tax = subtotal * 0.1
    total = subtotal + tax
    return total

Select the function body:

vi{        " Visual select inside braces
>          " Indent right

Visual Block Mode (Column Editing)

This is one of Vim’s killer features:

<C-v>      " Enter visual block mode

Example - Add semicolons to multiple lines:

const name = "John"
const age = 30
const city = "NYC"
  1. Position cursor at end of first line

  2. <C-v> to enter block mode

  3. 2j to select down 2 lines (creates column)

  4. $ to extend to end of each line

  5. A to append

  6. Type ;

  7. <Esc> (semicolon appears on all lines)

Result:

const name = "John";
const age = 30;
const city = "NYC";

Example - Insert at beginning of multiple lines:

print("Line 1")
print("Line 2")
print("Line 3")
  1. Position cursor at start of first line

  2. <C-v> block mode

  3. 2j to select down

  4. I to insert before

  5. Type #

  6. <Esc>

Result:

# print("Line 1")
# print("Line 2")
# print("Line 3")

Visual Block with Ragged Lines

When lines have different lengths:

<C-v>      " Block mode
3j         " Down 3 lines
$          " Extend to end of each line (ragged)
A text<Esc>  " Append to all lines

Visual Line Mode

V          " Enter line-wise visual mode
3j         " Select 3 lines down
d          " Delete selected lines

Or directly:

3Vd        " Select 3 lines and delete

Increment/Decrement in Visual Block

<C-v>      " Block mode (select column of numbers)
g<C-a>     " Increment sequentially (1, 2, 3, 4...)
g<C-x>     " Decrement sequentially
<C-a>      " Increment all by 1
<C-x>      " Decrement all by 1

Example - Create numbered list:

Item Item Item Item

  1. Cursor at start of first line

  2. <C-v>3j to select column

  3. I1. <Esc> to insert “1.”

  4. Result:

  5. Item

  6. Item

  7. Item

  8. Item

  9. Reselect: gv

  10. g<C-a> to increment sequentially

Result:

  1. Item

  2. Item

  3. Item

  4. Item

3.8 Search and Replace

Basic Substitution

:s/old/new/          " Replace first occurrence in current line
:s/old/new/g         " Replace all occurrences in current line
:s/old/new/gc        " Replace all with confirmation
:%s/old/new/g        " Replace all in entire file
:%s/old/new/gc       " Replace all in entire file with confirmation
:5,10s/old/new/g     " Replace in lines 5-10
:'<,'>s/old/new/g    " Replace in visual selection

Flags:

  • g - Global (all occurrences in line)

  • c - Confirm each substitution

  • i - Case insensitive

  • I - Case sensitive

  • n - Report number of matches without substituting

Examples:

:%s/foo/bar/g        " Replace all 'foo' with 'bar'
:%s/foo/bar/gc       " Same, but ask for confirmation
:%s/foo/bar/gn       " Count how many 'foo' exist
:%s/\<foo\>/bar/g    " Replace whole word 'foo' only
:%s/foo/bar/gi       " Case-insensitive replacement

Range Substitution

:1,10s/old/new/g     " Lines 1 to 10
:.,+5s/old/new/g     " Current line plus next 5
:.-3,.+3s/old/new/g  " 3 lines before to 3 lines after
:1,$s/old/new/g      " All lines (same as %)
:.,$s/old/new/g      " Current line to end

Pattern Matching in Substitution

:%s/\d\+/NUMBER/g          " Replace all numbers with 'NUMBER'
:%s/^/# /                   " Comment out lines (add # at start)
:%s/$/ END/                 " Append ' END' to all lines
:%s/\s\+$//                 " Remove trailing whitespace
:%s/^\_s\+$//               " Remove blank lines

Capture Groups and Backreferences

:%s/\(\w\+\) \(\w\+\)/\2 \1/   " Swap first two words
:%s/"\(.*\)"/'\1'/              " Change double quotes to single
:%s/\(.*\)/[\1]/                " Wrap each line in brackets

Example:

Text: John Doe Jane Smith

Command:

:%s/\(\w\+\) \(\w\+\)/\2, \1/

Result: Doe, John Smith, Jane

Using \= for Expression Substitution

:%s/\d\+/\=submatch(0) * 2/    " Double all numbers
:%s/.*/\=line('.')/            " Replace each line with its line number

Example:

Text: Price: 10 Price: 20 Price: 30

Command:

:%s/\d\+/\=submatch(0) * 2/g

Result: Price: 20 Price: 40 Price: 60

Special Characters in Replacement

\r       " Insert newline
\n       " Insert null (appears as ^@)
\t       " Insert tab
&        " Insert the whole matched pattern
~        " Use replacement from previous substitute
\L       " Make following chars lowercase
\U       " Make following chars uppercase
\0       " Insert whole matched pattern (same as &)
\1-\9    " Insert captured groups

Example - Add newline after comma:

:%s/, /,\r/g

Text: apple, banana, cherry

Result: apple, banana, cherry

Global Command

:g/pattern/command          " Execute command on lines matching pattern
:g!/pattern/command         " Execute on lines NOT matching
:v/pattern/command          " Same as g! (inverse)

Examples:

:g/TODO/d               " Delete all lines with TODO
:g/^$/d                 " Delete empty lines
:g/pattern/m$           " Move matching lines to end of file
:g/pattern/t$           " Copy matching lines to end of file
:g/pattern/normal @a    " Execute macro 'a' on matching lines
:g/import/s/$/;/        " Add semicolon to lines with 'import'

Complex example - Sort CSS properties:

.container {
    padding: 10px;
    color: red;
    margin: 5px;
    display: flex;
}
:g/{/+1,/}/-1 sort

Result (properties sorted):

.container {
    color: red;
    display: flex;
    margin: 5px;
    padding: 10px;
}

3.9 Case Conversion

Toggle Case

~          " Toggle case of character under cursor
g~{motion} " Toggle case of text defined by motion
g~~        " Toggle case of entire line
g~iw       " Toggle case of inner word

Make Lowercase

gu{motion} " Make lowercase
guu        " Make current line lowercase
guiw       " Make inner word lowercase
gue        " Make to end of word lowercase
gu$        " Make to end of line lowercase
guG        " Make to end of file lowercase

Make Uppercase

gU{motion} " Make uppercase
gUU        " Make current line uppercase
gUiw       " Make inner word uppercase
gUe        " Make to end of word uppercase
gU$        " Make to end of line uppercase

Visual Mode Case Change

v          " Visual select
u          " Make selection lowercase
U          " Make selection uppercase
~          " Toggle case of selection

Title Case

Vim doesn’t have built-in title case, but you can use substitution:

:%s/\<\(\w\)\(\w*\)\>/\u\1\L\2/g

This capitalizes the first letter of each word and lowercases the rest.

3.10 Indentation

Manual Indentation

>>         " Indent current line right
<<         " Indent current line left
>>{motion} " Indent motion right
>i{        " Indent inside braces
>ap        " Indent a paragraph
5>>        " Indent 5 lines right

Visual Mode Indentation

v          " Visual select
>          " Indent right
<          " Indent left

" Stay in visual mode after indenting:
v
>          " Indent
gv         " Reselect
>          " Indent again

Better: Configure indentation to stay in visual mode:

vim.keymap.set('v', '<', '<gv', { desc = 'Indent left and reselect' })
vim.keymap.set('v', '>', '>gv', { desc = 'Indent right and reselect' })

Now you can repeatedly press > or < without losing selection.

Auto-Indent

={motion}  " Auto-indent motion
==         " Auto-indent current line
=ap        " Auto-indent paragraph
=i{        " Auto-indent inside braces
gg=G       " Auto-indent entire file

With LSP formatting:

vim.keymap.set('n', '<leader>f', vim.lsp.buf.format, { desc = 'Format buffer' })

Or:

:lua vim.lsp.buf.format()

Set Indentation Settings

vim.opt.shiftwidth = 4      -- Indent by 4 spaces
vim.opt.tabstop = 4         -- Tab displays as 4 spaces
vim.opt.expandtab = true    -- Convert tabs to spaces
vim.opt.smartindent = true  -- Auto-indent new lines
vim.opt.autoindent = true   -- Copy indent from current line

Reindent Pasted Text

]p         " Paste and adjust indent to match current line
[p         " Same, but paste above

Or after pasting normally:

p          " Paste
=`]        " Reindent from start to end of pasted text

3.11 Joining and Splitting Lines

Join Lines

J          " Join current line with next (adds space)
gJ         " Join without adding space
3J         " Join next 3 lines

Examples:

name = "John"
age = 30

Cursor on first line, J:

name = "John" age = 30

With gJ:

name = "John"age = 30

Visual Mode Join

V          " Line-wise visual
3j         " Select 3 more lines
J          " Join all selected lines

Join with Specific Separator

No built-in command, but can use substitution:

:%s/\n/, /g    " Join all lines with comma-space

Or with a range:

:5,10s/\n/, /g  " Join lines 5-10 with comma

Split Lines

Vim doesn’t have a dedicated split command, but you can:

Method 1: Insert newline in normal mode

i<CR><Esc>     " Split at cursor position

Method 2: Substitute

:s/, /,\r/g    " Split at each comma

Method 3: Macro for splitting

:norm f,a^M    " Find comma, append newline (^M = <C-v><CR>)

3.12 Increment and Decrement

Basic Increment/Decrement

<C-a>      " Increment number under cursor
<C-x>      " Decrement number under cursor
5<C-a>     " Increment by 5
10<C-x>    " Decrement by 10

Examples:

count = 10

Cursor on 10, press <C-a>:

count = 11

Press 5<C-a>:

count = 16

Increment/Decrement in Visual Mode

<C-v>      " Block mode, select column of numbers
g<C-a>     " Increment sequentially (1, 2, 3, 4...)
g<C-x>     " Decrement sequentially
<C-a>      " Increment all by same amount

Example:

Item 0 Item 0 Item 0

Select the numbers with <C-v>, then g<C-a>: Item 1 Item 2 Item 3

Alphabetic Increment

By default, Vim only increments numbers. To increment letters:

vim.opt.nrformats:append('alpha')

Now: version_a

Cursor on a, press <C-a>: version_b

3.13 Formatting and Wrapping

Format/Wrap Lines

gq{motion}  " Format (hard wrap) text
gqq         " Format current line
gqap        " Format a paragraph
gqG         " Format from cursor to end of file

Example with textwidth:

vim.opt.textwidth = 80

Long line: This is a very long line that exceeds the textwidth setting and should be wrapped to multiple lines.

Position cursor on line, gqq: This is a very long line that exceeds the textwidth setting and should be wrapped to multiple lines.

Format Without Moving

gw{motion}  " Format but keep cursor position
gwap        " Format paragraph, stay at cursor

Join Lines Without Extra Space

gJ         " Join lines without inserting space

Format Comments

With formatoptions set correctly:

vim.opt.formatoptions:append('r')  -- Auto-insert comment leader on <CR>
vim.opt.formatoptions:append('o')  -- Auto-insert comment leader on 'o'/'O'
vim.opt.formatoptions:append('q')  -- Allow formatting comments with gq
vim.opt.formatoptions:append('n')  -- Recognize numbered lists
vim.opt.formatoptions:append('j')  -- Remove comment leader when joining

Now gqap in a comment block properly formats it.

External Formatting

!{motion}{program}  " Filter through external program
!!{program}         " Filter current line
5!!{program}        " Filter 5 lines

Examples:

!apsort            " Sort paragraph
!5jsort            " Sort next 5 lines
:%!python -m json.tool   " Format entire file as JSON
:'<,'>!column -t   " Align visual selection as columns

3.14 Working with Registers

Register Types

"           " Unnamed register (default)
0-9         " Numbered registers (yank and delete history)
a-z         " Named registers (user-defined)
A-Z         " Append to named registers

-           " Small delete register (< 1 line)

+           " System clipboard

*           " Primary selection (X11)
/           " Last search pattern
:           " Last command
.           " Last inserted text
%           " Current filename
#           " Alternate filename
=           " Expression register
_           " Black hole register

Using Named Registers

"ayy       " Yank line to register 'a'
"bdd       " Delete line to register 'b'
"ap        " Paste from register 'a'
"bp        " Paste from register 'b'

Appending to Registers

"ayy       " Yank to register 'a'
j
"Ayy       " Append next line to register 'a' (uppercase A)
"ap        " Paste both lines

Black Hole Register

"_d        " Delete to black hole (doesn't overwrite other registers)
"_dd       " Delete line to black hole
"_x        " Delete character to black hole

Use case: You want to delete something without affecting your yank register:

yiw        " Yank word
/other     " Find other location
diw        " This would overwrite your yank!

" Instead:
yiw
/other
"_diw      " Delete to black hole
p          " Paste your original yank

Expression Register

"=         " Access expression register (in insert mode: <C-r>=)
<C-r>=5*8  " Insert result of 5*8

In normal mode:

"=5*8<CR>p  " Calculate 5*8 and paste result

View Register Contents

:reg       " View all registers
:reg a b   " View registers a and b
:reg "     " View unnamed register
:reg +     " View clipboard register

Last Inserted Text

.          " Repeat last change
".p        " Paste last inserted text

3.15 Recording Macros (Detailed)

Basic Macro Recording

q{register}   " Start recording to {register}
# Perform actions
q             " Stop recording
@{register}   " Play back macro
@@            " Repeat last played macro
5@{register}  " Play macro 5 times

Example - Add quotes around words:

apple banana cherry

  1. Position cursor on ‘a’ in ‘apple’

  2. qa - start recording to register ‘a’

  3. i"<Esc> - insert quote before

  4. ea"<Esc> - append quote after

  5. j0 - move to next line, column 0

  6. q - stop recording

Now execute:

2@a        " Run on next 2 lines

Result: “apple” “banana” “cherry”

Recursive Macros

qaq        " Clear register a
qa         " Start recording
# Perform action
@a         " Call macro recursively
q          " Stop recording
@a         " Execute (will run until it fails)

Example - Process until end of file:

qaq        " Clear a
qa
dd         " Delete line
j          " Move down
@a         " Recursive call
q

@a         " Runs on all lines until end

Editing Macros

" View macro content:
:reg a

" Paste macro to edit:
"ap

" Edit the line
# Make changes

" Yank back to register:
"add

" Or directly:
:let @a = 'i"<Esc>ea"<Esc>j0'

Parallel Macro Execution

Run macro on multiple lines:

:5,10normal @a    " Run macro 'a' on lines 5-10
:%normal @a       " Run on all lines
:'<,'>normal @a   " Run on visual selection

Macro Best Practices

  1. Start from known position (e.g., 0 for beginning of line)

  2. End in a repeatable position (e.g., j0 to move to next line start)

  3. Use motions, not counts when possible (more robust)

  4. Test on a few lines first

  5. Use recursive macros for unknown quantity of items

Example of robust vs fragile:

Fragile:

qa
3l        " Move right 3 times (fails if line is shorter)
x
q

Robust:

qa
f,        " Find comma (more reliable)
x
j0        " Next line, start
q

3.16 Advanced Text Objects (with Plugins)

Targets.vim Extended Text Objects

If you install targets.vim plugin, you get many more text objects:

" Pair text objects work with:
da)    " Delete around ) even when not inside
di)    " Delete inside )
dI)    " Delete inside ) excluding whitespace
dA)    " Delete around ) including whitespace

" Work with: () {} [] <> `` '' "" 

" Separator text objects:
di,    " Delete inside commas
da,    " Delete around commas (including separators)
dI,    " Delete inside, skip boundaries
dA,    " Delete around, including boundaries

" Work with: , . ; : + - = ~ _ * # / | \ & $

" Argument text objects:
cia    " Change inner argument
daa    " Delete an argument (including comma)

Example:

function example(arg1, arg2, arg3) {

Cursor anywhere in arg2:

daa    " Deletes ', arg2'

Tree-sitter Text Objects

With nvim-treesitter-textobjects:

require'nvim-treesitter.configs'.setup {
  textobjects = {
    select = {
      enable = true,
      keymaps = {
        ["af"] = "@function.outer",
        ["if"] = "@function.inner",
        ["ac"] = "@class.outer",
        ["ic"] = "@class.inner",
        ["aa"] = "@parameter.outer",
        ["ia"] = "@parameter.inner",
      },
    },
  },
}

Now you have:

daf    " Delete a function
cif    " Change inside function
vac    " Visual select a class
daa    " Delete a parameter/argument

These work across multiple programming languages!

3.17 Editing Best Practices

1. Prefer Text Objects Over Motions

Instead of:

dw     " Delete word (leaves trailing space)

Use:

daw    " Delete a word (cleaner)

Instead of:

f"     " Find quote
dt"    " Delete till quote

Use:

di"    " Delete inside quotes

2. Make Changes Repeatable

Structure edits so . works effectively:

Example - Replace multiple words:

/old_name<CR>   " Find first occurrence
cwnew_name<Esc> " Change word
n.              " Next occurrence, repeat
n.              " Continue

3. Use Macros for Complex Repetitions

If . isn’t enough, record a macro:

qa              " Start recording
# Complex multi-step edit
q               " Stop
@a              " Replay

4. Minimize Mode Switching

Instead of:

A<space><Esc>   " Append space, exit
A;<Esc>         " Append semicolon, exit (separate action)

Do:

A ;<Esc>        " Append both in one insert session

This makes . repeat both actions.

5. Use Composability

Combine operators and motions creatively:

c3w            " Change 3 words
d/end<CR>      " Delete to 'end'
y}             " Yank to end of paragraph
gU4j           " Uppercase 4 lines

6. Keep Register Hygiene

Use specific registers to avoid conflicts:

"ay            " Important yank to register 'a'
# Do other work
"ap            " Still have it

Use black hole register for throwaway deletes:

"_dd           " Delete without polluting registers

3.18 Editing Exercises

Exercise 1: Text Object Mastery

Open a code file and practice:


1. Position cursor in a function

2. Try: dif (delete inside function)

3. Undo: u

4. Try: vif (visual select inside function)

5. Try: yaf (yank around function)

Repeat with:

- di" and da" (quotes)

- di( and da( (parentheses)

- di{ and da{ (braces)

- diw and daw (word)

- dip and dap (paragraph)

Exercise 2: Dot Command Practice

// Original text (3 lines): const oldName = value const oldName = value const oldName = value

// Goal: Change all ‘oldName’ to ‘newName’

Solution:

/oldName<CR>    " Find first
ciwnewName<Esc> " Change it
n.              " Next and repeat
n.              " Next and repeat

Exercise 3: Visual Block Magic

// Original: Item 1 Item 1 Item 1 Item 1

// Goal: Item 1 Item 2 Item 3 Item 4

Solution:

<C-v>           " Block mode
3j              " Select down
$               " To end of lines
A<Backspace><Esc> " Delete the '1'
gv              " Reselect
g<C-a>          " Sequential increment

Exercise 4: Macro Recording

// Original (lines of words): apple banana cherry date

// Goal (wrap in array): [“apple”], [“banana”], [“cherry”], [“date”]

Solution:

qa              " Start recording to 'a'
I["<Esc>        " Insert '["' at beginning
A"],<Esc>       " Append '"],' at end
j0              " Next line, column 0
q               " Stop recording
3@a             " Repeat on remaining 3 lines

Exercise 5: Search and Replace

// Original: function calculate(x, y, z) result = calculate(a, b, c) value = calculate(1, 2, 3)

// Goal: Change ‘calculate’ to ‘compute’

Solution:

:%s/\<calculate\>/compute/g

Or with confirmation:

:%s/\<calculate\>/compute/gc

Exercise 6: Complex Deletion

Delete all lines containing “TODO” and “FIXME”:

:g/TODO\|FIXME/d

Or delete all empty lines:

:g/^$/d

Exercise 7: Indentation Challenge

# Badly indented:
def example():
print("hello")
if True:
print("world")
return

Fix it:

vi{     " Visual select inside function
=       " Auto-indent

Or entire file:

gg=G    " From top to bottom, indent

Chapter Summary

In this chapter, you learned:

  • Vim’s editing grammar: Operators + Motions + Text Objects forming a composable language

  • Complete operator set: d, c, y, ~, gu, gU, >, <, =, and more

  • All text objects: iw/aw, i"/a", i(/a(, it/at, ip/ap, etc.

  • Insert mode commands: i, I, a, A, o, O, gi

  • Deletion techniques: x, dd, D, d{motion}, text object deletion

  • Yank and paste: Registers, system clipboard integration, paste modes

  • Undo/redo system: Linear undo, undo branches, time-based undo, persistent undo

  • Dot command: The most powerful editing tool for repeating changes

  • Visual modes: Character, line, and block-wise editing

  • Search and replace: Substitution, ranges, capture groups, global commands

  • Case conversion: ~, gu, gU with motions and text objects

  • Indentation: Manual and automatic, with LSP formatting

  • Joining and splitting: J, gJ, and techniques for line manipulation

  • Increment/decrement: <C-a>, <C-x>, sequential incrementing in visual block

  • Text formatting: gq, gw, external formatters

  • Registers: Named, special, system clipboard, black hole

  • Macro recording: Basic, recursive, editing macros, parallel execution

  • Advanced text objects: Plugin-enhanced with targets.vim and tree-sitter

  • Best practices: Repeatability, composability, efficiency

You now have a complete editing vocabulary. The key to mastery is practice—these commands need to become muscle memory. In the next chapter, we’ll explore Visual Mode in depth, including advanced selection techniques and visual mode operators that weren’t covered here.


Chapter 4: Search and Replace

Search and replace is where Vim’s modal editing and powerful pattern matching converge. While we touched on basic substitution in Chapter 3, this chapter dives deep into Vim’s search capabilities, regular expressions, the substitution command, and advanced pattern-matching techniques that make complex text transformations trivial.

4.1 Search Fundamentals

Basic Search Commands

/{pattern}       " Search forward for pattern
?{pattern}       " Search backward for pattern
n                " Repeat search in same direction
N                " Repeat search in opposite direction

*                " Search forward for word under cursor
#                " Search backward for word under cursor
g*               " Search forward for word under cursor (partial match)
g#               " Search backward for word under cursor (partial match)

Examples:

/function        " Search forward for "function"
n                " Next occurrence
N                " Previous occurrence

?return          " Search backward for "return"
n                " Next (backward)
N                " Previous (forward)


*                " Search for exact word under cursor
#                " Search backward for word under cursor

Case Sensitivity

/pattern         " Case sensitivity depends on 'ignorecase' setting
/pattern\c       " Force case-insensitive search
/pattern\C       " Force case-sensitive search

Configuration:

vim.opt.ignorecase = true      -- Ignore case in searches
vim.opt.smartcase = true       -- Override ignorecase if pattern has uppercase

With smartcase:

/hello           " Matches: hello, Hello, HELLO
/Hello           " Matches: Hello only (uppercase present)

Search Highlighting

vim.opt.hlsearch = true        -- Highlight all matches
vim.opt.incsearch = true       -- Show matches as you type

Clear highlighting:

:noh             " Or :nohlsearch - clear search highlighting

Better: Map a key to clear:

vim.keymap.set('n', '<Esc>', '<cmd>nohlsearch<CR>', { desc = 'Clear search highlighting' })

Search History

/{up-arrow}      " Cycle through previous search patterns
/{down-arrow}    " Cycle forward
/<C-r>/          " Insert last search pattern
q/               " Open search history window (editable)

Search history window (q/):

  • Edit any previous search

  • Press <CR> to execute

  • Press <C-c> to cancel

Search Offsets

Position cursor relative to match:

/pattern/        " Cursor at start of match
/pattern/e       " Cursor at end of match
/pattern/+2      " 2 lines below match
/pattern/-1      " 1 line above match
/pattern/e+1     " 1 character after end of match
/pattern/e-2     " 2 characters before end of match
/pattern/b+3     " 3 characters after beginning of match

Examples:

def calculate_total(price, quantity):
    return price * quantity
/def/e           " Cursor on 'f' (end of 'def')
/def/+1          " Cursor on line below
/price/e+2       " Cursor 2 chars after 'price' (on 'c' in 'calculate')

Search Under Cursor


*                " Search for exact word under cursor (forward)
#                " Search backward
g*               " Search forward (partial match)
g#               " Search backward (partial match)
gd               " Go to local definition
gD               " Go to global definition

Difference between * and g*:

count = 10
account = 20

Cursor on count, press *:

  • Matches: count only (exact word)

Press g*:

  • Matches: count and account (partial)

Select text in visual mode and search for it:

Manual:

v                " Visual select
y                " Yank
/<C-r>"          " Paste into search

Better with mapping:

vim.keymap.set('v', '*', 'y/\\V<C-R>=escape(@",\'/\\\')<CR><CR>', 
  { desc = 'Search for visual selection' })

Now:

v                " Visual select

*                " Search for selection

4.2 Regular Expressions in Vim

Vim uses its own regex flavor, which differs slightly from other tools.

Magic Modes

Vim has different “magic” levels affecting which characters need escaping:

\v               " Very magic (most chars have special meaning)
\m               " Magic (default)
\M               " No magic (most chars literal)
\V               " Very no magic (all chars literal except \)

Examples:

/\vword\d+       " Very magic: word followed by digits
/word\d\+        " Magic (default): same thing
/\Mword\d\+      " No magic: same, but needs escaping
/\Vword          " Very no magic: literal 'word' search

Best practice: Use \v (very magic) for complex patterns:

/\v(foo|bar)     " Match 'foo' or 'bar'
/(foo\|bar)      " Same in default magic mode (needs escaping)

Character Classes

.                " Any character except newline
\s               " Whitespace character (space, tab, newline)
\S               " Non-whitespace
\d               " Digit [0-9]
\D               " Non-digit
\w               " Word character [a-zA-Z0-9_]
\W               " Non-word character
\h               " Head of word character [a-zA-Z_]
\a               " Alphabetic character
\l               " Lowercase letter
\u               " Uppercase letter
\x               " Hexadecimal digit [0-9a-fA-F]

\_s              " Whitespace including newline
\_^              " Start of line (in multiline pattern)
\_$              " End of line (in multiline pattern)
\_. " Any character including newline

Examples:

/\d\+            " One or more digits
/\w\+@\w\+\.\w\+ " Simple email pattern
/\s\+            " One or more whitespace chars
/\h\w*           " Identifier (letter/underscore + word chars)

Quantifiers


*                " 0 or more (greedy)
\+               " 1 or more (greedy)
\?               " 0 or 1 (optional)
\{n}             " Exactly n times
\{n,}            " At least n times
\{,m}            " At most m times
\{n,m}           " Between n and m times

\{-}             " 0 or more (non-greedy)
\{-1,}           " 1 or more (non-greedy)
\{-,1}           " 0 or 1 (non-greedy)

Examples:

/\d\{3}          " Exactly 3 digits
/\d\{3,5}        " 3 to 5 digits
/\d\{3,}         " 3 or more digits
/\w\+            " 1 or more word characters
/\s\?            " Optional whitespace

Greedy vs Non-greedy:

Text: <div>content</div>

/<.*>            " Greedy: matches '<div>content</div>' (entire string)
/<.\{-}>         " Non-greedy: matches '<div>' (first tag)

Anchors

^                " Start of line
$                " End of line
\<               " Start of word
\>               " End of word
\%^              " Start of file
\%$              " End of file
\%V              " Inside visual selection
\%#              " Cursor position

Examples:

/^import         " Lines starting with 'import'
/;$              " Lines ending with semicolon
/\<def\>         " Exact word 'def' (not 'define' or 'ifdef')
/\%^#!/          " Shebang at start of file
/\%$             " Match at end of file

Grouping and Alternation

\(pattern\)      " Capture group (default magic)
\%(pattern\)     " Non-capturing group
\|               " Alternation (OR)

" With very magic (\v):
(pattern)        " Capture group
%(pattern)       " Non-capturing group
|                " Alternation

Examples:

/\(foo\|bar\)    " Match 'foo' or 'bar'
/\v(foo|bar)     " Same with very magic

/\(Mr\|Ms\|Dr\)\. " Match 'Mr.', 'Ms.', or 'Dr.'
/\v(Mr|Ms|Dr)\.  " Same with very magic

Backreferences

\1, \2, \3...    " Reference captured groups (in pattern)
\0               " Entire match

Examples:

/\(\w\+\) \1     " Match repeated word: "the the"
/\v(\w+) \1      " Same with very magic

/\v"(.{-})"      " Match quoted string (capture content)
/\v(['"])(.\{-})\1  " Match quoted string with same quote type

Example - Find duplicate words:

/\v<(\w+)\_s+\1>  " Find duplicate words (even across lines)

Text: The the cat sat on the mat. This is is a test.

Both duplicate pairs will be found.

Character Ranges

[abc]            " Match a, b, or c
[a-z]            " Match lowercase letter
[A-Z]            " Match uppercase letter
[0-9]            " Match digit
[a-zA-Z0-9]      " Match alphanumeric
[^abc]           " Match anything except a, b, c
[^0-9]           " Match non-digit

Examples:

/[aeiou]         " Match vowel
/[^aeiou]        " Match consonant (and other chars)
/[0-9]\{3}-[0-9]\{4}  " Match phone pattern: 123-4567
/\v[A-Z][a-z]+   " Capitalized word

Lookahead and Lookbehind

\@=              " Positive lookahead
\@!              " Negative lookahead
\@<=             " Positive lookbehind
\@<!             " Negative lookbehind

Positive lookahead - Match X only if followed by Y:

/\vfoo(@=bar)    " Match 'foo' only if followed by 'bar'
/foo\(bar\)\@=   " Same in default magic

Text: foobar foobaz

  • Matches: foo in foobar only

Negative lookahead - Match X only if NOT followed by Y:

/\vfoo(@!bar)    " Match 'foo' NOT followed by 'bar'

Text: foobar foobaz

  • Matches: foo in foobaz only

Positive lookbehind - Match X only if preceded by Y:

/\v(@<=foo)bar   " Match 'bar' only if preceded by 'foo'
/\(foo\)\@<=bar  " Same in default magic

Negative lookbehind - Match X only if NOT preceded by Y:

/\v(@<!foo)bar   " Match 'bar' NOT preceded by 'foo'

Practical example - Match numbers not in comments:

# price = 100
total = 200
/\v^[^#]*\zs\d+  " Match numbers not in lines starting with #

Matches: 200 only (not 100 because that line starts with #)

Zero-width Assertions

\zs              " Start of match (ignore everything before)
\ze              " End of match (ignore everything after)

Examples:

/foo\zsbar       " Match 'bar' (in 'foobar'), but highlight only 'bar'
/foo\zebar       " Match 'foo' (in 'foobar'), but highlight only 'foo'

Text: foobar

/foo\zsbar       " Cursor on 'b' (match starts at 'b')
/foo\zebar       " Cursor on 'f' (match ends before 'b')

Practical use - Extract URL path:

https://example.com/path/to/file.html

/\v\.com\/\zs.*  " Match everything after '.com/'

Match highlighted: path/to/file.html

4.3 Search Configuration

Essential Search Settings


-- Case handling
vim.opt.ignorecase = true      -- Ignore case by default
vim.opt.smartcase = true       -- Case-sensitive if uppercase present


-- Highlighting
vim.opt.hlsearch = true        -- Highlight matches
vim.opt.incsearch = true       -- Incremental search (show as you type)


-- Wrapping
vim.opt.wrapscan = true        -- Wrap around end of file


-- Show match count
vim.opt.shortmess:append("S")  -- Don't show "search hit BOTTOM"

Advanced Search Options


-- Show search count in command line
vim.opt.shortmess:remove("S")  -- Shows [1/5] when searching


-- Center screen on search
vim.keymap.set('n', 'n', 'nzz', { desc = 'Next search result (centered)' })
vim.keymap.set('n', 'N', 'Nzz', { desc = 'Previous search result (centered)' })


-- Clear search with Escape
vim.keymap.set('n', '<Esc>', '<cmd>nohlsearch<CR>', { desc = 'Clear search highlight' })


-- Use ripgrep for :grep if available
if vim.fn.executable('rg') == 1 then
  vim.opt.grepprg = 'rg --vimgrep --smart-case --follow'
  vim.opt.grepformat = '%f:%l:%c:%m'
end

-- Keep cursor centered when searching
vim.keymap.set('n', 'n', 'nzzzv', { desc = 'Next result (centered, unfold)' })
vim.keymap.set('n', 'N', 'Nzzzv', { desc = 'Previous result (centered, unfold)' })


-- Better star command (don't move on first *)
vim.keymap.set('n', '*', '*N', { desc = 'Search word under cursor (stay)' })
vim.keymap.set('n', '#', '#N', { desc = 'Search word backward (stay)' })


-- Visual mode search
vim.keymap.set('v', '*', 'y/\\V<C-R>=escape(@",\'/\\\')<CR><CR>',
  { desc = 'Search for visual selection' })

4.4 The Substitute Command

Basic Syntax

:[range]s/{pattern}/{replacement}/{flags}

Components:

  • [range]: Lines to operate on (optional, defaults to current line)

  • {pattern}: What to search for (regex)

  • {replacement}: What to replace with

  • {flags}: Modifiers (g, c, i, etc.)

Common Ranges

:s/old/new/          " Current line only
:%s/old/new/         " Entire file
:5,10s/old/new/      " Lines 5 to 10
:'<,'>s/old/new/     " Visual selection
:.,$s/old/new/       " Current line to end of file
:.,+5s/old/new/      " Current line plus 5 lines
:.-3,.+3s/old/new/   " 3 lines before to 3 after current
:1,$s/old/new/       " Line 1 to last line (same as %)

Mark-based ranges:

:'a,'bs/old/new/     " From mark a to mark b
:'{,'}s/old/new/     " Last visual selection (same as '<,'>)

Substitute Flags

g                " Global (all occurrences in line)
c                " Confirm each substitution
i                " Case insensitive
I                " Case sensitive
n                " Report count without substituting
e                " No error if pattern not found
&                " Reuse flags from previous substitute

Examples:

:%s/foo/bar/g        " Replace all 'foo' with 'bar'
:%s/foo/bar/gc       " Same, but ask for confirmation
:%s/foo/bar/gi       " Case-insensitive replacement
:%s/foo/bar/gn       " Count matches without replacing
:%s/foo/bar/ge       " No error if no matches found

Confirmation prompts: When using c flag: y - Yes, substitute this match n - No, skip this match a - All, substitute all remaining matches q - Quit, stop substituting l - Last, substitute this match and quit ^E - Scroll up ^Y - Scroll down

Special Characters in Replacement

\r               " Insert newline
\n               " Insert null byte (appears as ^@)
\t               " Insert tab
&                " Insert entire matched pattern
~                " Use replacement from previous substitute
\L               " Make following characters lowercase
\U               " Make following characters uppercase
\E               " End \L or \U
\l               " Make next character lowercase
\u               " Make next character uppercase
\0               " Insert entire match (same as &)
\1, \2, etc.     " Insert captured groups

Examples:

:%s/foo/& bar/       " Replace 'foo' with 'foo bar'
:%s/\(.*\)/[\1]/     " Wrap each line in brackets
:%s/\w\+/\u&/g       " Capitalize first letter of each word
:%s/\(.\)/\U\1/g     " Uppercase entire file
:%s/\(\w\)/\L\1/g    " Lowercase first letter of each word

Replace with Newline

:%s/, /,\r/g         " Replace comma-space with comma-newline

Example:

Text: apple, banana, cherry

After :s/, /,\r/g: apple, banana, cherry

Using Captured Groups

:\%s/\(\w\+\) \(\w\+\)/\2, \1/    " Swap first two words

Example:

Text: John Doe Jane Smith

After :%s/\(\w\+\) \(\w\+\)/\2, \1/: Doe, John Smith, Jane

Multiple Captures

:%s/\v(\d+)-(\d+)-(\d+)/\3\/\2\/\1/   " Date format: YYYY-MM-DD → DD/MM/YYYY

Example:

Text: 2024-10-19

After substitution: 19/10/2024

Expression Register in Replacement

Use \= to evaluate expressions:

:%s/\d\+/\=submatch(0) * 2/      " Double all numbers
:%s/$/\=line('.')/               " Append line number to each line
:%s/\d\+/\=printf('%03d', submatch(0))/  " Zero-pad numbers to 3 digits

Examples:

Text: Price: 10 Price: 25 Price: 100

After :%s/\d\+/\=submatch(0) * 2/g: Price: 20 Price: 50 Price: 200

Functions available in expression:

  • submatch(0) - entire match

  • submatch(1) - first capture group

  • line('.') - current line number

  • strlen() - string length

  • strftime() - date/time formatting

  • Any Vimscript/Lua function

Conditional Replacement

:%s/foo/\=submatch(0) == 'FOO' ? 'bar' : 'baz'/g

Replace ‘FOO’ with ‘bar’, other matches with ‘baz’.

Case-Aware Replacement

:%s/old/new/g        " Literal replacement
:%s/old/new/gi       " Case-insensitive search, literal replace

Smart case replacement (preserve original case):

:%s/\<old\>/new/gi   " Case-insensitive, but...

This doesn’t preserve case. For that, use a plugin or expression:

:%s/\c\<old\>/\=submatch(0) =~ '\u' ? 'New' : 'new'/g

This checks if match starts with uppercase, and adjusts replacement.

4.5 Global Command

The global command executes commands on lines matching a pattern.

Basic Syntax

:[range]g/{pattern}/{command}
:[range]g!/{pattern}/{command}    " Inverse (lines NOT matching)
:[range]v/{pattern}/{command}     " Same as g! (v = inverse)

Common Global Commands

:g/pattern/d         " Delete all lines matching pattern
:g!/pattern/d        " Delete lines NOT matching (keep matching)
:v/pattern/d         " Same as g! (keep matching lines)
:g/pattern/p         " Print matching lines
:g/pattern/m$        " Move matching lines to end
:g/pattern/t$        " Copy matching lines to end
:g/pattern/normal @a " Execute macro 'a' on matching lines

Examples:

:g/TODO/d            " Delete all TODO lines
:g/^$/d              " Delete empty lines
:g/^\s*$/d           " Delete blank lines (including whitespace-only)
:v/import/d          " Delete lines that don't contain 'import'
:g/debug/normal gcc  " Comment out lines with 'debug' (with commentary.vim)

Multiple Commands

:g/pattern/cmd1 | cmd2 | cmd3

Example:

:g/function/normal A {<CR><Esc>  " Append ' {' and newline to function declarations

Global with Ranges

:g/class/+1,/^}/d    " Delete content of all classes
:g/TODO/.+1d         " Delete line after each TODO
:g/START/,/END/s/foo/bar/g   " Replace in blocks between START and END

Example - Delete function bodies:

def func1():
    pass

def func2():
    pass
:g/^def/+1,/^$/d     " Delete from line after 'def' to next empty line

Inverse Global

:v/pattern/d         " Keep only matching lines (delete non-matching)
:v/^#/d              " Delete lines not starting with #
:g!/import/d         " Same as above (using g!)

Global with Substitute

:g/class/s/def/function/g    " In lines with 'class', replace 'def' with 'function'
:v/^#/.s/^/# /               " Comment non-comment lines

Collect Matching Lines

Copy to register:

qaq                  " Clear register a
:g/pattern/y A       " Append matching lines to register a

Copy to end of file:

:g/pattern/t$        " Copy matching lines to end

Move to new buffer:

:g/pattern/d A       " Delete matching lines, append to register a
:new                 " New buffer
"aP                  " Paste collected lines

Global Across Multiple Files

:args *.js           " Load all .js files
:argdo %s/var/let/ge | update   " Replace in all files and save

Or with buffers:

:bufdo %s/old/new/ge | w   " Replace in all buffers and save

Negated Patterns

:g!/import/d         " Delete lines without 'import'
:v/export/d          " Delete lines without 'export' (same as above)
:g!/^\s*#/d          " Delete lines that aren't comments

4.6 Advanced Search Techniques

/foo\_sbar           " Match 'foo' and 'bar' with any whitespace between (including newline)
/foo\_.bar           " Match 'foo' and 'bar' with any characters between (including newline)
/foo\(\n.*\)*bar     " Match 'foo' and 'bar' on different lines

Example:

Text:

def calculate(
    price,
    quantity
):
/def\_s*calculate\_s*\(    " Find function definition across lines

Search in Folds

vim.opt.foldopen:append('search')  -- Automatically open folds when searching

Or manually:

zn               " Disable folding temporarily
/pattern         " Search
zN               " Restore folding

Search in Visual Selection

\%V              " Inside visual selection atom

First visually select, then:

:'<,'>s/\%Vpattern/replacement/g   " Only in selection

Or use the visual selection range:

v                " Visual select
:'<,'>s/old/new/g   " Substitute only in selection

Search History Management

:history /       " Show search history
q/               " Open search history window (editable)
/<C-r>/          " Insert last search pattern
/<Up>            " Navigate search history

Clear search history:

:call histdel('search')     " Clear all search history
:call histdel('search', -1) " Remove last entry

Search with Very Magic

Always use very magic for complex patterns:

/\v(foo|bar)\d+  " Very magic: easier syntax

Configure default:


-- Make search use very magic by default
vim.keymap.set('n', '/', '/\\v', { desc = 'Search (very magic)' })
vim.keymap.set('n', '?', '?\\v', { desc = 'Backward search (very magic)' })

Now:

/(foo|bar)\d+    " No need for \v prefix

Search with Offset and Command

/pattern/+2      " Jump 2 lines below match
/pattern/e       " Jump to end of match

Combine with command:

/pattern/+1|d    " Find pattern, go 1 line down, delete it

4.7 Search and Replace Patterns Cookbook

Remove Trailing Whitespace

:%s/\s\+$//e     " Remove trailing whitespace from all lines

Remove All Empty Lines

:g/^$/d          " Using global command
:%s/\n\n/\r/g    " Or using substitute (collapse double newlines)

Remove Blank Lines (Including Whitespace)

:g/^\s*$/d       " Delete lines with only whitespace

Swap Two Words

:%s/\v(\w+)(\s+)(\w+)/\3\2\1/   " Swap word1 and word2

Add Line Numbers

:%s/^/\=line('.') . '. '/   " Prepend line number

Convert Tabs to Spaces

:%s/\t/    /g    " Replace tabs with 4 spaces

Or use retab:

:set expandtab
:retab           " Convert all tabs based on tabstop setting

Convert Spaces to Tabs

:set noexpandtab
:retab!          " Convert spaces to tabs

Remove Duplicate Lines

:sort u          " Sort and remove duplicates

Or without sorting:

:g/^\(.*\)$\n\1$/d   " Remove consecutive duplicates

Increment All Numbers

:%s/\d\+/\=submatch(0) + 1/g   " Increment all numbers by 1

Zero-Pad Numbers

:%s/\d\+/\=printf('%03d', submatch(0))/g   " Pad to 3 digits: 1 → 001

Extract URLs

:g/https\?:\/\//t$   " Copy all lines with URLs to end of file

Comment/Uncomment Lines

:g/pattern/s/^/# /        " Add # comment to matching lines
:g/pattern/s/^# //        " Remove # comment
:'<,'>s/^/# /             " Comment visual selection
:'<,'>s/^# //             " Uncomment visual selection

Convert CamelCase to snake_case

:%s/\v([a-z])([A-Z])/\1_\l\2/g   " camelCase → camel_case

Example:

myVariableName anotherExample

After: my_variable_name another_example

Convert snake_case to CamelCase

:%s/_\(\w\)/\u\1/g      " snake_case → snakeCase
:%s/\v_(.)/\u\1/g       " Same with very magic

Example:

my_variable_name another_example

After: myVariableName anotherExample

Extract Email Addresses

:g/\v\w+@\w+\.\w+/t$    " Copy lines with emails to end

Remove HTML Tags

:%s/<[^>]*>//g          " Remove all HTML tags
:%s/<\_.\{-}>//g        " Remove tags across lines (non-greedy)

Wrap Long Lines

:g/./ normal gqq        " Wrap all non-empty lines
:%normal gqq            " Wrap all lines

Or set textwidth:

vim.opt.textwidth = 80

Then:

gqG                     " Wrap from cursor to end

Add Semicolons to Lines

:g/^[^#]/s/$/;/         " Add ; to non-comment lines
:v/;$/normal A;         " Add ; to lines without it

Replace Smart Quotes

:%s/[""]/"/g            " Replace smart quotes with straight quotes
:%s/['']/'/g            " Replace smart single quotes

Convert Windows Line Endings to Unix

:%s/\r$//               " Remove carriage returns
:set ff=unix            " Set file format to Unix
:w                      " Save

Match Whole Words Only

:%s/\<word\>/replacement/g   " Match 'word' but not 'words' or 'password'

Case-Insensitive Whole Word Match

:%s/\c\<word\>/replacement/g   " Case-insensitive whole word

4.8 Working with Multiple Files

Search in Multiple Files (Vimgrep)

:vim /pattern/ **/*.js       " Search in all .js files recursively
:vim /pattern/ %             " Search in current file
:vim /pattern/ `find . -name '*.py'`  " Search in result of shell command

Navigate results:

:cn                          " Next match
:cp                          " Previous match
:copen                       " Open quickfix window
:cclose                      " Close quickfix window
:cfirst                      " First match
:clast                       " Last match

Quickfix list:

:clist                       " List all matches
:cc 5                        " Jump to 5th match

Search with External Tools

Using ripgrep:

:grep pattern **/*.js        " If grepprg is set to ripgrep
:grep! pattern **/*.js       " Don't jump to first match

Configuration:

if vim.fn.executable('rg') == 1 then
  vim.opt.grepprg = 'rg --vimgrep --smart-case --follow'
  vim.opt.grepformat = '%f:%l:%c:%m'
end

Replace in Multiple Files

:args **/*.js                " Load all .js files
:argdo %s/old/new/ge | update   " Replace and save each

Explanation:

  • args **/*.js - Load files into argument list

  • argdo - Execute command on each file in args

  • %s/old/new/ge - Substitute (e flag: no error if not found)

  • | - Command separator

  • update - Save only if modified

With confirmation:

:argdo %s/old/new/gce | update   " Confirm each replacement

Search and Replace with Quickfix

:vim /old/ **/*.js           " Find all occurrences
:cdo s/old/new/gc | update   " Replace in each file (with confirmation)

Difference between cdo and cfdo:

  • cdo - Execute on each quickfix entry

  • cfdo - Execute once per file in quickfix

:cfdo %s/old/new/ge | update  " Replace in each file (once per file)
:vimgrep /{pattern}/ %       " Search in current file, populate quickfix

Or use location list:

:lvim /pattern/ %            " Search, populate location list
:lopen                       " Open location list

4.9 Search and Replace Best Practices

1. Test Patterns First

Before replacing:

/pattern             " Test your pattern
:%s/pattern/replacement/gn   " Count matches without replacing

2. Use Confirmation for Important Changes

:%s/old/new/gc       " Confirm each change

3. Save Before Mass Replace

:w                   " Save first
:%s/old/new/g        " Replace
u                    " Undo if needed

Or use undo branches:

:%s/old/new/g
:earlier 1f          " Go back to before last file write

4. Use Very Magic for Complex Patterns

/\v(foo|bar)\d+      " Clearer than /\(foo\|bar\)\d\+

5. Capture Groups for Rearranging

:%s/\v(\w+) (\w+)/\2, \1/   " Swap words

6. Check Range Before Substituting

:5,10s/old/new/g     " Only lines 5-10
:'<,'>s/old/new/g    " Visual selection

7. Use Global Command for Line Operations

:g/pattern/d         " Delete matching lines
:g/pattern/normal @a " Execute macro on matches

8. Preserve Original in Register

let @a = @/          " Save search pattern to register a
:%s/old/new/g
let @/ = @a          " Restore search pattern

9. Use :help pattern for Reference

:help pattern        " Complete regex reference
:help sub-replace-expression  " Expression replacement
:help :substitute    " Substitute command details

10. Combine Search with Marks

/pattern<CR>         " Find pattern
ma                   " Mark position
# Navigate elsewhere
`a                   " Return to mark

4.10 Search Integration with Plugins

Telescope (Fuzzy Finder)

require('telescope.builtin').live_grep()      -- Search in files
require('telescope.builtin').grep_string()    -- Search word under cursor
require('telescope.builtin').search_history() -- Search history

Keymaps:

vim.keymap.set('n', '<leader>fg', ':Telescope live_grep<CR>', 
  { desc = 'Live grep' })
vim.keymap.set('n', '<leader>fw', ':Telescope grep_string<CR>',
  { desc = 'Grep word under cursor' })
vim.keymap.set('n', '<leader>fo', ':Telescope oldfiles<CR>',
  { desc = 'Recent files' })

Hop / Flash (Quick Navigation)

require('hop').hint_patterns({ current_line_only = true })  -- Search on line
require('hop').hint_patterns()                               -- Search in buffer

Keymaps:

vim.keymap.set('n', 's', ':HopPattern<CR>', { desc = 'Hop to pattern' })
vim.keymap.set('n', 'S', ':HopChar2<CR>', { desc = 'Hop to 2 chars' })

Shows search count and positions in virtual text:

require('hlslens').setup({
  calm_down = true,
  nearest_only = true,
})

vim.keymap.set('n', 'n', [[<Cmd>execute('normal! ' . v:count1 . 'n')<CR><Cmd>lua require('hlslens').start()<CR>]])
vim.keymap.set('n', 'N', [[<Cmd>execute('normal! ' . v:count1 . 'N')<CR><Cmd>lua require('hlslens').start()<CR>]])

Searchbox.nvim (Better Search UI)

require('searchbox').setup({
  popup = {
    border = { style = 'rounded' },
  },
})

vim.keymap.set('n', '/', ':SearchBoxIncSearch<CR>', { desc = 'Incremental search' })
vim.keymap.set('n', '?', ':SearchBoxIncSearch reverse=true<CR>', { desc = 'Reverse search' })

4.11 Practical Search and Replace Scenarios

Scenario 1: Refactoring Function Names

Task: Rename calculateTotal to computeTotal across multiple files.

:args **/*.js                              " Load all JS files
:argdo %s/\<calculateTotal\>/computeTotal/ge | update

Scenario 2: Update Import Paths

Task: Change import from '../utils' to import from '@/utils'.

:args src/**/*.js
:argdo %s/from ['"]\.\.\/utils['"]/from '@\/utils'/ge | update

Scenario 3: Add Type Annotations

Task: Add TypeScript type to function parameters.

Before:

function process(data) {

After:

function process(data: any) {

Command:

:%s/\vfunction (\w+)\((\w+)\)/function \1(\2: any)/g

Scenario 4: Format JSON

Task: Pretty-print JSON data.

:%!python -m json.tool     " Use external formatter

Or with jq:

:%!jq .                    " Format with jq

Scenario 5: Extract Configuration Values

Task: Find all hardcoded API URLs.

:vimgrep /https:\/\/api\./ **/*.js
:copen                     " Open quickfix to review all

Scenario 6: Batch Comment

Task: Comment out all console.log statements.

:g/console\.log/s/^/\/\/ /    " Add // comment

Or with visual block:

/console\.log<CR>         " Find first
<C-v>                      " Block mode
# Select multiple lines
I// <Esc>                 " Comment

Scenario 7: Clean Up Logs

Task: Remove all debug print statements.

:g/print.*debug/d          " Delete debug prints
:g/console\.debug/d        " Delete console.debug

Scenario 8: Normalize Whitespace

Task: Ensure consistent spacing around operators.

:%s/\s*=\s*/ = /g          " Space around =
:%s/\s*+\s*/ + /g          " Space around +
:%s/\s*,\s*/, /g           " Space after comma

Scenario 9: Extract TODO Comments

Task: Collect all TODO comments to end of file.

:g/TODO/t$                 " Copy to end

Or to a new buffer:

qaq                        " Clear register a
:g/TODO/y A                " Append to register a
:new                       " New buffer
"aP                        " Paste

Scenario 10: Version Number Update

Task: Increment version numbers.

Before:

"version": "1.2.3"

After:

"version": "1.2.4"

Command:

:g/version/s/\d\+$/\=submatch(0) + 1/

4.12 Search and Replace Exercises

Exercise 1: Basic Pattern Matching

Text: Contact: john@example.com Email: jane@test.org Reach us: support@company.net

Task: Extract all email addresses.

Solution:

:g/\v\w+@\w+\.\w+/t$

Exercise 2: Swap Date Format

Text: 2024-10-19 2024-11-25 2024-12-31

Task: Convert to DD/MM/YYYY.

Solution:

:%s/\v(\d{4})-(\d{2})-(\d{2})/\3\/\2\/\1/

Exercise 3: Remove Code Comments

Text:

x = 10  # This is a comment
y = 20  # Another comment
z = x + y

Task: Remove inline comments.

Solution:

:%s/\s*#.*$//

Exercise 4: Capitalize First Letter

Text: hello world welcome to vim happy coding

Task: Capitalize first letter of each line.

Solution:

:%s/\v^(\w)/\u\1/

Exercise 5: Create Numbered List

Text: apple banana cherry date

Task: Convert to numbered list.

Solution:

:%s/^/\=line('.') . '. '/

Result:

  1. apple

  2. banana

  3. cherry

  4. date

Exercise 6: Complex Refactoring

Text:

var name = "John";
var age = 30;
var city = "NYC";

Task: Convert var to const and add type annotations.

Solution:

:%s/\vvar (\w+) = (.+);/const \1: string = \2;/

For numbers, you’d need multiple passes or more complex pattern.


Chapter Summary

In this chapter, you mastered:

  • Search fundamentals: /, ?, *, #, n, N, search offsets

  • Regular expressions: Magic modes, character classes, quantifiers, anchors

  • Advanced regex: Grouping, backreferences, lookahead/lookbehind, \zs, \ze

  • Search configuration: Case sensitivity, highlighting, incremental search

  • Substitute command: Full syntax, ranges, flags, special replacements

  • Captured groups: Using \1, \2 for rearranging text

  • Expression replacement: \= for calculations and transformations

  • Global command: :g, :v for line-based operations

  • Multi-line search: \_s, \_. for patterns across lines

  • Search history: q/, command history, clearing highlights

  • Very magic mode: \v for cleaner complex patterns

  • Pattern cookbook: Common search/replace patterns for real tasks

  • Multiple files: vimgrep, grep, args, argdo, cdo, cfdo

  • Quickfix integration: Navigate search results across files

  • Best practices: Testing patterns, using confirmation, saving state

  • Plugin integration: Telescope, Hop, hlslens for enhanced search

  • Real-world scenarios: Refactoring, formatting, extracting data

The combination of Vim’s regex engine, substitute command, and global command creates an incredibly powerful text transformation system. With practice, you’ll find yourself solving complex text manipulation tasks in seconds that would take minutes in other editors.

In the next chapter, we’ll explore Command-Line Mode in depth, covering Ex commands, command-line editing, command ranges, and building custom commands to automate your workflow.


Chapter 5: Command-Line Mode and Ex Commands

Command-Line Mode is Vim’s powerhouse for executing complex operations, automating workflows, and manipulating text across entire files or projects. While Normal mode handles interactive editing, Command-Line mode excels at batch operations, file management, and advanced text transformations. This chapter explores Ex commands, command-line editing, ranges, and building custom commands.

5.1 Entering and Exiting Command-Line Mode

Entry Methods

:                " Enter command-line mode
/                " Enter search forward
?                " Enter search backward
!                " Enter filter (with motion)

From Insert mode:

<C-o>:           " Execute one command, return to Insert mode

From Visual mode:

:                " Automatically adds :'<,'> (visual range)

Exit Methods

<CR>             " Execute command
<Esc>            " Cancel and return to Normal mode
<C-c>            " Cancel (same as Esc)
<C-[>            " Cancel (same as Esc)

Command-Line Window

Open an editable command history window:

q:               " Open command-line window (from Normal mode)
<C-f>            " Open from Command-line mode
q/               " Open search history window
q?               " Open backward search history window

Usage:

  • Edit commands like regular text

  • j/k to navigate history

  • <CR> to execute command under cursor

  • <C-c> to close without executing

Configuration:

vim.opt.cmdwinheight = 10  -- Height of command window

5.2 Command-Line Editing

Cursor Movement

<C-b>            " Beginning of line (or <Home>)
<C-e>            " End of line (or <End>)
<Left>/<Right>   " Move one character
<C-Left>/<C-Right> " Move one word

Editing Operations

<C-u>            " Delete to beginning of line
<C-w>            " Delete word before cursor
<C-h>            " Delete character before cursor (or <BS>)
<Del>            " Delete character under cursor

Examples:

:echo "hello world"
" Press <C-u> → clears entire line
" Press <C-w> → deletes "world"

Insert from Registers

<C-r>{register}  " Insert contents of register
<C-r><C-w>       " Insert word under cursor
<C-r><C-a>       " Insert WORD under cursor (includes punctuation)
<C-r><C-f>       " Insert filename under cursor
<C-r><C-l>       " Insert line under cursor

Common registers:

<C-r>"           " Insert unnamed register (last yank/delete)
<C-r>+           " Insert system clipboard
<C-r>/           " Insert last search pattern
<C-r>:           " Insert last command
<C-r>%           " Insert current filename
<C-r>#           " Insert alternate filename

Examples:

" Yank a word in Normal mode
yiw
" In Command-line mode:
:echo "<C-r>""   " Pastes yanked word

" With cursor on a filename:
:e <C-r><C-f>    " Opens that file

Command-Line History

<Up>/<Down>      " Navigate command history
<C-p>/<C-n>      " Navigate history (Vim style)
<PageUp>/<PageDown> " Scroll history
<S-Up>/<S-Down>  " Navigate filtered history (matching typed prefix)

Smart history navigation:

:set            " Type 'set' then <Up> to cycle only 'set' commands

Command-Line Completion

<Tab>            " Complete command/file/option
<C-d>            " List all completions
<C-a>            " Insert all completions
<S-Tab>          " Complete backward

Completion modes:

vim.opt.wildmenu = true          -- Enhanced command-line completion
vim.opt.wildmode = 'longest:full,full'  -- Completion behavior
vim.opt.wildignore = '*.o,*.pyc,*.class'  -- Ignore patterns

Examples:

:colorsche<Tab>  " Completes to :colorscheme
:e ~/.con<Tab>   " Completes filename
:set number<C-d> " Lists all options starting with 'number'

Better completion with settings:

vim.opt.wildmenu = true
vim.opt.wildmode = 'longest:full,full'
vim.opt.wildoptions = 'pum'  -- Use popup menu for completion (Neovim)

Expression Register in Command-Line

<C-r>=           " Insert result of expression

Examples:

:echo <C-r>=2+2<CR>       " Inserts '4'
:e file_<C-r>=strftime("%Y%m%d")<CR>.txt  " file_20241019.txt

Advanced usage:

" In Lua (Neovim):
:lua print(<C-r>=vim.fn.expand('%:t')<CR>)  " Print current filename

5.3 Command Ranges

Ranges specify which lines a command affects.

Basic Range Syntax

:[range]command

:5,10command     " Lines 5 to 10
:%command        " All lines (entire file)
:.command        " Current line (default for most commands)
:$command        " Last line

Range Specifiers

.                " Current line
$                " Last line
%                " All lines (equivalent to 1,$)
'm               " Line with mark m
'<               " Start of visual selection
'>               " End of visual selection
/pattern/        " Next line matching pattern
?pattern?        " Previous line matching pattern
\&               " Line with last substitute match

Range Arithmetic

.+5              " 5 lines after current
.-3              " 3 lines before current
$-10             " 10 lines before end
/pattern/+2      " 2 lines after pattern match

Examples:

:.-5,.+5s/old/new/g    " Replace in 11 lines (5 before, current, 5 after)
:/foo/,/bar/d          " Delete from 'foo' to 'bar'
:.,$y                  " Yank from current line to end
:1,10t$                " Copy lines 1-10 to end of file

Common Range Patterns

:%               " Entire file (most common)
:'<,'>           " Visual selection (automatically added)
:.,.+5           " Current line plus next 5
:1,$             " First to last (same as %)
:.,/pattern/     " Current line to next pattern match
:/start/,/end/   " Between two patterns

Range Offset Examples

:10,20           " Lines 10 through 20
:10,+5           " Line 10 plus next 5 lines (10-15)
:-5,+5           " 5 before current to 5 after current
:/TODO/,/DONE/   " From line with TODO to line with DONE
:'a,'b           " From mark a to mark b

Using Patterns as Ranges

:/function/,/end/delete    " Delete from 'function' to 'end'
:/class/+1,/}/s/^/  /      " Indent inside class definition
:g/pattern/,/^$/delete     " Delete each pattern block (pattern to empty line)

Complex example:

# START
def function():
    pass
# END

# START
def another():
    pass
# END
:g/# START/+1,/# END/-1delete  " Delete function bodies between markers

Visual Selection Range

When you press : in Visual mode:

:'<,'>               " Automatically inserted

This represents the selected lines. Common usage:

:'<,'>s/old/new/g    " Replace in selection
:'<,'>!sort          " Sort selected lines
:'<,'>w partial.txt  " Write selection to file
:'<,'>normal @a      " Execute macro on each line

5.4 Essential Ex Commands

File Operations

:e {file}            " Edit file
:e!                  " Reload current file (discard changes)
:e #                 " Edit alternate file
:e %:h/{file}        " Edit file in same directory
:enew                " New empty buffer
:find {file}         " Find and edit file in 'path'
:w                   " Write (save) file
:w {file}            " Write to specific file
:w!                  " Force write
:w >> {file}         " Append to file
:sav {file}          " Save as (and edit new file)
:up[date]            " Write only if modified
:wa                  " Write all modified buffers
:wq                  " Write and quit
:x                   " Write if modified, then quit (same as :wq)
:q                   " Quit
:q!                  " Quit without saving
:qa                  " Quit all windows
:qa!                 " Force quit all, discard changes

Examples:

:e ~/.config/nvim/init.lua    " Edit Neovim config
:w                             " Save
:w backup.lua                  " Save copy as backup.lua
:e #                           " Return to previous file
:sav new_name.lua              " Save as and switch to new file
:up                            " Save only if changed
:x                             " Save and exit

Buffer Commands

:ls                  " List buffers
:buffers             " Same as :ls
:files               " Same as :ls
:b {n}               " Switch to buffer n
:b {name}            " Switch to buffer by name (partial match)
:bn                  " Next buffer
:bp                  " Previous buffer
:bf                  " First buffer
:bl                  " Last buffer
:b#                  " Alternate buffer
:bd                  " Delete current buffer
:bd {n}              " Delete buffer n
:bd!                 " Force delete buffer (discard changes)
:%bd                 " Delete all buffers
:%bd|e#|bd#          " Delete all except current

Buffer listing symbols: % - Current buffer # - Alternate buffer a - Active buffer (loaded and visible) h - Hidden buffer (loaded but not visible)

    • Non-modifiable buffer = - Readonly buffer
    • Modified buffer x - Buffer with errors

Examples:

:ls                  " List all buffers
:b init             " Switch to buffer matching 'init'
:bd 3               " Delete buffer 3
:bn                 " Next buffer
:b#                 " Previous buffer (alternate)

Delete all buffers except current:

:%bd|e#|bd#

Explanation: Delete all, edit alternate (#), delete alternate again.

Window Commands

:sp[lit] {file}      " Split horizontally, optionally open file
:vs[plit] {file}     " Split vertically
:new                 " New horizontal split
:vnew                " New vertical split
:on[ly]              " Close all windows except current
:clo[se]             " Close current window
:q                   " Quit window (same as :close)
:hide                " Hide current window
:resize {n}          " Resize horizontal split to n lines
:resize +5           " Increase height by 5
:resize -5           " Decrease height by 5
:vertical resize {n} " Resize vertical split to n columns

Window navigation (Ex commands):

<C-w>h               " Move to left window
<C-w>j               " Move to window below
<C-w>k               " Move to window above
<C-w>l               " Move to right window
<C-w>w               " Cycle through windows
<C-w>p               " Previous window
<C-w>o               " Close all except current (same as :only)

Examples:

:sp                  " Split current buffer
:vs ~/.bashrc        " Vertical split, open bashrc
:resize 20           " Set height to 20 lines
:vertical resize 80  " Set width to 80 columns
:only                " Close all other windows

Tab Commands

:tabe[dit] {file}    " Open file in new tab
:tabnew              " New empty tab
:tabc[lose]          " Close current tab
:tabo[nly]           " Close all tabs except current
:tabn[ext]           " Next tab
:tabp[revious]       " Previous tab
:tabfirst            " First tab
:tablast             " Last tab
:tabs                " List all tabs
:tabm[ove] {n}       " Move tab to position n
:tabm[ove] +1        " Move tab right
:tabm[ove] -1        " Move tab left

Tab navigation shortcuts:

gt                   " Next tab
gT                   " Previous tab
{n}gt                " Go to tab n

Examples:

:tabe file.txt       " Open in new tab
:tabn                " Next tab
:tabm 0              " Move to first position
:tabo                " Close all other tabs

Display Commands

:echo {expr}         " Display expression
:echom {expr}        " Display and save to message history
:echon {expr}        " Display without newline
:mes[sages]          " Show message history
:mes clear           " Clear message history
:echohl {group}      " Set highlight group for next echo
:echohl None         " Reset highlight

Examples:

:echo "Hello"                    " Display message
:echo expand('%:p')              " Display full file path
:echom "Saved to history"        " Display and save
:messages                        " View history
:echohl WarningMsg | echo "Warning!" | echohl None

Neovim-specific (Lua):

:lua print("Hello from Lua")
:lua vim.notify("Notification", vim.log.levels.INFO)

Marks and Jumps

:marks               " List all marks
:marks {a-z}         " List specific marks
:delmarks {marks}    " Delete marks
:delmarks!           " Delete all lowercase marks
:jumps               " List jump history
:changes             " List change history
:clearjumps          " Clear jump history

Examples:

ma                   " Set mark 'a'
:marks               " View all marks
'a                   " Jump to mark 'a'
:delmarks a          " Delete mark 'a'
:delmarks a-z        " Delete marks a through z

Registers

:reg[isters]         " Display all registers
:reg {register}      " Display specific register
:let @a='text'       " Set register 'a' to 'text'

Examples:

:reg                 " Show all registers
:reg a               " Show register 'a'
:let @a='hello'      " Set register a
"ap                  " Paste from register a

Options

:set {option}        " Enable boolean option
:set no{option}      " Disable boolean option
:set {option}!       " Toggle boolean option
:set {option}?       " Query option value
:set {option}={value} " Set option to value
:set {option}+=value  " Append to option
:set {option}-=value  " Remove from option
:set all             " Show all options
:set all&            " Reset all options to defaults
:setlocal {option}   " Set option locally to buffer/window
:setglobal {option}  " Set option globally

Examples:

:set number          " Enable line numbers
:set nonumber        " Disable line numbers
:set number!         " Toggle line numbers
:set number?         " Query current value
:set tabstop=4       " Set tab width to 4
:set path+=**        " Add recursive search to path
:set all             " Show all settings

Neovim (Lua API):

:lua vim.opt.number = true
:lua vim.opt.tabstop = 4
:lua print(vim.opt.number:get())

5.5 Advanced Ex Commands

Substitute (Detailed)

We covered substitution in Chapter 4, but here are Ex-specific aspects:

:[range]s/{pattern}/{replacement}/{flags}
:s/old/new/          " Current line
:%s/old/new/g        " Entire file
:'<,'>s/old/new/g    " Visual selection
:5,10s/old/new/gc    " Lines 5-10 with confirmation

Repeat last substitute:

:s                   " Repeat with same flags
:&                   " Repeat with same flags
:~                   " Repeat with same replacement, new pattern
:&&                  " Repeat with same flags on all lines

Substitute special cases:

:s/pattern//         " Delete pattern (replace with nothing)
:s//new/             " Use last search pattern
:s/old/\=expression/ " Replace with expression result

Global Command

:[range]g/{pattern}/{command}
:[range]g!/{pattern}/{command}
:[range]v/{pattern}/{command}  " Inverse (same as g!)

Common global patterns:

:g/pattern/d         " Delete matching lines
:g/pattern/m$        " Move matching lines to end
:g/pattern/t$        " Copy matching lines to end
:g/pattern/y A       " Yank matching lines to register A
:g/pattern/normal @a " Execute macro 'a' on matching lines
:g/pattern/s/old/new/g " Substitute on matching lines

Complex global commands:

:g/TODO/+1,/^$/d     " Delete from line after TODO to next blank line
:g/^$/,/./-1sort     " Sort paragraphs (blank to blank)
:g/class/.,/^}/>>    " Indent class bodies

Nested global:

:g/outer/g/inner/d   " On lines with 'outer', delete if also has 'inner'

Normal Command

Execute Normal mode commands from Ex:

:[range]norm[al][!] {commands}

Examples:

:%norm A;            " Append ';' to every line
:'<,'>norm I//       " Comment visual selection
:5,10norm 0d$        " Delete content of lines 5-10
:%norm @a            " Execute macro 'a' on all lines
:g/pattern/norm dd   " Delete matching lines (alternative to :g/pattern/d)

With ! flag:

:norm! x             " Execute 'x' without mappings

Sort

:[range]sor[t][!] [options]

Options:

:sort                " Sort lines
:sort!               " Reverse sort
:sort i              " Case-insensitive sort
:sort u              " Remove duplicates
:sort n              " Numeric sort
:sort /pattern/      " Sort by pattern match

Examples:

:%sort               " Sort entire file
:'<,'>sort           " Sort selection
:sort! n             " Reverse numeric sort
:sort u              " Sort and remove duplicates
:10,20sort i         " Case-insensitive sort of lines 10-20

Sort by pattern:

:sort /\d\+/ n       " Sort by numbers in lines

Text: item_3 item_1 item_10 item_2

After :sort /\d\+/ n: item_1 item_2 item_3 item_10

External Commands

:!{command}          " Execute shell command
:r !{command}        " Read command output into buffer
:w !{command}        " Pipe buffer to command
:[range]!{filter}    " Filter lines through command

Examples:

:!ls                 " Execute 'ls' in shell
:!git status         " Show git status
:r !date             " Insert current date
:r !ls               " Insert directory listing
:%!sort              " Sort entire file with external sort
:'<,'>!column -t     " Format selection as table
:w !wc -l            " Count lines (without saving)
:w !sh               " Execute buffer as shell script

Pretty-print JSON:

:%!python -m json.tool

Or with jq:

:%!jq .

Read and Write

:r {file}            " Read file into buffer
:r !{command}        " Read command output
:w {file}            " Write to file
:w !{command}        " Pipe to command
:w >> {file}         " Append to file
:{range}w {file}     " Write range to file

Examples:

:r template.txt      " Insert template file
:r !date             " Insert date
:5,10w partial.txt   " Write lines 5-10 to file
:.w >> log.txt       " Append current line to log
:%w !pbcopy          " Copy buffer to macOS clipboard (pipe to pbcopy)

Copy and Move

:[range]co[py] {address}
:[range]t {address}      " Short form of :copy
:[range]m[ove] {address}

Examples:

:t$                  " Copy current line to end
:5,10t.              " Copy lines 5-10 to after current line
:.,+3m$              " Move current line and next 3 to end
:g/TODO/t$           " Copy all TODO lines to end
:'<,'>m0             " Move selection to top of file

Join

:[range]j[oin][!]

Examples:

:j                   " Join current and next line
:5,10j               " Join lines 5-10 into one
:j!                  " Join without adding space
:'<,'>j              " Join visual selection
:[range]p[rint]      " Print lines
:[range]l[ist]       " Print with special chars visible
:[range]#            " Print with line numbers
:[range]p #          " Print with line numbers (same as :#)

Examples:

:10,20p              " Print lines 10-20
:l                   " Show current line with invisible chars
:%l                  " Show all lines with special chars
:g/pattern/p         " Print matching lines (like grep)
:g/pattern/#         " Print matching lines with numbers

Undo and Redo

:u[ndo]              " Undo
:u {n}               " Undo to state n (from :undolist)
:red[o]              " Redo
:earlier {count}     " Go back in time
:later {count}       " Go forward in time
:undolist            " Show undo states

Time-based undo:

:earlier 5m          " Go back 5 minutes
:earlier 10s         " Go back 10 seconds
:earlier 1h          " Go back 1 hour
:earlier 2d          " Go back 2 days
:later 5m            " Go forward 5 minutes

File-based undo:

:earlier 1f          " Before last file save
:earlier 2f          " Before 2 file saves ago
:later 1f            " After next file save

Examples:

:earlier 3m          " Undo to 3 minutes ago
:later 30s           " Redo to 30 seconds later
:undolist            " View undo tree states
:undo 5              " Undo to state 5

Runtime Commands

:so[urce] {file}     " Execute commands from file
:so %                " Source current file
:runtime {file}      " Source file from runtimepath

Examples:

:source ~/.vimrc     " Reload vimrc
:so %                " Source current file
:runtime colors/desert.vim  " Load color scheme

Neovim Lua:

:luafile {file}      " Execute Lua file
:luafile %           " Execute current Lua file

Help

:h[elp] {topic}      " Open help
:h[elp]              " Open general help
:helpgrep {pattern}  " Search help files
:h {topic}<C-d>      " List help topics

Examples:

:h :substitute       " Help on substitute command
:h pattern           " Help on patterns
:h nvim-lua          " Neovim Lua guide
:h options           " Help on all options
:helpgrep TODO       " Search for TODO in help

Navigate help:

<C-]>                " Jump to tag under cursor
<C-t>                " Jump back
:h motion.txt        " Open specific help file

5.6 Command Modifiers

Modifiers change how commands execute:

:verbose {command}   " Show verbose output
:silent {command}    " Suppress output
:silent! {command}   " Suppress output and errors
:hide {command}      " Hide buffer instead of unloading
:keepalt {command}   " Don't change alternate file
:keepjumps {command} " Don't change jump list
:keepmarks {command} " Don't change marks
:keeppatterns {command} " Don't change search pattern
:lockmarks {command} " Don't adjust marks
:noautocmd {command} " Don't trigger autocommands
:topleft {command}   " Open splits at top/left
:botright {command}  " Open splits at bottom/right
:vertical {command}  " Make split vertical
:tab {command}       " Open in new tab

Examples:

:verbose set number?          " Show where 'number' was last set
:silent !make                 " Compile without output
:silent! source ~/.extra.vim  " Source if exists, no error
:keepjumps /pattern           " Search without affecting jump list
:noautocmd write              " Save without triggering autocmds
:vertical help pattern        " Open help in vertical split
:tab help motion              " Open help in new tab
:topleft split                " Split at top
:botright vsplit              " Vertical split at right

Combining modifiers:

:silent! noautocmd write      " Save silently without autocmds
:vertical botright split      " Vertical split at bottom-right

5.7 Custom Commands

Defining Commands

:command[!] {name} {replacement}

Components:

  • ! - Force overwrite existing command

  • {name} - Command name (must start with uppercase)

  • {replacement} - What the command expands to

Simple examples:

:command W w                  " Alias :W to :w
:command Q q                  " Alias :Q to :q
:command Wq wq                " Alias :Wq to :wq

Neovim (Lua):

vim.api.nvim_create_user_command('W', 'w', {})
vim.api.nvim_create_user_command('Q', 'q', {})

Command Arguments


-nargs=0     " No arguments (default)

-nargs=1     " Exactly one argument

-nargs=*     " Any number of arguments

-nargs=?     " 0 or 1 argument

-nargs=+     " One or more arguments

<args>       " Placeholder for arguments
<f-args>     " Arguments as separate items (for functions)
<q-args>     " Arguments as single quoted string

Examples:

:command -nargs=1 Search vimgrep /<args>/ **/*.txt
:command -nargs=* Tabopen tabe <args>
:command -nargs=? Backup w! <args>.bak

Usage:

:Search pattern              " Searches for 'pattern'
:Tabopen file1.txt file2.txt " Opens both in tabs
:Backup                      " Saves as current_file.bak
:Backup myfile               " Saves as myfile.bak

Command with Range


-range       " Command accepts range (defaults to current line)

-range=%     " Defaults to whole file

-range=5     " Defaults to 5 lines

<line1>      " Start of range
<line2>      " End of range

Examples:

:command -range CommentLines <line1>,<line2>s/^/# /
:command -range=% SortFile <line1>,<line2>sort

Usage:

:5,10CommentLines    " Comment lines 5-10
:CommentLines        " Comment current line
:%SortFile           " Sort entire file

Command with Count


-count={default}     " Accept count (with default)

<count>              " The count value

Example:

:command -count=1 Dup <line1>t<line2>+<count>

Usage:

:3Dup        " Duplicate line 3 times
:Dup         " Duplicate once (default)

Command Completion


-complete=file       " File completion

-complete=buffer     " Buffer completion

-complete=command    " Command completion

-complete=custom,{function} " Custom completion

-complete=customlist,{function} " Custom list

Examples:

:command -nargs=1 -complete=file Edit e <args>
:command -nargs=1 -complete=buffer Buf b <args>
:command -nargs=* -complete=command Exec <args>

Custom completion (Neovim Lua):

vim.api.nvim_create_user_command('MyCmd', function(opts)
  print(opts.args)
end, {
  nargs = 1,
  complete = function(arg_lead, cmd_line, cursor_pos)
    return {'option1', 'option2', 'option3'}
  end,
})

Practical Custom Commands

Save as root:

:command W w !sudo tee % > /dev/null

Format JSON:

:command FormatJSON %!python -m json.tool

Open config:

:command Config e ~/.config/nvim/init.lua

Reload config:

:command Reload source ~/.config/nvim/init.lua

Delete trailing whitespace:

:command Trim %s/\s\+$//e

Toggle relative numbers:

:command ToggleRelative set relativenumber!

Git commands:

:command Gstatus !git status
:command Gdiff !git diff %
:command Gblame !git blame %

Neovim (Lua) - More complex:


-- Open file in vertical split
vim.api.nvim_create_user_command('Vs', function(opts)
  vim.cmd('vsplit ' .. opts.args)
end, {
  nargs = 1,
  complete = 'file',
})


-- Search and replace in project
vim.api.nvim_create_user_command('Replace', function(opts)
  local args = vim.split(opts.args, ' ')
  if #args ~= 2 then
    print('Usage: Replace <old> <new>')
    return
  end
  vim.cmd(string.format('args **/*.lua | argdo %%s/%s/%s/ge | update', args[1], args[2]))
end, {
  nargs = '+',
})


-- Create scratch buffer
vim.api.nvim_create_user_command('Scratch', function()
  vim.cmd('enew')
  vim.bo.buftype = 'nofile'
  vim.bo.bufhidden = 'hide'
  vim.bo.swapfile = false
end, {})

Deleting Custom Commands

:delcommand {name}   " Delete command
:comclear            " Delete all user-defined commands

5.8 Command-Line Expressions

Literal Insert

<C-v>{char}          " Insert character literally
<C-v><Tab>           " Insert literal tab
<C-v><CR>            " Insert literal newline (in some contexts)

String Concatenation

:echo "Hello " . "World"
:echo "File: " . expand('%')

Conditionals in Commands

:if {condition} | {command} | endif

Example:

:if line('$') > 100 | echo "Large file" | else | echo "Small file" | endif

Multiple lines:

:if &modified
:  echo "File is modified"
:else
:  echo "File is unchanged"
:endif

Command with Ternary

:echo &modified ? "Modified" : "Unchanged"
:execute &modified ? 'write' : 'echo "No changes"'

Execute Command

:execute builds and executes a command string:

:execute "normal! " . v:count1 . "j"
:execute "edit " . expand('%:h') . '/new.txt'

Examples:

:let n = 5
:execute n . "," . (n+5) . "delete"    " Delete lines 5-10

:let filename = "config.lua"
:execute "edit " . filename            " Open file

:execute "normal! gg=G"                " Format file

Command Chaining

Chain multiple commands with |:

:w | source %                " Save and source
:e file.txt | vsplit other.txt  " Open two files
:%s/old/new/g | w | q        " Replace, save, quit

With conditionals:

:if &modified | write | endif | quit

5.9 Command-Line Functions

Expand Function

:echo expand('%')        " Current filename
:echo expand('%:p')      " Full path
:echo expand('%:h')      " Directory
:echo expand('%:t')      " Filename only
:echo expand('%:r')      " Root (without extension)
:echo expand('%:e')      " Extension only

Modifiers:

%:p              " Full path
%:p:h            " Directory of full path
%:p:t            " Filename from full path
%:r              " Remove extension
%:e              " Extension only
%:t:r            " Filename without extension
%:s/old/new/     " Substitute in filename
%:gs/old/new/    " Global substitute

Examples:

" Current file: /home/user/project/src/main.lua
:echo expand('%')      " src/main.lua
:echo expand('%:p')    " /home/user/project/src/main.lua
:echo expand('%:h')    " src
:echo expand('%:t')    " main.lua
:echo expand('%:r')    " src/main
:echo expand('%:e')    " lua
:echo expand('%:t:r')  " main

Line Functions

line('.')            " Current line number
line('$')            " Last line number
line('w0')           " First line in window
line('w$')           " Last line in window
line("'a")           " Line of mark a

Column Functions

col('.')             " Current column
col('$')             " Last column in line
virtcol('.')         " Virtual column (accounts for tabs)

File Functions

filereadable({file}) " Check if file is readable
isdirectory({dir})   " Check if directory exists
executable({prog})   " Check if program exists
glob({pattern})      " Get matching files
globpath({path}, {pattern}) " Search in path

Examples:

:if filereadable('config.lua')
:  source config.lua
:endif

:if executable('rg')
:  set grepprg=rg\ --vimgrep
:endif

Buffer/Window Functions

bufnr('%')           " Current buffer number
bufname('%')         " Current buffer name
winnr()              " Current window number
bufexists({buf})     " Check if buffer exists
buflisted({buf})     " Check if buffer is listed

5.10 Command-Line Abbreviations

Defining Abbreviations

:cabbrev {lhs} {rhs}     " Command-line abbreviation
:cnoreabbrev {lhs} {rhs} " Non-recursive abbreviation
:cunabbrev {lhs}         " Remove abbreviation
:cabclear                " Clear all abbreviations

Examples:

:cabbrev W w
:cabbrev Q q
:cabbrev Wq wq
:cabbrev Set set

With condition (only expand at start of line):

:cnoreabbrev <expr> W getcmdtype() == ':' && getcmdline() == 'W' ? 'w' : 'W'

Neovim (Lua):

vim.cmd('cabbrev W w')
vim.cmd('cabbrev Q q')


-- Or with function:
local function cabbrev(lhs, rhs)
  vim.cmd(string.format('cabbrev %s %s', lhs, rhs))
end

cabbrev('W', 'w')
cabbrev('Wq', 'wq')

Practical Abbreviations

:cabbrev h vert help     " Vertical help
:cabbrev vh vert help    " Same
:cabbrev Trim %s/\s\+$//e   " Trailing whitespace
:cabbrev Json %!python -m json.tool  " Format JSON

5.11 Command-Line Best Practices

1. Use Command Completion

Always use <Tab> for completion:

:colorsch<Tab>       " Complete to :colorscheme
:e ~/.con<Tab>       " Complete file path

2. Leverage History

:<Up>                " Navigate history
:his :               " View command history
q:                   " Edit history in window

3. Create Useful Aliases

:command W w
:command Q q
:command Wq wq
:command WQ wq

4. Use Ranges Effectively

:'<,'>               " Visual selection
:.,$                 " Current to end
:%                   " Entire file

5. Chain Commands

:w | source %        " Save and reload
:%s/old/new/g | w    " Replace and save

6. Use Verbose for Debugging

:verbose set number?         " Where was option set?
:verbose map <leader>        " Where was mapping defined?

7. Silent for Cleaner Output

:silent !make        " Compile without spam
:silent write        " Save without message

8. Custom Commands for Workflows

:command Config edit ~/.config/nvim/init.lua
:command Reload source ~/.config/nvim/init.lua
:command Todo vimgrep /TODO/ **/*.lua | copen

9. Use Expression Register

<C-r>=2+2<CR>        " Insert 4
<C-r>=strftime('%Y-%m-%d')<CR>  " Insert date

10. Learn Key Combinations

<C-r><C-w>           " Insert word under cursor
<C-r><C-f>           " Insert filename under cursor
<C-r>%               " Insert current filename

5.12 Command-Line Mappings

Map keys specifically for Command-line mode:

:cnoremap <C-a> <Home>       " Ctrl-A to start
:cnoremap <C-e> <End>        " Ctrl-E to end (already default)
:cnoremap <C-p> <Up>         " Ctrl-P for history (already default)
:cnoremap <C-n> <Down>       " Ctrl-N for history

Neovim (Lua):

vim.keymap.set('c', '<C-a>', '<Home>', { desc = 'Beginning of line' })
vim.keymap.set('c', '<C-e>', '<End>', { desc = 'End of line' })

Practical mappings:


-- Sudo save
vim.keymap.set('c', 'w!!', 'w !sudo tee % >/dev/null', { desc = 'Save with sudo' })


-- Quick help in vertical split
vim.keymap.set('c', 'vh', 'vert help', { desc = 'Vertical help' })


-- Expand current directory
vim.keymap.set('c', '%%', "getcmdtype() == ':' ? expand('%:h').'/' : '%%'",
  { expr = true, desc = 'Expand directory' })

Usage:

:e %%            " Expands to :e current/directory/

5.13 Advanced Command Patterns

Conditional Execution

:if {condition}
:  {command}
:else
:  {command}
:endif

Example:

:if &modified
:  write
:  echo "Saved"
:else
:  echo "No changes"
:endif

One-liner:

:if &modified | write | endif

Loops

:for i in range(1, 10)
:  execute "echo " . i
:endfor

Example - Comment multiple lines:

:for i in range(5, 10)
:  execute i . "s/^/# /"
:endfor

Neovim (Lua):

:lua for i = 1, 10 do print(i) end

Try-Catch

:try
:  source risky_config.vim
:catch
:  echo "Failed to load config"
:endtry

Functions in Commands

:function! MyFunction()
:  echo "Hello from function"
:endfunction

:call MyFunction()

With arguments:

:function! Greet(name)
:  echo "Hello, " . a:name
:endfunction

:call Greet("World")

Neovim (Lua) - Preferred:

:lua function greet(name) print("Hello, " .. name) end
:lua greet("World")

5.14 Command-Line Recipes

Open File Under Cursor

:e <C-r><C-f>        " Insert filename, then edit

Or map it:

gf                   " Built-in: go to file

Search and Replace Across Project

:args **/*.lua
:argdo %s/old/new/ge | update

Delete All Marks

:delmarks!           " Delete all lowercase marks
:delmarks A-Z        " Delete all uppercase marks

Save Session with Custom Command

:command SaveSession mksession! ~/.vim/sessions/session.vim
:command LoadSession source ~/.vim/sessions/session.vim

Format Current Paragraph

:normal vipgq        " Visual select paragraph, format

Or as custom command:

:command FormatPara normal vipgq

Open Terminal in Split

:split | terminal    " Horizontal split with terminal
:vsplit | terminal   " Vertical split with terminal

Neovim:

:split term://bash
:vsplit term://zsh

Quick Calculator

:echo 2 + 2
:echo 100 / 3
:echo sqrt(144)
:echo pow(2, 10)

In Insert mode:

<C-r>=2+2<CR>        " Inserts 4

Convert Tabs/Spaces

:set expandtab       " Use spaces
:retab               " Convert tabs to spaces

:set noexpandtab     " Use tabs
:retab!              " Convert spaces to tabs

Show Full File Path

:echo expand('%:p')
:echo @%             " Current filename (relative)

Or map it:

:nnoremap <leader>fp :echo expand('%:p')<CR>

Count Pattern Matches

:%s/pattern//gn      " Count without replacing
:vimgrep /pattern/ % | copen  " List all matches

Chapter Summary

In this chapter, you mastered Command-Line Mode:

  • Basics: Entering (:, /, ?), exiting (<CR>, <Esc>), command-line window (q:)

  • Editing: Cursor movement (<C-b>, <C-e>), deletion (<C-u>, <C-w>)

  • Register insertion: <C-r>{register}, <C-r><C-w>, <C-r><C-f>

  • History: <Up>/<Down>, filtered history, :history

  • Completion: <Tab>, <C-d>, wildmenu settings

  • Expression register: <C-r>= for calculations

  • Ranges: ., $, %, '<,'>, patterns, marks, arithmetic

  • File operations: :e, :w, :sav, :up, :wq, :x

  • Buffer management: :ls, :b, :bn, :bp, :bd

  • Window/tab commands: :sp, :vs, :only, :tabe, :tabn

  • Display: :echo, :echom, :messages

  • Marks/jumps: :marks, :jumps, :changes

  • Options: :set, :setlocal, :setglobal

  • Substitute: Full syntax, ranges, flags, expressions

  • Global: :g, :v, nested patterns

  • Normal: :[range]norm for batch operations

  • Sort: :sort, :sort!, :sort u, numeric/pattern sorting

  • External: :!, :r !, :%! for filters

  • Copy/move: :t, :m with ranges

  • Join: :j, :j!

  • Undo/redo: :u, :earlier, :later, time-based undo

  • Runtime: :source, :runtime, :luafile (Neovim)

  • Help: :h, :helpgrep, tag navigation

  • Modifiers: :verbose, :silent, :noautocmd, :vertical, :tab

  • Custom commands: :command, arguments, ranges, completion

  • Functions: expand(), line(), col(), file checks

  • Abbreviations: :cabbrev for typo correction

  • Mappings: :cnoremap for command-line keys

  • Advanced patterns: Conditionals, loops, try-catch

  • Best practices: Completion, history, chaining, custom workflows

Command-Line Mode transforms Vim from a text editor into a programmable text manipulation environment. Combined with ranges, patterns, and Ex commands, you can perform complex operations across files that would require custom scripts in other editors.

In the next chapter, we’ll explore Registers and Macros in depth, covering register types, macro recording and editing, recursive macros, and building powerful automation workflows.


Chapter 6: Buffers, Windows, and Tabs

Vim’s workspace architecture differs fundamentally from traditional editors. Instead of a simple “file-in-window” model, Vim uses a three-layer hierarchy: buffers (in-memory file copies), windows (viewports into buffers), and tabs (collections of window layouts). Mastering this system unlocks powerful workflows for managing multiple files, comparing documents side-by-side, and organizing complex projects. This chapter explores each layer in depth with practical Neovim enhancements.

6.1 Understanding the Architecture

The Three-Layer Model

┌│─────────────────────────────────────┐

││ Tab Pages │

││ ┌─────────────┬─────────────────┐ │

││ │ Window 1 │ Window 2 │ │

││ │ (Buffer A)│ (Buffer B) │ │

││ ├─────────────┴─────────────────┤ │

││ │ Window 3 │ │

││ │ (Buffer C) │ │

││ └───────────────────────────────┘ │ └─────────────────────────────────────┘

Buffer List: [A, B, C, D, E, F] ← Buffers in memory └─ D, E, F are hidden (not in any window)

Key Concepts

Buffer

  • In-memory representation of a file

  • Holds file content, undo history, marks, options

  • Can be displayed in multiple windows simultaneously

  • Persists even when not visible (hidden)

  • Each buffer has a unique number

Window

  • Viewport displaying a buffer

  • Can be split horizontally or vertically

  • Multiple windows can display the same buffer

  • Each window has its own cursor position in the buffer

  • Closing a window doesn’t delete the buffer

Tab Page

  • Collection of windows arranged in a layout

  • Tabs are NOT file containers (unlike most editors)

  • Switching tabs shows a different window arrangement

  • All tabs share the same buffer list

  • Think of tabs as workspace layouts

Common Misconception

Traditional Editor Model: Tab 1: file1.txt Tab 2: file2.txt
Tab 3: file3.txt

Vim Model: Tab 1: [Window: file1.txt | Window: file2.txt] Tab 2: [Window: file3.txt] └─ file1.txt and file2.txt still in buffer list!

6.2 Buffer Management

Listing Buffers

:ls                  " List all buffers
:buffers             " Same as :ls
:files               " Same as :ls
:ls!                 " Show unlisted buffers too

Buffer Status Indicators: % - Current buffer (in current window) # - Alternate buffer (previous buffer) a - Active buffer (loaded and displayed in a window) h - Hidden buffer (loaded but not displayed)

    • Unmodifiable buffer = - Readonly buffer R - Readonly and modified (unusual state)
    • Modified buffer (unsaved changes) x - Buffer with read errors u - Unlisted buffer (not in buffer list)

Example output: 1 %a “init.lua” line 42 2 h “plugin.lua” line 15 3 #h “config.lua” line 1 4 h + “notes.txt” line 8

Interpretation:

  • Buffer 1: Current, active (displayed), line 42

  • Buffer 2: Hidden (loaded but not shown)

  • Buffer 3: Alternate buffer, hidden

  • Buffer 4: Hidden, modified (unsaved changes)

Creating and Opening Buffers

:e {file}            " Edit file (create new buffer or switch)
:enew                " Create new unnamed buffer
:badd {file}         " Add file to buffer list without opening
:find {file}         " Find file in 'path' and edit

Examples:

:e ~/.bashrc         " Edit bashrc (creates buffer)
:enew                " New scratch buffer
:badd TODO.md        " Add to buffer list, don't open
:find init.lua       " Search 'path' for init.lua

Neovim (Lua):

vim.cmd('edit ~/.bashrc')
vim.cmd('enew')


-- Or using API:
vim.api.nvim_command('edit file.txt')
:b {n}               " Go to buffer number n
:b {name}            " Go to buffer by name (partial match)
:bn                  " Next buffer
:bp                  " Previous buffer
:bf                  " First buffer
:bl                  " Last buffer
:b#                  " Alternate buffer (previous)

Smart buffer switching:

:b init             " Switches to any buffer matching 'init'
:b .lua             " Switches to buffer ending with '.lua'
:b config<Tab>      " Tab completion

Cycle through buffers:

:bn                 " Next
:bp                 " Previous
5:bn                " Go 5 buffers forward

Recommended mappings (Neovim):

vim.keymap.set('n', '<leader>bn', ':bn<CR>', { desc = 'Next buffer' })
vim.keymap.set('n', '<leader>bp', ':bp<CR>', { desc = 'Previous buffer' })
vim.keymap.set('n', '<leader>bd', ':bd<CR>', { desc = 'Delete buffer' })
vim.keymap.set('n', '<leader>bb', ':b#<CR>', { desc = 'Alternate buffer' })

Deleting Buffers

:bd                  " Delete current buffer
:bd {n}              " Delete buffer n
:bd {name}           " Delete buffer by name
:bd!                 " Force delete (discard changes)
:bd {n} {m} {o}      " Delete multiple buffers
:%bd                 " Delete all buffers
:bufdo bd            " Delete all (alternative)

Delete all except current:

:%bd|e#|bd#

Explanation:

  1. %bd - Delete all buffers

  2. e# - Edit alternate buffer (reopens current)

  3. bd# - Delete the duplicate alternate buffer

Delete hidden buffers:

:bufdo if !buflisted(bufnr('%')) | bd | endif

Neovim (Lua) - Better approach:


-- Delete all buffers except current
vim.api.nvim_create_user_command('BufOnly', function()
  local current = vim.api.nvim_get_current_buf()
  for _, buf in ipairs(vim.api.nvim_list_bufs()) do
    if buf ~= current and vim.api.nvim_buf_is_loaded(buf) then
      vim.api.nvim_buf_delete(buf, { force = false })
    end
  end
end, {})

Buffer Operations

:bufdo {cmd}         " Execute command on all buffers
:bufdo! {cmd}        " Execute on all, including hidden

Examples:

:bufdo %s/TODO/DONE/ge | update    " Replace in all, save
:bufdo set number                   " Enable line numbers in all
:bufdo normal gg=G                  " Format all buffers

Conditional buffer operations:

:bufdo if expand('%:e') == 'lua' | set ts=2 | endif

Buffer-Local Options

Set options for current buffer only:

:setlocal number         " Line numbers for this buffer
:setlocal spell          " Spellcheck for this buffer
:setlocal ts=2           " Tab width for this buffer

Neovim (Lua):

vim.opt_local.number = true
vim.opt_local.spell = true
vim.opt_local.tabstop = 2

Or using buffer-specific options:

vim.bo.tabstop = 2
vim.bo.shiftwidth = 2
vim.bo.expandtab = true

Scratch Buffers

Create temporary buffers for notes:

:enew                    " New buffer
:setlocal buftype=nofile " Not associated with file
:setlocal bufhidden=hide " Hide when abandoned
:setlocal noswapfile     " Don't create swap file

Complete scratch buffer command:

vim.api.nvim_create_user_command('Scratch', function()
  vim.cmd('enew')
  vim.bo.buftype = 'nofile'
  vim.bo.bufhidden = 'hide'
  vim.bo.swapfile = false
end, {})

Usage:

:Scratch             " Creates disposable buffer

Buffer Information

:file                    " Show current buffer info
:file {name}             " Rename current buffer
:buffers                 " List all buffers
:ls +                    " List modified buffers
:ls a                    " List active buffers
:ls h                    " List hidden buffers

Get buffer details programmatically (Neovim):

local buf = vim.api.nvim_get_current_buf()
local name = vim.api.nvim_buf_get_name(buf)
local lines = vim.api.nvim_buf_line_count(buf)
local modified = vim.bo.modified

print(string.format("Buffer: %s, Lines: %d, Modified: %s", 
  name, lines, modified))

Unlisted Buffers

Some buffers are unlisted (help buffers, plugins, etc.):

:ls!                 " Show all, including unlisted
:set buflisted       " Make current buffer listed
:set nobuflisted     " Make current buffer unlisted

Buffer Variables

Store data specific to a buffer:

:let b:my_var = 'value'    " Buffer-local variable
:echo b:my_var             " Access variable
:unlet b:my_var            " Delete variable

Neovim (Lua):

vim.b.my_var = 'value'
print(vim.b.my_var)
vim.b.my_var = nil  -- Delete

Use case - Track buffer state:


-- Mark buffer as configured
vim.b.lsp_configured = true


-- Check in autocommand
if not vim.b.lsp_configured then

  -- Setup LSP
  vim.b.lsp_configured = true
end

6.3 Window Management

Creating Windows

:split {file}        " Horizontal split (or :sp)
:vsplit {file}       " Vertical split (or :vs)
:new                 " New horizontal split, empty buffer
:vnew                " New vertical split, empty buffer
:split               " Split current buffer horizontally
:vsplit              " Split current buffer vertically

With size:

:10split             " 10 lines high
:vertical 50split    " 50 columns wide
:30vnew              " 30 columns wide, new buffer

Examples:

:sp config.lua       " Open config.lua in horizontal split
:vs                  " Vertical split, same buffer
:20split             " Horizontal split, 20 lines high
:vertical 80split    " Vertical split, 80 columns wide

Neovim mappings:

vim.keymap.set('n', '<leader>sh', ':split<CR>', { desc = 'Horizontal split' })
vim.keymap.set('n', '<leader>sv', ':vsplit<CR>', { desc = 'Vertical split' })

Motion commands:

<C-w>h               " Move to window on left
<C-w>j               " Move to window below
<C-w>k               " Move to window above
<C-w>l               " Move to window on right
<C-w>w               " Cycle to next window
<C-w>W               " Cycle to previous window
<C-w>p               " Previous window (last accessed)
<C-w>t               " Top-left window
<C-w>b               " Bottom-right window

Count prefix:

3<C-w>w              " Jump 3 windows forward
2<C-w>l              " Jump 2 windows right

Better navigation mappings:


-- Easier window navigation
vim.keymap.set('n', '<C-h>', '<C-w>h', { desc = 'Go to left window' })
vim.keymap.set('n', '<C-j>', '<C-w>j', { desc = 'Go to window below' })
vim.keymap.set('n', '<C-k>', '<C-w>k', { desc = 'Go to window above' })
vim.keymap.set('n', '<C-l>', '<C-w>l', { desc = 'Go to right window' })

Resizing Windows

<C-w>=               " Make all windows equal size
<C-w>_               " Maximize height
<C-w>|               " Maximize width
<C-w>+               " Increase height by 1
<C-w>-               " Decrease height by 1
<C-w>>               " Increase width by 1
<C-w><               " Decrease width by 1

With count:

10<C-w>+             " Increase height by 10
5<C-w>>              " Increase width by 5

Ex commands:

:resize 20           " Set height to 20 lines
:resize +5           " Increase height by 5
:resize -5           " Decrease height by 5
:vertical resize 80  " Set width to 80 columns
:vertical resize +10 " Increase width by 10

Better resize mappings:

vim.keymap.set('n', '<M-h>', ':vertical resize -2<CR>', { desc = 'Decrease width' })
vim.keymap.set('n', '<M-l>', ':vertical resize +2<CR>', { desc = 'Increase width' })
vim.keymap.set('n', '<M-j>', ':resize -2<CR>', { desc = 'Decrease height' })
vim.keymap.set('n', '<M-k>', ':resize +2<CR>', { desc = 'Increase height' })
vim.keymap.set('n', '<M-=>', '<C-w>=', { desc = 'Equal windows' })

Moving Windows

<C-w>r               " Rotate windows downward/rightward
<C-w>R               " Rotate windows upward/leftward
<C-w>x               " Exchange with next window
<C-w>H               " Move window to far left (full height)
<C-w>J               " Move window to bottom (full width)
<C-w>K               " Move window to top (full width)
<C-w>L               " Move window to far right (full height)
<C-w>T               " Move window to new tab

Examples:

<C-w>H               " Convert vertical split to horizontal
<C-w>J               " Convert horizontal split to vertical
<C-w>T               " Break window out into new tab

Visualizing rotation: Before r: After r:

┌│──────┬──────┐ ┌──────┬──────┐

││ A │ B │ │ B │ A │

├│──────┴──────┤ → ├──────┴──────┤

││ C │ │ C │ └─────────────┘ └─────────────┘

Closing Windows

:q                   " Quit window (close window)
:close               " Close current window
:only                " Close all windows except current
:hide                " Hide current window (keep buffer loaded)
<C-w>q               " Quit window
<C-w>c               " Close window
<C-w>o               " Only (close all others)

Close all but current:

:only                " Or <C-w>o

Force close:

:q!                  " Discard changes and close
:close!              " Force close

Window Position Modifiers

Open windows at specific positions:

:topleft split       " Split at top
:botright vsplit     " Vertical split at right
:leftabove split     " Split above current
:rightbelow vsplit   " Vertical split to right

Examples:

:topleft vsplit      " Vertical split at far left
:botright split      " Horizontal split at bottom
:vertical botright split  " Far right, vertical

Neovim configuration:

vim.opt.splitbelow = true   -- Horizontal splits go below
vim.opt.splitright = true   -- Vertical splits go right

Window-Local Options

:setlocal number         " Line numbers in this window only
:setlocal cursorline     " Cursor line in this window

Neovim (Lua):

vim.wo.number = true        -- Window option
vim.wo.cursorline = true
vim.wo.wrap = false

Difference between bo (buffer) and wo (window):

vim.bo.filetype = 'lua'     -- Buffer-specific (shared across windows)
vim.wo.number = true        -- Window-specific (each window can differ)

Scrolling Multiple Windows

Bind scrolling in multiple windows:

:set scrollbind          " Enable scroll binding in window
:set noscrollbind        " Disable scroll binding

Use case - Compare files side by side:

:vsplit other.txt        " Open second file
:set scrollbind          " In first window
<C-w>l                   " Move to second window
:set scrollbind          " In second window
" Now both windows scroll together!

Diff mode (automatic scrollbind):

:diffsplit file2.txt     " Open in diff mode
:diffthis                " Enable diff for current window

Window Focus Configuration

Automatic resize on focus:


-- Make focused window larger
vim.opt.winwidth = 84
vim.opt.winheight = 20
vim.opt.winminwidth = 15
vim.opt.winminheight = 5


-- Auto-resize on focus (requires autocmd)
local resize_group = vim.api.nvim_create_augroup('WindowResize', { clear = true })
vim.api.nvim_create_autocmd('WinEnter', {
  group = resize_group,
  pattern = '*',
  command = 'resize 30 | vertical resize 100'
})

Golden Ratio Windows

Create a plugin-like golden ratio effect:

local function golden_ratio()
  local wincount = vim.fn.winnr('$')
  if wincount == 1 then return end
  
  local width = math.floor(vim.o.columns * 0.618)
  local height = math.floor(vim.o.lines * 0.618)
  
  vim.cmd('resize ' .. height)
  vim.cmd('vertical resize ' .. width)
end

vim.keymap.set('n', '<leader>gr', golden_ratio, { desc = 'Golden ratio resize' })

6.4 Tab Pages

Creating Tabs

:tabnew              " New tab with empty buffer
:tabe {file}         " Open file in new tab
:tabedit {file}      " Same as :tabe
:tab {command}       " Execute command in new tab
:tab split           " Open current buffer in new tab
:tab help {topic}    " Open help in new tab

Examples:

:tabnew              " Empty tab
:tabe config.lua     " Open config.lua in new tab
:tab split           " Current buffer in new tab
:tab help buffers    " Help in new tab

Create tab with specific layout:

:tabnew | vsplit | split

Creates: New Tab

┌│──────────┬──────────┐

││ │ │

││ A ├──────────┤

││ │ B │ └──────────┴──────────┘

:tabn[ext]           " Next tab
:tabp[revious]       " Previous tab
:tabfirst            " First tab
:tablast             " Last tab
:tabn {n}            " Go to tab n

Normal mode shortcuts:

gt                   " Next tab
gT                   " Previous tab
{n}gt                " Go to tab n (e.g., 3gt)

Neovim mappings:

vim.keymap.set('n', '<leader>tn', ':tabnext<CR>', { desc = 'Next tab' })
vim.keymap.set('n', '<leader>tp', ':tabprevious<CR>', { desc = 'Previous tab' })
vim.keymap.set('n', '<leader>tf', ':tabfirst<CR>', { desc = 'First tab' })
vim.keymap.set('n', '<leader>tl', ':tablast<CR>', { desc = 'Last tab' })


-- Or override defaults:
vim.keymap.set('n', '<Tab>', ':tabnext<CR>', { desc = 'Next tab' })
vim.keymap.set('n', '<S-Tab>', ':tabprevious<CR>', { desc = 'Previous tab' })

Closing Tabs

:tabc[lose]          " Close current tab
:tabc {n}            " Close tab n
:tabo[nly]           " Close all tabs except current
:tabclose!           " Force close current tab

Examples:

:tabc                " Close current tab
:tabc 3              " Close tab 3
:tabo                " Close all other tabs

Moving Tabs

:tabm[ove] {n}       " Move tab to position n (0 = first)
:tabm[ove] +{n}      " Move tab n positions right
:tabm[ove] -{n}      " Move tab n positions left
:tabm[ove]           " Move to last position

Examples:

:tabm 0              " Move to first position
:tabm                " Move to last position
:tabm +1             " Move one position right
:tabm -1             " Move one position left

Tab Operations

:tabs                " List all tabs
:tabdo {cmd}         " Execute command on all tabs

Examples:

:tabs                " Show tab list with windows
:tabdo %s/TODO/DONE/ge | update  " Replace in all tabs

Tab list output: Tab page 1 [1] “init.lua” > [2] “plugin.lua” Tab page 2 [3] “README.md”

Tab-Scoped Variables

:let t:my_var = 'value'    " Tab-local variable
:echo t:my_var             " Access variable
:unlet t:my_var            " Delete variable

Neovim (Lua):

vim.t.my_var = 'value'
print(vim.t.my_var)
vim.t.my_var = nil  -- Delete

Use case:


-- Track tab purpose
vim.t.project_root = '/path/to/project'
vim.t.tab_label = 'Frontend'

Tab-Specific CWD

Each tab can have its own working directory:

:tcd {path}          " Set tab-local working directory
:tcd %:h             " Set to current file's directory
:pwd                 " Show current directory

Example workflow:

:tabnew              " New tab
:tcd ~/project1      " Set directory for this tab
:e src/main.lua      " Edit file relative to project1

:tabnew              " Another tab
:tcd ~/project2      " Different directory
:e src/main.lua      " Edit file from project2

Tab Line Configuration

Basic tab line:

vim.opt.showtabline = 2  -- Always show tab line (0=never, 1=auto, 2=always)

Custom tab line (Neovim):

function _G.custom_tabline()
  local s = ''
  for i = 1, vim.fn.tabpagenr('$') do
    local winnr = vim.fn.tabpagewinnr(i)
    local bufnr = vim.fn.tabpagebuflist(i)[winnr]
    local filename = vim.fn.bufname(bufnr)
    local name = vim.fn.fnamemodify(filename, ':t')
    
    if i == vim.fn.tabpagenr() then
      s = s .. '%#TabLineSel# ' .. i .. ': ' .. name .. ' '
    else
      s = s .. '%#TabLine# ' .. i .. ': ' .. name .. ' '
    end
  end
  return s .. '%#TabLineFill#'
end

vim.opt.tabline = '%!v:lua.custom_tabline()'

Or use a plugin:


-- Using bufferline.nvim (popular choice)
require('bufferline').setup{
  options = {
    mode = "tabs",  -- Show tabs instead of buffers
    numbers = "ordinal",
    close_command = "tabclose %d",
    diagnostics = "nvim_lsp",
  }
}

6.5 Advanced Workflows

Working with Multiple Files

Open multiple files at once:

:args *.lua          " Load all .lua files into argument list
:args **/*.lua       " Recursive search
:args src/**/*.lua test/**/*.lua  " Multiple patterns

Navigate argument list:

:next                " Next file in args
:prev                " Previous file in args
:first               " First file in args
:last                " Last file in args
:args                " Show argument list

Operations on argument list:

:argdo %s/old/new/ge | update   " Replace in all args
:argdo normal @a                 " Run macro on all args

Split Windows for Files

Horizontal split for each argument:

:args *.lua
:sall                " Split all arguments horizontally

Vertical split:

:vertical sall       " Split all arguments vertically

With limit:

:4sall               " Split only first 4 files

Diff Mode

Compare two files:

:diffsplit other.txt     " Open in diff mode
:diffthis                " Enable diff in current window
:diffoff                 " Disable diff
:diffoff!                " Disable diff in all windows

From command line:

nvim -d file1.txt file2.txt

Diff navigation:

]c                   " Next difference
[c                   " Previous difference
do                   " Diff obtain (get changes from other window)
dp                   " Diff put (put changes to other window)
:diffupdate          " Refresh diff
:diffget             " Get changes from specific buffer
:diffput             " Put changes to specific buffer

Three-way diff:

:diffsplit file2.txt
:vert diffsplit file3.txt

Buffer Browsing

Browse buffers interactively:

:ls                  " List buffers
:b <partial-name>    " Type partial name, use Tab

Or use telescope (Neovim plugin):

require('telescope.builtin').buffers()

Manual buffer menu:

vim.api.nvim_create_user_command('Buffers', function()
  local buffers = vim.api.nvim_list_bufs()
  local items = {}
  
  for _, buf in ipairs(buffers) do
    if vim.api.nvim_buf_is_loaded(buf) then
      local name = vim.api.nvim_buf_get_name(buf)
      table.insert(items, string.format("%d: %s", buf, name))
    end
  end
  
  vim.ui.select(items, {
    prompt = 'Select buffer:',
  }, function(choice)
    if choice then
      local bufnr = tonumber(choice:match("^(%d+):"))
      vim.api.nvim_set_current_buf(bufnr)
    end
  end)
end, {})

Session Management

Save session:

:mksession session.vim       " Save session
:mksession! session.vim      " Overwrite existing

Load session:

:source session.vim          " Load session

From command line:

nvim -S session.vim

What’s saved:

  • Window layouts

  • Tab pages

  • Buffer list

  • Working directory

  • Folds

  • Options

  • Mappings (optional)

Session options:

:set sessionoptions=blank,buffers,curdir,folds,help,tabpages,winsize

Neovim (Lua) - Auto-save session:

local session_dir = vim.fn.stdpath('data') .. '/sessions'
vim.fn.mkdir(session_dir, 'p')

local function save_session()
  local session_file = session_dir .. '/' .. vim.fn.getcwd():gsub('/', '_') .. '.vim'
  vim.cmd('mksession! ' .. session_file)
end

local function load_session()
  local session_file = session_dir .. '/' .. vim.fn.getcwd():gsub('/', '_') .. '.vim'
  if vim.fn.filereadable(session_file) == 1 then
    vim.cmd('source ' .. session_file)
  end
end

vim.api.nvim_create_autocmd('VimLeave', {
  callback = save_session,
  desc = 'Auto-save session on exit'
})

vim.api.nvim_create_user_command('LoadSession', load_session, {})

Project-Based Tabs

Organize work by projects:

" Tab 1: Frontend
:tcd ~/projects/frontend
:e src/App.jsx
:vs src/components/Header.jsx

" Tab 2: Backend  
:tabnew
:tcd ~/projects/backend
:e src/main.py
:vs src/api.py

" Tab 3: Documentation
:tabnew
:tcd ~/projects/docs
:e README.md

Quick project switcher (Lua):

local projects = {
  frontend = '~/projects/frontend',
  backend = '~/projects/backend',
  docs = '~/projects/docs',
}

vim.api.nvim_create_user_command('Project', function(opts)
  local project = opts.args
  if projects[project] then
    vim.cmd('tabnew')
    vim.cmd('tcd ' .. vim.fn.expand(projects[project]))
    vim.cmd('edit .')  -- Open netrw in project root
  else
    print('Unknown project: ' .. project)
  end
end, {
  nargs = 1,
  complete = function()
    return vim.tbl_keys(projects)
  end,
})

Usage:

:Project frontend    " Opens new tab with frontend project

Window Layouts

Common layouts:

Two-column split:

:vsplit              " Two equal vertical panes

Three-column:

:vsplit | vsplit
<C-w>=               " Equalize

Main + sidebar:

:vsplit
:vertical resize 30  " Sidebar 30 columns wide

Top bar + main + bottom:

:split
:resize 5            " Top bar 5 lines
<C-w>j
:split
:resize 10           " Bottom panel 10 lines

Save/restore layouts with sessions:

:mksession layout.vim
:source layout.vim

Window Zoom (Maximize Toggle)


-- Zoom current window (toggle)
vim.keymap.set('n', '<leader>z', function()
  if vim.t.zoomed then
    vim.cmd('tabclose')
    vim.t.zoomed = false
  else
    vim.cmd('tab split')
    vim.t.zoomed = true
  end
end, { desc = 'Toggle window zoom' })

Usage: <leader>z maximizes current window in new tab, press again to restore.

6.6 Buffer/Window/Tab Tips

Buffer Tips

1. Alternate file (#):

<C-^>                " Toggle between current and alternate buffer
:b#                  " Same as <C-^>

2. Hidden buffers:

vim.opt.hidden = true    -- Allow switching buffers without saving

3. Buffer grep:

:bufdo vimgrep /pattern/ % | copen

4. Wipeout vs delete:

:bd                  " Delete buffer (keeps in memory)
:bw                  " Wipeout buffer (completely remove)

Window Tips

1. Quick peek at file:

<C-w>f               " Open file under cursor in split
<C-w>gf              " Open in new tab

2. Preview window:

:pedit file.txt      " Open in preview window
<C-w>z               " Close preview window

3. Equal window sizes:

<C-w>=               " Make all windows equal

4. Split to new buffer:

:new                 " Horizontal
:vnew                " Vertical

Tab Tips

1. Tab-scoped working directory:

:tcd %:h             " Set tab CWD to current file's directory

2. Drop file in tab:

:tab drop file.txt   " Open in existing tab if already open

3. Tab-specific settings:

vim.api.nvim_create_autocmd('TabEnter', {
  callback = function()

    -- Apply settings when entering tab
    if vim.t.special_tab then
      vim.opt_local.number = false
    end
  end
})

6.7 Neovim-Specific Features

Floating Windows

Neovim supports floating windows (overlays):


-- Create floating window
local buf = vim.api.nvim_create_buf(false, true)  -- No file, scratch
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {'Hello', 'Floating', 'Window'})

local opts = {
  relative = 'editor',
  width = 40,
  height = 10,
  col = 10,
  row = 5,
  style = 'minimal',
  border = 'rounded',
}

local win = vim.api.nvim_open_win(buf, true, opts)

Center floating window:

local function create_centered_float()
  local width = 80
  local height = 20
  
  local buf = vim.api.nvim_create_buf(false, true)
  
  local opts = {
    relative = 'editor',
    width = width,
    height = height,
    col = math.floor((vim.o.columns - width) / 2),
    row = math.floor((vim.o.lines - height) / 2),
    style = 'minimal',
    border = 'rounded',
  }
  
  vim.api.nvim_open_win(buf, true, opts)
  return buf
end

vim.keymap.set('n', '<leader>fl', create_centered_float, { desc = 'Floating window' })

Buffer API

List all buffers:

local buffers = vim.api.nvim_list_bufs()
for _, buf in ipairs(buffers) do
  if vim.api.nvim_buf_is_loaded(buf) then
    print(vim.api.nvim_buf_get_name(buf))
  end
end

Create and modify buffer:

local buf = vim.api.nvim_create_buf(true, false)  -- Listed, not scratch
vim.api.nvim_buf_set_name(buf, 'my-buffer.txt')
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {'Line 1', 'Line 2'})
vim.api.nvim_set_current_buf(buf)

Get buffer info:

local buf = vim.api.nvim_get_current_buf()
local name = vim.api.nvim_buf_get_name(buf)
local line_count = vim.api.nvim_buf_line_count(buf)
local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
local modified = vim.bo[buf].modified

Window API

Get/set window info:

local win = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_win_get_buf(win)
local cursor = vim.api.nvim_win_get_cursor(win)  -- {line, col}
local width = vim.api.nvim_win_get_width(win)
local height = vim.api.nvim_win_get_height(win)


-- Set cursor
vim.api.nvim_win_set_cursor(win, {10, 5})  -- Line 10, column 5


-- Set dimensions
vim.api.nvim_win_set_width(win, 80)
vim.api.nvim_win_set_height(win, 30)

List all windows:

local windows = vim.api.nvim_list_wins()
for _, win in ipairs(windows) do
  print(vim.api.nvim_win_get_buf(win))
end

Tabpage API


-- List tabs
local tabs = vim.api.nvim_list_tabpages()


-- Get current tab
local current_tab = vim.api.nvim_get_current_tabpage()


-- Get windows in tab
local wins = vim.api.nvim_tabpage_list_wins(current_tab)


-- Set current tab
vim.api.nvim_set_current_tabpage(tabs[1])

6.8 Plugin Recommendations

Buffer Management

bufferline.nvim - Beautiful buffer/tab line

require('bufferline').setup{
  options = {
    numbers = "buffer_id",
    diagnostics = "nvim_lsp",
    separator_style = "slant",
    show_buffer_close_icons = true,
    show_close_icon = false,
  }
}

harpoon - Quick buffer navigation

local mark = require("harpoon.mark")
local ui = require("harpoon.ui")

vim.keymap.set("n", "<leader>a", mark.add_file)
vim.keymap.set("n", "<C-e>", ui.toggle_quick_menu)
vim.keymap.set("n", "<C-h>", function() ui.nav_file(1) end)
vim.keymap.set("n", "<C-t>", function() ui.nav_file(2) end)

Window Management

windows.nvim - Window picker and animator

require('windows').setup()
vim.keymap.set('n', '<leader>w', ':WindowsMaximize<CR>')

smart-splits.nvim - Better window navigation and resizing

require('smart-splits').setup()
vim.keymap.set('n', '<C-h>', require('smart-splits').move_cursor_left)
vim.keymap.set('n', '<C-j>', require('smart-splits').move_cursor_down)
vim.keymap.set('n', '<C-k>', require('smart-splits').move_cursor_up)
vim.keymap.set('n', '<C-l>', require('smart-splits').move_cursor_right)

Session Management

persistence.nvim - Auto-save sessions

require("persistence").setup()
vim.keymap.set("n", "<leader>qs", [[<cmd>lua require("persistence").load()<cr>]])
vim.keymap.set("n", "<leader>ql", [[<cmd>lua require("persistence").load({ last = true })<cr>]])

Chapter Summary

In this chapter, you mastered Vim/Neovim’s workspace architecture:

Buffers:

  • Three-layer model: buffers → windows → tabs

  • Buffer lifecycle: create, navigate, delete

  • :ls, :b, :bd, :bn, :bp

  • Buffer-local options (setlocal, vim.bo)

  • Scratch buffers for temporary work

  • Buffer operations: :bufdo

  • Hidden buffers and alternate file (<C-^>, :b#)

Windows:

  • Creating: :split, :vsplit, :new, :vnew

  • Navigation: <C-w>hjkl, <C-w>w, <C-w>p

  • Resizing: <C-w>=, :resize, :vertical resize

  • Moving: <C-w>HJKL, <C-w>r, <C-w>x

  • Closing: :q, :close, :only

  • Window-local options (vim.wo)

  • Scroll binding and diff mode

  • Floating windows (Neovim)

Tabs:

  • Tab pages as layout containers

  • Creating: :tabnew, :tabe, :tab {command}

  • Navigation: gt, gT, {n}gt

  • Managing: :tabc, :tabo, :tabm

  • Tab-local CWD (:tcd)

  • Tab-scoped variables (vim.t)

Advanced Workflows:

  • Argument list (:args, :argdo)

  • Multi-file operations

  • Diff mode (:diffsplit, ]c, [c, do, dp)

  • Session management (:mksession)

  • Project-based organization

  • Window layouts and zoom toggle

Neovim Features:

  • Floating windows API

  • Buffer/Window/Tabpage API

  • Lua-based automation

  • Plugin ecosystem

Key Insights:

  • Tabs are NOT file containers

  • Buffers persist independently of windows

  • One buffer can appear in multiple windows

  • Windows show viewports into buffers

  • Tabs organize window layouts

  • :bd deletes buffer, :q closes window

Master this architecture to unlock powerful multi-file workflows, efficient navigation, and optimal workspace organization for complex projects.

In the next chapter, we’ll explore Registers and Macros, covering register types, yank/delete registers, named registers, macro recording and editing, recursive macros, and building sophisticated automation sequences.


Chapter 7: Macros and Registers

Vim’s register system transforms the editor into a programmable text manipulation engine. While most editors provide simple copy-paste functionality, Vim offers 48+ named storage locations for text, commands, and automation sequences. Combined with macros—recordable command sequences—registers become the foundation for eliminating repetitive editing tasks. This chapter explores the complete register taxonomy, macro recording and editing techniques, and advanced automation patterns that exponentially multiply your editing efficiency.

7.1 Understanding Registers

What Are Registers?

Registers are named storage containers that hold:

  • Yanked (copied) text

  • Deleted text

  • Recorded macro sequences

  • Search patterns

  • File paths

  • Expression results

Key concept: Every yank, delete, and paste operation interacts with registers. Understanding this system unlocks precise control over text manipulation.

The Register Namespace

Vim provides multiple register categories:

┌│─────────────────────────────────────────┐

││ Vim Register System │

├│─────────────────────────────────────────┤

││ ” - Unnamed (default) │

││ 0 - Yank register │

││ 1-9 - Delete history (1=recent) │

││ - - Small delete (<1 line) │

││ a-z - Named (lowercase = replace) │

││ A-Z - Named (uppercase = append) │

││ : - Last Ex command │

││ / - Last search pattern │

││ % - Current filename │

││ # - Alternate filename │

││ * - System clipboard (X11 PRIMARY) │

││ + - System clipboard (CLIPBOARD) │

││ _ - Black hole (null register) │

││ = - Expression register │

││ . - Last inserted text │ └─────────────────────────────────────────┘

Viewing Register Contents

:registers           " Show all registers
:reg                 " Short form
:reg a b c           " Show specific registers
:reg "0-9            " Show unnamed and numbered

Output example: Type Name Content c “” last deleted text c “0 last yanked text c”1 previous delete l “a Some text I saved c”+ system clipboard content c “: last command c”/ last search pattern

Neovim (Lua):


-- View specific register
print(vim.fn.getreg('a'))
print(vim.fn.getreg('+'))  -- Clipboard


-- View all registers
vim.cmd('registers')

Register Access Syntax

In Normal mode:

"{register}          " Prefix for register access
"ayy                 " Yank line into register a
"ap                  " Paste from register a
"bdd                 " Delete line into register b

In Insert mode:

<C-r>{register}      " Paste from register
<C-r>a               " Paste register a
<C-r>+               " Paste clipboard
<C-r>=               " Expression register

In Command mode:

<C-r>{register}      " Insert register content
:%s/<C-r>a/new/g     " Replace with content from register a

7.2 Register Categories

7.2.1 The Unnamed Register (")

The default register for yank and delete operations.

yy                   " Yank line to unnamed register
dd                   " Delete line to unnamed register
p                    " Paste from unnamed register
""p                  " Explicitly paste from unnamed (same as p)

Important: The unnamed register is overwritten by most operations.

7.2.2 The Yank Register (0)

Stores the last yanked text (not affected by deletions).

yiw                  " Yank word to both " and 0
dd                   " Delete line (updates " but not 0)
"0p                  " Paste the yanked word (not the deleted line!)

Use case:

" Copy a word to paste multiple times
yiw                  " Yank word
dd                   " Delete some line (overwrites ")
"0p                  " Still pastes the yanked word!
dd
"0p                  " Still works!

This is crucial: 0 preserves yanks across deletions.

7.2.3 Numbered Registers (1-9)

Circular delete history (automatic undo-like buffer).

"1                   " Most recent deletion (>1 line)
"2                   " Second most recent
"9                   " Oldest in history

How it works:

dd                   " Delete line → goes to "1
dd                   " Delete another → previous moves to "2, new to "1
dd                   " Delete another → "3, "2, "1 shift

Cycling through delete history:

"1p                  " Paste most recent deletion
u                    " Undo
"2p                  " Paste second most recent
u
"3p                  " Paste third most recent

Better technique:

"1p                  " Paste from "1
.                    " Repeats last command: "2p (auto-increments!)
.                    " Pastes from "3
.                    " Pastes from "4

Magic insight: After "1p, the . command automatically increments to "2p, then "3p, etc!

7.2.4 Small Delete Register (-)

Stores deletions smaller than one line.

diw                  " Delete word → goes to "-
x                    " Delete char → goes to "-
dd                   " Delete line → goes to "1 (not "-)

Use case:

diw                  " Delete word (goes to "-)
dd                   " Delete line (goes to "1)
"-p                  " Paste the deleted word (not the line)

7.2.5 Named Registers (a-z, A-Z)

Manual storage for specific text snippets.

Lowercase = Replace:

"ayy                 " Yank line into register a
"byy                 " Yank line into register b
"ap                  " Paste from a

Uppercase = Append:

"ayy                 " Yank line into a
"Ayy                 " Append another line to a
"ap                  " Paste both lines!

Practical example:

" Collect function names from a file
/function            " Search for function
"ayw                 " Yank word to register a
n                    " Next match
"Ayw                 " Append to register a (note uppercase A)
n
"Ayw                 " Append again
"ap                  " Paste all collected names

Building a list incrementally:

" Collect TODO items across file
/TODO
"aY                  " Yank line to register a
n
"AY                  " Append
n
"AY                  " Append
:new                 " New buffer
"ap                  " Paste all TODOs

7.2.6 Read-Only Registers

Filename registers:

"%                   " Current filename
"#                   " Alternate filename

Usage:

<C-r>%               " Insert current filename (Insert mode)
:echo @%             " Display current filename
:!gcc <C-r>%         " Compile current file

Examples:

" In file: /home/user/project/main.lua
:echo @%             " → project/main.lua (relative path)
:echo expand('%:p')  " → /home/user/project/main.lua (absolute)
:echo expand('%:t')  " → main.lua (tail/filename only)
:echo expand('%:r')  " → project/main (remove extension)

Last command register (:):

@:                   " Repeat last Ex command
@@                   " Repeat last @{register} command

Example:

:s/old/new/g         " Substitute command
@:                   " Repeat substitution
@@                   " Repeat again

Last search register (/):

echo @/              " Display last search pattern
let @/ = 'newpattern'  " Set search pattern
:%s/<C-r>///new/g    " Replace last searched pattern

Last inserted text (.):

".p                  " Paste last inserted text
<C-r>.               " In Insert mode, repeat last insert

Example:

" In Insert mode, type: Hello World
<Esc>                " Exit Insert mode
".p                  " Pastes "Hello World"

7.2.7 System Clipboard Registers

Primary selection (*):

"*yy                 " Yank to PRIMARY (middle-click paste)
"*p                  " Paste from PRIMARY

Clipboard (+):

"+yy                 " Yank to system clipboard
"+p                  " Paste from clipboard
<C-r>+               " Paste in Insert/Command mode

Platform differences:

  • Linux (X11): Both * and + available

    • * = PRIMARY selection (mouse selection)

    • + = CLIPBOARD (Ctrl+C/Ctrl+V)

  • macOS/Windows: Usually only + (CLIPBOARD)

Configuration (Neovim):

vim.opt.clipboard = 'unnamedplus'  -- Use system clipboard by default

Now y and p automatically use + register.

Check clipboard support:

:echo has('clipboard')    " 1 if supported
:version                  " Look for +clipboard

7.2.8 Black Hole Register (_)

Deletes without affecting any register (true deletion).

"_dd                 " Delete line, don't store anywhere
"_x                  " Delete char, don't store
"_d{motion}          " Delete without polluting registers

Use case - Delete without clobbering:

yiw                  " Yank word to paste later
/garbage
"_dd                 " Delete garbage line (doesn't overwrite yank)
p                    " Paste original yanked word ✓

Common mapping:


-- Delete to black hole by default
vim.keymap.set('n', 'd', '"_d', { noremap = true })
vim.keymap.set('n', 'D', '"_D', { noremap = true })
vim.keymap.set('n', 'c', '"_c', { noremap = true })
vim.keymap.set('n', 'C', '"_C', { noremap = true })


-- Use leader for "cut" operations
vim.keymap.set('n', '<leader>d', 'd', { noremap = true, desc = 'Cut' })
vim.keymap.set('n', '<leader>D', 'D', { noremap = true, desc = 'Cut to EOL' })

7.2.9 Expression Register (=)

Evaluates Vimscript/Lua expressions and inserts the result.

In Insert mode:

<C-r>=2+2<CR>        " Inserts: 4
<C-r>=system('date')<CR>  " Inserts current date

In Command mode:

:put =range(1,10)    " Insert numbers 1-10
:put =system('ls')   " Insert directory listing

Examples:

" Insert current date
<C-r>=strftime('%Y-%m-%d')<CR>  " 2025-10-19

" Math calculation
<C-r>=15*8<CR>       " 120

" Environment variable
<C-r>=$USER<CR>      " Current username

" System command output
<C-r>=system('whoami')<CR>

Neovim (Lua in expression register):

<C-r>=luaeval('vim.loop.cwd()')<CR>  " Current directory
<C-r>=luaeval('math.random(1,100)')<CR>  " Random number

Advanced - Multi-line expression:

:put =range(1, 5)

Inserts: 1 2 3 4 5

7.3 Macro Basics

What Are Macros?

Macros record a sequence of keystrokes into a register, which can be replayed to automate repetitive tasks.

Fundamental workflow:

  1. Record keystrokes into a register

  2. Replay the register as a macro

  3. Repeat as needed (with counts)

Recording Macros

Syntax:

q{register}          " Start recording into register
... perform edits ...
q                    " Stop recording
@{register}          " Execute macro
@@                   " Repeat last executed macro

Example - Add semicolons to lines:

let x = 10
let y = 20
let z = 30

Record macro:

qa                   " Start recording into register a
A;<Esc>              " Append semicolon and return to Normal mode
j                    " Move down
q                    " Stop recording

Execute:

@a                   " Run macro once (processes one line)
2@a                  " Run macro twice (processes two more lines)

Result:

let x = 10;
let y = 20;
let z = 30;

Macro Execution

Basic execution:

@a                   " Execute macro in register a
@@                   " Repeat last executed macro

With count:

5@a                  " Execute macro 5 times
100@a                " Execute 100 times

Execute on visual selection:

:normal @a           " Execute on current line
:'<,'>normal @a      " Execute on visual selection
:%normal @a          " Execute on all lines

Example - Macro on range:

:10,20normal @a      " Execute macro on lines 10-20

Viewing Macro Contents

Since macros are stored in registers:

:reg a               " View macro in register a
:echo @a             " Display macro keystrokes

Output example: “a A;j

Shows raw keystrokes: A (append), ;, <Esc>, j (down).

7.4 Advanced Macro Techniques

7.4.1 Editing Macros

Macros are just text in registers—you can edit them!

Method 1: Paste, edit, yank back

:let @a='            " Display register a in command line
"ap                  " Paste macro into buffer
... edit the text ...
"ayy                 " Yank modified macro back to register a

Method 2: Direct register editing

:let @a='A;<Esc>j^'  " Directly set register a

Method 3: Append to existing macro

qa                   " Start recording in a
... perform edits ...
q                    " Stop recording

qA                   " APPEND to register a (uppercase A)
... more edits ...
q                    " Stop recording

Example - Build macro incrementally:

" First, record basic formatting
qa
:s/\s\+$//           " Remove trailing whitespace
j
q

" Later, add line numbering
qA                   " Append to register a
0i# <Esc>            " Add "# " at start
j
q

" Now @a does both operations!

7.4.2 Parallel Registers

Use multiple registers for different tasks.

qa                   " Record macro in register a
... format code ...
q

qb                   " Record different macro in register b
... add comments ...
q

@a                   " Format current section
@b                   " Add comments

Macro library example:

" Register a: Convert to uppercase
qa
viwU
j
q

" Register b: Surround with quotes
qb
ciw"<C-r>""<Esc>
j
q

" Register c: Delete trailing spaces
qc
:s/\s\+$//
j
q

" Use as needed:
@a  " Uppercase
@b  " Quote
@c  " Clean

7.4.3 Recursive Macros

Macros can call themselves to process entire files.

Pattern:

qa                   " Start recording
... edit current item ...
@a                   " Call macro recursively
q                    " Stop recording

Example - Process all lines:

" Add line numbers to all lines
qa                   " Start recording
I1. <Esc>            " Insert "1. " at line start
j                    " Move down
@a                   " Recursive call
q                    " Stop recording

Execute:

@a                   " Runs until end of file (or error)

Important: The macro stops when it encounters an error (e.g., j at last line fails).

Safe recursive macro:

" Better version with error handling
qa
I1. <Esc>
j
:if line('.') <= line('$') | @a | endif
q

Practical recursive example - Format JSON-like structure:

{name: John, age: 30, city: NYC}
{name: Jane, age: 25, city: LA}
{name: Bob, age: 35, city: SF}

Macro to format:

qa
:s/, /,\r  /g        " Replace commas with newlines
j
@a
q

Result:

{name: John,
  age: 30,
  city: NYC}
{name: Jane,
  age: 25,
  city: LA}
...

7.4.4 Conditional Macros

Use Ex commands with conditions.

qa
:if getline('.') =~ 'TODO' | s/TODO/DONE/ | endif
j
q

Only modifies lines containing “TODO”.

More complex example:

" Add semicolon only if line doesn't end with one
qa
:if getline('.') !~ ';$' | s/$/ ;/ | endif
j
q

7.4.5 Macro Patterns

Pattern 1: Position-then-action

qa
0                    " Go to start of line
f(                   " Find opening parenthesis
%                    " Jump to closing parenthesis
a;                   " Append semicolon
j
q

Pattern 2: Search-and-modify

qa
/pattern<CR>         " Search for pattern
cw replacement<Esc>  " Replace word
n                    " Next match
q

Repeat with @@ or 100@a.

Pattern 3: Multi-step transformation

qa
I## <Esc>            " Add markdown header
A {#id}<Esc>         " Add anchor ID
j
q

Converts: Introduction

To: ## Introduction {#id}

7.4.6 Aborting Macros Safely

If a macro runs amok:

<C-c>                " Interrupt execution
:messages            " Check for error messages

Design macros to fail gracefully:

" Good: Fails at EOF without damage
qa
j                    " Moves down (fails at end)
dd
q

" Bad: Deletes incorrectly if j fails
qa
dd                   " Deletes current line
j                    " Might fail, but damage done
q

Best practice: Put navigation first, operations second.

7.5 Register and Macro Workflows

7.5.1 Copy-Paste Workflow

Basic workflow:

yy                   " Yank line to "
p                    " Paste from "

Multi-snippet workflow:

"ayy                 " Yank line to register a
"byy                 " Yank different line to register b
"cyy                 " Yank another to register c

"ap                  " Paste from a
"bp                  " Paste from b
"cp                  " Paste from c

Clipboard integration:

"+yy                 " Yank to system clipboard
"+p                  " Paste from system clipboard

Cross-file workflow:

" In file1.txt
"ayy                 " Yank line to register a
:e file2.txt         " Open different file
"ap                  " Paste (registers persist across files!)

7.5.2 Template Expansion

Store boilerplate in registers.

Setup:

:let @h='<!DOCTYPE html>
\<html>
\  <head>
\    <title></title>
\  </head>
\  <body>
\    
\  </body>
\</html>'

Usage:

"hp                  " Paste HTML template

Neovim (Lua) - Better approach:

vim.api.nvim_create_user_command('HTMLTemplate', function()
  local template = {
    '<!DOCTYPE html>',
    '<html>',
    '  <head>',
    '    <title></title>',
    '  </head>',
    '  <body>',
    '    ',
    '  </body>',
    '</html>'
  }
  vim.api.nvim_put(template, 'l', true, true)
end, {})

Usage: :HTMLTemplate

7.5.3 Register as Scratch Space

Use named registers as temporary storage during complex edits.

" Save original line
"ayy

" Experiment with changes
ciwtest<Esc>

" Oops, want original back
"ap                  " Restore from register a

7.5.4 Macro Chains

Execute multiple macros in sequence.

qa                   " Macro a: Format
...
q

qb                   " Macro b: Add comment
...
q

@a@b                 " Run both in sequence

Or combine into one macro:

qc
@a@b
q

@c                   " Runs both a and b

7.5.5 Building Complex Macros

Incremental development:

  1. Start simple:
qa
I// <Esc>            " Add comment
j
q
  1. Test on one line:
@a
  1. Verify result, then enhance:
qA                   " Append to register a
A<Esc>               " Add space at end
k
q
  1. Test again:
@a
  1. Execute on range:
:5,10normal @a

Debugging macros:

:reg a               " View macro content
"ap                  " Paste into buffer to inspect

Look for issues:

  • Missing <Esc> to return to Normal mode

  • Wrong motion commands

  • Incorrect register usage

7.6 Advanced Register Manipulation

7.6.1 Programmatic Register Access

Vimscript:

:let @a = 'new content'           " Set register a
:echo @a                           " Read register a
:let @a = @a . ' appended'        " Append to register a
:let @a = substitute(@a, 'old', 'new', 'g')  " Transform

Neovim (Lua):

vim.fn.setreg('a', 'new content')        -- Set register a
print(vim.fn.getreg('a'))                 -- Read register a
vim.fn.setreg('a', vim.fn.getreg('a') .. ' appended')  -- Append

7.6.2 Register Types

Registers store type information:

Character-wise (c):

yiw                  " Yank word (character-wise)
"ap                  " Pastes inline

Line-wise (l):

yy                   " Yank line (line-wise)
"ap                  " Pastes as new line

Block-wise (b):

<C-v>                " Visual block mode
y                    " Yank block
"ap                  " Pastes as block

Force register type:

:call setreg('a', "text", 'l')    " Force line-wise
:call setreg('b', "text", 'c')    " Force character-wise

Neovim (Lua):

vim.fn.setreg('a', 'text', 'l')  -- Line-wise
vim.fn.setreg('b', 'text', 'c')  -- Character-wise
vim.fn.setreg('c', 'text', 'b')  -- Block-wise

7.6.3 Swapping Register Contents

:let tmp = @a
:let @a = @b
:let @b = tmp

Neovim (Lua):

local tmp = vim.fn.getreg('a')
vim.fn.setreg('a', vim.fn.getreg('b'))
vim.fn.setreg('b', tmp)

7.6.4 Clearing Registers

:let @a = ''         " Clear register a
:let @/ = ''         " Clear search register (removes highlight)

Neovim (Lua):

vim.fn.setreg('a', '')
vim.fn.setreg('/', '')  -- Clear search highlight

Clear all named registers:

:for i in range(char2nr('a'), char2nr('z'))
:  let @{nr2char(i)} = ''
:endfor

Neovim (Lua):

for i = string.byte('a'), string.byte('z') do
  vim.fn.setreg(string.char(i), '')
end

7.7 Practical Examples

Example 1: Format Function Calls

Before:

print(x)
print(y)
print(z)

After:

console.log(x);
console.log(y);
console.log(z);

Macro:

qa
^                    " Go to line start
cw console.log<Esc>  " Change "print" to "console.log"
A;<Esc>              " Add semicolon
j
q

3@a                  " Execute 3 times

Example 2: Convert CSV to Markdown Table

Before: Name,Age,City John,30,NYC Jane,25,LA

After: | Name | Age | City | |——|—–|——| | John | 30 | NYC | | Jane | 25 | LA |

Macro:

qa
:s/,/ | /g           " Replace commas with pipe separators
I| <Esc>             " Add opening pipe
A |<Esc>             " Add closing pipe
j
q

" Create separator line manually, then:
@a                   " Format data rows

Example 3: Add Line Numbers

Before: First line Second line Third line

After:

  1. First line

  2. Second line

  3. Third line

Macro (simple, fixed numbering):

let @a = 'I1. ^j'    " Hard-coded "1"

Better - Using expression register:

qa
:let @b = line('.') . '. '   " Get current line number
I<C-r>=@b<CR><Esc>           " Insert it
j
q

Best - Using global command:

:let counter = 1
:g/^/s//\=counter . '. '/ | let counter += 1

Example 4: Extract URLs

Text with URLs: Check out https://example.com for more info. Visit https://github.com for code.

Extract to register:

qa
/https\?:\/\/[^ ]*<CR>   " Search for URL
"Ayw                      " Append yank to register A
n
q

100@a                     " Collect all URLs
:new                      " New buffer
"Ap                       " Paste collected URLs

Example 5: Increment Numbers

Before: item 1 item 1 item 1

After: item 1 item 2 item 3

Macro using Ctrl-A (increment):

qa
j                    " Move down
<C-a>                " Increment number under cursor
q

2@a                  " Increment next 2 lines

Example 6: Surround Words with Tags

Before: title subtitle content

After:

title

subtitle

content

Macros:

" Macro a: h1 tag
:let @a = 'I<h1>^A</h1>^j'

" Macro b: h2 tag
:let @b = 'I<h2>^A</h2>^j'

" Macro c: p tag
:let @c = 'I<p>^A</p>^j'

@a                   " Apply h1 to first line
@b                   " Apply h2 to second line
@c                   " Apply p to third line

Or use visual selection + surround plugin:

" With vim-surround plugin
viw                  " Select word
S                    " Surround command
<h1>                 " Type opening tag (closing auto-added)

Example 7: Create Getters/Setters

From:

private String name;
private int age;

To:

private String name;
public String getName() { return name; }
public void setName(String name) { this.name = name; }

private int age;
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }

Macro:

qa
yiw                  " Yank field name
A<CR>public <Esc>    " Add newline, "public"
pa get<Esc>          " Paste name with "get" prefix
~                    " Uppercase first letter (or use built-in function)
A() { return <Esc>p  " Add method body
A; }<Esc>            " Close method
" ... similar for setter ...
j
q

This is complex—better to use a snippet plugin!

7.8 Register and Macro Tips

Tip 1: Preserve Yank Across Deletes

yiw                  " Yank word
dd                   " Delete line (overwrites ")
"0p                  " Paste from yank register (preserved!)

Tip 2: Quick Access to Numbered Registers

"1p                  " Paste most recent delete
.                    " Automatically pastes "2
.                    " Pastes "3
...

Tip 3: Clipboard Mappings


-- Map leader-y/p for clipboard operations
vim.keymap.set('n', '<leader>y', '"+y', { desc = 'Yank to clipboard' })
vim.keymap.set('v', '<leader>y', '"+y', { desc = 'Yank to clipboard' })
vim.keymap.set('n', '<leader>p', '"+p', { desc = 'Paste from clipboard' })
vim.keymap.set('n', '<leader>P', '"+P', { desc = 'Paste before from clipboard' })

Tip 4: Macro Debugging

:reg a               " View macro
"ap                  " Paste into buffer
" Inspect and edit
"ayy                 " Yank back to register

Tip 5: Safe Macro Execution

Always test on one line first:

qa                   " Record macro
... edits ...
q
@a                   " Test on current line
u                    " Undo if wrong

Then execute on range:

:5,50normal @a       " Apply to lines 5-50

Tip 6: Macro Libraries

Create reusable macro collections:

" ~/.vim/macros.vim
let @f = 'I// ^j'              " Comment with //
let @h = 'I# ^j'               " Comment with #
let @q = 'ciw"^r""^j'          " Quote word

Load in vimrc:

source ~/.vim/macros.vim

Neovim (Lua):


-- ~/.config/nvim/lua/macros.lua
vim.fn.setreg('f', 'I// \x1bj')
vim.fn.setreg('h', 'I# \x1bj')

-- Note: \x1b represents <Esc>

Tip 7: Visual Block + Registers

Combine visual block mode with registers:

<C-v>                " Visual block
jjj                  " Select multiple lines
"ay                  " Yank to register a
...
"ap                  " Paste as block

Tip 8: Expression Register Calculations

" Insert result of calculation
<C-r>=15*8<CR>       " Inserts: 120

" Insert date
<C-r>=strftime('%Y-%m-%d')<CR>

" Insert line count
<C-r>=line('$')<CR>

Tip 9: Record Over Existing Macro

qa                   " Records new macro (overwrites old @a)

To preserve, use different register or append with qA.

Tip 10: Macro Error Handling

Graceful failure:

qa
:try
  j
  dd
:catch
  " Silently fail at EOF
:endtry
@a
q

Or rely on Vim’s natural error stopping:

qa
j                    " Fails at EOF, stops macro
dd
q

7.9 Neovim-Specific Features

Register API


-- Get register content
local content = vim.fn.getreg('a')
local regtype = vim.fn.getregtype('a')  -- 'c', 'l', or 'b'


-- Set register content
vim.fn.setreg('a', 'new content', 'l')  -- Line-wise


-- Append to register
vim.fn.setreg('a', vim.fn.getreg('a') .. '\nappended', 'l')

Execute Macro from Lua


-- Execute macro in register a
vim.cmd('normal @a')


-- With count
vim.cmd('normal 10@a')


-- On range
vim.cmd('5,10normal @a')

Dynamic Macro Generation


-- Generate macro based on conditions
local function create_macro()
  if vim.bo.filetype == 'python' then
    vim.fn.setreg('a', 'I# \x1bj')  -- Python comment
  elseif vim.bo.filetype == 'lua' then
    vim.fn.setreg('a', 'I-- \x1bj')  -- Lua comment
  else
    vim.fn.setreg('a', 'I// \x1bj')  -- Default comment
  end
end

vim.keymap.set('n', '<leader>mc', function()
  create_macro()
  vim.cmd('normal @a')
end, { desc = 'Comment with context-aware macro' })

Register Inspection


-- List all non-empty registers
for i = string.byte('a'), string.byte('z') do
  local reg = string.char(i)
  local content = vim.fn.getreg(reg)
  if content ~= '' then
    print(reg .. ': ' .. content)
  end
end

Persistent Macros

Save macros across sessions:


-- Save macros on exit
vim.api.nvim_create_autocmd('VimLeave', {
  callback = function()
    local macros = {}
    for i = string.byte('a'), string.byte('z') do
      local reg = string.char(i)
      local content = vim.fn.getreg(reg)
      if content ~= '' then
        macros[reg] = content
      end
    end
    
    local file = io.open(vim.fn.stdpath('data') .. '/macros.json', 'w')
    if file then
      file:write(vim.fn.json_encode(macros))
      file:close()
    end
  end
})


-- Load macros on startup
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    local file = io.open(vim.fn.stdpath('data') .. '/macros.json', 'r')
    if file then
      local content = file:read('*all')
      file:close()
      local macros = vim.fn.json_decode(content)
      for reg, val in pairs(macros) do
        vim.fn.setreg(reg, val)
      end
    end
  end
})

Chapter Summary

In this chapter, you mastered Vim’s register and macro systems:

Register System:

  • 48+ registers for different purposes

  • Unnamed ("): Default yank/delete

  • Yank (0): Preserves yanks across deletes

  • Numbered (1-9): Delete history with auto-increment

  • Small delete (-): Sub-line deletions

  • Named (a-z): Manual storage; uppercase appends

  • Read-only (%, #, :, /, .): System information

  • Clipboard (*, +): System integration

  • Black hole (_): True deletion without storage

  • Expression (=): Evaluate and insert results

Macro Fundamentals:

  • q{register} to record, q to stop

  • @{register} to execute, @@ to repeat

  • Count prefix: 10@a executes 10 times

  • Execute on range: :'<,'>normal @a

Advanced Techniques:

  • Editing macros: Paste, modify, yank back

  • Appending: qA appends to register A

  • Recursive macros: Call @a within recording of a

  • Conditional macros: Use :if within macro

  • Parallel registers: Multiple macros for different tasks

  • Macro chains: @a@b executes both sequentially

Practical Workflows:

  • Preserve yanks with register 0

  • Build snippet libraries in named registers

  • Use expression register for calculations/dates

  • Delete to black hole to avoid register pollution

  • Cycle through delete history with . after "1p

  • Store templates in registers for quick insertion

Best Practices:

  • Test macros on single line before batch execution

  • Put navigation before operations for safe failure

  • Use descriptive register names (mental mapping)

  • Clear search register (let @/ = '') to remove highlights

  • Leverage clipboard registers for cross-application workflows

  • Build macros incrementally with testing at each step

Neovim Enhancements:

  • Lua API for register manipulation

  • Programmatic macro generation

  • Persistent macro storage

  • Dynamic register content based on context

Key Insights:

  • Registers are the foundation of Vim’s text manipulation

  • Every operation interacts with registers (explicitly or implicitly)

  • Macros are executable register contents

  • Understanding register types (character/line/block) enables precise operations

  • The yank register (0) is crucial for copy-paste workflows

  • Black hole register (_) prevents register pollution

  • Expression register (=) brings computational power to text editing

Master registers and macros to eliminate repetitive tasks, build automation sequences, and achieve editing speeds impossible in traditional editors. These tools transform Vim from a text editor into a programmable text manipulation engine.

In the next chapter, we’ll explore Text Objects and Motions, covering Vim’s composable grammar for selecting and operating on semantic text units—words, sentences, paragraphs, tags, and custom delimiters.


Chapter 8: Text Formatting and Layout

Vim excels at transforming text structure—reformatting paragraphs, aligning columns, managing indentation, and controlling whitespace. While most editors provide basic formatting through menus and buttons, Vim’s command-line interface and operator grammar enable surgical precision and batch operations. This chapter covers Vim’s comprehensive formatting toolkit: indentation control, text wrapping, joining and splitting lines, case transformation, alignment techniques, whitespace management, and specialized formatting for code and prose.

8.1 Indentation

8.1.1 Manual Indentation

Basic operators:

>>                   " Indent line (shift right)
<<                   " De-indent line (shift left)
==                   " Auto-indent line

>{motion}            " Indent motion target
<{motion}            " De-indent motion target
={motion}            " Auto-indent motion target

Examples:

>>                   " Indent current line
3>>                  " Indent 3 lines (current + 2 below)
>}                   " Indent to end of paragraph
>G                   " Indent to end of file
>'a                  " Indent to mark a

Visual mode indentation:

V                    " Select line
>                    " Indent selection
<                    " De-indent selection
=                    " Auto-indent selection

Repeat indentation:

>>                   " Indent once
.                    " Indent again (repeat)
.                    " And again...

Visual block indentation:

<C-v>                " Visual block mode
jjj                  " Select multiple lines
>                    " Indent block

Indent in Insert mode:

<C-t>                " Indent current line (in Insert mode)
<C-d>                " De-indent current line (in Insert mode)

Example:

# In Insert mode, typing:
def hello():<CR>
<C-t>print("Hello")  # Indents the print statement

8.1.2 Indentation Settings

Key options:

:set tabstop=4       " Tab character displays as 4 spaces
:set shiftwidth=4    " Indent/de-indent uses 4 spaces
:set expandtab       " Convert tabs to spaces
:set softtabstop=4   " Tab key inserts 4 spaces (with expandtab)
:set autoindent      " Copy indent from current line
:set smartindent     " Smart auto-indenting for C-like files

Neovim (Lua):

vim.opt.tabstop = 4        -- Tab width
vim.opt.shiftwidth = 4     -- Indent width
vim.opt.expandtab = true   -- Use spaces instead of tabs
vim.opt.softtabstop = 4    -- Tab key behavior
vim.opt.autoindent = true  -- Maintain indent on new lines
vim.opt.smartindent = true -- Smart indenting

Common configurations:

Spaces (Python, Lua):

vim.opt.expandtab = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4

Tabs (Go, Makefiles):

vim.opt.expandtab = false
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4

Per-filetype settings:

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    vim.opt_local.tabstop = 4
    vim.opt_local.shiftwidth = 4
    vim.opt_local.expandtab = true
  end
})

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'javascript',
  callback = function()
    vim.opt_local.tabstop = 2
    vim.opt_local.shiftwidth = 2
    vim.opt_local.expandtab = true
  end
})

EditorConfig support:


-- Install editorconfig-vim plugin or use Neovim 0.9+ built-in
vim.g.editorconfig = true  -- Neovim 0.9+

8.1.3 Auto-Indenting Entire File

Indent whole file:

gg=G                 " Go to top, auto-indent to bottom

Breakdown:

  • gg - Jump to first line

  • = - Auto-indent operator

  • G - Motion to last line

Alternative with range:

:1,$=                " Indent lines 1 to end
:%=                  " Same (% means all lines)

Visual selection:

ggVG                 " Select entire file
=                    " Auto-indent

Maintain cursor position:

" Save position, indent, restore
let save_pos = getpos('.')
normal! gg=G
call setpos('.', save_pos)

Neovim (Lua function):

vim.keymap.set('n', '<leader>fi', function()
  local save_pos = vim.api.nvim_win_get_cursor(0)
  vim.cmd('normal! gg=G')
  vim.api.nvim_win_set_cursor(0, save_pos)
end, { desc = 'Format indentation' })

8.1.4 Reindent Specific Ranges

:10,20>              " Indent lines 10-20
:10,20>>             " Indent twice (two levels)
:10,20<              " De-indent lines 10-20

:'<,'>               " Visual selection range (automatic)
:'<,'>>              " Indent visual selection

Using marks:

ma                   " Set mark a
... move cursor ...
mb                   " Set mark b
:'a,'b>              " Indent between marks

8.1.5 Indent-Preserving Operations

Problem: Pasting code loses indentation.

Solutions:

Method 1: Adjust indentation after paste:

p                    " Paste
V                    " Select pasted lines
=                    " Auto-indent

Method 2: Use ]p (paste and adjust):

]p                   " Paste and match indent to current line
[p                   " Paste above and match indent

Method 3: Bracket paste mode:

:set paste           " Enter paste mode (disables auto-indent)
" Paste from system clipboard (Ctrl+Shift+V)
:set nopaste         " Exit paste mode

Better - Toggle with key:

:set pastetoggle=<F2>

Neovim - Auto-detect paste:

vim.opt.paste = false  -- Neovim handles this automatically

Neovim detects bracketed paste automatically (no :set paste needed).

8.1.6 Indent Text Objects

Select by indentation level:

" Requires vim-indent-object plugin or similar
vii                  " Select inner indent block (same level)
vai                  " Select indent block including line above
vii                  " Select including lines above and below

Manual equivalent:

" Select paragraph with same indent
V                    " Line select
}                    " To end of paragraph

8.2 Line Joining and Splitting

8.2.1 Joining Lines

Basic join:

J                    " Join current line with next (adds space)
gJ                   " Join without adding space

Examples:

Before: Hello World

After J: Hello World

After gJ: HelloWorld

Join with count:

3J                   " Join next 3 lines

Before: Line 1 Line 2 Line 3 Line 4

After 3J: Line 1 Line 2 Line 3 Line 4

Join in Visual mode:

V                    " Line select
jj                   " Select 3 lines
J                    " Join selected lines

Join specific range:

:10,15j              " Join lines 10-15
:10,15j!             " Join without spaces (like gJ)

8.2.2 Splitting Lines

Vim doesn’t have a built-in “split line” command, but various techniques exist.

Method 1: Replace with newline:

:s/ /\r/g            " Replace spaces with newlines

Before: one two three four

After: one two three four

Method 2: Insert mode split:

" Position cursor where you want to split
i<CR><Esc>           " Enter Insert mode, press Enter, exit

Method 3: Substitute at specific position:

:s/,/,\r/g           " Split at commas

Before: apple,banana,cherry

After: apple, banana, cherry

Method 4: Visual selection + substitute:

" Select text
:'<,'>s/, /,\r  /g   " Split at commas with indent

Smart split function:


-- Neovim: Split line at cursor
vim.keymap.set('n', '<leader>sl', function()
  local line = vim.api.nvim_get_current_line()
  local col = vim.api.nvim_win_get_cursor(0)[2]
  
  local before = line:sub(1, col)
  local after = line:sub(col + 1)
  
  vim.api.nvim_set_current_line(before)
  local row = vim.api.nvim_win_get_cursor(0)[1]
  vim.api.nvim_buf_set_lines(0, row, row, false, { after })
end, { desc = 'Split line at cursor' })

8.2.3 Join with Separator

Add separator while joining:

:5,10s/$/,/          " Add comma to end of lines 5-10
:5,10j               " Join the lines

Result: line1,line2,line3,...

One-step approach:

:5,10j | s/ /, /g    " Join and replace spaces with comma-space

8.3 Text Wrapping and Formatting

8.3.1 Hard Wrapping (Text Reflow)

Format paragraph:

gq}                  " Format to end of paragraph
gqap                 " Format around paragraph (text object)
gqG                  " Format to end of file

How it works:

  • Reads textwidth option

  • Reflows text to fit within width

  • Preserves paragraph structure

Set text width:

:set textwidth=80    " Wrap at 80 columns

Neovim (Lua):

vim.opt.textwidth = 80

Format specific range:

:10,20gq             " Format lines 10-20
:'<,'>gq             " Format visual selection

Examples:

Before (textwidth=40): This is a very long line that exceeds the text width limit and needs to be reformatted.

After gqq (format current line): This is a very long line that exceeds the text width limit and needs to be reformatted.

Auto-format while typing:

:set formatoptions+=t   " Auto-wrap text using textwidth
:set formatoptions+=c   " Auto-wrap comments
:set formatoptions+=r   " Auto-insert comment leader on <Enter>

Neovim (Lua):

vim.opt.formatoptions:append('t')  -- Auto-wrap text
vim.opt.formatoptions:append('c')  -- Auto-wrap comments
vim.opt.formatoptions:append('r')  -- Continue comments

Disable auto-wrap:

vim.opt.formatoptions:remove('t')
vim.opt.formatoptions:remove('c')

8.3.2 Soft Wrapping (Visual Only)

Soft wrap doesn’t modify the file—just how lines display.

:set wrap            " Enable soft wrap (default)
:set nowrap          " Disable soft wrap

Wrap options:

:set linebreak       " Break at word boundaries (not mid-word)
:set breakindent     " Preserve indent on wrapped lines
:set showbreak=↪\    " Show symbol at wrap point

Neovim (Lua):

vim.opt.wrap = true          -- Soft wrap
vim.opt.linebreak = true     -- Break at words
vim.opt.breakindent = true   -- Indent wrapped lines
vim.opt.showbreak = '↪ '     -- Wrap indicator

Navigate wrapped lines:

gj                   " Move down one display line
gk                   " Move up one display line
g0                   " Go to start of display line
g$                   " Go to end of display line

Mapping for natural movement:


-- Make j/k move by display lines in wrapped text
vim.keymap.set('n', 'j', 'gj', { noremap = true })
vim.keymap.set('n', 'k', 'gk', { noremap = true })
vim.keymap.set('n', 'gj', 'j', { noremap = true })
vim.keymap.set('n', 'gk', 'k', { noremap = true })

8.3.3 Format Options

The formatoptions setting controls auto-formatting behavior.

:set formatoptions?  " Show current settings

Common format options:

Flag Description
t Auto-wrap text using textwidth
c Auto-wrap comments
r Auto-insert comment leader after <Enter> in Insert mode
o Auto-insert comment leader after o or O in Normal mode
q Allow formatting comments with gq
a Auto-format paragraphs (aggressive)
n Recognize numbered lists when formatting
2 Use second line’s indent for paragraph
j Remove comment leader when joining lines
l Don’t break long lines in Insert mode

Recommended settings:

vim.opt.formatoptions = 'jcroqlnt'

-- j: Remove comment leader when joining

-- c: Auto-wrap comments

-- r: Continue comments on Enter

-- o: Continue comments on o/O

-- q: Allow gq to format comments

-- l: Don't break long lines

-- n: Recognize numbered lists

-- t: Auto-wrap text

Per-filetype:

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'markdown',
  callback = function()
    vim.opt_local.formatoptions = 'jcroqln2t'
    vim.opt_local.textwidth = 80
  end
})

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    vim.opt_local.formatoptions = 'jcroql'
    vim.opt_local.textwidth = 88  -- Black formatter default
  end
})

8.3.4 External Formatting Programs

Use external formatter with = operator:

:set formatprg=prettier\ --stdin-filepath\ %

Now = uses Prettier instead of Vim’s built-in formatting.

Format with external command:

:%!prettier --stdin-filepath %
:%!black -                      " Python formatter
:%!rustfmt                      " Rust formatter
:%!gofmt                        " Go formatter

Format range:

:'<,'>!prettier --stdin-filepath %

Neovim (Lua) - LSP formatting:

vim.keymap.set('n', '<leader>f', function()
  vim.lsp.buf.format({ async = true })
end, { desc = 'Format buffer' })

Using formatters via LSP:


-- In LSP attach function
vim.keymap.set('n', '<leader>f', function()
  vim.lsp.buf.format({
    async = true,
    filter = function(client)

      -- Use specific formatter
      return client.name == 'null-ls' or client.name == 'efm'
    end
  })
end, { buffer = bufnr, desc = 'Format buffer' })

8.4 Case Transformation

8.4.1 Basic Case Operations

Toggle case:

~                    " Toggle case of character under cursor
g~{motion}           " Toggle case of motion target
g~~                  " Toggle case of line
g~iw                 " Toggle case of word

Uppercase:

gU{motion}           " Uppercase motion target
gUU                  " Uppercase line
gUiw                 " Uppercase word
gUG                  " Uppercase to end of file

Lowercase:

gu{motion}           " Lowercase motion target
guu                  " Lowercase line
guiw                 " Lowercase word

Examples:

Before: Hello World

After g~~: hELLO wORLD

After gUU: HELLO WORLD

After guu: hello world

8.4.2 Visual Mode Case Changes

v                    " Character select
~                    " Toggle case
U                    " Uppercase
u                    " Lowercase

Example workflow:

viw                  " Select word
U                    " Uppercase → WORD

8.4.3 Title Case and Sentence Case

Vim doesn’t have built-in title case, but you can use substitution:

Capitalize first letter:

:s/\<\(\w\)/\u\1/g   " Capitalize first letter of each word

Before: the quick brown fox

After: The Quick Brown Fox

Capitalize only first letter of sentence:

:s/\.\s*\(\w\)/\.\r\u\1/g

Smart title case function:


-- Neovim: Title case with exceptions
vim.keymap.set('n', '<leader>tc', function()
  local line = vim.api.nvim_get_current_line()
  local small_words = { 'a', 'an', 'and', 'as', 'at', 'but', 'by', 'for', 'in', 'of', 'on', 'or', 'the', 'to', 'up' }
  

  -- Split into words, capitalize appropriately
  local words = {}
  for word in line:gmatch('%S+') do
    local lower = word:lower()
    if vim.tbl_contains(small_words, lower) and #words > 0 then
      table.insert(words, lower)
    else
      table.insert(words, word:sub(1,1):upper() .. word:sub(2):lower())
    end
  end
  
  vim.api.nvim_set_current_line(table.concat(words, ' '))
end, { desc = 'Title case line' })

8.4.4 Case in Substitutions

Preserve case:

:%s/old/new/g        " Normal replacement
:%s/old/new/gi       " Case-insensitive search

Force case in replacement:

:%s/old/\Lnew/g      " Lowercase replacement
:%s/old/\Unew/g      " Uppercase replacement

Special replacement atoms:

  • \u - Uppercase next character

  • \U - Uppercase until \E or \e

  • \l - Lowercase next character

  • \L - Lowercase until \E or \e

  • \e or \E - End case conversion

Examples:

:%s/\<\(\w\)/\u\1/g          " Capitalize first letter
:%s/\<\(\w\+\)/\U\1/g        " All caps
:%s/\<\(\w\)\(\w*\)/\u\1\L\2/g  " Title case each word

8.5 Alignment and Columns

8.5.1 Visual Block Alignment

Insert at column position:

<C-v>                " Visual block
jjj                  " Select lines
I                    " Insert at start of block
" Type text
<Esc>                " Apply to all lines

Example - Add comments:

Before: int x = 10 int y = 20 int z = 30

After <C-v>jjI// <Esc>: // int x = 10 // int y = 20 // int z = 30

Append at end:

<C-v>                " Visual block
jjj
$                    " Extend to end of lines
A                    " Append
;                    " Type semicolon
<Esc>

8.5.2 Align at Character

Manual alignment using substitute:

Before: name = “John” age = 30 city = “NYC”

Align at =:

:5,7s/\s*=\s*/ = /

Better - align to column:

" Find longest line before =, then pad

Using external tools:

:%!column -t         " Align columns (Unix tool)

Before: name John 30 age 25 50 city NYC 100

After :%!column -t: name John 30 age 25 50 city NYC 100

Tabular plugin (popular):

" Install junegunn/vim-easy-align or godlygeek/tabular

:Tabularize /=       " Align at =
:Tabularize /,       " Align at ,

8.5.3 Aligning Comments

Example - Align end-of-line comments:

Before:

x = 10      # Variable x
y = 200     # Variable y
z = 3       # Variable z

Using Tabular:

:Tabularize /#

After:

x = 10      # Variable x
y = 200     # Variable y
z = 3       # Variable z

8.5.4 Number Alignment

Right-align numbers:

" Select numbers
<C-v>
" Right-align (manually space or use plugin)

Using external tool:

:%!awk '{printf "%10s\n", $1}'

8.6 Whitespace Management

8.6.1 Trailing Whitespace

Remove trailing whitespace:

:%s/\s\+$//          " Remove from all lines
:5,10s/\s\+$//       " Remove from lines 5-10

Visual feedback:

:set list            " Show invisible characters
:set listchars=trail:·,tab:→\ ,nbsp:␣

Neovim (Lua):

vim.opt.list = true
vim.opt.listchars = {
  trail = '·',
  tab = '→ ',
  nbsp = '␣',
  extends = '⟩',
  precedes = '⟨',
}

Auto-remove on save:

vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*',
  callback = function()

    -- Save cursor position
    local save_pos = vim.api.nvim_win_get_cursor(0)

    -- Remove trailing whitespace
    vim.cmd([[%s/\s\+$//e]])

    -- Restore cursor
    vim.api.nvim_win_set_cursor(0, save_pos)
  end
})

Selective removal (avoid breaking Markdown):

vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = { '*.lua', '*.py', '*.js', '*.ts', '*.rs' },
  callback = function()
    local save_pos = vim.api.nvim_win_get_cursor(0)
    vim.cmd([[%s/\s\+$//e]])
    vim.api.nvim_win_set_cursor(0, save_pos)
  end
})

8.6.2 Leading Whitespace

Remove leading spaces:

:%s/^\s\+//          " Remove all leading whitespace

Normalize to tabs:

:%s/^\s\+/\=repeat("\t", len(submatch(0))/&tabstop)/

8.6.3 Multiple Blank Lines

Collapse multiple blank lines to one:

:%s/\n\n\+/\r\r/     " Reduce to max 2 newlines (1 blank line)

Remove all blank lines:

:g/^$/d              " Global delete empty lines

Remove blank lines in range:

:10,20g/^$/d

8.6.4 Tab/Space Conversion

Convert tabs to spaces:

:set expandtab       " Enable space mode
:retab               " Convert existing tabs

Convert spaces to tabs:

:set noexpandtab     " Enable tab mode
:retab!              " Force conversion

Convert leading spaces only:

:%s/^\s\+/\=repeat("\t", len(submatch(0))/&shiftwidth)/

8.7 Line Manipulation

8.7.1 Sorting Lines

Sort entire file:

:%sort               " Alphabetical ascending
:%sort!              " Descending

Sort range:

:10,20sort           " Sort lines 10-20
:'<,'>sort           " Sort visual selection

Sort options:

:sort i              " Case-insensitive
:sort n              " Numerical sort
:sort u              " Remove duplicates
:sort! n             " Numerical descending

Examples:

Before: 30 10 20

After :sort n: 10 20 30

Sort by column:

:sort /.*\%5c/       " Sort by character at column 5

Sort by pattern:

:sort /\d\+/         " Sort by first number in line

8.7.2 Removing Duplicates

Remove duplicate lines:

:sort u              " Sort and unique

Remove duplicates without sorting:

" Requires external tool
:%!uniq

Or use Vimscript:

:let seen = {}
:g/^/if has_key(seen, getline('.')) | d | else | let seen[getline('.')] = 1 | endif

Neovim (Lua) - Remove duplicates:

vim.keymap.set('n', '<leader>du', function()
  local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
  local seen = {}
  local unique = {}
  
  for _, line in ipairs(lines) do
    if not seen[line] then
      seen[line] = true
      table.insert(unique, line)
    end
  end
  
  vim.api.nvim_buf_set_lines(0, 0, -1, false, unique)
end, { desc = 'Remove duplicate lines' })

8.7.3 Reversing Lines

Reverse entire file:

:g/^/m0              " Global move to top (reverses order)

Reverse range:

:10,20g/^/m9         " Reverse lines 10-20

Visual selection:

:'<,'>g/^/m'<

Using external tool:

:%!tac               " Unix tac command (reverse of cat)

8.7.4 Moving Lines

Move line up/down:

:m +1                " Move current line down
:m -2                " Move current line up

Move range:

:5,10m 20            " Move lines 5-10 to after line 20

Common mappings:


-- Move line up/down
vim.keymap.set('n', '<A-j>', ':m .+1<CR>==', { noremap = true, silent = true })
vim.keymap.set('n', '<A-k>', ':m .-2<CR>==', { noremap = true, silent = true })


-- Move visual selection up/down
vim.keymap.set('v', '<A-j>', ":m '>+1<CR>gv=gv", { noremap = true, silent = true })
vim.keymap.set('v', '<A-k>', ":m '<-2<CR>gv=gv", { noremap = true, silent = true })

8.8 Text Width and Columns

8.8.1 Column Ruler

Show vertical line at column:

:set colorcolumn=80     " Show line at column 80
:set colorcolumn=80,120 " Multiple columns
:set colorcolumn=       " Disable

Neovim (Lua):

vim.opt.colorcolumn = '80'

-- Or relative to textwidth:
vim.opt.colorcolumn = '+1'  -- One column after textwidth

Highlight over-length lines:

:match ErrorMsg /\%>80v.\+/

8.8.2 Virtual Edit Mode

Allow cursor past end of line:

:set virtualedit=all     " Cursor anywhere
:set virtualedit=block   " Only in Visual Block mode
:set virtualedit=insert  " Only in Insert mode
:set virtualedit=        " Disable

Useful for aligning text in Visual Block mode.

8.8.3 Cursor Column Display

:set cursorcolumn    " Highlight current column
:set nocursorcolumn  " Disable

Combined with cursor line:

vim.opt.cursorline = true
vim.opt.cursorcolumn = true

8.9 Code Formatting

8.9.1 Comment Toggling

Manual commenting:

I// <Esc>            " Add // comment
:s/^/\/\/ /          " Substitute-based

Visual block comments:

<C-v>jjI// <Esc>     " Add // to multiple lines

Using commentary.vim plugin:

gcc                  " Toggle comment on line
gc{motion}           " Comment motion target
gcap                 " Comment paragraph

Neovim - Comment.nvim:


-- Install numToStr/Comment.nvim
require('Comment').setup()


-- Usage:

-- gcc - Line comment

-- gbc - Block comment

-- gc{motion} - Comment motion

8.9.2 Code Blocks

Surround with braces:

" Select lines
V
jjj
S}                   " vim-surround plugin

Manual method:

O{<Esc>              " Add opening brace above
jo}<Esc>             " Add closing brace below
=iB                  " Re-indent inside braces

8.9.3 Function/Method Formatting

Reformat function arguments:

Before:

function test(arg1, arg2, arg3, arg4, arg5) {

After (multi-line):

function test(
  arg1,
  arg2,
  arg3,
  arg4,
  arg5
) {

Macro for argument splitting:

qa
f(                   " Find opening paren
a<CR><Esc>           " Add newline after
f,                   " Find comma
a<CR>  <Esc>         " Add newline with indent
@a                   " Recursive call
q

Better - use LSP formatter or Prettier.

8.10 Specialized Formatting

8.10.1 Table Formatting

ASCII table creation:

" Using external tool
:%!column -t -s '|'

Markdown table:

Before: Name|Age|City John|30|NYC Jane|25|LA

After formatting: | Name | Age | City | |——|—–|——| | John | 30 | NYC | | Jane | 25 | LA |

Using vim-table-mode plugin:

:TableModeEnable
" Type: | Name | Age | City
" Automatically formats on ||

8.10.2 List Formatting

Bullet lists:

:set formatoptions+=n   " Recognize numbered lists

Auto-numbering:

" Select lines
:'<,'>!nl -s '. '

Or macro:

" Add sequential numbers
let counter = 1
qa
I<C-r>=counter<CR>. <Esc>
:let counter += 1
j
q

8.10.3 JSON/XML Formatting

JSON:

:%!jq .              " Format JSON (requires jq)
:%!python -m json.tool

XML:

:%!xmllint --format -

Minify JSON:

:%!jq -c .

8.11 Practical Formatting Workflows

Workflow 1: Clean Up Pasted Code

" Paste code
p
" Fix indentation
=ip
" Remove trailing whitespace
:s/\s\+$//
" Convert tabs to spaces
:retab

Or create mapping:

vim.keymap.set('n', '<leader>cp', function()
  vim.cmd('normal! =ip')  -- Reindent paragraph
  local line = vim.fn.line('.')
  vim.cmd(line .. 's/\\s\\+$//e')  -- Remove trailing space
  vim.cmd('retab')  -- Convert tabs
end, { desc = 'Clean pasted code' })

Workflow 2: Format Entire File

vim.keymap.set('n', '<leader>ff', function()

  -- Save position
  local save_pos = vim.api.nvim_win_get_cursor(0)
  

  -- Format
  vim.cmd('normal! gg=G')  -- Reindent
  vim.cmd('%s/\\s\\+$//e') -- Remove trailing whitespace
  vim.cmd('%s/\\n\\n\\n\\+/\\r\\r/e')  -- Collapse blank lines
  

  -- Restore position
  vim.api.nvim_win_set_cursor(0, save_pos)
  
  print('File formatted')
end, { desc = 'Format entire file' })

Workflow 3: Format Selection

vim.keymap.set('v', '<leader>f', function()
  vim.cmd("'<,'>normal! =")  -- Reindent
  vim.cmd("'<,'>s/\\s\\+$//e")  -- Clean trailing space
end, { desc = 'Format selection' })

Workflow 4: Smart Auto-Format on Save


-- Auto-format specific filetypes on save
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = { '*.lua', '*.py', '*.js', '*.ts', '*.rs', '*.go' },
  callback = function()

    -- Try LSP formatting first
    if vim.lsp.buf.format then
      vim.lsp.buf.format({ timeout_ms = 2000 })
    else

      -- Fallback to built-in formatting
      local save_pos = vim.api.nvim_win_get_cursor(0)
      vim.cmd('normal! gg=G')
      vim.api.nvim_win_set_cursor(0, save_pos)
    end
    

    -- Remove trailing whitespace
    vim.cmd('%s/\\s\\+$//e')
  end
})

Chapter Summary

This chapter covered Vim’s comprehensive text formatting toolkit:

Indentation:

  • >>, <<, == for manual indenting

  • gg=G to auto-indent entire file

  • >}, <ap, =G for motion-based indenting

  • <C-t>, <C-d> for Insert mode indenting

  • Configure with tabstop, shiftwidth, expandtab, autoindent

  • Per-filetype settings via autocmds

Joining and Splitting:

  • J joins lines with space

  • gJ joins without space

  • :s/pattern/\r/g splits at pattern

  • Range joining: :10,15j

Text Wrapping:

  • Hard wrap with gq{motion}, gqap, gqG

  • Configure with textwidth and formatoptions

  • Soft wrap with wrap, linebreak, breakindent

  • Navigate with gj, gk, g0, g$

Case Transformation:

  • ~, g~{motion} toggle case

  • gU{motion} uppercase

  • gu{motion} lowercase

  • Substitution modifiers: \u, \U, \l, \L

Alignment:

  • Visual block: <C-v> + I or A

  • External tools: column -t, Tabular plugin

  • LSP formatters for code alignment

Whitespace Management:

  • Remove trailing: :%s/\s\+$//

  • Show invisibles: set list, listchars

  • Auto-remove on save via autocmd

  • Tab/space conversion: :retab, :retab!

Line Manipulation:

  • Sort: :sort, :sort n, :sort u

  • Reverse: :g/^/m0

  • Move: :m +1, :5,10m 20

  • Remove duplicates: :sort u or custom function

Code Formatting:

  • Comment toggling via plugins (Comment.nvim, commentary.vim)

  • Visual block commenting

  • LSP integration: vim.lsp.buf.format()

  • External formatters: :%!prettier, :%!black -

Specialized Formatting:

  • JSON: :%!jq .

  • XML: :%!xmllint --format -

  • Tables: vim-table-mode, column tool

  • Lists: auto-numbering, format options

Best Practices:

  • Set textwidth for prose, disable for code

  • Use formatoptions to control auto-formatting

  • Leverage LSP formatters over manual indenting

  • Create filetype-specific formatting rules

  • Map frequently used formatting to leader keys

  • Auto-format on save for consistent code style

  • Use soft wrap for prose, hard wrap sparingly

  • Combine operations in macros for complex formatting

Key Insights:

  • Formatting is composable: combine operators with motions

  • External tools (jq, prettier, black) integrate seamlessly

  • LSP provides language-aware formatting

  • Visual block mode is powerful for column editing

  • Format options control automatic behavior

  • Whitespace visibility aids manual cleanup

  • Auto-formatting on save ensures consistency

Master these formatting techniques to maintain clean, consistent code and prose. Vim’s formatting commands combine with its operator grammar to enable surgical precision and bulk operations—from indenting a single line to reformatting an entire project.

In the next chapter, we’ll explore Advanced Navigation, covering marks, jumps, tags, fuzzy finding, and file navigation systems that enable lightning-fast movement across your codebase.


Chapter 9: File Operations and Sessions

Vim’s file operations extend far beyond simple save and open commands. This chapter explores Vim’s sophisticated file management system: reading and writing files, working with multiple files simultaneously, managing sessions and views, leveraging the argument list, and integrating with external file operations. We’ll cover both traditional Vim workflows and modern Neovim enhancements that transform Vim into a powerful workspace manager.

9.1 Basic File Operations

9.1.1 Opening Files

Single file:

:e filename.txt      " Edit file (relative to current directory)
:e! filename.txt     " Reload file, discard changes
:e                   " Reload current file from disk
:e!                  " Reload current file, discard changes

Absolute paths:

:e /path/to/file.txt
:e ~/documents/file.txt
:e $HOME/.config/nvim/init.lua

Create new file:

:e newfile.txt       " Creates buffer, saved on :w
:enew                " Create unnamed buffer
:enew!               " Create buffer, discard changes

Open with line number:

:e +25 file.txt      " Open at line 25
:e +/pattern file.txt " Open at first pattern match
:e +$ file.txt       " Open at last line

From command line:

vim file.txt
vim +25 file.txt                    # Open at line 25
vim "+normal 10gg" file.txt         # Execute Normal mode command
vim -c "set number" file.txt        # Execute Ex command
vim -o file1.txt file2.txt          # Open in horizontal splits
vim -O file1.txt file2.txt          # Open in vertical splits
vim -p file1.txt file2.txt          # Open in tabs

Neovim additions:

nvim --headless -c "cmd" file.txt   # Headless mode (scripting)
nvim -d file1.txt file2.txt         # Diff mode

9.1.2 Writing Files

Basic save:

:w                   " Write current buffer
:w!                  " Force write (override protections)
:w filename.txt      " Write to new filename (doesn't change buffer name)
:w >> file.txt       " Append to file

Save specific range:

:5,10w partial.txt   " Write lines 5-10 to new file
:'<,'>w selection.txt " Write visual selection
:.w line.txt         " Write current line

Save and quit:

:wq                  " Write and quit
:x                   " Write (if changes) and quit
ZZ                   " Same as :x (Normal mode)
:wqa                 " Write all buffers and quit all
:xa                  " Write changed buffers and quit all

Save with sudo:

:w !sudo tee %       " Write with elevated permissions

Neovim (Lua) - Create mapping:

vim.keymap.set('n', '<leader>W', ':w !sudo tee %<CR>', 
  { desc = 'Save with sudo' })

Write to command output:

:w !wc -l            " Pipe buffer to word count
:5,10w !sort         " Pipe lines 5-10 to sort

9.1.3 Reading Files

Insert file contents:

:r filename.txt      " Read file below current line
:0r filename.txt     " Read at top of file
:$r filename.txt     " Read at end of file
:5r filename.txt     " Read after line 5

Read command output:

:r !ls               " Insert ls output
:r !date             " Insert current date
:r !curl https://api.example.com/data

Neovim example - Insert template:

vim.keymap.set('n', '<leader>it', function()
  local template = vim.fn.expand('~/.config/nvim/templates/python.txt')
  vim.cmd('0r ' .. template)
end, { desc = 'Insert Python template' })

9.1.4 File Information

Show file info:

:f                   " Show filename, position, status
:file                " Same as :f
<C-g>                " Quick file info (Normal mode)
g<C-g>               " Word count, character count

Output example: “init.lua” line 42 of 150 –28%– col 15

Detailed statistics:

g<C-g>               " Shows:
                     " Column, Line, Word, Char, Byte position

File type detection:

:set filetype?       " Show detected filetype
:set filetype=python " Manually set filetype

Neovim (Lua) - Custom status info:

vim.keymap.set('n', '<leader>fi', function()
  local bufnr = vim.api.nvim_get_current_buf()
  local lines = vim.api.nvim_buf_line_count(bufnr)
  local filename = vim.api.nvim_buf_get_name(bufnr)
  local filetype = vim.bo.filetype
  local cursor = vim.api.nvim_win_get_cursor(0)
  
  print(string.format(
    "File: %s | Type: %s | Lines: %d | Cursor: %d,%d",
    filename, filetype, lines, cursor[1], cursor[2]
  ))
end, { desc = 'Show file info' })

9.1.5 File Existence and Type Checks

Check if file exists:

:if filereadable('file.txt')
:  echo "File exists"
:endif

Neovim (Lua):

if vim.fn.filereadable('file.txt') == 1 then
  print('File exists and is readable')
end


-- Or using Lua filesystem
local uv = vim.loop
local stat = uv.fs_stat('file.txt')
if stat and stat.type == 'file' then
  print('File exists')
end

Check if directory exists:

:if isdirectory('path/to/dir')
:  echo "Directory exists"
:endif

Get file extension:

:echo expand('%:e')  " Extension
:echo expand('%:t')  " Filename without path
:echo expand('%:p')  " Full path
:echo expand('%:h')  " Directory

9.2 Buffer Management

9.2.1 Buffer Basics

Buffers are in-memory representations of files. Understanding buffer states is essential.

Buffer states:

  • Active: Displayed in a window

  • Hidden: Loaded but not displayed

  • Inactive: Not loaded

List buffers:

:ls                  " List all buffers
:buffers             " Same as :ls
:files               " Same as :ls

Buffer list flags: % - Current buffer # - Alternate buffer a - Active (loaded and visible) h - Hidden (loaded but not visible)

    • Not modifiable = - Readonly
    • Modified x - Read errors

Example output: 1 %a “init.lua” line 42 2 h “plugins.lua” line 15 3 a “keymaps.lua” line 1

Navigate buffers:

:b2                  " Switch to buffer 2
:b filename          " Switch by name (tab-completion works)
:bn                  " Next buffer
:bp                  " Previous buffer
:bf                  " First buffer
:bl                  " Last buffer
<C-^>                " Toggle between current and alternate buffer

Delete buffers:

:bd                  " Delete current buffer
:bd!                 " Force delete (discard changes)
:bd 2                " Delete buffer 2
:2,5bd               " Delete buffers 2-5
:bd filename         " Delete by name
:%bd                 " Delete all buffers
:%bd|e#              " Delete all except current

Unload vs Delete:

:bunload             " Unload buffer (keep in list)
:bdelete             " Delete buffer (remove from list)
:bwipeout            " Wipe buffer (remove completely)

9.2.2 Hidden Buffers

By default, Vim prevents switching buffers with unsaved changes.

:set hidden          " Allow hidden buffers with unsaved changes
:set nohidden        " Require save before switching (default)

Neovim (Lua):

vim.opt.hidden = true  -- Allow hidden buffers

Why use hidden?

  • Switch between files without saving

  • Maintain undo history across switches

  • Work with multiple files simultaneously

List modified buffers:

:ls +                " Show only modified buffers

9.2.3 Alternate Buffer

The alternate buffer (#) is the previously viewed buffer.

<C-^>                " Toggle to alternate buffer
:b#                  " Switch to alternate buffer
:e#                  " Edit alternate buffer

Use cases:

  • Quick toggle between two files

  • Compare implementations

  • Reference switching

Neovim mapping:


-- Enhanced buffer toggle
vim.keymap.set('n', '<leader><Tab>', '<C-^>', 
  { desc = 'Toggle alternate buffer' })

9.2.4 Buffer Operations

Execute command in all buffers:

:bufdo %s/old/new/ge " Substitute in all buffers
:bufdo set number    " Set option in all buffers
:bufdo w             " Save all buffers

Close other buffers:

:only                " Close all windows except current
:bo[tright] new      " Open new buffer in bottom window

Buffer-local options:


-- Set option for current buffer only
vim.bo.tabstop = 2
vim.bo.expandtab = true


-- Or using opt_local
vim.opt_local.tabstop = 2

9.2.5 Modern Buffer Navigation

Using bufferline.nvim (popular plugin):


-- Install akinsho/bufferline.nvim
require('bufferline').setup({
  options = {
    mode = 'buffers',
    numbers = 'ordinal',
    diagnostics = 'nvim_lsp',
    show_buffer_close_icons = true,
    show_close_icon = false,
  }
})


-- Mappings
vim.keymap.set('n', '<Tab>', ':BufferLineCycleNext<CR>')
vim.keymap.set('n', '<S-Tab>', ':BufferLineCyclePrev<CR>')
vim.keymap.set('n', '<leader>1', ':BufferLineGoToBuffer 1<CR>')
vim.keymap.set('n', '<leader>2', ':BufferLineGoToBuffer 2<CR>')

Telescope buffer picker:

vim.keymap.set('n', '<leader>fb', ':Telescope buffers<CR>', 
  { desc = 'Find buffers' })

9.3 Window and Split Management

9.3.1 Creating Splits

Horizontal splits:

:split filename      " Horizontal split, edit file
:sp filename         " Short form
<C-w>s               " Split current window
:new                 " Split with empty buffer

Vertical splits:

:vsplit filename     " Vertical split, edit file
:vs filename         " Short form
<C-w>v               " Vertical split current window
:vnew                " Vertical split with empty buffer

Split with size:

:10split file.txt    " 10-line horizontal split
:vertical 80split    " 80-column vertical split

Open in splits from command line:

vim -o file1 file2   # Horizontal splits
vim -O file1 file2   # Vertical splits
vim -o5 *.txt        # 5 horizontal splits max

Basic navigation:

<C-w>h               " Move to left window
<C-w>j               " Move down
<C-w>k               " Move up
<C-w>l               " Move right
<C-w>w               " Cycle through windows
<C-w>p               " Previous window

Direct window access:

<C-w>t               " Top-left window
<C-w>b               " Bottom-right window

Neovim - Easier navigation:


-- Map Ctrl+hjkl to window navigation
vim.keymap.set('n', '<C-h>', '<C-w>h')
vim.keymap.set('n', '<C-j>', '<C-w>j')
vim.keymap.set('n', '<C-k>', '<C-w>k')
vim.keymap.set('n', '<C-l>', '<C-w>l')

9.3.3 Resizing Windows

Manual resize:

<C-w>+               " Increase height
<C-w>-               " Decrease height
<C-w>>               " Increase width
<C-w><               " Decrease width
<C-w>=               " Equalize all windows
<C-w>|               " Maximize width
<C-w>_               " Maximize height

Precise resize:

:resize 20           " Set height to 20 lines
:vertical resize 80  " Set width to 80 columns
:resize +5           " Increase by 5 lines
:vertical resize -10 " Decrease by 10 columns

Neovim - Smart resize mappings:


-- Resize with arrow keys
vim.keymap.set('n', '<C-Up>', ':resize +2<CR>')
vim.keymap.set('n', '<C-Down>', ':resize -2<CR>')
vim.keymap.set('n', '<C-Left>', ':vertical resize -2<CR>')
vim.keymap.set('n', '<C-Right>', ':vertical resize +2<CR>')

9.3.4 Moving and Arranging Windows

Move window to position:

<C-w>H               " Move window to far left
<C-w>J               " Move window to bottom
<C-w>K               " Move window to top
<C-w>L               " Move window to far right
<C-w>r               " Rotate windows downward/rightward
<C-w>R               " Rotate windows upward/leftward
<C-w>x               " Exchange with next window

Convert split orientation:

<C-w>H               " Vertical → horizontal (move left)
<C-w>K               " Horizontal → vertical (move up)

9.3.5 Closing Windows

:q                   " Quit current window
:q!                  " Quit, discard changes
:qa                  " Quit all windows
:qa!                 " Quit all, discard all changes
<C-w>q               " Quit current window
<C-w>c               " Close current window
<C-w>o               " Close all windows except current
:only                " Same as <C-w>o

9.3.6 Floating Windows (Neovim)

Neovim supports floating windows—overlays not bound to the split grid.

Create floating window:

local buf = vim.api.nvim_create_buf(false, true)  -- No file, scratch buffer
local width = 60
local height = 20

local win = vim.api.nvim_open_win(buf, true, {
  relative = 'editor',
  width = width,
  height = height,
  col = (vim.o.columns - width) / 2,  -- Center horizontally
  row = (vim.o.lines - height) / 2,   -- Center vertically
  style = 'minimal',
  border = 'rounded',
})


-- Set content
vim.api.nvim_buf_set_lines(buf, 0, -1, false, {
  'Hello from floating window!',
  'Press q to close'
})


-- Close on 'q'
vim.keymap.set('n', 'q', ':close<CR>', { buffer = buf })

Floating terminal:

vim.keymap.set('n', '<leader>tt', function()
  local buf = vim.api.nvim_create_buf(false, true)
  local width = math.floor(vim.o.columns * 0.8)
  local height = math.floor(vim.o.lines * 0.8)
  
  vim.api.nvim_open_win(buf, true, {
    relative = 'editor',
    width = width,
    height = height,
    col = math.floor((vim.o.columns - width) / 2),
    row = math.floor((vim.o.lines - height) / 2),
    style = 'minimal',
    border = 'rounded',
  })
  
  vim.cmd('terminal')
end, { desc = 'Floating terminal' })

9.4 Tab Pages

Tab pages are collections of windows—separate layouts within one Vim instance.

9.4.1 Creating and Navigating Tabs

Create tabs:

:tabnew              " New tab with empty buffer
:tabe filename       " Open file in new tab
:tabedit filename    " Same as :tabe

Navigate tabs:

gt                   " Next tab
gT                   " Previous tab
:tabn                " Next tab
:tabp                " Previous tab
:tabfirst            " First tab
:tablast             " Last tab

Direct tab access:

1gt                  " Go to tab 1
2gt                  " Go to tab 2
:tabn 3              " Go to tab 3

Command line:

vim -p file1 file2 file3  # Open in separate tabs

9.4.2 Managing Tabs

Close tabs:

:tabc                " Close current tab
:tabclose            " Same
:tabo                " Close all tabs except current
:tabonly             " Same

Move tabs:

:tabm 0              " Move tab to position 0 (first)
:tabm                " Move tab to last position
:tabm +1             " Move tab one position right
:tabm -1             " Move tab one position left

Execute in all tabs:

:tabdo %s/old/new/ge " Substitute in all tabs
:tabdo set number    " Set option in all tabs

9.4.3 Tab-Scoped Settings

Tab-local directory:

:tcd ~/project       " Change directory for current tab only
:tcd -               " Return to previous directory
:pwd                 " Show current directory

Compare:

  • :cd - Changes global directory

  • :lcd - Changes window-local directory

  • :tcd - Changes tab-local directory

Tab-scoped variables:

:let t:project_name = 'MyProject'
:echo t:project_name

9.4.4 Tab Layouts

Multiple windows per tab:

:tabnew              " New tab
:vsplit file1.txt    " Vertical split in this tab
:split file2.txt     " Horizontal split

Copy window to new tab:

<C-w>T               " Move current window to new tab

Neovim - Tab mappings:

vim.keymap.set('n', '<leader>tn', ':tabnew<CR>', { desc = 'New tab' })
vim.keymap.set('n', '<leader>tc', ':tabclose<CR>', { desc = 'Close tab' })
vim.keymap.set('n', '<leader>to', ':tabonly<CR>', { desc = 'Only tab' })
vim.keymap.set('n', '<A-1>', '1gt', { desc = 'Tab 1' })
vim.keymap.set('n', '<A-2>', '2gt', { desc = 'Tab 2' })
vim.keymap.set('n', '<A-3>', '3gt', { desc = 'Tab 3' })

9.5 Argument List

The argument list is a subset of buffers—files passed at startup or added explicitly.

9.5.1 Argument List Basics

View argument list:

:args                " Show argument list
:args file1 file2    " Set argument list
:args *.txt          " Set to all .txt files
:args **/*.lua       " Set to all .lua files recursively

Navigate arguments:

:next                " Next argument
:n                   " Same
:previous            " Previous argument
:prev                " Same
:first               " First argument
:last                " Last argument
:argument 3          " Go to argument 3

From command line:

vim file1.txt file2.txt file3.txt
# Inside Vim: :args shows these three files

9.5.2 Adding and Removing Arguments

Add to argument list:

:argadd file.txt     " Add file
:argadd *.py         " Add all .py files

Remove from argument list:

:argdelete file.txt  " Remove file
:argdelete *         " Remove all

9.5.3 Operations on Argument List

Execute command on all arguments:

:argdo %s/old/new/ge " Substitute in all args
:argdo w             " Save all args
:argdo normal @q     " Execute macro q in all args

Useful workflow - Refactor across files:

:args **/*.py        " Select all Python files
:argdo %s/old_func/new_func/ge | update

The | update saves only if modified.

9.5.4 Local Argument List

Each tab can have its own argument list.

:arglocal *.txt      " Set local argument list for current tab

9.6 Sessions

Sessions save the entire Vim workspace: open files, window layouts, tabs, options, and more.

9.6.1 Creating Sessions

Save session:

:mksession session.vim        " Save to session.vim
:mksession! session.vim       " Overwrite existing
:mks ~/sessions/project.vim   " Save to specific path

What’s saved:

  • Open buffers and windows

  • Window sizes and positions

  • Tab pages

  • Current directory

  • Options and mappings

  • Marks and registers (optionally)

Restore session:

:source session.vim           " Load session

From command line:

vim -S session.vim
nvim -S session.vim

9.6.2 Session Options

Control what’s saved:

:set sessionoptions?          " View current settings

Common options:

:set sessionoptions=blank,buffers,curdir,folds,help,tabpages,winsize

Neovim (Lua):

vim.opt.sessionoptions = {
  'buffers',    -- All buffers
  'curdir',     -- Current directory
  'tabpages',   -- Tab pages
  'winsize',    -- Window sizes
  'help',       -- Help windows
  'globals',    -- Global variables
  'skiprtp',    -- Exclude runtime path
}

Minimal session (current tab only):

vim.opt.sessionoptions = { 'curdir', 'tabpages', 'winsize' }

9.6.3 Auto-Session Management

Manual save/restore workflow:


-- Quick session save/load
vim.keymap.set('n', '<leader>ss', ':mksession! .session.vim<CR>', 
  { desc = 'Save session' })
vim.keymap.set('n', '<leader>sl', ':source .session.vim<CR>', 
  { desc = 'Load session' })

Using persistence.nvim (plugin):


-- Install folke/persistence.nvim
require('persistence').setup({
  dir = vim.fn.expand(vim.fn.stdpath('state') .. '/sessions/'),
  options = { 'buffers', 'curdir', 'tabpages', 'winsize' }
})


-- Restore last session for current directory
vim.keymap.set('n', '<leader>qs', [[<cmd>lua require('persistence').load()<cr>]], 
  { desc = 'Restore session' })


-- Restore last session
vim.keymap.set('n', '<leader>ql', [[<cmd>lua require('persistence').load({ last = true })<cr>]], 
  { desc = 'Restore last session' })


-- Stop session recording
vim.keymap.set('n', '<leader>qd', [[<cmd>lua require('persistence').stop()<cr>]], 
  { desc = 'Stop session' })

Auto-save on exit:

vim.api.nvim_create_autocmd('VimLeavePre', {
  callback = function()
    vim.cmd('mksession! .session.vim')
  end
})

9.6.4 Project-Specific Sessions

Directory-based sessions:


-- Save session named after current directory
vim.keymap.set('n', '<leader>ss', function()
  local session_dir = vim.fn.stdpath('data') .. '/sessions'
  vim.fn.mkdir(session_dir, 'p')
  
  local cwd = vim.fn.getcwd()
  local session_name = cwd:gsub('/', '_'):gsub('^_', '')
  local session_file = session_dir .. '/' .. session_name .. '.vim'
  
  vim.cmd('mksession! ' .. session_file)
  print('Session saved: ' .. session_file)
end, { desc = 'Save project session' })


-- Load session for current directory
vim.keymap.set('n', '<leader>sl', function()
  local session_dir = vim.fn.stdpath('data') .. '/sessions'
  local cwd = vim.fn.getcwd()
  local session_name = cwd:gsub('/', '_'):gsub('^_', '')
  local session_file = session_dir .. '/' .. session_name .. '.vim'
  
  if vim.fn.filereadable(session_file) == 1 then
    vim.cmd('source ' .. session_file)
    print('Session loaded: ' .. session_file)
  else
    print('No session found for this directory')
  end
end, { desc = 'Load project session' })

9.7 Views

Views save window-specific settings: cursor position, folds, options. Unlike sessions (which save the entire workspace), views save individual window states.

9.7.1 Creating and Loading Views

Save view:

:mkview              " Save view (index 1 by default)
:mkview 2            " Save to slot 2 (up to 10 slots)
:mkview ~/.vim/views/myview.vim  " Save to file

Load view:

:loadview            " Load view (index 1)
:loadview 2          " Load from slot 2
:source ~/.vim/views/myview.vim  " Load from file

What’s saved in views:

  • Cursor position

  • Folds

  • Scroll position

  • Local options

  • Local mappings

9.7.2 Auto-Save Views

Save/restore views automatically:


-- Auto-save view on buffer leave
vim.api.nvim_create_autocmd('BufWinLeave', {
  pattern = '*',
  callback = function()
    if vim.bo.buftype == '' then  -- Only for regular files
      vim.cmd('silent! mkview')
    end
  end
})


-- Auto-load view on buffer enter
vim.api.nvim_create_autocmd('BufWinEnter', {
  pattern = '*',
  callback = function()
    if vim.bo.buftype == '' then
      vim.cmd('silent! loadview')
    end
  end
})

View options:

:set viewoptions?    " View what's saved
vim.opt.viewoptions = { 'cursor', 'folds', 'curdir' }

9.8 File Browsing

9.8.1 Netrw (Built-in File Browser)

Netrw is Vim’s built-in file explorer.

Open Netrw:

:Explore             " Open in current window
:Sexplore            " Open in horizontal split
:Vexplore            " Open in vertical split
:Texplore            " Open in new tab
:e .                 " Browse current directory
:e ..                " Browse parent directory

Navigate in Netrw:

<Enter>              " Open file/directory

-                    " Go to parent directory
i                    " Cycle through view modes
s                    " Cycle sort order
r                    " Reverse sort

Netrw operations:

d                    " Create directory
%                    " Create file
D                    " Delete file/directory
R                    " Rename file

Configure Netrw:

vim.g.netrw_banner = 0        -- Disable banner
vim.g.netrw_liststyle = 3     -- Tree view
vim.g.netrw_winsize = 25      -- Window size (percentage)
vim.g.netrw_browse_split = 4  -- Open in previous window

9.8.2 Modern File Explorers

nvim-tree.lua (popular):


-- Install nvim-tree/nvim-tree.lua
require('nvim-tree').setup({
  sort_by = 'case_sensitive',
  view = {
    width = 30,
  },
  filters = {
    dotfiles = false,
  },
})


-- Toggle tree
vim.keymap.set('n', '<leader>e', ':NvimTreeToggle<CR>', 
  { desc = 'Toggle file explorer' })

neo-tree.nvim (alternative):


-- Install nvim-neo-tree/neo-tree.nvim
require('neo-tree').setup({
  window = {
    width = 30,
  },
  filesystem = {
    follow_current_file = true,
  },
})

vim.keymap.set('n', '<leader>e', ':Neotree toggle<CR>', 
  { desc = 'Toggle Neo-tree' })

9.8.3 Fuzzy Finding (Telescope)

Telescope.nvim (essential plugin):


-- Install nvim-telescope/telescope.nvim
local builtin = require('telescope.builtin')

vim.keymap.set('n', '<leader>ff', builtin.find_files, { desc = 'Find files' })
vim.keymap.set('n', '<leader>fg', builtin.live_grep, { desc = 'Live grep' })
vim.keymap.set('n', '<leader>fb', builtin.buffers, { desc = 'Find buffers' })
vim.keymap.set('n', '<leader>fh', builtin.help_tags, { desc = 'Help tags' })
vim.keymap.set('n', '<leader>fr', builtin.oldfiles, { desc = 'Recent files' })
vim.keymap.set('n', '<leader>fw', builtin.grep_string, { desc = 'Find word' })

Advanced Telescope usage:


-- Find files including hidden
vim.keymap.set('n', '<leader>fa', function()
  builtin.find_files({ hidden = true, no_ignore = true })
end, { desc = 'Find all files' })


-- Grep in specific directory
vim.keymap.set('n', '<leader>fd', function()
  builtin.live_grep({ cwd = vim.fn.input('Directory: ') })
end, { desc = 'Grep in directory' })


-- Find in current buffer
vim.keymap.set('n', '<leader>/', builtin.current_buffer_fuzzy_find, 
  { desc = 'Fuzzy find in buffer' })

9.9 File Path Manipulation

9.9.1 Path Expansion

Vim provides modifiers to extract path components:

:echo expand('%')      " Relative path: src/main.lua
:echo expand('%:p')    " Absolute path: /home/user/project/src/main.lua
:echo expand('%:t')    " Filename: main.lua
:echo expand('%:r')    " Without extension: src/main
:echo expand('%:e')    " Extension: lua
:echo expand('%:h')    " Directory: src
:echo expand('%:p:h')  " Absolute directory: /home/user/project/src

Multiple modifiers:

:echo expand('%:p:h:h')  " Parent of absolute directory
:echo expand('%:t:r')    " Filename without extension

Alternate buffer path:

:echo expand('#')        " Alternate buffer path
:echo expand('#:p')      " Alternate absolute path

9.9.2 Special File Names

%                        " Current file
#                        " Alternate file
#3                       " Buffer 3
<cfile>                  " File under cursor
<cword>                  " Word under cursor
<sfile>                  " Sourced file (in scripts)

Using in commands:

:!cat %                  " Show current file content
:e %:h/new_file.txt      " Open file in current directory

9.9.3 Working Directory

Change directory:

:cd /path/to/dir         " Global change
:lcd /path/to/dir        " Window-local
:tcd /path/to/dir        " Tab-local
:pwd                     " Show current directory

Navigate directories:

:cd -                    " Return to previous directory
:cd %:p:h                " Change to current file's directory
:cd ..                   " Parent directory

Neovim - Auto-cd to file directory:

vim.opt.autochdir = true  -- Auto change to file's directory


-- Or do it manually per file:
vim.keymap.set('n', '<leader>cd', ':cd %:p:h<CR>:pwd<CR>', 
  { desc = 'CD to file directory' })

9.10 File Operations Workflows

Workflow 1: Multi-File Editing

Open related files:

:e src/main.lua
:vs src/config.lua
:sp test/main_test.lua

Or use argument list:

:args src/*.lua
:sall                    " Open all in splits
:vertical sall           " Open all in vertical splits

Workflow 2: Search and Replace Across Files

:args **/*.py            " Select all Python files
:argdo %s/old/new/ge | update  " Replace and save

Using Telescope + Quickfix:


-- 1. Grep for pattern (Telescope)

-- 2. Send results to quickfix (<C-q>)

-- 3. Run substitution on quickfix
vim.keymap.set('n', '<leader>qr', function()
  vim.ui.input({ prompt = 'Replace with: ' }, function(replacement)
    if replacement then
      vim.cmd('cfdo %s/' .. vim.fn.getreg('/') .. '/' .. replacement .. '/ge | update')
    end
  end)
end, { desc = 'Replace in quickfix' })

Workflow 3: Project Session Management


-- Save session when leaving project directory
vim.api.nvim_create_autocmd('VimLeavePre', {
  callback = function()

    -- Only save if in a git repo
    if vim.fn.isdirectory('.git') == 1 then
      vim.cmd('mksession! .session.vim')
    end
  end
})


-- Auto-load session in project directory
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    if vim.fn.argc() == 0 and vim.fn.filereadable('.session.vim') == 1 then
      vim.defer_fn(function()
        vim.cmd('source .session.vim')
      end, 10)
    end
  end
})

Workflow 4: File Templates


-- Auto-insert template for new files
vim.api.nvim_create_autocmd('BufNewFile', {
  pattern = '*.py',
  callback = function()
    local template = vim.fn.expand('~/.config/nvim/templates/python.txt')
    if vim.fn.filereadable(template) == 1 then
      vim.cmd('0r ' .. template)

      -- Replace placeholders
      vim.cmd('%s/{{FILENAME}}/' .. vim.fn.expand('%:t:r') .. '/ge')
      vim.cmd('%s/{{DATE}}/' .. os.date('%Y-%m-%d') .. '/ge')
    end
  end
})

Workflow 5: Backup and Undo Files

Enable persistent undo:

vim.opt.undofile = true
vim.opt.undodir = vim.fn.stdpath('data') .. '/undo'

Backup files:

vim.opt.backup = true
vim.opt.backupdir = vim.fn.stdpath('data') .. '/backup'
vim.fn.mkdir(vim.opt.backupdir:get()[1], 'p')

Swap files (disable for modern workflows):

vim.opt.swapfile = false  -- Rely on version control instead

Chapter Summary

This chapter covered Vim’s comprehensive file operation system:

Basic Operations:

  • :e, :w, :r for opening, saving, reading files

  • :w !sudo tee % for elevated saves

  • File info with :f, <C-g>, g<C-g>

  • Path expansion: %:p, %:h, %:t, %:r, %:e

Buffer Management:

  • :ls, :bn, :bp, <C-^> for navigation

  • :bd, :bw, :bunload for deletion

  • :bufdo for batch operations

  • set hidden for unsaved buffer switching

  • Modern plugins: bufferline.nvim

Windows and Splits:

  • :split, :vsplit, <C-w>s/v for creation

  • <C-w>hjkl for navigation

  • :resize, <C-w>= for sizing

  • <C-w>HJKL for repositioning

  • Floating windows in Neovim

Tab Pages:

  • :tabnew, :tabe for creation

  • gt, gT, 1gt for navigation

  • :tcd for tab-local directories

  • :tabdo for batch operations

Argument List:

  • :args **/*.lua for file selection

  • :next, :prev for navigation

  • :argdo for batch processing

  • Multi-file refactoring workflows

Sessions:

  • :mksession to save workspace

  • vim -S session.vim to restore

  • sessionoptions to control saved state

  • persistence.nvim for auto-session management

  • Directory-specific session patterns

Views:

  • :mkview, :loadview for window state

  • Auto-save/restore with autocmds

  • Cursor, folds, scroll preservation

File Browsing:

  • Netrw built-in browser

  • nvim-tree.lua, neo-tree.nvim file explorers

  • Telescope fuzzy finding

  • :find, :e . for quick access

Working Directory:

  • :cd, :lcd, :tcd for scope control

  • :cd %:p:h to follow file

  • autochdir option

Advanced Workflows:

  • Multi-file search and replace

  • Template insertion

  • Session management automation

  • Backup and undo persistence

Best Practices:

  • Enable hidden for flexible buffer switching

  • Use sessions for project restoration

  • Leverage Telescope for file discovery

  • Automate repetitive file operations

  • Organize sessions by project

  • Use LSP for file operations when available

  • Map frequent operations to leader keys

  • Disable swap files in favor of version control

Key Insights:

  • Buffers are in-memory, files are on-disk

  • Windows are viewports, tabs are layouts

  • Argument list is a curated subset of buffers

  • Sessions preserve entire workspace state

  • Views preserve window-specific state

  • Modern plugins enhance traditional workflows

  • Automation reduces manual file management

  • Path modifiers enable dynamic file operations

Master these file operations to efficiently manage complex projects spanning dozens or hundreds of files. Vim’s file system, combined with modern plugins, provides a powerful foundation for navigating and organizing large codebases.

In the next chapter, we’ll explore Visual Mode and Selection, covering character-wise, line-wise, and block-wise selection, visual operators, and advanced selection techniques that enable precise text manipulation.


Chapter 10: The Quickfix and Location Lists

The quickfix and location lists are among Vim’s most powerful yet underutilized features. Originally designed for compiler error navigation, these lists have evolved into versatile tools for managing any collection of file positions: search results, linter warnings, TODO markers, git conflicts, and more. This chapter explores how to harness these lists to dramatically accelerate multi-file workflows.

10.1 Understanding Quickfix and Location Lists

10.1.1 Conceptual Foundation

Quickfix list:

  • Global to the entire Vim instance

  • Shared across all windows and tabs

  • Typically used for compiler errors, grep results

  • One active quickfix list at a time (with history stack)

Location list:

  • Local to each window

  • Each window can have its own location list

  • Used for window-specific results (LSP diagnostics, local searches)

  • Multiple location lists can coexist simultaneously

Common structure: Both lists store entries with:

  • filename - File path

  • lnum - Line number

  • col - Column number

  • text - Description text

  • type - Entry type (E=error, W=warning, I=info)

  • valid - Whether entry is valid

10.1.2 When to Use Which

Use Quickfix when:

  • Compiling code and reviewing errors

  • Performing project-wide searches

  • Working with test results

  • Managing TODO lists across files

  • Results need to persist across window switches

Use Location list when:

  • Viewing LSP diagnostics for specific file

  • Per-window search results

  • Isolating results to specific context

  • Multiple developers/workflows need separate lists

10.2 Basic Quickfix Operations

10.2.1 Populating the Quickfix List

From compiler output:

:make                " Run make and populate quickfix
:make test           " Run 'make test'
:make!               " Run without jumping to first error

Configure make program:

:set makeprg=gcc\ %  " Set compiler
:set makeprg=pytest  " Set to pytest
:set makeprg=npm\ test

Neovim (Lua):

vim.opt.makeprg = 'cargo build'  -- Rust
vim.opt.makeprg = 'go build'     -- Go

From grep:

:grep pattern files          " Use external grep
:grep -r pattern .           " Recursive grep
:grep! pattern files         " Don't jump to first match
:lgrep pattern files         " Populate location list instead

Configure grep program:

:set grepprg=rg\ --vimgrep   " Use ripgrep
:set grepprg=ag\ --vimgrep   " Use silver searcher

Neovim (Lua):

vim.opt.grepprg = 'rg --vimgrep --smart-case'
vim.opt.grepformat = '%f:%l:%c:%m'

Internal vimgrep:

:vimgrep /pattern/ **/*.lua  " Search all Lua files
:vimgrep /TODO/ src/**/*     " Find all TODOs
:vimgrep /error/ %           " Search current file
:lvimgrep /pattern/ files    " Populate location list

Pattern modifiers:

:vimgrep /pattern/g **/*.py  " All matches per line (g flag)
:vimgrep /pattern/j **/*.py  " Don't jump to first (j flag)

Basic navigation:

:cnext               " Next entry
:cn                  " Short form
:cprevious           " Previous entry
:cp                  " Short form
:cfirst              " First entry
:clast               " Last entry
:cc 5                " Jump to entry 5
:cc                  " Jump to current entry (useful after :copen)

With count:

:5cnext              " Jump 5 entries forward
:3cprevious          " Jump 3 entries back
5]q                  " Jump 5 forward (if mapped)

Wrap around:

:set nowrapscan      " Prevent wrap (stays at last)
:set wrapscan        " Allow wrap to first (default)

Neovim - Quickfix navigation mappings:

vim.keymap.set('n', ']q', ':cnext<CR>', { desc = 'Next quickfix' })
vim.keymap.set('n', '[q', ':cprevious<CR>', { desc = 'Previous quickfix' })
vim.keymap.set('n', ']Q', ':clast<CR>', { desc = 'Last quickfix' })
vim.keymap.set('n', '[Q', ':cfirst<CR>', { desc = 'First quickfix' })

10.2.3 Viewing the Quickfix List

Open quickfix window:

:copen               " Open quickfix window
:copen 15            " Open with 15 lines height
:cwindow             " Open only if entries exist
:cclose              " Close quickfix window

Quickfix window features:

  • Press <Enter> on entry to jump to location

  • Window updates as you navigate with :cnext/:cprevious

  • Use dd to remove entries (in modifiable mode)

  • Close with :q or :cclose

Toggle quickfix:

vim.keymap.set('n', '<leader>q', function()
  for _, win in pairs(vim.fn.getwininfo()) do
    if win.quickfix == 1 then
      vim.cmd('cclose')
      return
    end
  end
  vim.cmd('copen')
end, { desc = 'Toggle quickfix' })

10.2.4 Quickfix History

Vim maintains a stack of quickfix lists.

Navigate history:

:colder              " Go to older quickfix list
:cnewer              " Go to newer quickfix list
:colder 3            " Go 3 lists back

Check history:

:chistory            " Show quickfix history

Output example: error list 1 of 3; 45 errors > error list 2 of 3; 12 errors error list 3 of 3; 8 errors

Neovim - History navigation:

vim.keymap.set('n', '<leader>co', ':colder<CR>', { desc = 'Older quickfix' })
vim.keymap.set('n', '<leader>ci', ':cnewer<CR>', { desc = 'Newer quickfix' })

10.3 Basic Location List Operations

Location lists work identically to quickfix, but with l prefix commands.

10.3.1 Location List Commands

Navigation:

:lnext               " Next entry
:ln                  
:lprevious           " Previous entry
:lp                  
:lfirst              " First entry
:llast               " Last entry
:ll 5                " Jump to entry 5

Window:

:lopen               " Open location window
:lopen 10            " Open with 10 lines
:lwindow             " Open if entries exist
:lclose              " Close location window

History:

:lolder              " Older location list
:lnewer              " Newer location list
:lhistory            " Show history

Neovim - Location list mappings:

vim.keymap.set('n', ']l', ':lnext<CR>', { desc = 'Next location' })
vim.keymap.set('n', '[l', ':lprevious<CR>', { desc = 'Previous location' })
vim.keymap.set('n', '<leader>l', ':lopen<CR>', { desc = 'Open location list' })

10.3.2 Location List Per Window

Each window has its own location list:

:vsplit              " Create vertical split
:lvimgrep /TODO/ **/*.lua  " Populate left window's location list
<C-w>l               " Move to right window
:lvimgrep /FIXME/ **/*.lua " Populate right window's location list

Now each window has its own list, navigable independently.

10.4 Advanced Quickfix Population

10.4.1 Using :cexpr and :lexpr

Manually populate lists from strings:

:cexpr "file.txt:10:5:Error message"
:cexpr ["file1.txt:10:Error 1", "file2.txt:20:Error 2"]

Neovim (Lua) - Build custom quickfix:

local function populate_quickfix()
  local entries = {
    { filename = 'file1.lua', lnum = 10, col = 5, text = 'TODO: Fix this' },
    { filename = 'file2.lua', lnum = 25, col = 1, text = 'FIXME: Refactor' },
  }
  vim.fn.setqflist(entries)
  vim.cmd('copen')
end

vim.keymap.set('n', '<leader>qt', populate_quickfix, 
  { desc = 'Populate TODO quickfix' })

10.4.2 Using :cfile and :lfile

Load from file:

:cfile errors.txt    " Load quickfix from file
:lfile warnings.txt  " Load location list from file

File format: src/main.lua:42:5: error: undefined variable src/utils.lua:10:1: warning: unused import test/spec.lua:100:12: error: assertion failed

10.4.3 Using :cgetexpr and :lgetexpr

Add to existing list without jumping:

:cgetexpr "file.txt:50:New error"  " Add without jumping
:caddexpr "file.txt:60:Another"    " Append to list

Neovim - Add LSP diagnostics to quickfix:

vim.keymap.set('n', '<leader>qd', function()
  vim.diagnostic.setqflist({ severity = vim.diagnostic.severity.ERROR })
  vim.cmd('copen')
end, { desc = 'Errors to quickfix' })

10.4.4 Filter and Transform

Get current quickfix list:

:let qflist = getqflist()
:echo qflist[0].text

Neovim (Lua) - Filter quickfix:


-- Keep only errors (remove warnings)
vim.keymap.set('n', '<leader>qe', function()
  local qflist = vim.fn.getqflist()
  local errors = vim.tbl_filter(function(item)
    return item.type == 'E' or item.type == ''
  end, qflist)
  vim.fn.setqflist(errors)
end, { desc = 'Filter quickfix to errors' })

Filter by filename pattern:

vim.keymap.set('n', '<leader>qf', function()
  vim.ui.input({ prompt = 'Filter pattern: ' }, function(pattern)
    if not pattern then return end
    local qflist = vim.fn.getqflist()
    local filtered = vim.tbl_filter(function(item)
      local bufname = vim.fn.bufname(item.bufnr)
      return bufname:match(pattern)
    end, qflist)
    vim.fn.setqflist(filtered)
    print('Filtered to ' .. #filtered .. ' entries')
  end)
end, { desc = 'Filter quickfix by pattern' })

10.5 Operating on Quickfix Entries

10.5.1 Batch Operations with :cdo and :cfdo

:cdo - Execute on each quickfix entry:

:cdo s/old/new/g     " Substitute in each entry
:cdo normal @q       " Execute macro q on each entry
:cdo delete          " Delete line at each entry

:cfdo - Execute on each file in quickfix:

:cfdo %s/old/new/ge  " Substitute in each file
:cfdo w              " Save each file

Combine for efficient refactoring:

:vimgrep /oldFunc/ **/*.lua   " Find all occurrences
:cfdo %s/oldFunc/newFunc/ge | update  " Replace and save

The e flag suppresses errors, update saves only if modified.

Neovim - Quickfix refactor workflow:

vim.keymap.set('n', '<leader>qr', function()

  -- Assume quickfix is already populated
  vim.ui.input({ prompt = 'Replace with: ' }, function(replacement)
    if not replacement then return end
    local search = vim.fn.getreg('/')  -- Get last search
    vim.cmd('cfdo %s/' .. search .. '/' .. replacement .. '/ge | update')
  end)
end, { desc = 'Replace in quickfix files' })

10.5.2 Location List Equivalents

:ldo s/old/new/g     " Execute on each location entry
:lfdo %s/old/new/ge  " Execute on each file in location list

10.5.3 Quickfix Macros

Record macro, execute across quickfix:

Workflow:

  1. Navigate to first quickfix entry: :cc

  2. Record macro: qa (record to register a)

  3. Perform edits

  4. Save and move to next: :w | cnext

  5. Stop recording: q

  6. Execute on remaining entries: :5@a or :999@a

Or use :cdo:

:cdo normal @a       " Execute macro a on all entries

10.6 Advanced Workflows

10.6.1 Project-Wide TODO Management

Find all TODOs:

:vimgrep /TODO\|FIXME\|HACK/ **/*.lua
:copen

Neovim - TODO finder:

vim.keymap.set('n', '<leader>ft', function()
  vim.cmd('vimgrep /TODO\\|FIXME\\|HACK/ **/*')
  vim.cmd('copen')
end, { desc = 'Find TODOs' })

Filter TODOs by priority:

vim.keymap.set('n', '<leader>fT', function()
  vim.cmd('vimgrep /TODO(\\d)/ **/*')  -- Matches TODO(1), TODO(2), etc.
  local qflist = vim.fn.getqflist()
  table.sort(qflist, function(a, b)
    local a_priority = a.text:match('TODO%((%d)%)')
    local b_priority = b.text:match('TODO%((%d)%)')
    return (a_priority or 9) < (b_priority or 9)
  end)
  vim.fn.setqflist(qflist)
  vim.cmd('copen')
end, { desc = 'Find prioritized TODOs' })

10.6.2 Test Failure Navigation

Pytest integration:

:set makeprg=pytest\ --tb=short
:make
:copen

Neovim - Run tests and populate quickfix:

vim.keymap.set('n', '<leader>tt', function()
  vim.cmd('make')
  vim.cmd('copen')
end, { desc = 'Run tests' })


-- Run specific test file
vim.keymap.set('n', '<leader>tf', function()
  vim.cmd('make %')  -- % = current file
  vim.cmd('copen')
end, { desc = 'Test current file' })

10.6.3 Git Conflict Resolution

Find merge conflicts:

:vimgrep /^<<<<<<<\|^=======\|^>>>>>>>/ **/*
:copen

Neovim - Conflict finder:

vim.keymap.set('n', '<leader>gc', function()
  vim.cmd('vimgrep /^<<<<<<<\\|^=======\\|^>>>>>>>/ **/*')
  vim.cmd('copen')
end, { desc = 'Find git conflicts' })

Navigate and resolve:

  1. :cnext to go to next conflict

  2. Resolve using visual mode or dd

  3. :w to save

  4. Repeat

10.6.4 Multi-File Search and Replace

Complete workflow:

" 1. Search across project
:vimgrep /oldFunction/ **/*.js

" 2. Review matches
:copen

" 3. Confirm pattern is correct
:cnext  " Check a few matches

" 4. Replace in all files
:cfdo %s/oldFunction/newFunction/ge | update

" 5. Verify changes
:cnext  " Check replacements

Neovim - Interactive replace:

vim.keymap.set('n', '<leader>sr', function()
  vim.ui.input({ prompt = 'Search: ' }, function(search)
    if not search then return end
    vim.ui.input({ prompt = 'Replace: ' }, function(replace)
      if not replace then return end
      

      -- Search
      vim.cmd('vimgrep /' .. search .. '/ **/*')
      vim.cmd('copen')
      

      -- Confirm
      vim.ui.input({ 
        prompt = 'Found ' .. #vim.fn.getqflist() .. ' matches. Replace all? (y/n): '
      }, function(confirm)
        if confirm == 'y' then
          vim.cmd('cfdo %s/' .. search .. '/' .. replace .. '/ge | update')
          print('Replaced in ' .. #vim.fn.getqflist() .. ' files')
        end
      end)
    end)
  end)
end, { desc = 'Search and replace' })

10.6.5 LSP Diagnostics Integration

Neovim - Populate quickfix with diagnostics:


-- All diagnostics
vim.keymap.set('n', '<leader>da', function()
  vim.diagnostic.setqflist()
  vim.cmd('copen')
end, { desc = 'All diagnostics to quickfix' })


-- Only errors
vim.keymap.set('n', '<leader>de', function()
  vim.diagnostic.setqflist({ severity = vim.diagnostic.severity.ERROR })
  vim.cmd('copen')
end, { desc = 'Errors to quickfix' })


-- Only warnings
vim.keymap.set('n', '<leader>dw', function()
  vim.diagnostic.setqflist({ severity = vim.diagnostic.severity.WARN })
  vim.cmd('copen')
end, { desc = 'Warnings to quickfix' })


-- Current buffer diagnostics to location list
vim.keymap.set('n', '<leader>dl', function()
  vim.diagnostic.setloclist()
  vim.cmd('lopen')
end, { desc = 'Buffer diagnostics to location list' })

10.7 Customizing Quickfix Display

10.7.1 Error Format

The errorformat option controls how Vim parses compiler/grep output.

Default format:

:set errorformat?

Common patterns:

" GCC/Clang format: file.c:42:5: error: message
set errorformat=%f:%l:%c:\ %t%*[^:]:\ %m

" Python traceback
set errorformat=%C\ %.%#,%A\ \ File\ \"%f\"\\,\ line\ %l%.%#,%Z%[%^\ ]%\\@=%m

" JavaScript/Node.js
set errorformat=%f:%l:%c\ -\ %trror:\ %m,%f:%l:%c\ -\ %tarning:\ %m

Neovim (Lua):

vim.opt.errorformat = {
  '%f:%l:%c: %t%*[^:]: %m',  -- Standard format
  '%f:%l: %t%*[^:]: %m',     -- Without column
  '%f(%l): %t%*[^:]: %m',    -- Windows format
}

10.7.2 Quickfix Highlighting

Neovim - Customize quickfix colors:

vim.api.nvim_set_hl(0, 'QuickFixLine', { bg = '#3a3a3a', fg = '#e0e0e0' })
vim.api.nvim_set_hl(0, 'qfFileName', { fg = '#61afef' })
vim.api.nvim_set_hl(0, 'qfLineNr', { fg = '#98c379' })

10.7.3 Custom Quickfix Window

Neovim - Enhanced quickfix:

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'qf',
  callback = function()

    -- Buffer-local mappings
    vim.keymap.set('n', '<CR>', '<CR>:cclose<CR>', { buffer = true })
    vim.keymap.set('n', 'dd', function()
      local line = vim.fn.line('.')
      local qflist = vim.fn.getqflist()
      table.remove(qflist, line)
      vim.fn.setqflist(qflist)
    end, { buffer = true })
    

    -- Set window height
    vim.cmd('resize ' .. math.min(#vim.fn.getqflist(), 10))
  end
})

10.8 Quickfix Plugins

10.8.1 trouble.nvim

A modern diagnostics/quickfix UI:


-- Install folke/trouble.nvim
require('trouble').setup({
  position = 'bottom',
  height = 10,
  icons = true,
  mode = 'workspace_diagnostics',
})

vim.keymap.set('n', '<leader>xx', ':TroubleToggle<CR>', 
  { desc = 'Toggle Trouble' })
vim.keymap.set('n', '<leader>xw', ':TroubleToggle workspace_diagnostics<CR>', 
  { desc = 'Workspace diagnostics' })
vim.keymap.set('n', '<leader>xd', ':TroubleToggle document_diagnostics<CR>', 
  { desc = 'Document diagnostics' })
vim.keymap.set('n', '<leader>xq', ':TroubleToggle quickfix<CR>', 
  { desc = 'Quickfix in Trouble' })

10.8.2 nvim-bqf

Better quickfix window:


-- Install kevinhwang91/nvim-bqf
require('bqf').setup({
  preview = {
    auto_preview = true,
    win_height = 12,
  },
  func_map = {
    vsplit = '<C-v>',
    ptogglemode = 'z,',
    stoggleup = '<',
  },
})

Features:

  • Fuzzy search in quickfix

  • Preview entries

  • Multi-select and batch operations

10.9 Practical Recipes

Recipe 1: Code Review Workflow


-- Mark lines for review
vim.keymap.set('n', '<leader>mr', function()
  local file = vim.fn.expand('%')
  local line = vim.fn.line('.')
  local text = vim.fn.input('Review note: ')
  

  -- Add to quickfix
  local qflist = vim.fn.getqflist()
  table.insert(qflist, {
    filename = file,
    lnum = line,
    text = 'REVIEW: ' .. text,
  })
  vim.fn.setqflist(qflist)
  print('Added review mark')
end, { desc = 'Mark for review' })


-- Review all marks
vim.keymap.set('n', '<leader>vr', function()
  local qflist = vim.fn.getqflist()
  local reviews = vim.tbl_filter(function(item)
    return item.text:match('^REVIEW:')
  end, qflist)
  vim.fn.setqflist(reviews)
  vim.cmd('copen')
end, { desc = 'View review marks' })

Recipe 2: Find Function Definitions

:vimgrep /\<def\s\+\w\+/ **/*.py  " Python functions
:vimgrep /\<function\s\+\w\+/ **/*.js  " JavaScript functions
:vimgrep /\<func\s\+\w\+/ **/*.go  " Go functions

Recipe 3: Dead Code Detection


-- Find unused imports (simple heuristic)
vim.keymap.set('n', '<leader>fu', function()

  -- Find all imports
  vim.cmd('vimgrep /^import\\s\\+/ **/*.py')
  local imports = vim.fn.getqflist()
  

  -- Check usage (simplified)
  local unused = {}
  for _, item in ipairs(imports) do
    local module = item.text:match('import%s+(%S+)')
    if module then

      -- Search for usage
      local usage = vim.fn.system('grep -r "' .. module .. '" .')
      if usage == '' then
        table.insert(unused, item)
      end
    end
  end
  
  vim.fn.setqflist(unused)
  vim.cmd('copen')
  print('Found ' .. #unused .. ' potentially unused imports')
end, { desc = 'Find unused imports' })

Recipe 4: Documentation Coverage


-- Find functions without docstrings
vim.keymap.set('n', '<leader>fd', function()
  vim.cmd('vimgrep /def\\s\\+\\w\\+.*:\\n\\s*"""/ **/*.py')
  local documented = vim.fn.getqflist()
  
  vim.cmd('vimgrep /def\\s\\+\\w\\+/ **/*.py')
  local all_funcs = vim.fn.getqflist()
  
  print('Documentation coverage: ' .. 
        math.floor(#documented / #all_funcs * 100) .. '%')
end, { desc = 'Check documentation coverage' })

Recipe 5: Quickfix Stack Management


-- Save current quickfix to file
vim.keymap.set('n', '<leader>qs', function()
  local filename = vim.fn.input('Save quickfix to: ', 'quickfix.txt')
  vim.cmd('call writefile(map(getqflist(), "v:val.text"), "' .. filename .. '")')
  print('Saved quickfix to ' .. filename)
end, { desc = 'Save quickfix' })


-- Load quickfix from file
vim.keymap.set('n', '<leader>ql', function()
  local filename = vim.fn.input('Load quickfix from: ', 'quickfix.txt')
  if vim.fn.filereadable(filename) == 1 then
    vim.cmd('cfile ' .. filename)
    vim.cmd('copen')
  end
end, { desc = 'Load quickfix' })

Chapter Summary

This chapter explored Vim’s powerful quickfix and location list systems:

Core Concepts:

  • Quickfix: Global list for project-wide results

  • Location list: Window-local list for contextual results

  • Both support navigation, filtering, batch operations

Population Methods:

  • :make - Compiler integration

  • :grep / :vimgrep - Search results

  • :cexpr / :cfile - Custom population

  • LSP diagnostics integration

Navigation:

  • :cnext/:cprevious for quickfix

  • :lnext/:lprevious for location list

  • History with :colder/:cnewer

  • Window management with :copen/:cclose

Batch Operations:

  • :cdo - Execute per entry

  • :cfdo - Execute per file

  • Combine with :update for safe saves

  • Macro integration

Advanced Features:

  • Filter and transform with Lua

  • Custom error formats

  • Plugin enhancements (trouble.nvim, nvim-bqf)

  • Integration with LSP, testing, git workflows

Practical Workflows:

  • Project-wide refactoring

  • TODO management

  • Test failure navigation

  • Code review marking

  • Multi-file search/replace

Best Practices:

  • Use quickfix for global, location for local

  • Always review before :cfdo operations

  • Leverage e flag to suppress errors

  • Use update instead of w to avoid unnecessary writes

  • Map frequently used commands to leader keys

  • Integrate with LSP for modern workflows

  • Customize display for better visibility

Key Insights:

  • Quickfix transforms multi-file editing

  • Location lists enable parallel workflows

  • Batch operations enable efficient refactoring

  • Integration with external tools amplifies power

  • Modern plugins enhance traditional workflows

  • Systematic navigation reduces context switching

Mastering quickfix and location lists transforms Vim from a single-file editor into a project-wide navigation and refactoring powerhouse. Combined with LSP, grep tools, and plugins, these lists become the central nervous system for managing complex codebases.

In the next chapter, we’ll explore Marks and Jumps, covering local and global marks, jump lists, change lists, and navigation patterns that enable rapid movement across files and history.


Chapter 11: Advanced Search Features

Vim’s search capabilities extend far beyond basic pattern matching. This chapter explores sophisticated search techniques, from multi-line patterns and look-arounds to search automation and integration with external tools. Mastering these features transforms search from a simple lookup operation into a precision navigation and refactoring tool.

11.1 Advanced Pattern Matching

11.1.1 Very Magic Mode

Standard Vim regex requires escaping special characters. Very magic mode (\v) makes patterns more intuitive:

Standard vs. Very Magic:

" Standard - requires escaping
/\(foo\|bar\)\+

" Very magic - cleaner syntax
/\v(foo|bar)+

" Even more examples
/\v\d+\.\d+           " Match decimal numbers: 3.14
/\v<word>             " Word boundaries
/\v[A-Z][a-z]+        " CamelCase words

Common conversions: | Standard | Very Magic | Meaning | |———-|———–|———| | \+ | + | One or more | | \? | ? | Zero or one | | \{n,m} | {n,m} | Between n and m | | \| | | | Alternation (OR) | | \( ) | ( ) | Grouping | | \< \> | < > | Word boundaries |

Neovim - Set very magic as default:


-- Map search to automatically use very magic
vim.keymap.set('n', '/', '/\\v', { desc = 'Search (very magic)' })
vim.keymap.set('n', '?', '?\\v', { desc = 'Backward search (very magic)' })

11.1.2 Very Nomagic Mode

For literal searches where special characters should be treated as regular characters:

" Very nomagic - only \ and terminator are special
/\V^literal$text.here

" Search for actual regex pattern
/\V(foo|bar)+        " Finds literal "(foo|bar)+"

When to use:

  • Searching for code containing regex

  • Searching for literal special characters

  • When you want minimal interpretation

11.1.3 Multi-Line Search Patterns

Match across line breaks:

" Search for function definition spanning multiple lines
/\vdef\s+\w+\_s*\(\_.\{-}\):

" Components:
" \_s  - whitespace including newline
" \_.  - any character including newline
" \{-} - non-greedy match

Common multi-line patterns:

" Function call with multiline arguments
/\vfunctionName\(\_.\{-}\)

" HTML/XML tag with content
/<div\_.\{-}<\/div>

" Python/C block
/if\s\+.*:\_.\{-}^end

" Comment block
/\/\*\_.\{-}\*\/

Find paragraph with specific word:

/\v(\_^\_s*\n){2}@<=\_.\{-}keyword\_.\{-}(\_^\_s*\n){2}@=

11.1.4 Zero-Width Assertions

Positive Lookahead (\@=): Match position followed by pattern without consuming it.

" Find 'foo' followed by 'bar' (but don't include 'bar' in match)
/foo\(bar\)\@=

" Match numbers followed by % sign
/\d\+\(%\)\@=

Negative Lookahead (\@!): Match position NOT followed by pattern.

" Find 'foo' not followed by 'bar'
/foo\(bar\)\@!

" Find function calls without arguments
/\w\+\(()\)\@!

Positive Lookbehind (\@<=): Match position preceded by pattern.

" Find digits after '$' sign
/\(\$\)\@<=\d\+

" Find text after 'TODO: '
/\(TODO:\s\)\@<=.*

Negative Lookbehind (\@<!): Match position NOT preceded by pattern.

" Find numbers not preceded by '$'
/\(\$\)\@<!\d\+

" Find 'bar' not preceded by 'foo'
/\(foo\)\@<!bar

Very magic equivalents:

/\vfoo(bar)@=         " Positive lookahead
/\vfoo(bar)@!         " Negative lookahead
/\v(\$)@<=\d+         " Positive lookbehind
/\v(\$)@<!\d+         " Negative lookbehind

11.1.5 Word Boundary Searches

Exact word matching:

/\<word\>            " Match 'word' but not 'wordy' or 'sword'
/\v<word>            " Very magic version

" Case variations
/\<\cword\>          " Case insensitive word
/\<\Cword\>          " Case sensitive word (override ignorecase)

Start/end of word:

/\<foo               " Match 'foo' and 'foobar' but not 'barfoo'
/bar\>               " Match 'bar' and 'foobar' but not 'barfoo'

11.1.6 Character Classes

Predefined classes:

\d    " Digit [0-9]
\D    " Non-digit
\w    " Word character [0-9A-Za-z_]
\W    " Non-word character
\s    " Whitespace [ \t]
\S    " Non-whitespace
\h    " Head of word [A-Za-z_]
\a    " Alphabetic [A-Za-z]
\l    " Lowercase [a-z]
\u    " Uppercase [A-Z]

Custom classes:

/[aeiou]             " Any vowel
/[^aeiou]            " Any non-vowel (^ negates)
/[0-9a-fA-F]         " Hexadecimal digit
/[[:alpha:]]         " POSIX character class

POSIX classes:

[[:alnum:]]          " Alphanumeric
[[:alpha:]]          " Alphabetic
[[:blank:]]          " Space and tab
[[:digit:]]          " Digits
[[:lower:]]          " Lowercase
[[:upper:]]          " Uppercase
[[:punct:]]          " Punctuation
[[:space:]]          " Whitespace

11.2 Search Modifiers and Flags

11.2.1 Inline Modifiers

Case sensitivity:

/\cpattern           " Case insensitive (override settings)
/\Cpattern           " Case sensitive (override settings)
/\vword\c            " Can appear anywhere in pattern

Magic modes within search:

/\v pattern here     " Very magic mode
/\V literal text     " Very nomagic mode
/\m standard mode    " Standard magic (default)
/\M nomagic mode     " Nomagic mode

11.2.2 Search Offset

Move cursor after match:

" Move to end of match
/pattern/e

" Move to start of match (default)
/pattern/s

" Move N lines down after match
/pattern/+3

" Move N lines up after match
/pattern/-2

" Move to specific column
/pattern/e+2         " 2 chars after end
/pattern/s-1         " 1 char before start

Practical examples:

" Find function, cursor on opening brace
/\vdef\s+\w+.*:/e

" Find TODO, cursor on next line
/TODO:/+1

" Find closing brace, cursor on next statement
/^}/+1

11.2.3 Search Count

Show match position:

Neovim built-in (version 0.5+):

:set shortmess-=S    " Show search count in command line

Shows: [1/5] indicating “match 1 of 5 total”

Manual count:

:%s/pattern//gn      " Count matches without replacing

Neovim (Lua) - Enhanced search count:

vim.opt.shortmess:remove('S')  -- Show count


-- Custom status line with search info
vim.api.nvim_create_autocmd('CmdlineLeave', {
  pattern = '/',
  callback = function()
    vim.defer_fn(function()
      local ok, result = pcall(vim.fn.searchcount, {recompute = true})
      if ok and result.total > 0 then
        print(string.format('Match %d of %d', result.current, result.total))
      end
    end, 100)
  end
})

11.3 Advanced Search Commands

11.3.1 Search in Range

Line range search:

:10,20/pattern       " Search in lines 10-20
:'<,'>s/pat/rep/g    " Search in visual selection
:.,$/pattern         " Search from cursor to end
:?^function?+1,/^end/-1s/old/new/g  " Between markers

11.3.2 Global Command Integration

The :global command executes commands on matching lines:

Basic syntax:

:g/pattern/command   " Execute command on matching lines
:g!/pattern/command  " Execute on non-matching lines (inverse)
:v/pattern/command   " Same as :g! (mnemonic: inVerse)

Common patterns:

" Delete all lines containing pattern
:g/pattern/d

" Copy matching lines to register
:g/pattern/y A       " Append to register a

" Move matching lines to end
:g/pattern/m$

" Add comment to matching lines
:g/function/normal I# 

" Execute macro on matching lines
:g/pattern/normal @q

" Complex: delete line after match
:g/pattern/+1d

Multi-line global:

" Delete function blocks
:g/^function/,/^end/d

" Comment out if-blocks
:g/^if/,/^endif/s/^/# /

11.3.3 Search and Execute

Normal mode commands after search:

" Search and delete matching lines
:g/TODO/d

" Search and change case
:g/^const/normal gUU

" Search and append text
:g/import/normal A  # type: ignore

Neovim - Search and edit interactively:

vim.keymap.set('n', '<leader>se', function()
  vim.ui.input({ prompt = 'Search: ' }, function(pattern)
    if not pattern then return end
    vim.ui.input({ prompt = 'Command: ' }, function(cmd)
      if not cmd then return end
      vim.cmd('g/' .. pattern .. '/' .. cmd)
    end)
  end)
end, { desc = 'Search and execute' })

11.4 Incremental Search Features

See matches as you type:

:set incsearch       " Enable incremental search
:set hlsearch        " Highlight all matches

Neovim enhanced:

vim.opt.incsearch = true
vim.opt.hlsearch = true


-- Clear highlight with Esc
vim.keymap.set('n', '<Esc>', ':nohlsearch<CR>', { silent = true })


-- Or clear on cursor move (more automatic)
vim.api.nvim_create_autocmd('CursorMoved', {
  pattern = '*',
  callback = function()
    vim.opt.hlsearch = false
  end
})


-- Re-enable on search
vim.api.nvim_create_autocmd('CmdlineEnter', {
  pattern = '/',
  callback = function()
    vim.opt.hlsearch = true
  end
})

11.4.2 Live Substitution Preview

Neovim only - see replacements before confirming:

:set inccommand=split    " Show preview in split
:set inccommand=nosplit  " Show inline preview
vim.opt.inccommand = 'split'  -- or 'nosplit'

Now when typing :s/old/new/g, you see live preview.

11.5 Pattern Memory and Reuse

11.5.1 Search History

Access search history:

q/                   " Open search history window
/<Up>                " Previous search
/<Down>              " Next search

Neovim - Enhanced history navigation:


-- Better history navigation in command mode
vim.keymap.set('c', '<C-p>', '<Up>', { silent = true })
vim.keymap.set('c', '<C-n>', '<Down>', { silent = true })

11.5.2 Named Patterns

Store patterns in variables:

Vimscript:

" Define pattern
let email_pattern = '\v[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}'

" Use pattern
exe '/'.email_pattern

Neovim (Lua):

local patterns = {
  email = [[\v[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}]],
  url = [[\vhttps?://[^\s]+]],
  ipv4 = [[\v(\d{1,3}\.){3}\d{1,3}]],
  hex_color = [[\v#[0-9a-fA-F]{6}]],
  phone_us = [[\v\d{3}-\d{3}-\d{4}]],
}


-- Search for email
vim.keymap.set('n', '<leader>fe', function()
  vim.fn.search(patterns.email)
end, { desc = 'Find email' })


-- Search for URL
vim.keymap.set('n', '<leader>fu', function()
  vim.fn.search(patterns.url)
end, { desc = 'Find URL' })

11.5.3 Last Search Register

The last search is stored in / register:

:echo @/             " Show last search
:let @/ = 'new'      " Set search pattern

Neovim - Manipulate search programmatically:


-- Get last search
local last_search = vim.fn.getreg('/')


-- Set search pattern
vim.fn.setreg('/', 'pattern')


-- Clear search
vim.fn.setreg('/', '')


-- Highlight without moving cursor
vim.keymap.set('n', '<leader>*', function()
  local word = vim.fn.expand('<cword>')
  vim.fn.setreg('/', '\\<' .. word .. '\\>')
  vim.opt.hlsearch = true
end, { desc = 'Highlight word under cursor' })

11.6 Search Automation

11.6.1 Automated Search Highlighting

Highlight multiple patterns simultaneously:

Neovim - Multi-pattern highlighting:

local M = {}
M.highlights = {}

function M.add_highlight(pattern, group)
  local id = vim.fn.matchadd(group or 'Search', pattern)
  table.insert(M.highlights, id)
  return id
end

function M.clear_highlights()
  for _, id in ipairs(M.highlights) do
    vim.fn.matchdelete(id)
  end
  M.highlights = {}
end


-- Usage
vim.keymap.set('n', '<leader>ha', function()
  vim.ui.input({ prompt = 'Pattern: ' }, function(pattern)
    if not pattern then return end
    M.add_highlight(pattern, 'DiffAdd')
    print('Added highlight: ' .. pattern)
  end)
end, { desc = 'Add highlight' })

vim.keymap.set('n', '<leader>hc', function()
  M.clear_highlights()
  print('Cleared all highlights')
end, { desc = 'Clear highlights' })

Highlight TODO, FIXME, etc.:

vim.api.nvim_create_autocmd({'BufEnter', 'InsertLeave'}, {
  pattern = '*',
  callback = function()
    vim.fn.matchadd('Todo', [[\v<(TODO|FIXME|NOTE|HACK|XXX)>]])
    vim.fn.matchadd('Error', [[\v<(ERROR|BUG|DEPRECATED)>]])
    vim.fn.matchadd('WarningMsg', [[\v<(WARNING|WARN)>]])
  end
})

Configuration:

:set ignorecase      " Ignore case when searching
:set smartcase       " Override ignorecase if search has capitals

Behavior:

  • /foo matches foo, Foo, FOO

  • /Foo matches only Foo

  • /FOO matches only FOO

vim.opt.ignorecase = true
vim.opt.smartcase = true

11.6.3 Search Under Cursor Enhancement

Improved * and #:


-- Search word under cursor without jumping
vim.keymap.set('n', '*', function()
  local word = vim.fn.expand('<cword>')
  vim.fn.setreg('/', '\\<' .. word .. '\\>')
  vim.opt.hlsearch = true
  vim.fn.searchcount({recompute = true})  -- Update count
end, { desc = 'Highlight word (no jump)' })


-- Visual mode search
vim.keymap.set('v', '*', function()
  local old_reg = vim.fn.getreg('v')
  vim.cmd('normal! "vy')
  local selection = vim.fn.getreg('v')
  vim.fn.setreg('v', old_reg)
  vim.fn.setreg('/', '\\V' .. vim.fn.escape(selection, '\\/'))
  vim.opt.hlsearch = true
end, { desc = 'Search visual selection' })

11.7 External Tool Integration

11.7.1 Ripgrep Integration

Configure Vim to use ripgrep:

:set grepprg=rg\ --vimgrep\ --smart-case
:set grepformat=%f:%l:%c:%m
vim.opt.grepprg = 'rg --vimgrep --smart-case'
vim.opt.grepformat = '%f:%l:%c:%m'

Enhanced ripgrep search:

vim.keymap.set('n', '<leader>rg', function()
  vim.ui.input({ prompt = 'Ripgrep: ' }, function(pattern)
    if not pattern then return end
    vim.cmd('silent grep! ' .. vim.fn.shellescape(pattern))
    vim.cmd('copen')
  end)
end, { desc = 'Ripgrep search' })


-- Search word under cursor with ripgrep
vim.keymap.set('n', '<leader>rw', function()
  local word = vim.fn.expand('<cword>')
  vim.cmd('silent grep! ' .. vim.fn.shellescape(word))
  vim.cmd('copen')
end, { desc = 'Ripgrep word' })


-- Search in specific filetype
vim.keymap.set('n', '<leader>rt', function()
  vim.ui.input({ prompt = 'File type: ' }, function(ft)
    if not ft then return end
    vim.ui.input({ prompt = 'Pattern: ' }, function(pattern)
      if not pattern then return end
      vim.cmd('silent grep! -t ' .. ft .. ' ' .. vim.fn.shellescape(pattern))
      vim.cmd('copen')
    end)
  end)
end, { desc = 'Ripgrep by type' })

11.7.2 FZF Integration

Install and configure fzf.vim:


-- With lazy.nvim
{
  'junegunn/fzf.vim',
  dependencies = { 'junegunn/fzf' },
  config = function()

    -- Search with ripgrep
    vim.keymap.set('n', '<leader>fg', ':Rg<CR>', { desc = 'FZF ripgrep' })
    

    -- Search in files
    vim.keymap.set('n', '<leader>ff', ':Files<CR>', { desc = 'FZF files' })
    

    -- Search in buffers
    vim.keymap.set('n', '<leader>fb', ':Buffers<CR>', { desc = 'FZF buffers' })
    

    -- Search command history
    vim.keymap.set('n', '<leader>fh', ':History:<CR>', { desc = 'FZF command history' })
    

    -- Search in lines
    vim.keymap.set('n', '<leader>fl', ':Lines<CR>', { desc = 'FZF lines' })
  end
}

11.7.3 Telescope Integration (Neovim)

Modern fuzzy finder:


-- With lazy.nvim
{
  'nvim-telescope/telescope.nvim',
  dependencies = { 'nvim-lua/plenary.nvim' },
  config = function()
    local telescope = require('telescope.builtin')
    

    -- Live grep
    vim.keymap.set('n', '<leader>sg', telescope.live_grep, 
      { desc = 'Search grep' })
    

    -- Grep word under cursor
    vim.keymap.set('n', '<leader>sw', telescope.grep_string, 
      { desc = 'Search word' })
    

    -- Fuzzy find in current buffer
    vim.keymap.set('n', '<leader>/', telescope.current_buffer_fuzzy_find, 
      { desc = 'Search buffer' })
    

    -- Find files
    vim.keymap.set('n', '<leader>sf', telescope.find_files, 
      { desc = 'Search files' })
    

    -- Search in old files
    vim.keymap.set('n', '<leader>so', telescope.oldfiles, 
      { desc = 'Search old files' })
    

    -- Search help tags
    vim.keymap.set('n', '<leader>sh', telescope.help_tags, 
      { desc = 'Search help' })
  end
}

11.8 Search Patterns Library

11.8.1 Common Patterns

Email addresses:

/\v[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}

URLs:

/\vhttps?://[^\s]+
/\vhttps?://[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b[-a-zA-Z0-9()@:%_\+.~#?&/=]*

IP addresses (IPv4):

/\v(\d{1,3}\.){3}\d{1,3}
/\v((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)

Hexadecimal colors:

/\v#[0-9a-fA-F]{6}
/\v#([0-9a-fA-F]{3}){1,2}

Phone numbers (US format):

/\v\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}

Dates:

" YYYY-MM-DD
/\v\d{4}-\d{2}-\d{2}

" MM/DD/YYYY
/\v\d{2}/\d{2}/\d{4}

" DD-MMM-YYYY
/\v\d{2}-[A-Z][a-z]{2}-\d{4}

Credit card numbers:

/\v\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}

Social Security Numbers:

/\v\d{3}-\d{2}-\d{4}

11.8.2 Code-Specific Patterns

Function definitions:

" Python
/\vdef\s+\w+\s*\(

" JavaScript/TypeScript
/\vfunction\s+\w+\s*\(
/\v(const|let|var)\s+\w+\s*\=\s*\(

" C/C++/Java
/\v(public|private|protected)?\s*(static)?\s*\w+\s+\w+\s*\(

" Go
/\vfunc\s+(\w+\s*)?\w+\s*\(

Class definitions:

" Python
/\vclass\s+\w+

" JavaScript/TypeScript
/\vclass\s+\w+

" Java
/\v(public|private|protected)?\s*class\s+\w+

Import statements:

" Python
/\v^(import|from)\s+

" JavaScript/TypeScript
/\v^import\s+

" Java
/\v^import\s+

Comments:

" Single-line comments
/\v(//|#|;).*$

" Multi-line C-style
/\v\/\*\_.\{-}\*\/

" Python docstrings
/\v"""\_.\{-}"""

TODO markers:

/\v<(TODO|FIXME|HACK|NOTE|XXX|BUG|OPTIMIZE)>
/\v(TODO|FIXME)\(\w+\):  " With author: TODO(username):

11.8.3 Data Format Patterns

JSON:

" JSON string
/\v"[^"]*"\s*:\s*"[^"]*"

" JSON number
/\v"[^"]*"\s*:\s*-?\d+\.?\d*

" JSON boolean
/\v"[^"]*"\s*:\s*(true|false)

XML/HTML:

" Opening tag
/\v\<\w+[^>]*\>

" Closing tag
/\v\<\/\w+\>

" Self-closing tag
/\v\<\w+[^>]*\/\>

" Tag with specific attribute
/\v\<\w+[^>]*class\="[^"]*"[^>]*\>

CSV:

" Quoted field
/\v"[^"]*"

" Line with N fields
/\v^([^,]*,){5}[^,]*$  " 6 fields

Markdown:

" Headers
/\v^#{1,6}\s+

" Links
/\v\[[^\]]+\]\([^\)]+\)

" Code blocks
/\v```\_.\{-}```

11.9 Search Performance Optimization

11.9.1 Lazy Redraw

Disable screen updates during macros and complex operations:

:set lazyredraw
vim.opt.lazyredraw = true

11.9.2 Search Timeout

Prevent hanging on complex regex:

:set maxmempattern=2000  " Max memory for pattern (default 1000)
vim.opt.maxmempattern = 2000

11.9.3 Efficient Pattern Design

Tips:

  • Use word boundaries (\<\>) when possible

  • Avoid nested quantifiers: \(.\{-}\)\+ is slow

  • Use atomic grouping: \@> for no backtracking

  • Prefer character classes over alternation: [abc] vs \(a\|b\|c\)

Optimized patterns:

" Slow
/\v(foo|bar|baz)+

" Faster
/\v[fbraoaz]+

" Slow (nested quantifiers)
/\v(.{-})+

" Faster (atomic group)
/\v(.{-})@>

11.10 Search Recipes

Recipe 1: Find Duplicate Lines

" Find duplicate consecutive lines
/\v^(.+)$\n\1$

" Find duplicate anywhere (more complex)
:g/^\(.*\)$/mo$ | g/^$$\n\1$/d

Neovim (Lua):

vim.keymap.set('n', '<leader>fd', function()
  local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
  local seen = {}
  local duplicates = {}
  
  for i, line in ipairs(lines) do
    if seen[line] then
      table.insert(duplicates, { lnum = i, text = line })
    else
      seen[line] = true
    end
  end
  
  vim.fn.setqflist(duplicates)
  vim.cmd('copen')
  print('Found ' .. #duplicates .. ' duplicate lines')
end, { desc = 'Find duplicate lines' })

Recipe 2: Find Long Lines

" Find lines longer than 80 characters
/\v^.{80,}$

Neovim - Add to quickfix:

vim.keymap.set('n', '<leader>fL', function()
  vim.ui.input({ prompt = 'Max length: ', default = '80' }, function(len)
    if not len then return end
    local pattern = '^.\\{' .. len .. ',\\}$'
    vim.cmd('vimgrep /' .. pattern .. '/ %')
    vim.cmd('copen')
  end)
end, { desc = 'Find long lines' })

Recipe 3: Find Trailing Whitespace

/\s\+$

Neovim - Highlight and remove:


-- Highlight trailing whitespace
vim.api.nvim_create_autocmd({'BufEnter', 'InsertLeave'}, {
  pattern = '*',
  callback = function()
    vim.fn.matchadd('Error', [[\s\+$]])
  end
})


-- Remove trailing whitespace
vim.keymap.set('n', '<leader>dw', function()
  local view = vim.fn.winsaveview()
  vim.cmd([[%s/\s\+$//e]])
  vim.fn.winrestview(view)
  print('Removed trailing whitespace')
end, { desc = 'Delete trailing whitespace' })

Recipe 4: Find Unmatched Brackets

" Simple - find lines with unmatched ( or )
/\v^[^()]*\([^()]*$|^[^()]*\)[^()]*$

Neovim - Bracket balance checker:

vim.keymap.set('n', '<leader>fb', function()
  local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
  local unmatched = {}
  
  for i, line in ipairs(lines) do
    local open = 0
    for char in line:gmatch('.') do
      if char == '(' then open = open + 1
      elseif char == ')' then open = open - 1 end
    end
    if open ~= 0 then
      table.insert(unmatched, { lnum = i, text = line })
    end
  end
  
  vim.fn.setqflist(unmatched)
  vim.cmd('copen')
end, { desc = 'Find unmatched brackets' })

Show context around matches:

" Show 2 lines before and after match
:vimgrep /pattern/ % | copen
:set previewheight=5

Neovim - Enhanced context:

vim.keymap.set('n', '<leader>sc', function()
  vim.ui.input({ prompt = 'Pattern: ' }, function(pattern)
    if not pattern then return end
    vim.ui.input({ prompt = 'Context lines: ', default = '2' }, function(ctx)
      if not ctx then return end
      
      local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
      local matches = {}
      local context = tonumber(ctx)
      
      for i, line in ipairs(lines) do
        if line:match(pattern) then

          -- Add context
          local start = math.max(1, i - context)
          local finish = math.min(#lines, i + context)
          
          table.insert(matches, {
            lnum = i,
            text = table.concat(
              vim.list_slice(lines, start, finish), '\n'
            )
          })
        end
      end
      
      vim.fn.setqflist(matches)
      vim.cmd('copen')
    end)
  end)
end, { desc = 'Search with context' })

Recipe 6: Incremental Pattern Builder

Build complex patterns interactively:

Neovim:

local PatternBuilder = {}
PatternBuilder.parts = {}

function PatternBuilder:add(part)
  table.insert(self.parts, part)
  return self
end

function PatternBuilder:build()
  return table.concat(self.parts, '')
end

function PatternBuilder:search()
  local pattern = self:build()
  vim.fn.setreg('/', pattern)
  vim.opt.hlsearch = true
  return self
end


-- Usage
vim.keymap.set('n', '<leader>pb', function()
  local builder = setmetatable({parts = {}}, {__index = PatternBuilder})
  
  builder:add([[\v]])  -- Very magic
  
  vim.ui.input({ prompt = 'Start pattern: ' }, function(part)
    if part then builder:add(part) end
    
    vim.ui.input({ prompt = 'Middle (optional): ' }, function(mid)
      if mid then builder:add(mid) end
      
      vim.ui.input({ prompt = 'End (optional): ' }, function(finish)
        if finish then builder:add(finish) end
        
        builder:search()
        print('Pattern: ' .. builder:build())
      end)
    end)
  end)
end, { desc = 'Pattern builder' })

Chapter Summary

This chapter explored Vim’s advanced search capabilities:

Pattern Matching:

  • Very magic mode (\v) for cleaner regex syntax

  • Multi-line patterns using \_s and \_.

  • Zero-width assertions: lookahead/lookbehind (\@=, \@!, \@<=, \@<!)

  • Word boundaries and character classes

  • Case sensitivity modifiers (\c, \C)

Search Modifiers:

  • Search offsets (/e, /s, /+n, /-n)

  • Inline pattern modifiers

  • Search count display (:set shortmess-=S)

  • Smart case search

Advanced Commands:

  • Range-specific search

  • Global command (:g) for batch operations

  • Search history and pattern reuse

  • Last search register (@/)

External Integration:

  • Ripgrep configuration for fast project search

  • FZF for fuzzy finding

  • Telescope for modern Neovim experience

  • Custom grep programs

Automation:

  • Multi-pattern highlighting

  • Automated TODO/FIXME highlighting

  • Enhanced * and # operators

  • Visual selection search

Pattern Library:

  • Email, URL, IP address patterns

  • Code-specific patterns (functions, classes, imports)

  • Data format patterns (JSON, XML, CSV, Markdown)

  • Common validation patterns

Performance:

  • Lazy redraw during complex operations

  • Search timeout configuration

  • Efficient pattern design strategies

  • Avoiding slow regex constructs

Practical Recipes:

  • Finding duplicates, long lines, trailing whitespace

  • Bracket matching validation

  • Context-aware searching

  • Interactive pattern building

Best Practices:

  • Use very magic mode for readable patterns

  • Leverage word boundaries for precision

  • Integrate external tools for performance

  • Store common patterns in named variables

  • Combine search with quickfix for multi-file workflows

  • Use lookarounds for complex matching

  • Test patterns on small datasets first

Key Insights:

  • Search is more than finding—it’s a navigation and refactoring tool

  • Pattern composition enables complex transformations

  • External tools complement Vim’s built-in capabilities

  • Automation reduces repetitive search tasks

  • Well-designed patterns significantly improve performance

  • Integration with quickfix multiplies search power

Mastering advanced search transforms Vim from a text editor into a sophisticated pattern-matching and code analysis tool. Combined with the quickfix list, macros, and external tools, these techniques enable systematic codebase exploration, validation, and transformation at scale.

In the next chapter, we’ll explore Marks and Jumps, covering position tracking, cross-file navigation, and movement history management.


Chapter 12: Completion and Omni-Completion

Vim’s completion system transforms text entry from manual typing into an intelligent, context-aware process. This chapter explores built-in completion mechanisms, omni-completion for language-aware suggestions, and modern completion frameworks that bring IDE-like features to Vim/Neovim.

12.1 Understanding Vim’s Completion System

12.1.1 Completion Philosophy

Vim offers multiple completion modes, each triggered by specific key combinations in Insert mode. Unlike IDEs that automatically show suggestions, Vim requires explicit activation, giving you precise control.

Core principle: <C-x> enters completion sub-mode, followed by a second key specifying completion type.

12.1.2 Completion Workflow

Insert Mode → <C-?> → Completion Mode → Navigation → Selection → Continue

In completion mode:

  • <C-n> - Next match

  • <C-p> - Previous match

  • <C-y> - Accept current match

  • <C-e> - Cancel and return to original text

  • <CR> - Accept match and insert newline

  • Any character - Accept match and insert that character

12.2 Basic Completion Types

12.2.1 Line Completion (<C-x><C-l>)

Complete entire lines from current buffer or other loaded buffers.

Use case: Repeating similar code structures.

# Type: print("Hello
# Press: <C-x><C-l>
# Result: print("Hello, world!")  # if this line exists elsewhere

Configuration:

set complete+=.     " Current buffer
set complete+=w     " Buffers in other windows
set complete+=b     " Other loaded buffers
set complete+=t     " Tags
vim.opt.complete = { '.', 'w', 'b', 't' }

12.2.2 Keyword Completion (<C-x><C-n> or <C-x><C-p>)

Complete words from current buffer and sources specified in complete option.

// Type: constru
// Press: <C-x><C-n>
// Suggests: constructor

Direct shortcuts (no <C-x> needed):

  • <C-n> - Next keyword match

  • <C-p> - Previous keyword match

12.2.3 Dictionary Completion (<C-x><C-k>)

Complete from dictionary files.

Setup:

set dictionary+=/usr/share/dict/words
set dictionary+=~/.vim/custom-dict.txt
vim.opt.dictionary = '/usr/share/dict/words,~/.config/nvim/custom-dict.txt'

Create custom dictionary:

# Create language-specific dictionary
echo "async\nawait\npromise\ncallback" > ~/.vim/javascript-dict.txt
autocmd FileType javascript set dictionary+=~/.vim/javascript-dict.txt

12.2.4 Thesaurus Completion (<C-x><C-t>)

Find synonyms and related words.

Setup:

# Download thesaurus (example: OpenOffice thesaurus)
wget https://raw.githubusercontent.com/LibreOffice/dictionaries/master/en/th_en_US_v2.dat \

  -O ~/.vim/thesaurus.txt
set thesaurus+=~/.vim/thesaurus.txt
vim.opt.thesaurus = vim.opt.thesaurus + '~/.config/nvim/thesaurus.txt'

12.2.5 File Path Completion (<C-x><C-f>)

Complete file and directory paths.

# Type: from /home/user/proj
# Press: <C-x><C-f>
# Navigates filesystem, suggests directories/files

Enhanced with wildmenu:

set wildmenu
set wildmode=longest:full,full
vim.opt.wildmenu = true
vim.opt.wildmode = { 'longest:full', 'full' }

12.2.6 Tag Completion (<C-x><C-]>)

Complete from ctags database.

Generate tags:

# Universal Ctags
ctags -R .

# For specific languages
ctags -R --languages=python .

Vim configuration:

set tags=./tags,tags;$HOME
vim.opt.tags = { './tags', 'tags', vim.fn.expand('$HOME') .. '/tags' }

12.2.7 Spell Check Completion (<C-x><C-s> or <C-x>s)

Suggest spelling corrections.

set spell spelllang=en_us
vim.opt.spell = true
vim.opt.spelllang = { 'en_us' }

Type: recomend

Press:

Suggests: recommend

12.2.8 Command-Line Completion (<C-x><C-v>)

Complete Vim commands.

# Type in Insert mode: echo getw
# Press: <C-x><C-v>
# Suggests: getwinvar, getwinpos, etc.

12.2.9 Defined Name Completion (<C-x><C-d>)

Complete preprocessor definitions (C/C++).

// Completes #define macros

12.2.10 Include File Completion (<C-x><C-i>)

Complete from included files (respects include option).

// Type in C file: prin
// Press: <C-x><C-i>
// Suggests: printf (from stdio.h)

12.3 Omni-Completion

12.3.1 What is Omni-Completion?

Language-aware, context-sensitive completion triggered by <C-x><C-o>. Provides:

  • Method/property suggestions

  • Function signatures

  • Module imports

  • Type information

12.3.2 Enabling Omni-Completion

Basic setup:

filetype plugin on
set omnifunc=syntaxcomplete#Complete
vim.cmd('filetype plugin on')
vim.opt.omnifunc = 'syntaxcomplete#Complete'

12.3.3 Language-Specific Omni-Completion

Vim includes built-in omni-completion for many languages:

Python:

autocmd FileType python setlocal omnifunc=python3complete#Complete

JavaScript:

autocmd FileType javascript setlocal omnifunc=javascriptcomplete#CompleteJS

HTML:

autocmd FileType html setlocal omnifunc=htmlcomplete#CompleteTags

CSS:

autocmd FileType css setlocal omnifunc=csscomplete#CompleteCSS

XML:

autocmd FileType xml setlocal omnifunc=xmlcomplete#CompleteTags

PHP:

autocmd FileType php setlocal omnifunc=phpcomplete#CompletePHP

12.3.4 Omni-Completion in Action

// Type: document.getElem
// Press: <C-x><C-o>
// Suggests: getElementById, getElementsByClassName, etc.
# Type: import os
# os.pa
# Press: <C-x><C-o>
# Suggests: path, pathconf, pathconf_names, etc.

12.3.5 Custom Omni-Functions

Create language-specific completion:

Vimscript example:

function! CustomOmniComplete(findstart, base)
  if a:findstart
    " Find start of word
    let line = getline('.')
    let start = col('.') - 1
    while start > 0 && line[start - 1] =~ '\a'
      let start -= 1
    endwhile
    return start
  else
    " Generate completions
    let completions = ['apple', 'banana', 'cherry']
    return filter(completions, 'v:val =~ "^" . a:base')
  endif
endfunction

set omnifunc=CustomOmniComplete

Neovim (Lua) example:

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'myfiletype',
  callback = function()
    vim.bo.omnifunc = 'v:lua.custom_omni_complete'
  end
})

function _G.custom_omni_complete(findstart, base)
  if findstart == 1 then

    -- Find start of word
    local line = vim.api.nvim_get_current_line()
    local col = vim.api.nvim_win_get_cursor(0)[2]
    local start = col
    
    while start > 0 and line:sub(start, start):match('%a') do
      start = start - 1
    end
    
    return start
  else

    -- Generate completions
    local completions = { 'apple', 'banana', 'cherry', 'date' }
    local matches = {}
    
    for _, word in ipairs(completions) do
      if word:sub(1, #base) == base then
        table.insert(matches, word)
      end
    end
    
    return matches
  end
end

12.4 Completion Options and Behavior

12.4.1 Complete Options

set completeopt=menu,menuone,noselect,preview

Options explained:

  • menu - Show popup menu

  • menuone - Show menu even with one match

  • noselect - Don’t auto-select first match

  • preview - Show extra info in preview window

  • noinsert - Don’t insert text until selection

  • longest - Insert longest common text

vim.opt.completeopt = { 'menu', 'menuone', 'noselect', 'preview' }
set pumheight=15        " Max items in popup menu
set pumwidth=20         " Min width of popup menu
vim.opt.pumheight = 15
vim.opt.pumwidth = 20

12.4.3 Completion Timeout

set updatetime=300      " Faster completion (default 4000ms)
vim.opt.updatetime = 300

12.4.4 Case Sensitivity

set ignorecase          " Ignore case when completing
set infercase           " Adjust case of completion based on typed text
vim.opt.ignorecase = true
vim.opt.infercase = true

12.5 User-Defined Completion

12.5.1 Complete Function Interface

Define custom completion sources with completefunc:

function! MyComplete(findstart, base)
  if a:findstart
    " Return start column of completion
    return col('.') - 1
  else
    " Return list of matches
    return ['match1', 'match2', 'match3']
  endif
endfunction

set completefunc=MyComplete

Trigger with <C-x><C-u>.

12.5.2 Complete Items Structure

Return structured completion items:

function! DetailedComplete(findstart, base)
  if a:findstart
    return col('.') - 1
  else
    return [
      \ {'word': 'function', 'menu': 'keyword', 'info': 'Define a function'},
      \ {'word': 'class', 'menu': 'keyword', 'info': 'Define a class'},
      \ {'word': 'import', 'menu': 'keyword', 'info': 'Import a module'}
      \ ]
  endif
endfunction

Item structure:

  • word - Text to insert

  • abbr - Abbreviated form shown in menu

  • menu - Extra text in menu (type info)

  • info - Extra info shown in preview

  • kind - Single letter kind indicator

  • icase - Ignore case (0 or 1)

  • dup - Allow duplicate (0 or 1)

  • empty - Allow empty string

12.5.3 Neovim Complete Item Example

function _G.detailed_complete(findstart, base)
  if findstart == 1 then
    local line = vim.api.nvim_get_current_line()
    local col = vim.api.nvim_win_get_cursor(0)[2]
    return col
  else
    local items = {
      {
        word = 'async',
        abbr = 'async',
        menu = '[keyword]',
        info = 'Async function modifier',
        kind = 'k',
      },
      {
        word = 'await',
        abbr = 'await',
        menu = '[keyword]',
        info = 'Await promise resolution',
        kind = 'k',
      },
      {
        word = 'Promise',
        abbr = 'Promise',
        menu = '[class]',
        info = 'Promise constructor',
        kind = 'c',
      },
    }
    

    -- Filter by base
    local matches = {}
    for _, item in ipairs(items) do
      if vim.startswith(item.word, base) then
        table.insert(matches, item)
      end
    end
    
    return matches
  end
end

vim.opt.completefunc = 'v:lua.detailed_complete'

12.6 Modern Completion Frameworks

12.6.1 nvim-cmp (Neovim)

Modern, extensible completion engine.

Installation (lazy.nvim):

{
  'hrsh7th/nvim-cmp',
  dependencies = {
    'hrsh7th/cmp-nvim-lsp',        -- LSP source
    'hrsh7th/cmp-buffer',          -- Buffer source
    'hrsh7th/cmp-path',            -- Path source
    'hrsh7th/cmp-cmdline',         -- Command-line source
    'hrsh7th/cmp-nvim-lua',        -- Neovim Lua API
    'saadparwaiz1/cmp_luasnip',    -- Snippet source
    'L3MON4D3/LuaSnip',            -- Snippet engine
  },
  config = function()
    local cmp = require('cmp')
    local luasnip = require('luasnip')
    
    cmp.setup({
      snippet = {
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },
      
      mapping = cmp.mapping.preset.insert({
        ['<C-b>'] = cmp.mapping.scroll_docs(-4),
        ['<C-f>'] = cmp.mapping.scroll_docs(4),
        ['<C-Space>'] = cmp.mapping.complete(),
        ['<C-e>'] = cmp.mapping.abort(),
        ['<CR>'] = cmp.mapping.confirm({ select = true }),
        

        -- Tab completion
        ['<Tab>'] = cmp.mapping(function(fallback)
          if cmp.visible() then
            cmp.select_next_item()
          elseif luasnip.expand_or_jumpable() then
            luasnip.expand_or_jump()
          else
            fallback()
          end
        end, { 'i', 's' }),
        
        ['<S-Tab>'] = cmp.mapping(function(fallback)
          if cmp.visible() then
            cmp.select_prev_item()
          elseif luasnip.jumpable(-1) then
            luasnip.jump(-1)
          else
            fallback()
          end
        end, { 'i', 's' }),
      }),
      
      sources = cmp.config.sources({
        { name = 'nvim_lsp' },
        { name = 'luasnip' },
        { name = 'nvim_lua' },
      }, {
        { name = 'buffer', keyword_length = 3 },
        { name = 'path' },
      }),
      
      formatting = {
        format = function(entry, vim_item)

          -- Kind icons
          vim_item.kind = string.format('%s %s', 
            kind_icons[vim_item.kind], vim_item.kind)
          

          -- Source name
          vim_item.menu = ({
            nvim_lsp = '[LSP]',
            luasnip = '[Snip]',
            buffer = '[Buf]',
            path = '[Path]',
            nvim_lua = '[Lua]',
          })[entry.source.name]
          
          return vim_item
        end,
      },
      
      window = {
        completion = cmp.config.window.bordered(),
        documentation = cmp.config.window.bordered(),
      },
      
      experimental = {
        ghost_text = true,  -- Show preview as ghost text
      },
    })
    

    -- Command-line completion
    cmp.setup.cmdline('/', {
      mapping = cmp.mapping.preset.cmdline(),
      sources = {
        { name = 'buffer' }
      }
    })
    
    cmp.setup.cmdline(':', {
      mapping = cmp.mapping.preset.cmdline(),
      sources = cmp.config.sources({
        { name = 'path' }
      }, {
        { name = 'cmdline' }
      })
    })
  end
}

12.6.2 nvim-cmp Source Priority

Control source order and behavior:

sources = cmp.config.sources({

  -- High priority
  { name = 'nvim_lsp', priority = 1000 },
  { name = 'luasnip', priority = 750 },
}, {

  -- Lower priority
  { name = 'buffer', priority = 500, keyword_length = 3 },
  { name = 'path', priority = 250 },
}),

12.6.3 Custom nvim-cmp Source

Create project-specific completion:

local source = {}

function source:is_available()
  return vim.bo.filetype == 'myfiletype'
end

function source:get_keyword_pattern()
  return [[\k\+]]
end

function source:complete(params, callback)
  local items = {
    { label = 'custom_func', kind = cmp.lsp.CompletionItemKind.Function },
    { label = 'custom_var', kind = cmp.lsp.CompletionItemKind.Variable },
  }
  callback({ items = items, isIncomplete = false })
end


-- Register source
require('cmp').register_source('my_source', source)


-- Use in setup
sources = {
  { name = 'my_source' },
  { name = 'nvim_lsp' },
}

12.6.4 CoC.nvim (Vim & Neovim)

VSCode-like completion using Language Server Protocol.

Installation:

" Using vim-plug
Plug 'neoclide/coc.nvim', {'branch': 'release'}

Basic configuration:

" Use Tab for trigger completion
inoremap <silent><expr> <TAB>
      \ coc#pum#visible() ? coc#pum#next(1) :
      \ CheckBackspace() ? "\<Tab>" :
      \ coc#refresh()

inoremap <expr><S-TAB> coc#pum#visible() ? coc#pum#prev(1) : "\<C-h>"

" Make <CR> to accept selected completion
inoremap <silent><expr> <CR> coc#pum#visible() ? coc#pum#confirm()
                              \: "\<C-g>u\<CR>\<c-r>=coc#on_enter()\<CR>"

function! CheckBackspace() abort
  let col = col('.') - 1
  return !col || getline('.')[col - 1]  =~# '\s'
endfunction

" Use <c-space> to trigger completion
inoremap <silent><expr> <c-space> coc#refresh()

" Navigate diagnostics
nmap <silent> [g <Plug>(coc-diagnostic-prev)
nmap <silent> ]g <Plug>(coc-diagnostic-next)

" GoTo code navigation
nmap <silent> gd <Plug>(coc-definition)
nmap <silent> gy <Plug>(coc-type-definition)
nmap <silent> gi <Plug>(coc-implementation)
nmap <silent> gr <Plug>(coc-references)

" Show documentation
nnoremap <silent> K :call ShowDocumentation()<CR>

function! ShowDocumentation()
  if CocAction('hasProvider', 'hover')
    call CocActionAsync('doHover')
  else
    call feedkeys('K', 'in')
  endif
endfunction

Install language servers:

:CocInstall coc-tsserver      " TypeScript/JavaScript
:CocInstall coc-pyright       " Python
:CocInstall coc-rust-analyzer " Rust
:CocInstall coc-json          " JSON
:CocInstall coc-html          " HTML
:CocInstall coc-css           " CSS

12.6.5 YouCompleteMe (Vim & Neovim)

Fast, compiled completion engine.

Installation:

" Using vim-plug
Plug 'ycm-core/YouCompleteMe'

Build with language support:

cd ~/.vim/plugged/YouCompleteMe
python3 install.py --all  # All languages
# Or specific:
python3 install.py --ts-completer    # TypeScript/JavaScript
python3 install.py --rust-completer  # Rust
python3 install.py --go-completer    # Go

Configuration:

let g:ycm_key_list_select_completion = ['<C-n>', '<Down>']
let g:ycm_key_list_previous_completion = ['<C-p>', '<Up>']
let g:ycm_auto_trigger = 1
let g:ycm_min_num_of_chars_for_completion = 2
let g:ycm_complete_in_comments = 1
let g:ycm_complete_in_strings = 1
let g:ycm_collect_identifiers_from_tags_files = 1

" Semantic triggers
let g:ycm_semantic_triggers =  {
  \ 'c,cpp,python,java,go,rust,cs,javascript,typescript': ['re!\w{2}'],
  \ }

12.7 LSP-Based Completion

12.7.1 Native Neovim LSP

Setup LSP with completion:


-- Install language servers via Mason
{
  'williamboman/mason.nvim',
  'williamboman/mason-lspconfig.nvim',
  'neovim/nvim-lspconfig',
}


-- Configure LSP
local lspconfig = require('lspconfig')
local capabilities = require('cmp_nvim_lsp').default_capabilities()


-- TypeScript
lspconfig.tsserver.setup({
  capabilities = capabilities,
  on_attach = function(client, bufnr)

    -- Keybindings
    local opts = { buffer = bufnr }
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
    vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
    vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
    vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts)
  end,
})


-- Python
lspconfig.pyright.setup({
  capabilities = capabilities,
})


-- Rust
lspconfig.rust_analyzer.setup({
  capabilities = capabilities,
  settings = {
    ['rust-analyzer'] = {
      checkOnSave = {
        command = 'clippy',
      },
    },
  },
})

12.7.2 LSP Signature Help

Show function signatures while typing:


-- Manual trigger
vim.keymap.set('i', '<C-k>', vim.lsp.buf.signature_help, 
  { desc = 'Signature help' })


-- Automatic with plugin
{
  'ray-x/lsp_signature.nvim',
  config = function()
    require('lsp_signature').setup({
      bind = true,
      handler_opts = {
        border = 'rounded'
      },
      hint_enable = false,
    })
  end
}

12.8 Snippet Integration

12.8.1 LuaSnip (Neovim)

Powerful snippet engine.

Basic snippets:

local ls = require('luasnip')
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node

ls.add_snippets('all', {
  s('todo', {
    t('TODO('), i(1, 'name'), t('): '), i(2, 'description')
  }),
})

ls.add_snippets('python', {
  s('def', {
    t('def '), i(1, 'function_name'), t('('), i(2, 'args'), t('):'),
    t({'', '    '}), i(3, 'pass'),
  }),
})


-- Keybindings
vim.keymap.set({'i', 's'}, '<C-l>', function()
  if ls.expand_or_jumpable() then
    ls.expand_or_jump()
  end
end, { silent = true })

vim.keymap.set({'i', 's'}, '<C-h>', function()
  if ls.jumpable(-1) then
    ls.jump(-1)
  end
end, { silent = true })

12.8.2 UltiSnips (Vim & Neovim)

Classic snippet engine.

Installation:

Plug 'SirVer/ultisnips'
Plug 'honza/vim-snippets'  " Snippet collection

Configuration:

let g:UltiSnipsExpandTrigger="<tab>"
let g:UltiSnipsJumpForwardTrigger="<c-b>"
let g:UltiSnipsJumpBackwardTrigger="<c-z>"
let g:UltiSnipsEditSplit="vertical"

Create custom snippet:

:UltiSnipsEdit
snippet func "Function definition"
function ${1:function_name}(${2:args}) {
    ${3:// body}
}
endsnippet

snippet for "For loop"
for (let ${1:i} = 0; $1 < ${2:count}; $1++) {
    ${3:// body}
}
endsnippet

12.9 Completion Recipes

Recipe 1: Context-Aware Completion


-- Complete based on filetype and context
function _G.smart_complete()
  local line = vim.api.nvim_get_current_line()
  local col = vim.api.nvim_win_get_cursor(0)[2]
  local before_cursor = line:sub(1, col)
  

  -- If after '.', use omni-completion
  if before_cursor:match('%.$') then
    return vim.fn['feedkeys'](vim.api.nvim_replace_termcodes(
      '<C-x><C-o>', true, false, true
    ))
  end
  

  -- If in string, use file completion
  if before_cursor:match('["\']') then
    return vim.fn['feedkeys'](vim.api.nvim_replace_termcodes(
      '<C-x><C-f>', true, false, true
    ))
  end
  

  -- Default to keyword completion
  return vim.fn['feedkeys'](vim.api.nvim_replace_termcodes(
    '<C-n>', true, false, true
  ))
end

vim.keymap.set('i', '<C-Space>', _G.smart_complete, { expr = true })

Recipe 2: Fuzzy File Finder Completion


-- Complete file paths with fuzzy matching
{
  'nvim-telescope/telescope.nvim',
  config = function()
    local telescope = require('telescope.builtin')
    

    -- Insert mode file completion
    vim.keymap.set('i', '<C-x><C-t>', function()
      telescope.find_files({
        attach_mappings = function(_, map)
          map('i', '<CR>', function(prompt_bufnr)
            local selection = require('telescope.actions.state')
              .get_selected_entry()
            require('telescope.actions').close(prompt_bufnr)
            

            -- Insert filename at cursor
            local row, col = unpack(vim.api.nvim_win_get_cursor(0))
            local line = vim.api.nvim_get_current_line()
            local new_line = line:sub(1, col) .. selection.value 
                            .. line:sub(col + 1)
            vim.api.nvim_set_current_line(new_line)
            vim.api.nvim_win_set_cursor(0, {row, col + #selection.value})
          end)
          return true
        end,
      })
    end, { desc = 'Fuzzy file completion' })
  end
}

Recipe 3: AI-Powered Completion


-- GitHub Copilot integration
{
  'github/copilot.vim',
  config = function()
    vim.g.copilot_no_tab_map = true
    vim.keymap.set('i', '<C-J>', 'copilot#Accept("\\<CR>")', {
      expr = true,
      replace_keycodes = false
    })
    vim.g.copilot_filetypes = {
      ['*'] = false,
      python = true,
      javascript = true,
      typescript = true,
      rust = true,
      go = true,
    }
  end
}


-- Alternative: Codeium
{
  'Exafunction/codeium.vim',
  config = function()
    vim.g.codeium_disable_bindings = 1
    vim.keymap.set('i', '<C-g>', function()
      return vim.fn['codeium#Accept']()
    end, { expr = true })
    vim.keymap.set('i', '<C-n>', function()
      return vim.fn['codeium#CycleCompletions'](1)
    end, { expr = true })
  end
}

Recipe 4: Abbreviation-Based Completion

Expand abbreviations automatically:

" Vimscript abbreviations
iabbrev teh the
iabbrev @@ your.email@example.com
iabbrev ccopy Copyright 2025 Your Name

" Date expansion
iabbrev <expr> dts strftime("%Y-%m-%d")
iabbrev <expr> dtt strftime("%H:%M:%S")

-- Neovim Lua abbreviations
vim.cmd([[
  iabbrev teh the
  iabbrev @@ your.email@example.com
  iabbrev <expr> dts strftime("%Y-%m-%d")
]])


-- Dynamic abbreviations
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    vim.cmd([[
      iabbrev <buffer> pdb import pdb; pdb.set_trace()
      iabbrev <buffer> ipdb import ipdb; ipdb.set_trace()
    ]])
  end
})

Recipe 5: Custom Dictionary Per Project


-- Load project-specific dictionary
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    local dict_file = vim.fn.getcwd() .. '/.vim-dictionary'
    if vim.fn.filereadable(dict_file) == 1 then
      vim.opt.dictionary:append(dict_file)
      print('Loaded project dictionary: ' .. dict_file)
    end
  end
})

Create .vim-dictionary in project root: asyncHandler promisify getElementById componentDidMount


Chapter Summary

This chapter explored Vim’s comprehensive completion system:

Built-in Completion Types:

  • Line completion (<C-x><C-l>)

  • Keyword completion (<C-n>, <C-p>)

  • Dictionary completion (<C-x><C-k>)

  • Thesaurus completion (<C-x><C-t>)

  • File path completion (<C-x><C-f>)

  • Tag completion (<C-x><C-]>)

  • Spell check completion (<C-x><C-s>)

  • Command completion (<C-x><C-v>)

  • Include file completion (<C-x><C-i>)

Omni-Completion:

  • Language-aware completion (<C-x><C-o>)

  • Built-in support for Python, JavaScript, HTML, CSS, PHP

  • Custom omni-functions for specialized completion

Completion Configuration:

  • completeopt settings (menu, preview, noselect)

  • Case sensitivity and inference

  • Popup menu customization

  • Completion sources and priorities

Custom Completion:

  • User-defined completion functions

  • Structured completion items

  • Custom sources for specific filetypes

Modern Frameworks:

  • nvim-cmp: Extensible, fast, modern (Neovim)

  • CoC.nvim: VSCode-like LSP integration

  • YouCompleteMe: Fast compiled engine

LSP Integration:

  • Native Neovim LSP support

  • Language server configuration

  • Signature help and documentation

  • Code actions and refactoring

Snippet Systems:

  • LuaSnip: Modern Neovim snippet engine

  • UltiSnips: Classic, powerful snippets

  • Custom snippet creation

  • Dynamic snippet expansion

Advanced Techniques:

  • Context-aware smart completion

  • Fuzzy file path insertion

  • AI-powered completion (Copilot, Codeium)

  • Abbreviation systems

  • Project-specific dictionaries

Best Practices:

  • Use noselect to avoid accidental insertions

  • Configure language servers for accurate suggestions

  • Combine multiple sources with priority

  • Create project-specific dictionaries

  • Map intuitive keys (Tab, Ctrl-Space)

  • Enable signature help for function parameters

  • Use ghost text for inline previews

  • Leverage snippets for boilerplate code

Key Insights:

  • Vim’s completion is modular—each type serves a purpose

  • Modern frameworks unify completion sources

  • LSP integration brings IDE-level intelligence

  • Snippets reduce repetitive typing

  • Combination of sources creates comprehensive completion

  • Context awareness improves accuracy

  • Custom completion functions enable domain-specific workflows

Mastering completion transforms coding from character-by-character typing into high-level intent expression. Combined with LSP, snippets, and modern frameworks, Vim becomes a fully-featured development environment rivaling traditional IDEs while maintaining its characteristic speed and keyboard-driven efficiency.

In the next chapter, we’ll explore Marks and Jumps, covering position tracking, cross-file navigation, and movement history management for efficient codebase exploration.


Chapter 13: The vimrc/init.vim/init.lua

Your configuration file is Vim’s DNA—a living document that evolves with your workflow. This chapter explores configuration architecture, best practices, and advanced techniques for creating a maintainable, performant setup.

13.1 Configuration File Basics

13.1.1 File Locations and Naming

Vim:

# Unix/Linux/macOS
~/.vimrc
~/.vim/vimrc

# Windows
$HOME/_vimrc
$HOME/vimfiles/vimrc

Neovim:

# Unix/Linux/macOS
~/.config/nvim/init.vim    # Vimscript
~/.config/nvim/init.lua    # Lua

# Windows
~/AppData/Local/nvim/init.vim
~/AppData/Local/nvim/init.lua

Check current configuration path:

:echo $MYVIMRC

13.1.2 Choosing Configuration Language

Neovim offers three approaches:

  1. Pure Vimscript (init.vim):

    • Maximum compatibility with Vim

    • Familiar syntax for Vim users

    • Access to all Vim functions

  2. Pure Lua (init.lua):

    • Better performance

    • Modern language features

    • First-class Neovim API access

  3. Hybrid (Lua + Vimscript):

    • Use Lua for performance-critical code

    • Keep Vimscript for simple settings

    • Gradual migration path

13.1.3 Configuration Loading Order

Vim’s startup sequence:

  1. System vimrc (/etc/vimrc)

  2. User vimrc (~/.vimrc)

  3. User gvimrc (~/.gvimrc) [GUI only]

  4. Defaults.vim (if no user vimrc found)

  5. Plugin scripts

  6. After directory scripts (~/.vim/after/)

Check what files are loaded:

:scriptnames

13.2 Basic Configuration Structure

13.2.1 Minimal Vimrc Example

" ~/.vimrc - Minimal configuration

" Compatibility
set nocompatible              " Use Vim defaults (not vi)

" Basic settings
set number                    " Show line numbers
set relativenumber            " Relative line numbers
set mouse=a                   " Enable mouse support
set encoding=utf-8            " UTF-8 encoding

" Indentation
set tabstop=4                 " Tab width
set shiftwidth=4              " Indent width
set expandtab                 " Use spaces instead of tabs
set smartindent               " Smart auto-indenting

" Search
set incsearch                 " Incremental search
set hlsearch                  " Highlight search results
set ignorecase                " Case-insensitive search
set smartcase                 " Case-sensitive if uppercase present

" Interface
set showcmd                   " Show command in bottom bar
set wildmenu                  " Visual autocomplete for command menu
set laststatus=2              " Always show status line

" Performance
set lazyredraw                " Don't redraw during macros
set updatetime=300            " Faster completion

" Backup and swap
set nobackup                  " No backup files
set nowritebackup             " No backup while editing
set noswapfile                " No swap files

" Enable filetype detection
filetype plugin indent on
syntax enable

13.2.2 Minimal init.lua Example


-- ~/.config/nvim/init.lua - Minimal configuration


-- Basic settings
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.mouse = 'a'
vim.opt.encoding = 'utf-8'


-- Indentation
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.smartindent = true


-- Search
vim.opt.incsearch = true
vim.opt.hlsearch = true
vim.opt.ignorecase = true
vim.opt.smartcase = true


-- Interface
vim.opt.showcmd = true
vim.opt.wildmenu = true
vim.opt.laststatus = 2


-- Performance
vim.opt.lazyredraw = true
vim.opt.updatetime = 300


-- Backup and swap
vim.opt.backup = false
vim.opt.writebackup = false
vim.opt.swapfile = false


-- Enable filetype detection
vim.cmd('filetype plugin indent on')
vim.cmd('syntax enable')

13.3 Modular Configuration

13.3.1 Organizing Vimscript Configuration

Directory structure: ~/.vim/

├│── vimrc # Main config

├│── autoload/ # Auto-loaded functions

├│── plugin/ # Plugin configurations

├│── ftplugin/ # Filetype-specific settings

├│── colors/ # Color schemes └── after/ # Late-loading scripts └── plugin/

Main vimrc with sourcing:

" ~/.vimrc

" Source modular configs
runtime config/settings.vim
runtime config/mappings.vim
runtime config/plugins.vim
runtime config/autocmds.vim

~/.vim/config/settings.vim:

" Basic settings module

set number
set relativenumber
set cursorline
set termguicolors

" Tab behavior
set tabstop=2
set shiftwidth=2
set expandtab

" Search settings
set ignorecase
set smartcase
set hlsearch
set incsearch

" Split behavior
set splitright
set splitbelow

~/.vim/config/mappings.vim:

" Key mappings module

" Set leader key
let mapleader = ' '
let maplocalleader = ','

" Quick save
nnoremap <leader>w :write<CR>

" Clear search highlighting
nnoremap <Esc><Esc> :nohlsearch<CR>

" Window navigation
nnoremap <C-h> <C-w>h
nnoremap <C-j> <C-w>j
nnoremap <C-k> <C-w>k
nnoremap <C-l> <C-w>l

" Buffer navigation
nnoremap <leader>bn :bnext<CR>
nnoremap <leader>bp :bprevious<CR>

13.3.2 Organizing Lua Configuration

Directory structure: ~/.config/nvim/

├│── init.lua # Entry point └── lua/ ├── core/ │ ├── options.lua │ ├── keymaps.lua │ └── autocmds.lua ├── plugins/ │ ├── init.lua │ ├── lsp.lua │ ├── telescope.lua │ └── treesitter.lua └── utils/ └── helpers.lua

~/.config/nvim/init.lua:


-- Entry point - load modules
require('core.options')
require('core.keymaps')
require('core.autocmds')
require('plugins')

~/.config/nvim/lua/core/options.lua:


-- Option settings module

local opt = vim.opt


-- Line numbers
opt.number = true
opt.relativenumber = true
opt.cursorline = true


-- Tabs and indentation
opt.tabstop = 2
opt.shiftwidth = 2
opt.expandtab = true
opt.autoindent = true
opt.smartindent = true


-- Search
opt.ignorecase = true
opt.smartcase = true
opt.hlsearch = true
opt.incsearch = true


-- Appearance
opt.termguicolors = true
opt.background = 'dark'
opt.signcolumn = 'yes'
opt.showmode = false


-- Splits
opt.splitright = true
opt.splitbelow = true


-- Clipboard
opt.clipboard = 'unnamedplus'


-- Performance
opt.updatetime = 250
opt.timeoutlen = 300


-- Backup
opt.swapfile = false
opt.backup = false
opt.undofile = true
opt.undodir = os.getenv('HOME') .. '/.vim/undodir'

~/.config/nvim/lua/core/keymaps.lua:


-- Keymap settings module

local keymap = vim.keymap
local opts = { noremap = true, silent = true }


-- Leader key
vim.g.mapleader = ' '
vim.g.maplocalleader = ','


-- Quick save
keymap.set('n', '<leader>w', ':w<CR>', opts)


-- Clear search highlighting
keymap.set('n', '<Esc>', ':nohlsearch<CR>', opts)


-- Window navigation
keymap.set('n', '<C-h>', '<C-w>h', opts)
keymap.set('n', '<C-j>', '<C-w>j', opts)
keymap.set('n', '<C-k>', '<C-w>k', opts)
keymap.set('n', '<C-l>', '<C-w>l', opts)


-- Window resizing
keymap.set('n', '<C-Up>', ':resize +2<CR>', opts)
keymap.set('n', '<C-Down>', ':resize -2<CR>', opts)
keymap.set('n', '<C-Left>', ':vertical resize -2<CR>', opts)
keymap.set('n', '<C-Right>', ':vertical resize +2<CR>', opts)


-- Buffer navigation
keymap.set('n', '<S-l>', ':bnext<CR>', opts)
keymap.set('n', '<S-h>', ':bprevious<CR>', opts)
keymap.set('n', '<leader>bd', ':bdelete<CR>', opts)


-- Indentation in visual mode
keymap.set('v', '<', '<gv', opts)
keymap.set('v', '>', '>gv', opts)


-- Move text up/down
keymap.set('v', 'J', ":m '>+1<CR>gv=gv", opts)
keymap.set('v', 'K', ":m '<-2<CR>gv=gv", opts)


-- Better paste
keymap.set('v', 'p', '"_dP', opts)

~/.config/nvim/lua/core/autocmds.lua:


-- Autocommand module

local autocmd = vim.api.nvim_create_autocmd
local augroup = vim.api.nvim_create_augroup


-- General settings group
local general = augroup('General', { clear = true })


-- Highlight on yank
autocmd('TextYankPost', {
  group = general,
  callback = function()
    vim.highlight.on_yank({ higroup = 'Visual', timeout = 200 })
  end,
})


-- Remove trailing whitespace on save
autocmd('BufWritePre', {
  group = general,
  pattern = '*',
  callback = function()
    local save_cursor = vim.fn.getpos('.')
    vim.cmd([[%s/\s\+$//e]])
    vim.fn.setpos('.', save_cursor)
  end,
})


-- Auto-format on save (specific filetypes)
autocmd('BufWritePre', {
  group = general,
  pattern = { '*.lua', '*.py', '*.js', '*.ts' },
  callback = function()
    vim.lsp.buf.format({ async = false })
  end,
})


-- Restore cursor position
autocmd('BufReadPost', {
  group = general,
  callback = function()
    local mark = vim.api.nvim_buf_get_mark(0, '"')
    local lcount = vim.api.nvim_buf_line_count(0)
    if mark[1] > 0 and mark[1] <= lcount then
      pcall(vim.api.nvim_win_set_cursor, 0, mark)
    end
  end,
})


-- Filetype-specific settings
local filetype = augroup('FileType', { clear = true })


-- Python
autocmd('FileType', {
  group = filetype,
  pattern = 'python',
  callback = function()
    vim.opt_local.tabstop = 4
    vim.opt_local.shiftwidth = 4
    vim.opt_local.textwidth = 88
  end,
})


-- Go
autocmd('FileType', {
  group = filetype,
  pattern = 'go',
  callback = function()
    vim.opt_local.tabstop = 4
    vim.opt_local.shiftwidth = 4
    vim.opt_local.expandtab = false
  end,
})


-- Web development
autocmd('FileType', {
  group = filetype,
  pattern = { 'javascript', 'typescript', 'html', 'css', 'json' },
  callback = function()
    vim.opt_local.tabstop = 2
    vim.opt_local.shiftwidth = 2
  end,
})


-- Markdown
autocmd('FileType', {
  group = filetype,
  pattern = 'markdown',
  callback = function()
    vim.opt_local.wrap = true
    vim.opt_local.spell = true
    vim.opt_local.textwidth = 80
  end,
})

13.4 Plugin Management

13.4.1 Using vim-plug (Vim & Neovim)

Installation:

# Vim
curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
    https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

# Neovim
sh -c 'curl -fLo "${XDG_DATA_HOME:-$HOME/.local/share}"/nvim/site/autoload/plug.vim --create-dirs \
       https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim'

Configuration (~/.vimrc or ~/.config/nvim/init.vim):

call plug#begin('~/.vim/plugged')

" Essential plugins
Plug 'tpope/vim-sensible'        " Sensible defaults
Plug 'tpope/vim-surround'        " Surround text objects
Plug 'tpope/vim-commentary'      " Comment/uncomment

" File navigation
Plug 'junegunn/fzf', { 'do': { -> fzf#install() } }
Plug 'junegunn/fzf.vim'

" Status line
Plug 'vim-airline/vim-airline'
Plug 'vim-airline/vim-airline-themes'

" Git integration
Plug 'tpope/vim-fugitive'
Plug 'airblade/vim-gitgutter'

" Syntax highlighting
Plug 'sheerun/vim-polyglot'

" Color schemes
Plug 'morhetz/gruvbox'
Plug 'dracula/vim', { 'as': 'dracula' }

" Conditional loading
Plug 'scrooloose/nerdtree', { 'on': 'NERDTreeToggle' }

call plug#end()

" Plugin settings
colorscheme gruvbox
let g:airline_theme = 'gruvbox'

Commands:

:PlugInstall    " Install plugins
:PlugUpdate     " Update plugins
:PlugClean      " Remove unlisted plugins
:PlugUpgrade    " Upgrade vim-plug itself
:PlugStatus     " Check plugin status

13.4.2 Using lazy.nvim (Neovim)

Modern, performance-focused plugin manager.

Bootstrap installation (~/.config/nvim/init.lua):


-- Bootstrap lazy.nvim
local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({
    'git',
    'clone',
    '--filter=blob:none',
    'https://github.com/folke/lazy.nvim.git',
    '--branch=stable',
    lazypath,
  })
end
vim.opt.rtp:prepend(lazypath)


-- Load plugins
require('lazy').setup('plugins', {
  defaults = {
    lazy = true,  -- Lazy-load by default
  },
  performance = {
    cache = {
      enabled = true,
    },
    rtp = {
      disabled_plugins = {
        'gzip',
        'tarPlugin',
        'tohtml',
        'tutor',
        'zipPlugin',
      },
    },
  },
})

~/.config/nvim/lua/plugins/init.lua:

return {

  -- Essential plugins
  { 'nvim-lua/plenary.nvim' },
  

  -- Treesitter
  {
    'nvim-treesitter/nvim-treesitter',
    build = ':TSUpdate',
    event = { 'BufReadPost', 'BufNewFile' },
    config = function()
      require('plugins.treesitter')
    end,
  },
  

  -- LSP
  {
    'neovim/nvim-lspconfig',
    event = { 'BufReadPre', 'BufNewFile' },
    dependencies = {
      'williamboman/mason.nvim',
      'williamboman/mason-lspconfig.nvim',
    },
    config = function()
      require('plugins.lsp')
    end,
  },
  

  -- Completion
  {
    'hrsh7th/nvim-cmp',
    event = 'InsertEnter',
    dependencies = {
      'hrsh7th/cmp-nvim-lsp',
      'hrsh7th/cmp-buffer',
      'hrsh7th/cmp-path',
      'L3MON4D3/LuaSnip',
      'saadparwaiz1/cmp_luasnip',
    },
    config = function()
      require('plugins.cmp')
    end,
  },
  

  -- Telescope
  {
    'nvim-telescope/telescope.nvim',
    cmd = 'Telescope',
    keys = {
      { '<leader>ff', '<cmd>Telescope find_files<cr>', desc = 'Find files' },
      { '<leader>fg', '<cmd>Telescope live_grep<cr>', desc = 'Live grep' },
      { '<leader>fb', '<cmd>Telescope buffers<cr>', desc = 'Buffers' },
    },
    config = function()
      require('plugins.telescope')
    end,
  },
  

  -- Git
  {
    'lewis6991/gitsigns.nvim',
    event = { 'BufReadPre', 'BufNewFile' },
    config = function()
      require('gitsigns').setup()
    end,
  },
  

  -- Color scheme
  {
    'catppuccin/nvim',
    name = 'catppuccin',
    lazy = false,
    priority = 1000,
    config = function()
      vim.cmd.colorscheme('catppuccin')
    end,
  },
  

  -- Status line
  {
    'nvim-lualine/lualine.nvim',
    event = 'VeryLazy',
    config = function()
      require('lualine').setup({
        options = {
          theme = 'catppuccin',
        },
      })
    end,
  },
}

Lazy-loading strategies:

{
  'plugin-name',
  

  -- Load on specific commands
  cmd = { 'Command1', 'Command2' },
  

  -- Load on specific events
  event = { 'BufReadPost', 'BufNewFile' },
  

  -- Load on specific filetypes
  ft = { 'python', 'javascript' },
  

  -- Load on keymap
  keys = {
    { '<leader>x', '<cmd>Command<cr>', desc = 'Description' },
  },
  

  -- Load immediately
  lazy = false,
  

  -- Load before other plugins
  priority = 1000,
  

  -- Load after another plugin
  dependencies = { 'other-plugin' },
}

13.4.3 Using Packer (Neovim - Legacy)

~/.config/nvim/lua/plugins.lua:


-- Auto-install packer
local ensure_packer = function()
  local fn = vim.fn
  local install_path = fn.stdpath('data')..'/site/pack/packer/start/packer.nvim'
  if fn.empty(fn.glob(install_path)) > 0 then
    fn.system({
      'git', 'clone', '--depth', '1',
      'https://github.com/wbthomason/packer.nvim',
      install_path
    })
    vim.cmd [[packadd packer.nvim]]
    return true
  end
  return false
end

local packer_bootstrap = ensure_packer()

return require('packer').startup(function(use)
  use 'wbthomason/packer.nvim'
  
  use {
    'nvim-treesitter/nvim-treesitter',
    run = ':TSUpdate'
  }
  
  use {
    'neovim/nvim-lspconfig',
    requires = {
      'williamboman/mason.nvim',
      'williamboman/mason-lspconfig.nvim',
    },
    config = function()
      require('lsp-config')
    end
  }
  

  -- Auto-compile on save
  if packer_bootstrap then
    require('packer').sync()
  end
end)

13.5 Performance Optimization

13.5.1 Profiling Startup Time

Vim:

vim --startuptime startup.log

Neovim:

nvim --startuptime startup.log

Analyze results:

sort -k2 -n startup.log | tail -20

13.5.2 Lazy Loading Techniques

Defer non-essential plugins:


-- Load after UI is ready
vim.defer_fn(function()
  require('plugins.non_essential')
end, 100)

Autoload filetype-specific plugins:

" Vimscript
autocmd FileType python packadd python-specific-plugin

-- Lua
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    require('python_specific_config')
  end,
})

13.5.3 Disable Built-in Plugins


-- Disable unused built-in plugins
vim.g.loaded_gzip = 1
vim.g.loaded_zip = 1
vim.g.loaded_zipPlugin = 1
vim.g.loaded_tar = 1
vim.g.loaded_tarPlugin = 1
vim.g.loaded_getscript = 1
vim.g.loaded_getscriptPlugin = 1
vim.g.loaded_vimball = 1
vim.g.loaded_vimballPlugin = 1
vim.g.loaded_2html_plugin = 1
vim.g.loaded_logiPat = 1
vim.g.loaded_rrhelper = 1
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1
vim.g.loaded_netrwSettings = 1
vim.g.loaded_netrwFileHandlers = 1

13.5.4 Optimize Options


-- Performance settings
vim.opt.updatetime = 250        -- Faster completion
vim.opt.timeoutlen = 300        -- Faster key sequence timeout
vim.opt.lazyredraw = true       -- Don't redraw during macros
vim.opt.synmaxcol = 240         -- Don't highlight long lines
vim.opt.re = 0                  -- Use newer regex engine


-- Disable unnecessary features
vim.opt.backup = false
vim.opt.writebackup = false
vim.opt.swapfile = false

13.6 Advanced Configuration Patterns

13.6.1 Conditional Configuration

Load different configs based on environment:


-- Detect environment
local is_work = vim.fn.isdirectory(vim.fn.expand('~/work')) == 1
local is_wsl = vim.fn.has('wsl') == 1
local is_gui = vim.fn.has('gui_running') == 1

if is_work then
  require('work_config')
else
  require('personal_config')
end

if is_wsl then
  vim.opt.clipboard = 'unnamedplus'
  vim.g.clipboard = {
    name = 'WslClipboard',
    copy = {
      ['+'] = 'clip.exe',
      ['*'] = 'clip.exe',
    },
    paste = {
      ['+'] = 'powershell.exe -c [Console]::Out.Write($(Get-Clipboard -Raw).tostring().replace("`r", ""))',
      ['*'] = 'powershell.exe -c [Console]::Out.Write($(Get-Clipboard -Raw).tostring().replace("`r", ""))',
    },
    cache_enabled = 0,
  }
end

Machine-specific overrides:


-- ~/.config/nvim/init.lua
require('core.options')
require('core.keymaps')
require('plugins')


-- Load local overrides if they exist
local local_config = vim.fn.stdpath('config') .. '/lua/local.lua'
if vim.fn.filereadable(local_config) == 1 then
  dofile(local_config)
end

~/.config/nvim/lua/local.lua (gitignored):


-- Machine-specific settings
vim.opt.guifont = 'JetBrainsMono Nerd Font:h12'


-- Work-specific plugins
require('lazy').setup({
  { 'company/internal-plugin' },
})

13.6.2 Multi-File Configuration Loading

~/.config/nvim/lua/utils/helpers.lua:

local M = {}


-- Source all Lua files in a directory
function M.source_dir(dir)
  local config_path = vim.fn.stdpath('config') .. '/lua/' .. dir
  local files = vim.fn.glob(config_path .. '/*.lua', false, true)
  
  for _, file in ipairs(files) do
    local module = file:match('lua/(.+)%.lua$'):gsub('/', '.')
    require(module)
  end
end


-- Create autocommand helper
function M.augroup(name, commands)
  local group = vim.api.nvim_create_augroup(name, { clear = true })
  for _, command in ipairs(commands) do
    local event = command[1]
    local pattern = command[2]
    local callback = command[3]
    vim.api.nvim_create_autocmd(event, {
      group = group,
      pattern = pattern,
      callback = callback,
    })
  end
end


-- Keymap helper with descriptions
function M.map(mode, lhs, rhs, desc, opts)
  opts = opts or {}
  opts.desc = desc
  opts.noremap = opts.noremap == nil and true or opts.noremap
  opts.silent = opts.silent == nil and true or opts.silent
  vim.keymap.set(mode, lhs, rhs, opts)
end

return M

Usage:

local helpers = require('utils.helpers')


-- Load all configs from 'config' directory
helpers.source_dir('config')


-- Create autocommands
helpers.augroup('MyGroup', {
  { 'BufWritePre', '*.lua', function() vim.lsp.buf.format() end },
  { 'TextYankPost', '*', function() vim.highlight.on_yank() end },
})


-- Create keymaps
helpers.map('n', '<leader>w', ':w<CR>', 'Save file')
helpers.map('n', '<leader>q', ':q<CR>', 'Quit')

13.6.3 Dynamic Option Setting

Context-aware settings:


-- Adjust based on window size
vim.api.nvim_create_autocmd('VimResized', {
  callback = function()
    local width = vim.api.nvim_win_get_width(0)
    if width < 100 then
      vim.opt.number = false
      vim.opt.signcolumn = 'no'
    else
      vim.opt.number = true
      vim.opt.signcolumn = 'yes'
    end
  end,
})


-- Adjust based on file size
vim.api.nvim_create_autocmd('BufReadPre', {
  callback = function()
    local file_size = vim.fn.getfsize(vim.fn.expand('<afile>'))
    if file_size > 1024 * 1024 then  -- 1MB
      vim.opt_local.syntax = 'off'
      vim.opt_local.foldmethod = 'manual'
      print('Large file detected: syntax disabled')
    end
  end,
})

13.6.4 Project-Specific Settings

Using .nvim.lua or .exrc:


-- Enable reading local config files
vim.opt.exrc = true
vim.opt.secure = true  -- Restrict dangerous commands


-- ~/.config/nvim/init.lua

-- Load project-specific config
vim.api.nvim_create_autocmd('DirChanged', {
  callback = function()
    local project_config = vim.fn.getcwd() .. '/.nvim.lua'
    if vim.fn.filereadable(project_config) == 1 then
      dofile(project_config)
    end
  end,
})

Example project config (.nvim.lua):


-- Project-specific settings
vim.opt_local.tabstop = 4
vim.opt_local.shiftwidth = 4
vim.opt_local.textwidth = 100


-- Project-specific mappings
vim.keymap.set('n', '<leader>r', ':!npm run dev<CR>')
vim.keymap.set('n', '<leader>t', ':!npm test<CR>')


-- Add project directories to path
vim.opt.path:append('src/**')

13.7 Essential Settings Reference

13.7.1 Editor Behavior


-- Line numbers
vim.opt.number = true
vim.opt.relativenumber = true


-- Cursor
vim.opt.cursorline = true
vim.opt.scrolloff = 8        -- Keep 8 lines visible above/below cursor
vim.opt.sidescrolloff = 8    -- Keep 8 columns visible left/right


-- Mouse
vim.opt.mouse = 'a'


-- Clipboard integration
vim.opt.clipboard = 'unnamedplus'


-- Persistent undo
vim.opt.undofile = true
vim.opt.undodir = vim.fn.stdpath('data') .. '/undo'
vim.opt.undolevels = 10000


-- Split behavior
vim.opt.splitright = true
vim.opt.splitbelow = true


-- Command line
vim.opt.cmdheight = 1
vim.opt.showcmd = true
vim.opt.wildmode = { 'longest:full', 'full' }


-- Messages
vim.opt.shortmess:append('c')  -- Don't show completion messages
vim.opt.showmode = false       -- Don't show mode (use statusline)

13.7.2 Indentation and Formatting


-- Tabs
vim.opt.tabstop = 2
vim.opt.softtabstop = 2
vim.opt.shiftwidth = 2
vim.opt.expandtab = true


-- Auto-indenting
vim.opt.smartindent = true
vim.opt.autoindent = true
vim.opt.breakindent = true


-- Line wrapping
vim.opt.wrap = false
vim.opt.linebreak = true     -- Break at word boundaries


-- Text width
vim.opt.textwidth = 80
vim.opt.colorcolumn = '80'

13.7.3 Search and Replace


-- Search
vim.opt.ignorecase = true
vim.opt.smartcase = true
vim.opt.incsearch = true
vim.opt.hlsearch = true


-- Substitution
vim.opt.inccommand = 'split'  -- Show substitution preview


-- Grep program
vim.opt.grepprg = 'rg --vimgrep --no-heading --smart-case'
vim.opt.grepformat = '%f:%l:%c:%m'

13.7.4 Visual Appearance


-- Colors
vim.opt.termguicolors = true
vim.opt.background = 'dark'


-- UI elements
vim.opt.signcolumn = 'yes'
vim.opt.laststatus = 3       -- Global statusline
vim.opt.pumheight = 10       -- Popup menu height
vim.opt.pumblend = 10        -- Popup transparency
vim.opt.winblend = 0         -- Window transparency


-- Concealment
vim.opt.conceallevel = 0


-- Fill characters
vim.opt.fillchars = {
  fold = ' ',
  eob = ' ',        -- End of buffer
  diff = '╱',
  foldsep = ' ',
  foldopen = '',
  foldclose = '',
}

13.7.5 Performance


-- Faster updates
vim.opt.updatetime = 250
vim.opt.timeoutlen = 300


-- Redraw optimization
vim.opt.lazyredraw = true


-- Syntax
vim.opt.synmaxcol = 240      -- Don't highlight long lines


-- Folds
vim.opt.foldmethod = 'expr'
vim.opt.foldexpr = 'nvim_treesitter#foldexpr()'
vim.opt.foldenable = false   -- Start with folds open


-- Complete options
vim.opt.completeopt = { 'menu', 'menuone', 'noselect' }

13.8 Configuration Best Practices

13.8.1 Version Control

Recommended .gitignore:

# Plugin directories
plugged/
lazy-lock.json
plugin/packer_compiled.lua

# Local overrides
lua/local.lua

# Generated files

*.log
.netrwhist
undodir/

# OS files
.DS_Store
Thumbs.db

Track with Git:

cd ~/.config/nvim
git init
git add init.lua lua/
git commit -m "Initial Neovim configuration"

# Push to remote
git remote add origin https://github.com/username/nvim-config.git
git push -u origin main

13.8.2 Documentation

Add comments explaining non-obvious choices:


-- Use space as leader for thumb accessibility
vim.g.mapleader = ' '


-- Relative numbers for efficient motion (5j, 10k)
vim.opt.relativenumber = true


-- Persist undo history across sessions

-- Location: ~/.local/share/nvim/undo/
vim.opt.undofile = true
vim.opt.undodir = vim.fn.stdpath('data') .. '/undo'


-- Intelligent case search:

-- 'foo' matches Foo/foo/FOO

-- 'Foo' only matches Foo
vim.opt.ignorecase = true
vim.opt.smartcase = true

13.8.3 Portability

Make config work across machines:


-- Detect OS
local is_windows = vim.fn.has('win32') == 1
local is_mac = vim.fn.has('mac') == 1
local is_linux = vim.fn.has('unix') == 1 and not is_mac


-- OS-specific settings
if is_windows then
  vim.opt.shell = 'pwsh'
  vim.opt.shellcmdflag = '-NoLogo -NoProfile -ExecutionPolicy RemoteSigned -Command'
elseif is_mac then
  vim.opt.clipboard = 'unnamed'  -- Use system clipboard
end


-- Check for executables before configuring
if vim.fn.executable('rg') == 1 then
  vim.opt.grepprg = 'rg --vimgrep'
end

if vim.fn.executable('fd') == 1 then
  vim.env.FZF_DEFAULT_COMMAND = 'fd --type f'
end

13.8.4 Progressive Enhancement

Start minimal, add gradually:


-- ~/.config/nvim/init.lua

-- Core (always loaded)
require('core.options')
require('core.keymaps')


-- Plugins (optional)
local plugin_ok, _ = pcall(require, 'plugins')
if not plugin_ok then
  vim.notify('Plugins not loaded - run :Lazy install', vim.log.levels.WARN)
  return
end


-- Advanced features (graceful degradation)
if vim.fn.has('nvim-0.9') == 1 then
  require('advanced.features')
end

13.8.5 Testing Configuration Changes

Create temporary config:

# Test new config without affecting current setup
NVIM_APPNAME=nvim-test nvim

# Locations:
# Config: ~/.config/nvim-test/
# Data:   ~/.local/share/nvim-test/

Bisect problematic changes:


-- Binary search for issue

-- Comment out half of config, test, repeat


-- Enable debug logging
vim.cmd('set verbose=9')
vim.cmd('set verbosefile=~/.config/nvim/debug.log')

Chapter Summary

This chapter explored Vim/Neovim configuration comprehensively:

Configuration Fundamentals:

  • File locations and naming conventions

  • Vimscript vs. Lua approaches

  • Loading order and precedence

  • Minimal viable configurations

Modular Organization:

  • Splitting config into logical modules

  • Directory structures for maintainability

  • Vimscript runtime path

  • Lua module system

Plugin Management:

  • vim-plug: Simple, universal

  • lazy.nvim: Modern, performance-focused

  • Packer: Lua-native (legacy)

  • Lazy-loading strategies

Performance Optimization:

  • Startup time profiling

  • Lazy-loading techniques

  • Disabling unused built-in plugins

  • Optimizing runtime options

Advanced Patterns:

  • Conditional configuration (environment-aware)

  • Multi-file loading systems

  • Dynamic option adjustment

  • Project-specific settings

  • Helper function libraries

Essential Settings:

  • Editor behavior (cursor, mouse, clipboard)

  • Indentation and formatting

  • Search and replace

  • Visual appearance

  • Performance tuning

Best Practices:

  • Version control workflows

  • Documentation standards

  • Cross-platform portability

  • Progressive enhancement

  • Safe testing procedures

Key Insights:

  • Start minimal, expand based on needs

  • Modularize for maintainability

  • Lazy-load for performance

  • Document non-obvious choices

  • Make portable across environments

  • Version control configuration

  • Test changes in isolation

  • Use helpers to reduce boilerplate

  • Prefer Lua for Neovim (performance)

  • Keep machine-specific settings separate

Configuration Philosophy:

  1. Simplicity: Don’t add what you don’t use

  2. Performance: Lazy-load everything possible

  3. Maintainability: Clear structure and documentation

  4. Portability: Work across machines/OSes

  5. Evolution: Configuration grows with expertise

Your vimrc/init.lua is a living document—it should evolve as your workflow matures. Start with basics, add features as you encounter limitations, and ruthlessly remove what you don’t use. A well-crafted configuration transforms Vim from a text editor into a personalized development environment perfectly suited to your workflow.

In the next chapter, we’ll explore Marks and Jumps, covering position tracking, cross-file navigation, and movement history management for efficient codebase exploration.


Chapter 14: Options and Settings

Vim’s power lies not just in its commands, but in its configurability. This chapter provides a comprehensive reference to Vim’s options system, covering how to set, query, and manipulate settings that control every aspect of editor behavior.

14.1 Understanding the Options System

14.1.1 Option Types

Vim options fall into three categories:

Boolean Options:

" Enable/disable
set number          " Enable line numbers
set nonumber        " Disable line numbers
set number!         " Toggle line numbers
set invnumber       " Toggle (alternative syntax)

" Query state
set number?         " Shows 'number' or 'nonumber'

Numeric Options:

" Set value
set tabstop=4
set shiftwidth=2
set history=1000

" Query value
set tabstop?        " Shows 'tabstop=4'

String Options:

" Set value
set backspace=indent,eol,start
set wildmode=longest:full,full
set listchars=tab:▸\ ,trail:·

" Query value
set backspace?      " Shows current value

14.1.2 Option Scope

Global Options: Affect all buffers and windows.

set background=dark
set updatetime=300

Local Options: Specific to buffer or window.

" Buffer-local
setlocal tabstop=2
setlocal textwidth=80

" Window-local
setlocal wrap
setlocal cursorline

Global-Local Options: Have both global default and local override.

" Set global default
setglobal tabstop=4

" Set local override
setlocal tabstop=2

" Query both
set tabstop?        " Local value
setglobal tabstop?  " Global default

14.1.3 Lua API for Options (Neovim)


-- Set options
vim.opt.number = true
vim.opt.tabstop = 4
vim.opt.wildmode = { 'longest:full', 'full' }


-- Append/remove from list
vim.opt.runtimepath:append('~/.config/nvim/custom')
vim.opt.shortmess:append('c')
vim.opt.shortmess:remove('F')


-- Buffer-local
vim.opt_local.tabstop = 2


-- Window-local
vim.wo.cursorline = true


-- Global
vim.go.updatetime = 250


-- Get option value
print(vim.opt.tabstop:get())

14.2 Display and Interface Options

14.2.1 Line Numbers and Columns

" Line numbers
set number                  " Show absolute line numbers
set relativenumber          " Show relative line numbers
set numberwidth=4           " Width of number column (default: 4)

" Sign column (for marks, diagnostics)
set signcolumn=yes          " Always show
set signcolumn=auto         " Show when signs present
set signcolumn=number       " Merge with number column (Neovim)

" Color column (visual guide)
set colorcolumn=80          " Single column
set colorcolumn=80,120      " Multiple columns
set colorcolumn=+1          " One column after 'textwidth'

Lua equivalent:

vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.numberwidth = 4
vim.opt.signcolumn = 'yes'
vim.opt.colorcolumn = '80'

14.2.2 Cursor Display

" Cursor line/column
set cursorline              " Highlight current line
set nocursorline            " Disable highlight
set cursorcolumn            " Highlight current column

" Cursor shape (Neovim)
set guicursor=n-v-c:block-Cursor/lCursor
set guicursor+=i-ci-ve:ver25-Cursor
set guicursor+=r-cr-o:hor20-Cursor

" Cursor blink
set guicursor+=a:blinkon500-blinkoff500

Lua cursor configuration:

vim.opt.cursorline = true
vim.opt.guicursor = {
  'n-v-c:block-Cursor/lCursor',
  'i-ci-ve:ver25-Cursor',
  'r-cr-o:hor20-Cursor',
  'a:blinkon500-blinkoff500'
}

14.2.3 Scrolling Behavior

" Keep cursor centered
set scrolloff=8             " Minimum lines above/below cursor
set sidescrolloff=8         " Minimum columns left/right

" Smooth scrolling
set scroll=10               " Lines to scroll with C-d/C-u
set scrolljump=5            " Lines to jump when cursor leaves screen

" Horizontal scrolling
set sidescroll=1            " Columns to scroll horizontally
set nowrap                  " Don't wrap long lines

Context-aware scrolloff:


-- Dynamic scrolloff based on window height
vim.api.nvim_create_autocmd('VimResized', {
  callback = function()
    local height = vim.api.nvim_win_get_height(0)
    vim.opt.scrolloff = math.floor(height / 4)
  end,
})

14.2.4 Whitespace and Special Characters

" Show invisible characters
set list                    " Enable
set nolist                  " Disable

" Define what to show
set listchars=tab:▸\ ,trail:·,nbsp:␣,extends:›,precedes:‹
set listchars+=eol:¬        " Show end-of-line

" Fill characters
set fillchars=vert:│,fold:─,diff:─
set fillchars+=eob:~        " End of buffer character

" Show tabs and trailing spaces only
set listchars=tab:»\ ,trail:·

Conditional list display:

" Show whitespace in specific filetypes
autocmd FileType python,javascript setlocal list
autocmd FileType markdown setlocal nolist

14.2.5 Window Splitting

" Split direction
set splitright              " Vertical splits go right
set splitbelow              " Horizontal splits go below

" Equalize splits
set equalalways             " Auto-equalize on resize
set noequalalways           " Manual control

" Split sizes
set winwidth=84             " Minimum width for current window
set winheight=10            " Minimum height for current window
set winminwidth=5           " Minimum width for any window
set winminheight=1          " Minimum height for any window

14.2.6 Status and Command Line

" Status line
set laststatus=2            " Always show (all windows)
set laststatus=3            " Global statusline (Neovim)
set laststatus=0            " Never show

" Command line
set cmdheight=1             " Height of command line
set cmdheight=0             " Hide when not in use (Neovim)
set showcmd                 " Show partial commands
set noshowcmd               " Don't show

" Mode display
set showmode                " Show current mode
set noshowmode              " Hide mode (use statusline)

" Messages
set shortmess=atI           " Abbreviate messages
set shortmess+=c            " Don't show completion messages
set shortmess+=F            " Don't show file info when editing

Shortmess flags: a: All abbreviations t: Truncate file messages I: No intro message c: No completion messages F: No file info W: No [w] when writing A: No attention messages

14.3 Editing Behavior Options

14.3.1 Indentation

" Tab settings
set tabstop=4               " Tab display width
set softtabstop=4           " Tab insert width
set shiftwidth=4            " Indent width
set expandtab               " Use spaces instead of tabs
set noexpandtab             " Use actual tabs

" Auto-indent
set autoindent              " Copy indent from current line
set smartindent             " Smart auto-indenting
set cindent                 " C-style indenting
set indentexpr=GetPythonIndent()  " Custom indent expression

" Indent rounding
set shiftround              " Round indent to shiftwidth multiple

" Backspace behavior
set backspace=indent,eol,start  " Allow backspacing over everything

Filetype-specific indentation:

autocmd FileType python setlocal ts=4 sw=4 sts=4 et
autocmd FileType javascript setlocal ts=2 sw=2 sts=2 et
autocmd FileType go setlocal ts=4 sw=4 sts=4 noet

Lua approach:

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    vim.opt_local.tabstop = 4
    vim.opt_local.shiftwidth = 4
    vim.opt_local.softtabstop = 4
    vim.opt_local.expandtab = true
  end,
})

14.3.2 Line Wrapping and Formatting

" Visual wrapping
set wrap                    " Wrap long lines
set nowrap                  " Don't wrap
set linebreak               " Break at word boundaries
set breakindent             " Indent wrapped lines
set breakindentopt=shift:2  " Extra indent for wrapped lines

" Show wrap indicator
set showbreak=↪\ 

" Text width
set textwidth=80            " Auto-wrap at 80 characters
set textwidth=0             " Disable auto-wrap

" Format options
set formatoptions=tcqrn1
" t: Auto-wrap text using textwidth
" c: Auto-wrap comments
" q: Allow formatting comments with 'gq'
" r: Auto-insert comment leader after <CR>
" n: Recognize numbered lists
" 1: Don't break line after one-letter word

" Add/remove format options
set formatoptions+=j        " Remove comment leader when joining
set formatoptions-=o        " Don't insert comment leader with 'o'/'O'

Lua format options:

vim.opt.formatoptions = {
  t = true,  -- Auto-wrap text
  c = true,  -- Auto-wrap comments
  q = true,  -- Allow gq formatting
  r = true,  -- Continue comments
  n = true,  -- Recognize numbered lists
  j = true,  -- Remove comment leader when joining
}

14.3.3 Joining and Deletion

" Join behavior
set nojoinspaces            " Don't insert two spaces after punctuation

" Virtual editing
set virtualedit=block       " Allow cursor beyond EOL in visual block
set virtualedit=all         " Allow everywhere
set virtualedit=onemore     " Allow one char beyond EOL

" Deletion
set backspace=indent,eol,start  " Allow backspace over everything

14.3.4 Insert Mode Options

" Timeout for key sequences
set timeout                 " Enable timeout
set timeoutlen=500          " Timeout in ms (default: 1000)
set ttimeoutlen=10          " Key code timeout (faster Esc)

" Insert completion
set completeopt=menu,menuone,noselect
" menu:     Use popup menu
" menuone:  Show menu even for single match
" noselect: Don't auto-select
" preview:  Show preview window

" Popup menu
set pumheight=15            " Maximum items in popup
set pumwidth=20             " Minimum width
set pumblend=10             " Transparency (0-100, Neovim)

14.4 Search and Pattern Matching

14.4.1 Search Behavior

" Case sensitivity
set ignorecase              " Case-insensitive search
set smartcase               " Case-sensitive if uppercase present
set noignorecase            " Always case-sensitive

" Search highlighting
set hlsearch                " Highlight matches
set nohlsearch              " No highlighting
set incsearch               " Incremental search (show while typing)

" Search wrapping
set wrapscan                " Wrap around file
set nowrapscan              " Stop at end of file

" Show match count (Neovim)
set shortmess-=S            " Show search count in messages

Live substitution preview:

set inccommand=split        " Show preview in split (Neovim)
set inccommand=nosplit      " Show preview inline

14.4.2 Pattern Matching

" Magic mode (regex behavior)
set magic                   " Default regex mode
set nomagic                 " All chars except ^$ are literal

" Regex engine
set regexpengine=0          " Auto-select (default)
set regexpengine=1          " Old engine
set regexpengine=2          " NFA engine (faster)

" Max column for syntax highlighting
set synmaxcol=200           " Don't highlight beyond column 200

14.4.3 Grep Integration

" External grep program
set grepprg=grep\ -n\ $*\ /dev/null
set grepprg=rg\ --vimgrep\ --no-heading\ --smart-case

" Grep format
set grepformat=%f:%l:%c:%m,%f:%l:%m

Lua configuration:


-- Use ripgrep if available
if vim.fn.executable('rg') == 1 then
  vim.opt.grepprg = 'rg --vimgrep --no-heading --smart-case'
  vim.opt.grepformat = '%f:%l:%c:%m'
end

14.5 File and Buffer Options

14.5.1 File Handling

" File encoding
set encoding=utf-8          " Internal encoding
set fileencoding=utf-8      " File encoding when saving
set fileencodings=utf-8,latin1  " Encodings to try when reading

" File format (line endings)
set fileformat=unix         " Use LF
set fileformat=dos          " Use CRLF
set fileformats=unix,dos,mac  " Formats to try

" Auto-read/write
set autoread                " Reload file if changed outside Vim
set autowrite               " Auto-write before commands like :next
set autowriteall            " Auto-write on more events

" File detection
filetype on                 " Enable filetype detection
filetype plugin on          " Enable filetype plugins
filetype indent on          " Enable filetype-based indentation

14.5.2 Backup, Swap, and Undo

" Backup files
set backup                  " Keep backup file
set nobackup                " Don't keep backup
set writebackup             " Make backup before overwriting
set backupdir=~/.vim/backup " Backup directory
set backupext=.bak          " Backup file extension

" Swap files
set swapfile                " Use swap file
set noswapfile              " Disable swap
set directory=~/.vim/swap   " Swap file directory
set updatecount=200         " Write swap after N characters
set updatetime=4000         " Write swap after N ms idle

" Persistent undo
set undofile                " Enable persistent undo
set noundofile              " Disable
set undodir=~/.vim/undo     " Undo file directory
set undolevels=1000         " Max undo levels
set undoreload=10000        " Max lines to save for undo on reload

Lua setup with auto-create directories:


-- Create directories if they don't exist
local data_dir = vim.fn.stdpath('data')

local function ensure_dir(dir)
  if vim.fn.isdirectory(dir) == 0 then
    vim.fn.mkdir(dir, 'p')
  end
  return dir
end

vim.opt.backup = false
vim.opt.writebackup = false
vim.opt.swapfile = false

vim.opt.undofile = true
vim.opt.undodir = ensure_dir(data_dir .. '/undo')
vim.opt.undolevels = 10000

14.5.3 Buffer Behavior

" Hidden buffers
set hidden                  " Allow hidden buffers with unsaved changes
set nohidden                " Require saving before hiding

" Buffer switching
set switchbuf=useopen       " Jump to open window if buffer visible
set switchbuf+=usetab       " Include other tabs in search

" Write behavior
set write                   " Allow writing files
set nowrite                 " Prevent writing

14.5.4 Session and View Options

" Session save options
set sessionoptions=blank,buffers,curdir,folds,help,tabpages,winsize
set sessionoptions+=terminal  " Save terminal buffers (Neovim)

" View save options
set viewoptions=folds,cursor,curdir

14.6 Completion and Wildmenu

14.6.1 Command-Line Completion

" Enable wildmenu
set wildmenu                " Visual command completion
set nowildmenu              " Disable

" Completion mode
set wildmode=full           " Complete first full match
set wildmode=longest        " Complete longest common string
set wildmode=longest:full   " Complete longest, then full
set wildmode=list           " List all matches
set wildmode=longest:full,full  " Longest then cycle through full

" Ignore patterns
set wildignore=*.o,*.obj,*.pyc
set wildignore+=*/.git/*,*/node_modules/*
set wildignore+=*.jpg,*.png,*.gif

" Completion options
set wildoptions=pum         " Use popup menu (Neovim)
set wildoptions+=fuzzy      " Fuzzy matching (Neovim 0.10+)

Lua wildignore patterns:

vim.opt.wildignore = {
  '*.o', '*.obj', '*.pyc',
  '*/.git/*', '*/node_modules/*',
  '*.jpg', '*.png', '*.gif',
  '*.swp', '*.bak',
}

14.6.2 Insert Mode Completion

" Completion sources
set complete=.,w,b,u,t,i
" .: Current buffer
" w: Other windows
" b: Other buffers
" u: Unloaded buffers
" t: Tags
" i: Current and included files

" Completion options
set completeopt=menu,menuone,preview
set completeopt=menu,menuone,noselect  " Don't auto-select

" Dictionary/thesaurus
set dictionary=/usr/share/dict/words
set thesaurus=~/.vim/thesaurus.txt

" Scan depth
for completion
set complete=.,w,b,u,t
set completeopt=menu,menuone,noselect
set completeopt+=preview         " add preview


**Lua equivalent:**
lua
vim.opt.complete = { '.', 'w', 'b', 'u', 't' }
vim.opt.completeopt = { 'menu', 'menuone', 'noselect', 'preview' }

## 14.7 Reference Tables

| Category | Common Options | Purpose |
|-----------|----------------|----------|
| **Editor Behavior** | `scrolloff`, `hidden`, `switchbuf`, `updatetime` | Control general editing and buffer behavior |
| **Indentation** | `tabstop`, `shiftwidth`, `expandtab`, `smartindent` | Manage indentation style |
| **Search** | `ignorecase`, `smartcase`, `hlsearch`, `incsearch`, `inccommand` | Configure searching behavior |
| **Visual Appearance** | `number`, `relativenumber`, `signcolumn`, `cursorline`, `colorcolumn`, `laststatus` | Display and interface customization |
| **Performance** | `lazyredraw`, `ttyfast`, `synmaxcol`, `updatetime` | Speed and optimization settings |


**Boolean, Numeric, and String Value Summary**

| Type | Example | Description |
|------|----------|-------------|
| Boolean | `set wrap` / `set nowrap` | True/False toggle |
| Numeric | `set tabstop=4` | Integer-based setting |
| String | `set listchars=tab:▸\ ,trail:·` | Textual setting (commas separate values) |

## 14.8 Best Practices

### 14.8.1 Configuration Tracking
Keep your configuration version-controlled:
bash
$ git init ~/.config/nvim
$ echo 'plugged/' >> ~/.config/nvim/.gitignore
$ echo 'undodir/' >> ~/.config/nvim/.gitignore
$ echo 'lua/local.lua' >> ~/.config/nvim/.gitignore

### 14.8.2 Documentation
Document non-obvious settings inside your config:
vim
" Ensure caching only updates on idle
set updatetime=250  " default 4000ms

### 14.8.3 Portability
When using plugins or features, test gracefully:
lua
if vim.fn.has('unix') == 1 then
  vim.opt.shell = 'bash'
end

if vim.fn.executable('rg') == 1 then
  vim.opt.grepprg = 'rg --vimgrep'
end

### 14.8.4 Configuration Testing
Create temporary isolated environments to test:
bash
$ NVIM_APPNAME=nvim-test nvim
This loads a dedicated config tree at `~/.local/share/nvim-test`.


---

✅ **End of Chapter 14: Options and Settings**

Next up in the sequence is **Chapter #15: Marks and Jumps** — focusing on motion history, jump lists, marks (local and global), and efficient navigation through code or documents.

---

# Chapter 15: Key Mappings

Key mappings are the foundation of personalizing Vim to match your workflow. This chapter explores Vim's mapping system in depth, from basic remaps to complex mode-specific mappings, providing you with the knowledge to craft an efficient, personalized editing environment.

## 15.1 Understanding the Mapping System

### 15.1.1 Basic Mapping Syntax

The fundamental mapping commands follow this pattern:

```vim
{mode}{nore}map {lhs} {rhs}

Where:

  • {mode}: Specifies which mode(s) the mapping applies to

  • {nore}: Optional - prevents recursive mapping

  • {lhs}: Left-hand side - the keys you press

  • {rhs}: Right-hand side - the action performed

Simple example:

" Map jk to escape in insert mode
inoremap jk <Esc>

" Map space to fold toggle in normal mode
nnoremap <Space> za

15.1.2 Mapping Modes

Command Modes Description
map Normal, Visual, Select, Operator-pending General mapping
nmap Normal Normal mode only
vmap Visual, Select Visual and Select modes
xmap Visual Visual mode only
smap Select Select mode only
imap Insert Insert mode
cmap Command-line Command-line mode
omap Operator-pending After an operator (like d, c)
tmap Terminal Terminal mode (Neovim)
lmap Insert, Command-line, Lang-Arg Language mappings

Mode-specific examples:

" Normal mode: quick save
nnoremap <C-s> :w<CR>

" Visual mode: surround with quotes
xnoremap <leader>q c"<C-r>""<Esc>

" Insert mode: auto-complete parentheses
inoremap ( ()<Left>

" Operator-pending: operate on entire function
onoremap af :<C-u>normal! gg0vG$<CR>

" Terminal mode: easy escape (Neovim)
tnoremap <Esc> <C-\><C-n>

15.1.3 Recursive vs Non-Recursive

Recursive mappings (map):

" Can trigger other mappings
nmap x dd
nmap y x  " y will trigger dd (through x)

Non-recursive mappings (noremap):

" Direct, no expansion
nnoremap x dd
nnoremap y x  " y will only trigger literal x key

Best practice: Always prefer noremap variants unless you specifically need recursion.

" Bad - could cause unexpected behavior
imap jk <Esc>
nmap <Space> :echo "pressed"<CR>

" Good - explicit and safe
inoremap jk <Esc>
nnoremap <Space> :echo "pressed"<CR>

15.2 Special Keys and Notation

15.2.1 Special Key Codes

<CR>        " Enter/Return
<Esc>       " Escape
<Space>     " Space bar
<Tab>       " Tab
<BS>        " Backspace
<Del>       " Delete
<Up>        " Up arrow
<Down>      " Down arrow
<Left>      " Left arrow
<Right>     " Right arrow
<Home>      " Home
<End>       " End
<PageUp>    " Page Up
<PageDown>  " Page Down
<F1>-<F12>  " Function keys
<Insert>    " Insert

15.2.2 Modifier Keys

<C-x>       " Ctrl+x
<M-x>       " Alt+x (Meta)
<S-x>       " Shift+x
<A-x>       " Alt+x (alternative)
<D-x>       " Cmd+x (macOS GUI)

" Combinations
<C-S-x>     " Ctrl+Shift+x
<C-M-x>     " Ctrl+Alt+x

Practical examples:

" Control combinations
nnoremap <C-h> <C-w>h  " Navigate left window
nnoremap <C-j> <C-w>j  " Navigate down window
nnoremap <C-k> <C-w>k  " Navigate up window
nnoremap <C-l> <C-w>l  " Navigate right window

" Alt combinations (terminal-dependent)
nnoremap <M-j> :m .+1<CR>==    " Move line down
nnoremap <M-k> :m .-2<CR>==    " Move line up

" Function keys
nnoremap <F2> :set invpaste<CR>
nnoremap <F3> :set invnumber<CR>
nnoremap <F4> :set invrelativenumber<CR>

15.2.3 Special Sequences

<Leader>    " Leader key (default: \)
<LocalLeader> " Local leader (filetype-specific)
<silent>    " Don't echo command
<expr>      " Evaluate as expression
<buffer>    " Buffer-local mapping
<nowait>    " Don't wait for more keys
<script>    " Only use script-local mappings
<unique>    " Fail if mapping already exists

15.3 Leader Key Mappings

15.3.1 Configuring the Leader

" Set leader before any leader mappings
let mapleader = " "        " Space as leader
let maplocalleader = ","   " Comma as local leader

Lua configuration:

vim.g.mapleader = ' '
vim.g.maplocalleader = ','

15.3.2 Leader-Based Workflows

" File operations
nnoremap <leader>w :w<CR>
nnoremap <leader>q :q<CR>
nnoremap <leader>x :x<CR>

" Buffer management
nnoremap <leader>bd :bdelete<CR>
nnoremap <leader>bn :bnext<CR>
nnoremap <leader>bp :bprevious<CR>
nnoremap <leader>bl :buffers<CR>

" Window operations
nnoremap <leader>v :vsplit<CR>
nnoremap <leader>s :split<CR>
nnoremap <leader>c :close<CR>
nnoremap <leader>o :only<CR>

" Search and navigation
nnoremap <leader>h :nohlsearch<CR>
nnoremap <leader>f :find 
nnoremap <leader>b :buffer 

" Quick edit config
nnoremap <leader>ev :edit $MYVIMRC<CR>
nnoremap <leader>sv :source $MYVIMRC<CR>

15.3.3 Organized Leader Mappings

Group related functionality with mnemonic prefixes:

" Code operations (c prefix)
nnoremap <leader>cf :lua vim.lsp.buf.format()<CR>
nnoremap <leader>cr :lua vim.lsp.buf.rename()<CR>
nnoremap <leader>ca :lua vim.lsp.buf.code_action()<CR>

" Git operations (g prefix)
nnoremap <leader>gs :Git status<CR>
nnoremap <leader>gc :Git commit<CR>
nnoremap <leader>gp :Git push<CR>
nnoremap <leader>gl :Git log<CR>

" Toggle options (t prefix)
nnoremap <leader>tn :set invnumber<CR>
nnoremap <leader>tr :set invrelativenumber<CR>
nnoremap <leader>tw :set invwrap<CR>
nnoremap <leader>tl :set invlist<CR>

" Search/Find (f prefix)
nnoremap <leader>ff :Files<CR>
nnoremap <leader>fg :Rg<CR>
nnoremap <leader>fb :Buffers<CR>
nnoremap <leader>fh :History<CR>

15.4 Buffer-Local and Filetype Mappings

15.4.1 Buffer-Local Mappings

" In specific buffer
nnoremap <buffer> <leader>r :!python %<CR>

" Auto-command approach
autocmd FileType python nnoremap <buffer> <F5> :!python %<CR>
autocmd FileType javascript nnoremap <buffer> <F5> :!node %<CR>

Lua approach:


-- In ftplugin/python.lua
vim.keymap.set('n', '<F5>', ':!python %<CR>', { buffer = true })


-- Or with autocmd
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    vim.keymap.set('n', '<F5>', ':!python %<CR>', { buffer = true })
  end,
})

15.4.2 Filetype-Specific Mappings

Create dedicated filetype plugin files:

~/.vim/after/ftplugin/markdown.vim:

" Markdown-specific mappings
nnoremap <buffer> <leader>p :MarkdownPreview<CR>
nnoremap <buffer> <leader>b :! pandoc % -o %:r.pdf<CR>
inoremap <buffer> ;b ****<Left><Left>
inoremap <buffer> ;i **<Left>
inoremap <buffer> ;c ``<Left>

~/.config/nvim/after/ftplugin/lua.lua:


-- Lua-specific mappings
vim.keymap.set('n', '<leader>r', ':luafile %<CR>', { buffer = true })
vim.keymap.set('n', '<leader>t', ':lua require("plenary.test").test_file()<CR>', { buffer = true })

~/.vim/after/ftplugin/go.vim:

" Go-specific mappings
nnoremap <buffer> <leader>b :GoBuild<CR>
nnoremap <buffer> <leader>r :GoRun<CR>
nnoremap <buffer> <leader>t :GoTest<CR>
nnoremap <buffer> <leader>c :GoCoverageToggle<CR>

15.5 Advanced Mapping Techniques

15.5.1 Expression Mappings

Use <expr> to evaluate Vim expressions:

" Smart tab completion
inoremap <expr> <Tab> pumvisible() ? "\<C-n>" : "\<Tab>"
inoremap <expr> <S-Tab> pumvisible() ? "\<C-p>" : "\<S-Tab>"

" Conditional mapping based on context
inoremap <expr> <CR> pumvisible() ? "\<C-y>" : "\<CR>"

" Smart home key
nnoremap <expr> <Home> col('.') == 1 ? '^' : '0'

" Context-aware paste
inoremap <expr> <C-v> getline('.')[col('.')-2] ==# ' ' ? '<C-r>"' : ' <C-r>"'

Lua expression mappings:


-- Smart tab completion
vim.keymap.set('i', '<Tab>', function()
  return vim.fn.pumvisible() == 1 and '<C-n>' or '<Tab>'
end, { expr = true })


-- Smart enter
vim.keymap.set('i', '<CR>', function()
  return vim.fn.pumvisible() == 1 and '<C-y>' or '<CR>'
end, { expr = true })

15.5.2 Silent Mappings

Suppress command echo with <silent>:

" Shows ":write" in command line
nnoremap <leader>w :write<CR>

" Silent - no echo
nnoremap <silent> <leader>w :write<CR>

" Useful for complex commands
nnoremap <silent> <leader>d :lua vim.diagnostic.open_float()<CR>

15.5.3 Script-Local Mappings

Use <script> to restrict remapping to script-local mappings only:

nnoremap <script> <SID>MyFunc :call <SID>LocalFunction()<CR>
nnoremap <unique> <script> <Plug>MyPlugin :call <SID>PluginFunc()<CR>

15.5.4 Mapping with Arguments

" Command with arguments
nnoremap <leader>g :Rg<Space>

" Pre-filled search
nnoremap <leader>* :Rg <C-r><C-w><CR>

" Visual mode - search selected text
xnoremap <leader>* y:Rg <C-r>"<CR>

15.5.5 Multi-Line Mappings

" Using bar separator
nnoremap <leader>ev :edit $MYVIMRC<CR>

" Using continuation
nnoremap <leader>sv :source $MYVIMRC
    \ \| echo "Vimrc reloaded!"<CR>

" Complex command
nnoremap <leader>p :let @+ = expand("%:p")
    \ \| echo "Copied: " . @+<CR>

15.6 Common Mapping Patterns

15.6.1 Window Navigation

" Simplified window movement
nnoremap <C-h> <C-w>h
nnoremap <C-j> <C-w>j
nnoremap <C-k> <C-w>k
nnoremap <C-l> <C-w>l

" Window resizing
nnoremap <C-Up> :resize +2<CR>
nnoremap <C-Down> :resize -2<CR>
nnoremap <C-Left> :vertical resize -2<CR>
nnoremap <C-Right> :vertical resize +2<CR>

" Window creation
nnoremap <leader>v :vsplit<CR>
nnoremap <leader>s :split<CR>

15.6.2 Buffer Navigation

" Quick buffer switching
nnoremap <leader>bn :bnext<CR>
nnoremap <leader>bp :bprevious<CR>
nnoremap <leader>bd :bdelete<CR>
nnoremap <leader>bl :buffers<CR>:buffer<Space>

" Jump to specific buffer
nnoremap <leader>1 :buffer 1<CR>
nnoremap <leader>2 :buffer 2<CR>
nnoremap <leader>3 :buffer 3<CR>

15.6.3 Search Enhancements

" Clear search highlight
nnoremap <Esc><Esc> :nohlsearch<CR>
nnoremap <leader>h :nohlsearch<CR>

" Search and replace current word
nnoremap <leader>r :%s/\<<C-r><C-w>\>//g<Left><Left>

" Search visual selection
xnoremap * y/\V<C-r>"<CR>
xnoremap # y?\V<C-r>"<CR>

" Center search results
nnoremap n nzzzv
nnoremap N Nzzzv

15.6.4 Text Manipulation

" Move lines up/down
nnoremap <M-j> :m .+1<CR>==
nnoremap <M-k> :m .-2<CR>==
vnoremap <M-j> :m '>+1<CR>gv=gv
vnoremap <M-k> :m '<-2<CR>gv=gv

" Duplicate line/selection
nnoremap <leader>d yyp
vnoremap <leader>d y`>p

" Join without spaces
nnoremap <leader>J gJ

" Toggle case of word
nnoremap <leader>u viwU<Esc>
nnoremap <leader>l viwu<Esc>

15.6.5 Quick Edits

" Insert line above/below without leaving normal mode
nnoremap <leader>o o<Esc>
nnoremap <leader>O O<Esc>

" Append semicolon/comma to end of line
nnoremap <leader>; A;<Esc>
nnoremap <leader>, A,<Esc>

" Delete without yanking
nnoremap <leader>d "_d
vnoremap <leader>d "_d

" Paste without yanking in visual mode
xnoremap <leader>p "_dP

15.7 Lua Mapping API (Neovim)

15.7.1 Basic Lua Mappings


-- vim.keymap.set(mode, lhs, rhs, opts)
vim.keymap.set('n', '<leader>w', ':w<CR>')


-- Multiple modes
vim.keymap.set({'n', 'v'}, '<leader>y', '"+y')


-- With options
vim.keymap.set('n', '<leader>f', ':Files<CR>', {
  silent = true,
  noremap = true,
  desc = 'Find files'
})

15.7.2 Mapping Options

local opts = {
  noremap = true,   -- Non-recursive (default in vim.keymap.set)
  silent = true,    -- Don't echo command
  expr = false,     -- Not an expression
  buffer = nil,     -- Global (or buffer number for buffer-local)
  nowait = false,   -- Don't wait for more keys
  desc = nil,       -- Description (for which-key, etc.)
}

vim.keymap.set('n', '<leader>d', vim.diagnostic.open_float, opts)

15.7.3 Function Mappings


-- Direct function call
vim.keymap.set('n', '<leader>f', function()
  print("Hello from Lua!")
end, { silent = true })


-- With LSP
vim.keymap.set('n', 'gd', vim.lsp.buf.definition, { desc = 'Go to definition' })
vim.keymap.set('n', 'K', vim.lsp.buf.hover, { desc = 'Hover documentation' })


-- Complex logic
vim.keymap.set('n', '<leader>p', function()
  local path = vim.fn.expand('%:p')
  vim.fn.setreg('+', path)
  print('Copied: ' .. path)
end, { desc = 'Copy file path' })

15.7.4 Buffer-Local Lua Mappings


-- In autocmd
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    vim.keymap.set('n', '<F5>', ':!python %<CR>', { buffer = true })
  end,
})


-- In ftplugin file
local bufnr = vim.api.nvim_get_current_buf()
vim.keymap.set('n', '<leader>r', ':!python %<CR>', { buffer = bufnr })

15.7.5 Deleting Mappings


-- Delete mapping
vim.keymap.del('n', '<leader>w')


-- Delete buffer-local mapping
vim.keymap.del('n', '<leader>w', { buffer = true })

15.8 Practical Mapping Examples

15.8.1 Quick File Operations

" Quick save
nnoremap <leader>w :w<CR>
inoremap <C-s> <Esc>:w<CR>a

" Save and quit
nnoremap <leader>x :x<CR>

" Quit without saving
nnoremap <leader>q :q!<CR>

" Save all buffers
nnoremap <leader>wa :wa<CR>

" Source current file
nnoremap <leader>so :source %<CR>

15.8.2 Terminal Integration (Neovim)


-- Toggle terminal
vim.keymap.set('n', '<C-`>', ':terminal<CR>i', { desc = 'Open terminal' })
vim.keymap.set('t', '<Esc>', '<C-\\><C-n>', { desc = 'Exit terminal mode' })


-- Send current line to terminal
vim.keymap.set('n', '<leader>sl', function()
  local line = vim.api.nvim_get_current_line()
  vim.fn.chansend(vim.b.terminal_job_id, line .. '\n')
end, { desc = 'Send line to terminal' })

15.8.3 Diagnostic Navigation (Neovim)

vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, { desc = 'Previous diagnostic' })
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, { desc = 'Next diagnostic' })
vim.keymap.set('n', '<leader>e', vim.diagnostic.open_float, { desc = 'Show diagnostic' })
vim.keymap.set('n', '<leader>q', vim.diagnostic.setloclist, { desc = 'Diagnostic list' })

15.8.4 LSP Mappings (Neovim)


-- Attach to buffer when LSP connects
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    local bufnr = args.buf
    local opts = { buffer = bufnr }
    
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
    vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts)
    vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
    vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts)
    vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
    vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, opts)
    vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
    vim.keymap.set('n', '<leader>ca', vim.lsp.buf.code_action, opts)
    vim.keymap.set('n', '<leader>f', vim.lsp.buf.format, opts)
  end,
})

15.8.5 Clipboard Operations

" Copy to system clipboard
vnoremap <leader>y "+y
nnoremap <leader>Y "+yg_
nnoremap <leader>y "+y

" Paste from system clipboard
nnoremap <leader>p "+p
nnoremap <leader>P "+P
vnoremap <leader>p "+p
vnoremap <leader>P "+P

" Cut to system clipboard
vnoremap <leader>d "+d

15.9 Debugging and Discovering Mappings

15.9.1 Viewing Current Mappings

" View all mappings
:map

" View mode-specific mappings
:nmap           " Normal mode
:imap           " Insert mode
:vmap           " Visual/Select mode
:xmap           " Visual mode only

" View mappings for specific key
:nmap <leader>
:verbose nmap <C-l>

" View mappings in specific buffer
:map <buffer>

Lua equivalent:


-- Get all keymaps
vim.api.nvim_get_keymap('n')


-- Get buffer-local keymaps
vim.api.nvim_buf_get_keymap(0, 'n')

15.9.2 Finding Mapping Conflicts

" Verbose shows where mapping was defined
:verbose nmap <leader>f

" Check if key is mapped
:nmap <Space>
" Output: No mapping found

15.9.3 Temporary Disabling

" Unmap temporarily
:nunmap <leader>w

" Restore (if you saved it)
nnoremap <leader>w :w<CR>

" Clear all mappings for a mode
:mapclear
:nmapclear
:imapclear

15.10 Best Practices

15.10.1 Mapping Guidelines

  1. Always use noremap variants unless you specifically need recursion

  2. Use <leader> for custom mappings to avoid conflicts

  3. Make mappings mnemonic - easier to remember

  4. Document complex mappings with comments

  5. Use <silent> for mappings that don’t need command echo

  6. Prefer buffer-local mappings for filetype-specific functionality

  7. Group related mappings with consistent prefixes

15.10.2 Organization Strategy

Example structure in init.lua:


-- ~/.config/nvim/lua/mappings.lua
local M = {}


-- General mappings
M.general = function()
  vim.keymap.set('n', '<leader>w', ':w<CR>')
  vim.keymap.set('n', '<leader>q', ':q<CR>')
end


-- Window mappings
M.windows = function()
  vim.keymap.set('n', '<C-h>', '<C-w>h')
  vim.keymap.set('n', '<C-j>', '<C-w>j')
  vim.keymap.set('n', '<C-k>', '<C-w>k')
  vim.keymap.set('n', '<C-l>', '<C-w>l')
end


-- Buffer mappings
M.buffers = function()
  vim.keymap.set('n', '<leader>bn', ':bnext<CR>')
  vim.keymap.set('n', '<leader>bp', ':bprevious<CR>')
end


-- Initialize all
M.setup = function()
  M.general()
  M.windows()
  M.buffers()
end

return M

Then in init.lua:

require('mappings').setup()

15.10.3 Testing Mappings


-- Conditional mapping based on feature availability
if vim.fn.has('nvim-0.10') == 1 then
  vim.keymap.set('n', '<leader>f', vim.lsp.buf.format)
else
  vim.keymap.set('n', '<leader>f', ':!prettier %<CR>')
end


-- Check for plugin before mapping
if pcall(require, 'telescope') then
  vim.keymap.set('n', '<leader>ff', ':Telescope find_files<CR>')
end

End of Chapter 15: Key Mappings

You now have comprehensive knowledge of Vim’s mapping system, from basic remaps to advanced Lua-based configurations. The next chapter in the sequence would naturally be Chapter #16: Marks and Jumps — exploring navigation history, jump lists, marks (local, global, and special), and efficient movement through your editing history.


Chapter 16: Autocommands and Events

Autocommands are Vim’s event-driven automation system, allowing you to execute commands automatically in response to specific events. This chapter explores how to leverage autocommands to create dynamic, context-aware editing environments.

16.1 Understanding Autocommands

16.1.1 Basic Syntax

Vimscript syntax:

autocmd [group] {event} {pattern} {command}

Where:

  • [group]: Optional autocmd group name

  • {event}: The triggering event (e.g., BufRead, FileType)

  • {pattern}: File pattern to match (e.g., *.py, *.md)

  • {command}: Command(s) to execute

Simple examples:

" Auto-save when focus is lost
autocmd FocusLost * silent! wa

" Set line numbers for Python files
autocmd FileType python setlocal number

" Remove trailing whitespace on save
autocmd BufWritePre * :%s/\s\+$//e

16.1.2 Lua API (Neovim)

Basic structure:

vim.api.nvim_create_autocmd({event}, {
  pattern = {pattern},
  callback = function()

    -- your code here
  end,
  group = {group},  -- optional
  desc = "Description",  -- optional
})

Examples:


-- Auto-save on focus lost
vim.api.nvim_create_autocmd('FocusLost', {
  pattern = '*',
  command = 'silent! wa',
  desc = 'Auto-save all buffers'
})


-- Set options for Python files
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    vim.opt_local.number = true
    vim.opt_local.expandtab = true
    vim.opt_local.shiftwidth = 4
  end,
  desc = 'Python file settings'
})

16.2 Common Events

16.2.1 Buffer Events

Event Triggered When
BufNewFile Creating a new buffer for a file that doesn’t exist
BufRead, BufReadPost After reading a buffer (editing a new file)
BufReadPre Before reading a buffer
BufWrite, BufWritePost After writing the entire buffer
BufWritePre Before writing the entire buffer
BufWriteCmd Before writing, for custom write commands
BufEnter After entering a buffer
BufLeave Before leaving a buffer
BufWinEnter After a buffer is displayed in a window
BufWinLeave Before a buffer is removed from a window
BufUnload Before unloading a buffer
BufDelete Before deleting a buffer
BufHidden Just after a buffer becomes hidden

Examples:

" Create directory if it doesn't exist when saving
autocmd BufWritePre * call mkdir(expand('<afile>:p:h'), 'p')

" Jump to last known cursor position
autocmd BufReadPost * 
  \ if line("'\"") >= 1 && line("'\"") <= line("$") && &ft !~# 'commit'
  \ |   exe "normal! g`\""
  \ | endif

" Auto-reload file if changed externally
autocmd FocusGained,BufEnter * checktime

Lua equivalents:


-- Create directory on save
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*',
  callback = function()
    local dir = vim.fn.expand('<afile>:p:h')
    if vim.fn.isdirectory(dir) == 0 then
      vim.fn.mkdir(dir, 'p')
    end
  end,
  desc = 'Create parent directories on save'
})


-- Jump to last position
vim.api.nvim_create_autocmd('BufReadPost', {
  callback = function()
    local mark = vim.api.nvim_buf_get_mark(0, '"')
    local lcount = vim.api.nvim_buf_line_count(0)
    if mark[1] > 0 and mark[1] <= lcount then
      pcall(vim.api.nvim_win_set_cursor, 0, mark)
    end
  end,
  desc = 'Jump to last position'
})


-- Auto-reload on focus
vim.api.nvim_create_autocmd({'FocusGained', 'BufEnter'}, {
  pattern = '*',
  command = 'checktime',
  desc = 'Check if file changed outside Vim'
})

16.2.2 File Type Events

" FileType triggers when filetype is set
autocmd FileType python setlocal expandtab shiftwidth=4
autocmd FileType javascript setlocal expandtab shiftwidth=2
autocmd FileType go setlocal noexpandtab tabstop=4

" Multiple file types
autocmd FileType python,ruby,yaml setlocal expandtab shiftwidth=2

" File pattern matching
autocmd BufRead,BufNewFile *.conf setfiletype conf
autocmd BufRead,BufNewFile Dockerfile* setfiletype dockerfile

Lua approach:


-- Single filetype
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    vim.opt_local.expandtab = true
    vim.opt_local.shiftwidth = 4
    vim.opt_local.tabstop = 4
  end,
})


-- Multiple filetypes
vim.api.nvim_create_autocmd('FileType', {
  pattern = {'python', 'ruby', 'yaml'},
  callback = function()
    vim.opt_local.expandtab = true
    vim.opt_local.shiftwidth = 2
  end,
})


-- Detect filetype by pattern
vim.api.nvim_create_autocmd({'BufRead', 'BufNewFile'}, {
  pattern = '*.conf',
  callback = function()
    vim.bo.filetype = 'conf'
  end,
})

16.2.3 Window and Tab Events

Event Triggered When
WinEnter After entering a window
WinLeave Before leaving a window
WinNew After creating a new window
WinClosed After closing a window
TabEnter After entering a tab page
TabLeave Before leaving a tab page
TabNew After creating a new tab page
TabClosed After closing a tab page

Examples:

" Highlight active window
autocmd WinEnter * setlocal cursorline
autocmd WinLeave * setlocal nocursorline

" Auto-resize windows on vim resize
autocmd VimResized * wincmd =

" Set working directory to current file's directory
autocmd BufEnter * silent! lcd %:p:h

Lua equivalents:


-- Highlight active window
local cursorline_group = vim.api.nvim_create_augroup('CursorLine', { clear = true })

vim.api.nvim_create_autocmd('WinEnter', {
  group = cursorline_group,
  pattern = '*',
  callback = function()
    vim.opt_local.cursorline = true
  end,
})

vim.api.nvim_create_autocmd('WinLeave', {
  group = cursorline_group,
  pattern = '*',
  callback = function()
    vim.opt_local.cursorline = false
  end,
})


-- Auto-resize windows
vim.api.nvim_create_autocmd('VimResized', {
  pattern = '*',
  command = 'wincmd =',
  desc = 'Equalize window sizes on resize'
})

16.2.4 UI and Display Events

Event Triggered When
VimEnter After doing all the startup stuff
VimLeave Before exiting Vim
VimResized After the Vim window was resized
FocusGained Vim got input focus
FocusLost Vim lost input focus
ColorScheme After loading a color scheme
OptionSet After setting an option
CursorHold User doesn’t press a key for updatetime milliseconds
CursorHoldI Like CursorHold but in Insert mode
CursorMoved Cursor moved in Normal mode
CursorMovedI Cursor moved in Insert mode

Examples:

" Auto-save on focus lost
autocmd FocusLost * silent! wa

" Show cursor line only in active window
autocmd VimEnter,WinEnter,BufWinEnter * setlocal cursorline
autocmd WinLeave * setlocal nocursorline

" Trigger after period of inactivity (useful for linting)
autocmd CursorHold,CursorHoldI * silent! checktime

" React to option changes
autocmd OptionSet background call SetupColorscheme()

Lua equivalents:


-- Auto-save on focus lost
vim.api.nvim_create_autocmd('FocusLost', {
  pattern = '*',
  command = 'silent! wa',
})


-- Update diagnostics on cursor hold
vim.api.nvim_create_autocmd('CursorHold', {
  callback = function()
    vim.diagnostic.open_float(nil, { focus = false, scope = 'cursor' })
  end,
  desc = 'Show diagnostics on cursor hold'
})


-- React to colorscheme changes
vim.api.nvim_create_autocmd('ColorScheme', {
  callback = function()

    -- Custom highlight adjustments
    vim.api.nvim_set_hl(0, 'Normal', { bg = 'NONE' })
    vim.api.nvim_set_hl(0, 'NormalFloat', { bg = 'NONE' })
  end,
})

16.2.5 Editing Events

Event Triggered When
InsertEnter Just before starting Insert mode
InsertLeave Just after leaving Insert mode
InsertChange When typing <Insert> in Insert/Replace mode
TextChanged After a change was made in Normal mode
TextChangedI After a change was made in Insert mode
TextYankPost After text has been yanked or deleted

Examples:

" Show relative numbers in normal mode, absolute in insert
autocmd InsertEnter * set norelativenumber
autocmd InsertLeave * set relativenumber

" Highlight yanked text
autocmd TextYankPost * silent! lua vim.highlight.on_yank()

" Auto-format on text change (with debounce)
autocmd TextChanged,TextChangedI <buffer> 
  \ call timer_start(500, {-> execute('lua vim.lsp.buf.format()')})

Lua equivalents:


-- Relative numbers toggle
local number_toggle = vim.api.nvim_create_augroup('NumberToggle', { clear = true })

vim.api.nvim_create_autocmd('InsertEnter', {
  group = number_toggle,
  pattern = '*',
  callback = function()
    vim.opt.relativenumber = false
  end,
})

vim.api.nvim_create_autocmd('InsertLeave', {
  group = number_toggle,
  pattern = '*',
  callback = function()
    vim.opt.relativenumber = true
  end,
})


-- Highlight on yank
vim.api.nvim_create_autocmd('TextYankPost', {
  callback = function()
    vim.highlight.on_yank({ higroup = 'IncSearch', timeout = 200 })
  end,
  desc = 'Highlight yanked text'
})

16.2.6 LSP and Diagnostic Events (Neovim)

Event Triggered When
LspAttach When LSP client attaches to buffer
LspDetach When LSP client detaches from buffer
DiagnosticChanged When diagnostics change

Examples:


-- Set up LSP keymaps when LSP attaches
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    local bufnr = args.buf
    local client = vim.lsp.get_client_by_id(args.data.client_id)
    

    -- Set keymaps
    local opts = { buffer = bufnr }
    vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
    vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
    vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
    

    -- Enable inlay hints if supported
    if client.server_capabilities.inlayHintProvider then
      vim.lsp.inlay_hint.enable(bufnr, true)
    end
  end,
  desc = 'LSP setup on attach'
})


-- Update diagnostics display
vim.api.nvim_create_autocmd('DiagnosticChanged', {
  callback = function()
    vim.diagnostic.setloclist({ open = false })
  end,
})

16.3 Autocommand Groups

16.3.1 Why Use Groups?

Groups help organize autocommands and prevent duplication when sourcing your vimrc multiple times.

Without groups (bad):

" Each time you source vimrc, this gets added again!
autocmd BufWritePre * :%s/\s\+$//e
autocmd BufWritePre * :%s/\s\+$//e  " Now it runs twice!

With groups (good):

augroup TrimWhitespace
  autocmd!  " Clear existing autocmds in this group
  autocmd BufWritePre * :%s/\s\+$//e
augroup END

16.3.2 Vimscript Groups

" Create and populate a group
augroup MyCustomGroup
  autocmd!  " Clear all autocmds in this group
  autocmd FileType python setlocal expandtab
  autocmd FileType javascript setlocal shiftwidth=2
  autocmd BufWritePre * :%s/\s\+$//e
augroup END

" Multiple groups for organization
augroup FileTypeSettings
  autocmd!
  autocmd FileType python setlocal expandtab shiftwidth=4
  autocmd FileType ruby setlocal expandtab shiftwidth=2
augroup END

augroup SaveSettings
  autocmd!
  autocmd BufWritePre * :%s/\s\+$//e
  autocmd FocusLost * silent! wa
augroup END

16.3.3 Lua Groups


-- Create a group
local mygroup = vim.api.nvim_create_augroup('MyCustomGroup', { clear = true })


-- Add autocmds to the group
vim.api.nvim_create_autocmd('FileType', {
  group = mygroup,
  pattern = 'python',
  callback = function()
    vim.opt_local.expandtab = true
    vim.opt_local.shiftwidth = 4
  end,
})

vim.api.nvim_create_autocmd('BufWritePre', {
  group = mygroup,
  pattern = '*',
  callback = function()

    -- Remove trailing whitespace
    local save_cursor = vim.fn.getpos('.')
    vim.cmd([[%s/\s\+$//e]])
    vim.fn.setpos('.', save_cursor)
  end,
})


-- Organize by functionality
local filetype_group = vim.api.nvim_create_augroup('FileTypeSettings', { clear = true })
local save_group = vim.api.nvim_create_augroup('SaveSettings', { clear = true })

vim.api.nvim_create_autocmd('FileType', {
  group = filetype_group,
  pattern = {'python', 'ruby', 'lua'},
  callback = function()
    vim.opt_local.expandtab = true
  end,
})

vim.api.nvim_create_autocmd({'FocusLost', 'BufLeave'}, {
  group = save_group,
  pattern = '*',
  command = 'silent! wa',
})

16.4 Advanced Patterns and Techniques

16.4.1 Multiple Patterns

" Match multiple file extensions
autocmd BufRead,BufNewFile *.txt,*.md,*.markdown setfiletype markdown

" Match multiple events
autocmd BufRead,BufNewFile *.sh,*.bash setfiletype sh

" Complex patterns
autocmd BufRead,BufNewFile */templates/*.html setfiletype htmldjango
autocmd BufRead,BufNewFile .env* setfiletype sh

Lua equivalent:


-- Multiple patterns
vim.api.nvim_create_autocmd({'BufRead', 'BufNewFile'}, {
  pattern = {'*.txt', '*.md', '*.markdown'},
  callback = function()
    vim.bo.filetype = 'markdown'
  end,
})


-- Complex path patterns
vim.api.nvim_create_autocmd({'BufRead', 'BufNewFile'}, {
  pattern = '*/templates/*.html',
  callback = function()
    vim.bo.filetype = 'htmldjango'
  end,
})

16.4.2 Conditional Execution

" Only for certain filetypes
autocmd FileType python if &textwidth == 0 | setlocal textwidth=79 | endif

" Check if plugin exists
autocmd VimEnter * if exists(':NERDTree') | echom "NERDTree available" | endif

" Pattern matching
autocmd BufWritePost *.vim if &filetype == 'vim' | source % | endif

Lua with conditions:


-- Conditional setting
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = function()
    if vim.bo.textwidth == 0 then
      vim.bo.textwidth = 79
    end
  end,
})


-- Check plugin availability
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    if pcall(require, 'telescope') then
      print('Telescope available')
    end
  end,
})


-- Pattern-based logic
vim.api.nvim_create_autocmd('BufWritePost', {
  pattern = '*.lua',
  callback = function()
    if vim.bo.filetype == 'lua' then
      vim.cmd('source %')
    end
  end,
})

16.4.3 Using Functions

" Define function first
function! StripTrailingWhitespace()
  let l:save = winsaveview()
  keeppatterns %s/\s\+$//e
  call winrestview(l:save)
endfunction

" Use in autocmd
autocmd BufWritePre * call StripTrailingWhitespace()

" More complex example
function! SetupPython()
  setlocal expandtab
  setlocal shiftwidth=4
  setlocal colorcolumn=80
  nnoremap <buffer> <F5> :!python %<CR>
endfunction

autocmd FileType python call SetupPython()

Lua functions:


-- Define reusable functions
local function strip_trailing_whitespace()
  local save_cursor = vim.fn.getpos('.')
  vim.cmd([[keeppatterns %s/\s\+$//e]])
  vim.fn.setpos('.', save_cursor)
end

vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*',
  callback = strip_trailing_whitespace,
})


-- Complex setup function
local function setup_python()
  vim.opt_local.expandtab = true
  vim.opt_local.shiftwidth = 4
  vim.opt_local.colorcolumn = '80'
  vim.keymap.set('n', '<F5>', ':!python %<CR>', { buffer = true })
end

vim.api.nvim_create_autocmd('FileType', {
  pattern = 'python',
  callback = setup_python,
})

16.4.4 Nested Autocommands

" Enable nested autocommand execution
autocmd FileType * ++nested call SomeFunction()

" Example: cascade effect
augroup NestedExample
  autocmd!
  autocmd BufRead *.txt setfiletype text
  autocmd FileType text ++nested setlocal spell
augroup END

Lua equivalent:

vim.api.nvim_create_autocmd('FileType', {
  pattern = '*',
  callback = function()

    -- nested = true allows other autocmds to trigger
  end,
  nested = true,
})

16.4.5 One-Time Autocommands

" Execute only once then remove
autocmd VimEnter * ++once echo "Welcome to Vim!"

" Lua doesn't have built-in once, but you can implement it

Lua implementation of once:

vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    print('Welcome to Neovim!')
    return true  -- returning true deletes the autocmd
  end,
  once = true,  -- Neovim 0.8+
})

16.5 Practical Autocommand Recipes

16.5.1 File Management


-- Auto-create directories on save
vim.api.nvim_create_autocmd('BufWritePre', {
  callback = function()
    local dir = vim.fn.expand('<afile>:p:h')
    if vim.fn.isdirectory(dir) == 0 then
      vim.fn.mkdir(dir, 'p')
    end
  end,
})


-- Automatically delete trailing whitespace
local function trim_whitespace()
  local save_cursor = vim.fn.getpos('.')
  local old_query = vim.fn.getreg('/')
  vim.cmd([[silent! %s/\s\+$//e]])
  vim.fn.setpos('.', save_cursor)
  vim.fn.setreg('/', old_query)
end

vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = {'*.py', '*.js', '*.lua', '*.rs', '*.go'},
  callback = trim_whitespace,
})


-- Automatically chmod +x for shell scripts
vim.api.nvim_create_autocmd('BufWritePost', {
  pattern = {'*.sh', '*.bash', '*.zsh'},
  callback = function()
    vim.fn.system('chmod +x ' .. vim.fn.expand('%'))
  end,
})


-- Auto-reload files changed outside Vim
vim.api.nvim_create_autocmd({'FocusGained', 'TermClose', 'TermLeave'}, {
  command = 'checktime',
})

16.5.2 Window and UI Management


-- Equalize window sizes on resize
vim.api.nvim_create_autocmd('VimResized', {
  pattern = '*',
  command = 'tabdo wincmd =',
})


-- Highlight on yank
vim.api.nvim_create_autocmd('TextYankPost', {
  callback = function()
    vim.highlight.on_yank({
      higroup = 'IncSearch',
      timeout = 150,
    })
  end,
})


-- Disable cursorline in inactive windows
local cursorline_group = vim.api.nvim_create_augroup('CursorLine', { clear = true })

vim.api.nvim_create_autocmd({'WinEnter', 'BufEnter'}, {
  group = cursorline_group,
  callback = function()
    if vim.bo.filetype ~= 'TelescopePrompt' then
      vim.opt_local.cursorline = true
    end
  end,
})

vim.api.nvim_create_autocmd('WinLeave', {
  group = cursorline_group,
  callback = function()
    vim.opt_local.cursorline = false
  end,
})


-- Close certain filetypes with 'q'
vim.api.nvim_create_autocmd('FileType', {
  pattern = {'help', 'qf', 'man', 'lspinfo'},
  callback = function(event)
    vim.bo[event.buf].buflisted = false
    vim.keymap.set('n', 'q', '<cmd>close<cr>', { buffer = event.buf, silent = true })
  end,
})

16.5.3 Format and Linting


-- Auto-format on save (with LSP)
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = {'*.py', '*.lua', '*.rs', '*.go'},
  callback = function()
    vim.lsp.buf.format({ timeout_ms = 2000 })
  end,
})


-- Format with external tool
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*.py',
  callback = function()
    vim.cmd([[silent! !black %]])
  end,
})


-- Run linter after save
vim.api.nvim_create_autocmd('BufWritePost', {
  pattern = '*.js',
  callback = function()
    vim.fn.system('eslint --fix ' .. vim.fn.expand('%'))
    vim.cmd('edit')  -- Reload file
  end,
})

16.5.4 Project-Specific Settings


-- Load project-specific vimrc
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    local project_vimrc = vim.fn.getcwd() .. '/.nvimrc'
    if vim.fn.filereadable(project_vimrc) == 1 then
      vim.cmd('source ' .. project_vimrc)
    end
  end,
})


-- Set working directory to project root
vim.api.nvim_create_autocmd('BufEnter', {
  callback = function()
    local root_markers = {'.git', 'package.json', 'Cargo.toml', 'go.mod'}
    local root_dir = vim.fs.dirname(vim.fs.find(root_markers, {
      upward = true,
      path = vim.fn.expand('%:p:h')
    })[1])
    if root_dir then
      vim.fn.chdir(root_dir)
    end
  end,
})

16.5.5 Terminal Integration


-- Start terminal in insert mode
vim.api.nvim_create_autocmd('TermOpen', {
  callback = function()
    vim.opt_local.number = false
    vim.opt_local.relativenumber = false
    vim.opt_local.signcolumn = 'no'
    vim.cmd('startinsert')
  end,
})


-- Close terminal buffer without confirmation
vim.api.nvim_create_autocmd('TermClose', {
  callback = function()
    vim.cmd('bdelete!')
  end,
})

16.5.6 Session and State Management


-- Auto-save session on exit
vim.api.nvim_create_autocmd('VimLeavePre', {
  callback = function()
    vim.cmd('mksession! ~/.config/nvim/session/last.vim')
  end,
})


-- Restore cursor position
vim.api.nvim_create_autocmd('BufReadPost', {
  callback = function()
    local mark = vim.api.nvim_buf_get_mark(0, '"')
    local lcount = vim.api.nvim_buf_line_count(0)
    if mark[1] > 0 and mark[1] <= lcount then
      pcall(vim.api.nvim_win_set_cursor, 0, mark)
    end
  end,
})


-- Save folds
vim.api.nvim_create_autocmd('BufWinLeave', {
  pattern = '*.*',
  command = 'mkview',
})

vim.api.nvim_create_autocmd('BufWinEnter', {
  pattern = '*.*',
  command = 'silent! loadview',
})

16.6 Performance Considerations

16.6.1 Debouncing Frequent Events


-- Bad: Runs on every cursor movement
vim.api.nvim_create_autocmd('CursorMoved', {
  callback = function()

    -- Expensive operation
    vim.lsp.buf.hover()
  end,
})


-- Good: Debounce with timer
local hover_timer = nil
vim.api.nvim_create_autocmd('CursorHold', {
  callback = function()
    if hover_timer then
      vim.fn.timer_stop(hover_timer)
    end
    hover_timer = vim.fn.timer_start(500, function()
      vim.lsp.buf.hover()
    end)
  end,
})

16.6.2 Limiting Patterns

" Bad: Runs on all files
autocmd BufWritePre * call ExpensiveFunction()

" Good: Only specific files
autocmd BufWritePre *.py,*.lua call ExpensiveFunction()

" Better: Use buffer-local when possible
autocmd FileType python autocmd BufWritePre <buffer> call PythonFormat()

16.6.3 Lazy Loading


-- Load heavy plugins only when needed
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'markdown',
  once = true,
  callback = function()
    require('markdown-preview').setup()
  end,
})


-- Defer expensive setup
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    vim.defer_fn(function()

      -- Load non-critical plugins
      require('telescope').setup()
    end, 100)
  end,
})

16.7 Debugging Autocommands

16.7.1 Listing Autocommands

" View all autocommands
:autocmd

" View specific event
:autocmd BufWritePre

" View specific group
:autocmd MyGroup

" View for specific pattern
:autocmd * *.py

" Verbose output shows where defined
:verbose autocmd BufWritePre

Lua inspection:


-- Get all autocommands
vim.api.nvim_get_autocmds({})


-- Get specific group
vim.api.nvim_get_autocmds({ group = 'MyGroup' })


-- Get specific event
vim.api.nvim_get_autocmds({ event = 'BufWritePre' })

16.7.2 Temporary Disabling

" Disable all autocommands temporarily
:autocmd! MyGroup

" Disable specific autocommand
:autocmd! MyGroup BufWritePre

" Re-enable by re-sourcing config
:source $MYVIMRC

16.7.3 Testing with echom

" Add debug output
augroup TestGroup
  autocmd!
  autocmd BufWritePre * echom "About to write: " . expand('%')
augroup END

" View messages
:messages

Lua debugging:

vim.api.nvim_create_autocmd('BufWritePre', {
  callback = function()
    print('Writing file: ' .. vim.fn.expand('%'))

    -- Your actual code here
  end,
})

16.8 Best Practices

16.8.1 Organization Strategy


-- ~/.config/nvim/lua/autocmds.lua
local M = {}

function M.setup()

  -- File management
  M.setup_file_management()
  

  -- UI enhancements
  M.setup_ui()
  

  -- LSP integration
  M.setup_lsp()
  

  -- Filetype specific
  M.setup_filetypes()
end

function M.setup_file_management()
  local group = vim.api.nvim_create_augroup('FileManagement', { clear = true })
  
  vim.api.nvim_create_autocmd('BufWritePre', {
    group = group,
    callback = function()
      local dir = vim.fn.expand('<afile>:p:h')
      if vim.fn.isdirectory(dir) == 0 then
        vim.fn.mkdir(dir, 'p')
      end
    end,
  })
end

function M.setup_ui()
  local group = vim.api.nvim_create_augroup('UIEnhancements', { clear = true })
  
  vim.api.nvim_create_autocmd('TextYankPost', {
    group = group,
    callback = function()
      vim.highlight.on_yank({ timeout = 200 })
    end,
  })
end

return M

In init.lua:

require('autocmds').setup()

16.8.2 Guidelines

  1. Always use augroups to prevent duplicate autocmds

  2. Clear groups with autocmd! in Vimscript or clear = true in Lua

  3. Use specific patterns instead of * when possible

  4. Prefer callback over command in Lua for better error handling

  5. Add descriptions to document purpose

  6. Test expensive operations for performance impact

  7. Use buffer-local autocmds for filetype-specific behavior

  8. Defer non-critical operations to improve startup time

16.8.3 Common Pitfalls to Avoid


-- ❌ Bad: No group, will duplicate
vim.api.nvim_create_autocmd('BufWritePre', {
  callback = function() end,
})


-- ✅ Good: Uses group
local group = vim.api.nvim_create_augroup('MyGroup', { clear = true })
vim.api.nvim_create_autocmd('BufWritePre', {
  group = group,
  callback = function() end,
})


-- ❌ Bad: Too broad pattern
vim.api.nvim_create_autocmd('CursorMoved', {
  pattern = '*',
  callback = expensive_function,
})


-- ✅ Good: Use CursorHold and specific patterns
vim.api.nvim_create_autocmd('CursorHold', {
  pattern = '*.py',
  callback = expensive_function,
})


-- ❌ Bad: Synchronous heavy operation
vim.api.nvim_create_autocmd('BufWritePost', {
  callback = function()
    vim.fn.system('sleep 2')  -- Blocks Vim!
  end,
})


-- ✅ Good: Async operation
vim.api.nvim_create_autocmd('BufWritePost', {
  callback = function()
    vim.fn.jobstart('long-running-command')
  end,
})

End of Chapter 16: Autocommands and Events

You now have comprehensive knowledge of Vim’s event-driven automation system. You can create sophisticated, responsive configurations that adapt to different contexts, file types, and editing scenarios. Master these patterns to build a truly personalized and efficient editing environment.


Chapter 17: VimScript Fundamentals

VimScript is Vim’s built-in scripting language, essential for customization, plugin development, and automation. While Neovim increasingly favors Lua, VimScript remains vital for compatibility and understanding existing configurations.

17.1 Understanding VimScript

17.1.1 What is VimScript?

VimScript (also called Vimscript or VimL) is:

  • A domain-specific language designed for Vim customization

  • Interpreted, not compiled

  • Dynamically typed with loose type checking

  • Command-oriented, reflecting Vim’s Ex-command heritage

  • Essential for understanding most Vim plugins and configurations

17.1.2 Where VimScript Lives

" In your .vimrc or init.vim
set number
nnoremap <leader>w :w<CR>

" In autoload files: ~/.vim/autoload/myutils.vim
function! myutils#greeting()
  echo "Hello!"
endfunction

" In plugin files: ~/.vim/plugin/myplugin.vim
if exists('g:loaded_myplugin')
  finish
endif
let g:loaded_myplugin = 1

" In ftplugin: ~/.vim/ftplugin/python.vim
setlocal expandtab
setlocal shiftwidth=4

17.1.3 Executing VimScript

" Execute a single command
:echo "Hello, Vim!"

" Execute multiple lines
:execute "normal! gg" | echo "Jumped to top"

" Source a file
:source ~/.vimrc
:source %        " Source current file

" Evaluate an expression
:echo 2 + 2      " Outputs: 4

" Execute normal mode commands
:normal! ggVG    " Select all

17.2 Variables and Data Types

17.2.1 Variable Scopes

VimScript has explicit scope prefixes:

Prefix Scope Example
g: Global let g:myvar = 1
l: Function-local let l:temp = 2
s: Script-local let s:counter = 0
a: Function argument function! Func(arg) uses a:arg
b: Buffer-local let b:filename = "test.txt"
w: Window-local let w:view = "split"
t: Tab-local let t:name = "Project"
v: Vim predefined v:version, v:true
$ Environment $HOME, $PATH
@ Register @a, @"
& Option &number, &expandtab

Examples:

" Global variable (accessible everywhere)
let g:my_global_var = "accessible everywhere"

" Script-local (only in this script file)
let s:my_script_var = "only in this file"

" Buffer-local (specific to current buffer)
let b:my_buffer_var = "buffer specific"

" Window-local
let w:my_window_var = "window specific"

" Function local
function! MyFunc()
  let l:local_var = "only in function"
  echo l:local_var
endfunction

" No prefix defaults to function-local inside functions,
" global outside functions
let implicit_global = "global by default"

17.2.2 Data Types

Numbers
" Integers
let decimal = 42
let hex = 0x2A        " Hexadecimal
let octal = 052       " Octal

" Arithmetic
let sum = 10 + 5      " 15
let diff = 10 - 5     " 5
let prod = 10 * 5     " 50
let quot = 10 / 3     " 3 (integer division)
let mod = 10 % 3      " 1
Floats
" Floating point numbers (Vim 7.2+)
let pi = 3.14159
let sci = 1.5e-3      " Scientific notation

" Float operations
let result = 10.0 / 3.0   " 3.333333
let rounded = floor(3.7)  " 3.0
let ceiled = ceil(3.2)    " 4.0
Strings
" Single vs double quotes
let single = 'literal string, no escapes except '''
let double = "can have\nnew lines and\ttabs"

" String concatenation
let name = "Vim"
let greeting = "Hello, " . name . "!"
let concat = "Line1\n" . "Line2"

" String operations
let length = len("Hello")           " 5
let upper = toupper("hello")        " HELLO
let lower = tolower("HELLO")        " hello
let sub = strpart("Hello", 1, 3)    " ell
let idx = stridx("Hello", "ll")     " 2

" String formatting
let formatted = printf("Value: %d", 42)
let padded = printf("%5s", "Hi")    " "   Hi"
Lists
" Creating lists
let empty = []
let numbers = [1, 2, 3, 4, 5]
let mixed = [1, "two", 3.0, [4, 5]]

" Accessing elements
let first = numbers[0]      " 1
let last = numbers[-1]      " 5
let slice = numbers[1:3]    " [2, 3, 4]

" List operations
call add(numbers, 6)        " Add to end: [1, 2, 3, 4, 5, 6]
call insert(numbers, 0)     " Insert at start
call remove(numbers, 0)     " Remove first element
call extend(numbers, [7, 8]) " Extend list

let length = len(numbers)   " List length
let joined = join(numbers, ", ")  " "1, 2, 3, 4, 5"
let reversed = reverse(copy(numbers))

" Iterating
for num in numbers
  echo num
endfor

" List functions
let sorted = sort(copy(numbers))
let unique = uniq(sort(copy(numbers)))
let found = index(numbers, 3)    " Find index
Dictionaries
" Creating dictionaries
let empty = {}
let person = {
  \ "name": "Alice",
  \ "age": 30,
  \ "active": 1
  \ }

" Accessing values
let name = person["name"]       " Using bracket notation
let age = person.age            " Using dot notation (if key is valid identifier)

" Modifying
let person.city = "New York"    " Add key
let person["age"] = 31          " Update value
unlet person.active             " Remove key

" Dictionary operations
let keys = keys(person)         " Get all keys
let values = values(person)     " Get all values
let has_key = has_key(person, "name")  " Check key exists

" Iterating
for key in keys(person)
  echo key . ": " . person[key]
endfor

" Using items() for key-value pairs
for [key, value] in items(person)
  echo key . " => " . value
endfor

" Merge dictionaries
let defaults = {"theme": "dark", "font": "mono"}
let config = {"theme": "light"}
let merged = extend(copy(defaults), config)  " config overrides defaults
Booleans
" VimScript uses 0 for false, non-zero for true
let is_true = 1
let is_false = 0

" Special constants (Vim 7.4.1154+)
let really_true = v:true
let really_false = v:false
let nothing = v:null
let not_a_number = v:none

" Boolean operations
let and_result = 1 && 0      " 0
let or_result = 1 || 0       " 1
let not_result = !1          " 0

" Comparison
let equal = (5 == 5)         " 1
let not_equal = (5 != 3)     " 1
let greater = (5 > 3)        " 1
let less_eq = (3 <= 5)       " 1

" String comparison (case-sensitive by default)
let same = "abc" ==# "abc"   " 1 (case-sensitive)
let diff = "abc" ==? "ABC"   " 1 (case-insensitive)

17.2.3 Type Checking and Conversion

" Type checking
echo type(42)           " 0 (Number)
echo type(3.14)         " 5 (Float)
echo type("string")     " 1 (String)
echo type([])           " 3 (List)
echo type({})           " 4 (Dictionary)
echo type(function('tr'))  " 2 (Funcref)

" Type constants
let is_number = type(var) == type(0)
let is_string = type(var) == type("")
let is_list = type(var) == type([])
let is_dict = type(var) == type({})

" Type conversion
let str_to_num = str2nr("42")      " 42
let num_to_str = string(42)        " "42"
let str_to_float = str2float("3.14")  " 3.14
let list_to_str = string([1, 2, 3])   " "[1, 2, 3]"

" Evaluation
let code = "2 + 2"
let result = eval(code)  " 4

17.3 Operators and Expressions

17.3.1 Arithmetic Operators

let a = 10
let b = 3

echo a + b    " 13
echo a - b    " 7
echo a * b    " 30
echo a / b    " 3 (integer division)
echo a % b    " 1 (modulo)
echo a . b    " "103" (string concatenation)

17.3.2 Comparison Operators

" Numeric comparison
echo 5 == 5   " 1 (true)
echo 5 != 3   " 1 (true)
echo 5 > 3    " 1
echo 5 < 3    " 0 (false)
echo 5 >= 5   " 1
echo 5 <= 3   " 0

" String comparison - depends on 'ignorecase' setting
echo "abc" == "ABC"   " Depends on 'ignorecase'

" Case-sensitive operators
echo "abc" ==# "ABC"  " 0 (case-sensitive, always)
echo "abc" !=# "ABC"  " 1

" Case-insensitive operators
echo "abc" ==? "ABC"  " 1 (case-insensitive, always)
echo "abc" !=? "ABC"  " 0

" Pattern matching
echo "hello" =~ "hel"      " 1 (matches)
echo "hello" =~ "^hel"     " 1 (starts with)
echo "HELLO" =~# "hello"   " 0 (case-sensitive)
echo "HELLO" =~? "hello"   " 1 (case-insensitive)

" Negative pattern matching
echo "hello" !~ "xyz"      " 1 (doesn't match)

17.3.3 Logical Operators

" AND operator
echo 1 && 1   " 1
echo 1 && 0   " 0
echo 0 && 1   " 0

" OR operator
echo 1 || 0   " 1
echo 0 || 0   " 0

" NOT operator
echo !1       " 0
echo !0       " 1
echo !!5      " 1 (double negation)

" Short-circuit evaluation
let result = expensive_func() || cheap_func()  " If first is true, second not evaluated

17.3.4 Ternary Operator

" condition ? true_value : false_value
let max = a > b ? a : b
let status = is_active ? "active" : "inactive"

" Nested ternary
let grade = score >= 90 ? 'A' : score >= 80 ? 'B' : score >= 70 ? 'C' : 'F'

17.4 Control Flow

17.4.1 If Statements

" Basic if
if condition
  echo "True branch"
endif

" If-else
if score >= 60
  echo "Pass"
else
  echo "Fail"
endif

" If-elseif-else
if score >= 90
  echo "A"
elseif score >= 80
  echo "B"
elseif score >= 70
  echo "C"
elseif score >= 60
  echo "D"
else
  echo "F"
endif

" One-liner (using bar |)
if exists('g:myvar') | echo g:myvar | endif

" Checking existence
if exists('g:my_variable')
  echo g:my_variable
endif

if has('nvim')
  " Neovim-specific code
elseif has('patch-8.2.0')
  " Vim 8.2+ specific code
endif

" Check if function exists
if exists('*MyFunction')
  call MyFunction()
endif

17.4.2 For Loops

" Iterate over list
let numbers = [1, 2, 3, 4, 5]
for num in numbers
  echo num
endfor

" Iterate over range
for i in range(5)      " 0, 1, 2, 3, 4
  echo i
endfor

for i in range(1, 5)   " 1, 2, 3, 4, 5
  echo i
endfor

for i in range(0, 10, 2)  " 0, 2, 4, 6, 8, 10 (step of 2)
  echo i
endfor

" Iterate over dictionary
let person = {"name": "Alice", "age": 30}
for key in keys(person)
  echo key . ": " . person[key]
endfor

" Using items() for key-value pairs
for [key, val] in items(person)
  echo key . " => " . val
endfor

" Continue and break
for i in range(10)
  if i == 3
    continue  " Skip to next iteration
  endif
  if i == 7
    break     " Exit loop
  endif
  echo i
endfor

17.4.3 While Loops

" Basic while
let counter = 0
while counter < 5
  echo counter
  let counter += 1
endwhile

" With break
let i = 0
while 1  " Infinite loop
  let i += 1
  if i > 10
    break
  endif
  echo i
endwhile

" With continue
let i = 0
while i < 10
  let i += 1
  if i % 2 == 0
    continue  " Skip even numbers
  endif
  echo i
endwhile

17.4.4 Try-Catch

" Basic try-catch
try
  " Code that might fail
  call NonExistentFunction()
catch
  echo "An error occurred!"
endtry

" Catch specific errors
try
  let result = 10 / 0
catch /E484:/  " Division by zero
  echo "Cannot divide by zero"
catch /E117:/  " Unknown function
  echo "Function not found"
catch
  echo "Unknown error: " . v:exception
endtry

" Finally block (always executed)
try
  call SomeFunction()
catch
  echo "Error: " . v:exception
finally
  echo "Cleanup code"
endtry

" Throwing exceptions
function! Divide(a, b)
  if a:b == 0
    throw "Cannot divide by zero"
  endif
  return a:a / a:b
endfunction

try
  echo Divide(10, 0)
catch
  echo "Caught: " . v:exception
endtry

17.5 Functions

17.5.1 Defining Functions

" Basic function
function! Greet()
  echo "Hello!"
endfunction

" Call it
call Greet()

" Function with arguments
function! Greet(name)
  echo "Hello, " . a:name . "!"
endfunction

call Greet("Alice")

" Multiple arguments
function! Add(x, y)
  return a:x + a:y
endfunction

echo Add(5, 3)  " 8

" Variable arguments (...)
function! Sum(...)
  let total = 0
  for num in a:000  " a:000 is list of all arguments
    let total += num
  endfor
  return total
endfunction

echo Sum(1, 2, 3, 4, 5)  " 15

" Mixed regular and variable arguments
function! LogMessage(level, ...)
  echo a:level . ": " . join(a:000, " ")
endfunction

call LogMessage("ERROR", "File", "not", "found")

17.5.2 Function Modifiers

" abort: Stop on first error
function! SafeFunc() abort
  let x = undefined_var  " Would stop here
  echo "This won't execute"
endfunction

" range: Function works on range
function! ProcessRange() range
  echo "First line: " . a:firstline
  echo "Last line: " . a:lastline
endfunction

" Call with range: :5,10call ProcessRange()

" dict: Function is dictionary method
let myobj = {}
function! myobj.method() dict
  echo "Called on: " . string(self)
endfunction

call myobj.method()

" Combining modifiers
function! MyFunc() range abort
  " Function body
endfunction

17.5.3 Return Values

" Explicit return
function! Square(n)
  return a:n * a:n
endfunction

" Early return
function! IsPositive(n)
  if a:n <= 0
    return 0
  endif
  return 1
endfunction

" No return (implicitly returns 0)
function! NoReturn()
  echo "No return value"
endfunction

echo NoReturn()  " 0

17.5.4 Local Variables in Functions

function! Calculate()
  " Explicit local scope
  let l:temp = 10
  let l:result = l:temp * 2
  
  " Implicit local scope (inside function)
  let another = 5
  
  return l:result + another
endfunction

17.5.5 Autoload Functions

" File: ~/.vim/autoload/myutils.vim
function! myutils#hello()
  echo "Hello from autoload!"
endfunction

function! myutils#math#add(a, b)
  return a:a + a:b
endfunction

" In your vimrc or other scripts:
call myutils#hello()
echo myutils#math#add(5, 3)

17.5.6 Lambda Functions (Vim 8+)

" Lambda syntax: {args -> expression}
let Double = {x -> x * 2}
echo Double(5)  " 10

" Multiple arguments
let Add = {x, y -> x + y}
echo Add(3, 4)  " 7

" Using with map()
let numbers = [1, 2, 3, 4]
let doubled = map(copy(numbers), {idx, val -> val * 2})
echo doubled  " [2, 4, 6, 8]

" Using with filter()
let evens = filter(copy(numbers), {idx, val -> val % 2 == 0})
echo evens  " [2, 4]

17.5.7 Function References

" Get reference to function
function! MyFunc()
  echo "Called!"
endfunction

let Funcref = function('MyFunc')
call Funcref()

" Partial application
function! Greet(greeting, name)
  echo a:greeting . ", " . a:name
endfunction

let SayHello = function('Greet', ['Hello'])
call SayHello('Alice')  " "Hello, Alice"

" Check if funcref
echo type(Funcref) == type(function('tr'))  " 1

17.6 Working with Vim’s Environment

17.6.1 Options

" Set options
set number
set relativenumber
set tabstop=4

" Get option value
echo &number        " 1
echo &tabstop       " 4

" Set option from variable
let &tabstop = 8

" Toggle boolean options
set number!         " Toggle
set nonumber        " Disable
set number          " Enable

" Local vs global options
setlocal tabstop=4     " Buffer-local
setglobal tabstop=8    " Global default
set tabstop=2          " Set both

" Query options
set number?         " Shows current value
set tabstop?

17.6.2 Registers

" Access registers
echo @"         " Unnamed register (last yank/delete)
echo @0         " Last yank
echo @a         " Named register a
echo @/         " Last search pattern
echo @:         " Last command

" Set registers
let @a = "text to store"
let @/ = "search pattern"

" Append to register
let @A = "append this"  " Capital letter appends

" Clear register
let @a = ""

17.6.3 Environment Variables

" Access environment variables
echo $HOME
echo $USER
echo $PATH

" Set environment variable
let $MY_VAR = "value"

" Check if exists
if !empty($MY_VAR)
  echo "Variable is set"
endif

17.6.4 Executing External Commands

" Run shell command
:!ls -la

" Capture output
let output = system('ls -la')
echo output

" Execute and insert output
:read !date

" Execute current line as shell command
:.w !sh

" Use systemlist() for list of lines
let files = systemlist('ls')
for file in files
  echo file
endfor

17.7 String Manipulation

17.7.1 Common String Functions

" Length
echo len("Hello")  " 5

" Case conversion
echo toupper("hello")  " HELLO
echo tolower("HELLO")  " hello

" Substring
echo strpart("Hello World", 6, 5)  " World
echo "Hello"[1:3]  " ell

" Finding substrings
echo stridx("Hello", "ll")  " 2 (index)
echo match("Hello", "l")    " 2 (first match)
echo strridx("Hello", "l")  " 3 (last occurrence)

" Splitting and joining
let parts = split("one,two,three", ",")  " ['one', 'two', 'three']
let joined = join(parts, "-")             " "one-two-three"

" Trimming
echo trim("  hello  ")  " "hello"

" Replacement
echo substitute("Hello World", "World", "Vim", "")  " "Hello Vim"
echo substitute("a b c", " ", "-", "g")  " "a-b-c" (global)

" Repeating
echo repeat("ab", 3)  " "ababab"

" Formatting
echo printf("%s is %d years old", "Alice", 30)
echo printf("%5d", 42)    " "   42" (padded)
echo printf("%-5s", "Hi") " "Hi   " (left-aligned)

17.7.2 Regular Expressions in VimScript

" Pattern matching
if "hello" =~ "hel"
  echo "Match found"
endif

" Extract matches
let text = "Price: $42.50"
let match = matchstr(text, '\d\+\.\d\+')  " "42.50"

" Get match position
let pos = match(text, '\d')  " Position of first digit

" Get all matches
let numbers = matchlist("abc123def456", '\d\+')
echo numbers  " ['123', '', '', '', '', '', '', '', '', '']

" Substitute
let newtext = substitute("hello world", "world", "vim", "")
" With regex
let result = substitute("test123", '\d\+', 'NUMBER', '')  " "testNUMBER"

17.8 Working with Buffers, Windows, and Files

17.8.1 Buffer Operations

" Get current buffer number
echo bufnr('%')

" Get buffer name
echo bufname('%')     " Current buffer
echo bufname(3)       " Buffer 3

" Check if buffer exists
if bufexists('myfile.txt')
  echo "Buffer exists"
endif

" Check if buffer is loaded
if bufloaded(3)
  echo "Buffer 3 is loaded"
endif

" Get buffer list
let buffers = getbufinfo()
for buf in buffers
  echo buf.bufnr . ": " . buf.name
endfor

" Execute in buffer
call setbufvar(2, '&number', 1)  " Set option in buffer 2
let value = getbufvar(2, '&number')  " Get option from buffer 2

17.8.2 Window Operations

" Get current window number
echo winnr()

" Get window count
echo winnr('$')

" Get window ID
echo win_getid()

" Move between windows
execute "normal! \<C-w>w"

" Window dimensions
echo winwidth(0)   " Current window width
echo winheight(0)  " Current window height

" Save and restore view
let saved_view = winsaveview()
" ... do operations ...
call winrestview(saved_view)

17.8.3 File Operations

" Read file into list
let lines = readfile('myfile.txt')

" Write list to file
call writefile(lines, 'output.txt')

" Append to file
call writefile(['new line'], 'output.txt', 'a')

" Check if file exists
if filereadable('myfile.txt')
  echo "File is readable"
endif

" Get file info
echo getfsize('myfile.txt')   " Size in bytes
echo getftime('myfile.txt')   " Modification time

" File type checks
echo isdirectory('/tmp')      " 1
echo filereadable('/etc/hosts') " 1 (usually)

" Expand filename
echo expand('%')        " Current file
echo expand('%:p')      " Full path
echo expand('%:h')      " Head (directory)
echo expand('%:t')      " Tail (filename)
echo expand('%:r')      " Root (without extension)
echo expand('%:e')      " Extension

" Globbing
let files = glob('*.txt')      " Returns string
let file_list = globpath('.', '*.txt')  " Search in path

17.9 Practical Examples

17.9.1 Toggle Comment Function

function! ToggleComment()
  let comment_char = {
    \ 'vim': '"',
    \ 'python': '#',
    \ 'javascript': '//',
    \ 'c': '//',
    \ }
  
  let ft = &filetype
  if !has_key(comment_char, ft)
    echo "No comment character defined for " . ft
    return
  endif
  
  let char = comment_char[ft]
  let line = getline('.')
  
  if line =~ '^\s*' . char
    " Remove comment
    execute 's/^\(\s*\)' . char . '\s*/\1/'
  else
    " Add comment
    execute 's/^/'. char . ' /'
  endif
endfunction

nnoremap <leader>c :call ToggleComment()<CR>

17.9.2 Create a Simple Plugin

" File: ~/.vim/plugin/timestamp.vim

if exists('g:loaded_timestamp')
  finish
endif
let g:loaded_timestamp = 1

function! s:InsertTimestamp()
  let timestamp = strftime('%Y-%m-%d %H:%M:%S')
  execute "normal! i" . timestamp
endfunction

command! Timestamp call s:InsertTimestamp()

nnoremap <leader>ts :Timestamp<CR>

17.9.3 Custom Status Line Function

function! GitBranch()
  if !isdirectory('.git')
    return ''
  endif
  
  try
    let branch = system('git branch --show-current 2>/dev/null')
    return ' [' . substitute(branch, '\n', '', '') . ']'
  catch
    return ''
  endtry
endfunction

function! CustomStatusLine()
  let status = ''
  let status .= ' %f'                    " Filename
  let status .= ' %m'                    " Modified flag
  let status .= GitBranch()              " Git branch
  let status .= '%='                     " Right align
  let status .= ' %y'                    " File type
  let status .= ' %p%%'                  " Percentage through file
  let status .= ' %l:%c'                 " Line:Column
  return status
endfunction

set statusline=%!CustomStatusLine()

17.9.4 Simple Autocomplete Function

function! SimpleComplete(findstart, base)
  if a:findstart
    " Find start of word
    let line = getline('.')
    let start = col('.') - 1
    while start > 0 && line[start - 1] =~ '\a'
      let start -= 1
    endwhile
    return start
  else
    " Find matching words
    let words = ['apple', 'application', 'apply', 'banana', 'band']
    let matches = []
    for word in words
      if word =~ '^' . a:base
        call add(matches, word)
      endif
    endfor
    return matches
  endif
endfunction

set completefunc=SimpleComplete
" Use with <C-x><C-u> in insert mode

End of Chapter 17: VimScript Fundamentals

You now have a solid foundation in VimScript programming. You understand variables, data types, control flow, functions, and practical applications. This knowledge enables you to read and write VimScript for customization, plugin development, and automation tasks. In the next chapter, we’ll explore Lua integration in Neovim, which builds upon these concepts with a modern scripting approach.


Chapter 18: Advanced VimScript

Building on the fundamentals, this chapter explores advanced VimScript techniques for creating sophisticated plugins, optimizing performance, and mastering intricate language features.

18.1 Advanced Function Techniques

18.1.1 Closure and Partial Application

" Creating closures
function! MakeCounter()
  let count = 0
  
  function! Increment() closure
    let count += 1
    return count
  endfunction
  
  return function('Increment')
endfunction

let Counter1 = MakeCounter()
let Counter2 = MakeCounter()

echo Counter1()  " 1
echo Counter1()  " 2
echo Counter2()  " 1 (independent counter)

Practical Example: Memoization

function! Memoize(func)
  let cache = {}
  
  function! Wrapper(...) closure
    let key = string(a:000)
    if !has_key(cache, key)
      let cache[key] = call(a:func, a:000)
    endif
    return cache[key]
  endfunction
  
  return function('Wrapper')
endfunction

" Expensive function
function! Fibonacci(n)
  if a:n <= 1
    return a:n
  endif
  return Fibonacci(a:n - 1) + Fibonacci(a:n - 2)
endfunction

let FastFib = Memoize(function('Fibonacci'))
echo FastFib(30)  " Fast after first call

18.1.2 Function Factories

" Create functions dynamically
function! MakeGreeter(greeting)
  let Greeter = {name -> a:greeting . ', ' . name . '!'}
  return Greeter
endfunction

let SayHello = MakeGreeter('Hello')
let SayHi = MakeGreeter('Hi')

echo SayHello('Alice')  " Hello, Alice!
echo SayHi('Bob')       " Hi, Bob!

Creating Command Wrappers:

function! MakeCommand(cmd, opts)
  function! Wrapper(...) closure
    let args = join(a:000, ' ')
    let full_cmd = a:cmd . ' ' . a:opts . ' ' . args
    return system(full_cmd)
  endfunction
  
  return function('Wrapper')
endfunction

let GitLog = MakeCommand('git', 'log --oneline')
let GitStatus = MakeCommand('git', 'status --short')

echo GitLog('-5')      " Last 5 commits
echo GitStatus()       " Short status

18.1.3 Method Chaining with Dictionaries

" Fluent interface pattern
let Builder = {
  \ 'value': '',
  \ }

function! Builder.append(text) dict
  let self.value .= a:text
  return self
endfunction

function! Builder.uppercase() dict
  let self.value = toupper(self.value)
  return self
endfunction

function! Builder.build() dict
  return self.value
endfunction

function! Builder.new()
  return copy(Builder)
endfunction

" Usage
let result = Builder.new()
  \.append('hello ')
  \.append('world')
  \.uppercase()
  \.build()

echo result  " HELLO WORLD

18.2 Advanced Data Structure Patterns

18.2.1 Object-Oriented Programming

" Base class
let Animal = {
  \ 'name': '',
  \ 'age': 0,
  \ }

function! Animal.init(name, age) dict
  let self.name = a:name
  let self.age = a:age
  return self
endfunction

function! Animal.speak() dict
  echo self.name . ' makes a sound'
endfunction

function! Animal.new(...)
  let obj = copy(self)
  if a:0 > 0
    call call(obj.init, a:000, obj)
  endif
  return obj
endfunction

" Derived class
let Dog = copy(Animal)

function! Dog.speak() dict
  echo self.name . ' barks: Woof!'
endfunction

function! Dog.fetch() dict
  echo self.name . ' is fetching!'
endfunction

" Usage
let mydog = Dog.new('Rex', 3)
call mydog.speak()   " Rex barks: Woof!
call mydog.fetch()   " Rex is fetching!

Advanced Inheritance:

" Mixin pattern
let Serializable = {}

function! Serializable.toJSON() dict
  return json_encode(self)
endfunction

function! Serializable.fromJSON(json) dict
  let data = json_decode(a:json)
  call extend(self, data)
  return self
endfunction

" Mix it in
call extend(Dog, Serializable)

let dog = Dog.new('Max', 5)
let json = dog.toJSON()
echo json

let restored = Dog.new()
call restored.fromJSON(json)

18.2.2 State Machines

" Traffic light state machine
let TrafficLight = {
  \ 'state': 'red',
  \ 'transitions': {
  \   'red': 'green',
  \   'green': 'yellow',
  \   'yellow': 'red',
  \ },
  \ 'durations': {
  \   'red': 3000,
  \   'green': 5000,
  \   'yellow': 2000,
  \ }
  \ }

function! TrafficLight.next() dict
  let self.state = self.transitions[self.state]
  echo 'Light is now: ' . toupper(self.state)
  return self.state
endfunction

function! TrafficLight.duration() dict
  return self.durations[self.state]
endfunction

function! TrafficLight.reset() dict
  let self.state = 'red'
endfunction

" Usage
let light = copy(TrafficLight)
call light.next()     " Green
call light.next()     " Yellow
call light.next()     " Red

18.2.3 Observer Pattern

let Observable = {
  \ 'observers': []
  \ }

function! Observable.subscribe(observer) dict
  call add(self.observers, a:observer)
endfunction

function! Observable.unsubscribe(observer) dict
  call filter(self.observers, 'v:val != a:observer')
endfunction

function! Observable.notify(event) dict
  for observer in self.observers
    call observer(a:event)
  endfor
endfunction

" Usage
function! LogObserver(event)
  echo 'Logged: ' . string(a:event)
endfunction

function! AlertObserver(event)
  if a:event.priority == 'high'
    echo 'ALERT: ' . a:event.message
  endif
endfunction

let subject = copy(Observable)
call subject.subscribe(function('LogObserver'))
call subject.subscribe(function('AlertObserver'))

call subject.notify({'message': 'Test', 'priority': 'high'})

18.3 Metaprogramming

18.3.1 Dynamic Function Creation

" Generate getter/setter functions
function! GenerateAccessors(dict, fields)
  for field in a:fields
    " Getter
    let getter_name = 'get_' . field
    let a:dict[getter_name] = function('s:Getter', [field])
    
    " Setter
    let setter_name = 'set_' . field
    let a:dict[setter_name] = function('s:Setter', [field])
  endfor
endfunction

function! s:Getter(field) dict
  return self[a:field]
endfunction

function! s:Setter(field, value) dict
  let self[a:field] = a:value
endfunction

" Usage
let Person = {'name': '', 'age': 0}
call GenerateAccessors(Person, ['name', 'age'])

let p = copy(Person)
call p.set_name('Alice')
call p.set_age(30)
echo p.get_name()  " Alice

18.3.2 Code Generation

" Generate repetitive mappings
function! GenerateMappings(prefix, commands)
  for [key, cmd] in items(a:commands)
    let mapping = a:prefix . key
    execute 'nnoremap ' . mapping . ' :' . cmd . '<CR>'
  endfor
endfunction

call GenerateMappings('<leader>g', {
  \ 's': 'Git status',
  \ 'c': 'Git commit',
  \ 'p': 'Git push',
  \ 'l': 'Git log',
  \ })

Template-Based Generation:

function! GenerateClass(name, fields)
  let template = [
    \ 'let {{CLASS}} = {',
    \ ]
  
  " Add fields
  for field in a:fields
    call add(template, "  \\ '" . field . "': '',")
  endfor
  
  call add(template, '  \\ }')
  call add(template, '')
  
  " Add constructor
  call add(template, 'function! {{CLASS}}.new(...) dict')
  call add(template, '  let obj = copy(self)')
  
  for i in range(len(a:fields))
    let field = a:fields[i]
    let template_line = printf('  if a:0 > %d | let obj.%s = a:%d | endif',
      \ i, field, i + 1)
    call add(template, template_line)
  endfor
  
  call add(template, '  return obj')
  call add(template, 'endfunction')
  
  " Replace placeholder
  let code = []
  for line in template
    call add(code, substitute(line, '{{CLASS}}', a:name, 'g'))
  endfor
  
  return code
endfunction

" Generate and execute
let class_code = GenerateClass('User', ['name', 'email', 'role'])
for line in class_code
  execute line
endfor

let user = User.new('Alice', 'alice@example.com', 'admin')

18.3.3 Reflection and Introspection

" Inspect function signature
function! InspectFunction(funcname)
  let info = {
    \ 'exists': exists('*' . a:funcname),
    \ 'name': a:funcname,
    \ }
  
  if info.exists
    " Get function definition
    redir => output
    silent execute 'function ' . a:funcname
    redir END
    
    let lines = split(output, '\n')
    let info.definition = lines
    
    " Parse arguments
    if len(lines) > 0
      let sig = lines[0]
      let info.signature = sig
    endif
  endif
  
  return info
endfunction

echo InspectFunction('strftime')

Discover Available Methods:

function! GetMethods(dict)
  let methods = []
  for [key, Val] in items(a:dict)
    if type(Val) == v:t_func
      call add(methods, key)
    endif
  endfor
  return sort(methods)
endfunction

" Usage
let obj = copy(Dog)
echo GetMethods(obj)

18.4 Performance Optimization

18.4.1 Profiling VimScript

" Profile a script
:profile start profile.log
:profile func *
:profile file *

" Your operations here
source myscript.vim

:profile pause
:noautocmd qall!

Profiling Specific Functions:

function! ProfiledFunction()
  let start = reltime()
  
  " Your code here
  for i in range(10000)
    let x = i * 2
  endfor
  
  let elapsed = reltimefloat(reltime(start))
  echo printf('Elapsed: %.6f seconds', elapsed)
endfunction

18.4.2 Optimization Techniques

Use dict Functions for Methods:

" Slow - searches for 'self'
function! MyObj.method()
  echo self.value
endfunction

" Fast - knows it's a dict method
function! MyObj.method() dict
  echo self.value
endfunction

Avoid Repeated Lookups:

" Slow
for i in range(1000)
  let x = g:my_complex_dict.deep.nested.value
endfor

" Fast - cache the lookup
let cached = g:my_complex_dict.deep.nested.value
for i in range(1000)
  let x = cached
endfor

Use += for String Concatenation (Carefully):

" Slower for many iterations (creates many intermediate strings)
let result = ''
for i in range(1000)
  let result = result . i
endfor

" Faster - build list then join
let parts = []
for i in range(1000)
  call add(parts, i)
endfor
let result = join(parts, '')

Lazy Evaluation:

" Evaluate only when needed
let g:lazy_value = ''

function! GetLazyValue()
  if empty(g:lazy_value)
    let g:lazy_value = ExpensiveComputation()
  endif
  return g:lazy_value
endfunction

18.4.3 Avoiding Common Performance Pitfalls

" BAD: O(n²) complexity
function! RemoveDuplicates_Slow(list)
  let result = []
  for item in a:list
    if index(result, item) == -1
      call add(result, item)
    endif
  endfor
  return result
endfunction

" GOOD: O(n) using dictionary
function! RemoveDuplicates_Fast(list)
  let seen = {}
  let result = []
  for item in a:list
    if !has_key(seen, item)
      let seen[item] = 1
      call add(result, item)
    endif
  endfor
  return result
endfunction

" BEST: Use built-in
function! RemoveDuplicates_Best(list)
  return uniq(sort(copy(a:list)))
endfunction

18.5 Error Handling and Debugging

18.5.1 Advanced Error Handling

" Custom exception types
function! ThrowTyped(type, message)
  throw a:type . ': ' . a:message
endfunction

function! ParseConfig(file)
  if !filereadable(a:file)
    call ThrowTyped('FileNotFound', a:file)
  endif
  
  let lines = readfile(a:file)
  if empty(lines)
    call ThrowTyped('EmptyFile', a:file)
  endif
  
  " Parse logic...
  return {'status': 'ok'}
endfunction

" Usage with specific error handling
try
  let config = ParseConfig('config.txt')
catch /^FileNotFound:/
  echo 'Config file not found, using defaults'
  let config = GetDefaultConfig()
catch /^EmptyFile:/
  echo 'Config file is empty'
  let config = {}
catch
  echo 'Unknown error: ' . v:exception
  let config = {}
endtry

18.5.2 Assertion Framework

let g:assert_enabled = 1

function! Assert(condition, message)
  if !g:assert_enabled
    return
  endif
  
  if !a:condition
    let stack = expand('<sfile>')
    throw 'AssertionError: ' . a:message . ' at ' . stack
  endif
endfunction

function! AssertEqual(expected, actual)
  let msg = printf('Expected %s but got %s', 
    \ string(a:expected), string(a:actual))
  call Assert(a:expected == a:actual, msg)
endfunction

function! AssertType(value, expected_type)
  let actual = type(a:value)
  let msg = printf('Expected type %d but got %d', a:expected_type, actual)
  call Assert(actual == a:expected_type, msg)
endfunction

" Usage
function! Divide(a, b)
  call Assert(a:b != 0, 'Division by zero')
  return a:a / a:b
endfunction

18.5.3 Debug Utilities

" Debug logger
let s:debug_file = expand('~/vim_debug.log')

function! DebugLog(...)
  if !exists('g:debug_mode') || !g:debug_mode
    return
  endif
  
  let timestamp = strftime('%Y-%m-%d %H:%M:%S')
  let message = join(a:000, ' ')
  let line = printf('[%s] %s', timestamp, message)
  
  call writefile([line], s:debug_file, 'a')
endfunction

" Variable inspector
function! Inspect(var)
  echo '=== Variable Inspection ==='
  echo 'Type: ' . type(a:var)
  echo 'Value: ' . string(a:var)
  
  if type(a:var) == v:t_dict
    echo 'Keys: ' . string(keys(a:var))
  elseif type(a:var) == v:t_list
    echo 'Length: ' . len(a:var)
  endif
endfunction

" Stack trace
function! GetStackTrace()
  let stack = []
  let level = 0
  
  while 1
    try
      let info = 'level ' . level . ': ' . expand('<sfile>')
      call add(stack, info)
      let level += 1
    catch
      break
    endtry
  endwhile
  
  return stack
endfunction

18.6 Advanced Text Processing

18.6.1 Complex Substitutions

" Substitution with expression
function! IncrementNumbers()
  let counter = 0
  %s/\d\+/\=counter + (counter += 1)/g
endfunction

" Context-aware replacement
function! SmartReplace()
  " Different replacement based on context
  %s/\<\(\w\+\)\>/\=s:TransformWord(submatch(1))/g
endfunction

function! s:TransformWord(word)
  if a:word =~ '^\u'  " Starts with uppercase
    return toupper(a:word)
  else
    return tolower(a:word)
  endif
endfunction

" Multi-pattern replacement
function! ReplaceMultiple(patterns)
  for [pattern, replacement] in items(a:patterns)
    execute '%s/' . pattern . '/' . replacement . '/ge'
  endfor
endfunction

call ReplaceMultiple({
  \ 'TODO': 'DONE',
  \ 'FIXME': 'FIXED',
  \ 'XXX': 'RESOLVED'
  \ })

18.6.2 Text Object Manipulation

" Extract all matches from buffer
function! ExtractMatches(pattern)
  let matches = []
  let pos = [1, 1]
  
  while 1
    let match = searchpos(a:pattern, 'W', 0, 0, 'n')
    if match == [0, 0]
      break
    endif
    
    call setpos('.', [0] + match + [0])
    let text = matchstr(getline('.'), a:pattern)
    call add(matches, text)
    
    " Move past this match
    call search(a:pattern, 'W')
  endwhile
  
  return matches
endfunction

" Parse structured text
function! ParseKeyValue(text)
  let result = {}
  for line in split(a:text, '\n')
    let match = matchlist(line, '^\s*\(\w\+\)\s*=\s*\(.*\)\s*$')
    if !empty(match)
      let result[match[1]] = match[2]
    endif
  endfor
  return result
endfunction

18.6.3 Advanced Buffer Manipulation

" Safe buffer modification
function! ModifyBuffer(bufnr, func)
  let saved_view = winsaveview()
  let saved_buf = bufnr('%')
  
  try
    execute 'buffer ' . a:bufnr
    call a:func()
  finally
    execute 'buffer ' . saved_buf
    call winrestview(saved_view)
  endtry
endfunction

" Batch operations on lines
function! ProcessLines(start, end, func)
  let lines = getline(a:start, a:end)
  let new_lines = map(lines, a:func)
  call setline(a:start, new_lines)
endfunction

" Usage: uppercase lines 1-10
call ProcessLines(1, 10, {idx, val -> toupper(val)})

" Virtual text manipulation (conceptual)
function! TransformSelection(func)
  let [start_line, start_col] = getpos("'<")[1:2]
  let [end_line, end_col] = getpos("'>")[1:2]
  
  if start_line == end_line
    " Single line
    let line = getline(start_line)
    let before = line[:start_col-2]
    let selection = line[start_col-1:end_col-1]
    let after = line[end_col:]
    
    let transformed = before . a:func(selection) . after
    call setline(start_line, transformed)
  else
    " Multi-line - more complex
    " Left as exercise...
  endif
endfunction

18.7 Plugin Development Patterns

18.7.1 Plugin Structure

" plugin/myplugin.vim - Entry point
if exists('g:loaded_myplugin')
  finish
endif
let g:loaded_myplugin = 1

" Set defaults
if !exists('g:myplugin_option')
  let g:myplugin_option = 'default'
endif

" Define commands
command! MyPluginCommand call myplugin#main#run()

" Define mappings
nnoremap <Plug>(myplugin-action) :call myplugin#action#execute()<CR>

if !exists('g:myplugin_no_mappings')
  nmap <leader>mp <Plug>(myplugin-action)
endif
" autoload/myplugin/main.vim - Core functionality
let s:save_cpo = &cpo
set cpo&vim

function! myplugin#main#run()
  " Main logic
  echo "Running MyPlugin"
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo

18.7.2 Configuration System

" Flexible configuration
let s:default_config = {
  \ 'enabled': 1,
  \ 'timeout': 5000,
  \ 'callbacks': {
  \   'on_success': function('s:DefaultSuccess'),
  \   'on_error': function('s:DefaultError'),
  \ }
  \ }

function! s:GetConfig()
  if !exists('g:myplugin_config')
    let g:myplugin_config = {}
  endif
  
  return extend(copy(s:default_config), g:myplugin_config)
endfunction

function! s:ValidateConfig(config)
  call Assert(has_key(a:config, 'enabled'), 'Missing enabled key')
  call AssertType(a:config.enabled, v:t_number)
  " More validation...
endfunction

" User configuration
let g:myplugin_config = {
  \ 'timeout': 10000,
  \ 'callbacks': {
  \   'on_success': function('MyCustomSuccess')
  \ }
  \ }

18.7.3 Event System

" Custom event dispatcher
let s:event_handlers = {}

function! myplugin#event#on(event, handler)
  if !has_key(s:event_handlers, a:event)
    let s:event_handlers[a:event] = []
  endif
  call add(s:event_handlers[a:event], a:handler)
endfunction

function! myplugin#event#emit(event, data)
  if !has_key(s:event_handlers, a:event)
    return
  endif
  
  for Handler in s:event_handlers[a:event]
    try
      call Handler(a:data)
    catch
      call s:LogError('Event handler failed: ' . v:exception)
    endtry
  endfor
endfunction

" Usage
call myplugin#event#on('file_saved', function('MyHandler'))
call myplugin#event#emit('file_saved', {'file': expand('%')})

18.8 Asynchronous Operations (Vim 8+)

18.8.1 Jobs and Channels

" Simple job execution
function! RunAsync(cmd, callback)
  let options = {
    \ 'out_cb': {channel, msg -> a:callback(msg)},
    \ 'err_cb': {channel, msg -> s:ErrorHandler(msg)},
    \ 'exit_cb': {job, status -> s:ExitHandler(status)}
    \ }
  
  let job = job_start(a:cmd, options)
  return job
endfunction

" Example: Async grep
function! AsyncGrep(pattern, callback)
  let cmd = ['grep', '-r', a:pattern, '.']
  call RunAsync(cmd, a:callback)
endfunction

function! HandleGrepResult(line)
  echo 'Found: ' . a:line
endfunction

call AsyncGrep('TODO', function('HandleGrepResult'))

18.8.2 Progress Tracking

let s:async_tasks = {}

function! TrackAsyncTask(name, cmd)
  let task = {
    \ 'name': a:name,
    \ 'status': 'running',
    \ 'output': [],
    \ 'start_time': localtime()
    \ }
  
  let options = {
    \ 'out_cb': {ch, msg -> s:TaskOutput(a:name, msg)},
    \ 'exit_cb': {job, status -> s:TaskComplete(a:name, status)}
    \ }
  
  let task.job = job_start(a:cmd, options)
  let s:async_tasks[a:name] = task
  
  return task
endfunction

function! s:TaskOutput(name, msg)
  call add(s:async_tasks[a:name].output, a:msg)
  call s:UpdateProgress(a:name)
endfunction

function! s:TaskComplete(name, status)
  let s:async_tasks[a:name].status = a:status == 0 ? 'success' : 'failed'
  let s:async_tasks[a:name].end_time = localtime()
  call s:UpdateProgress(a:name)
endfunction

function! s:UpdateProgress(name)
  " Update UI to show progress
  redraw
  echo 'Task ' . a:name . ': ' . s:async_tasks[a:name].status
endfunction

18.9 Testing VimScript

18.9.1 Unit Testing Framework

" Simple test framework
let s:tests = []
let s:test_results = {'passed': 0, 'failed': 0}

function! Test(name, func)
  call add(s:tests, {'name': a:name, 'func': a:func})
endfunction

function! RunTests()
  for test in s:tests
    echo 'Running: ' . test.name
    try
      call test.func()
      let s:test_results.passed += 1
      echo '  ✓ PASSED'
    catch
      let s:test_results.failed += 1
      echo '  ✗ FAILED: ' . v:exception
    endtry
  endfor
  
  echo printf("\nResults: %d passed, %d failed",
    \ s:test_results.passed, s:test_results.failed)
endfunction

" Example tests
call Test('Addition works', {-> AssertEqual(4, 2 + 2)})
call Test('Strings concat', {-> AssertEqual('ab', 'a' . 'b')})

call RunTests()

18.9.2 Mock Objects

" Mock system for testing
function! MockFunction(name, return_value)
  let s:mocks[a:name] = {
    \ 'calls': [],
    \ 'return_value': a:return_value
    \ }
  
  execute printf('function! %s(...)\n return s:CallMock("%s", a:000)\nendfunction',
    \ a:name, a:name)
endfunction

function! s:CallMock(name, args)
  call add(s:mocks[a:name].calls, a:args)
  return s:mocks[a:name].return_value
endfunction

function! VerifyMockCalled(name, times)
  let actual = len(s:mocks[a:name].calls)
  call AssertEqual(a:times, actual)
endfunction

" Usage
let s:mocks = {}
call MockFunction('ExpensiveFunc', 42)

" Test code that calls ExpensiveFunc
let result = ExpensiveFunc(1, 2, 3)
call VerifyMockCalled('ExpensiveFunc', 1)

End of Chapter 18: Advanced VimScript

You’ve now mastered advanced VimScript techniques including closures, OOP patterns, metaprogramming, performance optimization, asynchronous operations, and testing strategies. These skills enable you to build sophisticated, maintainable plugins and deeply customize your Vim environment. The next chapter will explore Lua integration in Neovim, offering a modern alternative with enhanced capabilities.


Chapter 19: Writing VimScript Plugins

This chapter guides you through the complete process of creating, structuring, and distributing professional VimScript plugins, from initial design to publication.

19.1 Plugin Architecture and Design Patterns

19.1.1 Standard Plugin Directory Structure

" Recommended plugin layout:
"
" myplugin/
" ├── plugin/           " Loaded once on startup
" │   └── myplugin.vim
" ├── autoload/         " Lazy-loaded functionality
" │   ├── myplugin.vim
" │   └── myplugin/
" │       ├── core.vim
" │       ├── utils.vim
" │       └── ui.vim
" ├── ftplugin/         " Filetype-specific code
" │   └── python.vim
" ├── syntax/           " Syntax definitions
" │   └── myplugin.vim
" ├── doc/              " Documentation
" │   └── myplugin.txt
" ├── after/            " Loaded after other plugins
" │   └── plugin/
" ├── colors/           " Color schemes
" ├── compiler/         " Compiler definitions
" ├── indent/           " Indentation rules
" └── README.md

19.1.2 Plugin Entry Point Pattern

" plugin/myplugin.vim - Main entry point

" Guard against multiple loading
if exists('g:loaded_myplugin')
  finish
endif
let g:loaded_myplugin = 1

" Save user's cpoptions and set to vim default
let s:save_cpo = &cpo
set cpo&vim

" Check version compatibility
if v:version < 800
  echohl WarningMsg
  echomsg 'MyPlugin requires Vim 8.0 or later'
  echohl None
  finish
endif

" Define default configuration
if !exists('g:myplugin_enabled')
  let g:myplugin_enabled = 1
endif

if !exists('g:myplugin_mappings')
  let g:myplugin_mappings = {}
endif

if !exists('g:myplugin_options')
  let g:myplugin_options = {
    \ 'auto_save': 0,
    \ 'timeout': 3000,
    \ 'debug': 0
    \ }
endif

" Define user commands
command! -nargs=0 MyPluginEnable call myplugin#enable()
command! -nargs=0 MyPluginDisable call myplugin#disable()
command! -nargs=? MyPluginToggle call myplugin#toggle(<f-args>)
command! -nargs=* -complete=customlist,myplugin#complete 
  \ MyPluginAction call myplugin#action(<f-args>)

" Define <Plug> mappings (no default keys, let user choose)
nnoremap <silent> <Plug>(myplugin-do-something)
  \ :<C-u>call myplugin#do_something()<CR>

vnoremap <silent> <Plug>(myplugin-visual-action)
  \ :<C-u>call myplugin#visual_action()<CR>

" Optional default mappings (with opt-out mechanism)
if !exists('g:myplugin_no_default_mappings')
  if empty(maparg('<leader>mp', 'n'))
    nmap <leader>mp <Plug>(myplugin-do-something)
  endif
endif

" Auto-commands for initialization
augroup MyPluginInit
  autocmd!
  autocmd VimEnter * call myplugin#on_vim_enter()
  autocmd BufEnter * call myplugin#on_buf_enter()
augroup END

" Restore cpoptions
let &cpo = s:save_cpo
unlet s:save_cpo

19.1.3 Autoload Pattern for Performance

" autoload/myplugin.vim - Lazy-loaded core functionality

let s:save_cpo = &cpo
set cpo&vim

" Script-local variables (shared across all autoload functions)
let s:plugin_name = 'MyPlugin'
let s:initialized = 0

" Private initialization function
function! s:initialize()
  if s:initialized
    return
  endif
  
  " Perform expensive setup only once
  let s:cache = {}
  let s:state = {
    \ 'enabled': g:myplugin_enabled,
    \ 'buffers': {}
    \ }
  
  let s:initialized = 1
  
  if g:myplugin_options.debug
    echom s:plugin_name . ' initialized'
  endif
endfunction

" Public API functions
function! myplugin#enable()
  call s:initialize()
  let s:state.enabled = 1
  call s:apply_to_all_buffers()
  echom s:plugin_name . ' enabled'
endfunction

function! myplugin#disable()
  let s:state.enabled = 0
  call s:cleanup_all_buffers()
  echom s:plugin_name . ' disabled'
endfunction

function! myplugin#toggle(...)
  if s:state.enabled
    call myplugin#disable()
  else
    call myplugin#enable()
  endif
endfunction

function! myplugin#do_something()
  call s:initialize()
  
  if !s:state.enabled
    echohl WarningMsg
    echo s:plugin_name . ' is disabled'
    echohl None
    return
  endif
  
  " Main functionality
  let result = s:perform_action()
  call s:update_state(result)
  
  return result
endfunction

" Completion function for commands
function! myplugin#complete(ArgLead, CmdLine, CursorPos)
  let options = ['enable', 'disable', 'toggle', 'status', 'reset']
  return filter(copy(options), 'v:val =~ "^" . a:ArgLead')
endfunction

" Event handlers
function! myplugin#on_vim_enter()
  call s:initialize()
  
  if s:state.enabled
    call s:setup_global_features()
  endif
endfunction

function! myplugin#on_buf_enter()
  if !s:initialized || !s:state.enabled
    return
  endif
  
  let bufnr = bufnr('%')
  
  " Track buffer if not already tracked
  if !has_key(s:state.buffers, bufnr)
    call s:setup_buffer(bufnr)
  endif
endfunction

" Private helper functions
function! s:perform_action()
  " Delegate to specialized modules
  return myplugin#core#process(getline(1, '$'))
endfunction

function! s:setup_buffer(bufnr)
  let s:state.buffers[a:bufnr] = {
    \ 'initialized': localtime(),
    \ 'modified': 0
    \ }
  
  " Set buffer-local options
  call setbufvar(a:bufnr, '&omnifunc', 'myplugin#complete')
  
  " Create buffer-local commands
  call setbufvar(a:bufnr, 'myplugin_buffer_cmd',
    \ 'command! -buffer MyPluginLocal call myplugin#buffer_action()')
  execute 'au BufEnter <buffer=' . a:bufnr . '> ' . 
    \ getbufvar(a:bufnr, 'myplugin_buffer_cmd')
endfunction

function! s:apply_to_all_buffers()
  for bufnr in range(1, bufnr('$'))
    if buflisted(bufnr)
      call s:setup_buffer(bufnr)
    endif
  endfor
endfunction

function! s:cleanup_all_buffers()
  for bufnr in keys(s:state.buffers)
    call s:cleanup_buffer(bufnr)
  endfor
  let s:state.buffers = {}
endfunction

function! s:cleanup_buffer(bufnr)
  " Remove buffer-local settings
  silent! call setbufvar(a:bufnr, '&omnifunc', '')
  silent! execute 'au! BufEnter <buffer=' . a:bufnr . '>'
endfunction

function! s:update_state(result)
  " Update internal state based on action result
  if type(a:result) == v:t_dict && has_key(a:result, 'cache')
    let s:cache = extend(s:cache, a:result.cache)
  endif
endfunction

function! s:setup_global_features()
  " Setup features that affect all buffers
  if has('timers')
    let s:timer = timer_start(1000, function('s:periodic_check'), 
      \ {'repeat': -1})
  endif
endfunction

function! s:periodic_check(timer)
  " Periodic maintenance task
  if s:state.enabled
    call s:cleanup_old_cache()
  endif
endfunction

function! s:cleanup_old_cache()
  let cutoff = localtime() - 3600  " 1 hour ago
  call filter(s:cache, 'v:val.timestamp > ' . cutoff)
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo

19.1.4 Modular Design with Submodules

" autoload/myplugin/core.vim - Core processing logic

let s:save_cpo = &cpo
set cpo&vim

function! myplugin#core#process(lines)
  let processed = []
  
  for line in a:lines
    let result = s:process_line(line)
    if !empty(result)
      call add(processed, result)
    endif
  endfor
  
  return {
    \ 'lines': processed,
    \ 'count': len(processed),
    \ 'cache': s:build_cache(processed)
    \ }
endfunction

function! s:process_line(line)
  " Use utility functions from another module
  let cleaned = myplugin#utils#strip_whitespace(a:line)
  
  if empty(cleaned)
    return ''
  endif
  
  return s:transform(cleaned)
endfunction

function! s:transform(text)
  " Actual transformation logic
  return toupper(a:text)
endfunction

function! s:build_cache(lines)
  let cache = {}
  for line in a:lines
    let key = s:compute_key(line)
    let cache[key] = {
      \ 'text': line,
      \ 'timestamp': localtime()
      \ }
  endfor
  return cache
endfunction

function! s:compute_key(text)
  " Simple hash function
  let hash = 0
  for char in split(a:text, '\zs')
    let hash = (hash * 31 + char2nr(char)) % 1000000
  endfor
  return string(hash)
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo
" autoload/myplugin/utils.vim - Utility functions

let s:save_cpo = &cpo
set cpo&vim

function! myplugin#utils#strip_whitespace(text)
  return substitute(a:text, '^\s*\|\s*$', '', 'g')
endfunction

function! myplugin#utils#is_empty_line(text)
  return a:text =~# '^\s*$'
endfunction

function! myplugin#utils#ensure_list(value)
  return type(a:value) == v:t_list ? a:value : [a:value]
endfunction

function! myplugin#utils#safe_execute(cmd)
  try
    execute a:cmd
    return {'success': 1}
  catch
    return {
      \ 'success': 0,
      \ 'error': v:exception,
      \ 'throwpoint': v:throwpoint
      \ }
  endtry
endfunction

function! myplugin#utils#debounce(func, delay)
  " Returns a debounced version of the function
  if !has('timers')
    return a:func
  endif
  
  let context = {
    \ 'func': a:func,
    \ 'delay': a:delay,
    \ 'timer': -1
    \ }
  
  function! context.call(...) dict
    if self.timer != -1
      call timer_stop(self.timer)
    endif
    
    let Args = a:000
    let self.timer = timer_start(self.delay,
      \ {_ -> call(self.func, Args)})
  endfunction
  
  return context.call
endfunction

" File system utilities
function! myplugin#utils#read_json(filepath)
  if !filereadable(a:filepath)
    throw 'File not found: ' . a:filepath
  endif
  
  let content = join(readfile(a:filepath), "\n")
  
  if exists('*json_decode')
    return json_decode(content)
  else
    " Fallback for older Vim versions
    return eval(content)
  endif
endfunction

function! myplugin#utils#write_json(filepath, data)
  let content = json_encode(a:data)
  call writefile(split(content, "\n"), a:filepath)
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo

19.2 Configuration and Options Management

19.2.1 Flexible Configuration System

" autoload/myplugin/config.vim - Configuration management

let s:save_cpo = &cpo
set cpo&vim

" Default configuration
let s:defaults = {
  \ 'enable': 1,
  \ 'auto_start': 1,
  \ 'mappings': {
  \   'toggle': '<leader>mt',
  \   'execute': '<leader>me',
  \   'reset': '<leader>mr'
  \ },
  \ 'features': {
  \   'auto_save': 0,
  \   'notifications': 1,
  \   'syntax_highlight': 1
  \ },
  \ 'paths': {
  \   'cache': expand('~/.cache/myplugin'),
  \   'config': expand('~/.config/myplugin')
  \ },
  \ 'hooks': {
  \   'pre_process': v:null,
  \   'post_process': v:null,
  \   'on_error': v:null
  \ }
  \ }

" Merge user config with defaults
function! myplugin#config#get()
  if !exists('s:config')
    let user_config = exists('g:myplugin_config') ? 
      \ g:myplugin_config : {}
    let s:config = s:deep_merge(s:defaults, user_config)
    call s:validate_config(s:config)
  endif
  
  return s:config
endfunction

function! myplugin#config#set(key, value)
  let config = myplugin#config#get()
  
  " Support nested keys: 'features.auto_save'
  let keys = split(a:key, '\.')
  let current = config
  
  for key in keys[:-2]
    if !has_key(current, key)
      let current[key] = {}
    endif
    let current = current[key]
  endfor
  
  let current[keys[-1]] = a:value
  
  " Re-validate after change
  call s:validate_config(config)
  
  " Trigger update hooks
  call s:on_config_change(a:key, a:value)
endfunction

function! myplugin#config#get_value(key, ...)
  let config = myplugin#config#get()
  let default = a:0 > 0 ? a:1 : v:null
  
  let keys = split(a:key, '\.')
  let value = config
  
  for key in keys
    if type(value) != v:t_dict || !has_key(value, key)
      return default
    endif
    let value = value[key]
  endfor
  
  return value
endfunction

function! myplugin#config#reset()
  unlet! s:config
  echom 'MyPlugin configuration reset to defaults'
endfunction

" Deep merge two dictionaries
function! s:deep_merge(base, override)
  let result = copy(a:base)
  
  for [key, value] in items(a:override)
    if type(value) == v:t_dict && type(get(result, key, '')) == v:t_dict
      let result[key] = s:deep_merge(result[key], value)
    else
      let result[key] = value
    endif
  endfor
  
  return result
endfunction

" Validate configuration
function! s:validate_config(config)
  " Type checking
  call s:assert_type(a:config.enable, v:t_number, 'enable')
  call s:assert_type(a:config.mappings, v:t_dict, 'mappings')
  call s:assert_type(a:config.features, v:t_dict, 'features')
  
  " Value constraints
  if a:config.features.auto_save
    if !exists('*timer_start')
      throw 'auto_save requires Vim with +timers'
    endif
  endif
  
  " Path validation
  for [key, path] in items(a:config.paths)
    if !isdirectory(fnamemodify(path, ':h'))
      echohl WarningMsg
      echom 'Creating directory: ' . path
      echohl None
      call mkdir(path, 'p')
    endif
  endfor
endfunction

function! s:assert_type(value, expected_type, name)
  let actual_type = type(a:value)
  if actual_type != a:expected_type
    throw printf('Config error: %s must be type %d, got %d',
      \ a:name, a:expected_type, actual_type)
  endif
endfunction

" Configuration change hooks
function! s:on_config_change(key, value)
  if a:key == 'features.auto_save'
    if a:value
      call myplugin#features#enable_autosave()
    else
      call myplugin#features#disable_autosave()
    endif
  endif
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo

19.2.2 User Configuration Examples

" User's vimrc/init.vim configuration examples

" Simple configuration
let g:myplugin_config = {
  \ 'enable': 1,
  \ 'features': {'auto_save': 1}
  \ }

" Advanced configuration with hooks
let g:myplugin_config = {
  \ 'enable': 1,
  \ 'mappings': {
  \   'toggle': '<F5>',
  \   'execute': '<F6>'
  \ },
  \ 'features': {
  \   'auto_save': 1,
  \   'notifications': 1
  \ },
  \ 'hooks': {
  \   'pre_process': function('MyPreProcess'),
  \   'post_process': function('MyPostProcess')
  \ }
  \ }

function! MyPreProcess(data)
  " Custom preprocessing
  echo 'Pre-processing: ' . string(a:data)
  return a:data
endfunction

function! MyPostProcess(result)
  " Custom post-processing
  echo 'Result: ' . string(a:result)
endfunction

19.3 Error Handling and User Feedback

19.3.1 Comprehensive Error Handling

" autoload/myplugin/error.vim - Error handling system

let s:save_cpo = &cpo
set cpo&vim

let s:error_log = []
let s:max_log_size = 100

" Error levels
let s:ERROR = 0
let s:WARNING = 1
let s:INFO = 2

function! myplugin#error#throw(message, ...)
  let level = a:0 > 0 ? a:1 : s:ERROR
  let context = a:0 > 1 ? a:2 : {}
  
  let error = {
    \ 'message': a:message,
    \ 'level': level,
    \ 'timestamp': strftime('%Y-%m-%d %H:%M:%S'),
    \ 'context': context,
    \ 'stacktrace': s:get_stacktrace()
    \ }
  
  call s:log_error(error)
  
  " Call user hook if configured
  let hook = myplugin#config#get_value('hooks.on_error', v:null)
  if hook != v:null && type(hook) == v:t_func
    call hook(error)
  endif
  
  " Display to user based on level
  call s:display_error(error)
  
  " Throw exception for ERROR level
  if level == s:ERROR
    throw 'MyPlugin: ' . a:message
  endif
endfunction

function! myplugin#error#wrap(func, ...)
  " Wraps a function with error handling
  let context = a:0 > 0 ? a:1 : {}
  
  try
    return call(a:func, a:000[1:])
  catch
    call myplugin#error#throw(
      \ 'Error in ' . string(a:func) . ': ' . v:exception,
      \ s:ERROR,
      \ context
      \ )
  endtry
endfunction

function! myplugin#error#get_log()
  return copy(s:error_log)
endfunction

function! myplugin#error#clear_log()
  let s:error_log = []
endfunction

function! myplugin#error#show_log()
  if empty(s:error_log)
    echo 'No errors logged'
    return
  endif
  
  for entry in s:error_log[-10:]  " Show last 10
    echo printf('[%s] %s: %s',
      \ entry.timestamp,
      \ s:level_name(entry.level),
      \ entry.message)
  endfor
endfunction

function! s:log_error(error)
  call add(s:error_log, a:error)
  
  " Trim log if too large
  if len(s:error_log) > s:max_log_size
    let s:error_log = s:error_log[-s:max_log_size:]
  endif
  
  " Write to file if debug mode
  if myplugin#config#get_value('debug', 0)
    call s:write_to_file(a:error)
  endif
endfunction

function! s:display_error(error)
  let level_names = ['ERROR', 'WARNING', 'INFO']
  let colors = ['ErrorMsg', 'WarningMsg', 'MoreMsg']
  
  execute 'echohl ' . colors[a:error.level]
  echom '[MyPlugin ' . level_names[a:error.level] . '] ' . a:error.message
  echohl None
endfunction

function! s:get_stacktrace()
  let stack = []
  let level = 1
  
  while 1
    let location = 'level' . level
    try
      " This will error when we run out of stack
      let info = expand('<sfile>')
      call add(stack, info)
      let level += 1
    catch
      break
    endtry
    
    if level > 20  " Safety limit
      break
    endif
  endwhile
  
  return stack
endfunction

function! s:write_to_file(error)
  let logfile = myplugin#config#get_value('paths.cache') . '/error.log'
  let line = printf('[%s] %s: %s',
    \ a:error.timestamp,
    \ s:level_name(a:error.level),
    \ a:error.message)
  
  call writefile([line], logfile, 'a')
endfunction

function! s:level_name(level)
  return ['ERROR', 'WARNING', 'INFO'][a:level]
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo

19.3.2 User Notifications

" autoload/myplugin/notify.vim - Notification system

let s:save_cpo = &cpo
set cpo&vim

function! myplugin#notify#info(message)
  if !myplugin#config#get_value('features.notifications', 1)
    return
  endif
  
  echohl MoreMsg
  echo '[MyPlugin] ' . a:message
  echohl None
endfunction

function! myplugin#notify#warn(message)
  echohl WarningMsg
  echom '[MyPlugin Warning] ' . a:message
  echohl None
endfunction

function! myplugin#notify#error(message)
  echohl ErrorMsg
  echom '[MyPlugin Error] ' . a:message
  echohl None
endfunction

function! myplugin#notify#progress(current, total, message)
  " Show progress bar
  let percent = (a:current * 100) / a:total
  let bar_width = 30
  let filled = (bar_width * a:current) / a:total
  
  let bar = '[' . repeat('=', filled) . repeat(' ', bar_width - filled) . ']'
  
  redraw
  echo printf('%s %d%% - %s', bar, percent, a:message)
endfunction

function! myplugin#notify#confirm(message, ...)
  let choices = a:0 > 0 ? a:1 : ['Yes', 'No']
  let default = a:0 > 1 ? a:2 : 1
  
  let choice_str = join(map(copy(choices), 
    \ '(v:key + 1) . ". " . v:val'), "\n")
  
  let response = inputlist([a:message, choice_str])
  
  if response < 1 || response > len(choices)
    return default - 1
  endif
  
  return response - 1
endfunction

let &cpo = s:save_cpo
unlet s:save_cpo

19.4 Testing Your Plugin

19.4.1 Unit Testing Framework

" test/test_runner.vim - Simple test framework

let s:tests = []
let s:suite_name = ''

function! Test(name, func)
  call add(s:tests, {
    \ 'name': s:suite_name . ' - ' . a:name,
    \ 'func': a:func
    \ })
endfunction

function! Suite(name)
  let s:suite_name = a:name
endfunction

function! RunAllTests()
  let passed = 0
  let failed = 0
  let errors = []
  
  echo 'Running ' . len(s:tests) . ' tests...'
  echo '=================================='
  
  for test in s:tests
    try
      call test.func()
      let passed += 1
      echo '✓ ' . test.name
    catch
      let failed += 1
      echo '✗ ' . test.name
      call add(errors, {
        \ 'test': test.name,
        \ 'error': v:exception
        \ })
    endtry
  endfor
  
  echo '=================================='
  echo printf('Results: %d passed, %d failed', passed, failed)
  
  if !empty(errors)
    echo "\nFailures:"
    for err in errors
      echo err.test . ': ' . err.error
    endfor
  endif
  
  return failed == 0
endfunction

" Assertion functions
function! AssertEqual(expected, actual, ...)
  let msg = a:0 > 0 ? a:1 : 'Assertion failed'
  if a:expected != a:actual
    throw msg . printf(': expected %s, got %s',
      \ string(a:expected), string(a:actual))
  endif
endfunction

function! AssertTrue(condition, ...)
  let msg = a:0 > 0 ? a:1 : 'Expected true'
  if !a:condition
    throw msg
  endif
endfunction

function! AssertFalse(condition, ...)
  let msg = a:0 > 0 ? a:1 : 'Expected false'
  if a:condition
    throw msg
  endif
endfunction

function! AssertThrows(func, ...)
  let pattern = a:0 > 0 ? a:1 : '.*'
  let threw = 0
  
  try
    call a:func()
  catch
    if v:exception =~ pattern
      let threw = 1
    else
      throw 'Wrong exception: expected /' . pattern . 
        \ '/, got: ' . v:exception
    endif
  endtry
  
  if !threw
    throw 'Expected function to throw exception'
  endif
endfunction

19.4.2 Example Test Suite

" test/test_myplugin.vim

source test/test_runner.vim
source plugin/myplugin.vim

call Suite('Core Functionality')

call Test('initialization works', {->
  \ AssertTrue(exists('g:loaded_myplugin'))
  \ })

call Test('config merges correctly', {->
  \ AssertEqual(1, myplugin#config#get_value('enable'))
  \ })

call Test('utils strip whitespace', {->
  \ AssertEqual('hello', myplugin#utils#strip_whitespace('  hello  '))
  \ })

call Suite('Error Handling')

call Test('error thrown for invalid input', {->
  \ AssertThrows({-> myplugin#process('')}, 'Invalid')
  \ })

call Suite('Processing')

call Test('processes lines correctly', {->
  \ let result = myplugin#core#process(['hello', 'world'])
  \,
  \ AssertEqual(2, result.count)
  \ })

" Run all tests
if RunAllTests()
  quit
else
  cquit
endif

19.5 Documentation

19.5.1 Writing Help Documentation

" doc/myplugin.txt - Plugin documentation


*myplugin.txt*  Description of MyPlugin

Author: Your Name <email@example.com>
License: MIT
Version: 1.0.0

==============================================================================
CONTENTS                                                  *myplugin-contents*

    1. Introduction ......................... |myplugin-intro|
    2. Installation ......................... |myplugin-installation|
    3. Usage ................................ |myplugin-usage|
    4. Configuration ........................ |myplugin-config|
    5. Commands ............................. |myplugin-commands|
    6. Mappings ............................. |myplugin-mappings|
    7. Functions ............................ |myplugin-functions|
    8. FAQ .................................. |myplugin-faq|
    9. License .............................. |myplugin-license|

==============================================================================

1. Introduction                                              *myplugin-intro*

MyPlugin is a powerful tool for doing X, Y, and Z in Vim.

Features:~

  * Feature 1

  * Feature 2

  * Feature 3

Requirements:~

  * Vim 8.0+ or Neovim 0.5+

  * (Optional) Feature X for advanced functionality

==============================================================================

2. Installation                                      *myplugin-installation*

Using vim-plug:~
>
    Plug 'username/myplugin'
<

Using packer.nvim:~
>
    use 'username/myplugin'
<

Manual installation:~

Copy the plugin files to your Vim directory:
>
    ~/.vim/
    ├── plugin/myplugin.vim
    ├── autoload/myplugin.vim
    └── doc/myplugin.txt
<

After installation, run: >
    :helptags ~/.vim/doc
<

==============================================================================

3. Usage                                                    *myplugin-usage*

Basic usage example:~
>
    :MyPluginEnable
    :MyPluginAction process
<

For more examples, see |myplugin-examples|.

==============================================================================

4. Configuration                                           *myplugin-config*


                                                       *g:myplugin_config*
Configure MyPlugin by setting this variable in your vimrc:
>
    let g:myplugin_config = {
      \ 'enable': 1,
      \ 'features': {
      \   'auto_save': 0,
      \   'notifications': 1
      \ },
      \ 'mappings': {
      \   'toggle': '<leader>mt'
      \ }
      \ }
<

Options:~

enable                          Enable/disable plugin (default: 1)
features.auto_save              Auto-save on changes (default: 0)
features.notifications          Show notifications (default: 1)
mappings.toggle                 Key mapping for toggle (default: <leader>mt)

==============================================================================

5. Commands                                              *myplugin-commands*

:MyPluginEnable                                          *:MyPluginEnable*
    Enable the plugin.

:MyPluginDisable                                        *:MyPluginDisable*
    Disable the plugin.

:MyPluginToggle                                          *:MyPluginToggle*
    Toggle plugin on/off.

:MyPluginAction {action}                                *:MyPluginAction*
    Execute specified action. Available actions:

      - process: Process current buffer

      - reset: Reset to defaults

      - status: Show current status

==============================================================================

6. Mappings                                              *myplugin-mappings*


                                                  *<Plug>(myplugin-toggle)*
<Plug>(myplugin-toggle)
    Toggle the plugin functionality.
    
    Default mapping: <leader>mt (can be changed via config)
    
    Example custom mapping: >
        nmap <F5> <Plug>(myplugin-toggle)
<


                                                 *<Plug>(myplugin-execute)*
<Plug>(myplugin-execute)
    Execute main plugin action.
    
    Example: >
        nmap <leader>me <Plug>(myplugin-execute)
<

==============================================================================

7. Functions                                            *myplugin-functions*

myplugin#enable()                                      *myplugin#enable()*
    Enable the plugin programmatically.
    
    Example: >
        call myplugin#enable()
<

myplugin#config#set({key}, {value})              *myplugin#config#set()*
    Set configuration value at runtime.
    
    Parameters:~
        {key}    Configuration key (supports dot notation)
        {value}  New value
    
    Example: >
        call myplugin#config#set('features.auto_save', 1)
<

myplugin#config#get_value({key}, [{default}]) *myplugin#config#get_value()*
    Get configuration value.
    
    Parameters:~
        {key}      Configuration key
        {default}  Optional default if key not found
    
    Returns: Configuration value or default
    
    Example: >
        let auto_save = myplugin#config#get_value('features.auto_save', 0)
<

==============================================================================

8. FAQ                                                        *myplugin-faq*

Q: The plugin doesn't work!
A: Make sure you have Vim 8.0+ and run :MyPluginEnable

Q: How do I disable default mappings?
A: Set this in your vimrc: >
    let g:myplugin_no_default_mappings = 1
<

Q: Can I use custom hooks?
A: Yes! See |myplugin-config| for hook configuration.

==============================================================================

9. License                                                *myplugin-license*

MIT License

Copyright (c) 2025 Your Name

Permission is hereby granted...
(full license text)

vim:tw=78:ts=8:ft=help:norl:

19.6 Publishing Your Plugin

19.6.1 Creating a README

# MyPlugin

A powerful Vim plugin for X, Y, and Z.

## Features


- ✨ Feature 1

- 🚀 Feature 2

- 🎯 Feature 3

## Installation

### Using [vim-plug](https://github.com/junegunn/vim-plug)

```vim
Plug 'username/myplugin'

### Using [packer.nvim](https://github.com/wbthomason/packer.nvim)

lua
use 'username/myplugin'

## Quick Start

vim
" Enable the plugin
:MyPluginEnable

" Execute action
:MyPluginAction process

## Configuration

vim
let g:myplugin_config = {
  \ 'enable': 1,
  \ 'features': {
  \   'auto_save': 0,
  \   'notifications': 1
  \ }
  \ }

For full configuration options, see [documentation](doc/myplugin.txt).

## Documentation

Full documentation is available via `:help myplugin` after installation.

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md).

## License

MIT License - see [LICENSE](LICENSE) for details.


### 19.6.2 Version Control Best Practices

```bash
# .gitignore for Vim plugins

*.swp

*.swo

*~
.DS_Store
tags
.netrwhist

# Test artifacts
test/tmp/
test/fixtures/

# Documentation generated files
doc/tags

19.6.3 Semantic Versioning

" Maintain version in plugin
let g:myplugin_version = '1.2.3'

" Breaking changes: increment MAJOR (1.0.0 -> 2.0.0)
" New features: increment MINOR (1.0.0 -> 1.1.0)
" Bug fixes: increment PATCH (1.0.0 -> 1.0.1)

19.7 Advanced Plugin Patterns

19.7.1 Plugin Extensions and Hooks

" Allow other plugins to extend functionality
let g:myplugin_extensions = []

function! myplugin#register_extension(extension)
  call add(g:myplugin_extensions, a:extension)
endfunction

function! myplugin#call_extensions(hook, ...)
  for ext in g:myplugin_extensions
    if has_key(ext, a:hook)
      call call(ext[a:hook], a:000)
    endif
  endfor
endfunction

" Other plugins can register:
" call myplugin#register_extension({
"   \ 'on_process': function('MyCustomProcessor')
"   \ })

19.7.2 Lazy Loading Optimization

" plugin/myplugin.vim - Minimal loading

if exists('g:loaded_myplugin')
  finish
endif
let g:loaded_myplugin = 1

" Only define commands, delay everything else
command! -nargs=0 MyPluginLoad call s:load_plugin()

function! s:load_plugin()
  " Load actual functionality
  runtime! autoload/myplugin.vim
  call myplugin#initialize()
  
  " Remove load command, add real commands
  delcommand MyPluginLoad
  command! -nargs=0 MyPluginEnable call myplugin#enable()
  " ... other commands
endfunction

" Auto-load on specific events if desired
augroup MyPluginAutoLoad
  autocmd!
  autocmd FileType python,javascript call s:load_plugin()
augroup END

End of Chapter 19: Writing VimScript Plugins

You now have comprehensive knowledge of plugin architecture, configuration systems, error handling, testing, documentation, and publication. These patterns will help you create professional, maintainable plugins that follow community best practices. The next chapter explores Lua programming for Neovim, offering modern alternatives to VimScript.


Chapter 20: Lua Basics for Neovim

This chapter introduces Lua programming in the context of Neovim, covering the fundamentals needed to write modern Neovim configurations and plugins using Lua instead of VimScript.

20.1 Why Lua in Neovim?

20.1.1 Advantages of Lua

Lua offers several benefits over VimScript:

  • Performance: Lua is significantly faster, especially for complex operations

  • Modern Language: First-class functions, proper scoping, better data structures

  • LuaJIT: Neovim uses LuaJIT for JIT compilation and FFI capabilities

  • Rich Ecosystem: Access to Lua libraries and better tooling

  • Cleaner Syntax: More readable and maintainable code

20.1.2 Neovim’s Lua Integration

Neovim embeds Lua through:

  • :lua command: Execute Lua code directly

  • :luafile command: Execute Lua files

  • init.lua: Lua-based configuration file (alternative to init.vim)

  • Lua modules: Organize code in lua/ directory

  • Vim API: Access all Vim functionality from Lua

20.2 Lua Language Fundamentals

20.2.1 Basic Syntax and Data Types


-- Comments start with double dash

--[[
  Multi-line comments
  use this syntax
]]


-- Variables and assignment
local name = "Neovim"           -- String
local version = 0.9             -- Number
local is_awesome = true         -- Boolean
local nothing = nil             -- Nil (null equivalent)


-- No declaration needed for globals (avoid these!)
global_var = "bad practice"


-- Always use 'local' for scope-limited variables
local x = 10


-- Multiple assignment
local a, b, c = 1, 2, 3
local x, y = 10                 -- y is nil


-- Swap variables
a, b = b, a

20.2.2 Strings


-- String literals
local single = 'single quotes'
local double = "double quotes"
local multiline = [[
  Multi-line string
  preserves formatting
]]


-- String concatenation
local greeting = "Hello" .. " " .. "Neovim"
local message = "Version: " .. 0.9


-- String methods
local text = "Neovim"
print(string.len(text))        -- 6
print(string.upper(text))      -- NEOVIM
print(string.lower(text))      -- neovim
print(string.sub(text, 1, 3))  -- Neo
print(string.rep("*", 5))      -- *****


-- String formatting
local formatted = string.format("Version %d.%d", 0, 9)
local hex = string.format("0x%x", 255)  -- 0xff


-- Pattern matching (similar to regex)
local matched = string.match("hello world", "(%w+)")  -- hello
local replaced = string.gsub("foo bar", "foo", "baz") -- baz bar


-- String iteration
for char in string.gmatch("abc", ".") do
  print(char)  -- prints a, b, c
end

20.2.3 Numbers and Arithmetic


-- Integer and floating point
local int = 42
local float = 3.14159
local scientific = 1.5e-3      -- 0.0015
local hex = 0xFF               -- 255


-- Arithmetic operators
local sum = 10 + 5             -- 15
local diff = 10 - 5            -- 5
local product = 10 * 5         -- 50
local quotient = 10 / 3        -- 3.333...
local floor_div = math.floor(10 / 3)  -- 3
local modulo = 10 % 3          -- 1
local power = 2 ^ 3            -- 8


-- Math library functions
local abs_val = math.abs(-5)           -- 5
local ceiling = math.ceil(3.2)         -- 4
local floor = math.floor(3.8)          -- 3
local maximum = math.max(1, 5, 3)      -- 5
local minimum = math.min(1, 5, 3)      -- 1
local random = math.random(1, 10)      -- random between 1-10
local sqrt = math.sqrt(16)             -- 4


-- Comparison
local equal = (5 == 5)         -- true
local not_equal = (5 ~= 3)     -- true (note: ~= not !=)
local greater = (5 > 3)        -- true
local less_eq = (5 <= 5)       -- true

20.2.4 Booleans and Logic


-- Boolean values
local yes = true
local no = false
local unknown = nil            -- Treated as false in conditions


-- Logical operators
local and_result = true and false    -- false
local or_result = true or false      -- true
local not_result = not true          -- false


-- Short-circuit evaluation
local value = false and expensive_function()  -- doesn't call function
local default = nil or "default value"        -- "default value"


-- Truthiness: only false and nil are falsy
if 0 then print("0 is truthy") end           -- prints
if "" then print("empty string is truthy") end  -- prints
if nil then print("won't print") end         -- doesn't print
if false then print("won't print") end       -- doesn't print


-- Ternary-like operator (using and/or)
local result = condition and true_value or false_value
local status = is_enabled and "ON" or "OFF"


-- Comparison returns booleans
local is_greater = 5 > 3       -- true
local is_equal = "a" == "a"    -- true

20.3 Control Flow

20.3.1 Conditional Statements


-- if-then-else
local score = 85

if score >= 90 then
  print("A grade")
elseif score >= 80 then
  print("B grade")
elseif score >= 70 then
  print("C grade")
else
  print("Need improvement")
end


-- One-line if
if score > 60 then print("Pass") end


-- Nested conditions
if score > 50 then
  if score > 80 then
    print("Excellent!")
  else
    print("Good")
  end
end


-- Using logical operators
if score >= 60 and score < 90 then
  print("Pass but not excellent")
end


-- Check for nil/existence
local config = get_config()  -- might return nil

if config then

  -- config exists
  use_config(config)
else

  -- config is nil
  use_defaults()
end


-- Multiple conditions
if score >= 90 or extra_credit then
  print("A grade")
end

20.3.2 Loops


-- while loop
local i = 1
while i <= 5 do
  print(i)
  i = i + 1
end


-- repeat-until loop (do-while equivalent)
local count = 0
repeat
  count = count + 1
  print(count)
until count >= 5


-- Numeric for loop
for i = 1, 10 do
  print(i)  -- 1 to 10
end


-- For loop with step
for i = 10, 1, -1 do
  print(i)  -- 10 down to 1
end

for i = 0, 100, 10 do
  print(i)  -- 0, 10, 20, ..., 100
end


-- Generic for loop (iterators)
local items = {"apple", "banana", "cherry"}

for index, value in ipairs(items) do
  print(index, value)
end


-- Loop control
for i = 1, 10 do
  if i == 5 then
    break  -- Exit loop
  end
  
  if i % 2 == 0 then
    goto continue  -- Skip to next iteration (Lua 5.2+)
  end
  
  print(i)
  
  ::continue::
end


-- Infinite loop with break
while true do
  local input = get_input()
  if input == "quit" then
    break
  end
  process(input)
end

20.4 Tables (Lua’s Primary Data Structure)

20.4.1 Tables as Arrays


-- Array-like tables (1-indexed!)
local fruits = {"apple", "banana", "cherry"}


-- Access elements
print(fruits[1])  -- apple (note: 1-indexed, not 0!)
print(fruits[2])  -- banana


-- Modify elements
fruits[1] = "apricot"
fruits[4] = "date"  -- Add new element


-- Get table length
print(#fruits)  -- 4


-- Iterate over array
for i = 1, #fruits do
  print(i, fruits[i])
end


-- Using ipairs (safer for arrays)
for index, value in ipairs(fruits) do
  print(index, value)
end


-- Table manipulation functions
table.insert(fruits, "elderberry")           -- Append
table.insert(fruits, 2, "blueberry")         -- Insert at position
local removed = table.remove(fruits)         -- Remove last
local specific = table.remove(fruits, 2)     -- Remove at position
table.sort(fruits)                           -- Sort in place


-- Create array with constructor
local numbers = {1, 2, 3, 4, 5}
local mixed = {10, "text", true, {nested = "table"}}


-- Empty array
local empty = {}

20.4.2 Tables as Dictionaries/Objects


-- Dictionary/hash table
local person = {
  name = "John",
  age = 30,
  city = "New York"
}


-- Access fields
print(person.name)      -- John (dot notation)
print(person["age"])    -- 30 (bracket notation)


-- Add/modify fields
person.email = "john@example.com"
person["phone"] = "555-1234"


-- Check if key exists
if person.email then
  print("Has email")
end


-- Remove key
person.email = nil


-- Iterate over dictionary with pairs
for key, value in pairs(person) do
  print(key, value)
end


-- Mixed keys (string and number)
local mixed = {
  [1] = "first",
  [2] = "second",
  name = "test",
  ["special-key"] = "value"
}


-- Dynamic keys
local key_name = "dynamic"
local data = {
  [key_name] = "value"  -- Use variable as key
}


-- Nested tables
local config = {
  editor = {
    theme = "dark",
    font_size = 12,
    features = {
      auto_save = true,
      line_numbers = true
    }
  },
  plugins = {"lsp", "treesitter"}
}


-- Access nested values
print(config.editor.theme)                    -- dark
print(config.editor.features.auto_save)       -- true
print(config.plugins[1])                      -- lsp

20.4.3 Table Functions


-- Table construction
local t1 = {1, 2, 3}
local t2 = {a = 1, b = 2}


-- Concatenate array elements
local joined = table.concat(t1, ", ")  -- "1, 2, 3"


-- Sort with custom comparator
local items = {5, 2, 8, 1, 9}
table.sort(items, function(a, b)
  return a > b  -- Sort descending
end)


-- Get table size (works for dictionaries too)
local function table_length(t)
  local count = 0
  for _ in pairs(t) do
    count = count + 1
  end
  return count
end


-- Copy table (shallow copy)
local function shallow_copy(t)
  local copy = {}
  for k, v in pairs(t) do
    copy[k] = v
  end
  return copy
end


-- Deep copy table (recursive)
local function deep_copy(t)
  if type(t) ~= 'table' then
    return t
  end
  
  local copy = {}
  for k, v in pairs(t) do
    copy[k] = deep_copy(v)
  end
  return copy
end


-- Merge tables
local function merge(t1, t2)
  local result = shallow_copy(t1)
  for k, v in pairs(t2) do
    result[k] = v
  end
  return result
end


-- Check if table is empty
local function is_empty(t)
  return next(t) == nil
end


-- Get keys
local function keys(t)
  local result = {}
  for k in pairs(t) do
    table.insert(result, k)
  end
  return result
end


-- Get values
local function values(t)
  local result = {}
  for _, v in pairs(t) do
    table.insert(result, v)
  end
  return result
end

20.5 Functions

20.5.1 Function Definition and Calling


-- Basic function
function greet(name)
  print("Hello, " .. name)
end

greet("Neovim")  -- Hello, Neovim


-- Function with return value
function add(a, b)
  return a + b
end

local sum = add(5, 3)  -- 8


-- Multiple return values
function get_dimensions()
  return 1920, 1080
end

local width, height = get_dimensions()


-- Ignore some return values
local w = get_dimensions()  -- Only get first value
local _, h = get_dimensions()  -- Ignore first, get second


-- Variable number of arguments
function sum_all(...)
  local args = {...}  -- Pack into table
  local total = 0
  for _, v in ipairs(args) do
    total = total + v
  end
  return total
end

print(sum_all(1, 2, 3, 4, 5))  -- 15


-- Named parameters using tables
function create_window(opts)
  opts = opts or {}  -- Default to empty table
  local width = opts.width or 80
  local height = opts.height or 24
  local title = opts.title or "Window"
  
  print(string.format("%s: %dx%d", title, width, height))
end

create_window{width = 100, height = 30, title = "Editor"}


-- Anonymous functions
local square = function(x)
  return x * x
end

print(square(5))  -- 25

20.5.2 Local Functions and Scope


-- Local function
local function helper()
  return "helper result"
end


-- Function scope
do
  local function local_func()
    print("Only visible in this block")
  end
  
  local_func()  -- Works
end


-- local_func()  -- Error: undefined


-- Forward declaration for mutual recursion
local is_even, is_odd

function is_even(n)
  if n == 0 then return true end
  return is_odd(n - 1)
end

function is_odd(n)
  if n == 0 then return false end
  return is_even(n - 1)
end

20.5.3 Closures and Higher-Order Functions


-- Closure: function that captures outer scope
function make_counter()
  local count = 0
  
  return function()
    count = count + 1
    return count
  end
end

local counter1 = make_counter()
local counter2 = make_counter()

print(counter1())  -- 1
print(counter1())  -- 2
print(counter2())  -- 1 (separate counter)


-- Factory function
function make_adder(x)
  return function(y)
    return x + y
  end
end

local add5 = make_adder(5)
print(add5(10))  -- 15


-- Map function
local function map(tbl, func)
  local result = {}
  for i, v in ipairs(tbl) do
    result[i] = func(v)
  end
  return result
end

local numbers = {1, 2, 3, 4}
local squared = map(numbers, function(x) return x * x end)

-- squared = {1, 4, 9, 16}


-- Filter function
local function filter(tbl, predicate)
  local result = {}
  for _, v in ipairs(tbl) do
    if predicate(v) then
      table.insert(result, v)
    end
  end
  return result
end

local evens = filter({1, 2, 3, 4, 5, 6}, function(x)
  return x % 2 == 0
end)

-- evens = {2, 4, 6}


-- Reduce function
local function reduce(tbl, func, initial)
  local acc = initial
  for _, v in ipairs(tbl) do
    acc = func(acc, v)
  end
  return acc
end

local sum = reduce({1, 2, 3, 4}, function(acc, x)
  return acc + x
end, 0)

-- sum = 10

20.5.4 Variadic Functions and Argument Handling


-- Variadic function
function printf(format, ...)
  print(string.format(format, ...))
end

printf("Score: %d, Grade: %s", 95, "A")


-- Accessing variadic arguments
function print_args(...)
  local args = {...}
  print("Argument count:", #args)
  
  for i, v in ipairs(args) do
    print(i, v)
  end
end


-- Getting vararg count
function count_args(...)
  return select('#', ...)
end

print(count_args(1, 2, 3, nil, 5))  -- 5 (includes nil)


-- Select specific arguments
function get_nth_arg(n, ...)
  return select(n, ...)
end

print(get_nth_arg(2, "a", "b", "c"))  -- b


-- Mix fixed and variadic arguments
function log(level, format, ...)
  print("[" .. level .. "]", string.format(format, ...))
end

log("INFO", "Processing %d items", 42)


-- Forwarding arguments
function wrapper(...)
  return actual_function(...)
end

20.6 Metatables and Metamethods

20.6.1 Basic Metatables


-- Create table and metatable
local my_table = {value = 10}
local my_metatable = {
  __index = function(table, key)
    return "Key '" .. key .. "' not found"
  end
}


-- Set metatable
setmetatable(my_table, my_metatable)

print(my_table.value)        -- 10 (exists)
print(my_table.missing_key)  -- "Key 'missing_key' not found"


-- Get metatable
local mt = getmetatable(my_table)


-- Using __index for inheritance
local parent = {
  name = "Parent",
  greet = function(self)
    print("Hello from " .. self.name)
  end
}

local child = {
  name = "Child"
}

setmetatable(child, {__index = parent})

child:greet()  -- Hello from Child (method from parent)

20.6.2 Common Metamethods


-- Arithmetic metamethods
local Vector = {}
Vector.__index = Vector

function Vector.new(x, y)
  local v = {x = x, y = y}
  setmetatable(v, Vector)
  return v
end


-- Addition
function Vector.__add(a, b)
  return Vector.new(a.x + b.x, a.y + b.y)
end


-- Subtraction
function Vector.__sub(a, b)
  return Vector.new(a.x - b.x, a.y - b.y)
end


-- Multiplication (scalar)
function Vector.__mul(a, scalar)
  if type(a) == "number" then
    a, scalar = scalar, a
  end
  return Vector.new(a.x * scalar, a.y * scalar)
end


-- String representation
function Vector.__tostring(v)
  return string.format("Vector(%d, %d)", v.x, v.y)
end


-- Comparison
function Vector.__eq(a, b)
  return a.x == b.x and a.y == b.y
end


-- Usage
local v1 = Vector.new(3, 4)
local v2 = Vector.new(1, 2)
local v3 = v1 + v2          -- Vector(4, 6)
local v4 = v1 * 2           -- Vector(6, 8)
print(v3)                   -- Vector(4, 6)
print(v1 == v2)             -- false


-- Call metamethod
local Calculator = {value = 0}
Calculator.__index = Calculator

function Calculator.new()
  return setmetatable({value = 0}, Calculator)
end

function Calculator:__call(...)
  local sum = 0
  for _, v in ipairs({...}) do
    sum = sum + v
  end
  self.value = sum
  return sum
end

local calc = Calculator.new()
print(calc(1, 2, 3, 4))  -- 10 (called like a function)


-- Length metamethod
local CustomArray = {}
CustomArray.__index = CustomArray

function CustomArray.new(...)
  local arr = {...}
  return setmetatable(arr, CustomArray)
end

function CustomArray:__len()
  local count = 0
  for _ in pairs(self) do
    count = count + 1
  end
  return count
end

local arr = CustomArray.new(1, 2, 3)
print(#arr)  -- 3

20.6.3 Object-Oriented Programming with Metatables


-- Class-like structure
local Animal = {}
Animal.__index = Animal

function Animal.new(name, sound)
  local self = setmetatable({}, Animal)
  self.name = name
  self.sound = sound
  return self
end

function Animal:speak()
  print(self.name .. " says " .. self.sound)
end

function Animal:get_name()
  return self.name
end


-- Inheritance
local Dog = setmetatable({}, {__index = Animal})
Dog.__index = Dog

function Dog.new(name)
  local self = Animal.new(name, "Woof!")
  setmetatable(self, Dog)
  return self
end

function Dog:fetch()
  print(self.name .. " fetches the ball")
end


-- Usage
local dog = Dog.new("Buddy")
dog:speak()      -- Buddy says Woof! (inherited)
dog:fetch()      -- Buddy fetches the ball
print(dog:get_name())  -- Buddy (inherited)

20.7 Modules and Require

20.7.1 Creating Modules


-- File: lua/mymodule.lua
local M = {}  -- Module table


-- Private function (local to module)
local function private_helper()
  return "private result"
end


-- Public function
function M.public_function()
  return "public: " .. private_helper()
end


-- Module data
M.version = "1.0.0"
M.config = {
  enabled = true,
  timeout = 5000
}


-- Module initialization
function M.setup(opts)
  opts = opts or {}
  M.config = vim.tbl_extend("force", M.config, opts)
end

return M

20.7.2 Loading Modules


-- Load module
local mymodule = require('mymodule')


-- Use module
print(mymodule.version)
print(mymodule.public_function())
mymodule.setup({timeout = 10000})


-- Module caching: require caches modules
local m1 = require('mymodule')
local m2 = require('mymodule')

-- m1 and m2 reference same table


-- Reload module (clear cache)
package.loaded['mymodule'] = nil
local fresh = require('mymodule')


-- Conditional loading
local ok, module = pcall(require, 'optional_module')
if ok then
  module.use()
else
  print("Module not found, using fallback")
end

20.7.3 Module Organization


-- File structure:

-- lua/

--   myapp/

--     init.lua       -- Main module entry

--     config.lua     -- Configuration

--     utils.lua      -- Utilities

--     ui/

--       init.lua     -- UI submodule

--       colors.lua   -- Color utilities


-- lua/myapp/init.lua
local M = {}

M.config = require('myapp.config')
M.utils = require('myapp.utils')
M.ui = require('myapp.ui')

function M.setup(opts)
  M.config.setup(opts)
  M.ui.initialize()
end

return M


-- Usage in init.lua
require('myapp').setup({
  theme = 'dark',
  font_size = 12
})

20.8 Error Handling

20.8.1 Protected Calls


-- pcall: protected call
local success, result = pcall(function()
  return risky_operation()
end)

if success then
  print("Result:", result)
else
  print("Error:", result)  -- result contains error message
end


-- Inline pcall
local ok, value = pcall(some_function, arg1, arg2)


-- xpcall: protected call with error handler
local function error_handler(err)
  print("Error occurred:", err)
  print(debug.traceback())
  return err
end

local success, result = xpcall(
  function() return risky_operation() end,
  error_handler
)

20.8.2 Error Throwing and Assertions


-- Throwing errors
function divide(a, b)
  if b == 0 then
    error("Division by zero!")
  end
  return a / b
end


-- Error with level (for stack trace)
function validate(value)
  if type(value) ~= "number" then
    error("Expected number, got " .. type(value), 2)  -- level 2
  end
end


-- Assert
function process(config)
  assert(config, "Config is required")
  assert(config.name, "Config must have name")
  assert(type(config.timeout) == "number", "Timeout must be number")
  

  -- Process...
end


-- Assert with custom error
local value = assert(get_value(), "Failed to get value")

20.9 Practical Lua Patterns for Neovim

20.9.1 Configuration Pattern


-- lua/myconfig/init.lua
local M = {}

M.defaults = {
  enable = true,
  features = {
    lsp = true,
    treesitter = true,
    completion = true
  },
  keymaps = {
    leader = " ",
    toggle = "<leader>t"
  }
}

M.options = {}

function M.setup(opts)

  -- Merge user options with defaults
  M.options = vim.tbl_deep_extend("force", M.defaults, opts or {})
  

  -- Validate
  assert(type(M.options.enable) == "boolean", "enable must be boolean")
  

  -- Apply configuration
  if M.options.enable then
    M.apply()
  end
  
  return M.options
end

function M.apply()

  -- Apply settings
  if M.options.features.lsp then
    require('myconfig.lsp').setup()
  end
  

  -- Set keymaps
  vim.g.mapleader = M.options.keymaps.leader
end

function M.get(key)
  local keys = vim.split(key, '.', {plain = true})
  local value = M.options
  
  for _, k in ipairs(keys) do
    if type(value) ~= 'table' then
      return nil
    end
    value = value[k]
  end
  
  return value
end

return M

20.9.2 Utility Functions Pattern


-- lua/myconfig/utils.lua
local M = {}

function M.is_empty(tbl)
  return next(tbl) == nil
end

function M.tbl_keys(tbl)
  local keys = {}
  for k in pairs(tbl) do
    table.insert(keys, k)
  end
  return keys
end

function M.tbl_values(tbl)
  local values = {}
  for _, v in pairs(tbl) do
    table.insert(values, v)
  end
  return values
end

function M.tbl_map(tbl, func)
  local result = {}
  for k, v in pairs(tbl) do
    result[k] = func(v, k)
  end
  return result
end

function M.tbl_filter(tbl, predicate)
  local result = {}
  for k, v in pairs(tbl) do
    if predicate(v, k) then
      result[k] = v
    end
  end
  return result
end

function M.debounce(func, timeout)
  local timer_id = nil
  
  return function(...)
    local args = {...}
    
    if timer_id then
      vim.fn.timer_stop(timer_id)
    end
    
    timer_id = vim.fn.timer_start(timeout, function()
      func(unpack(args))
      timer_id = nil
    end)
  end
end

return M

20.9.3 Plugin Structure Pattern


-- lua/myplugin/init.lua
local M = {}

M.config = require('myplugin.config')
local utils = require('myplugin.utils')

local initialized = false

function M.setup(opts)
  if initialized then
    utils.warn("Plugin already initialized")
    return
  end
  
  M.config.setup(opts)
  

  -- Setup submodules
  require('myplugin.commands').setup()
  require('myplugin.keymaps').setup()
  require('myplugin.autocmds').setup()
  
  initialized = true
  
  utils.info("Plugin initialized successfully")
end

function M.do_something()
  if not initialized then
    error("Plugin not initialized. Call setup() first.")
  end
  

  -- Implementation
end

return M

End of Chapter 20: Lua Basics for Neovim

You now have a solid foundation in Lua programming tailored for Neovim use. This chapter covered the essential language features, data structures, functions, metatables, modules, error handling, and practical patterns. The next chapter will explore the Neovim Lua API, showing how to interact with Vim functionality using these Lua fundamentals.


Chapter 21: The Neovim Lua API

This chapter explores Neovim’s Lua API, which provides comprehensive access to Vim functionality through Lua. You’ll learn how to interact with buffers, windows, options, commands, keymaps, and autocommands using idiomatic Lua code.

21.1 Introduction to the Neovim Lua API

21.1.1 API Namespaces

Neovim’s Lua API is organized into logical namespaces:


-- Main API namespaces
vim.api       -- Core API functions
vim.fn        -- Access to VimScript functions
vim.cmd       -- Execute Ex commands
vim.opt       -- Option manipulation (modern interface)
vim.o         -- Global options
vim.bo        -- Buffer-local options
vim.wo        -- Window-local options
vim.g         -- Global variables
vim.b         -- Buffer variables
vim.w         -- Window variables
vim.t         -- Tabpage variables
vim.env       -- Environment variables
vim.loop      -- libuv event loop (async operations)
vim.lsp       -- LSP client
vim.treesitter -- Tree-sitter integration
vim.diagnostic -- Diagnostic API

21.1.2 Executing Vim Commands


-- Execute Ex commands
vim.cmd('set number')
vim.cmd('colorscheme gruvbox')
vim.cmd([[
  augroup MyGroup
    autocmd!
    autocmd BufWritePre * :echo "Saving..."
  augroup END
]])


-- Execute and capture output
local output = vim.fn.execute('messages')
print(output)


-- Alternative: using vim.api
vim.api.nvim_command('set relativenumber')


-- Multiple commands
vim.cmd([[
  set number
  set relativenumber
  set cursorline
]])

21.1.3 Calling VimScript Functions


-- Call VimScript functions via vim.fn
local line_count = vim.fn.line('$')
local current_line = vim.fn.getline('.')
local expanded_path = vim.fn.expand('%:p')
local file_exists = vim.fn.filereadable('file.txt')


-- Functions with multiple arguments
vim.fn.search('pattern', 'nW')
vim.fn.setline(5, 'New line content')


-- Get function result
local cwd = vim.fn.getcwd()
local modified = vim.fn.getbufvar(1, '&modified')


-- Check if function exists
if vim.fn.exists('*SomeFunction') == 1 then
  vim.fn.SomeFunction()
end


-- Call user-defined functions
vim.fn['MyCustomFunction']('arg1', 'arg2')

21.2 Working with Options

21.2.1 Modern Option Interface (vim.opt)


-- Set options using vim.opt (modern, preferred)
vim.opt.number = true
vim.opt.relativenumber = true
vim.opt.tabstop = 4
vim.opt.shiftwidth = 4
vim.opt.expandtab = true
vim.opt.wrap = false
vim.opt.ignorecase = true
vim.opt.smartcase = true


-- List options (append, prepend, remove)
vim.opt.wildignore:append('*.pyc')
vim.opt.wildignore:append({ '*.o', '*.obj' })
vim.opt.path:prepend('src')
vim.opt.path:remove('/usr/include')


-- Get option value
local tabsize = vim.opt.tabstop:get()
local ignores = vim.opt.wildignore:get()


-- Set multiple values
vim.opt.shortmess:append('c')  -- Append to option
vim.opt.completeopt = { 'menu', 'menuone', 'noselect' }


-- String options
vim.opt.colorcolumn = '80'
vim.opt.colorcolumn = { '80', '120' }  -- Multiple columns

21.2.2 Direct Option Access


-- Global options (vim.o)
vim.o.number = true
vim.o.relativenumber = true
vim.o.hlsearch = false
vim.o.backup = false
vim.o.swapfile = false


-- Buffer-local options (vim.bo)
vim.bo.filetype = 'lua'
vim.bo.tabstop = 2
vim.bo.shiftwidth = 2
vim.bo.expandtab = true


-- Window-local options (vim.wo)
vim.wo.number = true
vim.wo.relativenumber = true
vim.wo.cursorline = true
vim.wo.signcolumn = 'yes'


-- For specific buffer/window
vim.bo[bufnr].filetype = 'python'
vim.wo[winnr].number = true


-- Read option value
local current_ft = vim.bo.filetype
local is_number_set = vim.wo.number

21.2.3 Option Scoping


-- Setting options for specific buffer/window
local bufnr = vim.api.nvim_get_current_buf()
local winnr = vim.api.nvim_get_current_win()


-- Buffer options
vim.api.nvim_buf_set_option(bufnr, 'filetype', 'lua')
vim.api.nvim_buf_set_option(bufnr, 'tabstop', 2)


-- Window options
vim.api.nvim_win_set_option(winnr, 'number', true)
vim.api.nvim_win_set_option(winnr, 'cursorline', true)


-- Get options
local ft = vim.api.nvim_buf_get_option(bufnr, 'filetype')
local has_numbers = vim.api.nvim_win_get_option(winnr, 'number')


-- Set global option
vim.api.nvim_set_option('ignorecase', true)

21.3 Buffer Management

21.3.1 Buffer Operations


-- Get current buffer
local bufnr = vim.api.nvim_get_current_buf()


-- List all buffers
local buffers = vim.api.nvim_list_bufs()


-- Create new buffer
local new_buf = vim.api.nvim_create_buf(false, true)

-- Args: listed (false = unlisted), scratch (true = scratch buffer)


-- Delete buffer
vim.api.nvim_buf_delete(bufnr, { force = true })


-- Check if buffer is valid
local is_valid = vim.api.nvim_buf_is_valid(bufnr)


-- Check if buffer is loaded
local is_loaded = vim.api.nvim_buf_is_loaded(bufnr)


-- Get buffer name (filepath)
local bufname = vim.api.nvim_buf_get_name(bufnr)


-- Set buffer name
vim.api.nvim_buf_set_name(bufnr, '/path/to/file.txt')


-- Get buffer variable
local var = vim.api.nvim_buf_get_var(bufnr, 'my_variable')


-- Set buffer variable
vim.api.nvim_buf_set_var(bufnr, 'my_variable', 'value')


-- Delete buffer variable
vim.api.nvim_buf_del_var(bufnr, 'my_variable')


-- Alternative: using vim.b
vim.b.my_var = 'value'
local value = vim.b.my_var

21.3.2 Reading and Writing Buffer Content

local bufnr = vim.api.nvim_get_current_buf()


-- Get all lines
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)

-- Args: buffer, start (0-indexed), end (-1 = all), strict_indexing


-- Get specific range
local range = vim.api.nvim_buf_get_lines(bufnr, 5, 10, false)

-- Lines 6-10 (0-indexed start, exclusive end)


-- Get single line
local line = vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1]


-- Set all lines
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, {
  'Line 1',
  'Line 2',
  'Line 3'
})


-- Insert lines at position
vim.api.nvim_buf_set_lines(bufnr, 5, 5, false, {
  'Inserted line 1',
  'Inserted line 2'
})


-- Replace range
vim.api.nvim_buf_set_lines(bufnr, 2, 5, false, {
  'Replacement line'
})


-- Append to end
local line_count = vim.api.nvim_buf_line_count(bufnr)
vim.api.nvim_buf_set_lines(bufnr, line_count, line_count, false, {
  'New line at end'
})


-- Get/set text (for character-level operations)
local text = vim.api.nvim_buf_get_text(bufnr, 0, 0, 0, 5, {})

-- Args: buf, start_row, start_col, end_row, end_col, opts

vim.api.nvim_buf_set_text(bufnr, 0, 0, 0, 5, {'Hello'})

21.3.3 Buffer Marks and Positions

local bufnr = vim.api.nvim_get_current_buf()


-- Get mark position
local mark = vim.api.nvim_buf_get_mark(bufnr, 'a')

-- Returns: {row, col} (1-indexed row, 0-indexed col)


-- Set mark
vim.api.nvim_buf_set_mark(bufnr, 'a', 10, 5, {})

-- Args: buffer, mark_name, line, col, opts


-- Delete mark
vim.api.nvim_buf_del_mark(bufnr, 'a')


-- Get all marks
local marks = vim.fn.getmarklist(bufnr)


-- Get cursor position
local cursor = vim.api.nvim_win_get_cursor(0)
local row, col = cursor[1], cursor[2]


-- Set cursor position
vim.api.nvim_win_set_cursor(0, {10, 5})

-- Args: window, {row, col} (1-indexed row, 0-indexed col)

21.4 Window Management

21.4.1 Window Operations


-- Get current window
local winnr = vim.api.nvim_get_current_win()


-- List all windows
local windows = vim.api.nvim_list_wins()


-- Get window buffer
local bufnr = vim.api.nvim_win_get_buf(winnr)


-- Set window buffer
vim.api.nvim_win_set_buf(winnr, bufnr)


-- Check if window is valid
local is_valid = vim.api.nvim_win_is_valid(winnr)


-- Get window position
local pos = vim.api.nvim_win_get_position(winnr)
local row, col = pos[1], pos[2]


-- Get window dimensions
local width = vim.api.nvim_win_get_width(winnr)
local height = vim.api.nvim_win_get_height(winnr)


-- Set window dimensions
vim.api.nvim_win_set_width(winnr, 80)
vim.api.nvim_win_set_height(winnr, 24)


-- Close window
vim.api.nvim_win_close(winnr, false)  -- force = false


-- Hide window (close but keep buffer)
vim.api.nvim_win_hide(winnr)

21.4.2 Creating and Splitting Windows


-- Open new window (split)
vim.cmd('split')      -- Horizontal split
vim.cmd('vsplit')     -- Vertical split


-- Open buffer in new split
vim.cmd('split | buffer ' .. bufnr)


-- Create floating window
local buf = vim.api.nvim_create_buf(false, true)
local opts = {
  relative = 'editor',
  width = 50,
  height = 10,
  col = 10,
  row = 5,
  style = 'minimal',
  border = 'rounded'
}
local win = vim.api.nvim_open_win(buf, true, opts)


-- Relative to cursor
local opts_cursor = {
  relative = 'cursor',
  width = 40,
  height = 8,
  col = 0,
  row = 1,
  style = 'minimal',
  border = 'single'
}


-- Relative to window
local opts_win = {
  relative = 'win',
  win = 0,  -- Current window
  width = 30,
  height = 15,
  col = 5,
  row = 2
}


-- Update floating window config
vim.api.nvim_win_set_config(win, {
  width = 60,
  height = 20
})


-- Get window config
local config = vim.api.nvim_win_get_config(win)

21.4.3 Window Variables

local winnr = vim.api.nvim_get_current_win()


-- Set window variable
vim.api.nvim_win_set_var(winnr, 'my_var', 'value')


-- Get window variable
local value = vim.api.nvim_win_get_var(winnr, 'my_var')


-- Delete window variable
vim.api.nvim_win_del_var(winnr, 'my_var')


-- Using vim.w
vim.w.my_window_var = 123
local val = vim.w.my_window_var

21.5 Keymaps

21.5.1 Setting Keymaps


-- Basic keymap
vim.keymap.set('n', '<leader>w', ':w<CR>', { desc = 'Save file' })


-- Multiple modes
vim.keymap.set({'n', 'v'}, '<leader>y', '"+y', { desc = 'Yank to clipboard' })


-- With options
vim.keymap.set('n', '<leader>f', ':Telescope find_files<CR>', {
  noremap = true,  -- Non-recursive (default: true)
  silent = true,   -- Don't show in command line
  desc = 'Find files'
})


-- Call Lua function
vim.keymap.set('n', '<leader>h', function()
  print('Hello from keymap!')
end, { desc = 'Print hello' })


-- With expression
vim.keymap.set('i', '<C-j>', function()
  return vim.fn.pumvisible() == 1 and '<C-n>' or '<C-j>'
end, { expr = true })


-- Buffer-local keymap
vim.keymap.set('n', '<leader>r', ':!node %<CR>', {
  buffer = true,  -- Current buffer only
  desc = 'Run with Node.js'
})


-- Specific buffer
vim.keymap.set('n', '<leader>r', ':!node %<CR>', {
  buffer = bufnr,
  desc = 'Run with Node.js'
})


-- Replace existing mapping
vim.keymap.set('n', 'j', 'gj', { remap = true })

21.5.2 Getting and Deleting Keymaps


-- Get keymap
local keymaps = vim.api.nvim_get_keymap('n')


-- Get buffer keymaps
local buf_keymaps = vim.api.nvim_buf_get_keymap(0, 'n')


-- Delete keymap
vim.keymap.del('n', '<leader>w')


-- Delete buffer keymap
vim.keymap.del('n', '<leader>r', { buffer = true })


-- Check if keymap exists
local function keymap_exists(mode, lhs)
  local keymaps = vim.api.nvim_get_keymap(mode)
  for _, keymap in ipairs(keymaps) do
    if keymap.lhs == lhs then
      return true
    end
  end
  return false
end

21.5.3 Common Keymap Patterns


-- Leader key setup
vim.g.mapleader = ' '
vim.g.maplocalleader = ','


-- Save file
vim.keymap.set('n', '<leader>w', '<cmd>w<CR>', { desc = 'Save' })


-- Quit
vim.keymap.set('n', '<leader>q', '<cmd>q<CR>', { desc = 'Quit' })


-- Better window navigation
vim.keymap.set('n', '<C-h>', '<C-w>h', { desc = 'Go to left window' })
vim.keymap.set('n', '<C-j>', '<C-w>j', { desc = 'Go to lower window' })
vim.keymap.set('n', '<C-k>', '<C-w>k', { desc = 'Go to upper window' })
vim.keymap.set('n', '<C-l>', '<C-w>l', { desc = 'Go to right window' })


-- Resize windows
vim.keymap.set('n', '<C-Up>', ':resize +2<CR>', { desc = 'Increase height' })
vim.keymap.set('n', '<C-Down>', ':resize -2<CR>', { desc = 'Decrease height' })
vim.keymap.set('n', '<C-Left>', ':vertical resize -2<CR>', { desc = 'Decrease width' })
vim.keymap.set('n', '<C-Right>', ':vertical resize +2<CR>', { desc = 'Increase width' })


-- Navigate buffers
vim.keymap.set('n', '<S-l>', ':bnext<CR>', { desc = 'Next buffer' })
vim.keymap.set('n', '<S-h>', ':bprevious<CR>', { desc = 'Previous buffer' })


-- Stay in indent mode
vim.keymap.set('v', '<', '<gv', { desc = 'Indent left' })
vim.keymap.set('v', '>', '>gv', { desc = 'Indent right' })


-- Move text up and down
vim.keymap.set('v', '<A-j>', ":m '>+1<CR>gv=gv", { desc = 'Move line down' })
vim.keymap.set('v', '<A-k>', ":m '<-2<CR>gv=gv", { desc = 'Move line up' })


-- Keep paste buffer when pasting over
vim.keymap.set('v', 'p', '"_dP', { desc = 'Paste without yanking' })


-- Clear search highlighting
vim.keymap.set('n', '<Esc>', '<cmd>nohlsearch<CR>', { desc = 'Clear search' })

21.6 Autocommands

21.6.1 Creating Autocommands


-- Basic autocommand
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*',
  callback = function()
    print('File about to be saved!')
  end
})


-- With pattern matching
vim.api.nvim_create_autocmd('BufRead', {
  pattern = '*.lua',
  callback = function()
    vim.bo.tabstop = 2
    vim.bo.shiftwidth = 2
  end
})


-- Multiple events
vim.api.nvim_create_autocmd({'BufEnter', 'BufWinEnter'}, {
  pattern = '*.py',
  callback = function()
    vim.bo.expandtab = true
  end
})


-- Execute command instead of callback
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*',
  command = 'echo "Saving..."'
})


-- With description
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'lua',
  desc = 'Setup Lua filetype options',
  callback = function()
    vim.opt_local.tabstop = 2
  end
})


-- Buffer-specific autocommand
vim.api.nvim_create_autocmd('BufWritePost', {
  buffer = bufnr,
  callback = function()
    print('This buffer was saved')
  end
})


-- Once (execute only once then delete)
vim.api.nvim_create_autocmd('VimEnter', {
  once = true,
  callback = function()
    print('Neovim started!')
  end
})

21.6.2 Autocommand Groups


-- Create augroup
local augroup = vim.api.nvim_create_augroup('MyGroup', { clear = true })


-- Add autocommands to group
vim.api.nvim_create_autocmd('BufWritePre', {
  group = augroup,
  pattern = '*',
  callback = function()

    -- Remove trailing whitespace
    vim.cmd([[%s/\s\+$//e]])
  end
})

vim.api.nvim_create_autocmd('BufRead', {
  group = augroup,
  pattern = '*.lua',
  callback = function()
    vim.bo.tabstop = 2
  end
})


-- Create group and autocommands together
local group = vim.api.nvim_create_augroup('FileTypeSettings', { clear = true })

vim.api.nvim_create_autocmd('FileType', {
  group = group,
  pattern = 'python',
  callback = function()
    vim.opt_local.tabstop = 4
    vim.opt_local.shiftwidth = 4
  end
})

vim.api.nvim_create_autocmd('FileType', {
  group = group,
  pattern = 'javascript',
  callback = function()
    vim.opt_local.tabstop = 2
    vim.opt_local.shiftwidth = 2
  end
})


-- Clear augroup
vim.api.nvim_clear_autocmds({ group = augroup })


-- Delete specific autocommands
vim.api.nvim_clear_autocmds({
  event = 'BufWritePre',
  pattern = '*.lua'
})

21.6.3 Practical Autocommand Examples


-- Auto-format on save
local format_group = vim.api.nvim_create_augroup('AutoFormat', { clear = true })

vim.api.nvim_create_autocmd('BufWritePre', {
  group = format_group,
  pattern = { '*.lua', '*.py', '*.js' },
  callback = function()
    vim.lsp.buf.format({ async = false })
  end
})


-- Highlight on yank
vim.api.nvim_create_autocmd('TextYankPost', {
  callback = function()
    vim.highlight.on_yank({ higroup = 'IncSearch', timeout = 200 })
  end
})


-- Auto-save
vim.api.nvim_create_autocmd({ 'FocusLost', 'BufLeave' }, {
  pattern = '*',
  callback = function()
    if vim.bo.modified and not vim.bo.readonly and vim.fn.expand('%') ~= '' then
      vim.cmd('silent! write')
    end
  end
})


-- Restore cursor position
vim.api.nvim_create_autocmd('BufReadPost', {
  callback = function()
    local mark = vim.api.nvim_buf_get_mark(0, '"')
    local line_count = vim.api.nvim_buf_line_count(0)
    if mark[1] > 0 and mark[1] <= line_count then
      vim.api.nvim_win_set_cursor(0, mark)
    end
  end
})


-- Auto-create parent directories
vim.api.nvim_create_autocmd('BufWritePre', {
  callback = function(event)
    local file = vim.loop.fs_realpath(event.match) or event.match
    local dir = vim.fn.fnamemodify(file, ':p:h')
    
    if vim.fn.isdirectory(dir) == 0 then
      vim.fn.mkdir(dir, 'p')
    end
  end
})


-- Close certain filetypes with 'q'
vim.api.nvim_create_autocmd('FileType', {
  pattern = { 'help', 'qf', 'man', 'lspinfo' },
  callback = function(event)
    vim.bo[event.buf].buflisted = false
    vim.keymap.set('n', 'q', '<cmd>close<CR>', { buffer = event.buf })
  end
})


-- Terminal settings
vim.api.nvim_create_autocmd('TermOpen', {
  callback = function()
    vim.wo.number = false
    vim.wo.relativenumber = false
    vim.cmd('startinsert')
  end
})

21.7 User Commands

21.7.1 Creating User Commands


-- Basic command
vim.api.nvim_create_user_command('Hello', function()
  print('Hello, Neovim!')
end, {})


-- Command with arguments
vim.api.nvim_create_user_command('Greet', function(opts)
  print('Hello, ' .. opts.args)
end, { nargs = 1 })


-- Optional arguments
vim.api.nvim_create_user_command('Log', function(opts)
  local message = opts.args ~= '' and opts.args or 'Default message'
  print('[LOG]', message)
end, { nargs = '?' })


-- Variable arguments
vim.api.nvim_create_user_command('Echo', function(opts)
  print(opts.args)
end, { nargs = '*' })


-- Command with range
vim.api.nvim_create_user_command('Count', function(opts)
  local lines = vim.api.nvim_buf_get_lines(0, opts.line1 - 1, opts.line2, false)
  print('Lines:', #lines)
end, { range = true })


-- Command with completion
vim.api.nvim_create_user_command('Edit', function(opts)
  vim.cmd('edit ' .. opts.args)
end, {
  nargs = 1,
  complete = 'file'  -- File completion
})


-- Custom completion
vim.api.nvim_create_user_command('Theme', function(opts)
  vim.cmd('colorscheme ' .. opts.args)
end, {
  nargs = 1,
  complete = function()
    return vim.fn.getcompletion('', 'color')
  end
})


-- Force command (with bang)
vim.api.nvim_create_user_command('Delete', function(opts)
  if opts.bang then

    -- Force delete
    vim.cmd('bdelete!')
  else
    vim.cmd('bdelete')
  end
end, { bang = true })

21.7.2 Command Options and Attributes


-- With description
vim.api.nvim_create_user_command('Format', function()
  vim.lsp.buf.format()
end, {
  desc = 'Format current buffer'
})


-- Buffer-local command
vim.api.nvim_buf_create_user_command(0, 'BufferCommand', function()
  print('This command is buffer-local')
end, {})


-- Command with count
vim.api.nvim_create_user_command('Jump', function(opts)
  local count = opts.count ~= -1 and opts.count or 1
  vim.cmd('normal! ' .. count .. 'j')
end, { count = true })


-- Register/bar handling
vim.api.nvim_create_user_command('Test', function(opts)

  -- opts.reg contains the register if specified

  -- opts.args contains arguments
  print('Register:', opts.reg)
  print('Args:', opts.args)
end, {
  nargs = '*',
  register = true,
  bar = true  -- Allow command chaining with |
})


-- Access fargs (argument list)
vim.api.nvim_create_user_command('Multi', function(opts)
  for i, arg in ipairs(opts.fargs) do
    print(i, arg)
  end
end, { nargs = '+' })  -- At least one argument

21.7.3 Practical Command Examples


-- Reload configuration
vim.api.nvim_create_user_command('ReloadConfig', function()
  for name, _ in pairs(package.loaded) do
    if name:match('^user') or name:match('^plugins') then
      package.loaded[name] = nil
    end
  end
  dofile(vim.env.MYVIMRC)
  print('Configuration reloaded!')
end, { desc = 'Reload Neovim configuration' })


-- Toggle option
vim.api.nvim_create_user_command('ToggleWrap', function()
  vim.wo.wrap = not vim.wo.wrap
  print('Wrap:', vim.wo.wrap)
end, { desc = 'Toggle line wrapping' })


-- Quick fix list navigation
vim.api.nvim_create_user_command('Cnext', function()
  pcall(vim.cmd, 'cnext')
end, { desc = 'Next quickfix item' })

vim.api.nvim_create_user_command('Cprev', function()
  pcall(vim.cmd, 'cprevious')
end, { desc = 'Previous quickfix item' })


-- Open file at line
vim.api.nvim_create_user_command('EditLine', function(opts)
  local parts = vim.split(opts.args, ':')
  local file = parts[1]
  local line = tonumber(parts[2]) or 1
  
  vim.cmd('edit ' .. file)
  vim.api.nvim_win_set_cursor(0, {line, 0})
end, {
  nargs = 1,
  complete = 'file',
  desc = 'Open file at specific line (file:line)'
})


-- Search and replace in multiple files
vim.api.nvim_create_user_command('Replace', function(opts)
  local args = vim.split(opts.args, ' ')
  if #args < 3 then
    print('Usage: Replace <pattern> <replacement> <files>')
    return
  end
  
  local pattern = args[1]
  local replacement = args[2]
  local files = vim.list_slice(args, 3)
  
  vim.cmd('args ' .. table.concat(files, ' '))
  vim.cmd('argdo %s/' .. pattern .. '/' .. replacement .. '/ge | update')
end, {
  nargs = '+',
  desc = 'Replace pattern in multiple files'
})

21.8 Global Variables

21.8.1 Setting and Getting Variables


-- Global variables (vim.g)
vim.g.mapleader = ' '
vim.g.maplocalleader = ','
vim.g.my_config = {
  theme = 'dark',
  font_size = 12
}


-- Get variable
local leader = vim.g.mapleader
local config = vim.g.my_config


-- Buffer variables (vim.b)
vim.b.is_processed = true
vim.b.custom_setting = 'value'


-- Window variables (vim.w)
vim.w.my_window_data = {
  created_at = os.time()
}


-- Tabpage variables (vim.t)
vim.t.tab_name = 'Main'


-- Environment variables (vim.env)
local home = vim.env.HOME
local path = vim.env.PATH
vim.env.MY_VAR = 'custom value'


-- Check if variable exists
if vim.g.some_var ~= nil then

  -- Variable exists
end


-- Delete variable
vim.g.obsolete_var = nil

21.8.2 Using Variables with Plugins


-- Plugin configuration via globals
vim.g.netrw_banner = 0
vim.g.netrw_liststyle = 3
vim.g.loaded_matchparen = 1  -- Disable plugin


-- Conditional plugin loading
if vim.g.vscode == nil then

  -- Not in VSCode, load normal plugins
  require('plugins.telescope')
else

  -- In VSCode, minimal setup
end


-- Feature flags
vim.g.enable_experimental_features = true

if vim.g.enable_experimental_features then
  require('experimental.features')
end

21.9 Highlights and Colors

21.9.1 Setting Highlights


-- Set highlight group
vim.api.nvim_set_hl(0, 'MyHighlight', {
  fg = '#ff0000',
  bg = '#000000',
  bold = true
})


-- Link highlight groups
vim.api.nvim_set_hl(0, 'MyCustomHL', { link = 'Comment' })


-- Get highlight definition
local hl = vim.api.nvim_get_hl_by_name('Comment', true)
print(vim.inspect(hl))


-- Clear highlight
vim.api.nvim_set_hl(0, 'MyHighlight', {})


-- Common highlight attributes
vim.api.nvim_set_hl(0, 'CustomHL', {
  fg = '#ffffff',           -- Foreground color
  bg = '#000000',           -- Background color
  sp = '#ff0000',           -- Special color (underline, etc.)
  blend = 50,               -- Transparency (0-100)
  bold = true,
  italic = true,
  underline = true,
  undercurl = true,
  strikethrough = true,
  reverse = true,
  standout = true
})


-- Set highlight in namespace
local ns_id = vim.api.nvim_create_namespace('my_namespace')
vim.api.nvim_set_hl(ns_id, 'MyHL', { fg = '#00ff00' })

21.9.2 Temporary Highlights


-- Highlight range temporarily
local ns = vim.api.nvim_create_namespace('temp_highlight')

vim.api.nvim_buf_add_highlight(
  0,        -- buffer
  ns,       -- namespace
  'Search', -- highlight group
  0,        -- line
  0,        -- start col

  -1        -- end col (-1 = end of line)
)


-- Clear highlights in namespace
vim.api.nvim_buf_clear_namespace(0, ns, 0, -1)


-- Set extmark with highlight
vim.api.nvim_buf_set_extmark(0, ns, 0, 0, {
  end_row = 0,
  end_col = 10,
  hl_group = 'IncSearch'
})

21.10 Practical API Utilities

21.10.1 Buffer Utilities

local M = {}


-- Get all listed buffers
function M.get_listed_buffers()
  local buffers = {}
  for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
    if vim.bo[bufnr].buflisted then
      table.insert(buffers, bufnr)
    end
  end
  return buffers
end


-- Get buffer by name
function M.get_buffer_by_name(name)
  for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
    local bufname = vim.api.nvim_buf_get_name(bufnr)
    if bufname:match(name) then
      return bufnr
    end
  end
  return nil
end


-- Check if buffer is empty
function M.is_buffer_empty(bufnr)
  bufnr = bufnr or 0
  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
  return #lines == 1 and lines[1] == ''
end


-- Delete all buffers except current
function M.delete_other_buffers()
  local current = vim.api.nvim_get_current_buf()
  for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do
    if bufnr ~= current and vim.bo[bufnr].buflisted then
      vim.api.nvim_buf_delete(bufnr, { force = false })
    end
  end
end

return M

21.10.2 Window Utilities

local M = {}


-- Center window on screen
function M.center_window()
  local win_h = vim.api.nvim_win_get_height(0)
  local win_pos = vim.api.nvim_win_get_position(0)
  local cursor = vim.api.nvim_win_get_cursor(0)
  
  local target_row = math.floor(win_h / 2)
  local scroll = cursor[1] - target_row - win_pos[1]
  
  vim.cmd('normal! ' .. scroll .. '\\<C-e>')
end


-- Get visible windows
function M.get_visible_windows()
  local windows = {}
  for _, win in ipairs(vim.api.nvim_list_wins()) do
    local config = vim.api.nvim_win_get_config(win)
    if config.relative == '' then  -- Not floating
      table.insert(windows, win)
    end
  end
  return windows
end


-- Get floating windows
function M.get_floating_windows()
  local windows = {}
  for _, win in ipairs(vim.api.nvim_list_wins()) do
    local config = vim.api.nvim_win_get_config(win)
    if config.relative ~= '' then
      table.insert(windows, win)
    end
  end
  return windows
end


-- Close all floating windows
function M.close_all_floating()
  for _, win in ipairs(M.get_floating_windows()) do
    vim.api.nvim_win_close(win, true)
  end
end

return M

21.10.3 Notification Utility

local M = {}


-- Show notification
function M.notify(message, level, opts)
  opts = opts or {}
  level = level or vim.log.levels.INFO
  
  vim.notify(message, level, {
    title = opts.title or 'Notification',
    timeout = opts.timeout or 3000,
    on_open = opts.on_open,
    on_close = opts.on_close
  })
end


-- Convenience wrappers
function M.info(message, opts)
  M.notify(message, vim.log.levels.INFO, opts)
end

function M.warn(message, opts)
  M.notify(message, vim.log.levels.WARN, opts)
end

function M.error(message, opts)
  M.notify(message, vim.log.levels.ERROR, opts)
end

function M.debug(message, opts)
  if vim.g.debug_mode then
    M.notify(message, vim.log.levels.DEBUG, opts)
  end
end

return M

End of Chapter 21: The Neovim Lua API

You now have comprehensive knowledge of Neovim’s Lua API, covering options, buffers, windows, keymaps, autocommands, user commands, variables, highlights, and practical utilities. This chapter provides the foundation for writing powerful Neovim configurations and plugins using idiomatic Lua. The next chapter will explore writing complete Neovim plugins using these API concepts.


Chapter 22: Advanced Lua Patterns in Neovim

This chapter explores advanced Lua programming patterns specifically designed for Neovim plugin development and configuration. You’ll learn about module design, asynchronous programming, event handling, and sophisticated techniques for building robust Neovim extensions.

22.1 Module Organization and Loading

22.1.1 Plugin Structure


-- Standard plugin structure:

-- lua/

--   myplugin/

--     init.lua          -- Main entry point

--     config.lua        -- Configuration

--     utils.lua         -- Utility functions

--     commands.lua      -- User commands

--     autocmds.lua      -- Autocommands

--     keymaps.lua       -- Keybindings

--     health.lua        -- Health checks


-- lua/myplugin/init.lua
local M = {}

M.config = {
  enabled = true,
  debug = false,
  options = {}
}

function M.setup(opts)
  M.config = vim.tbl_deep_extend('force', M.config, opts or {})
  
  if not M.config.enabled then
    return
  end
  
  require('myplugin.commands').setup()
  require('myplugin.autocmds').setup()
  require('myplugin.keymaps').setup()
end

return M

22.1.2 Lazy Loading Patterns


-- Autoload pattern
local M = {}


-- Cache loaded modules
local _cache = {}

function M.load(module_name)
  if not _cache[module_name] then
    _cache[module_name] = require('myplugin.' .. module_name)
  end
  return _cache[module_name]
end


-- Lazy require
local function lazy_require(module)
  return setmetatable({}, {
    __index = function(_, key)
      return require(module)[key]
    end,
    __call = function(_, ...)
      return require(module)(...)
    end
  })
end


-- Usage
local utils = lazy_require('myplugin.utils')
utils.some_function()  -- Module loaded only when called

22.1.3 Configuration Merging

local M = {}


-- Deep merge configurations
function M.merge_config(defaults, user_config)
  local config = vim.deepcopy(defaults)
  
  if not user_config then
    return config
  end
  
  return vim.tbl_deep_extend('force', config, user_config)
end


-- Validate configuration
function M.validate_config(config, schema)
  for key, spec in pairs(schema) do
    local value = config[key]
    

    -- Check required fields
    if spec.required and value == nil then
      error(string.format('Missing required config: %s', key))
    end
    

    -- Check type
    if value ~= nil and spec.type then
      local value_type = type(value)
      if value_type ~= spec.type then
        error(string.format(
          'Invalid type for %s: expected %s, got %s',
          key, spec.type, value_type
        ))
      end
    end
    

    -- Check allowed values
    if value ~= nil and spec.values then
      local found = false
      for _, allowed in ipairs(spec.values) do
        if value == allowed then
          found = true
          break
        end
      end
      if not found then
        error(string.format(
          'Invalid value for %s: %s not in allowed values',
          key, vim.inspect(value)
        ))
      end
    end
    

    -- Validate nested tables
    if spec.schema and type(value) == 'table' then
      M.validate_config(value, spec.schema)
    end
  end
  
  return true
end


-- Example usage
local schema = {
  enabled = { type = 'boolean', required = true },
  level = { type = 'string', values = { 'debug', 'info', 'warn', 'error' } },
  options = {
    type = 'table',
    schema = {
      timeout = { type = 'number' },
      retries = { type = 'number' }
    }
  }
}

local config = {
  enabled = true,
  level = 'info',
  options = {
    timeout = 5000,
    retries = 3
  }
}

M.validate_config(config, schema)

return M

22.2 Asynchronous Programming

22.2.1 Using vim.loop (libuv)

local M = {}
local uv = vim.loop


-- Basic async file read
function M.read_file_async(path, callback)
  uv.fs_open(path, 'r', 438, function(err, fd)
    if err then
      callback(err, nil)
      return
    end
    
    uv.fs_fstat(fd, function(err, stat)
      if err then
        uv.fs_close(fd, function() end)
        callback(err, nil)
        return
      end
      
      uv.fs_read(fd, stat.size, 0, function(err, data)
        uv.fs_close(fd, function() end)
        if err then
          callback(err, nil)
        else
          callback(nil, data)
        end
      end)
    end)
  end)
end


-- Promisify pattern
function M.promisify(fn)
  return function(...)
    local args = {...}
    local co = coroutine.running()
    
    table.insert(args, function(err, result)
      if co then
        coroutine.resume(co, err, result)
      end
    end)
    
    fn(unpack(args))
    
    if co then
      return coroutine.yield()
    end
  end
end


-- Async/await pattern
function M.async(fn)
  return function(...)
    local co = coroutine.create(fn)
    local function step(...)
      local ok, err_or_result = coroutine.resume(co, ...)
      if not ok then
        error(err_or_result)
      end
    end
    step(...)
  end
end


-- Usage example
M.read_file = M.promisify(M.read_file_async)

M.async(function()
  local err, content = M.read_file('/path/to/file.txt')
  if err then
    print('Error:', err)
  else
    print('Content:', content)
  end
end)()

return M

22.2.2 Timer and Scheduling

local M = {}
local uv = vim.loop


-- Debounce function calls
function M.debounce(fn, ms)
  local timer = uv.new_timer()
  return function(...)
    local args = {...}
    timer:stop()
    timer:start(ms, 0, function()
      fn(unpack(args))
    end)
  end
end


-- Throttle function calls
function M.throttle(fn, ms)
  local timer = uv.new_timer()
  local running = false
  
  return function(...)
    if not running then
      local args = {...}
      running = true
      fn(unpack(args))
      
      timer:start(ms, 0, function()
        running = false
      end)
    end
  end
end


-- Schedule callback on main thread
function M.schedule(fn, ...)
  local args = {...}
  vim.schedule(function()
    fn(unpack(args))
  end)
end


-- Defer execution
function M.defer(fn, ms)
  local timer = uv.new_timer()
  timer:start(ms or 0, 0, vim.schedule_wrap(function()
    timer:close()
    fn()
  end))
end


-- Interval execution
function M.interval(fn, ms)
  local timer = uv.new_timer()
  timer:start(ms, ms, vim.schedule_wrap(fn))
  
  return {
    stop = function()
      timer:stop()
      timer:close()
    end
  }
end


-- Usage examples
local debounced_save = M.debounce(function()
  print('Saving...')
end, 500)

local throttled_update = M.throttle(function()
  print('Updating...')
end, 1000)


-- Run every 5 seconds
local interval_handle = M.interval(function()
  print('Periodic task')
end, 5000)


-- Stop after 30 seconds
M.defer(function()
  interval_handle.stop()
end, 30000)

return M

22.2.3 Job Control

local M = {}


-- Run external command
function M.run_command(cmd, opts)
  opts = opts or {}
  local stdout_chunks = {}
  local stderr_chunks = {}
  
  local job_id = vim.fn.jobstart(cmd, {
    on_stdout = function(_, data)
      if data then
        vim.list_extend(stdout_chunks, data)
      end
    end,
    on_stderr = function(_, data)
      if data then
        vim.list_extend(stderr_chunks, data)
      end
    end,
    on_exit = function(_, exit_code)
      local stdout = table.concat(stdout_chunks, '\n')
      local stderr = table.concat(stderr_chunks, '\n')
      
      if opts.on_exit then
        vim.schedule(function()
          opts.on_exit(exit_code, stdout, stderr)
        end)
      end
    end,
    stdout_buffered = true,
    stderr_buffered = true,
  })
  
  return job_id
end


-- Run with input
function M.run_with_input(cmd, input, opts)
  opts = opts or {}
  
  local job_id = M.run_command(cmd, opts)
  
  if input then
    vim.fn.chansend(job_id, input)
    vim.fn.chanclose(job_id, 'stdin')
  end
  
  return job_id
end


-- Run and wait
function M.run_sync(cmd, timeout)
  local result = vim.fn.system(cmd)
  local exit_code = vim.v.shell_error
  
  return {
    exit_code = exit_code,
    stdout = result,
    success = exit_code == 0
  }
end


-- Usage examples
M.run_command({'git', 'status'}, {
  on_exit = function(code, stdout, stderr)
    if code == 0 then
      print('Output:', stdout)
    else
      print('Error:', stderr)
    end
  end
})


-- Async command with callback
function M.git_status(callback)
  M.run_command({'git', 'status', '--short'}, {
    on_exit = function(code, stdout, stderr)
      if code == 0 then
        local files = vim.split(stdout, '\n', { trimempty = true })
        callback(nil, files)
      else
        callback(stderr, nil)
      end
    end
  })
end

return M

22.3 Event System

22.3.1 Custom Event Emitter

local M = {}

function M.new()
  local emitter = {
    _listeners = {}
  }
  

  -- Register event listener
  function emitter:on(event, callback)
    if not self._listeners[event] then
      self._listeners[event] = {}
    end
    table.insert(self._listeners[event], callback)
    

    -- Return unsubscribe function
    return function()
      self:off(event, callback)
    end
  end
  

  -- Register one-time listener
  function emitter:once(event, callback)
    local function wrapper(...)
      callback(...)
      self:off(event, wrapper)
    end
    return self:on(event, wrapper)
  end
  

  -- Remove listener
  function emitter:off(event, callback)
    if not self._listeners[event] then
      return
    end
    
    for i, listener in ipairs(self._listeners[event]) do
      if listener == callback then
        table.remove(self._listeners[event], i)
        break
      end
    end
  end
  

  -- Emit event
  function emitter:emit(event, ...)
    if not self._listeners[event] then
      return
    end
    
    local args = {...}
    for _, listener in ipairs(self._listeners[event]) do
      vim.schedule(function()
        listener(unpack(args))
      end)
    end
  end
  

  -- Remove all listeners for event
  function emitter:remove_all_listeners(event)
    if event then
      self._listeners[event] = nil
    else
      self._listeners = {}
    end
  end
  

  -- Get listener count
  function emitter:listener_count(event)
    if not self._listeners[event] then
      return 0
    end
    return #self._listeners[event]
  end
  
  return emitter
end


-- Usage example
local events = M.new()


-- Subscribe to events
events:on('file:saved', function(filename)
  print('File saved:', filename)
end)

events:once('plugin:loaded', function()
  print('Plugin loaded (once)')
end)


-- Emit events
events:emit('file:saved', 'config.lua')
events:emit('plugin:loaded')

return M

22.3.2 Autocmd Event Wrapper

local M = {}


-- Wrap autocommands as events
function M.create_autocmd_events()
  local emitter = require('myplugin.events').new()
  local augroup = vim.api.nvim_create_augroup('MyPluginEvents', { clear = true })
  
  local event_map = {
    buffer_enter = 'BufEnter',
    buffer_write_pre = 'BufWritePre',
    buffer_write_post = 'BufWritePost',
    file_type = 'FileType',
    cursor_moved = 'CursorMoved',
    insert_enter = 'InsertEnter',
    insert_leave = 'InsertLeave',
  }
  
  for event_name, autocmd_event in pairs(event_map) do
    vim.api.nvim_create_autocmd(autocmd_event, {
      group = augroup,
      callback = function(args)
        emitter:emit(event_name, args)
      end
    })
  end
  
  return emitter
end


-- Usage
local events = M.create_autocmd_events()

events:on('buffer_write_pre', function(args)
  print('About to save:', args.file)
end)

events:on('file_type', function(args)
  if args.match == 'lua' then
    print('Lua file detected')
  end
end)

return M

22.4 State Management

22.4.1 Simple State Container

local M = {}

function M.create_store(initial_state)
  local state = vim.deepcopy(initial_state or {})
  local listeners = {}
  
  local store = {}
  

  -- Get state
  function store.get_state()
    return vim.deepcopy(state)
  end
  

  -- Get specific value
  function store.get(key)
    return vim.deepcopy(state[key])
  end
  

  -- Set state
  function store.set_state(new_state)
    local old_state = vim.deepcopy(state)
    state = vim.deepcopy(new_state)
    

    -- Notify listeners
    for _, listener in ipairs(listeners) do
      vim.schedule(function()
        listener(state, old_state)
      end)
    end
  end
  

  -- Update state
  function store.update(updates)
    local old_state = vim.deepcopy(state)
    state = vim.tbl_deep_extend('force', state, updates)
    

    -- Notify listeners
    for _, listener in ipairs(listeners) do
      vim.schedule(function()
        listener(state, old_state)
      end)
    end
  end
  

  -- Subscribe to changes
  function store.subscribe(listener)
    table.insert(listeners, listener)
    

    -- Return unsubscribe function
    return function()
      for i, l in ipairs(listeners) do
        if l == listener then
          table.remove(listeners, i)
          break
        end
      end
    end
  end
  

  -- Reset to initial state
  function store.reset()
    store.set_state(initial_state)
  end
  
  return store
end


-- Usage example
local store = M.create_store({
  count = 0,
  name = 'Plugin',
  enabled = true
})


-- Subscribe to changes
local unsubscribe = store.subscribe(function(new_state, old_state)
  print('State changed:', vim.inspect(new_state))
end)


-- Update state
store.update({ count = store.get('count') + 1 })
store.update({ enabled = false })


-- Get state
local current_state = store.get_state()
print(vim.inspect(current_state))

return M

22.4.2 Persistent State

local M = {}


-- Save state to file
function M.save_state(state, filepath)
  local content = vim.inspect(state)
  local file = io.open(filepath, 'w')
  if file then
    file:write('return ' .. content)
    file:close()
    return true
  end
  return false
end


-- Load state from file
function M.load_state(filepath)
  if vim.fn.filereadable(filepath) == 0 then
    return nil
  end
  
  local ok, state = pcall(dofile, filepath)
  if ok then
    return state
  end
  return nil
end


-- Create persistent store
function M.create_persistent_store(name, initial_state)
  local data_path = vim.fn.stdpath('data')
  local filepath = string.format('%s/%s.lua', data_path, name)
  

  -- Load existing state or use initial
  local state = M.load_state(filepath) or initial_state
  
  local store = require('myplugin.state').create_store(state)
  

  -- Wrap update to auto-save
  local original_update = store.update
  function store.update(updates)
    original_update(updates)
    M.save_state(store.get_state(), filepath)
  end
  
  local original_set_state = store.set_state
  function store.set_state(new_state)
    original_set_state(new_state)
    M.save_state(new_state, filepath)
  end
  
  return store
end


-- Usage
local persistent_store = M.create_persistent_store('my_plugin_state', {
  last_used = os.time(),
  preferences = {}
})

return M

22.5 Memoization and Caching

22.5.1 Function Memoization

local M = {}


-- Basic memoization
function M.memoize(fn)
  local cache = {}
  
  return function(...)
    local key = vim.inspect({...})
    
    if cache[key] == nil then
      cache[key] = fn(...)
    end
    
    return cache[key]
  end
end


-- Memoize with TTL (time to live)
function M.memoize_with_ttl(fn, ttl_ms)
  local cache = {}
  
  return function(...)
    local key = vim.inspect({...})
    local now = vim.loop.now()
    
    if cache[key] then
      if now - cache[key].timestamp < ttl_ms then
        return cache[key].value
      end
    end
    
    local value = fn(...)
    cache[key] = {
      value = value,
      timestamp = now
    }
    
    return value
  end
end


-- Memoize with custom key function
function M.memoize_by(fn, key_fn)
  local cache = {}
  
  return function(...)
    local key = key_fn(...)
    
    if cache[key] == nil then
      cache[key] = fn(...)
    end
    
    return cache[key]
  end
end


-- LRU Cache
function M.create_lru_cache(max_size)
  local cache = {}
  local order = {}
  
  local function update_access(key)
    for i, k in ipairs(order) do
      if k == key then
        table.remove(order, i)
        break
      end
    end
    table.insert(order, key)
    

    -- Remove oldest if over limit
    if #order > max_size then
      local oldest = table.remove(order, 1)
      cache[oldest] = nil
    end
  end
  
  return {
    get = function(key)
      if cache[key] ~= nil then
        update_access(key)
        return cache[key]
      end
      return nil
    end,
    
    set = function(key, value)
      cache[key] = value
      update_access(key)
    end,
    
    clear = function()
      cache = {}
      order = {}
    end,
    
    size = function()
      return #order
    end
  }
end


-- Usage examples
local expensive_fn = M.memoize(function(n)

  -- Simulate expensive computation
  vim.wait(100)
  return n * n
end)

print(expensive_fn(5))  -- Slow first call
print(expensive_fn(5))  -- Fast cached call

local cached_api_call = M.memoize_with_ttl(function(endpoint)
  return vim.fn.system('curl ' .. endpoint)
end, 60000)  -- Cache for 1 minute

return M

22.5.2 Result Caching Decorator

local M = {}


-- Cache decorator
function M.cached(opts)
  opts = opts or {}
  local ttl = opts.ttl
  local key_fn = opts.key or function(...) return vim.inspect({...}) end
  
  return function(fn)
    local cache = {}
    
    return function(...)
      local key = key_fn(...)
      local now = vim.loop.now()
      

      -- Check cache validity
      if cache[key] then
        if not ttl or (now - cache[key].timestamp < ttl) then
          return unpack(cache[key].value)
        end
      end
      

      -- Call function and cache result
      local result = {fn(...)}
      cache[key] = {
        value = result,
        timestamp = now
      }
      
      return unpack(result)
    end
  end
end


-- Usage with decorator pattern
local get_git_branch = M.cached({ ttl = 5000 })(function()
  return vim.fn.system('git rev-parse --abbrev-ref HEAD'):gsub('\n', '')
end)


-- Multiple calls within 5 seconds use cache
print(get_git_branch())
print(get_git_branch())  -- Cached

return M

22.6 Error Handling Patterns

22.6.1 Safe Function Execution

local M = {}


-- Safely execute function with error handling
function M.safe_call(fn, ...)
  local ok, result = pcall(fn, ...)
  if not ok then
    vim.notify(
      'Error: ' .. tostring(result),
      vim.log.levels.ERROR
    )
    return nil, result
  end
  return result, nil
end


-- Try-catch pattern
function M.try(fn)
  return {
    catch = function(self, handler)
      local ok, err = pcall(fn)
      if not ok and handler then
        handler(err)
      end
      return self
    end,
    
    finally = function(self, handler)
      if handler then
        handler()
      end
      return self
    end
  }
end


-- Retry with exponential backoff
function M.retry(fn, max_attempts, initial_delay)
  max_attempts = max_attempts or 3
  initial_delay = initial_delay or 100
  
  local attempt = 1
  local delay = initial_delay
  
  while attempt <= max_attempts do
    local ok, result = pcall(fn)
    if ok then
      return result
    end
    
    if attempt < max_attempts then
      vim.wait(delay)
      delay = delay * 2  -- Exponential backoff
      attempt = attempt + 1
    else
      error('Max retry attempts reached')
    end
  end
end


-- Usage examples
M.try(function()
  error('Something went wrong')
end)
:catch(function(err)
  print('Caught error:', err)
end)
:finally(function()
  print('Cleanup')
end)

local result = M.retry(function()

  -- Potentially failing operation
  return api_call()
end, 3, 100)

return M

22.6.2 Validation and Assertions

local M = {}


-- Assert with custom message
function M.assert(condition, message, ...)
  if not condition then
    local msg = string.format(message or 'Assertion failed', ...)
    error(msg, 2)
  end
  return condition
end


-- Type checking
function M.check_type(value, expected_type, name)
  local actual_type = type(value)
  if actual_type ~= expected_type then
    error(string.format(
      '%s: expected %s, got %s',
      name or 'argument',
      expected_type,
      actual_type
    ), 2)
  end
  return value
end


-- Multiple type checking
function M.check_types(value, expected_types, name)
  local actual_type = type(value)
  for _, expected in ipairs(expected_types) do
    if actual_type == expected then
      return value
    end
  end
  
  error(string.format(
    '%s: expected one of %s, got %s',
    name or 'argument',
    table.concat(expected_types, ', '),
    actual_type
  ), 2)
end


-- Range checking
function M.check_range(value, min, max, name)
  M.check_type(value, 'number', name)
  
  if value < min or value > max then
    error(string.format(
      '%s: value %d out of range [%d, %d]',
      name or 'argument',
      value,
      min,
      max
    ), 2)
  end
  
  return value
end


-- Not nil check
function M.check_not_nil(value, name)
  if value == nil then
    error(string.format(
      '%s cannot be nil',
      name or 'argument'
    ), 2)
  end
  return value
end


-- Function argument validator
function M.validate_args(args, schema)
  for i, spec in ipairs(schema) do
    local value = args[i]
    local name = spec.name or ('arg' .. i)
    

    -- Required check
    if spec.required and value == nil then
      error(string.format('Missing required argument: %s', name), 2)
    end
    

    -- Type check
    if value ~= nil and spec.type then
      if type(spec.type) == 'table' then
        M.check_types(value, spec.type, name)
      else
        M.check_type(value, spec.type, name)
      end
    end
    

    -- Custom validator
    if value ~= nil and spec.validator then
      if not spec.validator(value) then
        error(string.format(
          'Validation failed for argument: %s',
          name
        ), 2)
      end
    end
  end
end


-- Usage examples
function some_function(name, age, options)
  M.validate_args({name, age, options}, {
    { name = 'name', type = 'string', required = true },
    { name = 'age', type = 'number', required = true,
      validator = function(v) return v >= 0 and v <= 150 end },
    { name = 'options', type = 'table' }
  })
  

  -- Function implementation
end

return M

22.7 Functional Programming Patterns

22.7.1 Functional Utilities

local M = {}


-- Map over list
function M.map(list, fn)
  local result = {}
  for i, value in ipairs(list) do
    result[i] = fn(value, i)
  end
  return result
end


-- Filter list
function M.filter(list, predicate)
  local result = {}
  for _, value in ipairs(list) do
    if predicate(value) then
      table.insert(result, value)
    end
  end
  return result
end


-- Reduce list
function M.reduce(list, fn, initial)
  local acc = initial
  for i, value in ipairs(list) do
    if acc == nil and i == 1 then
      acc = value
    else
      acc = fn(acc, value, i)
    end
  end
  return acc
end


-- Find first matching item
function M.find(list, predicate)
  for i, value in ipairs(list) do
    if predicate(value, i) then
      return value, i
    end
  end
  return nil
end


-- Check if all items match
function M.all(list, predicate)
  for i, value in ipairs(list) do
    if not predicate(value, i) then
      return false
    end
  end
  return true
end


-- Check if any item matches
function M.any(list, predicate)
  for i, value in ipairs(list) do
    if predicate(value, i) then
      return true
    end
  end
  return false
end


-- Flatten nested list
function M.flatten(list)
  local result = {}
  for _, value in ipairs(list) do
    if type(value) == 'table' then
      vim.list_extend(result, M.flatten(value))
    else
      table.insert(result, value)
    end
  end
  return result
end


-- Group by key
function M.group_by(list, key_fn)
  local result = {}
  for _, value in ipairs(list) do
    local key = key_fn(value)
    if not result[key] then
      result[key] = {}
    end
    table.insert(result[key], value)
  end
  return result
end


-- Partition into two lists
function M.partition(list, predicate)
  local truthy = {}
  local falsy = {}
  
  for _, value in ipairs(list) do
    if predicate(value) then
      table.insert(truthy, value)
    else
      table.insert(falsy, value)
    end
  end
  
  return truthy, falsy
end


-- Usage examples
local numbers = {1, 2, 3, 4, 5}

local doubled = M.map(numbers, function(n) return n * 2 end)

-- {2, 4, 6, 8, 10}

local evens = M.filter(numbers, function(n) return n % 2 == 0 end)

-- {2, 4}

local sum = M.reduce(numbers, function(acc, n) return acc + n end, 0)

-- 15

return M

22.7.2 Function Composition

local M = {}


-- Compose functions (right to left)
function M.compose(...)
  local fns = {...}
  
  return function(...)
    local result = {...}
    for i = #fns, 1, -1 do
      result = {fns[i](unpack(result))}
    end
    return unpack(result)
  end
end


-- Pipe functions (left to right)
function M.pipe(...)
  local fns = {...}
  
  return function(...)
    local result = {...}
    for _, fn in ipairs(fns) do
      result = {fn(unpack(result))}
    end
    return unpack(result)
  end
end


-- Partial application
function M.partial(fn, ...)
  local bound_args = {...}
  
  return function(...)
    local args = vim.deepcopy(bound_args)
    vim.list_extend(args, {...})
    return fn(unpack(args))
  end
end


-- Curry function
function M.curry(fn, arity)
  arity = arity or debug.getinfo(fn, 'u').nparams
  
  local function curried(args)
    return function(...)
      local new_args = vim.deepcopy(args)
      vim.list_extend(new_args, {...})
      
      if #new_args >= arity then
        return fn(unpack(new_args))
      else
        return curried(new_args)
      end
    end
  end
  
  return curried({})
end


-- Usage examples
local add = function(a, b) return a + b end
local multiply = function(a, b) return a * b end
local square = function(x) return x * x end


-- Composition
local square_then_double = M.compose(
  function(x) return x * 2 end,
  square
)
print(square_then_double(3))  -- (3^2) * 2 = 18


-- Pipe
local double_then_square = M.pipe(
  function(x) return x * 2 end,
  square
)
print(double_then_square(3))  -- (3 * 2)^2 = 36


-- Partial application
local add5 = M.partial(add, 5)
print(add5(3))  -- 8


-- Currying
local curried_add = M.curry(add)
print(curried_add(5)(3))  -- 8

return M

22.8 Plugin Development Patterns

22.8.1 Health Check System


-- lua/myplugin/health.lua
local M = {}

local health = vim.health or require('health')

function M.check()
  health.report_start('My Plugin')
  

  -- Check Neovim version
  local required_version = {0, 8, 0}
  if vim.fn.has('nvim-' .. table.concat(required_version, '.')) == 1 then
    health.report_ok('Neovim version >= ' .. table.concat(required_version, '.'))
  else
    health.report_error(
      'Neovim version too old',
      'Upgrade to >= ' .. table.concat(required_version, '.')
    )
  end
  

  -- Check for required plugins
  local has_telescope = pcall(require, 'telescope')
  if has_telescope then
    health.report_ok('telescope.nvim is installed')
  else
    health.report_warn(
      'telescope.nvim not found',
      'Some features will be unavailable'
    )
  end
  

  -- Check for external dependencies
  if vim.fn.executable('rg') == 1 then
    health.report_ok('ripgrep is installed')
  else
    health.report_error(
      'ripgrep not found in PATH',
      'Install ripgrep for search functionality'
    )
  end
  

  -- Check configuration
  local config = require('myplugin').config
  if config.api_key then
    health.report_ok('API key is configured')
  else
    health.report_info('API key not set (optional)')
  end
  

  -- Check file permissions
  local data_dir = vim.fn.stdpath('data') .. '/myplugin'
  if vim.fn.isdirectory(data_dir) == 1 then
    if vim.fn.filewritable(data_dir) == 2 then
      health.report_ok('Data directory is writable')
    else
      health.report_error(
        'Data directory is not writable: ' .. data_dir
      )
    end
  else
    health.report_info('Data directory will be created on first use')
  end
end

return M

22.8.2 Command Builder Pattern

local M = {}

function M.create_command_builder(name)
  local builder = {
    _name = name,
    _callback = nil,
    _opts = {}
  }
  
  function builder:callback(fn)
    self._callback = fn
    return self
  end
  
  function builder:nargs(value)
    self._opts.nargs = value
    return self
  end
  
  function builder:range(value)
    self._opts.range = value or true
    return self
  end
  
  function builder:bang(value)
    self._opts.bang = value or true
    return self
  end
  
  function builder:complete(fn)
    self._opts.complete = fn
    return self
  end
  
  function builder:desc(text)
    self._opts.desc = text
    return self
  end
  
  function builder:buffer(bufnr)
    self._opts.buffer = bufnr
    return self
  end
  
  function builder:build()
    if self._opts.buffer then
      vim.api.nvim_buf_create_user_command(
        self._opts.buffer,
        self._name,
        self._callback,
        self._opts
      )
    else
      vim.api.nvim_create_user_command(
        self._name,
        self._callback,
        self._opts
      )
    end
  end
  
  return builder
end


-- Usage
M.create_command_builder('MyCommand')
  :callback(function(opts)
    print('Args:', opts.args)
  end)
  :nargs('*')
  :desc('My custom command')
  :complete('file')
  :build()

return M

22.8.3 Telescope Integration Pattern

local M = {}

function M.create_telescope_picker(opts)
  local pickers = require('telescope.pickers')
  local finders = require('telescope.finders')
  local conf = require('telescope.config').values
  local actions = require('telescope.actions')
  local action_state = require('telescope.actions.state')
  
  opts = opts or {}
  
  return function(custom_opts)
    custom_opts = custom_opts or {}
    
    pickers.new(custom_opts, {
      prompt_title = opts.prompt_title or 'Picker',
      finder = finders.new_table {
        results = opts.results or {},
        entry_maker = opts.entry_maker or function(entry)
          return {
            value = entry,
            display = entry,
            ordinal = entry,
          }
        end
      },
      sorter = conf.generic_sorter(custom_opts),
      attach_mappings = function(prompt_bufnr, map)
        actions.select_default:replace(function()
          local selection = action_state.get_selected_entry()
          actions.close(prompt_bufnr)
          
          if opts.on_select then
            opts.on_select(selection.value)
          end
        end)
        
        if opts.mappings then
          for mode, mode_mappings in pairs(opts.mappings) do
            for key, action in pairs(mode_mappings) do
              map(mode, key, action)
            end
          end
        end
        
        return true
      end,
    }):find()
  end
end


-- Usage example
local my_picker = M.create_telescope_picker({
  prompt_title = 'My Custom Picker',
  results = {'Option 1', 'Option 2', 'Option 3'},
  on_select = function(item)
    print('Selected:', item)
  end,
  mappings = {
    i = {
      ['<C-d>'] = function(bufnr)

        -- Custom action
        print('Custom action triggered')
      end
    }
  }
})


-- Call picker
my_picker()

return M

End of Chapter 22: Advanced Lua Patterns in Neovim

You now have comprehensive knowledge of advanced Lua programming patterns for Neovim, including module organization, asynchronous programming, event systems, state management, memoization, error handling, functional programming, and plugin development patterns. These techniques will help you build robust, maintainable, and performant Neovim plugins and configurations. The next chapter will explore LSP (Language Server Protocol) integration and custom language server configurations.


Chapter 23: Writing Neovim Plugins in Lua

This chapter guides you through the complete process of creating professional Neovim plugins using Lua. You’ll learn plugin architecture, best practices, testing strategies, documentation, and distribution methods.

23.1 Plugin Architecture Fundamentals

23.1.1 Standard Plugin Structure


-- Recommended directory structure:

-- my-plugin.nvim/

--   ├── lua/

--   │   └── my-plugin/

--   │       ├── init.lua           -- Main entry point

--   │       ├── config.lua         -- Configuration handling

--   │       ├── core.lua           -- Core functionality

--   │       ├── ui.lua             -- UI components

--   │       ├── utils.lua          -- Utility functions

--   │       └── commands.lua       -- User commands

--   ├── plugin/

--   │   └── my-plugin.lua          -- Auto-load setup

--   ├── doc/

--   │   └── my-plugin.txt          -- Help documentation

--   ├── README.md

--   └── LICENSE


-- lua/my-plugin/init.lua
local M = {}


-- Private state
local _state = {
  initialized = false,
  config = {}
}


-- Default configuration
M.defaults = {
  enabled = true,
  debug = false,
  keymaps = {
    enable = true,
    prefix = '<leader>mp'
  },
  ui = {
    border = 'rounded',
    width = 80,
    height = 20
  }
}


-- Setup function (main API)
function M.setup(opts)
  if _state.initialized then
    vim.notify('my-plugin already initialized', vim.log.levels.WARN)
    return
  end


  -- Merge user config with defaults
  _state.config = vim.tbl_deep_extend('force', M.defaults, opts or {})
  
  if not _state.config.enabled then
    return
  end


  -- Initialize components
  require('my-plugin.commands').setup(_state.config)
  
  if _state.config.keymaps.enable then
    require('my-plugin.keymaps').setup(_state.config)
  end

  _state.initialized = true
  
  if _state.config.debug then
    vim.notify('my-plugin initialized', vim.log.levels.INFO)
  end
end


-- Public API functions
function M.do_something(arg)
  if not _state.initialized then
    vim.notify('my-plugin not initialized', vim.log.levels.ERROR)
    return
  end
  
  require('my-plugin.core').do_something(arg, _state.config)
end


-- Get current configuration
function M.get_config()
  return vim.deepcopy(_state.config)
end


-- Check if initialized
function M.is_initialized()
  return _state.initialized
end

return M

23.1.2 Lazy Loading Setup


-- plugin/my-plugin.lua

-- This file is auto-sourced by Neovim

if vim.g.loaded_my_plugin then
  return
end
vim.g.loaded_my_plugin = 1


-- Create commands that lazy-load the plugin
vim.api.nvim_create_user_command('MyPlugin', function(opts)
  require('my-plugin').do_something(opts.args)
end, {
  nargs = '*',
  desc = 'My plugin command'
})


-- Auto-load on specific events
vim.api.nvim_create_autocmd('FileType', {
  pattern = {'lua', 'vim'},
  callback = function()

    -- Lazy load plugin
    require('my-plugin').setup()
  end,
  once = true
})

23.1.3 Configuration Management


-- lua/my-plugin/config.lua
local M = {}


-- Configuration schema for validation
M.schema = {
  enabled = { type = 'boolean', default = true },
  debug = { type = 'boolean', default = false },
  log_level = {
    type = 'string',
    default = 'info',
    enum = { 'debug', 'info', 'warn', 'error' }
  },
  keymaps = {
    type = 'table',
    default = {},
    schema = {
      enable = { type = 'boolean', default = true },
      prefix = { type = 'string', default = '<leader>mp' }
    }
  },
  ui = {
    type = 'table',
    default = {},
    schema = {
      border = {
        type = 'string',
        default = 'rounded',
        enum = { 'none', 'single', 'double', 'rounded', 'solid', 'shadow' }
      },
      width = {
        type = 'number',
        default = 80,
        validator = function(v) return v > 0 and v <= 200 end
      },
      height = {
        type = 'number',
        default = 20,
        validator = function(v) return v > 0 and v <= 100 end
      }
    }
  }
}


-- Validate configuration against schema
function M.validate(config, schema)
  schema = schema or M.schema
  
  for key, spec in pairs(schema) do
    local value = config[key]
    

    -- Use default if not provided
    if value == nil then
      if spec.default ~= nil then
        config[key] = vim.deepcopy(spec.default)
      end
      value = config[key]
    end
    

    -- Type validation
    if value ~= nil and spec.type then
      local actual_type = type(value)
      if actual_type ~= spec.type then
        return false, string.format(
          'Invalid type for "%s": expected %s, got %s',
          key, spec.type, actual_type
        )
      end
    end
    

    -- Enum validation
    if value ~= nil and spec.enum then
      local valid = false
      for _, allowed in ipairs(spec.enum) do
        if value == allowed then
          valid = true
          break
        end
      end
      if not valid then
        return false, string.format(
          'Invalid value for "%s": must be one of %s',
          key, vim.inspect(spec.enum)
        )
      end
    end
    

    -- Custom validator
    if value ~= nil and spec.validator then
      if not spec.validator(value) then
        return false, string.format(
          'Validation failed for "%s"',
          key
        )
      end
    end
    

    -- Nested validation
    if spec.schema and type(value) == 'table' then
      local ok, err = M.validate(value, spec.schema)
      if not ok then
        return false, key .. '.' .. err
      end
    end
  end
  
  return true, nil
end


-- Get configuration with validation
function M.get(user_config)
  local config = vim.deepcopy(user_config or {})
  
  local ok, err = M.validate(config)
  if not ok then
    error('Configuration error: ' .. err)
  end
  
  return config
end


-- Update configuration at runtime
function M.update(current_config, updates)
  local new_config = vim.tbl_deep_extend('force', current_config, updates)
  
  local ok, err = M.validate(new_config)
  if not ok then
    return nil, err
  end
  
  return new_config, nil
end

return M

23.2 Core Functionality Implementation

23.2.1 Buffer Management


-- lua/my-plugin/buffer.lua
local M = {}


-- Create a scratch buffer
function M.create_scratch_buffer(opts)
  opts = opts or {}
  
  local buf = vim.api.nvim_create_buf(false, true)
  

  -- Set buffer options
  vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile')
  vim.api.nvim_buf_set_option(buf, 'bufhidden', opts.bufhidden or 'wipe')
  vim.api.nvim_buf_set_option(buf, 'swapfile', false)
  vim.api.nvim_buf_set_option(buf, 'filetype', opts.filetype or 'my-plugin')
  
  if opts.name then
    vim.api.nvim_buf_set_name(buf, opts.name)
  end
  

  -- Set buffer local keymaps
  if opts.keymaps then
    for mode, mappings in pairs(opts.keymaps) do
      for lhs, rhs in pairs(mappings) do
        vim.api.nvim_buf_set_keymap(buf, mode, lhs, rhs, {
          noremap = true,
          silent = true,
          nowait = true
        })
      end
    end
  end
  
  return buf
end


-- Manage buffer content
function M.set_content(buf, lines)
  vim.api.nvim_buf_set_option(buf, 'modifiable', true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
  vim.api.nvim_buf_set_option(buf, 'modifiable', false)
  vim.api.nvim_buf_set_option(buf, 'modified', false)
end


-- Append to buffer
function M.append(buf, lines)
  vim.api.nvim_buf_set_option(buf, 'modifiable', true)
  local line_count = vim.api.nvim_buf_line_count(buf)
  vim.api.nvim_buf_set_lines(buf, line_count, line_count, false, lines)
  vim.api.nvim_buf_set_option(buf, 'modifiable', false)
end


-- Clear buffer
function M.clear(buf)
  M.set_content(buf, {})
end


-- Add highlights to buffer
function M.add_highlight(buf, ns_id, hl_group, line, col_start, col_end)
  vim.api.nvim_buf_add_highlight(
    buf,
    ns_id,
    hl_group,
    line,
    col_start,
    col_end or -1
  )
end


-- Create namespace for highlights
function M.create_namespace(name)
  return vim.api.nvim_create_namespace(name)
end


-- Clear namespace highlights
function M.clear_namespace(buf, ns_id)
  vim.api.nvim_buf_clear_namespace(buf, ns_id, 0, -1)
end


-- Check if buffer is valid
function M.is_valid(buf)
  return buf and vim.api.nvim_buf_is_valid(buf)
end


-- Delete buffer safely
function M.delete(buf)
  if M.is_valid(buf) then
    vim.api.nvim_buf_delete(buf, { force = true })
  end
end

return M

23.2.2 Window Management


-- lua/my-plugin/window.lua
local M = {}


-- Create a floating window
function M.create_float(buf, opts)
  opts = opts or {}
  

  -- Calculate dimensions
  local width = opts.width or 80
  local height = opts.height or 20
  

  -- Handle percentage values
  if width < 1 then
    width = math.floor(vim.o.columns * width)
  end
  if height < 1 then
    height = math.floor(vim.o.lines * height)
  end
  

  -- Calculate position
  local row = opts.row or math.floor((vim.o.lines - height) / 2)
  local col = opts.col or math.floor((vim.o.columns - width) / 2)
  

  -- Window configuration
  local win_opts = {
    relative = opts.relative or 'editor',
    width = width,
    height = height,
    row = row,
    col = col,
    style = 'minimal',
    border = opts.border or 'rounded',
    focusable = opts.focusable ~= false,
    zindex = opts.zindex or 50
  }
  
  if opts.title then
    win_opts.title = opts.title
    win_opts.title_pos = opts.title_pos or 'center'
  end
  

  -- Create window
  local win = vim.api.nvim_open_win(buf, opts.enter ~= false, win_opts)
  

  -- Set window options
  if opts.wrap ~= nil then
    vim.api.nvim_win_set_option(win, 'wrap', opts.wrap)
  end
  
  if opts.cursorline ~= nil then
    vim.api.nvim_win_set_option(win, 'cursorline', opts.cursorline)
  end
  

  -- Set window local keymaps
  if opts.keymaps then
    for mode, mappings in pairs(opts.keymaps) do
      for lhs, rhs in pairs(mappings) do
        vim.api.nvim_buf_set_keymap(buf, mode, lhs, rhs, {
          noremap = true,
          silent = true,
          nowait = true
        })
      end
    end
  end
  
  return win
end


-- Create a split window
function M.create_split(buf, opts)
  opts = opts or {}
  

  -- Create split
  local cmd = opts.vertical and 'vsplit' or 'split'
  vim.cmd(cmd)
  

  -- Get the new window
  local win = vim.api.nvim_get_current_win()
  

  -- Set buffer in window
  vim.api.nvim_win_set_buf(win, buf)
  

  -- Set dimensions
  if opts.size then
    if opts.vertical then
      vim.api.nvim_win_set_width(win, opts.size)
    else
      vim.api.nvim_win_set_height(win, opts.size)
    end
  end
  
  return win
end


-- Close window safely
function M.close(win)
  if win and vim.api.nvim_win_is_valid(win) then
    vim.api.nvim_win_close(win, true)
  end
end


-- Center cursor in window
function M.center_cursor(win)
  if not vim.api.nvim_win_is_valid(win) then
    return
  end
  
  local buf = vim.api.nvim_win_get_buf(win)
  local line_count = vim.api.nvim_buf_line_count(buf)
  local height = vim.api.nvim_win_get_height(win)
  
  local target_line = math.floor(line_count / 2)
  
  vim.api.nvim_win_set_cursor(win, { target_line, 0 })
  vim.api.nvim_win_call(win, function()
    vim.cmd('normal! zz')
  end)
end


-- Make window manager class
function M.create_manager()
  local manager = {
    windows = {},
    buffers = {}
  }
  
  function manager:create_window(opts)
    local buf = require('my-plugin.buffer').create_scratch_buffer(opts.buffer)
    local win = M.create_float(buf, opts.window)
    
    table.insert(self.windows, win)
    table.insert(self.buffers, buf)
    
    return win, buf
  end
  
  function manager:close_all()
    for _, win in ipairs(self.windows) do
      M.close(win)
    end
    
    for _, buf in ipairs(self.buffers) do
      require('my-plugin.buffer').delete(buf)
    end
    
    self.windows = {}
    self.buffers = {}
  end
  
  return manager
end

return M

23.2.3 User Interface Components


-- lua/my-plugin/ui.lua
local M = {}
local buffer = require('my-plugin.buffer')
local window = require('my-plugin.window')


-- Create a selection menu
function M.create_menu(items, opts)
  opts = opts or {}
  

  -- Create buffer
  local buf = buffer.create_scratch_buffer({
    filetype = 'my-plugin-menu'
  })
  

  -- Format items
  local lines = {}
  for i, item in ipairs(items) do
    local line = string.format('%d. %s', i, item.text or tostring(item))
    table.insert(lines, line)
  end
  
  buffer.set_content(buf, lines)
  

  -- Create window
  local win = window.create_float(buf, {
    width = opts.width or 60,
    height = math.min(#items + 2, opts.max_height or 20),
    title = opts.title or 'Select Item',
    border = opts.border or 'rounded',
    cursorline = true
  })
  

  -- Add selection callback
  vim.api.nvim_buf_set_keymap(buf, 'n', '<CR>', '', {
    noremap = true,
    silent = true,
    callback = function()
      local cursor = vim.api.nvim_win_get_cursor(win)
      local line = cursor[1]
      
      window.close(win)
      buffer.delete(buf)
      
      if opts.on_select and items[line] then
        opts.on_select(items[line], line)
      end
    end
  })
  

  -- Add close keymaps
  for _, key in ipairs({'q', '<Esc>'}) do
    vim.api.nvim_buf_set_keymap(buf, 'n', key, '', {
      noremap = true,
      silent = true,
      callback = function()
        window.close(win)
        buffer.delete(buf)
        if opts.on_close then
          opts.on_close()
        end
      end
    })
  end
  
  return win, buf
end


-- Create an input dialog
function M.create_input(opts)
  opts = opts or {}
  
  vim.ui.input({
    prompt = opts.prompt or 'Input: ',
    default = opts.default or '',
  }, function(input)
    if input and opts.on_submit then
      opts.on_submit(input)
    elseif opts.on_cancel then
      opts.on_cancel()
    end
  end)
end


-- Create a confirmation dialog
function M.create_confirm(message, opts)
  opts = opts or {}
  
  local choices = opts.choices or {'Yes', 'No'}
  
  vim.ui.select(choices, {
    prompt = message,
  }, function(choice, idx)
    if choice and opts.on_confirm then
      opts.on_confirm(idx == 1, choice, idx)
    end
  end)
end


-- Create a progress indicator
function M.create_progress(opts)
  opts = opts or {}
  
  local progress = {
    _current = 0,
    _total = opts.total or 100,
    _title = opts.title or 'Progress',
    _buf = nil,
    _win = nil,
    _ns = buffer.create_namespace('my-plugin-progress')
  }
  
  function progress:start()
    self._buf = buffer.create_scratch_buffer({
      filetype = 'my-plugin-progress'
    })
    
    self._win = window.create_float(self._buf, {
      width = 50,
      height = 3,
      title = self._title,
      border = 'rounded'
    })
    
    self:_render()
  end
  
  function progress:update(current, message)
    self._current = current
    self._message = message
    self:_render()
  end
  
  function progress:_render()
    if not buffer.is_valid(self._buf) then
      return
    end
    
    local percentage = math.floor((self._current / self._total) * 100)
    local bar_width = 40
    local filled = math.floor((percentage / 100) * bar_width)
    
    local bar = string.rep('█', filled) .. string.rep('░', bar_width - filled)
    local lines = {
      string.format('%s %d%%', bar, percentage),
    }
    
    if self._message then
      table.insert(lines, self._message)
    end
    
    buffer.set_content(self._buf, lines)
  end
  
  function progress:finish()
    if self._win then
      vim.defer_fn(function()
        window.close(self._win)
        buffer.delete(self._buf)
      end, 500)
    end
  end
  
  return progress
end


-- Create a notification
function M.notify(message, level, opts)
  opts = opts or {}
  
  vim.notify(message, level or vim.log.levels.INFO, {
    title = opts.title or 'My Plugin',
    timeout = opts.timeout or 3000,
    on_open = opts.on_open,
    on_close = opts.on_close
  })
end

return M

23.3 Command System

23.3.1 User Command Registration


-- lua/my-plugin/commands.lua
local M = {}


-- Command registry
local _commands = {}


-- Register a command
function M.register(name, handler, opts)
  opts = opts or {}
  
  _commands[name] = {
    handler = handler,
    opts = opts
  }
  
  vim.api.nvim_create_user_command(name, function(cmd_opts)

    -- Parse arguments
    local args = {}
    if cmd_opts.args and cmd_opts.args ~= '' then
      args = vim.split(cmd_opts.args, '%s+')
    end
    

    -- Execute handler
    local ok, err = pcall(handler, {
      args = args,
      bang = cmd_opts.bang,
      line1 = cmd_opts.line1,
      line2 = cmd_opts.line2,
      range = cmd_opts.range,
      count = cmd_opts.count,
      mods = cmd_opts.mods,
      fargs = cmd_opts.fargs
    })
    
    if not ok then
      vim.notify(
        string.format('Command error: %s', err),
        vim.log.levels.ERROR
      )
    end
  end, opts)
end


-- Unregister a command
function M.unregister(name)
  if _commands[name] then
    pcall(vim.api.nvim_del_user_command, name)
    _commands[name] = nil
  end
end


-- Setup default commands
function M.setup(config)

  -- Main command
  M.register('MyPlugin', function(opts)
    local subcommand = opts.args[1]
    
    if not subcommand then
      require('my-plugin.ui').notify('No subcommand specified', vim.log.levels.WARN)
      return
    end
    

    -- Route to subcommand
    local cmd_name = 'MyPlugin' .. subcommand:sub(1,1):upper() .. subcommand:sub(2)
    if _commands[cmd_name] then

      -- Remove subcommand from args
      table.remove(opts.args, 1)
      _commands[cmd_name].handler(opts)
    else
      require('my-plugin.ui').notify(
        string.format('Unknown subcommand: %s', subcommand),
        vim.log.levels.ERROR
      )
    end
  end, {
    nargs = '*',
    desc = 'My plugin main command',
    complete = function(arg_lead, cmd_line, cursor_pos)
      local subcommands = {
        'open', 'close', 'toggle', 'status', 'config'
      }
      return vim.tbl_filter(function(cmd)
        return vim.startswith(cmd, arg_lead)
      end, subcommands)
    end
  })
  

  -- Subcommands
  M.register('MyPluginOpen', function(opts)
    require('my-plugin.core').open()
  end, {
    desc = 'Open my plugin interface'
  })
  
  M.register('MyPluginClose', function(opts)
    require('my-plugin.core').close()
  end, {
    desc = 'Close my plugin interface'
  })
  
  M.register('MyPluginToggle', function(opts)
    require('my-plugin.core').toggle()
  end, {
    desc = 'Toggle my plugin interface'
  })
  
  M.register('MyPluginStatus', function(opts)
    local status = require('my-plugin').get_status()
    print(vim.inspect(status))
  end, {
    desc = 'Show my plugin status'
  })
  
  M.register('MyPluginConfig', function(opts)
    local action = opts.args[1]
    
    if action == 'show' then
      print(vim.inspect(require('my-plugin').get_config()))
    elseif action == 'edit' then
      vim.cmd('edit ' .. vim.fn.stdpath('config') .. '/lua/my-plugin/config.lua')
    else
      require('my-plugin.ui').notify('Usage: MyPluginConfig [show|edit]')
    end
  end, {
    nargs = '?',
    desc = 'Manage my plugin configuration',
    complete = function(arg_lead)
      return vim.tbl_filter(function(opt)
        return vim.startswith(opt, arg_lead)
      end, {'show', 'edit'})
    end
  })
end

return M

23.3.2 Keymap Management


-- lua/my-plugin/keymaps.lua
local M = {}


-- Keymap registry
local _keymaps = {}


-- Register a keymap
function M.register(mode, lhs, rhs, opts)
  opts = opts or {}
  

  -- Store for later removal
  table.insert(_keymaps, {
    mode = mode,
    lhs = lhs,
    opts = opts
  })
  

  -- Set keymap
  vim.keymap.set(mode, lhs, rhs, opts)
end


-- Unregister all keymaps
function M.unregister_all()
  for _, map in ipairs(_keymaps) do
    pcall(vim.keymap.del, map.mode, map.lhs, map.opts)
  end
  _keymaps = {}
end


-- Setup default keymaps
function M.setup(config)
  local prefix = config.keymaps.prefix or '<leader>mp'
  
  M.register('n', prefix .. 'o', function()
    require('my-plugin.core').open()
  end, {
    desc = 'Open my plugin'
  })
  
  M.register('n', prefix .. 'c', function()
    require('my-plugin.core').close()
  end, {
    desc = 'Close my plugin'
  })
  
  M.register('n', prefix .. 't', function()
    require('my-plugin.core').toggle()
  end, {
    desc = 'Toggle my plugin'
  })
  
  M.register('n', prefix .. 's', function()
    local status = require('my-plugin').get_status()
    require('my-plugin.ui').notify(vim.inspect(status))
  end, {
    desc = 'Show my plugin status'
  })
  

  -- Visual mode keymaps
  M.register('v', prefix .. 'f', function()
    local start_pos = vim.fn.getpos("'<")
    local end_pos = vim.fn.getpos("'>")
    local lines = vim.api.nvim_buf_get_lines(
      0,
      start_pos[2] - 1,
      end_pos[2],
      false
    )
    
    require('my-plugin.core').process_selection(lines)
  end, {
    desc = 'Process selection with my plugin'
  })
end

return M

23.4 Autocommands and Events

23.4.1 Autocommand Management


-- lua/my-plugin/autocmds.lua
local M = {}


-- Autocommand group
local _augroup = nil


-- Setup autocommands
function M.setup(config)
  _augroup = vim.api.nvim_create_augroup('MyPlugin', { clear = true })
  

  -- File type specific setup
  vim.api.nvim_create_autocmd('FileType', {
    group = _augroup,
    pattern = config.filetypes or {'lua', 'vim'},
    callback = function(args)
      require('my-plugin.core').setup_buffer(args.buf)
    end
  })
  

  -- Buffer enter event
  vim.api.nvim_create_autocmd('BufEnter', {
    group = _augroup,
    callback = function(args)
      require('my-plugin.core').on_buffer_enter(args.buf)
    end
  })
  

  -- Before save
  vim.api.nvim_create_autocmd('BufWritePre', {
    group = _augroup,
    callback = function(args)
      if config.auto_format then
        require('my-plugin.core').format_buffer(args.buf)
      end
    end
  })
  

  -- After save
  vim.api.nvim_create_autocmd('BufWritePost', {
    group = _augroup,
    callback = function(args)
      require('my-plugin.core').on_buffer_save(args.buf)
    end
  })
  

  -- Cursor hold (for auto-update)
  if config.auto_update then
    vim.api.nvim_create_autocmd('CursorHold', {
      group = _augroup,
      callback = function()
        require('my-plugin.core').update()
      end
    })
  end
  

  -- Vim leave (cleanup)
  vim.api.nvim_create_autocmd('VimLeavePre', {
    group = _augroup,
    callback = function()
      require('my-plugin.core').cleanup()
    end
  })
end


-- Clear all autocommands
function M.clear()
  if _augroup then
    vim.api.nvim_clear_autocmds({ group = _augroup })
  end
end

return M

23.4.2 Custom Event System


-- lua/my-plugin/events.lua
local M = {}


-- Event emitter instance
local _emitter = nil


-- Initialize event system
function M.init()
  if not _emitter then
    _emitter = require('my-plugin.lib.event-emitter').new()
  end
  return _emitter
end


-- Emit plugin events
function M.emit(event, data)
  if _emitter then
    _emitter:emit(event, data)
  end
end


-- Subscribe to plugin events
function M.on(event, callback)
  M.init()
  return _emitter:on(event, callback)
end


-- One-time subscription
function M.once(event, callback)
  M.init()
  return _emitter:once(event, callback)
end


-- Unsubscribe
function M.off(event, callback)
  if _emitter then
    _emitter:off(event, callback)
  end
end


-- Setup event bridges
function M.setup()
  M.init()
  
  local augroup = vim.api.nvim_create_augroup('MyPluginEventBridge', { clear = true })
  

  -- Bridge Neovim autocommands to plugin events
  vim.api.nvim_create_autocmd('BufEnter', {
    group = augroup,
    callback = function(args)
      M.emit('buffer:enter', {
        buf = args.buf,
        file = args.file
      })
    end
  })
  
  vim.api.nvim_create_autocmd('BufWritePost', {
    group = augroup,
    callback = function(args)
      M.emit('buffer:save', {
        buf = args.buf,
        file = args.file
      })
    end
  })
  
  vim.api.nvim_create_autocmd('CursorMoved', {
    group = augroup,
    callback = function()
      M.emit('cursor:moved', {
        pos = vim.api.nvim_win_get_cursor(0)
      })
    end
  })
end


-- Example usage in plugin:

-- events.on('buffer:save', function(data)

--   print('Buffer saved:', data.file)

-- end)

return M

23.5 Testing Your Plugin

23.5.1 Unit Testing Setup


-- tests/minimal_init.lua

-- Minimal init for testing


-- Add plugin to runtime path
vim.cmd([[set runtimepath=$VIMRUNTIME]])
vim.cmd([[set packpath=/tmp/nvim/site]])

local package_root = '/tmp/nvim/site/pack'
local install_path = package_root .. '/packer/start/packer.nvim'

local function load_plugins()
  require('packer').startup({
    function(use)
      use 'wbthomason/packer.nvim'
      use 'nvim-lua/plenary.nvim'  -- For testing
      

      -- Your plugin
      use { '/path/to/your/plugin', as = 'my-plugin' }
    end,
    config = {
      package_root = package_root,
      compile_path = install_path .. '/plugin/packer_compiled.lua'
    }
  })
end

if vim.fn.isdirectory(install_path) == 0 then
  vim.fn.system({
    'git', 'clone', 'https://github.com/wbthomason/packer.nvim', install_path
  })
end

load_plugins()
require('packer').sync()

23.5.2 Writing Tests with Plenary


-- tests/my-plugin_spec.lua
local plugin = require('my-plugin')

describe('my-plugin', function()
  before_each(function()

    -- Reset plugin state
    plugin.reset()
  end)
  
  after_each(function()

    -- Cleanup
    plugin.cleanup()
  end)
  
  describe('setup', function()
    it('should initialize with default config', function()
      plugin.setup()
      assert.is_true(plugin.is_initialized())
    end)
    
    it('should merge user config with defaults', function()
      plugin.setup({ debug = true })
      local config = plugin.get_config()
      assert.is_true(config.debug)
      assert.is_not_nil(config.enabled)
    end)
    
    it('should validate configuration', function()
      assert.has_error(function()
        plugin.setup({ invalid_option = 'test' })
      end)
    end)
  end)
  
  describe('core functionality', function()
    before_each(function()
      plugin.setup()
    end)
    
    it('should open interface', function()
      plugin.open()
      assert.is_true(plugin.is_open())
    end)
    
    it('should close interface', function()
      plugin.open()
      plugin.close()
      assert.is_false(plugin.is_open())
    end)
    
    it('should toggle interface', function()
      local initial_state = plugin.is_open()
      plugin.toggle()
      assert.are_not.equal(initial_state, plugin.is_open())
    end)
  end)
  
  describe('buffer management', function()
    it('should create scratch buffer', function()
      local buf = require('my-plugin.buffer').create_scratch_buffer()
      assert.is_true(vim.api.nvim_buf_is_valid(buf))
    end)
    
    it('should set buffer content', function()
      local buf = require('my-plugin.buffer').create_scratch_buffer()
      local content = {'line1', 'line2', 'line3'}
      
      require('my-plugin.buffer').set_content(buf, content)
      
      local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false)
      assert.are.same(content, lines)
    end)
  end)
  
  describe('event system', function()
    it('should emit and receive events', function()
      local events = require('my-plugin.events')
      local called = false
      local received_data = nil
      
      events.on('test:event', function(data)
        called = true
        received_data = data
      end)
      
      events.emit('test:event', { value = 123 })
      
      assert.is_true(called)
      assert.are.equal(123, received_data.value)
    end)
    
    it('should handle one-time events', function()
      local events = require('my-plugin.events')
      local call_count = 0
      
      events.once('test:once', function()
        call_count = call_count + 1
      end)
      
      events.emit('test:once')
      events.emit('test:once')
      
      assert.are.equal(1, call_count)
    end)
  end)
end)

23.5.3 Running Tests

#!/bin/bash
# tests/run_tests.sh

# Run tests with minimal init
nvim --headless --noplugin -u tests/minimal_init.lua \

  -c "PlenaryBustedDirectory tests/ { minimal_init = 'tests/minimal_init.lua' }"

-- Makefile
.PHONY: test test-watch lint format

test:
    @nvim --headless --noplugin -u tests/minimal_init.lua \

        -c "PlenaryBustedDirectory tests/ { minimal_init = 'tests/minimal_init.lua' }"

test-watch:
    @echo "Watching for changes..."
    @while true; do \
        inotifywait -r -e modify lua/ tests/; \
        make test; \
    done

lint:
    @luacheck lua/ --globals vim

format:
    @stylua lua/ tests/

23.6 Documentation

23.6.1 Help File Format


*my-plugin.txt*  Description of my plugin

Author: Your Name <email@example.com>
License: MIT
Version: 1.0.0

==============================================================================
CONTENTS                                                  *my-plugin-contents*

    1. Introduction ...................... |my-plugin-introduction|
    2. Requirements ...................... |my-plugin-requirements|
    3. Installation ...................... |my-plugin-installation|
    4. Configuration ..................... |my-plugin-configuration|
    5. Usage ............................. |my-plugin-usage|
    6. Commands .......................... |my-plugin-commands|
    7. API ............................... |my-plugin-api|
    8. Highlights ........................ |my-plugin-highlights|
    9. FAQ ............................... |my-plugin-faq|
   10. Contributing ...................... |my-plugin-contributing|

==============================================================================
INTRODUCTION                                          *my-plugin-introduction*

My Plugin is a Neovim plugin that does amazing things. It provides a
seamless interface for managing your workflow efficiently.

Features:~
  • Feature one with detailed description
  • Feature two with benefits
  • Feature three with use cases

==============================================================================
REQUIREMENTS                                          *my-plugin-requirements*

  • Neovim >= 0.8.0
  • Optional: ripgrep for search functionality
  • Optional: telescope.nvim for picker integration

==============================================================================
INSTALLATION                                          *my-plugin-installation*

Using lazy.nvim:~
>lua
    {
      'username/my-plugin.nvim',
      dependencies = { 'nvim-lua/plenary.nvim' },
      config = function()
        require('my-plugin').setup({

          -- your configuration
        })
      end
    }
<

Using packer.nvim:~
>lua
    use {
      'username/my-plugin.nvim',
      requires = { 'nvim-lua/plenary.nvim' },
      config = function()
        require('my-plugin').setup()
      end
    }
<

==============================================================================
CONFIGURATION                                        *my-plugin-configuration*


                                                          *my-plugin.setup()*
setup({opts})
    Configure the plugin. This must be called to initialize the plugin.

    Parameters:~
        {opts} (table|nil) Configuration options

    Valid keys for {opts}:~
        • {enabled} (boolean, default: true)
          Enable or disable the plugin
        
        • {debug} (boolean, default: false)
          Enable debug mode
        
        • {log_level} (string, default: 'info')
          Logging level: 'debug', 'info', 'warn', 'error'
        
        • {keymaps} (table)
          Keymap configuration
          • {enable} (boolean, default: true)
          • {prefix} (string, default: '<leader>mp')
        
        • {ui} (table)
          UI configuration
          • {border} (string, default: 'rounded')
            Border style: 'none', 'single', 'double', 'rounded'
          • {width} (number, default: 80)
          • {height} (number, default: 20)

    Example:~
>lua
    require('my-plugin').setup({
      debug = true,
      keymaps = {
        prefix = '<leader>p'
      },
      ui = {
        border = 'double',
        width = 100
      }
    })
<

==============================================================================
USAGE                                                        *my-plugin-usage*

Basic usage:~
    1. Open the interface with |:MyPluginOpen|
    2. Navigate using standard Vim motions
    3. Press <CR> to select an item
    4. Press <Esc> or 'q' to close

Keymaps:~
    Default prefix: `<leader>mp`
    
    • {prefix}o - Open interface
    • {prefix}c - Close interface
    • {prefix}t - Toggle interface
    • {prefix}s - Show status

==============================================================================
COMMANDS                                                  *my-plugin-commands*

:MyPlugin {subcommand}                                            *:MyPlugin*
    Main command for the plugin. Available subcommands:
    
    • open    - Open the plugin interface
    • close   - Close the plugin interface
    • toggle  - Toggle the plugin interface
    • status  - Show plugin status
    • config  - Manage configuration

:MyPluginOpen                                                 *:MyPluginOpen*
    Open the plugin interface.

:MyPluginClose                                               *:MyPluginClose*
    Close the plugin interface.

:MyPluginToggle                                             *:MyPluginToggle*
    Toggle the plugin interface.

:MyPluginStatus                                             *:MyPluginStatus*
    Display current plugin status.

:MyPluginConfig {action}                                   *:MyPluginConfig*
    Manage plugin configuration.
    
    Actions:~
        • show - Display current configuration
        • edit - Open configuration file

==============================================================================
API                                                            *my-plugin-api*


                                                            *my-plugin.open()*
open()
    Open the plugin interface.
    
    Usage:~
>lua
    require('my-plugin').open()
<


                                                           *my-plugin.close()*
close()
    Close the plugin interface.


                                                          *my-plugin.toggle()*
toggle()
    Toggle the plugin interface.


                                                      *my-plugin.get_config()*
get_config()
    Get current configuration.
    
    Returns:~
        (table) Current configuration


                                                      *my-plugin.get_status()*
get_status()
    Get plugin status information.
    
    Returns:~
        (table) Status information containing:
            • {initialized} (boolean)
            • {open} (boolean)
            • {version} (string)

==============================================================================
HIGHLIGHTS                                              *my-plugin-highlights*

MyPluginNormal                                              *hl-MyPluginNormal*
    Normal text in plugin interface.
    Default: links to |hl-Normal|

MyPluginBorder                                              *hl-MyPluginBorder*
    Border of floating windows.
    Default: links to |hl-FloatBorder|

MyPluginTitle                                                *hl-MyPluginTitle*
    Title text in plugin interface.
    Default: links to |hl-Title|

MyPluginSelected                                          *hl-MyPluginSelected*
    Selected item highlight.
    Default: links to |hl-Visual|

To customize:~
>vim
    highlight MyPluginNormal guifg=#abb2bf guibg=#282c34
    highlight MyPluginBorder guifg=#61afef
<

==============================================================================
FAQ                                                            *my-plugin-faq*

Q: The plugin doesn't work after installation
A: Make sure you've called |my-plugin.setup()| in your configuration.

Q: How do I change the default keymaps?
A: Pass a custom prefix in the setup configuration:
>lua
   require('my-plugin').setup({
     keymaps = { prefix = '<leader>p' }
   })
<

Q: Can I disable certain features?
A: Yes, see |my-plugin-configuration| for all available options.

==============================================================================
CONTRIBUTING                                          *my-plugin-contributing*

Contributions are welcome! Please visit:
https://github.com/username/my-plugin.nvim

Bug reports and feature requests:
https://github.com/username/my-plugin.nvim/issues

==============================================================================
vim:tw=78:ts=8:ft=help:norl:

23.6.2 README Structure

# my-plugin.nvim

> A powerful Neovim plugin for [purpose]

[![CI](https://github.com/username/my-plugin.nvim/workflows/CI/badge.svg)](https://github.com/username/my-plugin.nvim/actions)
[![License](https://img.shields.io/github/license/username/my-plugin.nvim)](LICENSE)

## ✨ Features


- 🚀 Feature one with emoji

- ⚡ Feature two showing benefits

- 🎯 Feature three with use case

- 📦 Zero dependencies (or list them)

## 📋 Requirements


- Neovim >= 0.8.0

- Optional: [ripgrep](https://github.com/BurntSushi/ripgrep) for search

- Optional: [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim)

## 📦 Installation

### [lazy.nvim](https://github.com/folke/lazy.nvim)

```lua
{
  'username/my-plugin.nvim',
  dependencies = { 'nvim-lua/plenary.nvim' },
  config = function()
    require('my-plugin').setup({

      -- your configuration
    })
  end,
  keys = {
    { '<leader>mp', '<cmd>MyPluginToggle<cr>', desc = 'Toggle My Plugin' }
  }
}

### [packer.nvim](https://github.com/wbthomason/packer.nvim)

lua
use {
  'username/my-plugin.nvim',
  requires = { 'nvim-lua/plenary.nvim' },
  config = function()
    require('my-plugin').setup()
  end
}

## ⚙️ Configuration

Default configuration:

lua
require('my-plugin').setup({
  enabled = true,
  debug = false,
  log_level = 'info',
  
  keymaps = {
    enable = true,
    prefix = '<leader>mp'
  },
  
  ui = {
    border = 'rounded',
    width = 80,
    height = 20
  }
})

## 🚀 Usage

### Basic Usage


1. Open the interface: `:MyPluginOpen` or `<leader>mpo`

2. Navigate with `j`/`k`

3. Select with `<CR>`

4. Close with `q` or `<Esc>`

### Commands


- `:MyPlugin {subcommand}` - Main command

- `:MyPluginOpen` - Open interface

- `:MyPluginClose` - Close interface

- `:MyPluginToggle` - Toggle interface

- `:MyPluginStatus` - Show status

### Lua API

lua
local plugin = require('my-plugin')


-- Open interface
plugin.open()


-- Close interface
plugin.close()


-- Toggle interface
plugin.toggle()


-- Get configuration
local config = plugin.get_config()


-- Get status
local status = plugin.get_status()

## 🎨 Customization

### Highlight Groups

vim
" Customize colors
highlight MyPluginNormal guifg=#abb2bf guibg=#282c34
highlight MyPluginBorder guifg=#61afef
highlight MyPluginTitle guifg=#e5c07b gui=bold
highlight MyPluginSelected guifg=#282c34 guibg=#61afef

### Custom Keymaps

lua
require('my-plugin').setup({
  keymaps = {
    prefix = '<leader>p',

    -- Disable default keymaps
    enable = false
  }
})


-- Define your own keymaps
vim.keymap.set('n', '<leader>po', '<cmd>MyPluginOpen<cr>')
vim.keymap.set('n', '<leader>pc', '<cmd>MyPluginClose<cr>')

## 🔧 Advanced Usage

### Integration with Other Plugins

lua

-- Telescope integration
require('my-plugin').setup({
  telescope = {
    enable = true,
    layout_strategy = 'vertical'
  }
})


-- LSP integration
require('my-plugin').setup({
  lsp = {
    enable = true,
    servers = { 'lua_ls', 'tsserver' }
  }
})

### Events

lua

-- Listen to plugin events
local events = require('my-plugin.events')

events.on('buffer:save', function(data)
  print('Buffer saved:', data.file)
end)

events.on('interface:open', function()
  print('Interface opened')
end)

## 🧪 Development

### Running Tests

bash
make test

### Linting

bash
make lint

### Formatting

bash
make format

## 🤝 Contributing

Contributions are welcome! Please read [CONTRIBUTING.md](CONTRIBUTING.md).


1. Fork the repository

2. Create your feature branch (`git checkout -b feature/amazing`)

3. Commit your changes (`git commit -m 'Add amazing feature'`)

4. Push to the branch (`git push origin feature/amazing`)

5. Open a Pull Request

## 📝 License

[MIT](LICENSE) © Your Name

## 🙏 Acknowledgments


- [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) for utilities

- [telescope.nvim](https://github.com/nvim-telescope/telescope.nvim) for inspiration

- Community contributors

## 📚 Resources


- [Documentation](doc/my-plugin.txt)

- [Wiki](https://github.com/username/my-plugin.nvim/wiki)

- [Issue Tracker](https://github.com/username/my-plugin.nvim/issues)

- [Discussions](https://github.com/username/my-plugin.nvim/discussions)


## 23.7 Publishing and Distribution

### 23.7.1 Release Checklist

```markdown
# Release Checklist

## Pre-release


- [ ] All tests passing

- [ ] Documentation updated

- [ ] CHANGELOG.md updated

- [ ] Version bumped in all files

- [ ] No debug code or console.log statements

- [ ] All TODOs addressed or documented

- [ ] Screenshots/GIFs updated if UI changed

## Testing


- [ ] Manual testing on Linux

- [ ] Manual testing on macOS

- [ ] Manual testing on Windows (if applicable)

- [ ] Tested with minimal config

- [ ] Tested with various plugin managers

## Documentation


- [ ] README.md complete and accurate

- [ ] Help file (doc/*.txt) up to date

- [ ] API documentation complete

- [ ] Examples working

- [ ] Migration guide (if breaking changes)

## Release


- [ ] Tag created (vX.Y.Z)

- [ ] GitHub release created

- [ ] Announcement posted

- [ ] Added to plugin lists/registries

23.7.2 Version Management


-- lua/my-plugin/version.lua
local M = {}

M.VERSION = '1.0.0'
M.VERSION_MAJOR = 1
M.VERSION_MINOR = 0
M.VERSION_PATCH = 0

function M.compare(version_string)
  local parts = vim.split(version_string, '%.')
  local major = tonumber(parts[1]) or 0
  local minor = tonumber(parts[2]) or 0
  local patch = tonumber(parts[3]) or 0
  
  if M.VERSION_MAJOR ~= major then
    return M.VERSION_MAJOR > major and 1 or -1
  end
  
  if M.VERSION_MINOR ~= minor then
    return M.VERSION_MINOR > minor and 1 or -1
  end
  
  if M.VERSION_PATCH ~= patch then
    return M.VERSION_PATCH > patch and 1 or -1
  end
  
  return 0
end

function M.is_compatible(required_version)
  return M.compare(required_version) >= 0
end

function M.check_nvim_version(required)
  if not vim.fn.has('nvim-' .. required) then
    vim.notify(
      string.format(
        'my-plugin requires Neovim >= %s, but you have %s',
        required,
        vim.fn.execute('version'):match('NVIM v(%S+)')
      ),
      vim.log.levels.ERROR
    )
    return false
  end
  return true
end

return M

23.7.3 GitHub Actions CI

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        nvim_version: ['v0.8.0', 'v0.9.0', 'nightly']
    
    steps:

      - uses: actions/checkout@v3
      

      - name: Install Neovim
        uses: rhysd/action-setup-vim@v1
        with:
          neovim: true
          version: ${{ matrix.nvim_version }}
      

      - name: Install dependencies
        run: |
          git clone --depth 1 https://github.com/nvim-lua/plenary.nvim \
            ~/.local/share/nvim/site/pack/vendor/start/plenary.nvim
          ln -s $(pwd) ~/.local/share/nvim/site/pack/vendor/start
      

      - name: Run tests
        run: |
          nvim --version
          make test
  
  lint:
    runs-on: ubuntu-latest
    
    steps:

      - uses: actions/checkout@v3
      

      - name: Setup Lua
        uses: leafo/gh-actions-lua@v9
        with:
          luaVersion: "5.1"
      

      - name: Setup Luarocks
        uses: leafo/gh-actions-luarocks@v4
      

      - name: Install luacheck
        run: luarocks install luacheck
      

      - name: Run luacheck
        run: make lint
  
  format:
    runs-on: ubuntu-latest
    
    steps:

      - uses: actions/checkout@v3
      

      - name: Stylua
        uses: JohnnyMorganz/stylua-action@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          version: latest
          args: --check lua/

End of Chapter 23: Writing Neovim Plugins in Lua

You now have comprehensive knowledge of creating professional Neovim plugins, from architecture and core functionality to testing, documentation, and distribution. This chapter covered the complete plugin development lifecycle, including buffer/window management, UI components, command systems, event handling, testing strategies, and publishing workflows. The next chapter will explore LSP integration and creating custom language server configurations for your plugins.


Chapter 24: Syntax Highlighting

This chapter explores Vim and Neovim’s syntax highlighting systems, from traditional Vim syntax to modern Tree-sitter integration. You’ll learn how to create custom syntax definitions, work with highlight groups, and leverage Tree-sitter for advanced highlighting.

24.1 Understanding Syntax Highlighting Fundamentals

24.1.1 How Syntax Highlighting Works

" Syntax highlighting operates in layers:
" 1. Syntax items define patterns (syntax match, keyword, region)
" 2. Highlight groups define visual appearance
" 3. Links connect syntax items to highlight groups

" Basic flow:
" Pattern Match → Syntax Group → Highlight Group → Visual Style

24.1.2 Checking Current Syntax State

" Show syntax group under cursor
:echo synIDattr(synID(line('.'), col('.'), 1), 'name')

" Show linked highlight group
:echo synIDattr(synIDtrans(synID(line('.'), col('.'), 1)), 'name')

" Show full syntax stack
:echo map(synstack(line('.'), col('.')), 'synIDattr(v:val, "name")')

" Function to show complete syntax info
function! SyntaxInfo()
  let stack = synstack(line('.'), col('.'))
  let info = []
  
  for id in stack
    let name = synIDattr(id, 'name')
    let trans = synIDattr(synIDtrans(id), 'name')
    call add(info, name . ' -> ' . trans)
  endfor
  
  echo join(info, ' | ')
endfunction

nnoremap <F10> :call SyntaxInfo()<CR>

24.1.3 Built-in Highlight Groups

" Standard highlight groups (partial list)
:highlight Comment      " Comments
:highlight Constant     " Constants (strings, numbers, booleans)
:highlight String       " String constants
:highlight Number       " Number constants
:highlight Boolean      " Boolean constants
:highlight Identifier   " Variable names
:highlight Function     " Function names
:highlight Statement    " Keywords (if, for, while)
:highlight Conditional  " if, else, switch
:highlight Repeat       " for, while, repeat
:highlight Label        " case, default
:highlight Operator     " +, -, *, /
:highlight Keyword      " Other keywords
:highlight Exception    " try, catch, throw
:highlight PreProc      " Preprocessor directives
:highlight Include      " #include, import, use
:highlight Define       " #define
:highlight Macro        " Macros
:highlight Type         " int, char, class
:highlight StorageClass " static, const, volatile
:highlight Structure    " struct, union, enum
:highlight Typedef      " typedef
:highlight Special      " Special symbols
:highlight SpecialChar  " Escape sequences
:highlight Tag          " HTML/XML tags
:highlight Delimiter    " Brackets, braces
:highlight SpecialComment " Special comments
:highlight Debug        " Debug statements
:highlight Underlined   " Underlined text
:highlight Ignore       " Invisible text
:highlight Error        " Error highlights
:highlight Todo         " TODO, FIXME, XXX

" View all highlight groups
:so $VIMRUNTIME/syntax/hitest.vim

24.2 Creating Custom Vim Syntax Files

24.2.1 Basic Syntax File Structure

" syntax/myfiletype.vim
" Vim syntax file
" Language: My Custom Language
" Maintainer: Your Name
" Latest Revision: 2025-10-19

if exists("b:current_syntax")
  finish
endif

" Keywords
syntax keyword myKeyword if else while for return
syntax keyword myBoolean true false
syntax keyword myNull null undefined

" Operators
syntax match myOperator "\(+\|-\|\*\|/\|=\|<\|>\)"

" Numbers
syntax match myNumber "\<\d\+\>"
syntax match myFloat "\<\d\+\.\d\+\>"

" Strings
syntax region myString start='"' end='"' contains=myEscape
syntax region myString start="'" end="'" contains=myEscape
syntax match myEscape "\\." contained

" Comments
syntax keyword myTodo TODO FIXME XXX NOTE contained
syntax match myComment "//.*$" contains=myTodo
syntax region myComment start="/\*" end="\*/" contains=myTodo

" Functions
syntax match myFunction "\w\+\s*("me=e-1

" Highlight links
highlight default link myKeyword Keyword
highlight default link myBoolean Boolean
highlight default link myNull Constant
highlight default link myOperator Operator
highlight default link myNumber Number
highlight default link myFloat Float
highlight default link myString String
highlight default link myEscape SpecialChar
highlight default link myComment Comment
highlight default link myTodo Todo
highlight default link myFunction Function

let b:current_syntax = "myfiletype"

24.2.2 Advanced Syntax Patterns

" syntax/advanced.vim

" Case-insensitive matching
syntax case ignore
syntax keyword myKeyword SELECT FROM WHERE
syntax case match

" Nextgroup and containment
syntax match myType "\<\(int\|float\|string\)\>" nextgroup=myIdentifier skipwhite
syntax match myIdentifier "\w\+" contained

" Clusters for grouping
syntax cluster myExpressions contains=myNumber,myString,myVariable
syntax region myParens start="(" end=")" contains=@myExpressions

" Regions with skip patterns
syntax region myString start=+"+ skip=+\\"+ end=+"+ contains=myEscape

" Multi-line regions
syntax region myBlock start="{" end="}" fold transparent contains=ALL

" Transparent groups (don't affect highlighting)
syntax region myTransparent start="(" end=")" transparent contains=ALL

" Synchronization for large files
syntax sync minlines=50 maxlines=500
syntax sync match mySync grouphere myBlock "{"
syntax sync match mySync groupthere myBlock "}"

" Folding integration
syntax region myFunction start="function" end="endfunction" fold
setlocal foldmethod=syntax

" Concealing (hide/replace characters)
syntax match myArrow "->" conceal cchar=→
syntax match myLambda "lambda" conceal cchar=λ
setlocal conceallevel=2

24.2.3 Complex Syntax Example: Custom Config Language

" syntax/customconfig.vim
" Syntax for a YAML-like configuration language

if exists("b:current_syntax")
  finish
endif

" Comments
syntax match configComment "#.*$" contains=configTodo
syntax keyword configTodo TODO FIXME XXX NOTE contained

" Keys (before colon)
syntax match configKey "^\s*\w\+\ze:" contains=configSpecialKey
syntax keyword configSpecialKey version name port host contained

" Values
syntax match configString "\w\+" contained
syntax region configQuotedString start='"' end='"' skip='\\"'
syntax region configQuotedString start="'" end="'" skip="\\'"

" Numbers and Booleans
syntax match configNumber "\<\d\+\>" contained
syntax match configFloat "\<\d\+\.\d\+\>" contained
syntax keyword configBoolean true false yes no on off contained

" Special values
syntax keyword configNull null nil contained

" Arrays
syntax region configArray start="\[" end="\]" contains=configNumber,configString,configQuotedString,configBoolean,configNull

" Nested structures (indentation-based)
syntax match configIndent "^\s\+"

" Environment variables
syntax match configEnvVar "\$\w\+" contained
syntax match configEnvVar "\${\w\+}" contained

" Key-value pairs with value types
syntax region configValue start=":" end="$" contained contains=configNumber,configFloat,configBoolean,configNull,configQuotedString,configEnvVar,configArray

" Complete line pattern
syntax match configLine "^\s*\w\+:.*$" contains=configKey,configValue

" Special sections
syntax region configSection start="^\w\+:$" end="^\w" contains=configKey fold

" Highlighting
highlight default link configComment Comment
highlight default link configTodo Todo
highlight default link configKey Identifier
highlight default link configSpecialKey Keyword
highlight default link configString String
highlight default link configQuotedString String
highlight default link configNumber Number
highlight default link configFloat Float
highlight default link configBoolean Boolean
highlight default link configNull Constant
highlight default link configEnvVar PreProc

let b:current_syntax = "customconfig"

24.3 Color Schemes and Highlight Customization

24.3.1 Creating a Custom Color Scheme

" colors/myscheme.vim
" A custom color scheme

" Initialize
highlight clear
if exists("syntax_on")
  syntax reset
endif

let g:colors_name = "myscheme"

" Helper function for setting highlights
function! s:HL(group, fg, bg, attr)
  let l:cmd = 'highlight ' . a:group
  
  if !empty(a:fg)
    let l:cmd .= ' guifg=' . a:fg . ' ctermfg=' . a:fg
  endif
  
  if !empty(a:bg)
    let l:cmd .= ' guibg=' . a:bg . ' ctermbg=' . a:bg
  endif
  
  if !empty(a:attr)
    let l:cmd .= ' gui=' . a:attr . ' cterm=' . a:attr
  endif
  
  execute l:cmd
endfunction

" Color palette
let s:black       = '#282c34'
let s:white       = '#abb2bf'
let s:red         = '#e06c75'
let s:green       = '#98c379'
let s:yellow      = '#e5c07b'
let s:blue        = '#61afef'
let s:magenta     = '#c678dd'
let s:cyan        = '#56b6c2'
let s:gray        = '#5c6370'
let s:light_gray  = '#3e4452'

" UI Elements
call s:HL('Normal',       s:white,      s:black,      '')
call s:HL('LineNr',       s:gray,       s:black,      '')
call s:HL('CursorLine',   '',           s:light_gray, 'none')
call s:HL('CursorLineNr', s:yellow,     s:light_gray, 'bold')
call s:HL('Visual',       '',           s:light_gray, '')
call s:HL('MatchParen',   s:cyan,       s:light_gray, 'bold')

" Syntax Groups
call s:HL('Comment',      s:gray,       '',           'italic')
call s:HL('Constant',     s:cyan,       '',           '')
call s:HL('String',       s:green,      '',           '')
call s:HL('Number',       s:yellow,     '',           '')
call s:HL('Boolean',      s:yellow,     '',           '')
call s:HL('Identifier',   s:red,        '',           '')
call s:HL('Function',     s:blue,       '',           '')
call s:HL('Statement',    s:magenta,    '',           '')
call s:HL('Keyword',      s:magenta,    '',           '')
call s:HL('Type',         s:yellow,     '',           '')
call s:HL('Special',      s:cyan,       '',           '')
call s:HL('PreProc',      s:yellow,     '',           '')

" Git diff
call s:HL('DiffAdd',      s:green,      s:light_gray, '')
call s:HL('DiffChange',   s:yellow,     s:light_gray, '')
call s:HL('DiffDelete',   s:red,        s:light_gray, '')
call s:HL('DiffText',     s:blue,       s:light_gray, 'bold')

" Cleanup
delfunction s:HL

24.3.2 Dynamic Highlight Modification

" Modify highlights at runtime
function! AdjustHighlights()
  " Make comments more subtle
  highlight Comment guifg=#5c6370 ctermfg=59 gui=italic
  
  " Bold keywords
  highlight Statement gui=bold cterm=bold
  
  " Custom colors for specific groups
  highlight Todo guifg=#e5c07b guibg=#3e4452 gui=bold
  
  " Link groups
  highlight link pythonFunction Function
  highlight link luaFunction Function
endfunction

autocmd ColorScheme * call AdjustHighlights()

" Toggle between light and dark variants
function! ToggleBackground()
  if &background ==# 'dark'
    set background=light
    highlight Normal guibg=#ffffff guifg=#000000
  else
    set background=dark
    highlight Normal guibg=#282c34 guifg=#abb2bf
  endif
endfunction

nnoremap <leader>tb :call ToggleBackground()<CR>

24.3.3 Highlight Group Utilities


-- lua/highlight_utils.lua
local M = {}


-- Get highlight group attributes
function M.get_hl(group)
  local hl = vim.api.nvim_get_hl_by_name(group, true)
  return {
    fg = hl.foreground and string.format('#%06x', hl.foreground),
    bg = hl.background and string.format('#%06x', hl.background),
    bold = hl.bold,
    italic = hl.italic,
    underline = hl.underline,
    undercurl = hl.undercurl,
    strikethrough = hl.strikethrough
  }
end


-- Set highlight group
function M.set_hl(group, opts)
  vim.api.nvim_set_hl(0, group, opts)
end


-- Copy highlight from one group to another
function M.copy_hl(from, to)
  local hl = M.get_hl(from)
  M.set_hl(to, hl)
end


-- Blend two colors
function M.blend_colors(color1, color2, ratio)
  local r1 = tonumber(color1:sub(2, 3), 16)
  local g1 = tonumber(color1:sub(4, 5), 16)
  local b1 = tonumber(color1:sub(6, 7), 16)
  
  local r2 = tonumber(color2:sub(2, 3), 16)
  local g2 = tonumber(color2:sub(4, 5), 16)
  local b2 = tonumber(color2:sub(6, 7), 16)
  
  local r = math.floor(r1 * ratio + r2 * (1 - ratio))
  local g = math.floor(g1 * ratio + g2 * (1 - ratio))
  local b = math.floor(b1 * ratio + b2 * (1 - ratio))
  
  return string.format('#%02x%02x%02x', r, g, b)
end


-- Darken a color
function M.darken(color, amount)
  return M.blend_colors(color, '#000000', 1 - amount)
end


-- Lighten a color
function M.lighten(color, amount)
  return M.blend_colors(color, '#ffffff', 1 - amount)
end


-- Create gradient of highlight groups
function M.create_gradient(base_group, prefix, steps)
  local base_hl = M.get_hl(base_group)
  
  for i = 1, steps do
    local ratio = i / steps
    local new_fg = base_hl.fg and M.blend_colors(base_hl.fg, '#ffffff', ratio)
    local new_bg = base_hl.bg and M.blend_colors(base_hl.bg, '#000000', ratio)
    
    M.set_hl(prefix .. i, {
      fg = new_fg,
      bg = new_bg,
      bold = base_hl.bold,
      italic = base_hl.italic
    })
  end
end


-- Apply highlight to buffer range
function M.highlight_range(buf, ns, group, start_line, end_line)
  for line = start_line, end_line do
    vim.api.nvim_buf_add_highlight(buf, ns, group, line, 0, -1)
  end
end

return M

24.4 Tree-sitter Integration

24.4.1 Understanding Tree-sitter


-- Tree-sitter provides:

-- 1. Fast, incremental parsing

-- 2. Accurate syntax tree

-- 3. Language-agnostic queries

-- 4. Better performance than regex-based syntax


-- Check if Tree-sitter is available
if vim.fn.has('nvim-0.8') == 1 then
  local ts_available, _ = pcall(require, 'nvim-treesitter')
  if ts_available then
    print('Tree-sitter is available')
  end
end

24.4.2 Basic Tree-sitter Configuration


-- Using nvim-treesitter plugin
require('nvim-treesitter.configs').setup({

  -- Install parsers
  ensure_installed = {
    'lua', 'vim', 'vimdoc',
    'python', 'javascript', 'typescript',
    'rust', 'go', 'c', 'cpp',
    'html', 'css', 'json', 'yaml'
  },
  

  -- Auto-install missing parsers
  auto_install = true,
  

  -- Highlighting
  highlight = {
    enable = true,
    

    -- Disable for large files
    disable = function(lang, buf)
      local max_filesize = 100 * 1024 -- 100 KB
      local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(buf))
      if ok and stats and stats.size > max_filesize then
        return true
      end
    end,
    

    -- Use Vim regex highlighting in addition to Tree-sitter
    additional_vim_regex_highlighting = false
  },
  

  -- Incremental selection
  incremental_selection = {
    enable = true,
    keymaps = {
      init_selection = '<CR>',
      node_incremental = '<CR>',
      scope_incremental = '<S-CR>',
      node_decremental = '<BS>'
    }
  },
  

  -- Indentation
  indent = {
    enable = true,
    disable = { 'python', 'yaml' }  -- Problematic for some languages
  },
  

  -- Folding
  fold = {
    enable = true
  }
})


-- Enable Tree-sitter folding
vim.opt.foldmethod = 'expr'
vim.opt.foldexpr = 'nvim_treesitter#foldexpr()'

24.4.3 Custom Tree-sitter Queries

;; queries/lua/highlights.scm
;; Custom Tree-sitter highlight queries for Lua

; Function calls
(function_call
  name: (identifier) @function.call)

(function_call
  name: (dot_index_expression
    field: (identifier) @function.method.call))

; Module imports
(function_call
  name: (identifier) @keyword.import (#eq? @keyword.import "require"))

; String interpolation
(string content: (string_content) @string)

; Table fields
(field
  name: (identifier) @field)

; Custom keywords
(identifier) @custom.keyword (#any-of? @custom.keyword "self" "cls")

; Constants
(variable_declaration
  (assignment_statement
    (variable_list
      name: (identifier) @constant
      (#match? @constant "^[A-Z][A-Z_0-9]*$"))))

; Documentation comments
(comment) @comment.documentation
  (#match? @comment.documentation "^---")

-- lua/treesitter_custom.lua
local M = {}


-- Set custom highlight groups for Tree-sitter
function M.setup_highlights()
  vim.api.nvim_set_hl(0, '@function.call', { link = 'Function' })
  vim.api.nvim_set_hl(0, '@function.method.call', { link = 'Function' })
  vim.api.nvim_set_hl(0, '@keyword.import', { fg = '#c678dd', bold = true })
  vim.api.nvim_set_hl(0, '@field', { fg = '#e06c75' })
  vim.api.nvim_set_hl(0, '@custom.keyword', { fg = '#d19a66', italic = true })
  vim.api.nvim_set_hl(0, '@constant', { fg = '#56b6c2', bold = true })
  vim.api.nvim_set_hl(0, '@comment.documentation', { fg = '#5c6370', italic = true })
end


-- Get syntax tree
function M.get_tree()
  local parser = vim.treesitter.get_parser()
  if not parser then
    return nil
  end
  
  local tree = parser:parse()[1]
  return tree
end


-- Get node at cursor
function M.get_node_at_cursor()
  local cursor = vim.api.nvim_win_get_cursor(0)
  local row = cursor[1] - 1
  local col = cursor[2]
  
  local tree = M.get_tree()
  if not tree then
    return nil
  end
  
  return tree:root():named_descendant_for_range(row, col, row, col)
end


-- Get node text
function M.get_node_text(node, bufnr)
  bufnr = bufnr or 0
  return vim.treesitter.get_node_text(node, bufnr)
end


-- Print node info
function M.inspect_node()
  local node = M.get_node_at_cursor()
  if not node then
    print('No node found')
    return
  end
  
  local info = {
    type = node:type(),
    text = M.get_node_text(node),
    start_row = node:start(),
    end_row = node:end_(),
    parent = node:parent() and node:parent():type() or 'none'
  }
  
  print(vim.inspect(info))
end


-- Navigate to parent node
function M.goto_parent_node()
  local node = M.get_node_at_cursor()
  if not node then
    return
  end
  
  local parent = node:parent()
  if parent then
    local start_row, start_col = parent:start()
    vim.api.nvim_win_set_cursor(0, { start_row + 1, start_col })
  end
end


-- Navigate to next sibling
function M.goto_next_sibling()
  local node = M.get_node_at_cursor()
  if not node then
    return
  end
  
  local sibling = node:next_named_sibling()
  if sibling then
    local start_row, start_col = sibling:start()
    vim.api.nvim_win_set_cursor(0, { start_row + 1, start_col })
  end
end


-- Query nodes
function M.query_nodes(query_string, bufnr)
  bufnr = bufnr or 0
  local parser = vim.treesitter.get_parser(bufnr)
  local tree = parser:parse()[1]
  local root = tree:root()
  
  local lang = parser:lang()
  local query = vim.treesitter.query.parse(lang, query_string)
  
  local results = {}
  for id, node in query:iter_captures(root, bufnr) do
    local name = query.captures[id]
    table.insert(results, {
      name = name,
      node = node,
      text = M.get_node_text(node, bufnr)
    })
  end
  
  return results
end


-- Find all functions in buffer
function M.find_functions(bufnr)
  bufnr = bufnr or 0
  local query_string = [[
    (function_declaration
      name: (identifier) @function.name)
    (function_definition
      name: (identifier) @function.name)
  ]]
  
  return M.query_nodes(query_string, bufnr)
end

return M

24.4.4 Advanced Tree-sitter Usage


-- lua/treesitter_advanced.lua
local M = {}
local ts_utils = require('nvim-treesitter.ts_utils')


-- Smart selection expansion
function M.smart_select()
  local node = ts_utils.get_node_at_cursor()
  if not node then
    return
  end
  

  -- Expand selection to current node
  ts_utils.update_selection(0, node)
end


-- Swap with next sibling
function M.swap_next()
  local node = ts_utils.get_node_at_cursor()
  if not node then
    return
  end
  
  local next_sibling = node:next_named_sibling()
  if not next_sibling then
    return
  end
  

  -- Swap nodes
  local node_text = vim.treesitter.get_node_text(node, 0)
  local sibling_text = vim.treesitter.get_node_text(next_sibling, 0)
  

  -- Replace text
  local node_range = { node:range() }
  local sibling_range = { next_sibling:range() }
  
  vim.api.nvim_buf_set_text(0,
    sibling_range[1], sibling_range[2],
    sibling_range[3], sibling_range[4],
    vim.split(node_text, '\n'))
  
  vim.api.nvim_buf_set_text(0,
    node_range[1], node_range[2],
    node_range[3], node_range[4],
    vim.split(sibling_text, '\n'))
end


-- Jump to definition using Tree-sitter
function M.goto_definition()
  local node = ts_utils.get_node_at_cursor()
  if not node then
    return
  end
  

  -- Get identifier
  while node and node:type() ~= 'identifier' do
    node = node:parent()
  end
  
  if not node then
    return
  end
  
  local identifier = vim.treesitter.get_node_text(node, 0)
  

  -- Search for definition
  local query_string = string.format([[
    (function_declaration
      name: (identifier) @name (#eq? @name "%s"))
    (variable_declaration
      (assignment_statement
        (variable_list
          name: (identifier) @name (#eq? @name "%s"))))
  ]], identifier, identifier)
  
  local results = require('treesitter_custom').query_nodes(query_string)
  
  if #results > 0 then
    local def_node = results[1].node
    local row, col = def_node:start()
    vim.api.nvim_win_set_cursor(0, { row + 1, col })
  end
end


-- Highlight matching nodes
function M.highlight_matching(pattern)
  local bufnr = 0
  local ns = vim.api.nvim_create_namespace('ts_highlight')
  

  -- Clear previous highlights
  vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
  

  -- Query matching nodes
  local results = require('treesitter_custom').query_nodes(pattern, bufnr)
  

  -- Highlight each match
  for _, result in ipairs(results) do
    local start_row, start_col, end_row, end_col = result.node:range()
    vim.highlight.range(
      bufnr,
      ns,
      'Search',
      { start_row, start_col },
      { end_row, end_col }
    )
  end
end


-- Text objects using Tree-sitter
function M.setup_textobjects()
  require('nvim-treesitter.configs').setup({
    textobjects = {
      select = {
        enable = true,
        lookahead = true,
        keymaps = {
          ['af'] = '@function.outer',
          ['if'] = '@function.inner',
          ['ac'] = '@class.outer',
          ['ic'] = '@class.inner',
          ['ab'] = '@block.outer',
          ['ib'] = '@block.inner',
          ['aa'] = '@parameter.outer',
          ['ia'] = '@parameter.inner',
        }
      },
      move = {
        enable = true,
        set_jumps = true,
        goto_next_start = {
          [']f'] = '@function.outer',
          [']c'] = '@class.outer',
        },
        goto_next_end = {
          [']F'] = '@function.outer',
          [']C'] = '@class.outer',
        },
        goto_previous_start = {
          ['[f'] = '@function.outer',
          ['[c'] = '@class.outer',
        },
        goto_previous_end = {
          ['[F'] = '@function.outer',
          ['[C'] = '@class.outer',
        },
      },
      swap = {
        enable = true,
        swap_next = {
          ['<leader>sn'] = '@parameter.inner',
        },
        swap_previous = {
          ['<leader>sp'] = '@parameter.inner',
        },
      },
    }
  })
end

return M

24.5 Semantic Highlighting

24.5.1 LSP-based Semantic Tokens


-- lua/semantic_highlighting.lua
local M = {}


-- Enable semantic highlighting
function M.setup()

  -- Enable semantic tokens if LSP supports it
  vim.api.nvim_create_autocmd('LspAttach', {
    callback = function(args)
      local client = vim.lsp.get_client_by_id(args.data.client_id)
      
      if client.server_capabilities.semanticTokensProvider then
        vim.lsp.semantic_tokens.start(args.buf, client.id)
      end
    end
  })
end


-- Custom semantic token highlights
function M.setup_highlights()

  -- Semantic token types
  vim.api.nvim_set_hl(0, '@lsp.type.class', { link = 'Type' })
  vim.api.nvim_set_hl(0, '@lsp.type.decorator', { link = 'Function' })
  vim.api.nvim_set_hl(0, '@lsp.type.enum', { link = 'Type' })
  vim.api.nvim_set_hl(0, '@lsp.type.enumMember', { link = 'Constant' })
  vim.api.nvim_set_hl(0, '@lsp.type.function', { link = 'Function' })
  vim.api.nvim_set_hl(0, '@lsp.type.interface', { link = 'Type' })
  vim.api.nvim_set_hl(0, '@lsp.type.macro', { link = 'Macro' })
  vim.api.nvim_set_hl(0, '@lsp.type.method', { link = 'Function' })
  vim.api.nvim_set_hl(0, '@lsp.type.namespace', { link = 'Include' })
  vim.api.nvim_set_hl(0, '@lsp.type.parameter', { link = 'Identifier' })
  vim.api.nvim_set_hl(0, '@lsp.type.property', { link = 'Identifier' })
  vim.api.nvim_set_hl(0, '@lsp.type.struct', { link = 'Type' })
  vim.api.nvim_set_hl(0, '@lsp.type.type', { link = 'Type' })
  vim.api.nvim_set_hl(0, '@lsp.type.typeParameter', { link = 'Type' })
  vim.api.nvim_set_hl(0, '@lsp.type.variable', { link = 'Identifier' })
  

  -- Semantic token modifiers
  vim.api.nvim_set_hl(0, '@lsp.mod.readonly', { italic = true })
  vim.api.nvim_set_hl(0, '@lsp.mod.deprecated', { strikethrough = true })
  vim.api.nvim_set_hl(0, '@lsp.mod.static', { bold = true })
  

  -- Combined type and modifier
  vim.api.nvim_set_hl(0, '@lsp.typemod.function.readonly', {
    fg = '#61afef',
    italic = true
  })
  vim.api.nvim_set_hl(0, '@lsp.typemod.variable.readonly', {
    fg = '#e06c75',
    italic = true
  })
end


-- Toggle semantic highlighting
function M.toggle()
  local bufnr = vim.api.nvim_get_current_buf()
  local clients = vim.lsp.get_active_clients({ bufnr = bufnr })
  
  for _, client in ipairs(clients) do
    if client.server_capabilities.semanticTokensProvider then
      if vim.b[bufnr].semantic_tokens_enabled then
        vim.lsp.semantic_tokens.stop(bufnr, client.id)
        vim.b[bufnr].semantic_tokens_enabled = false
        print('Semantic highlighting disabled')
      else
        vim.lsp.semantic_tokens.start(bufnr, client.id)
        vim.b[bufnr].semantic_tokens_enabled = true
        print('Semantic highlighting enabled')
      end
    end
  end
end

return M

24.5.2 Custom Semantic Highlighting


-- lua/custom_semantic.lua
local M = {}


-- Namespace for custom highlights
local ns = vim.api.nvim_create_namespace('custom_semantic')


-- Highlight specific patterns with semantic meaning
function M.highlight_constants(bufnr)
  bufnr = bufnr or 0
  

  -- Clear existing highlights
  vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
  
  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
  
  for line_num, line in ipairs(lines) do

    -- Highlight UPPER_CASE as constants
    for match in line:gmatch('%f[%w]([A-Z][A-Z_0-9]+)%f[%W]') do
      local start_col = line:find(match, 1, true)
      if start_col then
        vim.api.nvim_buf_add_highlight(
          bufnr,
          ns,
          'Constant',
          line_num - 1,
          start_col - 1,
          start_col - 1 + #match
        )
      end
    end
    

    -- Highlight numbers with units
    for match in line:gmatch('%d+%s*[a-z]+') do
      local start_col = line:find(match, 1, true)
      if start_col then
        vim.api.nvim_buf_add_highlight(
          bufnr,
          ns,
          'Number',
          line_num - 1,
          start_col - 1,
          start_col - 1 + #match
        )
      end
    end
  end
end


-- Auto-update on buffer changes
function M.setup_autocmds(bufnr)
  bufnr = bufnr or 0
  
  local group = vim.api.nvim_create_augroup('CustomSemantic', { clear = true })
  
  vim.api.nvim_create_autocmd({ 'TextChanged', 'TextChangedI' }, {
    group = group,
    buffer = bufnr,
    callback = function()
      vim.defer_fn(function()
        M.highlight_constants(bufnr)
      end, 100)
    end
  })
end

return M

24.6 Performance Optimization

24.6.1 Lazy Syntax Loading

" Defer syntax loading for large files
augroup FastSyntax
  autocmd!
  autocmd BufReadPre * if getfsize(expand("%")) > 1000000 | syntax clear | endif
  autocmd BufReadPost * if getfsize(expand("%")) > 1000000 | setlocal syntax=OFF | endif
augroup END

" Progressive syntax loading
function! ProgressiveSyntax()
  if line('$') > 5000
    " Load syntax in chunks
    syntax sync minlines=100 maxlines=200
  else
    syntax sync fromstart
  endif
endfunction

autocmd BufEnter * call ProgressiveSyntax()

24.6.2 Optimized Highlighting


-- lua/optimized_highlighting.lua
local M = {}


-- Debounced highlighting update
function M.debounced_highlight(callback, delay)
  local timer = vim.loop.new_timer()
  
  return function()
    timer:stop()
    timer:start(
      delay,
      0,
      vim.schedule_wrap(callback)
    )
  end
end


-- Highlight visible range only
function M.highlight_visible_range(bufnr, callback)
  bufnr = bufnr or 0
  local win = vim.fn.bufwinid(bufnr)
  
  if win == -1 then
    return
  end
  
  local top_line = vim.fn.line('w0', win)
  local bottom_line = vim.fn.line('w$', win)
  
  callback(bufnr, top_line - 1, bottom_line)
end


-- Cached highlighting
local highlight_cache = {}

function M.cached_highlight(key, compute_fn)
  if highlight_cache[key] then
    return highlight_cache[key]
  end
  
  local result = compute_fn()
  highlight_cache[key] = result
  

  -- Clear cache after delay
  vim.defer_fn(function()
    highlight_cache[key] = nil
  end, 5000)
  
  return result
end

return M

24.6.3 Profiling Syntax Performance

" Profile syntax highlighting
function! ProfileSyntax()
  syntime on
  " Perform some operations
  redraw!
  syntime report
  syntime clear
endfunction

command! ProfileSyntax call ProfileSyntax()

End of Chapter 24: Syntax Highlighting

You now have comprehensive knowledge of Vim and Neovim’s syntax highlighting systems, from traditional Vim syntax to modern Tree-sitter integration. This chapter covered creating custom syntax files, color schemes, Tree-sitter queries, semantic highlighting via LSP, and performance optimization techniques for highlighting large files efficiently.


Chapter 25: Colorschemes and Themes

This chapter explores the art and science of colorschemes in Vim and Neovim. You’ll learn how to create, customize, and manage colorschemes, understand color theory principles, work with different terminal environments, and build dynamic, adaptive themes.

25.1 Understanding Colorscheme Fundamentals

25.1.1 Color Basics

" Color specification methods in Vim/Neovim:

" 1. GUI colors (24-bit RGB)
highlight Normal guifg=#abb2bf guibg=#282c34

" 2. Terminal colors (256-color palette)
highlight Normal ctermfg=249 ctermbg=235

" 3. Basic 16-color names
highlight Normal ctermfg=white ctermbg=black

" 4. Attributes
highlight Statement gui=bold cterm=bold
highlight Comment gui=italic cterm=italic
highlight Error gui=underline,bold cterm=underline,bold

" Available attributes:
" - bold
" - italic
" - underline
" - undercurl (wavy underline, GUI only)
" - strikethrough
" - reverse (swap fg/bg)
" - standout
" - NONE (clear all attributes)

25.1.2 Color Code Formats

" Hexadecimal RGB (most common)
" Format: #RRGGBB
highlight Normal guifg=#ffffff guibg=#000000

" Short hex format (automatically expanded)
" #RGB -> #RRGGBB
highlight Comment guifg=#888  " Expands to #888888

" Named colors (X11 color names)
highlight Error guifg=red guibg=black
highlight Todo guifg=yellow guibg=blue

" Terminal color codes (0-255)
" 0-15: Basic colors
" 16-231: 216-color cube (6x6x6)
" 232-255: Grayscale ramp
highlight Normal ctermfg=15 ctermbg=0
highlight Comment ctermfg=244  " Gray

25.1.3 Checking Color Support

" Check terminal capabilities
function! CheckColorSupport()
  echo "GUI running: " . has('gui_running')
  echo "Termguicolors support: " . has('termguicolors')
  echo "256 colors: " . (&t_Co == 256)
  echo "RGB colors: " . has('gui_running') || has('termguicolors')
  
  if exists('+termguicolors')
    echo "Termguicolors setting: " . &termguicolors
  endif
  
  if exists('$COLORTERM')
    echo "COLORTERM: " . $COLORTERM
  endif
endfunction

command! CheckColors call CheckColorSupport()

-- lua/color_support.lua
local M = {}

function M.check_support()
  local info = {
    gui_running = vim.fn.has('gui_running') == 1,
    termguicolors = vim.fn.has('termguicolors') == 1,
    t_Co = vim.o.t_Co,
    rgb_support = vim.fn.has('gui_running') == 1 or 
                  (vim.fn.has('termguicolors') == 1 and vim.o.termguicolors),
    colorterm = vim.env.COLORTERM
  }
  
  return info
end

function M.enable_true_color()
  if vim.fn.has('termguicolors') == 1 then
    vim.o.termguicolors = true
  end
end

return M

25.2 Creating a Complete Colorscheme

25.2.1 Basic Colorscheme Structure

" colors/myscheme.vim
" Vim color scheme
" Name: myscheme
" Author: Your Name
" License: MIT

" Initialization {{{
if version > 580
  hi clear
  if exists("syntax_on")
    syntax reset
  endif
endif

let g:colors_name = "myscheme"

" Set background (optional)
set background=dark
" }}}

" Color Palette {{{
let s:palette = {}

" Base colors
let s:palette.bg0        = ['#1d2021', 234]
let s:palette.bg1        = ['#3c3836', 237]
let s:palette.bg2        = ['#504945', 239]
let s:palette.bg3        = ['#665c54', 241]
let s:palette.bg4        = ['#7c6f64', 243]

let s:palette.fg0        = ['#fbf1c7', 229]
let s:palette.fg1        = ['#ebdbb2', 223]
let s:palette.fg2        = ['#d5c4a1', 250]
let s:palette.fg3        = ['#bdae93', 248]
let s:palette.fg4        = ['#a89984', 246]

" Accent colors
let s:palette.red        = ['#fb4934', 167]
let s:palette.green      = ['#b8bb26', 142]
let s:palette.yellow     = ['#fabd2f', 214]
let s:palette.blue       = ['#83a598', 109]
let s:palette.purple     = ['#d3869b', 175]
let s:palette.aqua       = ['#8ec07c', 108]
let s:palette.orange     = ['#fe8019', 208]

" Dimmed variants
let s:palette.red_dim    = ['#cc2412', 124]
let s:palette.green_dim  = ['#98971a', 106]
let s:palette.yellow_dim = ['#d79921', 172]
let s:palette.blue_dim   = ['#458588', 66]
let s:palette.purple_dim = ['#b16286', 132]
let s:palette.aqua_dim   = ['#689d6a', 72]
let s:palette.orange_dim = ['#d65d0e', 166]
" }}}

" Helper Functions {{{
function! s:HL(group, fg, bg, attr)
  let l:fg = get(s:palette, a:fg, ['NONE', 'NONE'])
  let l:bg = get(s:palette, a:bg, ['NONE', 'NONE'])
  
  let l:cmd = 'highlight ' . a:group
  
  if l:fg[0] != 'NONE'
    let l:cmd .= ' guifg=' . l:fg[0]
  endif
  
  if l:fg[1] != 'NONE'
    let l:cmd .= ' ctermfg=' . l:fg[1]
  endif
  
  if l:bg[0] != 'NONE'
    let l:cmd .= ' guibg=' . l:bg[0]
  endif
  
  if l:bg[1] != 'NONE'
    let l:cmd .= ' ctermbg=' . l:bg[1]
  endif
  
  if a:attr != ''
    let l:cmd .= ' gui=' . a:attr . ' cterm=' . a:attr
  endif
  
  execute l:cmd
endfunction

function! s:Link(from, to)
  execute 'highlight! link ' . a:from . ' ' . a:to
endfunction
" }}}

" UI Elements {{{
call s:HL('Normal', 'fg1', 'bg0', '')
call s:HL('NormalFloat', 'fg1', 'bg1', '')
call s:HL('NormalNC', 'fg1', 'bg0', '')

call s:HL('Cursor', 'bg0', 'fg1', '')
call s:HL('CursorLine', '', 'bg1', '')
call s:HL('CursorColumn', '', 'bg1', '')
call s:HL('CursorLineNr', 'yellow', 'bg1', 'bold')

call s:HL('LineNr', 'bg4', '', '')
call s:HL('SignColumn', '', 'bg0', '')
call s:HL('ColorColumn', '', 'bg1', '')

call s:HL('Visual', '', 'bg2', '')
call s:HL('VisualNOS', '', 'bg2', '')

call s:HL('Search', 'bg0', 'yellow', '')
call s:HL('IncSearch', 'bg0', 'orange', '')
call s:HL('CurSearch', 'bg0', 'red', '')

call s:HL('MatchParen', 'orange', 'bg2', 'bold')

call s:HL('Pmenu', 'fg1', 'bg2', '')
call s:HL('PmenuSel', 'bg0', 'blue', 'bold')
call s:HL('PmenuSbar', '', 'bg2', '')
call s:HL('PmenuThumb', '', 'bg4', '')

call s:HL('StatusLine', 'fg1', 'bg2', '')
call s:HL('StatusLineNC', 'fg4', 'bg1', '')
call s:HL('TabLine', 'fg4', 'bg1', '')
call s:HL('TabLineFill', 'fg4', 'bg1', '')
call s:HL('TabLineSel', 'fg1', 'bg2', 'bold')

call s:HL('VertSplit', 'bg3', 'bg0', '')
call s:HL('WinSeparator', 'bg3', 'bg0', '')

call s:HL('Folded', 'fg4', 'bg1', 'italic')
call s:HL('FoldColumn', 'fg4', 'bg0', '')

call s:HL('DiffAdd', 'green', 'bg1', '')
call s:HL('DiffChange', 'yellow', 'bg1', '')
call s:HL('DiffDelete', 'red', 'bg1', '')
call s:HL('DiffText', 'yellow', 'bg2', 'bold')

call s:HL('SpellBad', 'red', '', 'undercurl')
call s:HL('SpellCap', 'blue', '', 'undercurl')
call s:HL('SpellLocal', 'aqua', '', 'undercurl')
call s:HL('SpellRare', 'purple', '', 'undercurl')

call s:HL('ErrorMsg', 'red', 'bg0', 'bold')
call s:HL('WarningMsg', 'yellow', 'bg0', 'bold')
call s:HL('ModeMsg', 'fg1', '', 'bold')
call s:HL('MoreMsg', 'green', '', 'bold')
call s:HL('Question', 'orange', '', 'bold')

call s:HL('Directory', 'blue', '', 'bold')
call s:HL('Title', 'green', '', 'bold')
call s:HL('SpecialKey', 'fg4', '', '')
call s:HL('NonText', 'bg4', '', '')
call s:HL('Whitespace', 'bg2', '', '')
call s:HL('Conceal', 'blue', '', '')
" }}}

" Syntax Highlighting {{{
call s:HL('Comment', 'fg4', '', 'italic')
call s:HL('SpecialComment', 'fg3', '', 'italic')
call s:HL('Todo', 'fg0', 'yellow', 'bold')

call s:HL('Constant', 'purple', '', '')
call s:HL('String', 'green', '', '')
call s:HL('Character', 'purple', '', '')
call s:HL('Number', 'purple', '', '')
call s:HL('Boolean', 'purple', '', '')
call s:HL('Float', 'purple', '', '')

call s:HL('Identifier', 'blue', '', '')
call s:HL('Function', 'green', '', 'bold')

call s:HL('Statement', 'red', '', '')
call s:HL('Conditional', 'red', '', '')
call s:HL('Repeat', 'red', '', '')
call s:HL('Label', 'red', '', '')
call s:HL('Operator', 'orange', '', '')
call s:HL('Keyword', 'red', '', '')
call s:HL('Exception', 'red', '', '')

call s:HL('PreProc', 'aqua', '', '')
call s:HL('Include', 'aqua', '', '')
call s:HL('Define', 'aqua', '', '')
call s:HL('Macro', 'aqua', '', '')
call s:HL('PreCondit', 'aqua', '', '')

call s:HL('Type', 'yellow', '', '')
call s:HL('StorageClass', 'orange', '', '')
call s:HL('Structure', 'aqua', '', '')
call s:HL('Typedef', 'yellow', '', '')

call s:HL('Special', 'orange', '', '')
call s:HL('SpecialChar', 'orange', '', '')
call s:HL('Tag', 'orange', '', '')
call s:HL('Delimiter', 'fg1', '', '')
call s:HL('Debug', 'red', '', '')

call s:HL('Underlined', 'blue', '', 'underline')
call s:HL('Ignore', 'fg0', '', '')
call s:HL('Error', 'red', 'bg0', 'bold,underline')
" }}}

" Language-Specific {{{
" Vim
call s:Link('vimCommand', 'Statement')
call s:Link('vimVar', 'Identifier')
call s:Link('vimFuncName', 'Function')
call s:Link('vimOption', 'Constant')

" Lua
call s:Link('luaFunction', 'Keyword')
call s:Link('luaTable', 'Structure')

" Python
call s:Link('pythonBuiltin', 'Function')
call s:Link('pythonDecorator', 'PreProc')

" JavaScript
call s:Link('javaScriptFunction', 'Keyword')
call s:Link('javaScriptIdentifier', 'Statement')

" HTML
call s:Link('htmlTag', 'Tag')
call s:Link('htmlEndTag', 'Tag')
call s:Link('htmlArg', 'Identifier')

" Markdown
call s:HL('markdownH1', 'red', '', 'bold')
call s:HL('markdownH2', 'orange', '', 'bold')
call s:HL('markdownH3', 'yellow', '', 'bold')
call s:HL('markdownH4', 'green', '', 'bold')
call s:HL('markdownH5', 'blue', '', 'bold')
call s:HL('markdownH6', 'purple', '', 'bold')
call s:HL('markdownCode', 'aqua', '', '')
call s:HL('markdownCodeBlock', 'aqua', '', '')
call s:HL('markdownUrl', 'blue', '', 'underline')
" }}}

" Plugin Support {{{
" GitGutter / Signify
call s:HL('GitGutterAdd', 'green', '', '')
call s:HL('GitGutterChange', 'yellow', '', '')
call s:HL('GitGutterDelete', 'red', '', '')
call s:HL('SignifySignAdd', 'green', '', '')
call s:HL('SignifySignChange', 'yellow', '', '')
call s:HL('SignifySignDelete', 'red', '', '')

" ALE
call s:HL('ALEError', 'red', '', 'underline')
call s:HL('ALEWarning', 'yellow', '', 'underline')
call s:HL('ALEInfo', 'blue', '', 'underline')

" LSP
call s:HL('DiagnosticError', 'red', '', '')
call s:HL('DiagnosticWarn', 'yellow', '', '')
call s:HL('DiagnosticInfo', 'blue', '', '')
call s:HL('DiagnosticHint', 'aqua', '', '')

" Telescope
call s:HL('TelescopeBorder', 'bg3', 'bg0', '')
call s:HL('TelescopeSelection', 'fg1', 'bg2', 'bold')
call s:HL('TelescopeMatching', 'orange', '', 'bold')

" NvimTree
call s:HL('NvimTreeFolderName', 'blue', '', 'bold')
call s:HL('NvimTreeOpenedFolderName', 'green', '', 'bold')
call s:HL('NvimTreeGitDirty', 'yellow', '', '')
call s:HL('NvimTreeGitNew', 'green', '', '')
call s:HL('NvimTreeGitDeleted', 'red', '', '')
" }}}

" Cleanup {{{
delfunction s:HL
delfunction s:Link
unlet s:palette
" }}}

" vim: set sw=2 ts=2 sts=2 et tw=80 ft=vim fdm=marker:

25.2.2 Lua-based Colorscheme


-- colors/myscheme.lua

-- Modern Neovim colorscheme in Lua


-- Clear existing highlights
vim.cmd('highlight clear')
if vim.fn.exists('syntax_on') then
  vim.cmd('syntax reset')
end

vim.g.colors_name = 'myscheme'
vim.o.background = 'dark'


-- Enable true color support
if vim.fn.has('termguicolors') == 1 then
  vim.o.termguicolors = true
end


-- Color palette
local colors = {

  -- Base colors
  bg0 = '#1d2021',
  bg1 = '#3c3836',
  bg2 = '#504945',
  bg3 = '#665c54',
  bg4 = '#7c6f64',
  
  fg0 = '#fbf1c7',
  fg1 = '#ebdbb2',
  fg2 = '#d5c4a1',
  fg3 = '#bdae93',
  fg4 = '#a89984',
  

  -- Accent colors
  red = '#fb4934',
  green = '#b8bb26',
  yellow = '#fabd2f',
  blue = '#83a598',
  purple = '#d3869b',
  aqua = '#8ec07c',
  orange = '#fe8019',
  

  -- Dimmed variants
  red_dim = '#cc2412',
  green_dim = '#98971a',
  yellow_dim = '#d79921',
  blue_dim = '#458588',
  purple_dim = '#b16286',
  aqua_dim = '#689d6a',
  orange_dim = '#d65d0e',
  

  -- Special
  none = 'NONE'
}


-- Helper function to set highlights
local function hl(group, opts)
  opts = opts or {}
  local cmd = { 'highlight', group }
  
  if opts.fg then table.insert(cmd, 'guifg=' .. opts.fg) end
  if opts.bg then table.insert(cmd, 'guibg=' .. opts.bg) end
  if opts.sp then table.insert(cmd, 'guisp=' .. opts.sp) end
  
  if opts.bold then table.insert(cmd, 'gui=bold cterm=bold')
  elseif opts.italic then table.insert(cmd, 'gui=italic cterm=italic')
  elseif opts.underline then table.insert(cmd, 'gui=underline cterm=underline')
  elseif opts.undercurl then table.insert(cmd, 'gui=undercurl')
  elseif opts.strikethrough then table.insert(cmd, 'gui=strikethrough')
  elseif opts.reverse then table.insert(cmd, 'gui=reverse cterm=reverse')
  elseif opts.standout then table.insert(cmd, 'gui=standout cterm=standout')
  elseif opts.attr then table.insert(cmd, 'gui=' .. opts.attr .. ' cterm=' .. opts.attr)
  end
  
  vim.cmd(table.concat(cmd, ' '))
end


-- Link one highlight group to another
local function link(from, to)
  vim.cmd(string.format('highlight! link %s %s', from, to))
end


-- UI Highlights
local ui_groups = {
  Normal = { fg = colors.fg1, bg = colors.bg0 },
  NormalFloat = { fg = colors.fg1, bg = colors.bg1 },
  NormalNC = { fg = colors.fg1, bg = colors.bg0 },
  
  Cursor = { fg = colors.bg0, bg = colors.fg1 },
  CursorLine = { bg = colors.bg1 },
  CursorColumn = { bg = colors.bg1 },
  CursorLineNr = { fg = colors.yellow, bg = colors.bg1, bold = true },
  
  LineNr = { fg = colors.bg4 },
  SignColumn = { bg = colors.bg0 },
  ColorColumn = { bg = colors.bg1 },
  
  Visual = { bg = colors.bg2 },
  VisualNOS = { bg = colors.bg2 },
  
  Search = { fg = colors.bg0, bg = colors.yellow },
  IncSearch = { fg = colors.bg0, bg = colors.orange },
  CurSearch = { fg = colors.bg0, bg = colors.red },
  
  MatchParen = { fg = colors.orange, bg = colors.bg2, bold = true },
  
  Pmenu = { fg = colors.fg1, bg = colors.bg2 },
  PmenuSel = { fg = colors.bg0, bg = colors.blue, bold = true },
  PmenuSbar = { bg = colors.bg2 },
  PmenuThumb = { bg = colors.bg4 },
  
  StatusLine = { fg = colors.fg1, bg = colors.bg2 },
  StatusLineNC = { fg = colors.fg4, bg = colors.bg1 },
  
  TabLine = { fg = colors.fg4, bg = colors.bg1 },
  TabLineFill = { fg = colors.fg4, bg = colors.bg1 },
  TabLineSel = { fg = colors.fg1, bg = colors.bg2, bold = true },
  
  VertSplit = { fg = colors.bg3, bg = colors.bg0 },
  WinSeparator = { fg = colors.bg3, bg = colors.bg0 },
  
  Folded = { fg = colors.fg4, bg = colors.bg1, italic = true },
  FoldColumn = { fg = colors.fg4, bg = colors.bg0 },
  
  DiffAdd = { fg = colors.green, bg = colors.bg1 },
  DiffChange = { fg = colors.yellow, bg = colors.bg1 },
  DiffDelete = { fg = colors.red, bg = colors.bg1 },
  DiffText = { fg = colors.yellow, bg = colors.bg2, bold = true },
  
  SpellBad = { fg = colors.red, undercurl = true, sp = colors.red },
  SpellCap = { fg = colors.blue, undercurl = true, sp = colors.blue },
  SpellLocal = { fg = colors.aqua, undercurl = true, sp = colors.aqua },
  SpellRare = { fg = colors.purple, undercurl = true, sp = colors.purple },
  
  ErrorMsg = { fg = colors.red, bg = colors.bg0, bold = true },
  WarningMsg = { fg = colors.yellow, bg = colors.bg0, bold = true },
  ModeMsg = { fg = colors.fg1, bold = true },
  MoreMsg = { fg = colors.green, bold = true },
  Question = { fg = colors.orange, bold = true },
  
  Directory = { fg = colors.blue, bold = true },
  Title = { fg = colors.green, bold = true },
  SpecialKey = { fg = colors.fg4 },
  NonText = { fg = colors.bg4 },
  Whitespace = { fg = colors.bg2 },
  Conceal = { fg = colors.blue },
}


-- Syntax Highlights
local syntax_groups = {
  Comment = { fg = colors.fg4, italic = true },
  SpecialComment = { fg = colors.fg3, italic = true },
  Todo = { fg = colors.fg0, bg = colors.yellow, bold = true },
  
  Constant = { fg = colors.purple },
  String = { fg = colors.green },
  Character = { fg = colors.purple },
  Number = { fg = colors.purple },
  Boolean = { fg = colors.purple },
  Float = { fg = colors.purple },
  
  Identifier = { fg = colors.blue },
  Function = { fg = colors.green, bold = true },
  
  Statement = { fg = colors.red },
  Conditional = { fg = colors.red },
  Repeat = { fg = colors.red },
  Label = { fg = colors.red },
  Operator = { fg = colors.orange },
  Keyword = { fg = colors.red },
  Exception = { fg = colors.red },
  
  PreProc = { fg = colors.aqua },
  Include = { fg = colors.aqua },
  Define = { fg = colors.aqua },
  Macro = { fg = colors.aqua },
  PreCondit = { fg = colors.aqua },
  
  Type = { fg = colors.yellow },
  StorageClass = { fg = colors.orange },
  Structure = { fg = colors.aqua },
  Typedef = { fg = colors.yellow },
  
  Special = { fg = colors.orange },
  SpecialChar = { fg = colors.orange },
  Tag = { fg = colors.orange },
  Delimiter = { fg = colors.fg1 },
  Debug = { fg = colors.red },
  
  Underlined = { fg = colors.blue, underline = true },
  Ignore = { fg = colors.fg0 },
  Error = { fg = colors.red, bg = colors.bg0, bold = true, underline = true },
}


-- Apply UI highlights
for group, opts in pairs(ui_groups) do
  hl(group, opts)
end


-- Apply syntax highlights
for group, opts in pairs(syntax_groups) do
  hl(group, opts)
end


-- Tree-sitter highlights
local treesitter_groups = {
  ['@variable'] = { fg = colors.blue },
  ['@variable.builtin'] = { fg = colors.purple },
  ['@variable.parameter'] = { fg = colors.fg1 },
  ['@variable.member'] = { fg = colors.blue },
  
  ['@constant'] = { fg = colors.purple },
  ['@constant.builtin'] = { fg = colors.purple },
  ['@constant.macro'] = { fg = colors.aqua },
  
  ['@string'] = { fg = colors.green },
  ['@string.escape'] = { fg = colors.orange },
  ['@string.special'] = { fg = colors.orange },
  
  ['@character'] = { fg = colors.purple },
  ['@number'] = { fg = colors.purple },
  ['@boolean'] = { fg = colors.purple },
  ['@float'] = { fg = colors.purple },
  
  ['@function'] = { fg = colors.green, bold = true },
  ['@function.builtin'] = { fg = colors.yellow },
  ['@function.macro'] = { fg = colors.aqua },
  ['@function.method'] = { fg = colors.green },
  
  ['@constructor'] = { fg = colors.yellow },
  ['@keyword'] = { fg = colors.red },
  ['@keyword.function'] = { fg = colors.red },
  ['@keyword.operator'] = { fg = colors.red },
  ['@keyword.return'] = { fg = colors.red },
  
  ['@conditional'] = { fg = colors.red },
  ['@repeat'] = { fg = colors.red },
  ['@label'] = { fg = colors.red },
  ['@operator'] = { fg = colors.orange },
  ['@exception'] = { fg = colors.red },
  
  ['@type'] = { fg = colors.yellow },
  ['@type.builtin'] = { fg = colors.yellow },
  ['@type.definition'] = { fg = colors.yellow },
  
  ['@attribute'] = { fg = colors.aqua },
  ['@property'] = { fg = colors.blue },
  ['@field'] = { fg = colors.blue },
  
  ['@comment'] = { fg = colors.fg4, italic = true },
  ['@comment.todo'] = { fg = colors.fg0, bg = colors.yellow, bold = true },
  ['@comment.warning'] = { fg = colors.fg0, bg = colors.orange, bold = true },
  ['@comment.error'] = { fg = colors.fg0, bg = colors.red, bold = true },
  
  ['@punctuation.delimiter'] = { fg = colors.fg1 },
  ['@punctuation.bracket'] = { fg = colors.fg1 },
  ['@punctuation.special'] = { fg = colors.orange },
  
  ['@tag'] = { fg = colors.orange },
  ['@tag.attribute'] = { fg = colors.blue },
  ['@tag.delimiter'] = { fg = colors.fg1 },
}

for group, opts in pairs(treesitter_groups) do
  hl(group, opts)
end


-- LSP Semantic tokens
local lsp_groups = {
  ['@lsp.type.class'] = { link = 'Type' },
  ['@lsp.type.decorator'] = { link = 'Function' },
  ['@lsp.type.enum'] = { link = 'Type' },
  ['@lsp.type.enumMember'] = { link = 'Constant' },
  ['@lsp.type.function'] = { link = 'Function' },
  ['@lsp.type.interface'] = { link = 'Type' },
  ['@lsp.type.macro'] = { link = 'Macro' },
  ['@lsp.type.method'] = { link = 'Function' },
  ['@lsp.type.namespace'] = { link = 'Include' },
  ['@lsp.type.parameter'] = { link = 'Identifier' },
  ['@lsp.type.property'] = { link = 'Identifier' },
  ['@lsp.type.struct'] = { link = 'Type' },
  ['@lsp.type.type'] = { link = 'Type' },
  ['@lsp.type.typeParameter'] = { link = 'Type' },
  ['@lsp.type.variable'] = { link = 'Identifier' },
}

for group, opts in pairs(lsp_groups) do
  if opts.link then
    link(group, opts.link)
  else
    hl(group, opts)
  end
end


-- Diagnostic highlights
hl('DiagnosticError', { fg = colors.red })
hl('DiagnosticWarn', { fg = colors.yellow })
hl('DiagnosticInfo', { fg = colors.blue })
hl('DiagnosticHint', { fg = colors.aqua })

hl('DiagnosticUnderlineError', { undercurl = true, sp = colors.red })
hl('DiagnosticUnderlineWarn', { undercurl = true, sp = colors.yellow })
hl('DiagnosticUnderlineInfo', { undercurl = true, sp = colors.blue })
hl('DiagnosticUnderlineHint', { undercurl = true, sp = colors.aqua })


-- Plugin-specific highlights

-- GitSigns
hl('GitSignsAdd', { fg = colors.green })
hl('GitSignsChange', { fg = colors.yellow })
hl('GitSignsDelete', { fg = colors.red })


-- Telescope
hl('TelescopeBorder', { fg = colors.bg3, bg = colors.bg0 })
hl('TelescopeSelection', { fg = colors.fg1, bg = colors.bg2, bold = true })
hl('TelescopeMatching', { fg = colors.orange, bold = true })


-- NvimTree
hl('NvimTreeFolderName', { fg = colors.blue, bold = true })
hl('NvimTreeOpenedFolderName', { fg = colors.green, bold = true })
hl('NvimTreeGitDirty', { fg = colors.yellow })
hl('NvimTreeGitNew', { fg = colors.green })
hl('NvimTreeGitDeleted', { fg = colors.red })


-- Indent Blankline
hl('IndentBlanklineChar', { fg = colors.bg2 })
hl('IndentBlanklineContextChar', { fg = colors.bg3 })


-- Which-key
hl('WhichKey', { fg = colors.red })
hl('WhichKeyGroup', { fg = colors.blue })
hl('WhichKeyDesc', { fg = colors.fg1 })
hl('WhichKeySeparator', { fg = colors.fg4 })


-- Dashboard
hl('DashboardHeader', { fg = colors.blue })
hl('DashboardCenter', { fg = colors.green })
hl('DashboardFooter', { fg = colors.fg4, italic = true })

25.3 Advanced Colorscheme Techniques

25.3.1 Dynamic Color Adjustment


-- lua/colorscheme/dynamic.lua
local M = {}


-- Convert hex to RGB
function M.hex_to_rgb(hex)
  hex = hex:gsub('#', '')
  return {
    r = tonumber(hex:sub(1, 2), 16),
    g = tonumber(hex:sub(3, 4), 16),
    b = tonumber(hex:sub(5, 6), 16)
  }
end


-- Convert RGB to hex
function M.rgb_to_hex(rgb)
  return string.format('#%02x%02x%02x',
    math.floor(rgb.r),
    math.floor(rgb.g),
    math.floor(rgb.b)
  )
end


-- Blend two colors
function M.blend(color1, color2, ratio)
  local c1 = M.hex_to_rgb(color1)
  local c2 = M.hex_to_rgb(color2)
  
  return M.rgb_to_hex({
    r = c1.r * ratio + c2.r * (1 - ratio),
    g = c1.g * ratio + c2.g * (1 - ratio),
    b = c1.b * ratio + c2.b * (1 - ratio)
  })
end


-- Lighten a color
function M.lighten(color, amount)
  return M.blend(color, '#ffffff', 1 - amount)
end


-- Darken a color
function M.darken(color, amount)
  return M.blend(color, '#000000', 1 - amount)
end


-- Adjust saturation
function M.saturate(color, amount)
  local rgb = M.hex_to_rgb(color)
  

  -- Convert to HSL
  local max = math.max(rgb.r, rgb.g, rgb.b)
  local min = math.min(rgb.r, rgb.g, rgb.b)
  local l = (max + min) / 2 / 255
  
  if max == min then
    return color  -- Grayscale
  end
  
  local d = max - min
  local s
  if l > 0.5 then
    s = d / (2 * 255 - max - min)
  else
    s = d / (max + min)
  end
  

  -- Adjust saturation
  s = math.max(0, math.min(1, s + amount))
  

  -- Convert back to RGB (simplified)
  local function hue_to_rgb(p, q, t)
    if t < 0 then t = t + 1 end
    if t > 1 then t = t - 1 end
    if t < 1/6 then return p + (q - p) * 6 * t end
    if t < 1/2 then return q end
    if t < 2/3 then return p + (q - p) * (2/3 - t) * 6 end
    return p
  end
  
  local q = l < 0.5 and l * (1 + s) or l + s - l * s
  local p = 2 * l - q
  

  -- Calculate hue
  local h
  if rgb.r == max then
    h = (rgb.g - rgb.b) / d + (rgb.g < rgb.b and 6 or 0)
  elseif rgb.g == max then
    h = (rgb.b - rgb.r) / d + 2
  else
    h = (rgb.r - rgb.g) / d + 4
  end
  h = h / 6
  
  return M.rgb_to_hex({
    r = hue_to_rgb(p, q, h + 1/3) * 255,
    g = hue_to_rgb(p, q, h) * 255,
    b = hue_to_rgb(p, q, h - 1/3) * 255
  })
end


-- Create color variations
function M.create_palette(base_color, options)
  options = options or {}
  local steps = options.steps or 5
  local darken_amount = options.darken or 0.2
  local lighten_amount = options.lighten or 0.2
  
  local palette = { [steps] = base_color }
  

  -- Create darker variants
  for i = steps - 1, 1, -1 do
    local amount = (steps - i) * darken_amount / (steps - 1)
    palette[i] = M.darken(base_color, amount)
  end
  

  -- Create lighter variants
  for i = steps + 1, steps * 2 - 1 do
    local amount = (i - steps) * lighten_amount / (steps - 1)
    palette[i] = M.lighten(base_color, amount)
  end
  
  return palette
end


-- Generate complementary colors
function M.complementary(color)
  local rgb = M.hex_to_rgb(color)
  return M.rgb_to_hex({
    r = 255 - rgb.r,
    g = 255 - rgb.g,
    b = 255 - rgb.b
  })
end


-- Generate analogous colors
function M.analogous(color, angle)
  angle = angle or 30
  local rgb = M.hex_to_rgb(color)
  

  -- Simplified rotation (not accurate HSL rotation)
  local function rotate(c, deg)
    local rad = math.rad(deg)
    return {
      r = math.max(0, math.min(255, c.r * math.cos(rad) - c.g * math.sin(rad))),
      g = math.max(0, math.min(255, c.r * math.sin(rad) + c.g * math.cos(rad))),
      b = c.b
    }
  end
  
  return {
    M.rgb_to_hex(rotate(rgb, -angle)),
    color,
    M.rgb_to_hex(rotate(rgb, angle))
  }
end


-- Adjust contrast
function M.ensure_contrast(fg, bg, min_ratio)
  min_ratio = min_ratio or 4.5  -- WCAG AA standard
  
  local function relative_luminance(color)
    local rgb = M.hex_to_rgb(color)
    local function adjust(val)
      val = val / 255
      return val <= 0.03928 and val / 12.92 or ((val + 0.055) / 1.055) ^ 2.4
    end
    return 0.2126 * adjust(rgb.r) + 0.7152 * adjust(rgb.g) + 0.0722 * adjust(rgb.b)
  end
  
  local function contrast_ratio(c1, c2)
    local l1 = relative_luminance(c1)
    local l2 = relative_luminance(c2)
    local lighter = math.max(l1, l2)
    local darker = math.min(l1, l2)
    return (lighter + 0.05) / (darker + 0.05)
  end
  
  local ratio = contrast_ratio(fg, bg)
  
  if ratio >= min_ratio then
    return fg
  end
  

  -- Adjust foreground
  local step = 0.1
  local attempts = 0
  local adjusted_fg = fg
  
  while ratio < min_ratio and attempts < 20 do
    if relative_luminance(bg) > 0.5 then
      adjusted_fg = M.darken(adjusted_fg, step)
    else
      adjusted_fg = M.lighten(adjusted_fg, step)
    end
    
    ratio = contrast_ratio(adjusted_fg, bg)
    attempts = attempts + 1
  end
  
  return adjusted_fg
end

return M

25.3.2 Time-based Color Adjustment


-- lua/colorscheme/adaptive.lua
local M = {}
local dynamic = require('colorscheme.dynamic')


-- Get time-based brightness adjustment
function M.get_time_adjustment()
  local hour = tonumber(os.date('%H'))
  

  -- Night: 22-6 (darker)

  -- Morning: 6-10 (medium)

  -- Day: 10-18 (brighter)

  -- Evening: 18-22 (medium-dark)
  
  if hour >= 22 or hour < 6 then
    return { brightness = -0.2, warmth = 0.1 }
  elseif hour >= 6 and hour < 10 then
    return { brightness = 0, warmth = 0.05 }
  elseif hour >= 10 and hour < 18 then
    return { brightness = 0.1, warmth = 0 }
  else
    return { brightness = -0.1, warmth = 0.05 }
  end
end


-- Adjust colorscheme based on time
function M.adjust_by_time(colors)
  local adjustment = M.get_time_adjustment()
  local adjusted = {}
  
  for name, color in pairs(colors) do
    if adjustment.brightness ~= 0 then
      if adjustment.brightness > 0 then
        color = dynamic.lighten(color, adjustment.brightness)
      else
        color = dynamic.darken(color, -adjustment.brightness)
      end
    end
    
    if adjustment.warmth ~= 0 then

      -- Add warmth by increasing red/yellow
      local rgb = dynamic.hex_to_rgb(color)
      rgb.r = math.min(255, rgb.r + adjustment.warmth * 50)
      rgb.g = math.min(255, rgb.g + adjustment.warmth * 30)
      color = dynamic.rgb_to_hex(rgb)
    end
    
    adjusted[name] = color
  end
  
  return adjusted
end


-- Auto-adjust every hour
function M.setup_auto_adjust()
  local timer = vim.loop.new_timer()
  
  local function apply_adjustment()

    -- Reload colorscheme with adjustments
    vim.cmd('colorscheme ' .. vim.g.colors_name)
  end
  

  -- Check every hour
  timer:start(0, 3600000, vim.schedule_wrap(apply_adjustment))
  
  return timer
end


-- Detect system dark mode
function M.is_system_dark_mode()

  -- macOS
  if vim.fn.has('mac') == 1 then
    local handle = io.popen('defaults read -g AppleInterfaceStyle 2>/dev/null')
    local result = handle:read('*a')
    handle:close()
    return result:match('Dark') ~= nil
  end
  

  -- Linux with gsettings (GNOME)
  if vim.fn.executable('gsettings') == 1 then
    local handle = io.popen("gsettings get org.gnome.desktop.interface gtk-theme 2>/dev/null")
    local result = handle:read('*a')
    handle:close()
    return result:match('[Dd]ark') ~= nil
  end
  

  -- Fallback to time-based detection
  local hour = tonumber(os.date('%H'))
  return hour < 6 or hour >= 18
end


-- Auto-switch based on system
function M.auto_switch()
  local is_dark = M.is_system_dark_mode()
  
  if is_dark then
    vim.o.background = 'dark'
  else
    vim.o.background = 'light'
  end
  

  -- Trigger ColorScheme autocmd
  vim.cmd('doautocmd ColorScheme')
end

return M

25.3.3 Colorscheme Builder Pattern


-- lua/colorscheme/builder.lua
local M = {}

M.Builder = {}
M.Builder.__index = M.Builder

function M.new(name)
  local self = setmetatable({}, M.Builder)
  self.name = name
  self.palette = {}
  self.highlights = {}
  self.links = {}
  self.options = {
    background = 'dark',
    terminal_colors = true,
    italic_comments = true,
    italic_keywords = false,
    transparent_background = false
  }
  return self
end

function M.Builder:set_palette(palette)
  self.palette = palette
  return self
end

function M.Builder:set_options(options)
  for k, v in pairs(options) do
    self.options[k] = v
  end
  return self
end

function M.Builder:add_highlight(group, opts)
  self.highlights[group] = opts
  return self
end

function M.Builder:add_link(from, to)
  self.links[from] = to
  return self
end

function M.Builder:add_ui_highlights()
  local c = self.palette
  local transparent = self.options.transparent_background
  
  self:add_highlight('Normal', {
    fg = c.fg,
    bg = transparent and 'NONE' or c.bg
  })
  
  self:add_highlight('CursorLine', {
    bg = transparent and 'NONE' or c.bg_highlight
  })
  
  self:add_highlight('Comment', {
    fg = c.comment,
    italic = self.options.italic_comments
  })
  
  return self
end

function M.Builder:add_syntax_highlights()
  local c = self.palette
  
  self:add_highlight('String', { fg = c.green })
  self:add_highlight('Number', { fg = c.orange })
  self:add_highlight('Boolean', { fg = c.orange })
  self:add_highlight('Function', { fg = c.blue })
  self:add_highlight('Keyword', {
    fg = c.purple,
    italic = self.options.italic_keywords
  })
  
  return self
end

function M.Builder:add_treesitter_highlights()
  self:add_link('@variable', 'Identifier')
  self:add_link('@function', 'Function')
  self:add_link('@keyword', 'Keyword')
  self:add_link('@string', 'String')
  self:add_link('@number', 'Number')
  
  return self
end

function M.Builder:add_lsp_highlights()
  local c = self.palette
  
  self:add_highlight('DiagnosticError', { fg = c.red })
  self:add_highlight('DiagnosticWarn', { fg = c.yellow })
  self:add_highlight('DiagnosticInfo', { fg = c.blue })
  self:add_highlight('DiagnosticHint', { fg = c.cyan })
  
  return self
end

function M.Builder:set_terminal_colors()
  if not self.options.terminal_colors then
    return self
  end
  
  local c = self.palette
  
  vim.g.terminal_color_0 = c.black
  vim.g.terminal_color_1 = c.red
  vim.g.terminal_color_2 = c.green
  vim.g.terminal_color_3 = c.yellow
  vim.g.terminal_color_4 = c.blue
  vim.g.terminal_color_5 = c.magenta
  vim.g.terminal_color_6 = c.cyan
  vim.g.terminal_color_7 = c.white
  vim.g.terminal_color_8 = c.bright_black
  vim.g.terminal_color_9 = c.bright_red
  vim.g.terminal_color_10 = c.bright_green
  vim.g.terminal_color_11 = c.bright_yellow
  vim.g.terminal_color_12 = c.bright_blue
  vim.g.terminal_color_13 = c.bright_magenta
  vim.g.terminal_color_14 = c.bright_cyan
  vim.g.terminal_color_15 = c.bright_white
  
  return self
end

function M.Builder:apply()

  -- Clear existing highlights
  vim.cmd('highlight clear')
  if vim.fn.exists('syntax_on') then
    vim.cmd('syntax reset')
  end
  

  -- Set colorscheme name
  vim.g.colors_name = self.name
  

  -- Set background
  vim.o.background = self.options.background
  

  -- Apply highlights
  for group, opts in pairs(self.highlights) do
    local cmd = 'highlight ' .. group
    
    if opts.fg then cmd = cmd .. ' guifg=' .. opts.fg end
    if opts.bg then cmd = cmd .. ' guibg=' .. opts.bg end
    if opts.sp then cmd = cmd .. ' guisp=' .. opts.sp end
    
    local attrs = {}
    if opts.bold then table.insert(attrs, 'bold') end
    if opts.italic then table.insert(attrs, 'italic') end
    if opts.underline then table.insert(attrs, 'underline') end
    if opts.undercurl then table.insert(attrs, 'undercurl') end
    if opts.strikethrough then table.insert(attrs, 'strikethrough') end
    if opts.reverse then table.insert(attrs, 'reverse') end
    
    if #attrs > 0 then
      local attr_str = table.concat(attrs, ',')
      cmd = cmd .. ' gui=' .. attr_str .. ' cterm=' .. attr_str
    end
    
    vim.cmd(cmd)
  end
  

  -- Apply links
  for from, to in pairs(self.links) do
    vim.cmd(string.format('highlight! link %s %s', from, to))
  end
  

  -- Set terminal colors
  self:set_terminal_colors()
end

function M.Builder:build()
  self:add_ui_highlights()
      :add_syntax_highlights()
      :add_treesitter_highlights()
      :add_lsp_highlights()
      :apply()
end


-- Example usage:

--[[
local builder = require('colorscheme.builder')

local palette = {
  bg = '#282c34',
  fg = '#abb2bf',
  red = '#e06c75',
  green = '#98c379',
  yellow = '#e5c07b',
  blue = '#61afef',
  purple = '#c678dd',
  cyan = '#56b6c2',
  orange = '#d19a66',

  -- ... more colors
}

builder.new('myscheme')
  :set_palette(palette)
  :set_options({ italic_comments = true })
  :build()
]]

return M

25.4 Terminal Color Management

25.4.1 Terminal Color Detection


-- lua/colorscheme/terminal.lua
local M = {}


-- Detect terminal emulator
function M.detect_terminal()
  local term_program = vim.env.TERM_PROGRAM
  local term = vim.env.TERM
  
  if term_program == 'iTerm.app' then
    return 'iterm2'
  elseif term_program == 'Apple_Terminal' then
    return 'apple_terminal'
  elseif vim.env.ALACRITTY_SOCKET then
    return 'alacritty'
  elseif vim.env.KITTY_WINDOW_ID then
    return 'kitty'
  elseif term:match('tmux') then
    return 'tmux'
  elseif term:match('screen') then
    return 'screen'
  elseif term:match('xterm') then
    return 'xterm'
  else
    return 'unknown'
  end
end


-- Check true color support
function M.supports_true_color()
  if vim.fn.has('gui_running') == 1 then
    return true
  end
  
  if vim.fn.has('termguicolors') == 0 then
    return false
  end
  
  local colorterm = vim.env.COLORTERM
  if colorterm == 'truecolor' or colorterm == '24bit' then
    return true
  end
  
  local term = M.detect_terminal()
  local true_color_terms = {
    iterm2 = true,
    kitty = true,
    alacritty = true
  }
  
  return true_color_terms[term] or false
end


-- Get color count
function M.get_color_count()
  if M.supports_true_color() then
    return 16777216  -- 24-bit
  elseif vim.o.t_Co == 256 then
    return 256
  else
    return 16
  end
end


-- Convert 24-bit color to 256-color
function M.rgb_to_256(hex)
  hex = hex:gsub('#', '')
  local r = tonumber(hex:sub(1, 2), 16)
  local g = tonumber(hex:sub(3, 4), 16)
  local b = tonumber(hex:sub(5, 6), 16)
  

  -- Grayscale
  if r == g and g == b then
    if r < 8 then
      return 16
    elseif r > 248 then
      return 231
    else
      return math.floor(((r - 8) / 10)) + 232
    end
  end
  

  -- Color cube
  local function to_6(val)
    if val < 48 then return 0
    elseif val < 115 then return 1
    else return math.floor((val - 35) / 40)
    end
  end
  
  return 16 + 36 * to_6(r) + 6 * to_6(g) + to_6(b)
end


-- Setup terminal colors based on capabilities
function M.setup_optimal_colors()
  if M.supports_true_color() then
    vim.o.termguicolors = true
    return '24bit'
  elseif vim.o.t_Co >= 256 then
    vim.o.termguicolors = false
    vim.o.t_Co = 256
    return '256color'
  else
    vim.o.termguicolors = false
    vim.o.t_Co = 16
    return '16color'
  end
end

return M

25.4.2 Fallback Color Schemes


-- lua/colorscheme/fallback.lua
local M = {}
local terminal = require('colorscheme.terminal')


-- Provide fallback colors for limited terminals
function M.get_fallback_palette()
  local color_count = terminal.get_color_count()
  
  if color_count >= 256 then
    return {
      black = 0,
      red = 1,
      green = 2,
      yellow = 3,
      blue = 4,
      magenta = 5,
      cyan = 6,
      white = 7,
      bright_black = 8,
      bright_red = 9,
      bright_green = 10,
      bright_yellow = 11,
      bright_blue = 12,
      bright_magenta = 13,
      bright_cyan = 14,
      bright_white = 15,

      -- Extended 256-color codes
      gray = 244,
      dark_red = 124,
      dark_green = 28,
      dark_yellow = 136,
      dark_blue = 24,
      dark_magenta = 90,
      dark_cyan = 23,
      orange = 208,
    }
  else
    return {
      black = 0,
      red = 1,
      green = 2,
      yellow = 3,
      blue = 4,
      magenta = 5,
      cyan = 6,
      white = 7,
      bright_black = 8,
      bright_red = 9,
      bright_green = 10,
      bright_yellow = 11,
      bright_blue = 12,
      bright_magenta = 13,
      bright_cyan = 14,
      bright_white = 15
    }
  end
end


-- Apply colorscheme with fallbacks
function M.apply_with_fallback(gui_colors, term_colors)
  local supports_true = terminal.supports_true_color()
  
  for group, opts in pairs(gui_colors) do
    local cmd = 'highlight ' .. group
    
    if supports_true then
      if opts.fg then cmd = cmd .. ' guifg=' .. opts.fg end
      if opts.bg then cmd = cmd .. ' guibg=' .. opts.bg end
    end
    

    -- Always provide terminal fallback
    if term_colors[group] then
      local term_opts = term_colors[group]
      if term_opts.fg then cmd = cmd .. ' ctermfg=' .. term_opts.fg end
      if term_opts.bg then cmd = cmd .. ' ctermbg=' .. term_opts.bg end
    end
    
    if opts.attr then
      cmd = cmd .. ' gui=' .. opts.attr .. ' cterm=' .. opts.attr
    end
    
    vim.cmd(cmd)
  end
end

return M

25.5 Colorscheme Management

25.5.1 Colorscheme Switcher


-- lua/colorscheme/switcher.lua
local M = {}

M.colorschemes = {}
M.current_index = 1


-- Register colorschemes
function M.register(schemes)
  M.colorschemes = schemes
end


-- Switch to next colorscheme
function M.next()
  M.current_index = M.current_index + 1
  if M.current_index > #M.colorschemes then
    M.current_index = 1
  end
  
  local scheme = M.colorschemes[M.current_index]
  vim.cmd('colorscheme ' .. scheme)
  print('Colorscheme: ' .. scheme)
end


-- Switch to previous colorscheme
function M.prev()
  M.current_index = M.current_index - 1
  if M.current_index < 1 then
    M.current_index = #M.colorschemes
  end
  
  local scheme = M.colorschemes[M.current_index]
  vim.cmd('colorscheme ' .. scheme)
  print('Colorscheme: ' .. scheme)
end


-- Random colorscheme
function M.random()
  math.randomseed(os.time())
  M.current_index = math.random(1, #M.colorschemes)
  
  local scheme = M.colorschemes[M.current_index]
  vim.cmd('colorscheme ' .. scheme)
  print('Colorscheme: ' .. scheme)
end


-- Select from menu
function M.select()
  vim.ui.select(M.colorschemes, {
    prompt = 'Select colorscheme:',
  }, function(choice, idx)
    if choice then
      M.current_index = idx
      vim.cmd('colorscheme ' .. choice)
    end
  end)
end


-- Setup keymaps
function M.setup_keymaps()
  vim.keymap.set('n', '<leader>cn', M.next, { desc = 'Next colorscheme' })
  vim.keymap.set('n', '<leader>cp', M.prev, { desc = 'Previous colorscheme' })
  vim.keymap.set('n', '<leader>cr', M.random, { desc = 'Random colorscheme' })
  vim.keymap.set('n', '<leader>cs', M.select, { desc = 'Select colorscheme' })
end


-- Example usage:

--[[
require('colorscheme.switcher').register({
  'gruvbox',
  'tokyonight',
  'catppuccin',
  'nord',
  'onedark'
})
]]

return M

25.5.2 Colorscheme Persistence


-- lua/colorscheme/persistence.lua
local M = {}

M.config_file = vim.fn.stdpath('data') .. '/colorscheme.json'


-- Save current colorscheme
function M.save()
  local data = {
    name = vim.g.colors_name,
    background = vim.o.background,
    timestamp = os.time()
  }
  
  local file = io.open(M.config_file, 'w')
  if file then
    file:write(vim.json.encode(data))
    file:close()
    return true
  end
  
  return false
end


-- Load saved colorscheme
function M.load()
  local file = io.open(M.config_file, 'r')
  if not file then
    return nil
  end
  
  local content = file:read('*a')
  file:close()
  
  local ok, data = pcall(vim.json.decode, content)
  if not ok or not data then
    return nil
  end
  
  return data
end


-- Apply saved colorscheme
function M.apply()
  local data = M.load()
  if not data then
    return false
  end
  
  pcall(function()
    vim.o.background = data.background
    vim.cmd('colorscheme ' .. data.name)
  end)
  
  return true
end


-- Auto-save on colorscheme change
function M.setup_autosave()
  vim.api.nvim_create_autocmd('ColorScheme', {
    callback = function()
      vim.defer_fn(M.save, 100)
    end
  })
end

return M

25.5.3 Preview System


-- lua/colorscheme/preview.lua
local M = {}


-- Create preview window
function M.create_preview_window()
  local buf = vim.api.nvim_create_buf(false, true)
  

  -- Sample code for preview
  local lines = {
    '-- Lua code sample',
    'local M = {}',
    '',
    'function M.greet(name)',
    '  -- Print greeting',
    '  print("Hello, " .. name .. "!")',
    '  return true',
    'end',
    '',
    'local numbers = {1, 2, 3, 4, 5}',
    'local result = 0',
    '',
    'for i, num in ipairs(numbers) do',
    '  result = result + num',
    'end',
    '',
    'return M',
  }
  
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
  vim.api.nvim_buf_set_option(buf, 'filetype', 'lua')
  vim.api.nvim_buf_set_option(buf, 'modifiable', false)
  

  -- Create floating window
  local width = 60
  local height = #lines + 2
  local win = vim.api.nvim_open_win(buf, true, {
    relative = 'editor',
    width = width,
    height = height,
    col = (vim.o.columns - width) / 2,
    row = (vim.o.lines - height) / 2,
    style = 'minimal',
    border = 'rounded',
    title = ' Colorscheme Preview ',
    title_pos = 'center'
  })
  
  return buf, win
end


-- Preview colorscheme
function M.preview(colorscheme)
  local original = vim.g.colors_name
  
  local buf, win = M.create_preview_window()
  

  -- Apply colorscheme
  pcall(vim.cmd, 'colorscheme ' .. colorscheme)
  

  -- Keymap to close and revert
  vim.keymap.set('n', 'q', function()
    vim.api.nvim_win_close(win, true)
    pcall(vim.cmd, 'colorscheme ' .. original)
  end, { buffer = buf })
  

  -- Keymap to accept
  vim.keymap.set('n', '<CR>', function()
    vim.api.nvim_win_close(win, true)
  end, { buffer = buf })
end


-- Preview all available colorschemes
function M.preview_all()
  local schemes = vim.fn.getcompletion('', 'color')
  local current_idx = 1
  
  local buf, win = M.create_preview_window()
  
  local function apply_scheme()
    if current_idx >= 1 and current_idx <= #schemes then
      pcall(vim.cmd, 'colorscheme ' .. schemes[current_idx])
      vim.api.nvim_buf_set_name(buf, 'Preview: ' .. schemes[current_idx])
    end
  end
  
  apply_scheme()
  

  -- Navigation keymaps
  vim.keymap.set('n', 'n', function()
    current_idx = current_idx + 1
    if current_idx > #schemes then current_idx = 1 end
    apply_scheme()
  end, { buffer = buf })
  
  vim.keymap.set('n', 'p', function()
    current_idx = current_idx - 1
    if current_idx < 1 then current_idx = #schemes end
    apply_scheme()
  end, { buffer = buf })
  
  vim.keymap.set('n', 'q', function()
    vim.api.nvim_win_close(win, true)
  end, { buffer = buf })
end

return M

End of Chapter 25: Colorschemes and Themes

You now have comprehensive knowledge of creating, managing, and optimizing colorschemes in Vim and Neovim. This chapter covered fundamental color theory, building complete colorschemes in both VimScript and Lua, dynamic color adjustment techniques, terminal color management, and advanced features like adaptive themes, colorscheme switching, and preview systems.


Chapter #26: Statusline and Tabline

The statusline and tabline are essential UI components in Vim/Neovim that provide visual feedback about your editing session. This chapter covers everything from basic configuration to building custom, feature-rich statuslines and tablines using both Vimscript and Lua.

Understanding the Statusline

The statusline appears at the bottom of each window, displaying information about the current buffer, file status, cursor position, and more.

Basic Statusline Configuration

The simplest way to configure a statusline is using the statusline option:

" Show basic file information
set statusline=%f\ %m%r%h%w

Statusline Format Syntax

Vim uses special % items to display dynamic information:

Item Description
%f Relative file path
%F Full file path
%t Filename (tail) only
%m Modified flag [+]
%r Readonly flag [RO]
%h Help buffer flag
%w Preview window flag
%y Filetype [vim]
%l Current line number
%L Total lines
%c Column number
%p Percentage through file
%= Separation point (left/right alignment)

Building a Custom Statusline

Here’s a more comprehensive statusline configuration:

set statusline=
set statusline+=%#PmenuSel#           " Highlight group
set statusline+=\ %f\                 " Filename
set statusline+=%#LineNr#             " Change highlight
set statusline+=\ %m%r%h%w            " Flags
set statusline+=%=                    " Right align
set statusline+=%#CursorColumn#       " Highlight group
set statusline+=\ %y                  " Filetype
set statusline+=\ %{&fileencoding?&fileencoding:&encoding}
set statusline+=\[%{&fileformat}\]    " File format
set statusline+=\ %p%%                " Percentage
set statusline+=\ %l:%c               " Line:Column
set statusline+=\ 

Conditional Display

Use ternary operators for conditional elements:

" Show modified indicator only when buffer is modified
set statusline+=%{&modified?'[+]':''}

" Show readonly indicator
set statusline+=%{&readonly?'[RO]':''}

" Show paste mode
set statusline+=%{&paste?'[PASTE]':''}

Advanced Statusline with Functions

For complex logic, define Vimscript functions:

function! GitBranch()
  let l:branch = system("git branch --show-current 2>/dev/null | tr -d '\n'")
  return l:branch != '' ? ' ' . l:branch : ''
endfunction

function! LspStatus() abort
  if luaeval('#vim.lsp.buf_get_clients() > 0')
    return ' LSP'
  endif
  return ''
endfunction

function! StatuslineMode()
  let l:mode = mode()
  let l:mode_map = {
    \ 'n': 'NORMAL',
    \ 'i': 'INSERT',
    \ 'v': 'VISUAL',
    \ 'V': 'V-LINE',
    \ "\<C-v>": 'V-BLOCK',
    \ 'c': 'COMMAND',
    \ 'R': 'REPLACE'
    \ }
  return get(l:mode_map, l:mode, l:mode)
endfunction

set statusline=%{StatuslineMode()}\ 
set statusline+=%f\ %m%r%h%w
set statusline+=%{GitBranch()}
set statusline+=%=
set statusline+=%{LspStatus()}
set statusline+=\ %y\ %l:%c\ %p%%

Lua-Based Statusline

Neovim’s Lua API provides more flexibility and better performance:


-- Basic Lua statusline
local function statusline()
  local parts = {}
  

  -- Mode
  local mode_map = {
    n = 'NORMAL',
    i = 'INSERT',
    v = 'VISUAL',
    V = 'V-LINE',
    ['\22'] = 'V-BLOCK',  -- Ctrl-V
    c = 'COMMAND',
    R = 'REPLACE',
  }
  
  local mode = vim.api.nvim_get_mode().mode
  table.insert(parts, mode_map[mode] or mode)
  

  -- Filename with modified flag
  local filename = vim.fn.expand('%:t')
  if filename == '' then filename = '[No Name]' end
  if vim.bo.modified then filename = filename .. ' [+]' end
  table.insert(parts, filename)
  

  -- Separator
  table.insert(parts, '%=')
  

  -- Filetype
  if vim.bo.filetype ~= '' then
    table.insert(parts, vim.bo.filetype)
  end
  

  -- Position
  local line = vim.fn.line('.')
  local col = vim.fn.col('.')
  local total = vim.fn.line('$')
  table.insert(parts, string.format('%d:%d/%d', line, col, total))
  
  return table.concat(parts, ' | ')
end

vim.opt.statusline = '%!v:lua.require("statusline").render()'

Modular Lua Statusline

Create a proper module structure:


-- lua/statusline.lua
local M = {}


-- Module components
M.components = {}

function M.components.mode()
  local mode_map = {
    n = { label = 'NORMAL', hl = 'StatusLineNormal' },
    i = { label = 'INSERT', hl = 'StatusLineInsert' },
    v = { label = 'VISUAL', hl = 'StatusLineVisual' },
    V = { label = 'V-LINE', hl = 'StatusLineVisual' },
    ['\22'] = { label = 'V-BLOCK', hl = 'StatusLineVisual' },
    c = { label = 'COMMAND', hl = 'StatusLineCommand' },
    R = { label = 'REPLACE', hl = 'StatusLineReplace' },
    t = { label = 'TERMINAL', hl = 'StatusLineTerminal' },
  }
  
  local mode = vim.api.nvim_get_mode().mode
  local mode_info = mode_map[mode] or { label = mode, hl = 'StatusLine' }
  
  return string.format('%%#%s# %s %%*', mode_info.hl, mode_info.label)
end

function M.components.filename()
  local filename = vim.fn.expand('%:t')
  if filename == '' then filename = '[No Name]' end
  
  local modified = vim.bo.modified and ' [+]' or ''
  local readonly = vim.bo.readonly and ' [RO]' or ''
  
  return string.format(' %s%s%s ', filename, modified, readonly)
end

function M.components.git_branch()

  -- Using vim.fn.system for git info
  local handle = io.popen('git branch --show-current 2>/dev/null')
  if not handle then return '' end
  
  local branch = handle:read('*a'):gsub('\n', '')
  handle:close()
  
  if branch ~= '' then
    return string.format('  %s ', branch)
  end
  return ''
end

function M.components.lsp_status()
  local clients = vim.lsp.get_active_clients({ bufnr = 0 })
  if #clients > 0 then
    local client_names = {}
    for _, client in ipairs(clients) do
      table.insert(client_names, client.name)
    end
    return string.format(' LSP[%s] ', table.concat(client_names, ','))
  end
  return ''
end

function M.components.diagnostics()
  if not vim.diagnostic then return '' end
  
  local counts = {
    errors = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.ERROR }),
    warnings = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.WARN }),
    info = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.INFO }),
    hints = #vim.diagnostic.get(0, { severity = vim.diagnostic.severity.HINT }),
  }
  
  local parts = {}
  if counts.errors > 0 then
    table.insert(parts, string.format('%%#DiagnosticError# E:%d%%*', counts.errors))
  end
  if counts.warnings > 0 then
    table.insert(parts, string.format('%%#DiagnosticWarn# W:%d%%*', counts.warnings))
  end
  if counts.info > 0 then
    table.insert(parts, string.format('%%#DiagnosticInfo# I:%d%%*', counts.info))
  end
  if counts.hints > 0 then
    table.insert(parts, string.format('%%#DiagnosticHint# H:%d%%*', counts.hints))
  end
  
  if #parts > 0 then
    return ' ' .. table.concat(parts, ' ') .. ' '
  end
  return ''
end

function M.components.filetype()
  local ft = vim.bo.filetype
  if ft ~= '' then
    return string.format(' %s ', ft)
  end
  return ''
end

function M.components.encoding()
  local encoding = vim.opt.fileencoding:get()
  if encoding == '' then
    encoding = vim.opt.encoding:get()
  end
  return string.format(' %s ', encoding)
end

function M.components.position()
  local line = vim.fn.line('.')
  local col = vim.fn.col('.')
  local total = vim.fn.line('$')
  local percent = math.floor((line / total) * 100)
  
  return string.format(' %d:%d %d%%%% ', line, col, percent)
end

function M.render()
  local left = {
    M.components.mode(),
    M.components.filename(),
    M.components.git_branch(),
  }
  
  local right = {
    M.components.diagnostics(),
    M.components.lsp_status(),
    M.components.filetype(),
    M.components.encoding(),
    M.components.position(),
  }
  
  return table.concat(left, '') .. '%=' .. table.concat(right, '')
end


-- Setup function
function M.setup()

  -- Define custom highlight groups
  vim.api.nvim_set_hl(0, 'StatusLineNormal', { bg = '#5e81ac', fg = '#2e3440', bold = true })
  vim.api.nvim_set_hl(0, 'StatusLineInsert', { bg = '#a3be8c', fg = '#2e3440', bold = true })
  vim.api.nvim_set_hl(0, 'StatusLineVisual', { bg = '#b48ead', fg = '#2e3440', bold = true })
  vim.api.nvim_set_hl(0, 'StatusLineCommand', { bg = '#ebcb8b', fg = '#2e3440', bold = true })
  vim.api.nvim_set_hl(0, 'StatusLineReplace', { bg = '#bf616a', fg = '#2e3440', bold = true })
  vim.api.nvim_set_hl(0, 'StatusLineTerminal', { bg = '#88c0d0', fg = '#2e3440', bold = true })
  

  -- Set the statusline
  vim.opt.statusline = '%!v:lua.require("statusline").render()'
  

  -- Refresh on certain events
  local group = vim.api.nvim_create_augroup('StatuslineRefresh', { clear = true })
  vim.api.nvim_create_autocmd({ 'ModeChanged', 'BufEnter', 'DiagnosticChanged' }, {
    group = group,
    callback = function()
      vim.cmd('redrawstatus')
    end,
  })
end

return M

Usage:


-- In your init.lua
require('statusline').setup()

Performance Optimization

Caching Expensive Operations

local M = {}


-- Cache with TTL
M.cache = {}

function M.cached_call(key, fn, ttl)
  ttl = ttl or 1000  -- Default 1 second
  
  local now = vim.loop.now()
  local cached = M.cache[key]
  
  if cached and (now - cached.time) < ttl then
    return cached.value
  end
  
  local value = fn()
  M.cache[key] = { value = value, time = now }
  return value
end


-- Use in components
function M.components.git_branch()
  return M.cached_call('git_branch', function()
    local handle = io.popen('git branch --show-current 2>/dev/null')
    if not handle then return '' end
    local branch = handle:read('*a'):gsub('\n', '')
    handle:close()
    return branch ~= '' and string.format('  %s ', branch) or ''
  end, 5000)  -- Cache for 5 seconds
end

Async Git Status

function M.components.git_branch_async()
  local cached = M.cache.git_branch or ''
  

  -- Update in background
  vim.fn.jobstart('git branch --show-current 2>/dev/null', {
    stdout_buffered = true,
    on_stdout = function(_, data, _)
      if data and data[1] and data[1] ~= '' then
        M.cache.git_branch = string.format('  %s ', data[1])
        vim.cmd('redrawstatus')
      end
    end,
  })
  
  return cached
end

Understanding the Tabline

The tabline appears at the top of the screen, showing open tabs and their windows.

Basic Tabline Configuration

" Enable tabline
set showtabline=2  " Always show tabline

" Simple tabline
set tabline=%!MyTabLine()

function! MyTabLine()
  let s = ''
  for i in range(tabpagenr('$'))
    " Select highlighting
    if i + 1 == tabpagenr()
      let s .= '%#TabLineSel#'
    else
      let s .= '%#TabLine#'
    endif
    
    " Set the tab page number (for mouse clicks)
    let s .= '%' . (i + 1) . 'T'
    
    " The label
    let s .= ' %{MyTabLabel(' . (i + 1) . ')} '
  endfor
  
  " After the last tab fill with TabLineFill and reset tab page nr
  let s .= '%#TabLineFill#%T'
  
  return s
endfunction

function! MyTabLabel(n)
  let buflist = tabpagebuflist(a:n)
  let winnr = tabpagewinnr(a:n)
  let bufname = bufname(buflist[winnr - 1])
  
  " Show buffer name or [No Name]
  if bufname == ''
    return '[No Name]'
  else
    return fnamemodify(bufname, ':t')
  endif
endfunction

Lua-Based Tabline


-- lua/tabline.lua
local M = {}

function M.get_tab_label(tab_nr)
  local buflist = vim.fn.tabpagebuflist(tab_nr)
  local winnr = vim.fn.tabpagewinnr(tab_nr)
  local bufnr = buflist[winnr]
  local bufname = vim.api.nvim_buf_get_name(bufnr)
  
  if bufname == '' then
    return '[No Name]'
  else
    return vim.fn.fnamemodify(bufname, ':t')
  end
end

function M.render()
  local tabs = {}
  local current_tab = vim.fn.tabpagenr()
  local total_tabs = vim.fn.tabpagenr('$')
  
  for i = 1, total_tabs do
    local hl = (i == current_tab) and '%#TabLineSel#' or '%#TabLine#'
    local label = M.get_tab_label(i)
    

    -- Modified indicator
    local buflist = vim.fn.tabpagebuflist(i)
    local has_modified = false
    for _, bufnr in ipairs(buflist) do
      if vim.api.nvim_buf_get_option(bufnr, 'modified') then
        has_modified = true
        break
      end
    end
    local modified = has_modified and ' [+]' or ''
    
    table.insert(tabs, string.format('%s %d: %s%s ', hl, i, label, modified))
  end
  
  return table.concat(tabs, '') .. '%#TabLineFill#'
end

function M.setup()
  vim.opt.showtabline = 2
  vim.opt.tabline = '%!v:lua.require("tabline").render()'
end

return M

Advanced Tabline with Icons


-- lua/tabline.lua (enhanced)
local M = {}


-- Devicons integration
local has_devicons, devicons = pcall(require, 'nvim-web-devicons')

function M.get_icon(filename)
  if not has_devicons then
    return ''
  end
  
  local extension = vim.fn.fnamemodify(filename, ':e')
  local icon, _ = devicons.get_icon(filename, extension, { default = true })
  return icon and (icon .. ' ') or ''
end

function M.get_tab_info(tab_nr)
  local buflist = vim.fn.tabpagebuflist(tab_nr)
  local winnr = vim.fn.tabpagewinnr(tab_nr)
  local bufnr = buflist[winnr]
  local bufname = vim.api.nvim_buf_get_name(bufnr)
  
  local filename = bufname == '' and '[No Name]' or vim.fn.fnamemodify(bufname, ':t')
  local icon = M.get_icon(filename)
  

  -- Check for modified buffers
  local has_modified = false
  for _, buf in ipairs(buflist) do
    if vim.api.nvim_buf_get_option(buf, 'modified') then
      has_modified = true
      break
    end
  end
  
  return {
    filename = filename,
    icon = icon,
    modified = has_modified,
    num_windows = #buflist,
  }
end

function M.render()
  local tabs = {}
  local current_tab = vim.fn.tabpagenr()
  local total_tabs = vim.fn.tabpagenr('$')
  
  for i = 1, total_tabs do
    local hl = (i == current_tab) and '%#TabLineSel#' or '%#TabLine#'
    local info = M.get_tab_info(i)
    
    local modified = info.modified and '● ' or ''
    local windows = info.num_windows > 1 and string.format('[%d] ', info.num_windows) or ''
    
    local label = string.format(
      '%s %d %s%s%s%s ',
      hl,
      i,
      windows,
      info.icon,
      info.filename,
      modified
    )
    

    -- Add click handler
    label = string.format('%%%dT%s', i, label)
    
    table.insert(tabs, label)
  end
  

  -- Add close button
  table.insert(tabs, '%#TabLine# %999X × ')
  
  return table.concat(tabs, '') .. '%#TabLineFill#'
end

function M.setup()

  -- Set up highlight groups
  vim.api.nvim_set_hl(0, 'TabLine', { bg = '#3b4252', fg = '#d8dee9' })
  vim.api.nvim_set_hl(0, 'TabLineSel', { bg = '#5e81ac', fg = '#2e3440', bold = true })
  vim.api.nvim_set_hl(0, 'TabLineFill', { bg = '#2e3440' })
  
  vim.opt.showtabline = 2
  vim.opt.tabline = '%!v:lua.require("tabline").render()'
end

return M

Winbar (Neovim 0.8+)

The winbar is similar to the statusline but appears at the top of each window:


-- lua/winbar.lua
local M = {}

function M.get_location()
  local breadcrumbs = {}
  

  -- Get file path components
  local filepath = vim.fn.expand('%:~:.')
  if filepath ~= '' then
    local parts = vim.split(filepath, '/')
    for i, part in ipairs(parts) do
      if i == #parts then
        table.insert(breadcrumbs, string.format('%%#WinBarFilename#%s%%*', part))
      else
        table.insert(breadcrumbs, part)
      end
    end
  end
  
  return table.concat(breadcrumbs, ' ' .. string.rep('>', 1) .. ' ')
end

function M.get_symbol()

  -- Integration with LSP document symbols (simplified)
  local clients = vim.lsp.get_active_clients({ bufnr = 0 })
  if #clients == 0 then return '' end
  

  -- This would require additional implementation with textDocument/documentSymbol
  return ''
end

function M.render()
  local parts = {}
  
  local location = M.get_location()
  if location ~= '' then
    table.insert(parts, location)
  end
  
  local symbol = M.get_symbol()
  if symbol ~= '' then
    table.insert(parts, symbol)
  end
  
  return table.concat(parts, ' ')
end

function M.setup()
  vim.api.nvim_set_hl(0, 'WinBar', { bg = 'NONE', fg = '#d8dee9' })
  vim.api.nvim_set_hl(0, 'WinBarFilename', { bg = 'NONE', fg = '#88c0d0', bold = true })
  
  vim.opt.winbar = '%!v:lua.require("winbar").render()'
end

return M

Integration with keyd and tmux

Based on the provided manpages, here’s how statusline/tabline integrate with your workflow:

Detecting tmux Sessions

function M.components.tmux_info()
  local tmux_session = vim.env.TMUX
  if tmux_session then

    -- Extract session name from TMUX environment variable
    local session = vim.fn.system("tmux display-message -p '#S'"):gsub('\n', '')
    return string.format(' [tmux:%s] ', session)
  end
  return ''
end

Keyboard Layout Detection (keyd)

Since keyd operates at the system level, you can show the current keyboard state:

function M.components.keyboard_layout()

  -- This would require custom integration with keyd

  -- Example: reading from a state file or socket
  local handle = io.popen('setxkbmap -query 2>/dev/null | grep layout | cut -d: -f2')
  if not handle then return '' end
  
  local layout = handle:read('*a'):gsub('%s+', '')
  handle:close()
  
  if layout ~= '' then
    return string.format(' [%s] ', layout:upper())
  end
  return ''
end

Complete Example Configuration

Here’s a full configuration bringing everything together:


-- lua/ui/init.lua
local M = {}

function M.setup()
  require('ui.statusline').setup()
  require('ui.tabline').setup()
  

  -- Optional: winbar for Neovim 0.8+
  if vim.fn.has('nvim-0.8') == 1 then
    require('ui.winbar').setup()
  end
end

return M

-- In your init.lua
require('ui').setup()

While custom statuslines are powerful, consider these popular alternatives:

lualine.nvim

require('lualine').setup {
  options = {
    theme = 'auto',
    section_separators = { left = '', right = '' },
    component_separators = { left = '', right = '' },
  },
  sections = {
    lualine_a = {'mode'},
    lualine_b = {'branch', 'diff', 'diagnostics'},
    lualine_c = {'filename'},
    lualine_x = {'encoding', 'fileformat', 'filetype'},
    lualine_y = {'progress'},
    lualine_z = {'location'}
  },
}

heirline.nvim

A more flexible, configuration-based approach:

local conditions = require("heirline.conditions")
local utils = require("heirline.utils")

local ViMode = {
  init = function(self)
    self.mode = vim.fn.mode(1)
  end,
  static = {
    mode_names = {
      n = "N",
      i = "I",
      v = "V",
      V = "V-L",
      ["\22"] = "V-B",
      c = "C",
      R = "R",
    },
  },
  provider = function(self)
    return " %2("..self.mode_names[self.mode].."%)"
  end,
  hl = function(self)
    local mode = self.mode:sub(1, 1)
    return { bg = self:mode_color(), bold = true }
  end,
}

require('heirline').setup({
  statusline = { ViMode, ... },
  tabline = { ... },
  winbar = { ... },
})

Testing and Debugging

Debug Statusline Rendering


-- Add debug component
function M.components.debug()
  if vim.g.statusline_debug then
    local time = os.date('%H:%M:%S')
    return string.format(' [%s] ', time)
  end
  return ''
end


-- Toggle debug mode
vim.keymap.set('n', '<leader>sd', function()
  vim.g.statusline_debug = not vim.g.statusline_debug
  vim.cmd('redrawstatus')
end, { desc = 'Toggle statusline debug' })

Performance Profiling

function M.profile()
  local start = vim.loop.hrtime()
  local result = M.render()
  local duration = (vim.loop.hrtime() - start) / 1e6
  
  print(string.format('Statusline render took %.2fms', duration))
  return result
end

Best Practices

  1. Keep it Fast: Statusline renders frequently. Cache expensive operations.

  2. Async Operations: Use vim.loop or vim.fn.jobstart for external commands.

  3. Conditional Features: Only show LSP status when LSP is active.

  4. Highlight Groups: Use semantic highlight groups for consistency with colorschemes.

  5. Mouse Support: Add click handlers with %{number}T and %X.

  6. Truncation: Use %< to mark truncation points for narrow windows.

  7. Window-Local: Consider vim.wo.statusline for window-specific statuslines.

Summary

This chapter covered:

  • Basic statusline/tabline configuration with format strings

  • Building custom statuslines using Vimscript functions

  • Advanced Lua-based implementations with modular components

  • Performance optimization techniques (caching, async)

  • Winbar configuration for Neovim 0.8+

  • Integration with tmux and system tools

  • Popular plugin alternatives

  • Testing and debugging approaches

The statusline and tabline are highly customizable UI elements that can significantly enhance your workflow. Whether you build your own or use a plugin, understanding the underlying mechanisms helps you create an interface that works best for your needs.


Chapter #27: Language Server Protocol (LSP)

The Language Server Protocol (LSP) revolutionizes how editors provide language intelligence features. Instead of each editor implementing language support independently, LSP enables a single language server to work with any LSP-compliant editor. This chapter covers LSP fundamentals, Neovim’s built-in LSP client, configuration, and advanced usage patterns.

Understanding LSP

What is LSP?

LSP is a protocol that standardizes communication between editors and language intelligence tools. It provides:

  • Code completion (IntelliSense)

  • Go to definition/references

  • Hover documentation

  • Diagnostics (errors, warnings)

  • Code actions (quick fixes, refactoring)

  • Formatting

  • Rename refactoring

  • Signature help

Architecture

┌│─────────────┐ ┌──────────────────┐

││ │ JSON-RPC │ │

││ Neovim │◄─────────►│ Language Server │

││ (Client) │ │ (rust-analyzer, │

││ │ │ pyright, etc) │ └─────────────┘ └──────────────────┘

Neovim’s Built-in LSP Client

Neovim 0.5+ includes a native LSP client accessible via vim.lsp.

Basic Setup


-- lua/lsp/init.lua
local M = {}

function M.setup()

  -- Set up LSP keymaps and options when a language server attaches
  vim.api.nvim_create_autocmd('LspAttach', {
    group = vim.api.nvim_create_augroup('UserLspConfig', {}),
    callback = function(ev)

      -- Enable completion triggered by <c-x><c-o>
      vim.bo[ev.buf].omnifunc = 'v:lua.vim.lsp.omnifunc'


      -- Buffer local mappings
      local opts = { buffer = ev.buf }
      
      vim.keymap.set('n', 'gD', vim.lsp.buf.declaration, opts)
      vim.keymap.set('n', 'gd', vim.lsp.buf.definition, opts)
      vim.keymap.set('n', 'K', vim.lsp.buf.hover, opts)
      vim.keymap.set('n', 'gi', vim.lsp.buf.implementation, opts)
      vim.keymap.set('n', '<C-k>', vim.lsp.buf.signature_help, opts)
      vim.keymap.set('n', '<leader>wa', vim.lsp.buf.add_workspace_folder, opts)
      vim.keymap.set('n', '<leader>wr', vim.lsp.buf.remove_workspace_folder, opts)
      vim.keymap.set('n', '<leader>wl', function()
        print(vim.inspect(vim.lsp.buf.list_workspace_folders()))
      end, opts)
      vim.keymap.set('n', '<leader>D', vim.lsp.buf.type_definition, opts)
      vim.keymap.set('n', '<leader>rn', vim.lsp.buf.rename, opts)
      vim.keymap.set({ 'n', 'v' }, '<leader>ca', vim.lsp.buf.code_action, opts)
      vim.keymap.set('n', 'gr', vim.lsp.buf.references, opts)
      vim.keymap.set('n', '<leader>f', function()
        vim.lsp.buf.format { async = true }
      end, opts)
    end,
  })
end

return M

Configuring Language Servers

Using nvim-lspconfig

The nvim-lspconfig plugin provides pre-configured settings for popular language servers:


-- Install nvim-lspconfig first

-- lua/lsp/servers.lua
local M = {}

function M.setup()
  local lspconfig = require('lspconfig')
  

  -- Default capabilities (for nvim-cmp integration)
  local capabilities = require('cmp_nvim_lsp').default_capabilities()
  

  -- Python (pyright)
  lspconfig.pyright.setup {
    capabilities = capabilities,
    settings = {
      python = {
        analysis = {
          typeCheckingMode = "basic",
          autoSearchPaths = true,
          useLibraryCodeForTypes = true,
        }
      }
    }
  }
  

  -- Lua (lua_ls)
  lspconfig.lua_ls.setup {
    capabilities = capabilities,
    settings = {
      Lua = {
        runtime = {
          version = 'LuaJIT',
        },
        diagnostics = {
          globals = { 'vim' },
        },
        workspace = {
          library = vim.api.nvim_get_runtime_file("", true),
          checkThirdParty = false,
        },
        telemetry = {
          enable = false,
        },
      },
    },
  }
  

  -- TypeScript/JavaScript (tsserver)
  lspconfig.tsserver.setup {
    capabilities = capabilities,
    on_attach = function(client, bufnr)

      -- Disable tsserver formatting (use prettier instead)
      client.server_capabilities.documentFormattingProvider = false
    end,
  }
  

  -- Rust (rust_analyzer)
  lspconfig.rust_analyzer.setup {
    capabilities = capabilities,
    settings = {
      ['rust-analyzer'] = {
        checkOnSave = {
          command = "clippy"
        },
        cargo = {
          allFeatures = true,
        },
      }
    }
  }
  

  -- Go (gopls)
  lspconfig.gopls.setup {
    capabilities = capabilities,
    settings = {
      gopls = {
        analyses = {
          unusedparams = true,
        },
        staticcheck = true,
      },
    },
  }
  

  -- C/C++ (clangd)
  lspconfig.clangd.setup {
    capabilities = capabilities,
    cmd = {
      "clangd",
      "--background-index",
      "--clang-tidy",
      "--header-insertion=iwyu",
      "--completion-style=detailed",
      "--function-arg-placeholders",
    },
  }
  

  -- JSON (jsonls)
  lspconfig.jsonls.setup {
    capabilities = capabilities,
    settings = {
      json = {
        schemas = require('schemastore').json.schemas(),
        validate = { enable = true },
      },
    },
  }
end

return M

Manual Server Configuration

Without nvim-lspconfig:


-- Start a language server manually
vim.lsp.start({
  name = 'my-server',
  cmd = { 'path/to/language-server' },
  root_dir = vim.fs.dirname(vim.fs.find({'package.json', '.git'}, { upward = true })[1]),
  capabilities = capabilities,
  settings = {

    -- Server-specific settings
  },
})

LSP Handlers and Customization

Customizing Hover Windows


-- Custom hover handler
vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(
  vim.lsp.handlers.hover, {
    border = "rounded",
    max_width = 80,
    max_height = 20,
  }
)


-- Or create completely custom handler
vim.lsp.handlers["textDocument/hover"] = function(_, result, ctx, config)
  config = config or {}
  config.focus_id = ctx.method
  
  if not (result and result.contents) then
    return
  end
  
  local markdown_lines = vim.lsp.util.convert_input_to_markdown_lines(result.contents)
  markdown_lines = vim.lsp.util.trim_empty_lines(markdown_lines)
  
  if vim.tbl_isempty(markdown_lines) then
    return
  end
  
  return vim.lsp.util.open_floating_preview(markdown_lines, "markdown", config)
end

Signature Help Configuration

vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(
  vim.lsp.handlers.signature_help, {
    border = "rounded",
    close_events = { "CursorMoved", "BufHidden", "InsertCharPre" },
  }
)

Diagnostics Configuration


-- Global diagnostic configuration
vim.diagnostic.config({
  virtual_text = {
    prefix = '●',
    source = "if_many",
  },
  signs = true,
  underline = true,
  update_in_insert = false,
  severity_sort = true,
  float = {
    border = 'rounded',
    source = 'always',
    header = '',
    prefix = '',
  },
})


-- Custom diagnostic signs
local signs = {
  Error = " ",
  Warn = " ",
  Hint = " ",
  Info = " "
}

for type, icon in pairs(signs) do
  local hl = "DiagnosticSign" .. type
  vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
end

Advanced LSP Features

Code Actions


-- Enhanced code action picker
local function code_action_menu()
  local context = { diagnostics = vim.lsp.diagnostic.get_line_diagnostics() }
  local params = vim.lsp.util.make_range_params()
  params.context = context
  
  vim.lsp.buf_request(0, 'textDocument/codeAction', params, function(err, result, ctx, config)
    if err or not result or vim.tbl_isempty(result) then
      print("No code actions available")
      return
    end
    
    local items = {}
    for i, action in ipairs(result) do
      local title = action.title:gsub('\r\n', '\\r\\n'):gsub('\n', '\\n')
      table.insert(items, string.format("%d. %s", i, title))
    end
    
    vim.ui.select(items, {
      prompt = 'Code Actions:',
    }, function(choice, idx)
      if not idx then return end
      
      local action = result[idx]
      if action.edit then
        vim.lsp.util.apply_workspace_edit(action.edit, 'utf-8')
      end
      if action.command then
        vim.lsp.buf.execute_command(action.command)
      end
    end)
  end)
end

vim.keymap.set('n', '<leader>ca', code_action_menu, { desc = 'Code actions' })

Workspace Symbols


-- Search workspace symbols with Telescope integration
local function workspace_symbols()
  require('telescope.builtin').lsp_dynamic_workspace_symbols({
    fname_width = 50,
    symbol_width = 40,
  })
end

vim.keymap.set('n', '<leader>ws', workspace_symbols, { desc = 'Workspace symbols' })

Document Symbols Outline


-- Simple document symbols outline
local function document_symbols()
  vim.lsp.buf.document_symbol()
  

  -- Or use a picker
  require('telescope.builtin').lsp_document_symbols({
    symbols = {
      "Class",
      "Function",
      "Method",
      "Constructor",
      "Interface",
      "Module",
      "Struct",
      "Trait",
    }
  })
end

vim.keymap.set('n', '<leader>ds', document_symbols, { desc = 'Document symbols' })

Inlay Hints (Neovim 0.10+)


-- Enable inlay hints
if vim.lsp.inlay_hint then
  vim.api.nvim_create_autocmd('LspAttach', {
    callback = function(args)
      local client = vim.lsp.get_client_by_id(args.data.client_id)
      if client.server_capabilities.inlayHintProvider then
        vim.lsp.inlay_hint.enable(args.buf, true)
      end
    end,
  })
  

  -- Toggle inlay hints
  vim.keymap.set('n', '<leader>th', function()
    vim.lsp.inlay_hint.enable(0, not vim.lsp.inlay_hint.is_enabled(0))
  end, { desc = 'Toggle inlay hints' })
end

LSP Progress Indication

Using fidget.nvim

require('fidget').setup {
  progress = {
    display = {
      render_limit = 16,
      done_ttl = 3,
      progress_icon = { pattern = "dots", period = 1 },
    },
  },
  notification = {
    window = {
      winblend = 0,
    },
  },
}

Custom Progress Handler

local progress_handler = function()
  local client_notifs = {}
  
  return function(err, result, ctx, config)
    local client_id = ctx.client_id
    local val = result.value
    
    if not val.kind then
      return
    end
    
    local notif_data = client_notifs[client_id]
    
    if val.kind == 'begin' then
      local message = string.format("%s: %s", result.token, val.title)
      notif_data = {
        notification = vim.notify(message, vim.log.levels.INFO, {
          title = 'LSP Progress',
          icon = '󰔟',
          timeout = false,
          hide_from_history = false,
        }),
        spinner = 1,
      }
      client_notifs[client_id] = notif_data
      
    elseif val.kind == 'report' and notif_data then
      local message = string.format(
        "%s: %s - %s",
        result.token,
        val.title or '',
        val.message or ''
      )
      notif_data.notification = vim.notify(message, vim.log.levels.INFO, {
        replace = notif_data.notification,
        hide_from_history = false,
      })
      
    elseif val.kind == 'end' and notif_data then
      local message = string.format("%s: Complete", result.token)
      notif_data.notification = vim.notify(message, vim.log.levels.INFO, {
        replace = notif_data.notification,
        timeout = 3000,
      })
      client_notifs[client_id] = nil
    end
  end
end

vim.lsp.handlers['$/progress'] = progress_handler()

Formatting and Linting

Format on Save


-- Format on save with timeout
local format_on_save = vim.api.nvim_create_augroup('FormatOnSave', { clear = true })

vim.api.nvim_create_autocmd('BufWritePre', {
  group = format_on_save,
  callback = function()
    vim.lsp.buf.format({ timeout_ms = 2000 })
  end,
})

Conditional Formatting


-- Format only specific file types
vim.api.nvim_create_autocmd('BufWritePre', {
  group = format_on_save,
  pattern = { '*.lua', '*.py', '*.rs', '*.go' },
  callback = function()
    vim.lsp.buf.format({ timeout_ms = 2000 })
  end,
})

Range Formatting


-- Format selection in visual mode
vim.keymap.set('v', '<leader>f', function()
  vim.lsp.buf.format({ async = true })
end, { desc = 'Format selection' })

Multiple Formatters


-- Choose specific formatter
local function format_with_client(client_name)
  return function()
    vim.lsp.buf.format({
      filter = function(client)
        return client.name == client_name
      end,
      timeout_ms = 2000,
    })
  end
end

vim.keymap.set('n', '<leader>fp', format_with_client('null-ls'), { desc = 'Format with null-ls' })
vim.keymap.set('n', '<leader>ft', format_with_client('tsserver'), { desc = 'Format with tsserver' })

Diagnostics Management


-- Jump to diagnostics
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, { desc = 'Previous diagnostic' })
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, { desc = 'Next diagnostic' })


-- Jump to errors only
vim.keymap.set('n', '[e', function()
  vim.diagnostic.goto_prev({ severity = vim.diagnostic.severity.ERROR })
end, { desc = 'Previous error' })

vim.keymap.set('n', ']e', function()
  vim.diagnostic.goto_next({ severity = vim.diagnostic.severity.ERROR })
end, { desc = 'Next error' })

Show Diagnostics


-- Show diagnostics in floating window
vim.keymap.set('n', '<leader>d', vim.diagnostic.open_float, { desc = 'Show diagnostics' })


-- Show all diagnostics in location list
vim.keymap.set('n', '<leader>q', vim.diagnostic.setloclist, { desc = 'Diagnostics to loclist' })


-- Show all diagnostics in quickfix
vim.keymap.set('n', '<leader>Q', vim.diagnostic.setqflist, { desc = 'Diagnostics to qflist' })

Custom Diagnostic Display


-- Custom diagnostic virtual text
vim.diagnostic.config({
  virtual_text = {
    format = function(diagnostic)
      if diagnostic.severity == vim.diagnostic.severity.ERROR then
        return string.format("❌ %s", diagnostic.message)
      elseif diagnostic.severity == vim.diagnostic.severity.WARN then
        return string.format("⚠️  %s", diagnostic.message)
      else
        return diagnostic.message
      end
    end,
  },
})

LSP Integration with Completion

nvim-cmp Setup

local cmp = require('cmp')
local luasnip = require('luasnip')

cmp.setup({
```lua
local cmp = require('cmp')
local luasnip = require('luasnip')

cmp.setup({
  snippet = {
    expand = function(args)
      luasnip.lsp_expand(args.body)
    end,
  },
  mapping = cmp.mapping.preset.insert({
    ['<C-b>'] = cmp.mapping.scroll_docs(-4),
    ['<C-f>'] = cmp.mapping.scroll_docs(4),
    ['<C-Space>'] = cmp.mapping.complete(),
    ['<C-e>'] = cmp.mapping.abort(),
    ['<CR>'] = cmp.mapping.confirm({ select = true }),
    ['<Tab>'] = cmp.mapping(function(fallback)
      if cmp.visible() then
        cmp.select_next_item()
      elseif luasnip.expand_or_jumpable() then
        luasnip.expand_or_jump()
      else
        fallback()
      end
    end, { 'i', 's' }),
    ['<S-Tab>'] = cmp.mapping(function(fallback)
      if cmp.visible() then
        cmp.select_prev_item()
      elseif luasnip.jumpable(-1) then
        luasnip.jump(-1)
      else
        fallback()
      end
    end, { 'i', 's' }),
  }),
  sources = cmp.config.sources({
    { name = 'nvim_lsp' },
    { name = 'luasnip' },
    { name = 'buffer' },
    { name = 'path' },
  }),
  window = {
    completion = cmp.config.window.bordered(),
    documentation = cmp.config.window.bordered(),
  },
  formatting = {
    format = require('lspkind').cmp_format({
      mode = 'symbol_text',
      maxwidth = 50,
      ellipsis_char = '...',
    }),
  },
})


-- Enable LSP capabilities
local capabilities = require('cmp_nvim_lsp').default_capabilities()
require('lspconfig').pyright.setup { capabilities = capabilities }
require('lspconfig').tsserver.setup { capabilities = capabilities }

Summary: Chapter #27 – Language Server Protocol (LSP)

This chapter covered:

  1. Concepts – The JSON-RPC-based architecture of the Language Server Protocol.

  2. Neovim Integration – How Neovim’s built-in LSP client provides advanced language features.

  3. Setup & Configuration – Using nvim-lspconfig for language servers (Python, Lua, C++, Rust, etc.).

  4. Customization – Modifying handlers for hover, diagnostics, and signature help.

  5. Advanced Features – Code actions, workspace/document symbols, inlay hints, async progress.

  6. Formatting and Diagnostics – Custom formatting logic, error navigation, and display configuration.

  7. Completions – Integration with nvim-cmp and snippet engines for smooth development workflows.

The next chapter will extend this foundation into Chapter #28: Treesitter and Semantic Syntax Analysis, covering syntax trees, incremental parsing, and plugin-based syntax enhancement.


Chapter #28: Tree-sitter in Neovim

Tree-sitter is a parsing system that builds and maintains syntax trees for source code. Unlike traditional regex-based syntax highlighting, Tree-sitter provides incremental parsing, accurate syntax understanding, and enables powerful text manipulation based on the code’s abstract syntax tree (AST).

Understanding Tree-sitter

What is Tree-sitter?

Tree-sitter is:

  • A parser generator that creates fast, incremental parsers

  • Error-resistant – continues parsing even with syntax errors

  • Incremental – only re-parses changed sections

  • Language-agnostic – supports 40+ programming languages

Architecture

┌│──────────────┐

││ Source Code │ └──────┬───────┘ │ ▼

┌│──────────────────┐

││ Tree-sitter │

││ Parser │ └──────┬───────────┘ │ ▼

┌│──────────────────┐ ┌─────────────────┐

││ Syntax Tree │───►│ Highlighting │

││ (AST) │ │ Indentation │

││ │ │ Text Objects │

││ │ │ Code Folding │ └──────────────────┘ └─────────────────┘

Advantages Over Regex

Feature Regex Tree-sitter
Accuracy Limited High
Context-aware No Yes
Performance Good Excellent
Incremental No Yes
Error recovery Poor Good

Setting Up Tree-sitter

Basic Installation


-- Using lazy.nvim
{
  'nvim-treesitter/nvim-treesitter',
  build = ':TSUpdate',
  config = function()
    require('nvim-treesitter.configs').setup({

      -- Install parsers synchronously (only applied to `ensure_installed`)
      sync_install = false,
      

      -- Automatically install missing parsers when entering buffer
      auto_install = true,
      

      -- List of parsers to install
      ensure_installed = {
        'lua',
        'vim',
        'vimdoc',
        'python',
        'javascript',
        'typescript',
        'rust',
        'go',
        'c',
        'cpp',
        'html',
        'css',
        'json',
        'yaml',
        'markdown',
        'markdown_inline',
        'bash',
      },
      

      -- Highlighting module
      highlight = {
        enable = true,
        

        -- Disable vim's regex highlighting
        additional_vim_regex_highlighting = false,
      },
    })
  end,
}

Manual Parser Installation

" Install a specific parser
:TSInstall python

" Update all installed parsers
:TSUpdate

" Check parser status
:TSInstallInfo

" Uninstall a parser
:TSUninstall python

Syntax Highlighting

Enable Highlighting

require('nvim-treesitter.configs').setup({
  highlight = {
    enable = true,
    

    -- Disable for large files
    disable = function(lang, buf)
      local max_filesize = 100 * 1024 -- 100 KB
      local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(buf))
      if ok and stats and stats.size > max_filesize then
        return true
      end
    end,
    

    -- Use both Tree-sitter and vim regex (not recommended)
    additional_vim_regex_highlighting = false,
  },
})

Custom Highlights


-- Define custom highlight groups
vim.api.nvim_set_hl(0, '@variable', { fg = '#e0def4' })
vim.api.nvim_set_hl(0, '@function', { fg = '#9ccfd8', bold = true })
vim.api.nvim_set_hl(0, '@keyword', { fg = '#31748f', italic = true })
vim.api.nvim_set_hl(0, '@string', { fg = '#f6c177' })
vim.api.nvim_set_hl(0, '@comment', { fg = '#6e6a86', italic = true })
vim.api.nvim_set_hl(0, '@type', { fg = '#9ccfd8' })
vim.api.nvim_set_hl(0, '@constant', { fg = '#eb6f92' })
vim.api.nvim_set_hl(0, '@parameter', { fg = '#e0def4', italic = true })


-- Language-specific overrides
vim.api.nvim_set_hl(0, '@function.python', { fg = '#ebbcba', bold = true })
vim.api.nvim_set_hl(0, '@keyword.return', { fg = '#eb6f92', bold = true })

Highlighting Queries

Tree-sitter uses queries to determine highlighting. Here’s a basic query structure:

; highlights.scm for a language
(function_definition
  name: (identifier) @function)

(function_call
  function: (identifier) @function.call)

(string) @string

(comment) @comment

(number) @number

(
  (identifier) @constant
  (#match? @constant "^[A-Z_]+$")
)

Incremental Selection

Configuration

require('nvim-treesitter.configs').setup({
  incremental_selection = {
    enable = true,
    keymaps = {
      init_selection = '<CR>',
      node_incremental = '<CR>',
      scope_incremental = '<TAB>',
      node_decremental = '<S-TAB>',
    },
  },
})

Usage Example

With cursor on a variable:

  1. <CR> – Select the identifier

  2. <CR> – Expand to the expression

  3. <CR> – Expand to the statement

  4. <S-TAB> – Shrink selection back

# Start here ▼
result = calculate(x + y)

# <CR> once: selects "calculate"
# <CR> twice: selects "calculate(x + y)"
# <CR> three times: selects entire line

Indentation

Enable Tree-sitter Indentation

require('nvim-treesitter.configs').setup({
  indent = {
    enable = true,
    

    -- Disable for specific languages
    disable = { 'python', 'yaml' },
  },
})

Override Indentation Logic


-- Custom indent expression
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'lua',
  callback = function()
    vim.bo.indentexpr = 'v:lua.require("nvim-treesitter.indent").get_indent()'
  end,
})

Text Objects

Configuration

require('nvim-treesitter.configs').setup({
  textobjects = {
    select = {
      enable = true,
      lookahead = true,
      keymaps = {

        -- Functions
        ['af'] = '@function.outer',
        ['if'] = '@function.inner',
        

        -- Classes
        ['ac'] = '@class.outer',
        ['ic'] = '@class.inner',
        

        -- Conditionals
        ['ai'] = '@conditional.outer',
        ['ii'] = '@conditional.inner',
        

        -- Loops
        ['al'] = '@loop.outer',
        ['il'] = '@loop.inner',
        

        -- Parameters/arguments
        ['aa'] = '@parameter.outer',
        ['ia'] = '@parameter.inner',
        

        -- Comments
        ['a/'] = '@comment.outer',
      },
    },
    

    -- Swap text objects
    swap = {
      enable = true,
      swap_next = {
        ['<leader>sn'] = '@parameter.inner',
      },
      swap_previous = {
        ['<leader>sp'] = '@parameter.inner',
      },
    },
    

    -- Move to next/previous text object
    move = {
      enable = true,
      set_jumps = true,
      goto_next_start = {
        [']m'] = '@function.outer',
        [']]'] = '@class.outer',
      },
      goto_next_end = {
        [']M'] = '@function.outer',
        [']['] = '@class.outer',
      },
      goto_previous_start = {
        ['[m'] = '@function.outer',
        ['[['] = '@class.outer',
      },
      goto_previous_end = {
        ['[M'] = '@function.outer',
        ['[]'] = '@class.outer',
      },
    },
    

    -- LSP interop
    lsp_interop = {
      enable = true,
      border = 'rounded',
      peek_definition_code = {
        ['<leader>df'] = '@function.outer',
        ['<leader>dF'] = '@class.outer',
      },
    },
  },
})

Usage Examples


-- Select outer function: vaf

-- Select inner function: vif

-- Delete a function: daf

-- Change inner class: cic

-- Yank a conditional: yai


-- Jump to next function: ]m

-- Jump to previous class: [[


-- Swap parameters: <leader>sn (cursor on parameter)

Code Folding

Enable Tree-sitter Folding


-- Set fold method to use expressions
vim.opt.foldmethod = 'expr'
vim.opt.foldexpr = 'nvim_treesitter#foldexpr()'


-- Don't fold by default
vim.opt.foldenable = false


-- Set fold level
vim.opt.foldlevel = 99


-- Minimum lines for a fold
vim.opt.foldminlines = 3

Custom Fold Text


-- Better fold display
vim.opt.foldtext = [[substitute(getline(v:foldstart),'\\t',repeat('\ ',&tabstop),'g').'...'.trim(getline(v:foldend)) ]]


-- Or use a Lua function
vim.opt.foldtext = 'v:lua.CustomFoldText()'

function _G.CustomFoldText()
  local line = vim.fn.getline(vim.v.foldstart)
  local line_count = vim.v.foldend - vim.v.foldstart + 1
  return line .. ' ... (' .. line_count .. ' lines)'
end

Fold Configuration

require('nvim-treesitter.configs').setup({
  fold = {
    enable = true,
    disable = {},
  },
})

Advanced Queries

Understanding Queries

Tree-sitter queries use S-expressions to match syntax nodes:

; Basic pattern
(function_definition) @function

; With fields
(function_definition
  name: (identifier) @function.name
  parameters: (parameters) @function.params)

; With predicates
(
  (identifier) @constant
  (#match? @constant "^[A-Z_][A-Z0-9_]*$")
)

; Negation
(
  (identifier) @variable
  (#not-match? @variable "^[A-Z]")
)

Custom Queries


-- Define custom queries in after/queries/{lang}/{query_type}.scm

-- Example: after/queries/python/highlights.scm


-- Or programmatically:
local query = vim.treesitter.query

local python_query = [[
  (call
    function: (attribute
      object: (identifier) @module
      attribute: (identifier) @function)
    (#eq? @module "os")
    (#eq? @function "path"))
]]

local parsed_query = query.parse('python', python_query)

Running Queries

local function find_os_path_calls()
  local bufnr = vim.api.nvim_get_current_buf()
  local parser = vim.treesitter.get_parser(bufnr, 'python')
  local tree = parser:parse()[1]
  local root = tree:root()
  
  local query_str = [[
    (call
      function: (attribute
        object: (identifier) @obj
        attribute: (identifier) @func))
  ]]
  
  local query = vim.treesitter.query.parse('python', query_str)
  
  for id, node in query:iter_captures(root, bufnr) do
    local name = query.captures[id]
    local text = vim.treesitter.get_node_text(node, bufnr)
    print(name, text)
  end
end

Tree-sitter Playground

Installation and Setup

{
  'nvim-treesitter/playground',
  dependencies = { 'nvim-treesitter/nvim-treesitter' },
  config = function()
    require('nvim-treesitter.configs').setup({
      playground = {
        enable = true,
        updatetime = 25,
        persist_queries = false,
        keybindings = {
          toggle_query_editor = 'o',
          toggle_hl_groups = 'i',
          toggle_injected_languages = 't',
          toggle_anonymous_nodes = 'a',
          toggle_language_display = 'I',
          focus_language = 'f',
          unfocus_language = 'F',
          update = 'R',
          goto_node = '<cr>',
          show_help = '?',
        },
      },
    })
  end,
}

Using Playground

" Open playground
:TSPlaygroundToggle

" Show syntax tree
:TSHighlightCapturesUnderCursor

" Edit queries
:EditQuery highlights

The playground shows:

  • Syntax tree of current buffer

  • Highlight captures under cursor

  • Query editor for testing patterns

Context-Aware Features

Show Current Context

{
  'nvim-treesitter/nvim-treesitter-context',
  config = function()
    require('treesitter-context').setup({
      enable = true,
      max_lines = 3,
      min_window_height = 20,
      line_numbers = true,
      multiline_threshold = 1,
      trim_scope = 'outer',
      mode = 'cursor',
      separator = nil,
      zindex = 20,
    })
  end,
}

This shows function/class headers at the top when scrolling:


-- When scrolled down inside a function:

┌│────────────────────────────────┐

││ function calculate(x, y)       │ ← Context line

├│────────────────────────────────┤

││   if x > 10 then

││     return x * y

││   end                          │ ← Current view
└────────────────────────────────┘

Sticky Context Toggle

vim.keymap.set('n', '<leader>tc', function()
  require('treesitter-context').toggle()
end, { desc = 'Toggle treesitter context' })

Refactoring with Tree-sitter

{
  'ThePrimeagen/refactoring.nvim',
  dependencies = {
    'nvim-lua/plenary.nvim',
    'nvim-treesitter/nvim-treesitter',
  },
  config = function()
    require('refactoring').setup({})
    

    -- Extract function
    vim.keymap.set('x', '<leader>re', function()
      require('refactoring').refactor('Extract Function')
    end, { desc = 'Extract function' })
    

    -- Extract variable
    vim.keymap.set('x', '<leader>rv', function()
      require('refactoring').refactor('Extract Variable')
    end, { desc = 'Extract variable' })
    

    -- Inline variable
    vim.keymap.set('n', '<leader>ri', function()
      require('refactoring').refactor('Inline Variable')
    end, { desc = 'Inline variable' })
  end,
}

Performance Optimization

Lazy Loading Parsers

require('nvim-treesitter.configs').setup({
  highlight = {
    enable = true,
    disable = function(lang, buf)

      -- Disable for very large files
      local max_filesize = 200 * 1024 -- 200 KB
      local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(buf))
      if ok and stats and stats.size > max_filesize then
        vim.notify(
          'Tree-sitter disabled: file too large',
          vim.log.levels.WARN
        )
        return true
      end
      

      -- Disable for specific patterns
      local disabled_patterns = { '%.min%.js$', '%.min%.css$' }
      local filename = vim.api.nvim_buf_get_name(buf)
      for _, pattern in ipairs(disabled_patterns) do
        if filename:match(pattern) then
          return true
        end
      end
    end,
  },
})

Async Parsing


-- Configure parser install behavior
require('nvim-treesitter.configs').setup({
  sync_install = false,  -- Install parsers asynchronously
  auto_install = true,   -- Auto-install missing parsers
})

Language-Specific Configurations

Injected Languages

require('nvim-treesitter.configs').setup({
  highlight = {
    enable = true,
  },
  

  -- Enable for markdown code blocks, Vue templates, etc.
  ensure_installed = {
    'markdown',
    'markdown_inline',
    'html',
    'javascript',
    'typescript',
    'vue',
  },
})

Custom Language Parsers


-- Register a custom parser
local parser_config = require('nvim-treesitter.parsers').get_parser_configs()

parser_config.mylang = {
  install_info = {
    url = '~/projects/tree-sitter-mylang',
    files = { 'src/parser.c' },
    branch = 'main',
  },
  filetype = 'mylang',
}


-- Then install

-- :TSInstall mylang

Debugging Tree-sitter

Inspect Syntax Tree

function _G.inspect_tree()
  local bufnr = vim.api.nvim_get_current_buf()
  local parser = vim.treesitter.get_parser(bufnr)
  local tree = parser:parse()[1]
  local root = tree:root()
  
  print('Root node type:', root:type())
  print('Start:', root:start())
  print('End:', root:end_())
  print('Text:', vim.treesitter.get_node_text(root, bufnr))
  

  -- Walk the tree
  local function walk(node, level)
    level = level or 0
    local indent = string.rep('  ', level)
    print(indent .. node:type())
    
    for child in node:iter_children() do
      walk(child, level + 1)
    end
  end
  
  walk(root)
end

vim.keymap.set('n', '<leader>ti', _G.inspect_tree, { desc = 'Inspect syntax tree' })

Get Node Under Cursor

function _G.get_node_at_cursor()
  local bufnr = vim.api.nvim_get_current_buf()
  local cursor = vim.api.nvim_win_get_cursor(0)
  local row, col = cursor[1] - 1, cursor[2]
  
  local parser = vim.treesitter.get_parser(bufnr)
  local tree = parser:parse()[1]
  local root = tree:root()
  
  local node = root:descendant_for_range(row, col, row, col)
  
  if node then
    print('Node type:', node:type())
    print('Text:', vim.treesitter.get_node_text(node, bufnr))
    print('Range:', node:range())
  end
end

vim.keymap.set('n', '<leader>tn', _G.get_node_at_cursor, { desc = 'Get node at cursor' })

Check Parser Status

function _G.check_parser()
  local bufnr = vim.api.nvim_get_current_buf()
  local ft = vim.bo[bufnr].filetype
  
  local parser = vim.treesitter.get_parser(bufnr, ft)
  
  if parser then
    print('Parser found for:', ft)
    print('Language:', parser:lang())
    
    local has_parser = pcall(vim.treesitter.get_query, ft, 'highlights')
    print('Has highlight queries:', has_parser)
  else
    print('No parser for:', ft)
  end
end

Integration with LSP

Combined Highlighting


-- Use both Tree-sitter and LSP semantic tokens
vim.api.nvim_create_autocmd('LspAttach', {
  callback = function(args)
    local client = vim.lsp.get_client_by_id(args.data.client_id)
    
    if client.server_capabilities.semanticTokensProvider then

      -- Enable semantic highlighting
      vim.lsp.semantic_tokens.start(args.buf, client.id)
    end
  end,
})


-- Ensure Tree-sitter highlights have higher priority
vim.highlight.priorities.semantic_tokens = 95
vim.highlight.priorities.treesitter = 100

Use Tree-sitter for LSP Features


-- Example: Use Tree-sitter for better code action context
local function get_function_node()
  local node = vim.treesitter.get_node()
  
  while node do
    if node:type() == 'function_definition' or 
       node:type() == 'function_declaration' then
      return node
    end
    node = node:parent()
  end
end

Best Practices

1. Selective Enabling


-- Disable for problematic file types
require('nvim-treesitter.configs').setup({
  highlight = {
    enable = true,
    disable = { 'latex', 'vim' },  -- Use regex for these
  },
})

2. Performance Monitoring


-- Check parsing time
function _G.benchmark_parsing()
  local start = vim.loop.hrtime()
  local parser = vim.treesitter.get_parser()
  parser:parse()
  local duration = (vim.loop.hrtime() - start) / 1e6
  print(string.format('Parsing took %.2f ms', duration))
end

3. Fallback to Regex


-- Use additional regex highlighting for edge cases
require('nvim-treesitter.configs').setup({
  highlight = {
    enable = true,
    additional_vim_regex_highlighting = { 'markdown' },
  },
})

4. Query Validation


-- Validate custom queries
function _G.validate_query(lang, query_type)
  local ok, query = pcall(vim.treesitter.query.get, lang, query_type)
  if ok then
    print('Query valid for', lang, query_type)
  else
    print('Query error:', query)
  end
end

Summary: Chapter #28 – Tree-sitter in Neovim

This chapter covered:

  1. Fundamentals – Understanding Tree-sitter’s incremental parsing and AST generation

  2. Setup – Installing parsers and configuring nvim-treesitter

  3. Syntax Highlighting – Accurate, context-aware highlighting using queries

  4. Text Objects – Powerful text manipulation based on syntax understanding

  5. Incremental Selection – Expanding/shrinking selections along the syntax tree

  6. Indentation & Folding – Tree-sitter-powered code structure features

  7. Advanced Queries – Writing and testing custom Tree-sitter queries

  8. Debugging Tools – Playground, inspection functions, and parser diagnostics

  9. Integration – Combining Tree-sitter with LSP for enhanced editing

  10. Performance – Optimization strategies and selective enabling

The next chapter will explore Chapter #29: Testing and Debugging Neovim Configuration, covering techniques to validate your configuration, debug issues, and profile performance.


k(n) - Change the access position for an open channel self


Chapter #30: Python Integration

Neovim has excellent Python integration through its built-in RPC API and the pynvim package. This allows you to write plugins, create custom commands, and extend Neovim’s functionality using Python.

Prerequisites and Setup

Installing pynvim

# Using pip
pip install pynvim

# Using pip3 (recommended)
pip3 install pynvim

# In a virtual environment
python3 -m venv ~/.nvim-venv
source ~/.nvim-venv/bin/activate
pip install pynvim

# System-wide with user installation
pip3 install --user pynvim

Configuring Python Provider


-- In your init.lua


-- Point to Python 3 executable
vim.g.python3_host_prog = '/usr/bin/python3'


-- Or use virtual environment
vim.g.python3_host_prog = vim.fn.expand('~/.nvim-venv/bin/python')


-- Disable Python 2 provider (deprecated)
vim.g.loaded_python_provider = 0


-- Check Python provider health

-- :checkhealth provider

Verify Installation

" Check Python 3 support
:echo has('python3')
" Should return 1

" Check pynvim version
:py3 import pynvim; print(pynvim.__version__)

" Provider info
:checkhealth provider

Python3 Commands in Neovim

Executing Python Code

" Execute single Python statement
:py3 print("Hello from Neovim")

" Execute multiple lines
:py3 << EOF
import vim
current_buffer = vim.current.buffer
print(f"Current buffer has {len(current_buffer)} lines")
EOF

" Execute Python file
:py3file ~/scripts/my_script.py

Lua Integration


-- Execute Python from Lua
vim.cmd([[
  py3 << EOF
import vim
vim.command('echo "Python via Lua"')
EOF
]])


-- Create command that runs Python
vim.api.nvim_create_user_command('PyHello', function()
  vim.cmd('py3 print("Hello from Python command")')
end, {})

Using the pynvim API

Basic API Structure

# my_plugin.py
import pynvim

@pynvim.plugin
class MyPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.command('HelloPython')
    def hello_command(self):
        self.nvim.out_write("Hello from Python plugin!\n")
    
    @pynvim.function('PyAdd')
    def add_function(self, args):
        return args[0] + args[1]
    
    @pynvim.autocmd('BufEnter', pattern='*.py')
    def on_python_file(self):
        self.nvim.out_write("Entered a Python file!\n")

Buffer Operations

import pynvim

@pynvim.plugin
class BufferPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.command('LineCount')
    def count_lines(self):
        buffer = self.nvim.current.buffer
        count = len(buffer)
        self.nvim.out_write(f"Buffer has {count} lines\n")
    
    @pynvim.command('InsertDate')
    def insert_date(self):
        from datetime import datetime
        date_str = datetime.now().strftime('%Y-%m-%d')
        
        # Get current position
        row, col = self.nvim.current.window.cursor
        
        # Insert at cursor
        line = self.nvim.current.line
        new_line = line[:col] + date_str + line[col:]
        self.nvim.current.line = new_line
    
    @pynvim.command('AppendLine', nargs='1')
    def append_line(self, args):
        text = args[0]
        buffer = self.nvim.current.buffer
        buffer.append(text)
    
    @pynvim.command('ReplaceAll', nargs='2')
    def replace_all(self, args):
        old, new = args
        buffer = self.nvim.current.buffer
        
        # Modify all lines
        for i, line in enumerate(buffer):
            buffer[i] = line.replace(old, new)
    
    @pynvim.command('GetSelection', range='')
    def get_selection(self, range_info):
        buffer = self.nvim.current.buffer
        start, end = range_info
        
        # Get lines in range (1-indexed to 0-indexed)
        lines = buffer[start-1:end]
        self.nvim.out_write('\n'.join(lines) + '\n')

Window and Tab Operations

@pynvim.plugin
class WindowPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.command('SplitAndEdit', nargs='1')
    def split_and_edit(self, args):
        filename = args[0]
        
        # Split window
        self.nvim.command('split')
        
        # Edit file in new window
        self.nvim.command(f'edit {filename}')
    
    @pynvim.command('WindowInfo')
    def window_info(self):
        win = self.nvim.current.window
        
        info = [
            f"Window number: {win.number}",
            f"Height: {win.height}",
            f"Width: {win.width}",
            f"Cursor: {win.cursor}",
        ]
        
        self.nvim.out_write('\n'.join(info) + '\n')
    
    @pynvim.command('ResizeWindow', nargs='2')
    def resize_window(self, args):
        width, height = map(int, args)
        win = self.nvim.current.window
        
        win.width = width
        win.height = height
    
    @pynvim.command('TabInfo')
    def tab_info(self):
        tabpage = self.nvim.current.tabpage
        windows = tabpage.windows
        
        info = [
            f"Tab number: {tabpage.number}",
            f"Number of windows: {len(windows)}",
        ]
        
        for i, win in enumerate(windows, 1):
            info.append(f"  Window {i}: {win.buffer.name}")
        
        self.nvim.out_write('\n'.join(info) + '\n')

Variable Operations

@pynvim.plugin
class VarPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.command('SetVar', nargs='2')
    def set_variable(self, args):
        var_name, value = args
        
        # Set global variable
        self.nvim.vars[var_name] = value
        self.nvim.out_write(f"Set g:{var_name} = {value}\n")
    
    @pynvim.command('GetVar', nargs='1')
    def get_variable(self, args):
        var_name = args[0]
        
        try:
            value = self.nvim.vars[var_name]
            self.nvim.out_write(f"g:{var_name} = {value}\n")
        except KeyError:
            self.nvim.err_write(f"Variable g:{var_name} not found\n")
    
    @pynvim.command('BufferVars')
    def list_buffer_vars(self):
        buf_vars = self.nvim.current.buffer.vars
        
        if not buf_vars:
            self.nvim.out_write("No buffer variables set\n")
            return
        
        for key, value in buf_vars.items():
            self.nvim.out_write(f"b:{key} = {value}\n")
    
    @pynvim.function('PyEval')
    def eval_expression(self, args):
        """Evaluate Python expression and return result"""
        expr = args[0]
        try:
            result = eval(expr)
            return result
        except Exception as e:
            return f"Error: {str(e)}"

Remote Plugin Development

Creating a Remote Plugin

# rplugin/python3/my_plugin.py
import pynvim
import os

@pynvim.plugin
class MyRemotePlugin:
    def __init__(self, nvim):
        self.nvim = nvim
        self.cache = {}
    
    @pynvim.command('PluginStatus')
    def status(self):
        """Show plugin status"""
        info = [
            "Plugin Status:",
            f"Cache size: {len(self.cache)}",
            f"Neovim version: {self.nvim.api.get_vvar('version')}",
        ]
        self.nvim.out_write('\n'.join(info) + '\n')
    
    @pynvim.command('CacheSet', nargs='2')
    def cache_set(self, args):
        """Set cache value"""
        key, value = args
        self.cache[key] = value
        self.nvim.out_write(f"Cached: {key} = {value}\n")
    
    @pynvim.command('CacheGet', nargs='1')
    def cache_get(self, args):
        """Get cached value"""
        key = args[0]
        value = self.cache.get(key, 'Not found')
        self.nvim.out_write(f"{key} = {value}\n")
    
    @pynvim.function('_my_plugin_complete')
    def complete_function(self, args):
        """Custom completion function"""
        # args: [arglead, cmdline, cursorpos]
        arglead = args[0]
        
        # Return matching cache keys
        matches = [k for k in self.cache.keys() if k.startswith(arglead)]
        return matches

Registering the Plugin


-- After creating the Python file, update remote plugins:

-- :UpdateRemotePlugins

-- Then restart Neovim


-- Check if plugin is loaded
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    vim.cmd('PluginStatus')
  end,
})

Asynchronous Operations

Using Async/Await

import pynvim
import asyncio
from typing import List

@pynvim.plugin
class AsyncPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.command('AsyncFetch', nargs='1')
    def fetch_url(self, args):
        """Fetch URL asynchronously"""
        url = args[0]
        
        # Run async operation
        asyncio.run(self._fetch_and_display(url))
    
    async def _fetch_and_display(self, url):
        """Internal async method"""
        try:
            # Simulate async operation
            self.nvim.out_write(f"Fetching {url}...\n")
            
            # Use aiohttp or requests
            import aiohttp
            async with aiohttp.ClientSession() as session:
                async with session.get(url) as response:
                    text = await response.text()
                    
                    # Create new buffer with results
                    self.nvim.command('new')
                    buffer = self.nvim.current.buffer
                    buffer[:] = text.split('\n')[:50]  # First 50 lines
                    
        except Exception as e:
            self.nvim.err_write(f"Error: {str(e)}\n")
    
    @pynvim.command('AsyncProcess', nargs='+')
    def run_process(self, args):
        """Run external process asynchronously"""
        cmd = ' '.join(args)
        asyncio.run(self._run_process(cmd))
    
    async def _run_process(self, cmd):
        """Run process and capture output"""
        process = await asyncio.create_subprocess_shell(
            cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        
        stdout, stderr = await process.communicate()
        
        if stdout:
            lines = stdout.decode().split('\n')
            self.nvim.command('new')
            self.nvim.current.buffer[:] = lines
        
        if stderr:
            self.nvim.err_write(stderr.decode())

Background Tasks

import pynvim
import threading
import time

@pynvim.plugin
class BackgroundPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
        self.running = False
        self.thread = None
    
    @pynvim.command('StartMonitor')
    def start_monitor(self):
        """Start background monitoring"""
        if self.running:
            self.nvim.out_write("Monitor already running\n")
            return
        
        self.running = True
        self.thread = threading.Thread(target=self._monitor, daemon=True)
        self.thread.start()
        self.nvim.out_write("Monitor started\n")
    
    @pynvim.command('StopMonitor')
    def stop_monitor(self):
        """Stop background monitoring"""
        self.running = False
        self.nvim.out_write("Monitor stopped\n")
    
    def _monitor(self):
        """Background monitoring task"""
        while self.running:
            try:
                # Check something (e.g., file changes)
                buffer = self.nvim.current.buffer
                
                # Safe operation using nvim.async_call
                self.nvim.async_call(
                    lambda: self.nvim.command(
                        f'echo "Monitoring... {time.time()}"'
                    )
                )
                
                time.sleep(5)  # Check every 5 seconds
                
            except Exception as e:
                self.nvim.async_call(
                    lambda: self.nvim.err_write(f"Monitor error: {e}\n")
                )
                break

Advanced Plugin Features

File Type Detection and Handling

import pynvim
import re

@pynvim.plugin
class FileTypePlugin:
    def __init__(self, nvim):
        self.nvim = nvim
        self.formatters = {
            'python': self._format_python,
            'json': self._format_json,
            'xml': self._format_xml,
        }
    
    @pynvim.autocmd('BufWritePre', pattern='*.py')
    def format_on_save_python(self):
        """Auto-format Python files on save"""
        self._format_python()
    
    @pynvim.command('FormatCurrent')
    def format_current_file(self):
        """Format current file based on filetype"""
        ft = self.nvim.eval('&filetype')
        
        formatter = self.formatters.get(ft)
        if formatter:
            formatter()
            self.nvim.out_write(f"Formatted {ft} file\n")
        else:
            self.nvim.err_write(f"No formatter for {ft}\n")
    
    def _format_python(self):
        """Format Python code using black"""
        import subprocess
        
        buffer = self.nvim.current.buffer
        code = '\n'.join(buffer[:])
        
        try:
            result = subprocess.run(
                ['black', '-q', '-'],
                input=code.encode(),
                capture_output=True,
                timeout=5
            )
            
            if result.returncode == 0:
                formatted = result.stdout.decode().split('\n')
                buffer[:] = formatted
        except Exception as e:
            self.nvim.err_write(f"Format error: {e}\n")
    
    def _format_json(self):
        """Format JSON"""
        import json
        
        buffer = self.nvim.current.buffer
        content = '\n'.join(buffer[:])
        
        try:
            data = json.loads(content)
            formatted = json.dumps(data, indent=2)
            buffer[:] = formatted.split('\n')
        except json.JSONDecodeError as e:
            self.nvim.err_write(f"JSON error: {e}\n")
    
    def _format_xml(self):
        """Format XML"""
        import xml.dom.minidom
        
        buffer = self.nvim.current.buffer
        content = '\n'.join(buffer[:])
        
        try:
            dom = xml.dom.minidom.parseString(content)
            formatted = dom.toprettyxml(indent='  ')
            # Remove extra blank lines
            lines = [l for l in formatted.split('\n') if l.strip()]
            buffer[:] = lines
        except Exception as e:
            self.nvim.err_write(f"XML error: {e}\n")

Completion Provider

import pynvim
import os

@pynvim.plugin
class CompletionPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.function('_python_path_complete', sync=True)
    def path_complete(self, args):
        """Custom path completion"""
        # args: [findstart, base]
        findstart, base = args
        
        if findstart:
            # Find start of completion
            line = self.nvim.current.line
            col = self.nvim.current.window.cursor[1]
            
            # Find path start
            start = col
            while start > 0 and line[start-1] not in (' ', '\t', '"', "'"):
                start -= 1
            
            return start
        else:
            # Return completion matches
            return self._get_path_matches(base)
    
    def _get_path_matches(self, base):
        """Get matching file paths"""
        if not base:
            base = '.'
        
        directory = os.path.dirname(base) or '.'
        prefix = os.path.basename(base)
        
        try:
            entries = os.listdir(directory)
            matches = []
            
            for entry in entries:
                if entry.startswith(prefix):
                    full_path = os.path.join(directory, entry)
                    
                    # Add trailing slash for directories
                    if os.path.isdir(full_path):
                        entry += '/'
                    
                    matches.append({
                        'word': entry,
                        'menu': '[Path]',
                        'kind': 'd' if os.path.isdir(full_path) else 'f',
                    })
            
            return matches
        except Exception:
            return []
    
    @pynvim.function('_python_snippet_complete', sync=True)
    def snippet_complete(self, args):
        """Snippet completion"""
        findstart, base = args
        
        if findstart:
            line = self.nvim.current.line
            col = self.nvim.current.window.cursor[1]
            
            start = col
            while start > 0 and line[start-1].isalnum():
                start -= 1
            return start
        else:
            snippets = {
                'def': 'def ${1:name}(${2:args}):\n    ${3:pass}',
                'class': 'class ${1:Name}:\n    def __init__(self):\n        ${2:pass}',
                'for': 'for ${1:item} in ${2:items}:\n    ${3:pass}',
                'if': 'if ${1:condition}:\n    ${2:pass}',
            }
            
            matches = []
            for key, snippet in snippets.items():
                if key.startswith(base):
                    matches.append({
                        'word': key,
                        'menu': '[Snippet]',
                        'info': snippet,
                    })
            
            return matches

Diagnostic and Linting Integration

import pynvim
import subprocess
import json

@pynvim.plugin
class LintPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
        self.diagnostics = {}
    
    @pynvim.command('LintCurrent')
    def lint_current(self):
        """Lint current Python file"""
        buffer = self.nvim.current.buffer
        filename = buffer.name
        
        if not filename.endswith('.py'):
            self.nvim.err_write("Not a Python file\n")
            return
        
        # Run pylint
        try:
            result = subprocess.run(
                ['pylint', '--output-format=json', filename],
                capture_output=True,
                text=True
            )
            
            diagnostics = json.loads(result.stdout)
            self._display_diagnostics(diagnostics)
            
        except Exception as e:
            self.nvim.err_write(f"Lint error: {e}\n")
    
    def _display_diagnostics(self, diagnostics):
        """Display diagnostics in quickfix"""
        qf_list = []
        
        for diag in diagnostics:
            qf_list.append({
                'filename': diag['path'],
                'lnum': diag['line'],
                'col': diag['column'],
                'text': diag['message'],
                'type': diag['type'][0].upper(),  # E, W, etc.
            })
        
        self.nvim.call('setqflist', qf_list)
        self.nvim.command('copen')
        
        if qf_list:
            self.nvim.out_write(f"Found {len(qf_list)} issues\n")
        else:
            self.nvim.out_write("No issues found\n")
    
    @pynvim.autocmd('BufWritePost', pattern='*.py')
    def lint_on_save(self):
        """Auto-lint on save"""
        # Could call self.lint_current() here
        pass

Integration with External Tools

Git Integration

import pynvim
import subprocess
import os

@pynvim.plugin
class GitPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.command('GitStatus')
    def git_status(self):
        """Show git status"""
        try:
            result = subprocess.run(
                ['git', 'status', '--short'],
                capture_output=True,
                text=True,
                cwd=self._get_git_root()
            )
            
            if result.returncode == 0:
                # Open new buffer with status
                self.nvim.command('new')
                buffer = self.nvim.current.buffer
                buffer.name = '[Git Status]'
                buffer[:] = result.stdout.split('\n')
                buffer.options['modifiable'] = False
            else:
                self.nvim.err_write(result.stderr)
                
        except Exception as e:
            self.nvim.err_write(f"Git error: {e}\n")
    
    @pynvim.command('GitBlame')
    def git_blame(self):
        """Show git blame for current file"""
        filename = self.nvim.current.buffer.name
        line_num = self.nvim.current.window.cursor[0]
        
        try:
            result = subprocess.run(
                ['git', 'blame', '-L', f'{line_num},{line_num}', filename],
                capture_output=True,
                text=True
            )
            
            if result.returncode == 0:
                self.nvim.out_write(result.stdout)
            else:
                self.nvim.err_write(result.stderr)
                
        except Exception as e:
            self.nvim.err_write(f"Blame error: {e}\n")
    
    @pynvim.command('GitDiff')
    def git_diff(self):
        """Show git diff for current file"""
        filename = self.nvim.current.buffer.name
        
        try:
            result = subprocess.run(
                ['git', 'diff', filename],
                capture_output=True,
                text=True
            )
            
            if result.returncode == 0:
                # Open diff in new buffer
                self.nvim.command('new')
                buffer = self.nvim.current.buffer
                buffer.name = f'[Git Diff] {os.path.basename(filename)}'
                buffer[:] = result.stdout.split('\n')
                buffer.options['filetype'] = 'diff'
                buffer.options['modifiable'] = False
                
        except Exception as e:
            self.nvim.err_write(f"Diff error: {e}\n")
    
    def _get_git_root(self):
        """Get git repository root"""
        try:
            result = subprocess.run(
                ['git', 'rev-parse', '--show-toplevel'],
                capture_output=True,
                text=True,
                cwd=os.path.dirname(self.nvim.current.buffer.name)
            )
            return result.stdout.strip()
        except Exception:
            return os.getcwd()

Database Integration

import pynvim
import sqlite3
import json

@pynvim.plugin
class DatabasePlugin:
    def __init__(self, nvim):
        self.nvim = nvim
        self.connection = None
    
    @pynvim.command('DBConnect', nargs='1')
    def connect_db(self, args):
        """Connect to SQLite database"""
        db_path = args[0]
        
        try:
            self.connection = sqlite3.connect(db_path)
            self.connection.row_factory = sqlite3.Row
            self.nvim.out_write(f"Connected to {db_path}\n")
        except Exception as e:
            self.nvim.err_write(f"Connection error: {e}\n")
    
    @pynvim.command('DBQuery', nargs='+')
    def execute_query(self, args):
        """Execute SQL query"""
        if not self.connection:
            self.nvim.err_write("Not connected to database\n")
            return
        
        query = ' '.join(args)
        
        try:
            cursor = self.connection.cursor()
            cursor.execute(query)
            
            # Fetch results
            rows = cursor.fetchall()
            
            if rows:
                # Format as table
                columns = [desc[0] for desc in cursor.description]
                self._display_table(columns, rows)
            else:
                self.nvim.out_write("Query executed successfully\n")
                
            self.connection.commit()
            
        except Exception as e:
            self.nvim.err_write(f"Query error: {e}\n")
    
    @pynvim.command('DBTables')
    def list_tables(self):
        """List database tables"""
        if not self.connection:
            self.nvim.err_write("Not connected to database\n")
            return
        
        try:
            cursor = self.connection.cursor()
            cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
            tables = [row[0] for row in cursor.fetchall()]
            
            self.nvim.out_write("Tables:\n")
            for table in tables:
                self.nvim.out_write(f"  - {table}\n")
                
        except Exception as e:
            self.nvim.err_write(f"Error: {e}\n")
    
    def _display_table(self, columns, rows):
        """Display results as formatted table"""
        # Create new buffer
        self.nvim.command('new')
        buffer = self.nvim.current.buffer
        buffer.name = '[Query Results]'
        
        # Format output
        lines = []
        
        # Header
        header = ' | '.join(columns)
        lines.append(header)
        lines.append('-' * len(header))
        
        # Rows
        for row in rows:
            row_data = [str(row[col]) for col in columns]
            lines.append(' | '.join(row_data))
        
        buffer[:] = lines
        buffer.options['modifiable'] = False

Testing Python Plugins

Unit Testing

# tests/test_my_plugin.py
import pytest
import pynvim

class TestMyPlugin:
    @pytest.fixture
    def nvim(self):
        """Create test Neovim instance"""
        child_argv = ['nvim', '--embed', '--headless']
        nvim = pynvim.attach('child', argv=child_argv)
        yield nvim
        nvim.close()
    
    def test_buffer_operations(self, nvim):
        """Test buffer operations"""
        # Create buffer
        buffer = nvim.current.buffer
        
        # Set content
        buffer[:] = ['line 1', 'line 2']
        
        # Verify
        assert len(buffer) == 2
        assert buffer[0] == 'line 1'
    
    def test_command_execution(self, nvim):
        """Test command execution"""
        # Load plugin
        nvim.command('runtime! plugin/**/*.vim')
        nvim.command('UpdateRemotePlugins')
        
        # Test command
        nvim.command('MyCommand')
        
        # Verify results
        # ...

Integration Testing

# tests/integration/test_plugin_integration.py
import pynvim
import os
import tempfile

def test_file_operations():
    """Test file-based operations"""
    nvim = pynvim.attach('child', argv=['nvim', '--embed', '--headless'])
    
    try:
        # Create temp file
        with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.py') as f:
            f.write('def test():\n    pass\n')
            temp_path = f.name
        
        # Open in Neovim
        nvim.command(f'edit {temp_path}')
        
        # Test plugin functionality
        buffer = nvim.current.buffer
        assert len(buffer) == 2
        
        # Cleanup
        os.unlink(temp_path)
        
    finally:
        nvim.close()

Performance Optimization

Caching

import pynvim
from functools import lru_cache
import time

@pynvim.plugin
class CachedPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
        self._cache = {}
        self._cache_timeout = 60  # seconds
    
    @pynvim.function('ExpensiveOperation')
    def expensive_op(self, args):
        """Cached expensive operation"""
        key = str(args)
        
        # Check cache
        if key in self._cache:
            cached_value, timestamp = self._cache[key]
            if time.time() - timestamp < self._cache_timeout:
                return cached_value
        
        # Compute
        result = self._compute_expensive(args)
        
        # Cache result
        self._cache[key] = (result, time.time())
        
        return result
    
    @lru_cache(maxsize=128)
    def _compute_expensive(self, args):
        """Expensive computation"""
        # Simulate expensive operation
        time.sleep(0.1)
        return sum(args)
    
    @pynvim.command('ClearCache')
    def clear_cache(self):
        """Clear plugin cache"""
        self._cache.clear()
        self._compute_expensive.cache_clear()
        self.nvim.out_write("Cache cleared\n")

Batching Operations

@pynvim.plugin
class BatchPlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.command('BatchUpdate', range='')
    def batch_update(self, range_info):
        """Update multiple lines efficiently"""
        start, end = range_info
        buffer = self.nvim.current.buffer
        
        # Batch operation instead of line-by-line
        lines = buffer[start-1:end]
        
        # Process all lines
        processed = [self._process_line(line) for line in lines]
        
        # Single update
        buffer[start-1:end] = processed
    
    def _process_line(self, line):
        """Process single line"""
        return line.upper()

Best Practices

1. Error Handling

@pynvim.plugin
class SafePlugin:
    def __init__(self, nvim):
        self.nvim = nvim
    
    @pynvim.command('SafeCommand')
    def safe_command(self):
        """Command with proper error handling"""
        try:
            # Risky operation
            result = self._risky_operation()
            self.nvim.out_write(f"Success: {result}\n")
            
        except FileNotFoundError as e:
            self.nvim.err_write(f"File not found: {e}\n")
        except PermissionError as e:
            self.nvim.err_write(f"Permission denied: {e}\n")
        except Exception as e:
            # Log unexpected errors
            self._log_error(e)
            self.nvim.err_write(f"Unexpected error: {e}\n")
    
    def _log_error(self, error):
        """Log error to file"""
        import traceback
        log_path = os.path.expanduser('~/.nvim-plugin-errors.log')
        with open(log_path, 'a') as f:
            f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')}\n")
            f.write(traceback.format_exc())
            f.write("\n\n")

2. Configuration Management

@pynvim.plugin
class ConfigurablePlugin:
    def __init__(self, nvim):
        self.nvim = nvim
        self.config = self._load_config()
    
    def _load_config(self):
        """Load plugin configuration"""
        return {
            'enabled': self.nvim.vars.get('plugin_enabled', True),
            'timeout': self.nvim.vars.get('plugin_timeout', 30),
            'cache_dir': self.nvim.vars.get(
                'plugin_cache_dir',
                os.path.expanduser('~/.cache/nvim-plugin')
            ),
        }
    
    @pynvim.command('PluginConfig')
    def show_config(self):
        """Show current configuration"""
        import json
        config_str = json.dumps(self.config, indent=2)
        self.nvim.out_write(config_str + '\n')
    
    @pynvim.command('PluginSetConfig', nargs='+')
    def set_config(self, args):
        """Set configuration value"""
        if len(args) < 2:
            self.nvim.err_write("Usage: PluginSetConfig <key> <value>\n")
            return
        
        key, value = args[0], ' '.join(args[1:])
        
        # Try to parse as JSON
        try:
            import json
            value = json.loads(value)
        except:
            pass  # Keep as string
        
        self.config[key] = value
        self.nvim.out_write(f"Set {key} = {value}\n")

3. Documentation

"""
My Neovim Plugin
================

A Python plugin for Neovim that provides...

Requirements:

    - pynvim >= 0.4.0

    - Python >= 3.6

Installation:
    1. Install pynvim: pip install pynvim
    2. Copy to rplugin/python3/
    3. Run :UpdateRemotePlugins

Commands:
    :MyCommand - Does something useful
    :MyStatus - Shows plugin status

Functions:
    MyFunc(args) - Callable function

Configuration:
    let g:my_plugin_enabled = 1
    let g:my_plugin_timeout = 30

License: MIT
"""

import pynvim

@pynvim.plugin
class MyPlugin:
    """Main plugin class"""
    
    def __init__(self, nvim):
        """
        Initialize plugin
        
        Args:
            nvim: Neovim instance
        """
        self.nvim = nvim
    
    @pynvim.command('MyCommand', nargs='*')
    def my_command(self, args):
        """
        Execute my command
        
        Args:
            args: Command arguments
            
        Example:
            :MyCommand arg1 arg2
        """
        pass

Summary: Chapter #30 – Python Integration

This chapter covered:

  1. Setup – Installing and configuring the Python provider

  2. Basic API – Executing Python code and using pynvim decorators

  3. Buffer/Window Operations – Manipulating Neovim buffers, windows, and tabs

  4. Remote Plugins – Creating full-featured Python plugins

  5. Async Operations – Background tasks and asynchronous programming

  6. Advanced Features – File type handling, completion, linting, and external tool integration

  7. Testing – Unit and integration testing strategies

  8. Performance – Caching and batching optimizations

  9. Best Practices – Error handling, configuration, and documentation

The next chapter will explore Chapter #31: Lua Plugin Development, covering how to create performant plugins using Neovim’s built-in Lua runtime.


Chapter #31: Ruby Integration

Neovim provides Ruby integration through its RPC API and the neovim Ruby gem. While less commonly used than Python or Lua, Ruby integration offers powerful capabilities for those familiar with the language.

Prerequisites and Setup

Installing the Ruby Provider

# Install Ruby (if not already installed)
# On Ubuntu/Debian
sudo apt-get install ruby ruby-dev

# On macOS
brew install ruby

# Install neovim gem
gem install neovim

# Or using bundler
bundle add neovim

# For system-wide installation
sudo gem install neovim

Configuring the Ruby Provider


-- In your init.lua


-- Point to Ruby executable
vim.g.ruby_host_prog = '/usr/bin/ruby'


-- Or specify gem binary path
vim.g.ruby_host_prog = vim.fn.expand('~/.gem/ruby/3.0.0/bin/neovim-ruby-host')


-- Check Ruby provider health

-- :checkhealth provider

Verify Installation

" Check Ruby support
:echo has('ruby')
" Should return 1

" Check provider
:checkhealth provider

" Test Ruby execution
:ruby puts "Hello from Ruby"

Ruby Commands in Neovim

Executing Ruby Code

" Execute single Ruby statement
:ruby puts "Hello from Neovim"

" Execute multiple lines
:ruby << EOF
  require 'neovim'
  buffer = Neovim.current.buffer
  puts "Current buffer has #{buffer.count} lines"
EOF

" Execute Ruby file
:rubyfile ~/scripts/my_script.rb

Lua Integration


-- Execute Ruby from Lua
vim.cmd([[
  ruby << EOF
    puts "Ruby executed from Lua"
EOF
]])


-- Create command that runs Ruby
vim.api.nvim_create_user_command('RubyHello', function()
  vim.cmd('ruby puts "Hello from Ruby command"')
end, {})

Using the Neovim Ruby API

Basic Plugin Structure

# my_plugin.rb
require 'neovim'

Neovim.plugin do |plug|
  # Define a command
  plug.command() do |nvim|
    nvim.out_write("Hello from Ruby plugin!\n")
  end
  
  # Define a function
  plug.function(, true) do |nvim, a, b|
    a + b
  end
  
  # Define an autocmd
  plug.autocmd(, '*.rb') do |nvim|
    nvim.out_write("Entered a Ruby file!\n")
  end
end

Buffer Operations

require 'neovim'

Neovim.plugin do |plug|
  plug.command() do |nvim|
    buffer = nvim.current.buffer
    count = buffer.count
    nvim.out_write("Buffer has #{count} lines\n")
  end
  
  plug.command() do |nvim|
    require 'date'
    date_str = Date.today.to_s
    
    # Get current position
    row, col = nvim.current.window.cursor
    
    # Get current line
    line = nvim.current.line
    
    # Insert at cursor
    new_line = line[0...col] + date_str + line[col..-1]
    nvim.current.line = new_line
  end
  
  plug.command(, 1) do |nvim, text|
    buffer = nvim.current.buffer
    buffer.append(buffer.count, text)
  end
  
  plug.command(, 2) do |nvim, old_text, new_text|
    buffer = nvim.current.buffer
    
    # Modify all lines
    buffer.count.times do |i|
      line = buffer[i]
      buffer[i] = line.gsub(old_text, new_text)
    end
  end
  
  plug.command(, true) do |nvim, start_line, end_line|
    buffer = nvim.current.buffer
    
    # Get lines in range (1-indexed)
    lines = buffer[start_line - 1...end_line]
    nvim.out_write(lines.join("\n") + "\n")
  end
  
  plug.command(, '%') do |nvim, start_line, end_line|
    buffer = nvim.current.buffer
    lines = buffer[start_line - 1...end_line]
    
    # Reverse the lines
    buffer[start_line - 1...end_line] = lines.reverse
  end
end

Window and Tab Operations

Neovim.plugin do |plug|
  plug.command(, 1) do |nvim, filename|
    # Split window
    nvim.command('split')
    
    # Edit file in new window
    nvim.command("edit #{filename}")
  end
  
  plug.command() do |nvim|
    win = nvim.current.window
    
    info = [
      "Window number: #{win.number}",
      "Height: #{win.height}",
      "Width: #{win.width}",
      "Cursor: #{win.cursor.inspect}"
    ]
    
    nvim.out_write(info.join("\n") + "\n")
  end
  
  plug.command(, 2) do |nvim, width, height|
    win = nvim.current.window
    win.width = width.to_i
    win.height = height.to_i
  end
  
  plug.command() do |nvim|
    tabpage = nvim.current.tabpage
    windows = tabpage.windows
    
    info = [
      "Tab number: #{tabpage.number}",
      "Number of windows: #{windows.length}"
    ]
    
    windows.each_with_index do |win, i|
      info << "  Window #{i + 1}: #{win.buffer.name}"
    end
    
    nvim.out_write(info.join("\n") + "\n")
  end
end

Variable Operations

Neovim.plugin do |plug|
  plug.command(, 2) do |nvim, var_name, value|
    # Set global variable
    nvim.set_var(var_name, value)
    nvim.out_write("Set g:#{var_name} = #{value}\n")
  end
  
  plug.command(, 1) do |nvim, var_name|
    begin
      value = nvim.get_var(var_name)
      nvim.out_write("g:#{var_name} = #{value}\n")
    rescue StandardError => e
      nvim.err_write("Variable g:#{var_name} not found\n")
    end
  end
  
  plug.command() do |nvim|
    buffer = nvim.current.buffer
    
    # Note: Direct buffer.vars access may vary by API version
    # This is a simplified example
    nvim.out_write("Buffer variables:\n")
    
    # List buffer-local variables through Vim evaluation
    vars = nvim.evaluate('getbufvar(bufnr("%"), "")')
    
    if vars.empty?
      nvim.out_write("No buffer variables set\n")
    else
      vars.each do |key, value|
        nvim.out_write("b:#{key} = #{value}\n")
      end
    end
  end
  
  plug.function(, true) do |nvim, expression|
    begin
      eval(expression)
    rescue StandardError => e
      "Error: #{e.message}"
    end
  end
end

Remote Plugin Development

Creating a Remote Plugin

# rplugin/ruby/my_plugin.rb
require 'neovim'

Neovim.plugin do |plug|
  # Plugin state
  @cache = {}
  
  plug.command() do |nvim|
    info = [
      "Plugin Status:",
      "Cache size: #{@cache.size}",
      "Ruby version: #{RUBY_VERSION}"
    ]
    nvim.out_write(info.join("\n") + "\n")
  end
  
  plug.command(, 2) do |nvim, key, value|
    @cache[key] = value
    nvim.out_write("Cached: #{key} = #{value}\n")
  end
  
  plug.command(, 1) do |nvim, key|
    value = @cache[key] || 'Not found'
    nvim.out_write("#{key} = #{value}\n")
  end
  
  plug.command() do |nvim|
    @cache.clear
    nvim.out_write("Cache cleared\n")
  end
  
  plug.function(, true) do |nvim, arglead, cmdline, cursorpos|
    # Custom completion function
    @cache.keys.select { |k| k.start_with?(arglead) }
  end
end

Registering the Plugin


-- After creating the Ruby file, update remote plugins

-- :UpdateRemotePlugins

-- Then restart Neovim


-- Verify plugin loaded
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function()
    vim.cmd('PluginStatus')
  end,
})

Working with Files and I/O

File Processing Plugin

Neovim.plugin do |plug|
  plug.command() do |nvim|
    require 'csv'
    
    buffer = nvim.current.buffer
    content = buffer.to_a.join("\n")
    
    begin
      # Parse CSV
      data = CSV.parse(content, true)
      
      # Process data (example: convert to markdown table)
      lines = []
      
      # Headers
      lines << "| #{data.headers.join(' | ')} |"
      lines << "|#{data.headers.map { '---' }.join('|')}|"
      
      # Rows
      data.each do |row|
        lines << "| #{row.fields.join(' | ')} |"
      end
      
      # Create new buffer with results
      nvim.command('new')
      nvim.current.buffer.append(0, lines)
      
    rescue StandardError => e
      nvim.err_write("CSV processing error: #{e.message}\n")
    end
  end
  
  plug.command(, 1) do |nvim, filepath|
    require 'json'
    
    begin
      content = File.read(File.expand_path(filepath))
      data = JSON.parse(content)
      
      # Pretty print to buffer
      pretty = JSON.pretty_generate(data)
      
      nvim.command('new')
      buffer = nvim.current.buffer
      buffer.append(0, pretty.split("\n"))
      buffer.name = "[JSON] #{File.basename(filepath)}"
      
    rescue StandardError => e
      nvim.err_write("JSON load error: #{e.message}\n")
    end
  end
  
  plug.command(, 1) do |nvim, filepath|
    require 'json'
    
    buffer = nvim.current.buffer
    content = buffer.to_a.join("\n")
    
    begin
      # Validate JSON
      JSON.parse(content)
      
      # Save to file
      File.write(File.expand_path(filepath), content)
      nvim.out_write("Saved to #{filepath}\n")
      
    rescue JSON::ParserError => e
      nvim.err_write("Invalid JSON: #{e.message}\n")
    rescue StandardError => e
      nvim.err_write("Save error: #{e.message}\n")
    end
  end
end

Advanced Features

Syntax Highlighting and Formatting

Neovim.plugin do |plug|
  plug.command() do |nvim|
    require 'tempfile'
    
    buffer = nvim.current.buffer
    code = buffer.to_a.join("\n")
    
    # Create temp file
    Tempfile.create(['nvim_ruby', '.rb']) do |f|
      f.write(code)
      f.flush
      
      # Format using rubocop
      system("rubocop -a #{f.path} 2>/dev/null")
      
      # Read formatted content
      formatted = File.read(f.path).split("\n")
      
      # Update buffer
      buffer.set_lines(0, -1, true, formatted)
    end
    
    nvim.out_write("Formatted Ruby code\n")
  rescue StandardError => e
    nvim.err_write("Format error: #{e.message}\n")
  end
  
  plug.command() do |nvim|
    buffer = nvim.current.buffer
    filename = buffer.name
    
    unless filename.end_with?('.rb')
      nvim.err_write("Not a Ruby file\n")
      return
    end
    
    # Run rubocop
    output = `rubocop --format json #{filename} 2>&1`
    
    begin
      require 'json'
      results = JSON.parse(output)
      
      # Convert to quickfix format
      qf_list = []
      
      results['files'].each do |file|
        file['offenses'].each do |offense|
          qf_list << {
            'filename' => file['path'],
            'lnum' => offense['location']['line'],
            'col' => offense['location']['column'],
            'text' => offense['message'],
            'type' => offense['severity'][0].upcase
          }
        end
      end
      
      # Set quickfix list
      nvim.call('setqflist', qf_list)
      nvim.command('copen')
      
      if qf_list.empty?
        nvim.out_write("No issues found\n")
      else
        nvim.out_write("Found #{qf_list.size} issues\n")
      end
      
    rescue StandardError => e
      nvim.err_write("Lint error: #{e.message}\n")
    end
  end
end

Text Processing Utilities

Neovim.plugin do |plug|
  plug.command(, '%') do |nvim, start_line, end_line|
    buffer = nvim.current.buffer
    lines = buffer[start_line - 1...end_line]
    
    sorted = lines.sort
    buffer.set_lines(start_line - 1, end_line, true, sorted)
  end
  
  plug.command(, '%') do |nvim, start_line, end_line|
    buffer = nvim.current.buffer
    lines = buffer[start_line - 1...end_line]
    
    unique = lines.uniq
    buffer.set_lines(start_line - 1, end_line, true, unique)
  end
  
  plug.command(, '%') do |nvim, start_line, end_line|
    buffer = nvim.current.buffer
    lines = buffer[start_line - 1...end_line]
    
    numbered = lines.each_with_index.map do |line, i|
      "#{i + 1}. #{line}"
    end
    
    buffer.set_lines(start_line - 1, end_line, true, numbered)
  end
  
  plug.command(, 1, '%') do |nvim, char, start_line, end_line|
    buffer = nvim.current.buffer
    lines = buffer[start_line - 1...end_line]
    
    # Find max position of alignment character
    positions = lines.map { |line| line.index(char) || 0 }
    max_pos = positions.max
    
    # Align lines
    aligned = lines.each_with_index.map do |line, i|
      pos = positions[i]
      if pos > 0
        spaces = ' ' * (max_pos - pos)
        line.sub(char, "#{spaces}#{char}")
      else
        line
      end
    end
    
    buffer.set_lines(start_line - 1, end_line, true, aligned)
  end
  
  plug.command() do |nvim|
    buffer = nvim.current.buffer
    text = buffer.to_a.join(' ')
    
    words = text.split(/\s+/).reject(&)
    chars = text.length
    
    info = [
      "Words: #{words.length}",
      "Characters: #{chars}",
      "Lines: #{buffer.count}"
    ]
    
    nvim.out_write(info.join("\n") + "\n")
  end
end

Integration with External Tools

Git Integration

Neovim.plugin do |plug|
  plug.command() do |nvim|
    output = `git status --short 2>&1`
    
    if $?.success?
      nvim.command('new')
      buffer = nvim.current.buffer
      buffer.name = '[Git Status]'
      buffer.append(0, output.split("\n"))
      nvim.command('setlocal nomodifiable buftype=nofile')
    else
      nvim.err_write("Git error: #{output}\n")
    end
  end
  
  plug.command() do |nvim|
    filename = nvim.current.buffer.name
    line_num = nvim.current.window.cursor[0]
    
    output = `git blame -L #{line_num},#{line_num} #{filename} 2>&1`
    
    if $?.success?
      nvim.out_write(output)
    else
      nvim.err_write("Blame error: #{output}\n")
    end
  end
  
  plug.command() do |nvim|
    filename = nvim.current.buffer.name
    output = `git diff #{filename} 2>&1`
    
    if $?.success?
      nvim.command('new')
      buffer = nvim.current.buffer
      buffer.name = "[Git Diff] #{File.basename(filename)}"
      buffer.append(0, output.split("\n"))
      nvim.command('setlocal filetype=diff nomodifiable buftype=nofile')
    else
      nvim.err_write("Diff error: #{output}\n")
    end
  end
  
  plug.command(, '?') do |nvim, count = '10'|
    output = `git log -n #{count} --oneline 2>&1`
    
    if $?.success?
      nvim.command('new')
      buffer = nvim.current.buffer
      buffer.name = '[Git Log]'
      buffer.append(0, output.split("\n"))
      nvim.command('setlocal nomodifiable buftype=nofile')
    else
      nvim.err_write("Log error: #{output}\n")
    end
  end
end

HTTP Client

Neovim.plugin do |plug|
  plug.command(, 1) do |nvim, url|
    require 'net/http'
    require 'uri'
    
    begin
      uri = URI.parse(url)
      response = Net::HTTP.get_response(uri)
      
      # Display response
      nvim.command('new')
      buffer = nvim.current.buffer
      buffer.name = "[HTTP] #{uri.host}"
      
      lines = [
        "Status: #{response.code} #{response.message}",
        "Headers:",

        *response.each_header.map { |k, v| "  #{k}: #{v}" },
        "",
        "Body:",

        *response.body.split("\n")
      ]
      
      buffer.append(0, lines)
      nvim.command('setlocal buftype=nofile')
      
    rescue StandardError => e
      nvim.err_write("HTTP error: #{e.message}\n")
    end
  end
  
  plug.command(, '+') do |nvim, url, *data|
    require 'net/http'
    require 'uri'
    
    begin
      uri = URI.parse(url)
      post_data = data.join(' ')
      
      response = Net::HTTP.post_form(uri, { post_data })
      
      nvim.out_write("Response: #{response.code} #{response.message}\n")
      nvim.out_write(response.body[0...500] + "\n")
      
    rescue StandardError => e
      nvim.err_write("HTTP error: #{e.message}\n")
    end
  end
end

Database Operations

Neovim.plugin do |plug|
  @db_connection = nil
  
  plug.command(, 1) do |nvim, db_path|
    require 'sqlite3'
    
    begin
      @db_connection = SQLite3::Database.new(File.expand_path(db_path))
      @db_connection.results_as_hash = true
      nvim.out_write("Connected to #{db_path}\n")
    rescue StandardError => e
      nvim.err_write("Connection error: #{e.message}\n")
    end
  end
  
  plug.command(, '+') do |nvim, *query_parts|
    unless @db_connection
      nvim.err_write("Not connected to database\n")
      return
    end
    
    query = query_parts.join(' ')
    
    begin
      results = @db_connection.execute(query)
      
      if results.empty?
        nvim.out_write("Query executed successfully\n")
      else
        # Format as table
        nvim.command('new')
        buffer = nvim.current.buffer
        buffer.name = '[Query Results]'
        
        # Headers
        headers = results.first.keys
        lines = [
          headers.join(' | '),
          headers.map { |h| '-' * h.length }.join('-+-')
        ]
        
        # Data rows
        results.each do |row|
          lines << headers.map { |h| row[h].to_s }.join(' | ')
        end
        
        buffer.append(0, lines)
        nvim.command('setlocal nomodifiable buftype=nofile')
      end
      
    rescue StandardError => e
      nvim.err_write("Query error: #{e.message}\n")
    end
  end
  
  plug.command() do |nvim|
    unless @db_connection
      nvim.err_write("Not connected to database\n")
      return
    end
    
    begin
      tables = @db_connection.execute(
        "SELECT name FROM sqlite_master WHERE type='table'"
      )
      
      nvim.out_write("Tables:\n")
      tables.each do |row|
        nvim.out_write("  - #{row['name']}\n")
      end
      
    rescue StandardError => e
      nvim.err_write("Error: #{e.message}\n")
    end
  end
end

Asynchronous Operations

Background Tasks

Neovim.plugin do |plug|
  @background_thread = nil
  @running = false
  
  plug.command() do |nvim|
    if @running
      nvim.out_write("Monitor already running\n")
      return
    end
    
    @running = true
    
    @background_thread = Thread.new do
      while @running
        begin
          # Perform background task
          sleep 5
          
          # Update Neovim (must be thread-safe)
          time = Time.now.strftime('%H:%M:%S')
          nvim.command("echo 'Monitor tick: #{time}'")
          
        rescue StandardError => e
          nvim.err_write("Monitor error: #{e.message}\n")
          break
        end
      end
    end
    
    nvim.out_write("Monitor started\n")
  end
  
  plug.command() do |nvim|
    @running = false
    
    if @background_thread
      @background_thread.join(2)
      @background_thread = nil
    end
    
    nvim.out_write("Monitor stopped\n")
  end
end

Event-Driven Processing

Neovim.plugin do |plug|
  @event_queue = Queue.new
  @processor = nil
  
  plug.command() do |nvim|
    return if @processor&.alive?
    
    @processor = Thread.new do
      loop do
        event = @event_queue.pop
        
        begin
          # Process event
          nvim.command("echo 'Processing: #{event}'")
          
          # Simulate processing
          sleep 1
          
        rescue StandardError => e
          nvim.err_write("Event processing error: #{e.message}\n")
        end
      end
    end
    
    nvim.out_write("Event processor started\n")
  end
  
  plug.command(, 1) do |nvim, event|
    @event_queue.push(event)
    nvim.out_write("Queued: #{event}\n")
  end
  
  plug.command() do |nvim|
    nvim.out_write("Queue size: #{@event_queue.size}\n")
    nvim.out_write("Processor alive: #{@processor&.alive?}\n")
  end
end

Testing Ruby Plugins

Unit Testing with RSpec

# spec/my_plugin_spec.rb
require 'rspec'
require 'neovim'

RSpec.describe 'MyPlugin' do
  let() do
    # Create embedded Neovim instance
    argv = ['nvim', '--embed', '--headless']
    Neovim.attach_child(argv)
  end
  
  after do
    nvim.shutdown
  end
  
  it 'executes commands' do
    nvim.command('echo "test"')
    expect(nvim).to be_truthy
  end
  
  it 'manipulates buffers' do
    buffer = nvim.current.buffer
    buffer.append(0, 'test line')
    
    expect(buffer.count).to be > 0
    expect(buffer[0]).to eq('test line')
  end
  
  it 'sets variables' do
    nvim.set_var('test_var', 'test_value')
    expect(nvim.get_var('test_var')).to eq('test_value')
  end
end

Integration Testing

# spec/integration/plugin_integration_spec.rb
require 'rspec'
require 'neovim'
require 'tempfile'

RSpec.describe 'Plugin Integration' do
  let() { Neovim.attach_child(['nvim', '--embed', '--headless']) }
  
  after { nvim.shutdown }
  
  it 'loads remote plugins' do
    # Load plugin
    nvim.command('runtime! plugin/**/*.vim')
    nvim.command('UpdateRemotePlugins')
    
    # Test plugin command exists
    commands = nvim.api.get_commands({})
    expect(commands).to have_key('MyCommand')
  end
  
  it 'processes files' do
    Tempfile.create(['test', '.rb']) do |f|
      f.write("def test\n  puts 'hello'\nend")
      f.flush
      
      nvim.command("edit #{f.path}")
      buffer = nvim.current.buffer
      
      expect(buffer.count).to eq(3)
    end
  end
end

Performance Optimization

Memoization

Neovim.plugin do |plug|
  @cache = {}
  
  plug.function(, true) do |nvim, *args|
    cache_key = args.to_s
    
    @cache[cache_key] ||= begin
      # Expensive computation
      sleep 0.1
      args.sum
    end
  end
  
  plug.command() do |nvim|
    @cache.clear
    nvim.out_write("Memoization cache cleared\n")
  end
end

Batch Processing

Neovim.plugin do |plug|
  plug.command(, '%') do |nvim, start_line, end_line|
    buffer = nvim.current.buffer
    
    # Get all lines at once
    lines = buffer.get_lines(start_line - 1, end_line, true)
    
    # Process in batch
    processed = lines.map(&)
    
    # Update all at once
    buffer.set_lines(start_line - 1, end_line, true, processed)
  end
end

Best Practices

1. Error Handling

Neovim.plugin do |plug|
  plug.command() do |nvim|
    begin
      # Risky operation
      result = risky_operation
      nvim.out_write("Success: #{result}\n")
      
    rescue Errno::ENOENT => e
      nvim.err_write("File not found: #{e.message}\n")
    rescue Errno::EACCES => e
      nvim.err_write("Permission denied: #{e.message}\n")
    rescue StandardError => e
      # Log unexpected errors
      log_error(e)
      nvim.err_write("Unexpected error: #{e.message}\n")
    end
  end
  
  def log_error(error)
    log_path = File.expand_path('~/.nvim-ruby-errors.log')
    File.open(log_path, 'a') do |f|
      f.puts "#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
      f.puts error.message
      f.puts error.backtrace.join("\n")
      f.puts "\n"
    end
  end
end

2. Configuration Management

Neovim.plugin do |plug|
  def load_config(nvim)
    {
      nvim.get_var('ruby_plugin_enabled') rescue true,
      nvim.get_var('ruby_plugin_timeout') rescue 30,
      nvim.get_var('ruby_plugin_cache_dir') rescue File.expand_path('~/.cache/nvim-ruby')
    }
  end
  
  plug.command() do |nvim|
    require 'json'
    config = load_config(nvim)
    nvim.out_write(JSON.pretty_generate(config) + "\n")
  end
  
  plug.command(, 2) do |nvim, key, value|
    # Parse value
    parsed_value = begin
      Integer(value)
    rescue ArgumentError
      value == 'true' ? true : value == 'false' ? false : value
    end
    
    nvim.set_var("ruby_plugin_#{key}", parsed_value)
    nvim.out_write("Set #{key} = #{parsed_value}\n")
  end
end

3. Documentation

# my_plugin.rb
# 
# My Neovim Ruby Plugin
# =====================
# 
# A Ruby plugin for Neovim that provides...
# 
# Requirements:
#   - neovim gem >= 0.9.0
#   - Ruby >= 2.7
# 
# Installation:
#   1. gem install neovim
#   2. Copy to rplugin/ruby/
#   3. Run :UpdateRemotePlugins
# 
# Commands:
#   :MyCommand - Does something useful
#   :MyStatus  - Shows plugin status
# 
# Configuration:
#   let g:my_plugin_enabled = 1
#   let g:my_plugin_timeout = 30
# 
# License: MIT

require 'neovim'

Neovim.plugin do |plug|
  # Plugin implementation
  
  plug.command(, '*') do |nvim, *args|
    # Command implementation
    # 
    # Args:
    #   args - Command arguments
    # 
    # Example:
    #   :MyCommand arg1 arg2
  end
end

Summary: Chapter #31 – Ruby Integration

This chapter covered:

  1. Setup – Installing and configuring the Ruby provider

  2. Basic API – Executing Ruby code and using the Neovim Ruby API

  3. Buffer/Window Operations – Manipulating Neovim buffers, windows, and tabs

  4. Remote Plugins – Creating full-featured Ruby plugins

  5. File Processing – Working with CSV, JSON, and other file formats

  6. Advanced Features – Syntax highlighting, linting, and text processing

  7. External Tools – Git, HTTP, and database integration

  8. Async Operations – Background tasks and event-driven processing

  9. Testing – Unit and integration testing with RSpec

  10. Performance – Memoization and batch processing

  11. Best Practices – Error handling, configuration, and documentation

The next chapter will explore Chapter #32: JavaScript/Node.js Integration, covering how to extend Neovim using JavaScript and the Node.js ecosystem.


Chapter #32: Git Integration

Neovim offers extensive capabilities for Git integration, from built-in features to powerful plugins. This chapter explores various approaches to working with Git seamlessly within your editor.

Built-in Git Features

Vim’s Native Git Support

" Read git output directly
:read !git status
:read !git diff %

" Write and commit in one go
:write | !git add % && git commit -m "Quick commit"

" Open file from specific commit
:edit .git/objects/...

" Use Git as external diff/merge tool
:set diffopt+=vertical

Terminal Integration


-- Terminal commands for Git
vim.keymap.set('n', '<leader>gs', ':terminal git status<CR>', { desc = 'Git Status' })
vim.keymap.set('n', '<leader>gl', ':terminal git log --oneline --graph --all<CR>', { desc = 'Git Log' })
vim.keymap.set('n', '<leader>gd', ':terminal git diff<CR>', { desc = 'Git Diff' })


-- Floating terminal for Git commands
vim.keymap.set('n', '<leader>gg', function()
  local buf = vim.api.nvim_create_buf(false, true)
  local width = math.floor(vim.o.columns * 0.8)
  local height = math.floor(vim.o.lines * 0.8)
  
  local win = vim.api.nvim_open_win(buf, true, {
    relative = 'editor',
    width = width,
    height = height,
    col = math.floor((vim.o.columns - width) / 2),
    row = math.floor((vim.o.lines - height) / 2),
    style = 'minimal',
    border = 'rounded',
  })
  
  vim.fn.termopen('git status', {
    on_exit = function()
      vim.api.nvim_win_close(win, true)
    end
  })
  
  vim.cmd('startinsert')
end, { desc = 'Git Status (Float)' })

Using vim-fugitive

Installation and Setup


-- Using lazy.nvim
{
  'tpope/vim-fugitive',
  cmd = { 'G', 'Git', 'Gdiffsplit', 'Gread', 'Gwrite', 'Ggrep', 'GMove', 'GDelete', 'GBrowse' },
  keys = {
    { '<leader>gs', ':Git<CR>', desc = 'Git Status' },
    { '<leader>gc', ':Git commit<CR>', desc = 'Git Commit' },
    { '<leader>gp', ':Git push<CR>', desc = 'Git Push' },
    { '<leader>gP', ':Git pull<CR>', desc = 'Git Pull' },
    { '<leader>gb', ':Git blame<CR>', desc = 'Git Blame' },
    { '<leader>gd', ':Gdiffsplit<CR>', desc = 'Git Diff' },
    { '<leader>gw', ':Gwrite<CR>', desc = 'Git Write (Stage)' },
    { '<leader>gr', ':Gread<CR>', desc = 'Git Read (Checkout)' },
  },
}


-- Basic configuration
vim.g.fugitive_git_executable = 'git'

Essential Fugitive Commands

" Git status interface
:Git
" or
:G

" In status window:
" - s: Stage file
" - u: Unstage file
" - =: Toggle inline diff
" - cc: Commit
" - ca: Commit --amend
" - ce: Commit --amend --no-edit
" - cw: Commit --amend --only
" - dv: Diff in vertical split
" - O: Open file in new tab

" Diff current file
:Gdiffsplit
:Gdiffsplit HEAD~1    " Compare with previous commit
:Gdiffsplit main      " Compare with main branch

" In diff window:
" - do: Obtain from other buffer
" - dp: Put to other buffer
" - ]c: Next change
" - [c: Previous change

" Stage and unstage
:Gwrite    " Stage current file
:Gread     " Checkout current file (unstage changes)

" Commit
:Git commit
:Git commit -m "Message"
:Git commit --amend

" Blame
:Git blame
" In blame window:
" - o: Open commit
" - O: Open commit in new tab
" - -: Reblame at commit

" Browse on GitHub/GitLab
:GBrowse
:GBrowse main:%       " Browse file on main branch
:'<,'>GBrowse         " Browse selection (creates link)

Advanced Fugitive Workflows


-- Custom commands and mappings
vim.api.nvim_create_user_command('Gst', 'Git', {})
vim.api.nvim_create_user_command('Gco', 'Git checkout <args>', { nargs = '+' })
vim.api.nvim_create_user_command('Gbr', 'Git branch <args>', { nargs = '*' })
vim.api.nvim_create_user_command('Glog', 'Git log --oneline --graph --all', {})


-- Quick commit current file
vim.keymap.set('n', '<leader>gq', function()
  local file = vim.fn.expand('%')
  vim.cmd('Git add ' .. file)
  
  local msg = vim.fn.input('Commit message: ')
  if msg ~= '' then
    vim.cmd('Git commit -m "' .. msg .. '"')
  end
end, { desc = 'Quick Commit Current File' })


-- Show file history
vim.keymap.set('n', '<leader>gh', function()
  vim.cmd('Git log --follow -p -- ' .. vim.fn.expand('%'))
end, { desc = 'File History' })


-- Resolve merge conflicts
vim.keymap.set('n', '<leader>gm', function()

  -- Open 3-way merge
  vim.cmd('Gdiffsplit!')
end, { desc = 'Merge Conflict (3-way)' })


-- In 3-way merge:

-- Target branch (HEAD): //2

-- Merge branch: //3

-- Working copy: (no suffix)
vim.keymap.set('n', '<leader>gh', ':diffget //2<CR>', { desc = 'Get from HEAD' })
vim.keymap.set('n', '<leader>gl', ':diffget //3<CR>', { desc = 'Get from merge branch' })

Fugitive Integration with Telescope

{
  'nvim-telescope/telescope.nvim',
  dependencies = { 'tpope/vim-fugitive' },
  config = function()
    local builtin = require('telescope.builtin')
    

    -- Git commits
    vim.keymap.set('n', '<leader>gc', builtin.git_commits, { desc = 'Git Commits' })
    

    -- Buffer commits
    vim.keymap.set('n', '<leader>gbc', builtin.git_bcommits, { desc = 'Buffer Commits' })
    

    -- Git branches
    vim.keymap.set('n', '<leader>gB', builtin.git_branches, { desc = 'Git Branches' })
    

    -- Git status
    vim.keymap.set('n', '<leader>gS', builtin.git_status, { desc = 'Git Status (Telescope)' })
    

    -- Git stash
    vim.keymap.set('n', '<leader>gt', builtin.git_stash, { desc = 'Git Stash' })
  end,
}

Using gitsigns.nvim

Installation and Configuration

{
  'lewis6991/gitsigns.nvim',
  event = { 'BufReadPre', 'BufNewFile' },
  opts = {
    signs = {
      add          = { text = '│' },
      change       = { text = '│' },
      delete       = { text = '_' },
      topdelete    = { text = '‾' },
      changedelete = { text = '~' },
      untracked    = { text = '┆' },
    },
    signcolumn = true,  -- Toggle with `:Gitsigns toggle_signs`
    numhl      = false, -- Toggle with `:Gitsigns toggle_numhl`
    linehl     = false, -- Toggle with `:Gitsigns toggle_linehl`
    word_diff  = false, -- Toggle with `:Gitsigns toggle_word_diff`
    
    watch_gitdir = {
      interval = 1000,
      follow_files = true
    },
    
    attach_to_untracked = true,
    current_line_blame = false, -- Toggle with `:Gitsigns toggle_current_line_blame`
    current_line_blame_opts = {
      virt_text = true,
      virt_text_pos = 'eol', -- 'eol' | 'overlay' | 'right_align'
      delay = 1000,
      ignore_whitespace = false,
    },
    current_line_blame_formatter = '<author>, <author_time:%Y-%m-%d> - <summary>',
    
    sign_priority = 6,
    update_debounce = 100,
    status_formatter = nil, -- Use default
    max_file_length = 40000,
    
    preview_config = {

      -- Options passed to nvim_open_win
      border = 'rounded',
      style = 'minimal',
      relative = 'cursor',
      row = 0,
      col = 1
    },
    
    on_attach = function(bufnr)
      local gs = package.loaded.gitsigns
      
      local function map(mode, l, r, opts)
        opts = opts or {}
        opts.buffer = bufnr
        vim.keymap.set(mode, l, r, opts)
      end
      

      -- Navigation
      map('n', ']c', function()
        if vim.wo.diff then return ']c' end
        vim.schedule(function() gs.next_hunk() end)
        return '<Ignore>'
      end, { expr = true, desc = 'Next Hunk' })
      
      map('n', '[c', function()
        if vim.wo.diff then return '[c' end
        vim.schedule(function() gs.prev_hunk() end)
        return '<Ignore>'
      end, { expr = true, desc = 'Previous Hunk' })
      

      -- Actions
      map('n', '<leader>hs', gs.stage_hunk, { desc = 'Stage Hunk' })
      map('n', '<leader>hr', gs.reset_hunk, { desc = 'Reset Hunk' })
      map('v', '<leader>hs', function() 
        gs.stage_hunk { vim.fn.line('.'), vim.fn.line('v') } 
      end, { desc = 'Stage Hunk' })
      map('v', '<leader>hr', function() 
        gs.reset_hunk { vim.fn.line('.'), vim.fn.line('v') } 
      end, { desc = 'Reset Hunk' })
      
      map('n', '<leader>hS', gs.stage_buffer, { desc = 'Stage Buffer' })
      map('n', '<leader>hu', gs.undo_stage_hunk, { desc = 'Undo Stage Hunk' })
      map('n', '<leader>hR', gs.reset_buffer, { desc = 'Reset Buffer' })
      map('n', '<leader>hp', gs.preview_hunk, { desc = 'Preview Hunk' })
      
      map('n', '<leader>hb', function() 
        gs.blame_line { full = true } 
      end, { desc = 'Blame Line' })
      map('n', '<leader>tb', gs.toggle_current_line_blame, { desc = 'Toggle Blame' })
      
      map('n', '<leader>hd', gs.diffthis, { desc = 'Diff This' })
      map('n', '<leader>hD', function() 
        gs.diffthis('~') 
      end, { desc = 'Diff This ~' })
      
      map('n', '<leader>td', gs.toggle_deleted, { desc = 'Toggle Deleted' })
      

      -- Text object
      map({'o', 'x'}, 'ih', ':<C-U>Gitsigns select_hunk<CR>', { desc = 'Select Hunk' })
    end
  },
}

Gitsigns Commands and Features


-- Common Gitsigns commands
vim.api.nvim_create_user_command('GitsignsStatus', function()
  local gs = package.loaded.gitsigns
  local status = vim.b.gitsigns_status_dict
  
  if status then
    print(string.format('Git Status: +%d ~%d -%d', 
      status.added or 0, 
      status.changed or 0, 
      status.removed or 0
    ))
  else
    print('Not in a git repository')
  end
end, {})


-- Custom statusline integration
local function git_status()
  local status = vim.b.gitsigns_status_dict
  if not status then return '' end
  
  local parts = {}
  if status.added and status.added > 0 then
    table.insert(parts, '+' .. status.added)
  end
  if status.changed and status.changed > 0 then
    table.insert(parts, '~' .. status.changed)
  end
  if status.removed and status.removed > 0 then
    table.insert(parts, '-' .. status.removed)
  end
  
  if #parts > 0 then
    return ' [' .. table.concat(parts, ' ') .. ']'
  end
  return ''
end


-- Add to statusline
vim.opt.statusline = table.concat({
  '%f',  -- filename
  '%{luaeval("require(\'gitsigns\').get_status()")}',
  '%m',  -- modified flag
  '%=',  -- right align
  '%l,%c',  -- line, column
}, ' ')

Using neogit

Installation and Configuration

{
  'TimUntersberger/neogit',
  dependencies = {
    'nvim-lua/plenary.nvim',
    'sindrets/diffview.nvim',  -- Optional
    'nvim-telescope/telescope.nvim',  -- Optional
  },
  cmd = 'Neogit',
  keys = {
    { '<leader>gn', ':Neogit<CR>', desc = 'Neogit' },
    { '<leader>gc', ':Neogit commit<CR>', desc = 'Neogit Commit' },
  },
  opts = {
    disable_signs = false,
    disable_hint = false,
    disable_context_highlighting = false,
    disable_commit_confirmation = false,
    

    -- Customize signs
    signs = {
      section = { '', '' },  -- or { '>', 'v' }
      item = { '', '' },
      hunk = { '', '' },
    },
    
    integrations = {
      diffview = true,
      telescope = true,
    },
    

    -- Sections to show
    sections = {
      untracked = {
        folded = false
      },
      unstaged = {
        folded = false
      },
      staged = {
        folded = false
      },
      stashes = {
        folded = true
      },
      unpulled = {
        folded = true
      },
      unmerged = {
        folded = false
      },
      recent = {
        folded = true
      },
    },
    

    -- Popup mappings
    mappings = {
      status = {
        ['q'] = 'Close',
        ['I'] = 'InitRepo',
        ['1'] = 'Depth1',
        ['2'] = 'Depth2',
        ['3'] = 'Depth3',
        ['4'] = 'Depth4',
        ['<tab>'] = 'Toggle',
        ['x'] = 'Discard',
        ['s'] = 'Stage',
        ['S'] = 'StageUnstaged',
        ['<c-s>'] = 'StageAll',
        ['u'] = 'Unstage',
        ['U'] = 'UnstageStaged',
        ['d'] = 'DiffAtFile',
        ['$'] = 'CommandHistory',
        ['<c-r>'] = 'RefreshBuffer',
        ['<enter>'] = 'GoToFile',
        ['<c-v>'] = 'VSplitOpen',
        ['<c-x>'] = 'SplitOpen',
        ['<c-t>'] = 'TabOpen',
        ['{'] = 'GoToPreviousHunkHeader',
        ['}'] = 'GoToNextHunkHeader',
      }
    },
    

    -- Auto refresh
    auto_refresh = true,
    

    -- Disable line numbers in neogit buffer
    disable_line_numbers = true,
    

    -- Commit editor configuration
    commit_editor = {
      kind = 'split',  -- 'split', 'vsplit', 'split_above', 'tab', 'floating'
    },
    

    -- Console output
    console_timeout = 2000,
    auto_show_console = true,
  },
}

Neogit Workflows


-- Quick commit workflow
vim.keymap.set('n', '<leader>gcc', function()
  require('neogit').open({ 'commit' })
end, { desc = 'Git Commit' })


-- Push workflow
vim.keymap.set('n', '<leader>gpp', function()
  require('neogit').open({ 'push' })
end, { desc = 'Git Push' })


-- Pull workflow
vim.keymap.set('n', '<leader>gPP', function()
  require('neogit').open({ 'pull' })
end, { desc = 'Git Pull' })


-- Log workflow
vim.keymap.set('n', '<leader>gll', function()
  require('neogit').open({ 'log' })
end, { desc = 'Git Log' })


-- Stash workflow
vim.keymap.set('n', '<leader>gss', function()
  require('neogit').open({ 'stash' })
end, { desc = 'Git Stash' })

Using diffview.nvim

Installation and Configuration

{
  'sindrets/diffview.nvim',
  dependencies = 'nvim-lua/plenary.nvim',
  cmd = { 'DiffviewOpen', 'DiffviewClose', 'DiffviewToggleFiles', 'DiffviewFocusFiles', 'DiffviewFileHistory' },
  keys = {
    { '<leader>gdo', ':DiffviewOpen<CR>', desc = 'Diffview Open' },
    { '<leader>gdc', ':DiffviewClose<CR>', desc = 'Diffview Close' },
    { '<leader>gdh', ':DiffviewFileHistory %<CR>', desc = 'File History' },
    { '<leader>gdH', ':DiffviewFileHistory<CR>', desc = 'Full History' },
  },
  opts = {
    diff_binaries = false,
    enhanced_diff_hl = true,
    git_cmd = { 'git' },
    use_icons = true,
    
    icons = {
      folder_closed = '',
      folder_open = '',
    },
    
    signs = {
      fold_closed = '',
      fold_open = '',
    },
    
    view = {
      default = {
        layout = 'diff2_horizontal',
        winbar_info = false,
      },
      merge_tool = {
        layout = 'diff3_horizontal',
        disable_diagnostics = true,
      },
      file_history = {
        layout = 'diff2_horizontal',
        winbar_info = false,
      },
    },
    
    file_panel = {
      listing_style = 'tree',  -- 'list' or 'tree'
      tree_options = {
        flatten_dirs = true,
        folder_statuses = 'only_folded',
      },
      win_config = {
        position = 'left',
        width = 35,
      },
    },
    
    file_history_panel = {
      log_options = {
        git = {
          single_file = {
            diff_merges = 'combined',
          },
          multi_file = {
            diff_merges = 'first-parent',
          },
        },
      },
      win_config = {
        position = 'bottom',
        height = 16,
      },
    },
    
    commit_log_panel = {
      win_config = {},
    },
    
    default_args = {
      DiffviewOpen = {},
      DiffviewFileHistory = {},
    },
    
    hooks = {
      diff_buf_read = function(bufnr)

        -- Set local options for diff buffers
        vim.opt_local.wrap = false
        vim.opt_local.list = false
        vim.opt_local.colorcolumn = ''
      end,
      
      diff_buf_win_enter = function(bufnr, winid, ctx)

        -- Highlight the line in diff view when entering
        if ctx.layout_name:match('^diff2') then
          if ctx.symbol == 'a' then
            vim.opt_local.winhl = table.concat({
              'DiffAdd:DiffviewDiffAddAsDelete',
              'DiffDelete:DiffviewDiffDelete',
            }, ',')
          elseif ctx.symbol == 'b' then
            vim.opt_local.winhl = table.concat({
              'DiffDelete:DiffviewDiffDelete',
            }, ',')
          end
        end
      end,
    },
    
    keymaps = {
      disable_defaults = false,
      view = {
        ['<tab>']      = require('diffview.actions').select_next_entry,
        ['<s-tab>']    = require('diffview.actions').select_prev_entry,
        ['gf']         = require('diffview.actions').goto_file,
        ['<C-w><C-f>'] = require('diffview.actions').goto_file_split,
        ['<C-w>gf']    = require('diffview.actions').goto_file_tab,
        ['<leader>e']  = require('diffview.actions').focus_files,
        ['<leader>b']  = require('diffview.actions').toggle_files,
      },
      file_panel = {
        ['j']          = require('diffview.actions').next_entry,
        ['k']          = require('diffview.actions').prev_entry,
        ['<cr>']       = require('diffview.actions').select_entry,
        ['o']          = require('diffview.actions').select_entry,
        ['<2-LeftMouse>'] = require('diffview.actions').select_entry,
        ['-']          = require('diffview.actions').toggle_stage_entry,
        ['S']          = require('diffview.actions').stage_all,
        ['U']          = require('diffview.actions').unstage_all,
        ['X']          = require('diffview.actions').restore_entry,
        ['R']          = require('diffview.actions').refresh_files,
        ['<tab>']      = require('diffview.actions').select_next_entry,
        ['<s-tab>']    = require('diffview.actions').select_prev_entry,
        ['gf']         = require('diffview.actions').goto_file,
        ['<C-w><C-f>'] = require('diffview.actions').goto_file_split,
        ['<C-w>gf']    = require('diffview.actions').goto_file_tab,
        ['i']          = require('diffview.actions').listing_style,
        ['f']          = require('diffview.actions').toggle_flatten_dirs,
        ['<leader>e']  = require('diffview.actions').focus_files,
        ['<leader>b']  = require('diffview.actions').toggle_files,
      },
      file_history_panel = {
        ['g!']         = require('diffview.actions').options,
        ['<C-A-d>']    = require('diffview.actions').open_in_diffview,
        ['y']          = require('diffview.actions').copy_hash,
        ['zR']         = require('diffview.actions').open_all_folds,
        ['zM']         = require('diffview.actions').close_all_folds,
        ['j']          = require('diffview.actions').next_entry,
        ['k']          = require('diffview.actions').prev_entry,
        ['<cr>']       = require('diffview.actions').select_entry,
        ['o']          = require('diffview.actions').select_entry,
        ['<2-LeftMouse>'] = require('diffview.actions').select_entry,
        ['<tab>']      = require('diffview.actions').select_next_entry,
        ['<s-tab>']    = require('diffview.actions').select_prev_entry,
        ['gf']         = require('diffview.actions').goto_file,
        ['<C-w><C-f>'] = require('diffview.actions').goto_file_split,
        ['<C-w>gf']    = require('diffview.actions').goto_file_tab,
        ['<leader>e']  = require('diffview.actions').focus_files,
        ['<leader>b']  = require('diffview.actions').toggle_files,
      },
      option_panel = {
        ['<tab>'] = require('diffview.actions').select_entry,
        ['q']     = require('diffview.actions').close,
      },
    },
  },
}

Diffview Usage

" Open diff view
:DiffviewOpen
:DiffviewOpen HEAD~2           " Compare with 2 commits ago
:DiffviewOpen HEAD~4..HEAD~2   " Compare commit range
:DiffviewOpen origin/main...HEAD  " Compare with origin/main

" File history
:DiffviewFileHistory           " All commits affecting current file
:DiffviewFileHistory %         " Current file history
:DiffviewFileHistory --range=origin/main...HEAD  " Specific range

" Close diff view
:DiffviewClose

" Toggle file panel
:DiffviewToggleFiles

" Focus file panel
:DiffviewFocusFiles

" Refresh
:DiffviewRefresh

Advanced Diffview Workflows


-- Compare branches
vim.keymap.set('n', '<leader>gdb', function()
  local branch = vim.fn.input('Branch to compare: ')
  if branch ~= '' then
    vim.cmd('DiffviewOpen ' .. branch)
  end
end, { desc = 'Compare Branch' })


-- Compare with remote
vim.keymap.set('n', '<leader>gdr', function()
  vim.cmd('DiffviewOpen origin/main...HEAD')
end, { desc = 'Compare with Remote' })


-- View merge conflicts
vim.keymap.set('n', '<leader>gdm', function()
  vim.cmd('DiffviewOpen')
end, { desc = 'View Merge Conflicts' })


-- File history with filters
vim.keymap.set('n', '<leader>gdf', function()
  local author = vim.fn.input('Author (optional): ')
  local cmd = 'DiffviewFileHistory'
  if author ~= '' then
    cmd = cmd .. ' --author=' .. author
  end
  vim.cmd(cmd)
end, { desc = 'Filtered File History' })

Custom Git Integration

Simple Git Commands


-- Create custom git commands
local function git_exec(cmd, show_output)
  local output = vim.fn.system('git ' .. cmd)
  local exit_code = vim.v.shell_error
  
  if exit_code ~= 0 then
    vim.notify('Git error: ' .. output, vim.log.levels.ERROR)
    return nil
  end
  
  if show_output then
    vim.notify(output, vim.log.levels.INFO)
  end
  
  return output
end


-- Git add current file
vim.keymap.set('n', '<leader>ga', function()
  local file = vim.fn.expand('%')
  git_exec('add ' .. file, true)
end, { desc = 'Git Add' })


-- Git reset current file
vim.keymap.set('n', '<leader>gR', function()
  local file = vim.fn.expand('%')
  local confirm = vim.fn.confirm('Reset ' .. file .. '?', '&Yes\n&No', 2)
  if confirm == 1 then
    git_exec('checkout -- ' .. file, true)
    vim.cmd('edit!')
  end
end, { desc = 'Git Reset File' })


-- Quick commit
vim.keymap.set('n', '<leader>gC', function()
  local msg = vim.fn.input('Commit message: ')
  if msg ~= '' then
    git_exec('commit -m "' .. msg .. '"', true)
  end
end, { desc = 'Quick Commit' })


-- Git log for current file
vim.keymap.set('n', '<leader>gL', function()
  local file = vim.fn.expand('%')
  local log = git_exec('log --oneline --follow ' .. file, false)
  
  if log then
    local lines = vim.split(log, '\n')
    vim.ui.select(lines, {
      prompt = 'Select commit to view:',
    }, function(choice)
      if choice then
        local commit = vim.split(choice, ' ')[1]
        vim.cmd('DiffviewOpen ' .. commit .. '^..' .. commit)
      end
    end)
  end
end, { desc = 'File Log' })

Git Status in Floating Window

local function git_status_float()
  local status = vim.fn.system('git status')
  

  -- Create buffer
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(status, '\n'))
  

  -- Set filetype for syntax highlighting
  vim.api.nvim_buf_set_option(buf, 'filetype', 'git')
  

  -- Calculate window size
  local width = math.min(80, vim.o.columns - 4)
  local height = math.min(20, vim.o.lines - 4)
  

  -- Open floating window
  local win = vim.api.nvim_open_win(buf, true, {
    relative = 'editor',
    width = width,
    height = height,
    col = math.floor((vim.o.columns - width) / 2),
    row = math.floor((vim.o.lines - height) / 2),
    style = 'minimal',
    border = 'rounded',
    title = ' Git Status ',
    title_pos = 'center',
  })
  

  -- Set buffer options
  vim.api.nvim_buf_set_option(buf, 'modifiable', false)
  vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe')
  

  -- Close on q or Esc
  vim.keymap.set('n', 'q', ':close<CR>', { buffer = buf, silent = true })
  vim.keymap.set('n', '<Esc>', ':close<CR>', { buffer = buf, silent = true })
end

vim.keymap.set('n', '<leader>gf', git_status_float, { desc = 'Git Status (Float)' })

Git Branch Switcher

local function git_branch_switcher()

  -- Get branches
  local branches = vim.fn.systemlist('git branch --all')
  

  -- Clean up branch names
  local branch_list = {}
  for _, branch in ipairs(branches) do
    local clean = branch:gsub('^[* ] ', ''):gsub('^remotes/', '')
    table.insert(branch_list, clean)
  end
  

  -- Show picker
  vim.ui.select(branch_list, {
    prompt = 'Select branch:',
    format_item = function(item)
      return item
    end,
  }, function(choice)
    if choice then

      -- Remove remote prefix if exists
      local branch = choice:gsub('^origin/', '')
      

      -- Checkout branch
      local result = vim.fn.system('git checkout ' .. branch)
      
      if vim.v.shell_error == 0 then
        vim.notify('Switched to branch: ' .. branch, vim.log.levels.INFO)

        -- Reload buffers
        vim.cmd('checktime')
      else
        vim.notify('Failed to switch branch: ' .. result, vim.log.levels.ERROR)
      end
    end
  end)
end

vim.keymap.set('n', '<leader>gbb', git_branch_switcher, { desc = 'Switch Branch' })

Git Commit Browser

local function git_commit_browser()
  local commits = vim.fn.systemlist('git log --oneline -n 100')
  
  vim.ui.select(commits, {
    prompt = 'Select commit:',
  }, function(choice)
    if choice then
      local commit = vim.split(choice, ' ')[1]
      

      -- Show options
      vim.ui.select({
        'View diff',
        'View files',
        'Checkout',
        'Cherry-pick',
        'Show details',
      }, {
        prompt = 'Action:',
      }, function(action)
        if action == 'View diff' then
          vim.cmd('DiffviewOpen ' .. commit .. '^..' .. commit)
        elseif action == 'View files' then
          local files = vim.fn.systemlist('git show --name-only --format="" ' .. commit)
          vim.notify('Files changed:\n' .. table.concat(files, '\n'), vim.log.levels.INFO)
        elseif action == 'Checkout' then
          vim.cmd('Git checkout ' .. commit)
        elseif action == 'Cherry-pick' then
          vim.cmd('Git cherry-pick ' .. commit)
        elseif action == 'Show details' then
          local details = vim.fn.system('git show ' .. commit)
          
          local buf = vim.api.nvim_create_buf(false, true)
          vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(details, '\n'))
          vim.api.nvim_buf_set_option(buf, 'filetype', 'git')
          
          vim.cmd('vsplit')
          vim.api.nvim_win_set_buf(0, buf)
        end
      end)
    end
  end)
end

vim.keymap.set('n', '<leader>gCB', git_commit_browser, { desc = 'Commit Browser' })

Git Worktree Management

Using git-worktree.nvim

{
  'ThePrimeagen/git-worktree.nvim',
  dependencies = {
    'nvim-telescope/telescope.nvim',
    'nvim-lua/plenary.nvim',
  },
  config = function()
    require('git-worktree').setup({
      change_directory_command = 'cd',
      update_on_change = true,
      update_on_change_command = 'e .',
      clearjumps_on_change = true,
      autopush = false,
    })
    

    -- Telescope integration
    require('telescope').load_extension('git_worktree')
    

    -- Keymaps
    vim.keymap.set('n', '<leader>gwt', 
      require('telescope').extensions.git_worktree.git_worktrees,
      { desc = 'Git Worktrees' }
    )
    
    vim.keymap.set('n', '<leader>gwc',
      require('telescope').extensions.git_worktree.create_git_worktree,
      { desc = 'Create Worktree' }
    )
    

    -- Hooks
    local worktree = require('git-worktree')
    
    worktree.on_tree_change(function(op, metadata)
      if op == worktree.Operations.Switch then
        print('Switched to ' .. metadata.path)
      elseif op == worktree.Operations.Create then
        print('Created worktree at ' .. metadata.path)
      elseif op == worktree.Operations.Delete then
        print('Deleted worktree at ' .. metadata.path)
      end
    end)
  end,
}

Manual Worktree Commands


-- Create worktree
vim.api.nvim_create_user_command('WorktreeCreate', function(opts)
  local branch = opts.fargs[1]
  local path = opts.fargs[2] or ('../' .. branch)
  
  local cmd = string.format('git worktree add %s %s', path, branch)
  local output = vim.fn.system(cmd)
  
  if vim.v.shell_error == 0 then
    vim.notify('Created worktree: ' .. path, vim.log.levels.INFO)
    

    -- Optionally switch to it
    local confirm = vim.fn.confirm('Switch to new worktree?', '&Yes\n&No', 1)
    if confirm == 1 then
      vim.cmd('cd ' .. path)
      vim.cmd('edit .')
    end
  else
    vim.notify('Failed: ' .. output, vim.log.levels.ERROR)
  end
end, { nargs = '+' })


-- List worktrees
vim.api.nvim_create_user_command('WorktreeList', function()
  local output = vim.fn.system('git worktree list')
  vim.notify(output, vim.log.levels.INFO)
end, {})


-- Remove worktree
vim.api.nvim_create_user_command('WorktreeRemove', function(opts)
  local path = opts.fargs[1]
  local cmd = 'git worktree remove ' .. path
  
  local confirm = vim.fn.confirm('Remove worktree: ' .. path .. '?', '&Yes\n&No', 2)
  if confirm == 1 then
    local output = vim.fn.system(cmd)
    if vim.v.shell_error == 0 then
      vim.notify('Removed worktree: ' .. path, vim.log.levels.INFO)
    else
      vim.notify('Failed: ' .. output, vim.log.levels.ERROR)
    end
  end
end, { nargs = 1 })

Git Conflict Resolution

Conflict Markers and Navigation


-- Highlight conflict markers
vim.api.nvim_create_autocmd('BufRead', {
  pattern = '*',
  callback = function()

    -- Check if file has conflict markers
    local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
    local has_conflicts = false
    
    for _, line in ipairs(lines) do
      if line:match('^<<<<<<<') or line:match('^=======') or line:match('^>>>>>>>') then
        has_conflicts = true
        break
      end
    end
    
    if has_conflicts then

      -- Set up conflict navigation
      vim.keymap.set('n', ']x', function()
        vim.fn.search('^<<<<<<< ', 'W')
      end, { buffer = true, desc = 'Next Conflict' })
      
      vim.keymap.set('n', '[x', function()
        vim.fn.search('^<<<<<<< ', 'bW')
      end, { buffer = true, desc = 'Previous Conflict' })
      

      -- Quick resolution
      vim.keymap.set('n', '<leader>co', function()

        -- Take ours (current branch)
        local line = vim.fn.line('.')
        vim.cmd(line .. ',/^=======/-1d')
        vim.cmd('/^>>>>>>>/d')
      end, { buffer = true, desc = 'Conflict: Ours' })
      
      vim.keymap.set('n', '<leader>ct', function()

        -- Take theirs (incoming branch)
        vim.cmd('/^<<<<<<</,/^=======/d')
        vim.cmd('/^>>>>>>>/d')
      end, { buffer = true, desc = 'Conflict: Theirs' })
      
      vim.keymap.set('n', '<leader>cb', function()

        -- Take both
        vim.cmd('/^<<<<<<</d')
        vim.cmd('/^=======/d')
        vim.cmd('/^>>>>>>>/d')
      end, { buffer = true, desc = 'Conflict: Both' })
      
      vim.notify('Conflict markers detected. Use ]x/[x to navigate, <leader>co/ct/cb to resolve', 
        vim.log.levels.WARN)
    end
  end,
})

Three-Way Merge Helper

local function setup_merge_tool()

  -- Detect if in merge conflict
  local output = vim.fn.system('git diff --check')
  if output:match('conflict') then

    -- Open three-way diff
    vim.cmd('Gdiffsplit!')
    

    -- Helpful keymaps in merge mode
    vim.keymap.set('n', '<leader>gh', ':diffget //2<CR>', { desc = 'Get from HEAD' })
    vim.keymap.set('n', '<leader>gt', ':diffget //3<CR>', { desc = 'Get from MERGE_HEAD' })
    vim.keymap.set('n', '<leader>gn', ']c', { desc = 'Next conflict', remap = true })
    vim.keymap.set('n', '<leader>gp', '[c', { desc = 'Previous conflict', remap = true })
  end
end

vim.keymap.set('n', '<leader>gM', setup_merge_tool, { desc = 'Setup Merge Tool' })

Advanced Git Workflows

Staging Hunks Interactively


-- Interactive hunk staging (requires gitsigns.nvim)
vim.keymap.set('n', '<leader>hv', function()
  local gs = package.loaded.gitsigns
  

  -- Preview hunk
  gs.preview_hunk()
  

  -- Wait for user input
  vim.defer_fn(function()
    local choice = vim.fn.input('Stage this hunk? (y/n/q): ')
    
    if choice == 'y' then
      gs.stage_hunk()

      -- Move to next hunk and repeat
      vim.schedule(function()
        gs.next_hunk()
        vim.cmd('normal! zz')

        -- Could recursively call this function
      end)
    elseif choice == 'n' then

      -- Skip to next hunk
      gs.next_hunk()
      vim.cmd('normal! zz')
    end

    -- 'q' quits
  end, 100)
end, { desc = 'Interactive Hunk Staging' })

Commit Message Templates


-- Set up commit message template
vim.api.nvim_create_autocmd('BufRead', {
  pattern = 'COMMIT_EDITMSG',
  callback = function()

    -- Insert template if buffer is empty
    local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
    local is_empty = #lines == 1 and lines[1] == ''
    
    if is_empty then
      local template = {
        '',
        '# Type: feat|fix|docs|style|refactor|test|chore',
        '# Scope: component|module',
        '# ',
        '# Subject: imperative mood, lowercase, no period',
        '# ',
        '# Body: explain what and why (optional)',
        '# ',
        '# Footer: breaking changes, issues closed (optional)',
      }
      
      vim.api.nvim_buf_set_lines(0, 0, 0, false, template)
      vim.api.nvim_win_set_cursor(0, {1, 0})
    end
    

    -- Enable spell checking
    vim.opt_local.spell = true
    vim.opt_local.spelllang = 'en_us'
    

    -- Set textwidth
    vim.opt_local.textwidth = 72
    vim.opt_local.colorcolumn = '50,72'
  end,
})

Git Stash Manager

local function git_stash_manager()
  local stashes = vim.fn.systemlist('git stash list')
  
  if #stashes == 0 then
    vim.notify('No stashes found', vim.log.levels.INFO)
    return
  end
  
  vim.ui.select(stashes, {
    prompt = 'Select stash:',
  }, function(choice)
    if choice then
      local stash_id = vim.split(choice, ':')[1]
      
      vim.ui.select({
        'Show',
        'Apply',
        'Pop',
        'Drop',
      }, {
        prompt = 'Action:',
      }, function(action)
        local cmd
        
        if action == 'Show' then
          local content = vim.fn.system('git stash show -p ' .. stash_id)
          
          local buf = vim.api.nvim_create_buf(false, true)
          vim.api.nvim_buf_set_lines(buf, 0, -1, false, vim.split(content, '\n'))
          vim.api.nvim_buf_set_option(buf, 'filetype', 'diff')
          
          vim.cmd('vsplit')
          vim.api.nvim_win_set_buf(0, buf)
          
        elseif action == 'Apply' then
          cmd = 'git stash apply ' .. stash_id
        elseif action == 'Pop' then
          cmd = 'git stash pop ' .. stash_id
        elseif action == 'Drop' then
          cmd = 'git stash drop ' .. stash_id
        end
        
        if cmd then
          local output = vim.fn.system(cmd)
          if vim.v.shell_error == 0 then
            vim.notify('Stash ' .. action:lower() .. 'ed successfully', vim.log.levels.INFO)
            vim.cmd('checktime')
          else
            vim.notify('Failed: ' .. output, vim.log.levels.ERROR)
          end
        end
      end)
    end
  end)
end

vim.keymap.set('n', '<leader>gst', git_stash_manager, { desc = 'Stash Manager' })

Performance Optimization

Lazy Loading Git Plugins


-- Lazy load git plugins only in git repositories
local function is_git_repo()
  local handle = io.popen('git rev-parse --is-inside-work-tree 2>/dev/null')
  if handle then
    local result = handle:read('*a')
    handle:close()
    return result:match('true')
  end
  return false
end


-- Conditional loading
{
  'lewis6991/gitsigns.nvim',
  cond = is_git_repo,
  event = { 'BufReadPre', 'BufNewFile' },
}


-- Or use event-based loading
{
  'tpope/vim-fugitive',
  cmd = { 'G', 'Git' },
  ft = { 'gitcommit', 'gitrebase' },
}

Optimizing Git Operations


-- Cache git root for faster operations
local git_root_cache = {}

local function get_git_root(path)
  if git_root_cache[path] then
    return git_root_cache[path]
  end
  
  local handle = io.popen('git -C ' .. path .. ' rev-parse --show-toplevel 2>/dev/null')
  if handle then
    local root = handle:read('*l')
    handle:close()
    
    if root and root ~= '' then
      git_root_cache[path] = root
      return root
    end
  end
  
  return nil
end


-- Use cached git root
vim.api.nvim_create_user_command('GitRoot', function()
  local root = get_git_root(vim.fn.getcwd())
  if root then
    vim.notify('Git root: ' .. root, vim.log.levels.INFO)
  else
    vim.notify('Not in a git repository', vim.log.levels.WARN)
  end
end, {})

Summary: Chapter #32 – Git Integration

This chapter covered comprehensive Git integration in Neovim:

  1. Built-in Features – Using Vim’s native Git support and terminal integration

  2. vim-fugitive – The classic Git wrapper with status, diff, blame, and merge capabilities

  3. gitsigns.nvim – Modern Git signs, hunk operations, and inline blame

  4. neogit – A Magit-inspired Git client for Neovim

  5. diffview.nvim – Advanced diff viewing and file history

  6. Custom Integration – Building custom Git commands and workflows

  7. Worktree Management – Managing Git worktrees efficiently

  8. Conflict Resolution – Tools and strategies for resolving merge conflicts

  9. Advanced Workflows – Interactive staging, commit templates, and stash management

  10. Performance – Lazy loading and optimization techniques

With these tools and configurations, you can perform nearly all Git operations without leaving Neovim, creating a seamless and efficient development workflow.


Chapter #33: File Navigation and Fuzzy Finders

Modern file navigation is essential for productive Neovim workflows. This chapter explores built-in navigation features, fuzzy finders, and file explorer plugins.

Built-in File Navigation

Native Netrw

Neovim includes netrw, a built-in file explorer.

" Open netrw
:Explore     " Open in current window
:Sexplore    " Open in horizontal split
:Vexplore    " Open in vertical split
:Texplore    " Open in new tab

" Navigate to specific directory
:Explore ~/projects
:Explore %:h    " Directory of current file

" Netrw browser commands (in netrw window):
" <CR>  - Open file/directory
" -     - Go up one directory
" d     - Create directory
" %     - Create new file
" D     - Delete file/directory
" R     - Rename file
" s     - Change sort order
" i     - Cycle through view modes
" gh    - Toggle hidden files
" qf    - Display file info
" v     - Open in vertical split
" t     - Open in new tab

Netrw Configuration


-- Disable netrw for plugin file explorers
vim.g.loaded_netrw = 1
vim.g.loaded_netrwPlugin = 1


-- Or configure netrw
vim.g.netrw_banner = 0           -- Hide banner
vim.g.netrw_liststyle = 3        -- Tree view
vim.g.netrw_browse_split = 4     -- Open in previous window
vim.g.netrw_altv = 1             -- Open splits to the right
vim.g.netrw_winsize = 25         -- Width percentage
vim.g.netrw_list_hide = '.git,.DS_Store'


-- Custom netrw mappings
vim.api.nvim_create_autocmd('FileType', {
  pattern = 'netrw',
  callback = function()
    local map = function(lhs, rhs)
      vim.keymap.set('n', lhs, rhs, { buffer = true, remap = true })
    end
    
    map('h', '-')           -- Go up with h
    map('l', '<CR>')        -- Open with l
    map('.', 'gh')          -- Toggle hidden files
  end,
})

Path and Find Commands

" Set path for file searching
:set path+=**              " Search recursively in current directory
:set path+=/usr/include    " Add specific directories

" Find files
:find filename.txt         " Find and open file
:find **/*filename*        " Fuzzy find with wildcards
:find *.lua                " Find by pattern

" Tab completion for find
:find conf<Tab>            " Cycle through matches

" Navigate to file under cursor
gf                         " Go to file
<C-w>gf                    " Open in new tab
<C-w>f                     " Open in new split

" Set suffixes for gf to find files
:set suffixesadd+=.lua,.js,.py

Buffer Navigation


-- Enhanced buffer navigation
vim.keymap.set('n', '<leader>bb', ':buffers<CR>:buffer<Space>', { desc = 'Buffer List' })
vim.keymap.set('n', '<leader>bn', ':bnext<CR>', { desc = 'Next Buffer' })
vim.keymap.set('n', '<leader>bp', ':bprevious<CR>', { desc = 'Previous Buffer' })
vim.keymap.set('n', '<leader>bd', ':bdelete<CR>', { desc = 'Delete Buffer' })
vim.keymap.set('n', '<leader>bw', ':bwipeout<CR>', { desc = 'Wipeout Buffer' })


-- Delete other buffers
vim.api.nvim_create_user_command('BufOnly', function()
  local current = vim.api.nvim_get_current_buf()
  local buffers = vim.api.nvim_list_bufs()
  
  for _, buf in ipairs(buffers) do
    if buf ~= current and vim.api.nvim_buf_is_loaded(buf) then
      vim.api.nvim_buf_delete(buf, { force = false })
    end
  end
end, {})

vim.keymap.set('n', '<leader>bo', ':BufOnly<CR>', { desc = 'Delete Other Buffers' })

Telescope.nvim - The Modern Fuzzy Finder

Installation and Basic Setup

{
  'nvim-telescope/telescope.nvim',
  branch = '0.1.x',
  dependencies = {
    'nvim-lua/plenary.nvim',
    { 
      'nvim-telescope/telescope-fzf-native.nvim', 
      build = 'make',
      cond = function()
        return vim.fn.executable('make') == 1
      end,
    },
    'nvim-tree/nvim-web-devicons',
  },
  config = function()
    local telescope = require('telescope')
    local actions = require('telescope.actions')
    local builtin = require('telescope.builtin')
    
    telescope.setup({
      defaults = {
        prompt_prefix = '🔍 ',
        selection_caret = '➤ ',
        path_display = { 'truncate' },
        
        mappings = {
          i = {
            ['<C-n>'] = actions.move_selection_next,
            ['<C-p>'] = actions.move_selection_previous,
            ['<C-c>'] = actions.close,
            ['<Down>'] = actions.move_selection_next,
            ['<Up>'] = actions.move_selection_previous,
            ['<CR>'] = actions.select_default,
            ['<C-x>'] = actions.select_horizontal,
            ['<C-v>'] = actions.select_vertical,
            ['<C-t>'] = actions.select_tab,
            ['<C-u>'] = actions.preview_scrolling_up,
            ['<C-d>'] = actions.preview_scrolling_down,
            ['<PageUp>'] = actions.results_scrolling_up,
            ['<PageDown>'] = actions.results_scrolling_down,
            ['<Tab>'] = actions.toggle_selection + actions.move_selection_worse,
            ['<S-Tab>'] = actions.toggle_selection + actions.move_selection_better,
            ['<C-q>'] = actions.send_to_qflist + actions.open_qflist,
            ['<M-q>'] = actions.send_selected_to_qflist + actions.open_qflist,
            ['<C-l>'] = actions.complete_tag,
            ['<C-_>'] = actions.which_key, -- keys from pressing <C-/>
          },
          n = {
            ['<esc>'] = actions.close,
            ['<CR>'] = actions.select_default,
            ['<C-x>'] = actions.select_horizontal,
            ['<C-v>'] = actions.select_vertical,
            ['<C-t>'] = actions.select_tab,
            ['<Tab>'] = actions.toggle_selection + actions.move_selection_worse,
            ['<S-Tab>'] = actions.toggle_selection + actions.move_selection_better,
            ['<C-q>'] = actions.send_to_qflist + actions.open_qflist,
            ['<M-q>'] = actions.send_selected_to_qflist + actions.open_qflist,
            ['j'] = actions.move_selection_next,
            ['k'] = actions.move_selection_previous,
            ['H'] = actions.move_to_top,
            ['M'] = actions.move_to_middle,
            ['L'] = actions.move_to_bottom,
            ['<Down>'] = actions.move_selection_next,
            ['<Up>'] = actions.move_selection_previous,
            ['gg'] = actions.move_to_top,
            ['G'] = actions.move_to_bottom,
            ['<C-u>'] = actions.preview_scrolling_up,
            ['<C-d>'] = actions.preview_scrolling_down,
            ['<PageUp>'] = actions.results_scrolling_up,
            ['<PageDown>'] = actions.results_scrolling_down,
            ['?'] = actions.which_key,
          },
        },
        
        file_ignore_patterns = {
          'node_modules',
          '.git/',
          'dist/',
          'build/',
          '%.jpg',
          '%.jpeg',
          '%.png',
          '%.svg',
          '%.otf',
          '%.ttf',
        },
        

        -- Sorting
        sorting_strategy = 'ascending',
        layout_strategy = 'horizontal',
        layout_config = {
          horizontal = {
            prompt_position = 'top',
            preview_width = 0.55,
            results_width = 0.8,
          },
          vertical = {
            mirror = false,
          },
          width = 0.87,
          height = 0.80,
          preview_cutoff = 120,
        },
        

        -- Border
        border = true,
        borderchars = { '─', '│', '─', '│', '╭', '╮', '╯', '╰' },
        

        -- Preview
        preview = {
          treesitter = true,
        },
        

        -- History
        history = {
          path = vim.fn.stdpath('data') .. '/telescope_history.sqlite3',
          limit = 100,
        },
      },
      
      pickers = {
        find_files = {
          theme = 'dropdown',
          previewer = false,
          hidden = true,
        },
        git_files = {
          theme = 'dropdown',
          previewer = false,
        },
        buffers = {
          theme = 'dropdown',
          previewer = false,
          sort_lastused = true,
          mappings = {
            i = {
              ['<C-d>'] = actions.delete_buffer,
            },
            n = {
              ['dd'] = actions.delete_buffer,
            },
          },
        },
        oldfiles = {
          theme = 'dropdown',
          previewer = false,
        },
        live_grep = {
          additional_args = function()
            return { '--hidden' }
          end,
        },
        grep_string = {
          additional_args = function()
            return { '--hidden' }
          end,
        },
      },
      
      extensions = {
        fzf = {
          fuzzy = true,
          override_generic_sorter = true,
          override_file_sorter = true,
          case_mode = 'smart_case',
        },
      },
    })
    

    -- Load extensions
    telescope.load_extension('fzf')
  end,
}

Essential Telescope Keymaps

local builtin = require('telescope.builtin')


-- File finders
vim.keymap.set('n', '<leader>ff', builtin.find_files, { desc = 'Find Files' })
vim.keymap.set('n', '<leader>fg', builtin.git_files, { desc = 'Git Files' })
vim.keymap.set('n', '<leader>fb', builtin.buffers, { desc = 'Buffers' })
vim.keymap.set('n', '<leader>fo', builtin.oldfiles, { desc = 'Old Files' })
vim.keymap.set('n', '<leader>fr', builtin.resume, { desc = 'Resume' })


-- Search
vim.keymap.set('n', '<leader>/', builtin.live_grep, { desc = 'Live Grep' })
vim.keymap.set('n', '<leader>fw', builtin.grep_string, { desc = 'Grep String' })
vim.keymap.set('n', '<leader>fc', builtin.current_buffer_fuzzy_find, { desc = 'Current Buffer' })


-- Help and documentation
vim.keymap.set('n', '<leader>fh', builtin.help_tags, { desc = 'Help Tags' })
vim.keymap.set('n', '<leader>fm', builtin.man_pages, { desc = 'Man Pages' })
vim.keymap.set('n', '<leader>fk', builtin.keymaps, { desc = 'Keymaps' })
vim.keymap.set('n', '<leader>fC', builtin.commands, { desc = 'Commands' })
vim.keymap.set('n', '<leader>fH', builtin.command_history, { desc = 'Command History' })


-- Vim internals
vim.keymap.set('n', '<leader>fv', builtin.vim_options, { desc = 'Vim Options' })
vim.keymap.set('n', '<leader>fa', builtin.autocommands, { desc = 'Autocommands' })
vim.keymap.set('n', '<leader>fq', builtin.quickfix, { desc = 'Quickfix' })
vim.keymap.set('n', '<leader>fl', builtin.loclist, { desc = 'Location List' })
vim.keymap.set('n', '<leader>fj', builtin.jumplist, { desc = 'Jumplist' })
vim.keymap.set('n', '<leader>fR', builtin.registers, { desc = 'Registers' })
vim.keymap.set('n', '<leader>fs', builtin.spell_suggest, { desc = 'Spell Suggest' })


-- Git
vim.keymap.set('n', '<leader>gc', builtin.git_commits, { desc = 'Git Commits' })
vim.keymap.set('n', '<leader>gbc', builtin.git_bcommits, { desc = 'Buffer Commits' })
vim.keymap.set('n', '<leader>gb', builtin.git_branches, { desc = 'Git Branches' })
vim.keymap.set('n', '<leader>gs', builtin.git_status, { desc = 'Git Status' })
vim.keymap.set('n', '<leader>gt', builtin.git_stash, { desc = 'Git Stash' })


-- LSP
vim.keymap.set('n', '<leader>lr', builtin.lsp_references, { desc = 'LSP References' })
vim.keymap.set('n', '<leader>ld', builtin.lsp_definitions, { desc = 'LSP Definitions' })
vim.keymap.set('n', '<leader>li', builtin.lsp_implementations, { desc = 'LSP Implementations' })
vim.keymap.set('n', '<leader>lt', builtin.lsp_type_definitions, { desc = 'LSP Type Definitions' })
vim.keymap.set('n', '<leader>ls', builtin.lsp_document_symbols, { desc = 'Document Symbols' })
vim.keymap.set('n', '<leader>lw', builtin.lsp_workspace_symbols, { desc = 'Workspace Symbols' })
vim.keymap.set('n', '<leader>le', builtin.diagnostics, { desc = 'Diagnostics' })


-- Treesitter
vim.keymap.set('n', '<leader>ft', builtin.treesitter, { desc = 'Treesitter Symbols' })

Custom Telescope Pickers


-- Find files in config directory
vim.keymap.set('n', '<leader>fN', function()
  builtin.find_files({
    prompt_title = 'Neovim Config',
    cwd = vim.fn.stdpath('config'),
  })
end, { desc = 'Neovim Config Files' })


-- Find in specific directory
vim.keymap.set('n', '<leader>fp', function()
  builtin.find_files({
    prompt_title = 'Project Files',
    cwd = '~/projects',
  })
end, { desc = 'Project Files' })


-- Search in current directory
vim.keymap.set('n', '<leader>fD', function()
  builtin.find_files({
    prompt_title = 'Current Directory',
    cwd = vim.fn.expand('%:p:h'),
  })
end, { desc = 'Current Directory Files' })


-- Search only in git tracked files
vim.keymap.set('n', '<leader>fG', function()
  builtin.git_files({
    show_untracked = false,
  })
end, { desc = 'Git Tracked Files' })


-- Grep in specific file types
vim.keymap.set('n', '<leader>fL', function()
  builtin.live_grep({
    prompt_title = 'Grep Lua Files',
    glob_pattern = '*.lua',
  })
end, { desc = 'Grep Lua Files' })


-- Find todos
vim.keymap.set('n', '<leader>fT', function()
  builtin.grep_string({
    prompt_title = 'Find TODOs',
    search = 'TODO|FIXME|HACK|NOTE',
    use_regex = true,
  })
end, { desc = 'Find TODOs' })


-- Search in open buffers
vim.keymap.set('n', '<leader>fB', function()
  builtin.live_grep({
    prompt_title = 'Grep Open Buffers',
    grep_open_files = true,
  })
end, { desc = 'Grep Buffers' })

Advanced Telescope Features


-- Multi-selection workflow
vim.keymap.set('n', '<leader>fM', function()
  builtin.find_files({
    attach_mappings = function(_, map)
      local action_state = require('telescope.actions.state')
      
      map('i', '<CR>', function(prompt_bufnr)
        local picker = action_state.get_current_picker(prompt_bufnr)
        local multi = picker:get_multi_selection()
        
        if #multi > 0 then

          -- Process multiple selections
          for _, entry in ipairs(multi) do
            print('Selected: ' .. entry.value)
          end
        else

          -- Single selection
          actions.select_default(prompt_bufnr)
        end
      end)
      
      return true
    end,
  })
end, { desc = 'Multi-select Files' })


-- Custom preview configuration
vim.keymap.set('n', '<leader>fP', function()
  builtin.find_files({
    previewer = require('telescope.previewers').vim_buffer_cat.new({}),
    preview = {
      hide_on_startup = false,
    },
  })
end, { desc = 'Files with Preview' })


-- Search with hidden files
vim.keymap.set('n', '<leader>f.', function()
  builtin.find_files({
    hidden = true,
    no_ignore = true,
  })
end, { desc = 'All Files (Hidden)' })

Telescope Extensions

File Browser Extension

{
  'nvim-telescope/telescope-file-browser.nvim',
  dependencies = { 'nvim-telescope/telescope.nvim' },
  config = function()
    require('telescope').setup({
      extensions = {
        file_browser = {
          theme = 'ivy',
          hijack_netrw = true,
          mappings = {
            ['i'] = {
              ['<C-n>'] = require('telescope._extensions.file_browser.actions').create,
              ['<C-r>'] = require('telescope._extensions.file_browser.actions').rename,
              ['<C-d>'] = require('telescope._extensions.file_browser.actions').remove,
            },
            ['n'] = {
              ['c'] = require('telescope._extensions.file_browser.actions').create,
              ['r'] = require('telescope._extensions.file_browser.actions').rename,
              ['d'] = require('telescope._extensions.file_browser.actions').remove,
              ['h'] = require('telescope._extensions.file_browser.actions').goto_parent_dir,
              ['l'] = require('telescope.actions').select_default,
            },
          },
        },
      },
    })
    
    require('telescope').load_extension('file_browser')
    
    vim.keymap.set('n', '<leader>fe', function()
      require('telescope').extensions.file_browser.file_browser({
        path = '%:p:h',
        select_buffer = true,
      })
    end, { desc = 'File Browser' })
  end,
}

Project Management Extension

{
  'nvim-telescope/telescope-project.nvim',
  dependencies = { 'nvim-telescope/telescope.nvim' },
  config = function()
    require('telescope').setup({
      extensions = {
        project = {
          base_dirs = {
            '~/projects',
            { '~/work', max_depth = 2 },
          },
          hidden_files = true,
          order_by = 'recent',
          sync_with_nvim_tree = true,
        },
      },
    })
    
    require('telescope').load_extension('project')
    
    vim.keymap.set('n', '<leader>fP', 
      require('telescope').extensions.project.project,
      { desc = 'Projects' }
    )
  end,
}

Frecency Extension

{
  'nvim-telescope/telescope-frecency.nvim',
  dependencies = { 
    'nvim-telescope/telescope.nvim',
    'kkharji/sqlite.lua',
  },
  config = function()
    require('telescope').setup({
      extensions = {
        frecency = {
          show_scores = false,
          show_unindexed = true,
          ignore_patterns = { '*.git/*', '*/tmp/*' },
          workspaces = {
            ['conf'] = vim.fn.stdpath('config'),
            ['data'] = vim.fn.stdpath('data'),
            ['proj'] = '~/projects',
          },
        },
      },
    })
    
    require('telescope').load_extension('frecency')
    
    vim.keymap.set('n', '<leader>fF',
      require('telescope').extensions.frecency.frecency,
      { desc = 'Frecent Files' }
    )
  end,
}

FZF.vim Alternative

For those who prefer the classic fzf.vim:

{
  'junegunn/fzf',
  build = './install --all',
}

{
  'junegunn/fzf.vim',
  dependencies = { 'junegunn/fzf' },
  config = function()

    -- FZF layout
    vim.g.fzf_layout = { window = { width = 0.9, height = 0.6 } }
    

    -- Custom FZF colors
    vim.g.fzf_colors = {
      fg = { 'fg', 'Normal' },
      bg = { 'bg', 'Normal' },
      hl = { 'fg', 'Comment' },
      ['fg+'] = { 'fg', 'CursorLine', 'CursorColumn', 'Normal' },
      ['bg+'] = { 'bg', 'CursorLine', 'CursorColumn' },
      ['hl+'] = { 'fg', 'Statement' },
      info = { 'fg', 'PreProc' },
      border = { 'fg', 'Ignore' },
      prompt = { 'fg', 'Conditional' },
      pointer = { 'fg', 'Exception' },
      marker = { 'fg', 'Keyword' },
      spinner = { 'fg', 'Label' },
      header = { 'fg', 'Comment' },
    }
    

    -- Keymaps
    vim.keymap.set('n', '<leader>ff', ':Files<CR>', { desc = 'Files' })
    vim.keymap.set('n', '<leader>fg', ':GFiles<CR>', { desc = 'Git Files' })
    vim.keymap.set('n', '<leader>fb', ':Buffers<CR>', { desc = 'Buffers' })
    vim.keymap.set('n', '<leader>/', ':Rg<CR>', { desc = 'Ripgrep' })
    vim.keymap.set('n', '<leader>fl', ':Lines<CR>', { desc = 'Lines' })
    vim.keymap.set('n', '<leader>fc', ':BLines<CR>', { desc = 'Buffer Lines' })
    vim.keymap.set('n', '<leader>ft', ':Tags<CR>', { desc = 'Tags' })
    vim.keymap.set('n', '<leader>fm', ':Marks<CR>', { desc = 'Marks' })
    vim.keymap.set('n', '<leader>fw', ':Windows<CR>', { desc = 'Windows' })
    vim.keymap.set('n', '<leader>fh', ':History<CR>', { desc = 'History' })
    vim.keymap.set('n', '<leader>f:', ':History:<CR>', { desc = 'Command History' })
    vim.keymap.set('n', '<leader>f/', ':History/<CR>', { desc = 'Search History' })
    vim.keymap.set('n', '<leader>fH', ':Helptags<CR>', { desc = 'Help Tags' })
    vim.keymap.set('n', '<leader>fC', ':Commands<CR>', { desc = 'Commands' })
    vim.keymap.set('n', '<leader>fM', ':Maps<CR>', { desc = 'Keymaps' })
    

    -- Custom commands
    vim.api.nvim_create_user_command('ProjectFiles', function()
      local git_root = vim.fn.system('git rev-parse --show-toplevel 2>/dev/null'):gsub('\n', '')
      if vim.v.shell_error == 0 then
        vim.fn['fzf#run'](vim.fn['fzf#wrap']({
          source = 'git ls-files',
          dir = git_root,
        }))
      else
        vim.cmd('Files')
      end
    end, {})
  end,
}

nvim-tree.lua - File Explorer

Installation and Configuration

{
  'nvim-tree/nvim-tree.lua',
  dependencies = { 'nvim-tree/nvim-web-devicons' },
  config = function()

    -- Disable netrw
    vim.g.loaded_netrw = 1
    vim.g.loaded_netrwPlugin = 1
    
    require('nvim-tree').setup({
      disable_netrw = true,
      hijack_netrw = true,
      hijack_cursor = true,
      hijack_unnamed_buffer_when_opening = false,
      sync_root_with_cwd = true,
      
      update_focused_file = {
        enable = true,
        update_root = false,
        ignore_list = {},
      },
      
      view = {
        adaptive_size = false,
        centralize_selection = false,
        width = 30,
        side = 'left',
        preserve_window_proportions = false,
        number = false,
        relativenumber = false,
        signcolumn = 'yes',
        float = {
          enable = false,
          quit_on_focus_loss = true,
          open_win_config = {
            relative = 'editor',
            border = 'rounded',
            width = 30,
            height = 30,
            row = 1,
            col = 1,
          },
        },
      },
      
      renderer = {
        add_trailing = false,
        group_empty = false,
        highlight_git = true,
        full_name = false,
        highlight_opened_files = 'none',
        root_folder_label = ':~:s?$?/..?',
        indent_width = 2,
        indent_markers = {
          enable = true,
          inline_arrows = true,
          icons = {
            corner = '└',
            edge = '│',
            item = '│',
            bottom = '─',
            none = ' ',
          },
        },
        icons = {
          webdev_colors = true,
          git_placement = 'before',
          padding = ' ',
          symlink_arrow = ' ➛ ',
          show = {
            file = true,
            folder = true,
            folder_arrow = true,
            git = true,
          },
          glyphs = {
            default = '',
            symlink = '',
            bookmark = '',
            folder = {
              arrow_closed = '',
              arrow_open = '',
              default = '',
              open = '',
              empty = '',
              empty_open = '',
              symlink = '',
              symlink_open = '',
            },
            git = {
              unstaged = '✗',
              staged = '✓',
              unmerged = '',
              renamed = '➜',
              untracked = '★',
              deleted = '',
              ignored = '◌',
            },
          },
        },
        special_files = { 'Cargo.toml', 'Makefile', 'README.md', 'readme.md' },
        symlink_destination = true,
      },
      
      filters = {
        dotfiles = false,
        git_clean = false,
        no_buffer = false,
        custom = { '^.git$', 'node_modules', '.cache' },
        exclude = {},
      },
      
      filesystem_watchers = {
        enable = true,
        debounce_delay = 50,
        ignore_dirs = {},
      },
      
      git = {
        enable = true,
        ignore = false,
        show_on_dirs = true,
        show_on_open_dirs = true,
        timeout = 400,
      },
      
      actions = {
        use_system_clipboard = true,
        change_dir = {
          enable = true,
          global = false,
          restrict_above_cwd = false,
        },
        expand_all = {
          max_folder_discovery = 300,
          exclude = {},
        },
        file_popup = {
          open_win_config = {
            col = 1,
            row = 1,
            relative = 'cursor',
            border = 'shadow',
            style = 'minimal',
          },
        },
        open_file = {
          quit_on_open = false,
          resize_window = true,
          window_picker = {
            enable = true,
            picker = 'default',
            chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',
            exclude = {
              filetype = { 'notify', 'packer', 'qf', 'diff', 'fugitive', 'fugitiveblame' },
              buftype = { 'nofile', 'terminal', 'help' },
            },
          },
        },
        remove_file = {
          close_window = true,
        },
      },
      
      trash = {
        cmd = 'trash',
        require_confirm = true,
      },
      
      live_filter = {
        prefix = '[FILTER]: ',
        always_show_folders = true,
      },
      
      tab = {
        sync = {
          open = false,
          close = false,
          ignore = {},
        },
      },
      
      notify = {
        threshold = vim.log.levels.INFO,
      },
      
      log = {
        enable = false,
        truncate = false,
        types = {
          all = false,
          profile = false,
          config = false,
          copy_paste = false,
          dev = false,
          diagnostics = false,
          git = false,
          watcher = false,
          web_devicons = false,
        },
      },
    })
    

    -- Keymaps
    vim.keymap.set('n', '<leader>e', ':NvimTreeToggle<CR>', { desc = 'Toggle File Explorer' })
    vim.keymap.set('n', '<leader>o', ':NvimTreeFocus<CR>', { desc = 'Focus File Explorer' })
    vim.keymap.set('n', '<leader>E', ':NvimTreeFindFile<CR>', { desc = 'Find in File Explorer' })
  end,
}

nvim-tree Custom Functions


-- Auto-close when last window
vim.api.nvim_create_autocmd('QuitPre', {
  callback = function()
    local tree_wins = {}
    local floating_wins = {}
    local wins = vim.api.nvim_list_wins()
    for _, w in ipairs(wins) do
      local bufname = vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(w))
      if bufname:match('NvimTree_') ~= nil then
        table.insert(tree_wins, w)
      end
      if vim.api.nvim_win_get_config(w).relative ~= '' then
        table.insert(floating_wins, w)
      end
    end
    if 1 == #wins - #floating_wins - #tree_wins then
      for _, w in ipairs(tree_wins) do
        vim.api.nvim_win_close(w, true)
      end
    end
  end
})


-- Open nvim-tree on startup for directory
vim.api.nvim_create_autocmd('VimEnter', {
  callback = function(data)
    local directory = vim.fn.isdirectory(data.file) == 1
    if directory then
      vim.cmd.cd(data.file)
      require('nvim-tree.api').tree.open()
    end
  end
})

Neo-tree.nvim Alternative

{
  'nvim-neo-tree/neo-tree.nvim',
  branch = 'v3.x',
  dependencies = {
    'nvim-lua/plenary.nvim',
    'nvim-tree/nvim-web-devicons',
    'MunifTanjim/nui.nvim',
  },
  config = function()
    require('neo-tree').setup({
      close_if_last_window = true,
      popup_border_style = 'rounded',
      enable_git_status = true,
      enable_diagnostics = true,
      
      default_component_configs = {
        container = {
          enable_character_fade = true
        },
        indent = {
          indent_size = 2,
          padding = 1,
          with_markers = true,
          indent_marker = '│',
          last_indent_marker = '└',
          highlight = 'NeoTreeIndentMarker',
          with_expanders = nil,
          expander_collapsed = '',
          expander_expanded = '',
          expander_highlight = 'NeoTreeExpander',
        },
        icon = {
          folder_closed = '',
          folder_open = '',
          folder_empty = '',
          default = '*',
          highlight = 'NeoTreeFileIcon'
        },
        modified = {
          symbol = '[+]',
          highlight = 'NeoTreeModified',
        },
        name = {
          trailing_slash = false,
          use_git_status_colors = true,
          highlight = 'NeoTreeFileName',
        },
        git_status = {
          symbols = {
            added     = '✚',
            modified  = '',
            deleted   = '✖',
            renamed   = '',
            untracked = '',
            ignored   = '',
            unstaged  = '',
            staged    = '',
            conflict  = '',
          }
        },
      },
      
      window = {
        position = 'left',
        width = 30,
        mapping_options = {
          noremap = true,
          nowait = true,
        },
        mappings = {
          ['<space>'] = {
            'toggle_node',
            nowait = false,
          },
          ['<2-LeftMouse>'] = 'open',
          ['<cr>'] = 'open',
          ['<esc>'] = 'revert_preview',
          ['P'] = { 'toggle_preview', config = { use_float = true } },
          ['l'] = 'focus_preview',
          ['S'] = 'open_split',
          ['s'] = 'open_vsplit',
          ['t'] = 'open_tabnew',
          ['w'] = 'open_with_window_picker',
          ['C'] = 'close_node',
          ['z'] = 'close_all_nodes',
          ['a'] = {
            'add',
            config = {
              show_path = 'none'
            }
          },
          ['A'] = 'add_directory',
          ['d'] = 'delete',
          ['r'] = 'rename',
          ['y'] = 'copy_to_clipboard',
          ['x'] = 'cut_to_clipboard',
          ['p'] = 'paste_from_clipboard',
          ['c'] = 'copy',
          ['m'] = 'move',
          ['q'] = 'close_window',
          ['R'] = 'refresh',
          ['?'] = 'show_help',
          ['<'] = 'prev_source',
          ['>'] = 'next_source',
        }
      },
      
      filesystem = {
        filtered_items = {
          visible = false,
          hide_dotfiles = false,
          hide_gitignored = false,
          hide_hidden = true,
          hide_by_name = {
            'node_modules'
          },
          hide_by_pattern = {

            --'*.meta',

            --'*/src/*/tsconfig.json',
          },
          always_show = {
            '.gitignored',
          },
          never_show = {
            '.DS_Store',
            'thumbs.db'
          },
          never_show_by_pattern = {

            --'.null-ls_*',
          },
        },
        follow_current_file = {
          enabled = true,
          leave_dirs_open = false,
        },
        group_empty_dirs = false,
        hijack_netrw_behavior = 'open_default',
        use_libuv_file_watcher = false,
        window = {
          mappings = {
            ['<bs>'] = 'navigate_up',
            ['.'] = 'set_root',
            ['H'] = 'toggle_hidden',
            ['/'] = 'fuzzy_finder',
            ['D'] = 'fuzzy_finder_directory',
            ['#'] = 'fuzzy_sorter',
            ['f'] = 'filter_on_submit',
            ['<c-x>'] = 'clear_filter',
            ['[g'] = 'prev_git_modified',
            [']g'] = 'next_git_modified',
          }
        },
        commands = {}
      },
      
      buffers = {
        follow_current_file = {
          enabled = true,
          leave_dirs_open = false,
        },
        group_empty_dirs = true,
        show_unloaded = true,
        window = {
          mappings = {
            ['bd'] = 'buffer_delete',
            ['<bs>'] = 'navigate_up',
            ['.'] = 'set_root',
          }
        },
      },
      
      git_status = {
        window = {
          position = 'float',
          mappings = {
            ['A']  = 'git_add_all',
            ['gu'] = 'git_unstage_file',
            ['ga'] = 'git_add_file',
            ['gr'] = 'git_revert_file',
            ['gc'] = 'git_commit',
            ['gp'] = 'git_push',
            ['gg'] = 'git_commit_and_push',
          }
        }
      }
    })
    
    vim.keymap.set('n', '<leader>e', ':Neotree toggle<CR>', { desc = 'Toggle Neo-tree' })
    vim.keymap.set('n', '<leader>o', ':Neotree focus<CR>', { desc = 'Focus Neo-tree' })
    vim.keymap.set('n', '<leader>E', ':Neotree reveal<CR>', { desc = 'Reveal in Neo-tree' })
    vim.keymap.set('n', '<leader>gb', ':Neotree float git_status<CR>', { desc = 'Git Status' })
    vim.keymap.set('n', '<leader>bb', ':Neotree float buffers<CR>', { desc = 'Buffers' })
  end,
}

Oil.nvim - Edit Your Filesystem

{
  'stevearc/oil.nvim',
  dependencies = { 'nvim-tree/nvim-web-devicons' },
  config = function()
    require('oil').setup({
      default_file_explorer = true,
      columns = {
        'icon',
        'permissions',
        'size',
        'mtime',
      },
      buf_options = {
        buflisted = false,
        bufhidden = 'hide',
      },
      win_options = {
        wrap = false,
        signcolumn = 'no',
        cursorcolumn = false,
        foldcolumn = '0',
        spell = false,
        list = false,
        conceallevel = 3,
        concealcursor = 'nvic',
      },
      delete_to_trash = false,
      skip_confirm_for_simple_edits = false,
      prompt_save_on_select_new_entry = true,
      cleanup_delay_ms = 2000,
      lsp_file_methods = {
        timeout_ms = 1000,
        autosave_changes = false,
      },
      constrain_cursor = 'editable',
      experimental_watch_for_changes = false,
      keymaps = {
        ['g?'] = 'actions.show_help',
        ['<CR>'] = 'actions.select',
        ['<C-s>'] = 'actions.select_vsplit',
        ['<C-h>'] = 'actions.select_split',
        ['<C-t>'] = 'actions.select_tab',
        ['<C-p>'] = 'actions.preview',
        ['<C-c>'] = 'actions.close',
        ['<C-l>'] = 'actions.refresh',
        ['-'] = 'actions.parent',
        ['_'] = 'actions.open_cwd',
        ['`'] = 'actions.cd',
        ['~'] = 'actions.tcd',
        ['gs'] = 'actions.change_sort',
        ['gx'] = 'actions.open_external',
        ['g.'] = 'actions.toggle_hidden',
        ['g\\'] = 'actions.toggle_trash',
      },
      use_default_keymaps = true,
      view_options = {
        show_hidden = false,
        is_hidden_file = function(name, bufnr)
          return vim.startswith(name, '.')
        end,
        is_always_hidden = function(name, bufnr)
          return false
        end,
        sort = {
          { 'type', 'asc' },
          { 'name', 'asc' },
        },
      },
      float = {
        padding = 2,
        max_width = 0,
        max_height = 0,
        border = 'rounded',
        win_options = {
          winblend = 0,
        },
        override = function(conf)
          return conf
        end,
      },
      preview = {
        max_width = 0.9,
        min_width = { 40, 0.4 },
        width = nil,
        max_height = 0.9,
        min_height = { 5, 0.1 },
        height = nil,
        border = 'rounded',
        win_options = {
          winblend = 0,
        },
      },
      progress = {
        max_width = 0.9,
        min_width = { 40, 0.4 },
        width = nil,
        max_height = { 10, 0.9 },
        min_height = { 5, 0.1 },
        height = nil,
        border = 'rounded',
        minimized_border = 'none',
        win_options = {
          winblend = 0,
        },
      },
    })
    

    -- Keymaps
    vim.keymap.set('n', '-', '<CMD>Oil<CR>', { desc = 'Open parent directory' })
    vim.keymap.set('n', '<leader>-', require('oil').toggle_float, { desc = 'Oil Float' })
  end,
}

Harpoon - Quick File Navigation

{
  'ThePrimeagen/harpoon',
  branch = 'harpoon2',
  dependencies = { 'nvim-lua/plenary.nvim' },
  config = function()
    local harpoon = require('harpoon')
    harpoon:setup({})
    

    -- Keymaps
    vim.keymap.set('n', '<leader>a', function() 
      harpoon:list():append() 
    end, { desc = 'Harpoon Add' })
    
    vim.keymap.set('n', '<C-e>', function() 
      harpoon.ui:toggle_quick_menu(harpoon:list()) 
    end, { desc = 'Harpoon Menu' })
    
    vim.keymap.set('n', '<C-h>', function() 
      harpoon:list():select(1) 
    end, { desc = 'Harpoon 1' })
    
    vim.keymap.set('n', '<C-j>', function() 
      harpoon:list():select(2) 
    end, { desc = 'Harpoon 2' })
    
    vim.keymap.set('n', '<C-k>', function() 
      harpoon:list():select(3) 
    end, { desc = 'Harpoon 3' })
    
    vim.keymap.set('n', '<C-l>', function() 
      harpoon:list():select(4) 
    end, { desc = 'Harpoon 4' })
    

    -- Toggle previous & next buffers stored within Harpoon list
    vim.keymap.set('n', '<C-S-P>', function() 
      harpoon:list():prev() 
    end, { desc = 'Harpoon Prev' })
    
    vim.keymap.set('n', '<C-S-N>', function() 
      harpoon:list():next() 
    end, { desc = 'Harpoon Next' })
    

    -- Telescope integration
    local conf = require('telescope.config').values
    local function toggle_telescope(harpoon_files)
      local file_paths = {}
      for _, item in ipairs(harpoon_files.items) do
        table.insert(file_paths, item.value)
      end
      
      require('telescope.pickers').new({}, {
        prompt_title = 'Harpoon',
        finder = require('telescope.finders').new_table({
          results = file_paths,
        }),
        previewer = conf.file_previewer({}),
        sorter = conf.generic_sorter({}),
      }):find()
    end
    
    vim.keymap.set('n', '<leader>fh', function() 
      toggle_telescope(harpoon:list()) 
    end, { desc = 'Harpoon Telescope' })
  end,
}

Arrow.nvim - Bookmark Files

{
  'otavioschwanck/arrow.nvim',
  opts = {
    show_icons = true,
    leader_key = ';',
    buffer_leader_key = 'm',
  },
  keys = {
    { ';', desc = 'Arrow' },
    { 'm', desc = 'Arrow Buffer' },
  },
}

Advanced Navigation Patterns

Jump to Definition Across Projects


-- Cross-project definition jump
vim.keymap.set('n', '<leader>gD', function()
  local word = vim.fn.expand('<cword>')
  
  builtin.live_grep({
    prompt_title = 'Find Definition: ' .. word,
    default_text = 'def ' .. word,
    search_dirs = { '~/projects' },
  })
end, { desc = 'Global Definition' })

Recent Files with Filter


-- Recent files from current project
vim.keymap.set('n', '<leader>fR', function()
  local git_root = vim.fn.system('git rev-parse --show-toplevel 2>/dev/null'):gsub('\n', '')
  
  if vim.v.shell_error == 0 then
    builtin.oldfiles({
      cwd = git_root,
      cwd_only = true,
    })
  else
    builtin.oldfiles()
  end
end, { desc = 'Recent Project Files' })

Smart Buffer Navigation


-- Navigate buffers by directory
vim.keymap.set('n', '<leader>bD', function()
  local cwd = vim.fn.getcwd()
  local buffers = vim.tbl_filter(function(buf)
    local bufpath = vim.api.nvim_buf_get_name(buf)
    return vim.startswith(bufpath, cwd)
  end, vim.api.nvim_list_bufs())
  
  local buffer_names = vim.tbl_map(function(buf)
    return vim.api.nvim_buf_get_name(buf)
  end, buffers)
  
  builtin.buffers({
    bufnrs = buffers,
  })
end, { desc = 'Directory Buffers' })

Summary: Chapter #33 – File Navigation and Fuzzy Finders

This chapter covered comprehensive file navigation solutions:

  1. Built-in Navigation – Netrw, path settings, and buffer management

  2. Telescope.nvim – Modern fuzzy finder with extensive features

  3. Telescope Extensions – File browser, projects, frecency

  4. FZF.vim – Classic fuzzy finder alternative

  5. nvim-tree.lua – Feature-rich file explorer

  6. Neo-tree – Modern file explorer with multiple views

  7. Oil.nvim – Edit filesystem like a buffer

  8. Harpoon – Quick file marking and navigation

  9. Arrow.nvim – Persistent bookmarks

  10. Advanced Patterns – Custom workflows and optimizations

These tools provide powerful ways to navigate files, projects, and code, making Neovim an incredibly efficient development environment.


Chapter #34: Snippets and Templates

Snippets are reusable code templates that dramatically speed up development by expanding short triggers into complete code structures. This chapter explores snippet engines, creation, and management in Neovim.

Understanding Snippets

What Are Snippets?

Snippets are expandable text templates with:

  • Trigger: Short text that expands (e.g., for → full for-loop)

  • Placeholders: Tabstops for cursor jumping

  • Variables: Dynamic content (filename, date, etc.)

  • Transformations: Text manipulation during expansion

Snippet Engines

Popular snippet engines for Neovim:

  1. LuaSnip - Lua-based, most flexible

  2. vim-vsnip - VSCode-compatible

  3. UltiSnips - Legacy Python-based (Vim)

  4. snippy.nvim - Minimal Lua engine

LuaSnip - Modern Snippet Engine

Installation and Setup

{
  'L3MON4D3/LuaSnip',
  version = 'v2.*',
  build = 'make install_jsregexp',
  dependencies = {
    'rafamadriz/friendly-snippets', -- Pre-made snippets
    'saadparwaiz1/cmp_luasnip',     -- nvim-cmp integration
  },
  config = function()
    local luasnip = require('luasnip')
    

    -- Configuration
    luasnip.setup({
      history = true,              -- Keep history for jumping back
      update_events = 'TextChanged,TextChangedI', -- Update as you type
      delete_check_events = 'TextChanged',
      enable_autosnippets = true,  -- Enable auto-triggered snippets
      store_selection_keys = '<Tab>', -- Visual selection key
      
      ext_opts = {
        [require('luasnip.util.types').choiceNode] = {
          active = {
            virt_text = { { '●', 'GruvboxOrange' } },
          },
        },
      },
    })
    

    -- Load friendly-snippets
    require('luasnip.loaders.from_vscode').lazy_load()
    

    -- Load custom snippets from ~/.config/nvim/snippets/
    require('luasnip.loaders.from_lua').load({
      paths = vim.fn.stdpath('config') .. '/snippets'
    })
    

    -- Load snippets from specific file
    require('luasnip.loaders.from_snipmate').lazy_load({
      paths = vim.fn.stdpath('config') .. '/snippets'
    })
  end,
}

Basic Keymaps

local luasnip = require('luasnip')


-- Expand or jump forward
vim.keymap.set({'i', 's'}, '<C-k>', function()
  if luasnip.expand_or_jumpable() then
    luasnip.expand_or_jump()
  end
end, { silent = true, desc = 'Expand or Jump' })


-- Jump backward
vim.keymap.set({'i', 's'}, '<C-j>', function()
  if luasnip.jumpable(-1) then
    luasnip.jump(-1)
  end
end, { silent = true, desc = 'Jump Back' })


-- Choice node selection
vim.keymap.set({'i', 's'}, '<C-l>', function()
  if luasnip.choice_active() then
    luasnip.change_choice(1)
  end
end, { desc = 'Next Choice' })

vim.keymap.set({'i', 's'}, '<C-h>', function()
  if luasnip.choice_active() then
    luasnip.change_choice(-1)
  end
end, { desc = 'Previous Choice' })


-- Reload snippets
vim.keymap.set('n', '<leader>rs', function()
  require('luasnip.loaders.from_lua').load({
    paths = vim.fn.stdpath('config') .. '/snippets'
  })
  print('Snippets reloaded!')
end, { desc = 'Reload Snippets' })

Creating Snippets in Lua

Create ~/.config/nvim/snippets/all.lua:

local ls = require('luasnip')
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local f = ls.function_node
local c = ls.choice_node
local d = ls.dynamic_node
local r = ls.restore_node
local sn = ls.snippet_node
local fmt = require('luasnip.extras.fmt').fmt
local rep = require('luasnip.extras').rep

return {

  -- Simple text insertion
  s('hello', {
    t('Hello, '),
    i(1, 'World'),
    t('!'),
  }),
  

  -- Using fmt for better formatting
  s('todo', fmt([[

    -- TODO({}): {}
    {}
  ]], {
    i(1, 'username'),
    i(2, 'description'),
    i(0),
  })),
  

  -- Repeat node
  s('dup', fmt([[
    local {} = {}
    print({})
  ]], {
    i(1, 'var'),
    i(2, 'value'),
    rep(1), -- Repeats first insert node
  })),
  

  -- Choice node
  s('log', fmt([[
    console.{}('{}')
  ]], {
    c(1, {
      t('log'),
      t('warn'),
      t('error'),
      t('info'),
    }),
    i(2, 'message'),
  })),
  

  -- Function node (dynamic content)
  s('date', {
    f(function()
      return os.date('%Y-%m-%d')
    end),
  }),
  

  -- Current filename
  s('fn', {
    f(function()
      return vim.fn.expand('%:t')
    end),
  }),
  

  -- Dynamic node
  s('dyn', {
    t('Value: '),
    i(1),
    t({ '', 'Repeated: ' }),
    d(2, function(args)

      -- args[1] contains text from i(1)
      return sn(nil, {
        t(args[1][1]:upper()),
      })
    end, {1}),
  }),
}

Language-Specific Snippets

Create ~/.config/nvim/snippets/lua.lua:

local ls = require('luasnip')
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local fmt = require('luasnip.extras.fmt').fmt

return {

  -- Function snippet
  s('fn', fmt([[
    function {}({})
      {}
    end
  ]], {
    i(1, 'name'),
    i(2, 'args'),
    i(0),
  })),
  

  -- Local function
  s('lfn', fmt([[
    local function {}({})
      {}
    end
  ]], {
    i(1, 'name'),
    i(2, 'args'),
    i(0),
  })),
  

  -- For loop
  s('for', fmt([[
    for {} = {}, {} do
      {}
    end
  ]], {
    i(1, 'i'),
    i(2, '1'),
    i(3, '10'),
    i(0),
  })),
  

  -- Pairs iterator
  s('forp', fmt([[
    for {}, {} in pairs({}) do
      {}
    end
  ]], {
    i(1, 'k'),
    i(2, 'v'),
    i(3, 'table'),
    i(0),
  })),
  

  -- Ipairs iterator
  s('fori', fmt([[
    for {}, {} in ipairs({}) do
      {}
    end
  ]], {
    i(1, 'i'),
    i(2, 'v'),
    i(3, 'table'),
    i(0),
  })),
  

  -- Require statement
  s('req', fmt([[
    local {} = require('{}')
  ]], {
    i(1, 'module'),
    i(2, 'module.path'),
  })),
  

  -- If statement
  s('if', fmt([[
    if {} then
      {}
    end
  ]], {
    i(1, 'condition'),
    i(0),
  })),
  

  -- If-else
  s('ife', fmt([[
    if {} then
      {}
    else
      {}
    end
  ]], {
    i(1, 'condition'),
    i(2),
    i(0),
  })),
}

Create ~/.config/nvim/snippets/python.lua:

local ls = require('luasnip')
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local c = ls.choice_node
local fmt = require('luasnip.extras.fmt').fmt
local rep = require('luasnip.extras').rep

return {

  -- Function
  s('def', fmt([[
    def {}({}):
        {}
        {}
  ]], {
    i(1, 'function_name'),
    i(2, 'args'),
    i(3, '"""Docstring."""'),
    i(0),
  })),
  

  -- Class
  s('class', fmt([[
    class {}:
        """{}"""
        
        def __init__(self, {}):
            {}
  ]], {
    i(1, 'ClassName'),
    i(2, 'Class description'),
    i(3, 'args'),
    i(0),
  })),
  

  -- If main
  s('ifmain', fmt([[
    if __name__ == '__main__':
        {}
  ]], {
    i(0),
  })),
  

  -- Try-except
  s('try', fmt([[
    try:
        {}
    except {} as {}:
        {}
  ]], {
    i(1),
    i(2, 'Exception'),
    i(3, 'e'),
    i(0),
  })),
  

  -- List comprehension
  s('lc', fmt([[
    [{} for {} in {}]
  ]], {
    i(1, 'item'),
    rep(1),
    i(2, 'iterable'),
  })),
  

  -- Dict comprehension
  s('dc', fmt([[
    {{{}: {} for {} in {}}}
  ]], {
    i(1, 'key'),
    i(2, 'value'),
    i(3, 'item'),
    i(4, 'iterable'),
  })),
  

  -- Dataclass
  s('dc', fmt([[
    @dataclass
    class {}:
        {}
  ]], {
    i(1, 'ClassName'),
    i(0),
  })),
  

  -- Property
  s('prop', fmt([[
    @property
    def {}(self):
        return self._{}
    
    @{}.setter
    def {}(self, value):
        self._{} = value
  ]], {
    i(1, 'name'),
    rep(1),
    rep(1),
    rep(1),
    rep(1),
  })),
}

Advanced Snippet Techniques

Context-Aware Snippets

-- Only expand in specific contexts
local function in_comment()
  return vim.fn.synIDattr(vim.fn.synID(vim.fn.line('.'), vim.fn.col('.'), true), 'name'):match('Comment') ~= nil
end

local function in_string()
  return vim.fn.synIDattr(vim.fn.synID(vim.fn.line('.'), vim.fn.col('.'), true), 'name'):match('String') ~= nil
end

return {

  -- Only in comments
  s({
    trig = 'todo',
    condition = in_comment,
  }, fmt([[
    TODO({}): {}
  ]], {
    i(1, vim.fn.expand('$USER')),
    i(0),
  })),
  

  -- Not in strings
  s({
    trig = 'fn',
    condition = function()
      return not in_string()
    end,
  }, fmt([[
    function {}({})
      {}
    end
  ]], {
    i(1),
    i(2),
    i(0),
  })),
}
Regex Triggers
return {

  -- Regex trigger: bb1, bb2, etc.
  s({
    trig = 'bb(%d)',
    regTrig = true,
  }, {
    f(function(args, snip)
      return 'Blackboard bold: ℝ' .. snip.captures[1]
    end),
  }),
  

  -- Auto-expand arithmetic
  s({
    trig = '(%d+)%+(%d+)',
    regTrig = true,
    wordTrig = false,
  }, {
    f(function(args, snip)
      return tonumber(snip.captures[1]) + tonumber(snip.captures[2])
    end),
  }),
}
Multi-line Snippets with Proper Indentation
return {
  s('switch', {
    t('switch ('), i(1, 'expression'), t(') {'),
    t({'', '\tcase '}), i(2, 'value'), t(':'),
    t({'', '\t\t'}), i(3), t({'', '\t\tbreak;'}),
    t({'', '\tdefault:'}),
    t({'', '\t\t'}), i(0), t({'', '\t\tbreak;'}),
    t({'', '}'}),
  }),
}
Snippet with User Input
local function get_comment_string()
  local cs = vim.bo.commentstring
  if cs == '' then cs = '# %s' end
  return cs:format('%s'):gsub('%%s', '')
end

return {
  s('box', {
    f(function()
      local comment = get_comment_string()
      return comment .. string.rep('=', 70)
    end),
    t({'', ''}),
    f(function()
      return get_comment_string()
    end),
    t(' '), i(1, 'Title'), t(' '),
    f(function()
      return string.rep('=', 70 - #get_comment_string())
    end),
    t({'', ''}),
    f(function()
      return get_comment_string() .. string.rep('=', 70)
    end),
  }),
}

nvim-cmp Integration

Setup with LuaSnip

{
  'hrsh7th/nvim-cmp',
  dependencies = {
    'L3MON4D3/LuaSnip',
    'saadparwaiz1/cmp_luasnip',
  },
  config = function()
    local cmp = require('cmp')
    local luasnip = require('luasnip')
    
    cmp.setup({
      snippet = {
        expand = function(args)
          luasnip.lsp_expand(args.body)
        end,
      },
      
      mapping = cmp.mapping.preset.insert({
        ['<C-b>'] = cmp.mapping.scroll_docs(-4),
        ['<C-f>'] = cmp.mapping.scroll_docs(4),
        ['<C-Space>'] = cmp.mapping.complete(),
        ['<C-e>'] = cmp.mapping.abort(),
        ['<CR>'] = cmp.mapping.confirm({ select = true }),
        

        -- Tab completion
        ['<Tab>'] = cmp.mapping(function(fallback)
          if cmp.visible() then
            cmp.select_next_item()
          elseif luasnip.expand_or_jumpable() then
            luasnip.expand_or_jump()
          else
            fallback()
          end
        end, { 'i', 's' }),
        
        ['<S-Tab>'] = cmp.mapping(function(fallback)
          if cmp.visible() then
            cmp.select_prev_item()
          elseif luasnip.jumpable(-1) then
            luasnip.jump(-1)
          else
            fallback()
          end
        end, { 'i', 's' }),
      }),
      
      sources = cmp.config.sources({
        { name = 'nvim_lsp' },
        { name = 'luasnip' },
        { name = 'buffer' },
        { name = 'path' },
      }),
      
      formatting = {
        format = function(entry, vim_item)

          -- Kind icons
          local icons = {
            Text = '',
            Method = '',
            Function = '',
            Constructor = '',
            Snippet = '',
          }
          vim_item.kind = string.format('%s %s', icons[vim_item.kind], vim_item.kind)
          vim_item.menu = ({
            nvim_lsp = '[LSP]',
            luasnip = '[Snip]',
            buffer = '[Buf]',
            path = '[Path]',
          })[entry.source.name]
          return vim_item
        end,
      },
    })
  end,
}

VSCode-Style Snippets

Using friendly-snippets


-- Load all VSCode snippets
require('luasnip.loaders.from_vscode').lazy_load()


-- Load from specific paths
require('luasnip.loaders.from_vscode').lazy_load({
  paths = { '~/.config/nvim/snippets/vscode' }
})


-- Exclude certain snippet packs
require('luasnip.loaders.from_vscode').lazy_load({
  exclude = { 'javascript', 'typescript' }
})

Creating VSCode-Style Snippets

Create ~/.config/nvim/snippets/vscode/package.json:

{
  "name": "custom-snippets",
  "engines": {
    "vscode": "^1.0.0"
  },
  "contributes": {
    "snippets": [
      {
        "language": "javascript",
        "path": "./javascript.json"
      },
      {
        "language": "python",
        "path": "./python.json"
      }
    ]
  }
}

Create ~/.config/nvim/snippets/vscode/javascript.json:

{
  "Import Statement": {
    "prefix": "imp",
    "body": [
      "import ${2:module} from '${1:package}';"
    ],
    "description": "Import a module"
  },
  
  "Arrow Function": {
    "prefix": "af",
    "body": [
      "const ${1:name} = (${2:params}) => {",
      "\t$0",
      "};"
    ],
    "description": "Arrow function"
  },
  
  "React Component": {
    "prefix": "rfc",
    "body": [
      "import React from 'react';",
      "",
      "const ${1:ComponentName} = () => {",
      "\treturn (",
      "\t\t<div>",
      "\t\t\t$0",
      "\t\t</div>",
      "\t);",
      "};",
      "",
      "export default ${1:ComponentName};"
    ],
    "description": "React functional component"
  },
  
  "Console Log": {
    "prefix": ["cl", "log"],
    "body": [
      "console.log('${1:label}:', $2);"
    ],
    "description": "Console log with label"
  },
  
  "Try-Catch": {
    "prefix": "tryc",
    "body": [
      "try {",
      "\t$1",
      "} catch (${2:error}) {",
      "\t${3:console.error(error);}",
      "}"
    ],
    "description": "Try-catch block"
  }
}

Auto-Snippets

Self-Expanding Snippets

local ls = require('luasnip')
local s = ls.snippet
local t = ls.text_node
local i = ls.insert_node
local fmt = require('luasnip.extras.fmt').fmt

return {

  -- Auto-expand when typing
  s({
    trig = '->',
    wordTrig = false,
    snippetType = 'autosnippet',
  }, {
    t(' => '),
  }),
  

  -- Auto-brackets
  s({
    trig = '(%w+)%(%)$',
    regTrig = true,
    snippetType = 'autosnippet',
  }, {
    f(function(args, snip)
      return snip.captures[1] .. '('
    end),
    i(1),
    t(')'),
  }),
  

  -- Math mode auto-snippets (LaTeX)
  s({
    trig = 'ff',
    snippetType = 'autosnippet',
    condition = function()
      return vim.bo.filetype == 'tex'
    end,
  }, fmt([[
    \frac{{{}}}{{{}}}
  ]], {
    i(1),
    i(2),
  })),
}

Snippet Management Commands

Custom Commands


-- List all available snippets for current filetype
vim.api.nvim_create_user_command('SnippetsList', function()
  local ft = vim.bo.filetype
  local snippets = require('luasnip').get_snippets(ft)
  
  local lines = {}
  for _, snip in ipairs(snippets or {}) do
    table.insert(lines, string.format('%-20s %s', snip.trigger, snip.name or ''))
  end
  
  vim.api.nvim_echo({{table.concat(lines, '\n'), 'Normal'}}, false, {})
end, {})


-- Edit snippets for current filetype
vim.api.nvim_create_user_command('SnippetsEdit', function()
  local ft = vim.bo.filetype
  local snippet_file = vim.fn.stdpath('config') .. '/snippets/' .. ft .. '.lua'
  

  -- Create directory if it doesn't exist
  vim.fn.mkdir(vim.fn.fnamemodify(snippet_file, ':h'), 'p')
  

  -- Create file with template if it doesn't exist
  if vim.fn.filereadable(snippet_file) == 0 then
    local template = {
      'local ls = require("luasnip")',
      'local s = ls.snippet',
      'local t = ls.text_node',
      'local i = ls.insert_node',
      'local fmt = require("luasnip.extras.fmt").fmt',
      '',
      'return {',
      '\t-- Your snippets here',
      '}',
    }
    vim.fn.writefile(template, snippet_file)
  end
  
  vim.cmd('edit ' .. snippet_file)
end, {})

vim.keymap.set('n', '<leader>se', ':SnippetsEdit<CR>', { desc = 'Edit Snippets' })
vim.keymap.set('n', '<leader>sl', ':SnippetsList<CR>', { desc = 'List Snippets' })

vim-vsnip Alternative

Setup

{
  'hrsh7th/vim-vsnip',
  dependencies = {
    'hrsh7th/vim-vsnip-integ',
    'rafamadriz/friendly-snippets',
  },
  config = function()
    vim.g.vsnip_snippet_dir = vim.fn.stdpath('config') .. '/snippets/vsnip'
    

    -- Keymaps
    vim.keymap.set({'i', 's'}, '<C-k>', function()
      if vim.fn['vsnip#available'](1) == 1 then
        return '<Plug>(vsnip-expand-or-jump)'
      end
      return '<C-k>'
    end, { expr = true, remap = true })
    
    vim.keymap.set({'i', 's'}, '<C-j>', function()
      if vim.fn['vsnip#jumpable'](-1) == 1 then
        return '<Plug>(vsnip-jump-prev)'
      end
      return '<C-j>'
    end, { expr = true, remap = true })
  end,
}

Templates and Skeletons

Auto-Templates on New Files


-- Create template directory
local template_dir = vim.fn.stdpath('config') .. '/templates'
vim.fn.mkdir(template_dir, 'p')


-- Auto-load template on new file
vim.api.nvim_create_autocmd('BufNewFile', {
  pattern = '*',
  callback = function()
    local ext = vim.fn.expand('%:e')
    local template = template_dir .. '/' .. ext .. '.template'
    
    if vim.fn.filereadable(template) == 1 then
      vim.cmd('0read ' .. template)
      

      -- Replace placeholders
      local replacements = {
        ['{{FILENAME}}'] = vim.fn.expand('%:t:r'),
        ['{{DATE}}'] = os.date('%Y-%m-%d'),
        ['{{YEAR}}'] = os.date('%Y'),
        ['{{AUTHOR}}'] = vim.fn.expand('$USER'),
      }
      
      local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
      for i, line in ipairs(lines) do
        for placeholder, value in pairs(replacements) do
          line = line:gsub(placeholder, value)
        end
        lines[i] = line
      end
      vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)
      

      -- Delete empty first line
      if vim.fn.getline(1) == '' then
        vim.cmd('1delete')
      end
    end
  end,
})

Example Templates

~/.config/nvim/templates/py.template:

#!/usr/bin/env python3
"""
{{FILENAME}}.py

Author: {{AUTHOR}}
Date: {{DATE}}
"""


def main():
    pass


if __name__ == '__main__':
    main()

~/.config/nvim/templates/lua.template:


--[[
  {{FILENAME}}.lua
  
  Author: {{AUTHOR}}
  Date: {{DATE}}

--]]

local M = {}


return M

~/.config/nvim/templates/sh.template:

#!/usr/bin/env bash
#
# {{FILENAME}}.sh
# Author: {{AUTHOR}}
# Date: {{DATE}}
#

set -euo pipefail

main() {
    :
}

main "$@"

Interactive Template Selection

vim.api.nvim_create_user_command('Template', function(opts)
  local template_dir = vim.fn.stdpath('config') .. '/templates'
  local templates = vim.fn.globpath(template_dir, '*.template', false, true)
  
  if #templates == 0 then
    print('No templates found in ' .. template_dir)
    return
  end
  
  vim.ui.select(templates, {
    prompt = 'Select template:',
    format_item = function(item)
      return vim.fn.fnamemodify(item, ':t:r')
    end,
  }, function(choice)
    if choice then
      vim.cmd('0read ' .. choice)
      vim.cmd('1delete')
    end
  end)
end, {})

vim.keymap.set('n', '<leader>tp', ':Template<CR>', { desc = 'Insert Template' })

Snippet Library Organization

~/.config/nvim/snippets/

├│── all.lua # Global snippets

├│── lua.lua # Lua-specific

├│── python.lua # Python-specific

├│── javascript.lua # JavaScript-specific

├│── typescript.lua # TypeScript-specific

├│── rust.lua # Rust-specific

├│── go.lua # Go-specific └── vscode/ # VSCode-style snippets ├── package.json ├── javascript.json └── python.json

Snippet Categories

Organize by category:


-- ~/.config/nvim/snippets/python.lua
local ls = require('luasnip')
local s = ls.snippet
local fmt = require('luasnip.extras.fmt').fmt
local i = ls.insert_node

local snippets = {

  -- Control flow
  control = {
    s('if', fmt('if {}:\n    {}', { i(1), i(0) })),
    s('for', fmt('for {} in {}:\n    {}', { i(1), i(2), i(0) })),
  },
  

  -- Functions
  functions = {
    s('def', fmt('def {}({}):\n    {}', { i(1), i(2), i(0) })),
    s('async', fmt('async def {}({}):\n    {}', { i(1), i(2), i(0) })),
  },
  

  -- Testing
  testing = {
    s('test', fmt('def test_{}():\n    {}', { i(1), i(0) })),
    s('assert', fmt('assert {} == {}', { i(1), i(2) })),
  },
}


-- Flatten categories
local all_snippets = {}
for _, category in pairs(snippets) do
  for _, snip in ipairs(category) do
    table.insert(all_snippets, snip)
  end
end

return all_snippets

Summary: Chapter #34 – Snippets and Templates

This chapter covered comprehensive snippet management:

  1. LuaSnip – Modern, flexible Lua-based snippet engine

  2. Snippet Creation – Simple to advanced snippet patterns

  3. Language-Specific – Targeted snippets for different languages

  4. Advanced Techniques – Context-aware, regex triggers, dynamic content

  5. nvim-cmp Integration – Seamless completion workflow

  6. VSCode Snippets – Compatible with existing snippet libraries

  7. Auto-Snippets – Self-expanding snippets

  8. Templates – File templates and skeletons

  9. Organization – Best practices for snippet management

Mastering snippets dramatically improves coding speed and consistency, making them an essential tool in modern development workflows.


Chapter #35: Terminal Integration

Neovim’s built-in terminal emulator provides seamless integration between your editor and shell commands, enabling a unified workflow without leaving your editing environment.

Understanding Neovim’s Terminal

Terminal Basics

Neovim’s terminal is a fully-featured terminal emulator built into the editor:

  • True Terminal: Not just command execution—full terminal emulation

  • Buffer-Based: Terminals are regular buffers with special properties

  • Job Control: Asynchronous process management

  • Multiple Modes: Normal, Insert, and Terminal modes

Opening Terminals


-- Horizontal split terminal
vim.keymap.set('n', '<leader>th', ':split | terminal<CR>', 
  { desc = 'Terminal Horizontal' })


-- Vertical split terminal
vim.keymap.set('n', '<leader>tv', ':vsplit | terminal<CR>', 
  { desc = 'Terminal Vertical' })


-- Full window terminal
vim.keymap.set('n', '<leader>tt', ':terminal<CR>', 
  { desc = 'Terminal' })


-- New tab terminal
vim.keymap.set('n', '<leader>tT', ':tabnew | terminal<CR>', 
  { desc = 'Terminal Tab' })


-- Terminal with specific command
vim.keymap.set('n', '<leader>tg', ':terminal lazygit<CR>', 
  { desc = 'LazyGit' })


-- Terminal with size
vim.keymap.set('n', '<leader>tb', ':15split | terminal<CR>', 
  { desc = 'Terminal Bottom' })

Terminal Mode Navigation


-- Exit terminal mode
vim.keymap.set('t', '<Esc><Esc>', '<C-\\><C-n>', 
  { desc = 'Exit Terminal Mode' })


-- Alternative exit (more convenient)
vim.keymap.set('t', '<C-x>', '<C-\\><C-n>', 
  { desc = 'Exit Terminal Mode' })


-- Window navigation from terminal
vim.keymap.set('t', '<C-h>', '<C-\\><C-n><C-w>h', 
  { desc = 'Go to Left Window' })
vim.keymap.set('t', '<C-j>', '<C-\\><C-n><C-w>j', 
  { desc = 'Go to Down Window' })
vim.keymap.set('t', '<C-k>', '<C-\\><C-n><C-w>k', 
  { desc = 'Go to Up Window' })
vim.keymap.set('t', '<C-l>', '<C-\\><C-n><C-w>l', 
  { desc = 'Go to Right Window' })


-- Send Ctrl-c to terminal (not exit)
vim.keymap.set('t', '<C-c><C-c>', '<C-c>', 
  { desc = 'Send Ctrl-C' })


-- Paste from register
vim.keymap.set('t', '<C-r>', [['<C-\><C-N>"' . nr2char(getchar()) . 'pi']], 
  { expr = true, desc = 'Paste Register' })

Advanced Terminal Configuration

Smart Terminal Setup


-- Terminal settings
vim.api.nvim_create_autocmd('TermOpen', {
  group = vim.api.nvim_create_augroup('custom-term-open', { clear = true }),
  callback = function()

    -- Disable line numbers in terminal
    vim.opt_local.number = false
    vim.opt_local.relativenumber = false
    

    -- Disable sign column
    vim.opt_local.signcolumn = 'no'
    

    -- Start in insert mode
    vim.cmd('startinsert')
    

    -- Set buffer options
    vim.opt_local.buflisted = false
    vim.opt_local.filetype = 'terminal'
    

    -- Custom buffer-local keymaps
    local opts = { buffer = 0 }
    vim.keymap.set('t', '<Esc>', '<C-\\><C-n>', opts)
    vim.keymap.set('t', '<C-v><Esc>', '<Esc>', opts)
  end,
})


-- Auto-close terminal when process exits
vim.api.nvim_create_autocmd('TermClose', {
  callback = function()

    -- Only close if exit status is 0
    if vim.v.event.status == 0 then
      vim.cmd('bdelete!')
    end
  end,
})

Terminal Window Management


-- Toggle terminal function
local term_buf = nil
local term_win = nil

local function toggle_terminal()

  -- Check if terminal buffer exists and is valid
  if term_buf and vim.api.nvim_buf_is_valid(term_buf) then

    -- Check if terminal window is visible
    if term_win and vim.api.nvim_win_is_valid(term_win) then

      -- Hide terminal
      vim.api.nvim_win_hide(term_win)
      term_win = nil
    else

      -- Show terminal in split
      vim.cmd('botright 15split')
      term_win = vim.api.nvim_get_current_win()
      vim.api.nvim_win_set_buf(term_win, term_buf)
      vim.cmd('startinsert')
    end
  else

    -- Create new terminal
    vim.cmd('botright 15split')
    vim.cmd('terminal')
    term_buf = vim.api.nvim_get_current_buf()
    term_win = vim.api.nvim_get_current_win()
    

    -- Set local options
    vim.api.nvim_buf_set_option(term_buf, 'buflisted', false)
  end
end

vim.keymap.set({'n', 't'}, '<C-\\>', toggle_terminal, 
  { desc = 'Toggle Terminal' })

Floating Terminal

local function create_floating_terminal(cmd)
  cmd = cmd or vim.o.shell
  

  -- Get editor dimensions
  local width = vim.o.columns
  local height = vim.o.lines
  

  -- Calculate floating window size
  local win_height = math.ceil(height * 0.8)
  local win_width = math.ceil(width * 0.8)
  

  -- Calculate starting position
  local row = math.ceil((height - win_height) / 2)
  local col = math.ceil((width - win_width) / 2)
  

  -- Create buffer
  local buf = vim.api.nvim_create_buf(false, true)
  

  -- Define window configuration
  local opts = {
    relative = 'editor',
    width = win_width,
    height = win_height,
    row = row,
    col = col,
    style = 'minimal',
    border = 'rounded',
  }
  

  -- Create window
  local win = vim.api.nvim_open_win(buf, true, opts)
  

  -- Start terminal
  vim.fn.termopen(cmd, {
    on_exit = function()
      vim.api.nvim_buf_delete(buf, { force = true })
    end,
  })
  

  -- Enter insert mode
  vim.cmd('startinsert')
  

  -- Close on <Esc><Esc>
  vim.keymap.set('t', '<Esc><Esc>', '<C-\\><C-n>:q<CR>', 
    { buffer = buf, silent = true })
end

vim.keymap.set('n', '<leader>tf', function()
  create_floating_terminal()
end, { desc = 'Floating Terminal' })

vim.keymap.set('n', '<leader>tg', function()
  create_floating_terminal('lazygit')
end, { desc = 'LazyGit Floating' })

Running Commands

Execute and Display Output


-- Run command and show in split
vim.keymap.set('n', '<leader>r', function()
  vim.ui.input({ prompt = 'Command: ' }, function(input)
    if input then
      vim.cmd('split | terminal ' .. input)
    end
  end)
end, { desc = 'Run Command' })


-- Run current file
vim.keymap.set('n', '<leader>x', function()
  local filetype = vim.bo.filetype
  local filename = vim.fn.expand('%')
  
  local runners = {
    python = 'python3 ' .. filename,
    lua = 'lua ' .. filename,
    javascript = 'node ' .. filename,
    typescript = 'ts-node ' .. filename,
    go = 'go run ' .. filename,
    rust = 'cargo run',
    sh = 'bash ' .. filename,
  }
  
  local cmd = runners[filetype]
  if cmd then
    vim.cmd('split | resize 15 | terminal ' .. cmd)
  else
    print('No runner configured for ' .. filetype)
  end
end, { desc = 'Execute File' })

Send Text to Terminal


-- Function to send text to terminal
local function send_to_term(text)

  -- Find terminal buffer
  local term_bufs = vim.tbl_filter(function(buf)
    return vim.bo[buf].buftype == 'terminal'
  end, vim.api.nvim_list_bufs())
  
  if #term_bufs == 0 then
    print('No terminal buffer found')
    return
  end
  

  -- Use most recent terminal
  local term_buf = term_bufs[#term_bufs]
  

  -- Get terminal channel
  local chan = vim.bo[term_buf].channel
  
  if chan then

    -- Send text
    vim.fn.chansend(chan, text .. '\n')
  end
end


-- Send current line
vim.keymap.set('n', '<leader>ts', function()
  local line = vim.api.nvim_get_current_line()
  send_to_term(line)
end, { desc = 'Send Line to Terminal' })


-- Send visual selection
vim.keymap.set('v', '<leader>ts', function()
  local start_line = vim.fn.line('v')
  local end_line = vim.fn.line('.')
  

  -- Get lines in correct order
  if start_line > end_line then
    start_line, end_line = end_line, start_line
  end
  
  local lines = vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
  send_to_term(table.concat(lines, '\n'))
end, { desc = 'Send Selection to Terminal' })


-- Send entire buffer
vim.keymap.set('n', '<leader>tS', function()
  local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
  send_to_term(table.concat(lines, '\n'))
end, { desc = 'Send Buffer to Terminal' })

REPL Integration

Interactive REPL Setup

local M = {}


-- REPL configurations
M.repls = {
  python = 'python3',
  lua = 'lua',
  node = 'node',
  ruby = 'irb',
  r = 'R',
  julia = 'julia',
}


-- Track REPL buffers per filetype
M.repl_buffers = {}

function M.start_repl()
  local ft = vim.bo.filetype
  local cmd = M.repls[ft]
  
  if not cmd then
    print('No REPL configured for ' .. ft)
    return
  end
  

  -- Check if REPL already exists
  if M.repl_buffers[ft] and vim.api.nvim_buf_is_valid(M.repl_buffers[ft]) then

    -- Focus existing REPL
    local wins = vim.fn.win_findbuf(M.repl_buffers[ft])
    if #wins > 0 then
      vim.api.nvim_set_current_win(wins[1])
    else
      vim.cmd('botright 15split')
      vim.api.nvim_win_set_buf(0, M.repl_buffers[ft])
    end
    vim.cmd('startinsert')
    return
  end
  

  -- Create new REPL
  vim.cmd('botright 15split')
  vim.cmd('terminal ' .. cmd)
  M.repl_buffers[ft] = vim.api.nvim_get_current_buf()
  

  -- Mark as REPL buffer
  vim.b.is_repl = true
end

function M.send_to_repl(text)
  local ft = vim.bo.filetype
  local repl_buf = M.repl_buffers[ft]
  
  if not repl_buf or not vim.api.nvim_buf_is_valid(repl_buf) then
    print('No REPL running for ' .. ft)
    return
  end
  
  local chan = vim.bo[repl_buf].channel
  if chan then
    vim.fn.chansend(chan, text .. '\n')
  end
end


-- Keymaps
vim.keymap.set('n', '<leader>rs', M.start_repl, { desc = 'Start REPL' })

vim.keymap.set('n', '<leader>rl', function()
  local line = vim.api.nvim_get_current_line()
  M.send_to_repl(line)
end, { desc = 'Send Line to REPL' })

vim.keymap.set('v', '<leader>rs', function()
  local start_line = vim.fn.line('v')
  local end_line = vim.fn.line('.')
  
  if start_line > end_line then
    start_line, end_line = end_line, start_line
  end
  
  local lines = vim.api.nvim_buf_get_lines(0, start_line - 1, end_line, false)
  M.send_to_repl(table.concat(lines, '\n'))
end, { desc = 'Send Selection to REPL' })

return M

Terminal Buffer Management

Named Terminals

local terminals = {}

local function create_named_terminal(name)
  name = name or 'default'
  

  -- Check if terminal exists
  if terminals[name] and vim.api.nvim_buf_is_valid(terminals[name]) then

    -- Switch to existing terminal
    local wins = vim.fn.win_findbuf(terminals[name])
    if #wins > 0 then
      vim.api.nvim_set_current_win(wins[1])
    else
      vim.cmd('botright 15split')
      vim.api.nvim_win_set_buf(0, terminals[name])
    end
    vim.cmd('startinsert')
  else

    -- Create new terminal
    vim.cmd('botright 15split')
    vim.cmd('terminal')
    terminals[name] = vim.api.nvim_get_current_buf()
    

    -- Set buffer name
    vim.api.nvim_buf_set_name(terminals[name], 'term://' .. name)
  end
end

vim.api.nvim_create_user_command('Term', function(opts)
  create_named_terminal(opts.args)
end, { nargs = '?' })


-- Quick access to named terminals
vim.keymap.set('n', '<leader>t1', function()
  create_named_terminal('main')
end, { desc = 'Terminal: main' })

vim.keymap.set('n', '<leader>t2', function()
  create_named_terminal('test')
end, { desc = 'Terminal: test' })

vim.keymap.set('n', '<leader>t3', function()
  create_named_terminal('dev')
end, { desc = 'Terminal: dev' })

Terminal List and Selection

local function list_terminals()
  local terms = {}
  
  for _, buf in ipairs(vim.api.nvim_list_bufs()) do
    if vim.bo[buf].buftype == 'terminal' then
      local name = vim.api.nvim_buf_get_name(buf)
      table.insert(terms, {
        buf = buf,
        name = name,
        display = name:match('term://(.+)') or name,
      })
    end
  end
  
  return terms
end

vim.keymap.set('n', '<leader>tl', function()
  local terms = list_terminals()
  
  if #terms == 0 then
    print('No terminal buffers')
    return
  end
  
  vim.ui.select(terms, {
    prompt = 'Select terminal:',
    format_item = function(item)
      return item.display
    end,
  }, function(choice)
    if choice then
      vim.cmd('buffer ' .. choice.buf)
      vim.cmd('startinsert')
    end
  end)
end, { desc = 'List Terminals' })

Job Control

Background Jobs


-- Run command in background
local function run_background_job(cmd, on_complete)
  local output = {}
  
  vim.fn.jobstart(cmd, {
    on_stdout = function(_, data)
      if data then
        vim.list_extend(output, data)
      end
    end,
    on_stderr = function(_, data)
      if data then
        vim.list_extend(output, data)
      end
    end,
    on_exit = function(_, exit_code)
      if on_complete then
        on_complete(output, exit_code)
      end
    end,
    stdout_buffered = true,
    stderr_buffered = true,
  })
end


-- Example: Run tests in background
vim.keymap.set('n', '<leader>mt', function()
  print('Running tests...')
  
  run_background_job('npm test', function(output, exit_code)
    if exit_code == 0 then
      print('Tests passed! ✓')
    else
      print('Tests failed! ✗')

      -- Show output in quickfix
      vim.fn.setqflist({}, 'r', {
        title = 'Test Results',
        lines = output,
      })
      vim.cmd('copen')
    end
  end)
end, { desc = 'Run Tests' })

Interactive Job Control

local function run_with_progress(cmd)
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_option(buf, 'buftype', 'nofile')
  vim.api.nvim_buf_set_option(buf, 'bufhidden', 'wipe')
  

  -- Open in split
  vim.cmd('botright 15split')
  vim.api.nvim_win_set_buf(0, buf)
  
  local job_id = vim.fn.jobstart(cmd, {
    on_stdout = function(_, data)
      if data then
        vim.api.nvim_buf_set_lines(buf, -1, -1, false, data)
      end
    end,
    on_stderr = function(_, data)
      if data then
        vim.api.nvim_buf_set_lines(buf, -1, -1, false, data)
      end
    end,
    on_exit = function(_, exit_code)
      local msg = exit_code == 0 and 'Success!' or 'Failed!'
      vim.api.nvim_buf_set_lines(buf, -1, -1, false, {
        '',
        '----------------------------------------',
        msg,
      })
      

      -- Auto-close on success after delay
      if exit_code == 0 then
        vim.defer_fn(function()
          if vim.api.nvim_buf_is_valid(buf) then
            vim.api.nvim_buf_delete(buf, { force = true })
          end
        end, 2000)
      end
    end,
  })
  

  -- Allow canceling job
  vim.keymap.set('n', 'q', function()
    vim.fn.jobstop(job_id)
    vim.cmd('bdelete!')
  end, { buffer = buf, desc = 'Cancel Job' })
end

vim.keymap.set('n', '<leader>mb', function()
  run_with_progress('npm run build')
end, { desc = 'Run Build' })

Terminal Plugins

toggleterm.nvim

{
  'akinsho/toggleterm.nvim',
  version = '*',
  config = function()
    require('toggleterm').setup({
      size = function(term)
        if term.direction == 'horizontal' then
          return 15
        elseif term.direction == 'vertical' then
          return vim.o.columns * 0.4
        end
      end,
      open_mapping = [[<C-\>]],
      hide_numbers = true,
      shade_terminals = true,
      shading_factor = 2,
      start_in_insert = true,
      insert_mappings = true,
      terminal_mappings = true,
      persist_size = true,
      persist_mode = true,
      direction = 'float',
      close_on_exit = true,
      shell = vim.o.shell,
      float_opts = {
        border = 'curved',
        winblend = 0,
        highlights = {
          border = 'Normal',
          background = 'Normal',
        },
      },
    })
    

    -- Custom terminals
    local Terminal = require('toggleterm.terminal').Terminal
    

    -- LazyGit
    local lazygit = Terminal:new({
      cmd = 'lazygit',
      hidden = true,
      direction = 'float',
      float_opts = {
        border = 'none',
        width = 100000,
        height = 100000,
      },
      on_open = function(_)
        vim.cmd('startinsert!')
      end,
    })
    
    vim.keymap.set('n', '<leader>tg', function()
      lazygit:toggle()
    end, { desc = 'LazyGit' })
    

    -- Python REPL
    local python = Terminal:new({
      cmd = 'python3',
      hidden = true,
      direction = 'horizontal',
    })
    
    vim.keymap.set('n', '<leader>tp', function()
      python:toggle()
    end, { desc = 'Python REPL' })
    

    -- htop
    local htop = Terminal:new({
      cmd = 'htop',
      hidden = true,
      direction = 'float',
    })
    
    vim.keymap.set('n', '<leader>th', function()
      htop:toggle()
    end, { desc = 'htop' })
  end,
}

FTerm.nvim (Alternative)

{
  'numToStr/FTerm.nvim',
  config = function()
    require('FTerm').setup({
      border = 'rounded',
      dimensions = {
        height = 0.9,
        width = 0.9,
      },
    })
    
    vim.keymap.set('n', '<C-\\>', '<CMD>lua require("FTerm").toggle()<CR>')
    vim.keymap.set('t', '<C-\\>', '<C-\\><C-n><CMD>lua require("FTerm").toggle()<CR>')
    

    -- Custom terminal for lazygit
    local fterm = require('FTerm')
    local lazygit = fterm:new({
      ft = 'fterm_lazygit',
      cmd = 'lazygit',
      dimensions = {
        height = 0.9,
        width = 0.9,
      },
    })
    
    vim.keymap.set('n', '<leader>tg', function()
      lazygit:toggle()
    end, { desc = 'LazyGit' })
  end,
}

Terminal Integration Patterns

Build and Test Runners

local M = {}

function M.run_make_target(target)
  vim.cmd('split | resize 15 | terminal make ' .. (target or ''))
end

function M.run_npm_script()

  -- Parse package.json for scripts
  local package_json = vim.fn.json_decode(vim.fn.readfile('package.json'))
  local scripts = vim.tbl_keys(package_json.scripts or {})
  
  vim.ui.select(scripts, {
    prompt = 'Select npm script:',
  }, function(choice)
    if choice then
      vim.cmd('split | resize 15 | terminal npm run ' .. choice)
    end
  end)
end

function M.run_cargo_command()
  local commands = {
    'build',
    'run',
    'test',
    'check',
    'clippy',
    'fmt',
  }
  
  vim.ui.select(commands, {
    prompt = 'Select cargo command:',
  }, function(choice)
    if choice then
      vim.cmd('split | resize 15 | terminal cargo ' .. choice)
    end
  end)
end


-- Keymaps
vim.keymap.set('n', '<leader>mm', function()
  M.run_make_target()
end, { desc = 'Make' })

vim.keymap.set('n', '<leader>mn', M.run_npm_script, { desc = 'NPM Script' })
vim.keymap.set('n', '<leader>mc', M.run_cargo_command, { desc = 'Cargo Command' })

return M

Git Integration

local function git_command(cmd)
  vim.cmd('split | resize 15 | terminal git ' .. cmd)
end

vim.keymap.set('n', '<leader>gs', function()
  git_command('status')
end, { desc = 'Git Status' })

vim.keymap.set('n', '<leader>gl', function()
  git_command('log --oneline -20')
end, { desc = 'Git Log' })

vim.keymap.set('n', '<leader>gd', function()
  git_command('diff')
end, { desc = 'Git Diff' })

vim.keymap.set('n', '<leader>gb', function()
  git_command('blame ' .. vim.fn.expand('%'))
end, { desc = 'Git Blame' })


-- Interactive git commands
vim.keymap.set('n', '<leader>gc', function()
  vim.ui.input({ prompt = 'Git command: ' }, function(input)
    if input then
      git_command(input)
    end
  end)
end, { desc = 'Git Command' })

Quick Terminal Actions


-- Open terminal in project root
vim.keymap.set('n', '<leader>tr', function()
  local root = vim.fn.getcwd()
  vim.cmd('split | terminal')
  vim.fn.chansend(vim.b.terminal_job_id, 'cd ' .. root .. '\n')
end, { desc = 'Terminal at Root' })


-- Open terminal in current file's directory
vim.keymap.set('n', '<leader>td', function()
  local dir = vim.fn.expand('%:p:h')
  vim.cmd('split | terminal')
  vim.fn.chansend(vim.b.terminal_job_id, 'cd ' .. dir .. '\n')
end, { desc = 'Terminal at Current Dir' })


-- Run last command
local last_command = ''

vim.keymap.set('n', '<leader>tc', function()
  vim.ui.input({
    prompt = 'Command: ',
    default = last_command,
  }, function(input)
    if input then
      last_command = input
      vim.cmd('split | resize 15 | terminal ' .. input)
    end
  end)
end, { desc = 'Run Command' })

vim.keymap.set('n', '<leader>tR', function()
  if last_command ~= '' then
    vim.cmd('split | resize 15 | terminal ' .. last_command)
  else
    print('No previous command')
  end
end, { desc = 'Repeat Last Command' })

Terminal Scrollback

Scrollback Configuration


-- Set scrollback limit
vim.opt.scrollback = 10000


-- Easier scrollback navigation
vim.api.nvim_create_autocmd('TermOpen', {
  callback = function()
    local opts = { buffer = 0 }
    

    -- Use normal mode for scrolling
    vim.keymap.set('t', '<C-[>', '<C-\\><C-n>', opts)
    

    -- Scroll up/down in terminal mode
    vim.keymap.set('t', '<PageUp>', '<C-\\><C-n><PageUp>', opts)
    vim.keymap.set('t', '<PageDown>', '<C-\\><C-n><PageDown>', opts)
    

    -- Search in terminal buffer
    vim.keymap.set('t', '<C-f>', '<C-\\><C-n>/', opts)
  end,
})

Summary: Chapter #35 – Terminal Integration

This chapter covered comprehensive terminal integration:

  1. Built-in Terminal – Neovim’s native terminal emulator capabilities

  2. Navigation – Efficient movement between terminal and editor modes

  3. Window Management – Smart terminal positioning and toggling

  4. Floating Terminals – Modal terminal overlays

  5. Command Execution – Running commands and capturing output

  6. REPL Integration – Interactive programming language shells

  7. Named Terminals – Managing multiple persistent terminals

  8. Job Control – Background processes and asynchronous execution

  9. Plugin Options – toggleterm.nvim and FTerm.nvim

  10. Common Patterns – Build runners, git integration, and workflow automation

Mastering terminal integration creates a seamless development environment where you can code, test, debug, and manage your project without leaving Neovim.


Chapter #36: Quality of Life Improvements

Small tweaks and configurations that dramatically improve your daily Neovim experience through enhanced usability, comfort, and efficiency.

Better Defaults

Essential Options


-- Better editing experience
vim.opt.number = true              -- Show line numbers
vim.opt.relativenumber = true      -- Relative line numbers
vim.opt.cursorline = true          -- Highlight current line
vim.opt.signcolumn = 'yes'         -- Always show sign column
vim.opt.wrap = false               -- No line wrapping


-- Smarter search
vim.opt.ignorecase = true          -- Case insensitive search
vim.opt.smartcase = true           -- Unless capital letter used
vim.opt.hlsearch = true            -- Highlight search matches
vim.opt.incsearch = true           -- Show matches while typing


-- Better indentation
vim.opt.expandtab = true           -- Use spaces instead of tabs
vim.opt.shiftwidth = 2             -- Indent width
vim.opt.tabstop = 2                -- Tab width
vim.opt.softtabstop = 2            -- Backspace removes this many spaces
vim.opt.smartindent = true         -- Auto-indent new lines


-- Better splits
vim.opt.splitbelow = true          -- Horizontal splits go below
vim.opt.splitright = true          -- Vertical splits go right


-- Better completion
vim.opt.completeopt = {'menu', 'menuone', 'noselect'}
vim.opt.pumheight = 10             -- Max completion menu height


-- Better performance
vim.opt.updatetime = 250           -- Faster completion
vim.opt.timeoutlen = 300           -- Faster key sequences
vim.opt.lazyredraw = true          -- Don't redraw during macros


-- Better visual feedback
vim.opt.showmode = false           -- Don't show mode (in statusline)
vim.opt.showcmd = true             -- Show command in bottom bar
vim.opt.cmdheight = 1              -- Command line height
vim.opt.ruler = true               -- Show cursor position


-- Better file handling
vim.opt.hidden = true              -- Keep buffers open
vim.opt.backup = false             -- No backup files
vim.opt.writebackup = false        -- No backup while editing
vim.opt.swapfile = false           -- No swap files
vim.opt.autoread = true            -- Auto-reload changed files
vim.opt.autowrite = true           -- Auto-save before some commands


-- Better undo
vim.opt.undofile = true            -- Persistent undo
vim.opt.undolevels = 10000         -- Maximum undo levels


-- Better mouse support
vim.opt.mouse = 'a'                -- Enable mouse in all modes


-- Better clipboard
vim.opt.clipboard = 'unnamedplus'  -- Use system clipboard


-- Better scrolling
vim.opt.scrolloff = 8              -- Keep 8 lines visible when scrolling
vim.opt.sidescrolloff = 8          -- Same for horizontal scrolling


-- Better wildcards
vim.opt.wildmode = {'longest:full', 'full'}
vim.opt.wildignore = {
  '*.o', '*.obj', '*.pyc',
  '*~', '*.swp',
  '.git/*', 'node_modules/*', '__pycache__/*',
}

Automatic Directory Setup


-- Create undo directory if it doesn't exist
local undodir = vim.fn.stdpath('data') .. '/undo'
if vim.fn.isdirectory(undodir) == 0 then
  vim.fn.mkdir(undodir, 'p')
end
vim.opt.undodir = undodir


-- Auto-create parent directories when saving
vim.api.nvim_create_autocmd('BufWritePre', {
  group = vim.api.nvim_create_augroup('auto-mkdir', { clear = true }),
  callback = function(event)
    local file = vim.loop.fs_realpath(event.match) or event.match
    local dir = vim.fn.fnamemodify(file, ':h')
    
    if vim.fn.isdirectory(dir) == 0 then
      vim.fn.mkdir(dir, 'p')
    end
  end,
})

Smart Navigation Enhancements

Improved Line Movement


-- Move by visual lines when wrapping
vim.keymap.set({'n', 'v'}, 'j', 'gj', { desc = 'Move Down (Visual)' })
vim.keymap.set({'n', 'v'}, 'k', 'gk', { desc = 'Move Up (Visual)' })
vim.keymap.set({'n', 'v'}, 'gj', 'j', { desc = 'Move Down (Real)' })
vim.keymap.set({'n', 'v'}, 'gk', 'k', { desc = 'Move Up (Real)' })


-- Better line start/end
vim.keymap.set({'n', 'v'}, 'H', '^', { desc = 'Start of Line' })
vim.keymap.set({'n', 'v'}, 'L', '$', { desc = 'End of Line' })


-- Move lines up/down
vim.keymap.set('n', '<A-j>', ':m .+1<CR>==', { desc = 'Move Line Down' })
vim.keymap.set('n', '<A-k>', ':m .-2<CR>==', { desc = 'Move Line Up' })
vim.keymap.set('v', '<A-j>', ":m '>+1<CR>gv=gv", { desc = 'Move Selection Down' })
vim.keymap.set('v', '<A-k>', ":m '<-2<CR>gv=gv", { desc = 'Move Selection Up' })


-- Better page up/down (keep cursor centered)
vim.keymap.set('n', '<C-d>', '<C-d>zz', { desc = 'Page Down (Centered)' })
vim.keymap.set('n', '<C-u>', '<C-u>zz', { desc = 'Page Up (Centered)' })


-- Better search navigation (keep cursor centered)
vim.keymap.set('n', 'n', 'nzzzv', { desc = 'Next Match (Centered)' })
vim.keymap.set('n', 'N', 'Nzzzv', { desc = 'Prev Match (Centered)' })

Quick Window Navigation


-- Simplified window movement
vim.keymap.set('n', '<C-h>', '<C-w>h', { desc = 'Go to Left Window' })
vim.keymap.set('n', '<C-j>', '<C-w>j', { desc = 'Go to Down Window' })
vim.keymap.set('n', '<C-k>', '<C-w>k', { desc = 'Go to Up Window' })
vim.keymap.set('n', '<C-l>', '<C-w>l', { desc = 'Go to Right Window' })


-- Window resizing
vim.keymap.set('n', '<C-Up>', ':resize +2<CR>', { desc = 'Increase Height' })
vim.keymap.set('n', '<C-Down>', ':resize -2<CR>', { desc = 'Decrease Height' })
vim.keymap.set('n', '<C-Left>', ':vertical resize -2<CR>', { desc = 'Decrease Width' })
vim.keymap.set('n', '<C-Right>', ':vertical resize +2<CR>', { desc = 'Increase Width' })


-- Equalize windows
vim.keymap.set('n', '<leader>we', '<C-w>=', { desc = 'Equalize Windows' })


-- Maximize current window
vim.keymap.set('n', '<leader>wm', '<C-w>|<C-w>_', { desc = 'Maximize Window' })

Smart Editing Features

Better Indentation


-- Stay in indent mode
vim.keymap.set('v', '<', '<gv', { desc = 'Indent Left' })
vim.keymap.set('v', '>', '>gv', { desc = 'Indent Right' })


-- Auto-indent entire file and return to position
vim.keymap.set('n', '<leader>i', function()
  local view = vim.fn.winsaveview()
  vim.cmd('normal! gg=G')
  vim.fn.winrestview(view)
end, { desc = 'Auto-Indent File' })

Smart Paste


-- Paste without overwriting register in visual mode
vim.keymap.set('v', 'p', '"_dP', { desc = 'Paste (Keep Register)' })


-- Paste from system clipboard
vim.keymap.set({'n', 'v'}, '<leader>p', '"+p', { desc = 'Paste from Clipboard' })
vim.keymap.set({'n', 'v'}, '<leader>P', '"+P', { desc = 'Paste Before from Clipboard' })


-- Yank to system clipboard
vim.keymap.set({'n', 'v'}, '<leader>y', '"+y', { desc = 'Yank to Clipboard' })
vim.keymap.set('n', '<leader>Y', '"+Y', { desc = 'Yank Line to Clipboard' })

Quick Deletion


-- Delete without yanking
vim.keymap.set({'n', 'v'}, '<leader>d', '"_d', { desc = 'Delete (No Yank)' })
vim.keymap.set('n', '<leader>D', '"_D', { desc = 'Delete to End (No Yank)' })


-- Change without yanking
vim.keymap.set({'n', 'v'}, '<leader>c', '"_c', { desc = 'Change (No Yank)' })
vim.keymap.set('n', '<leader>C', '"_C', { desc = 'Change to End (No Yank)' })


-- Delete blank lines
vim.keymap.set('n', '<leader>db', function()
  vim.cmd([[g/^$/d]])
end, { desc = 'Delete Blank Lines' })

Quick Text Operations


-- Join lines without moving cursor
vim.keymap.set('n', 'J', 'mzJ`z', { desc = 'Join Lines' })


-- Duplicate line or selection
vim.keymap.set('n', '<leader>ld', 'yyp', { desc = 'Duplicate Line' })
vim.keymap.set('v', '<leader>ld', 'y`>p', { desc = 'Duplicate Selection' })


-- Insert blank line above/below
vim.keymap.set('n', '[<Space>', ':put! =\'\'<CR>', { desc = 'Blank Line Above' })
vim.keymap.set('n', ']<Space>', ':put =\'\'<CR>', { desc = 'Blank Line Below' })


-- Quick substitution for word under cursor
vim.keymap.set('n', '<leader>s', [[:%s/\<<C-r><C-w>\>/<C-r><C-w>/gI<Left><Left><Left>]], 
  { desc = 'Substitute Word' })


-- Convert case of word under cursor
vim.keymap.set('n', '<leader>u', 'viwu', { desc = 'Lowercase Word' })
vim.keymap.set('n', '<leader>U', 'viwU', { desc = 'Uppercase Word' })


-- Surround word with quotes
vim.keymap.set('n', '<leader>"', 'viw<Esc>a"<Esc>bi"<Esc>', { desc = 'Surround Double Quotes' })
vim.keymap.set('n', "<leader>'", "viw<Esc>a'<Esc>bi'<Esc>", { desc = 'Surround Single Quotes' })

Better Visual Feedback

Highlight on Yank

vim.api.nvim_create_autocmd('TextYankPost', {
  group = vim.api.nvim_create_augroup('highlight-yank', { clear = true }),
  callback = function()
    vim.highlight.on_yank({
      higroup = 'IncSearch',
      timeout = 200,
    })
  end,
})

Clear Search Highlighting


-- Clear search highlight on <Esc>
vim.keymap.set('n', '<Esc>', '<cmd>nohlsearch<CR>', { desc = 'Clear Highlight' })


-- Auto-clear highlight after search
vim.api.nvim_create_autocmd('CursorMoved', {
  group = vim.api.nvim_create_augroup('auto-hlsearch', { clear = true }),
  callback = function()
    if vim.v.hlsearch == 1 and vim.fn.searchcount().current == 0 then
      vim.cmd.nohlsearch()
    end
  end,
})

Better Diagnostics Display


-- Diagnostic configuration
vim.diagnostic.config({
  virtual_text = {
    prefix = '●',
    spacing = 4,
  },
  signs = true,
  underline = true,
  update_in_insert = false,
  severity_sort = true,
  float = {
    border = 'rounded',
    source = 'always',
    header = '',
    prefix = '',
  },
})


-- Diagnostic signs
local signs = {
  Error = '✘',
  Warn = '▲',
  Hint = '⚑',
  Info = '»',
}

for type, icon in pairs(signs) do
  local hl = 'DiagnosticSign' .. type
  vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
end


-- Diagnostic navigation
vim.keymap.set('n', '[d', vim.diagnostic.goto_prev, { desc = 'Prev Diagnostic' })
vim.keymap.set('n', ']d', vim.diagnostic.goto_next, { desc = 'Next Diagnostic' })
vim.keymap.set('n', '<leader>e', vim.diagnostic.open_float, { desc = 'Show Diagnostic' })
vim.keymap.set('n', '<leader>q', vim.diagnostic.setloclist, { desc = 'Diagnostic List' })

Buffer Management

Smart Buffer Navigation


-- Next/previous buffer
vim.keymap.set('n', ']b', ':bnext<CR>', { desc = 'Next Buffer' })
vim.keymap.set('n', '[b', ':bprevious<CR>', { desc = 'Prev Buffer' })


-- Delete buffer without closing window
vim.keymap.set('n', '<leader>bd', function()
  local buf = vim.api.nvim_get_current_buf()
  local alt_buf = vim.fn.bufnr('#')
  

  -- Try to switch to alternate buffer
  if alt_buf ~= -1 and vim.api.nvim_buf_is_valid(alt_buf) then
    vim.cmd('buffer ' .. alt_buf)
  else
    vim.cmd('bprevious')
  end
  

  -- Delete the original buffer
  vim.api.nvim_buf_delete(buf, { force = false })
end, { desc = 'Delete Buffer' })


-- Force delete buffer
vim.keymap.set('n', '<leader>bD', function()
  vim.cmd('bdelete!')
end, { desc = 'Force Delete Buffer' })


-- Delete all buffers except current
vim.keymap.set('n', '<leader>bo', function()
  local current = vim.api.nvim_get_current_buf()
  for _, buf in ipairs(vim.api.nvim_list_bufs()) do
    if buf ~= current and vim.api.nvim_buf_is_valid(buf) then
      vim.api.nvim_buf_delete(buf, { force = false })
    end
  end
end, { desc = 'Delete Other Buffers' })

Buffer Utilities


-- Save all buffers
vim.keymap.set('n', '<leader>wa', ':wa<CR>', { desc = 'Save All' })


-- Close all buffers and quit
vim.keymap.set('n', '<leader>Q', ':qa<CR>', { desc = 'Quit All' })


-- List modified buffers
vim.keymap.set('n', '<leader>bm', function()
  local modified = vim.tbl_filter(function(buf)
    return vim.api.nvim_buf_get_option(buf, 'modified')
  end, vim.api.nvim_list_bufs())
  
  if #modified == 0 then
    print('No modified buffers')
  else
    for _, buf in ipairs(modified) do
      local name = vim.api.nvim_buf_get_name(buf)
      print(name)
    end
  end
end, { desc = 'List Modified Buffers' })

Command-Line Enhancements

Better Command Mode


-- Command-line navigation
vim.keymap.set('c', '<C-a>', '<Home>', { desc = 'Start of Line' })
vim.keymap.set('c', '<C-e>', '<End>', { desc = 'End of Line' })
vim.keymap.set('c', '<C-p>', '<Up>', { desc = 'Previous Command' })
vim.keymap.set('c', '<C-n>', '<Down>', { desc = 'Next Command' })


-- Expand %% to current directory
vim.keymap.set('c', '%%', function()
  return vim.fn.expand('%:h') .. '/'
end, { expr = true, desc = 'Current Directory' })


-- Expand :: to current file
vim.keymap.set('c', '::', function()
  return vim.fn.expand('%:p')
end, { expr = true, desc = 'Current File Path' })

Quick Commands


-- Save as sudo
vim.keymap.set('n', '<leader>W', ':w !sudo tee % > /dev/null<CR>', 
  { desc = 'Save as Sudo' })


-- Quick save and quit
vim.keymap.set('n', '<leader>w', ':w<CR>', { desc = 'Save' })
vim.keymap.set('n', '<leader>q', ':q<CR>', { desc = 'Quit' })


-- Source current file
vim.keymap.set('n', '<leader><leader>x', function()
  vim.cmd('source %')
  print('Sourced ' .. vim.fn.expand('%'))
end, { desc = 'Source File' })


-- Make current file executable
vim.keymap.set('n', '<leader>x', function()
  local file = vim.fn.expand('%')
  vim.fn.system('chmod +x ' .. file)
  print('Made executable: ' .. file)
end, { desc = 'Make Executable' })

File Management

Quick File Operations


-- Rename current file
vim.keymap.set('n', '<leader>fr', function()
  local old_name = vim.fn.expand('%')
  local new_name = vim.fn.input('New name: ', old_name)
  
  if new_name ~= '' and new_name ~= old_name then
    vim.cmd('saveas ' .. new_name)
    vim.fn.delete(old_name)
    vim.cmd('bdelete ' .. vim.fn.bufnr(old_name))
  end
end, { desc = 'Rename File' })


-- Delete current file
vim.keymap.set('n', '<leader>fD', function()
  local file = vim.fn.expand('%')
  local choice = vim.fn.confirm('Delete ' .. file .. '?', '&Yes\n&No', 2)
  
  if choice == 1 then
    vim.fn.delete(file)
    vim.cmd('bdelete!')
    print('Deleted: ' .. file)
  end
end, { desc = 'Delete File' })


-- Copy current file path
vim.keymap.set('n', '<leader>fp', function()
  local path = vim.fn.expand('%:p')
  vim.fn.setreg('+', path)
  print('Copied: ' .. path)
end, { desc = 'Copy File Path' })


-- Copy current file name
vim.keymap.set('n', '<leader>fn', function()
  local name = vim.fn.expand('%:t')
  vim.fn.setreg('+', name)
  print('Copied: ' .. name)
end, { desc = 'Copy File Name' })

Recent Files


-- Quick access to recent files
vim.keymap.set('n', '<leader>fo', function()
  vim.cmd('browse oldfiles')
end, { desc = 'Recent Files' })


-- Alternative using Telescope (if available)
vim.keymap.set('n', '<leader>fh', function()
  if pcall(require, 'telescope') then
    require('telescope.builtin').oldfiles()
  else
    vim.cmd('browse oldfiles')
  end
end, { desc = 'File History' })

Auto-Commands

File-Type Specific Settings


-- Auto-format on save for specific filetypes
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = {'*.go', '*.rs'},
  callback = function()
    vim.lsp.buf.format()
  end,
})


-- Remove trailing whitespace on save
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*',
  callback = function()
    local view = vim.fn.winsaveview()
    vim.cmd([[%s/\s\+$//e]])
    vim.fn.winrestview(view)
  end,
})


-- Return to last edit position
vim.api.nvim_create_autocmd('BufReadPost', {
  callback = function()
    local mark = vim.api.nvim_buf_get_mark(0, '"')
    local lcount = vim.api.nvim_buf_line_count(0)
    if mark[1] > 0 and mark[1] <= lcount then
      pcall(vim.api.nvim_win_set_cursor, 0, mark)
    end
  end,
})


-- Resize splits on window resize
vim.api.nvim_create_autocmd('VimResized', {
  callback = function()
    vim.cmd('tabdo wincmd =')
  end,
})


-- Close certain windows with 'q'
vim.api.nvim_create_autocmd('FileType', {
  pattern = {
    'qf', 'help', 'man', 'lspinfo',
    'checkhealth', 'startuptime',
  },
  callback = function(event)
    vim.bo[event.buf].buflisted = false
    vim.keymap.set('n', 'q', '<cmd>close<CR>', 
      { buffer = event.buf, silent = true })
  end,
})


-- Highlight TODO comments
vim.api.nvim_create_autocmd({'BufEnter', 'BufWinEnter'}, {
  pattern = '*',
  callback = function()
    vim.fn.matchadd('Todo', 'TODO\\|FIXME\\|NOTE\\|HACK\\|XXX')
  end,
})

Working with Numbers

Increment/Decrement Enhancement


-- Better increment/decrement
vim.keymap.set('n', '+', '<C-a>', { desc = 'Increment' })
vim.keymap.set('n', '-', '<C-x>', { desc = 'Decrement' })
vim.keymap.set('v', '+', 'g<C-a>', { desc = 'Increment (Visual)' })
vim.keymap.set('v', '-', 'g<C-x>', { desc = 'Decrement (Visual)' })


-- Create sequence
vim.keymap.set('v', '<leader>n', function()
  vim.cmd("'<,'>s/\\d\\+/\\=line('.')-line(\"'<\")+1/")
end, { desc = 'Create Sequence' })

Quick Toggles

Toggle Common Options


-- Toggle line numbers
vim.keymap.set('n', '<leader>tn', function()
  vim.opt.number = not vim.opt.number:get()
  vim.opt.relativenumber = not vim.opt.relativenumber:get()
end, { desc = 'Toggle Line Numbers' })


-- Toggle wrap
vim.keymap.set('n', '<leader>tw', function()
  vim.opt.wrap = not vim.opt.wrap:get()
end, { desc = 'Toggle Wrap' })


-- Toggle spell check
vim.keymap.set('n', '<leader>ts', function()
  vim.opt.spell = not vim.opt.spell:get()
end, { desc = 'Toggle Spell' })


-- Toggle cursor line
vim.keymap.set('n', '<leader>tc', function()
  vim.opt.cursorline = not vim.opt.cursorline:get()
end, { desc = 'Toggle Cursor Line' })


-- Toggle list (show whitespace)
vim.keymap.set('n', '<leader>tl', function()
  vim.opt.list = not vim.opt.list:get()
end, { desc = 'Toggle List Chars' })


-- Configure list characters
vim.opt.listchars = {
  tab = '→ ',
  trail = '·',
  nbsp = '␣',
  extends = '⟩',
  precedes = '⟨',
}

Session Management

Quick Session Helpers


-- Save session
vim.keymap.set('n', '<leader>ss', function()
  local session_dir = vim.fn.stdpath('data') .. '/sessions/'
  vim.fn.mkdir(session_dir, 'p')
  
  local session_name = vim.fn.input('Session name: ', vim.fn.getcwd():match('[^/]+$'))
  if session_name ~= '' then
    vim.cmd('mksession! ' .. session_dir .. session_name .. '.vim')
    print('Session saved: ' .. session_name)
  end
end, { desc = 'Save Session' })


-- Load session
vim.keymap.set('n', '<leader>sl', function()
  local session_dir = vim.fn.stdpath('data') .. '/sessions/'
  local sessions = vim.fn.glob(session_dir .. '*.vim', false, true)
  
  if #sessions == 0 then
    print('No sessions found')
    return
  end
  
  local names = vim.tbl_map(function(s)
    return vim.fn.fnamemodify(s, ':t:r')
  end, sessions)
  
  vim.ui.select(names, {
    prompt = 'Select session:',
  }, function(choice)
    if choice then
      vim.cmd('source ' .. session_dir .. choice .. '.vim')
    end
  end)
end, { desc = 'Load Session' })

Miscellaneous Enhancements

Quick Fixes


-- Fix common typos
vim.cmd([[
  cnoreabbrev W! w!
  cnoreabbrev W w
  cnoreabbrev Q! q!
  cnoreabbrev Q q
  cnoreabbrev Qa qa
  cnoreabbrev Qall qall
  cnoreabbrev Wq wq
  cnoreabbrev WQ wq
  cnoreabbrev wQ wq
]])


-- Better mark jumping (center screen)
vim.keymap.set('n', "'", "m'`", { desc = 'Jump to Mark' })
vim.keymap.set('n', "`", "m``zz", { desc = 'Jump to Mark (Centered)' })


-- Quick macros
vim.keymap.set('n', 'Q', '@q', { desc = 'Execute Macro q' })


-- Visual shifting (keeps selection)
vim.keymap.set('x', '<', '<gv', { desc = 'Shift Left' })
vim.keymap.set('x', '>', '>gv', { desc = 'Shift Right' })


-- Reselect last paste
vim.keymap.set('n', 'gp', '`[v`]', { desc = 'Reselect Paste' })


-- Quick formatting
vim.keymap.set('n', '<leader>=', 'gg=G``', { desc = 'Format Buffer' })


-- Count matches
vim.keymap.set('n', '<leader>*', '*<C-O>:%s///gn<CR>', { desc = 'Count Matches' })

Performance Helpers


-- Disable providers you don't use
vim.g.loaded_python3_provider = 0
vim.g.loaded_ruby_provider = 0
vim.g.loaded_perl_provider = 0
vim.g.loaded_node_provider = 0


-- Disable built-in plugins you don't need
local disabled_built_ins = {
  'netrw',
  'netrwPlugin',
  'netrwSettings',
  'netrwFileHandlers',
  'gzip',
  'zip',
  'zipPlugin',
  'tar',
  'tarPlugin',
  'getscript',
  'getscriptPlugin',
  'vimball',
  'vimballPlugin',
  '2html_plugin',
  'logipat',
  'rrhelper',
  'spellfile_plugin',
  'matchit',
}

for _, plugin in ipairs(disabled_built_ins) do
  vim.g['loaded_' .. plugin] = 1
end

Summary: Chapter #36 – Quality of Life Improvements

This chapter covered essential quality-of-life enhancements:

  1. Better Defaults – Sensible options for improved editing

  2. Smart Navigation – Enhanced movement commands

  3. Quick Editing – Faster text manipulation

  4. Visual Feedback – Better highlighting and diagnostics

  5. Buffer Management – Efficient buffer workflows

  6. Command-Line – Enhanced command mode

  7. File Operations – Quick file management

  8. Auto-Commands – Automatic improvements

  9. Quick Toggles – Fast option switching

  10. Miscellaneous – Various helpful tweaks

These small improvements compound to create a significantly more comfortable and efficient editing environment. Each tweak addresses common friction points and speeds up daily workflows.


Chapter #37: Lesser-Known Features

Discover powerful but often overlooked Neovim capabilities that can transform your workflow. These features are built into Neovim but rarely discussed in typical tutorials.

Text Objects and Motions

Advanced Text Objects


-- Around and inside various delimiters

-- ia/aa - argument

-- ib/ab - () block

-- iB/aB - {} block

-- it/at - tag block

-- iq/aq - quote


-- Custom text object for entire buffer
vim.keymap.set({'o', 'x'}, 'ae', ':<C-u>normal! ggVG<CR>', 
  { desc = 'Entire Buffer' })


-- Custom text object for current line (without whitespace)
vim.keymap.set({'o', 'x'}, 'il', ':<C-u>normal! ^v$h<CR>',
  { desc = 'Line Content' })


-- Custom text object for indentation level
vim.keymap.set({'o', 'x'}, 'ii', function()
  local line = vim.fn.line('.')
  local indent = vim.fn.indent(line)
  

  -- Find start of block
  local start = line
  while start > 1 and vim.fn.indent(start - 1) >= indent do
    start = start - 1
  end
  

  -- Find end of block
  local last = vim.fn.line('$')
  local finish = line
  while finish < last and vim.fn.indent(finish + 1) >= indent do
    finish = finish + 1
  end
  
  vim.cmd('normal! ' .. start .. 'GV' .. finish .. 'G')
end, { desc = 'Indent Block' })

Powerful Built-in Motions


-- ]) - Go to next } in first column

-- [) - Go to previous } in first column

-- ]] - Go to next { in first column

-- [[ - Go to previous { in first column


-- ]m - Go to next method start

-- [m - Go to previous method start

-- ]M - Go to next method end

-- [M - Go to previous method end


-- Sentence and paragraph motions

-- ) - Next sentence

-- ( - Previous sentence

-- } - Next paragraph

-- { - Previous paragraph


-- Jump to matching items

-- % - Go to matching bracket/tag

-- [( - Go to previous unmatched (

-- ]) - Go to next unmatched )


-- Demo keymaps for these
vim.keymap.set('n', '<leader>h]', ']]', { desc = 'Next { (column 1)' })
vim.keymap.set('n', '<leader>h[', '[[', { desc = 'Prev { (column 1)' })

The Power of g Commands

Lesser-Known g Commands


-- Useful g commands (normal mode):

-- gv - Reselect last visual selection

-- gn - Select next search match

-- gN - Select previous search match

-- gJ - Join lines without space

-- gq - Format text (respects textwidth)

-- gw - Format text (keep cursor position)

-- g& - Repeat last substitute on all lines

-- g; - Go to previous change position

-- g, - Go to next change position

-- gi - Go to last insert position and enter insert mode

-- gI - Insert at column 1

-- gf - Go to file under cursor

-- gx - Open URL under cursor

-- gd - Go to definition (LSP)

-- gD - Go to declaration

-- gu - Make lowercase (motion)

-- gU - Make uppercase (motion)

-- g~ - Toggle case (motion)

-- g? - ROT13 encode (motion)

-- g8 - Show UTF-8 byte sequence

-- ga - Show character code

-- gs - Sleep (useful in scripts)


-- Demonstrate some useful ones
vim.keymap.set('n', 'gV', '`[v`]', { desc = 'Select Last Paste' })
vim.keymap.set('n', 'gp', 'p`[v`]', { desc = 'Paste and Select' })


-- Use gn/gN for better search/replace workflow
vim.keymap.set('n', '<leader>sn', function()
  vim.cmd('normal! *')
  print('Press cgn to change and . to repeat')
end, { desc = 'Search Word (cgn ready)' })

The g Command Workflows


-- Search and replace with gn (better than :%s)

-- 1. Search with /pattern

-- 2. Use cgn to change first match

-- 3. Press . to repeat on next matches

-- 4. Press n to skip a match


-- Visual block with gv

-- 1. Make a visual selection

-- 2. Do something

-- 3. Press gv to reselect

-- 4. Do something else


-- Format with gq
vim.keymap.set('n', '<leader>gq', 'gqip', { desc = 'Format Paragraph' })
vim.keymap.set('v', '<leader>gq', 'gq', { desc = 'Format Selection' })

The :g Global Command

Powerful Global Commands


-- The :g command syntax: :[range]g/pattern/command


-- Delete all lines matching pattern
vim.keymap.set('n', '<leader>gd', function()
  local pattern = vim.fn.input('Delete lines matching: ')
  if pattern ~= '' then
    vim.cmd('g/' .. pattern .. '/d')
  end
end, { desc = 'Delete Matching Lines' })


-- Delete all lines NOT matching pattern
vim.keymap.set('n', '<leader>gD', function()
  local pattern = vim.fn.input('Delete lines NOT matching: ')
  if pattern ~= '' then
    vim.cmd('g!/' .. pattern .. '/d')
  end
end, { desc = 'Delete Non-Matching Lines' })


-- Copy all matching lines to register
vim.keymap.set('n', '<leader>gy', function()
  local pattern = vim.fn.input('Yank lines matching: ')
  if pattern ~= '' then
    vim.cmd('g/' .. pattern .. '/y A')
    print('Yanked matching lines to register a')
  end
end, { desc = 'Yank Matching Lines' })


-- Move all matching lines to end of file
vim.keymap.set('n', '<leader>gm', function()
  local pattern = vim.fn.input('Move lines matching: ')
  if pattern ~= '' then
    vim.cmd('g/' .. pattern .. '/m$')
  end
end, { desc = 'Move Matching to End' })


-- Examples of useful :g commands:

-- :g/TODO/p          - Print all TODO lines

-- :g/pattern/t$      - Copy matching lines to end

-- :g/^$/d            - Delete empty lines

-- :g/pattern/normal @q - Run macro q on matching lines

-- :g/function/+1d    - Delete line after each match

-- :g/pattern/s/old/new/g - Replace in matching lines only

Advanced :g Patterns


-- Create a command to show all TODO/FIXME/NOTE comments
vim.api.nvim_create_user_command('ShowTODO', function()
  vim.cmd('g/\\(TODO\\|FIXME\\|NOTE\\|HACK\\|XXX\\)/p')
end, {})


-- Create a command to collect matching lines in new buffer
vim.api.nvim_create_user_command('Collect', function(opts)
  local pattern = opts.args
  if pattern == '' then
    pattern = vim.fn.input('Pattern: ')
  end
  
  if pattern ~= '' then

    -- Copy to register a
    vim.cmd('g/' .. pattern .. '/y A')

    -- Create new buffer
    vim.cmd('new')

    -- Paste contents
    vim.cmd('put a')

    -- Delete first blank line
    vim.cmd('1d')
  end
end, { nargs = '?' })

The :norm Command

Execute Normal Mode Commands Programmatically


-- :norm allows running normal mode commands on multiple lines


-- Add semicolon to end of each line in range
vim.keymap.set('v', '<leader>;', ':norm A;<CR>', 
  { desc = 'Add ; to Lines' })


-- Comment out lines (assuming # is comment char)
vim.keymap.set('v', '<leader>#', ':norm I# <CR>',
  { desc = 'Comment Lines' })


-- Wrap each line in quotes
vim.keymap.set('v', '<leader>"w', ':norm I"<CR>gv:norm A"<CR>',
  { desc = 'Wrap in Quotes' })


-- Execute macro on multiple lines
vim.keymap.set('v', '<leader>@', function()
  local reg = vim.fn.input('Macro register: ')
  if reg:match('[a-z]') then
    vim.cmd("'<,'>norm @" .. reg)
  end
end, { desc = 'Execute Macro on Lines' })


-- Examples of useful :norm commands:

-- :norm 0               - Go to start of each line

-- :norm $               - Go to end of each line

-- :norm I"              - Insert " at start of line

-- :norm A;              - Append ; at end of line

-- :norm 0D              - Delete entire line contents

-- :norm >>              - Indent line

-- :norm .               - Repeat last change on each line

Expression Register

The = Register for Calculations


-- In insert mode: <C-r>= to evaluate expression

-- Examples:

-- <C-r>=2+2<CR>                    - Insert "4"

-- <C-r>=strftime('%Y-%m-%d')<CR>   - Insert current date

-- <C-r>=system('date')<CR>         - Insert command output


-- Quick calculator
vim.keymap.set('i', '<C-c>', '<C-r>=', { desc = 'Calculator' })


-- Insert current date/time shortcuts
vim.keymap.set('i', '<leader>dd', function()
  return vim.fn.strftime('%Y-%m-%d')
end, { expr = true, desc = 'Insert Date' })

vim.keymap.set('i', '<leader>dt', function()
  return vim.fn.strftime('%H:%M:%S')
end, { expr = true, desc = 'Insert Time' })

vim.keymap.set('i', '<leader>dD', function()
  return vim.fn.strftime('%Y-%m-%d %H:%M:%S')
end, { expr = true, desc = 'Insert DateTime' })


-- Insert from Lua expression
vim.keymap.set('i', '<C-l>', function()
  local expr = vim.fn.input('Lua: ')
  if expr ~= '' then
    local result = loadstring('return ' .. expr)()
    return tostring(result)
  end
  return ''
end, { expr = true, desc = 'Eval Lua' })

Marks and Positions

Advanced Mark Usage


-- Lowercase marks (a-z): buffer-local

-- Uppercase marks (A-Z): global (cross-file)

-- Number marks (0-9): last exit positions

-- Special marks:

--   ` - position before last jump

--   " - position when last editing this file

--   [ - start of last changed/yanked text

--   ] - end of last changed/yanked text

--   < - start of last visual selection

--   > - end of last visual selection

--   . - position of last change


-- Quick mark jumps
vim.keymap.set('n', '<leader>m1', "mA", { desc = 'Set Global Mark A' })
vim.keymap.set('n', '<leader>m2', "mB", { desc = 'Set Global Mark B' })
vim.keymap.set('n', '<leader>m3', "mC", { desc = 'Set Global Mark C' })
vim.keymap.set('n', '<leader>`1', "`A", { desc = 'Jump to Mark A' })
vim.keymap.set('n', '<leader>`2', "`B", { desc = 'Jump to Mark B' })
vim.keymap.set('n', '<leader>`3', "`C", { desc = 'Jump to Mark C' })


-- Show all marks
vim.keymap.set('n', '<leader>ml', ':marks<CR>', { desc = 'List Marks' })


-- Delete all marks in buffer
vim.keymap.set('n', '<leader>md', ':delmarks!<CR>', { desc = 'Delete Marks' })


-- Jump to last change and edit
vim.keymap.set('n', 'g.', '`.', { desc = 'Jump to Last Change' })


-- Use marks for quick navigation between files
vim.api.nvim_create_user_command('MarkFile', function(opts)
  local mark = opts.args
  if mark:match('[A-Z]') then
    vim.cmd('mark ' .. mark)
    print('Set mark ' .. mark .. ' in ' .. vim.fn.expand('%'))
  end
end, { nargs = 1 })

The Power of :h (Help)

Advanced Help Navigation


-- Help tag patterns:

-- :h pattern         - Search help

-- :h 'option'        - Help for option

-- :h :command        - Help for Ex command

-- :h function()      - Help for function

-- :h i_CTRL-R        - Help for insert mode mapping

-- :h c_CTRL-R        - Help for command mode mapping

-- :h v_motion        - Help for visual mode motion


-- Quick help lookups
vim.keymap.set('n', '<leader>H', function()
  local word = vim.fn.expand('<cword>')
  vim.cmd('help ' .. word)
end, { desc = 'Help for Word' })


-- Help for visual selection
vim.keymap.set('v', '<leader>H', function()
  local text = vim.fn.getregion(
    vim.fn.getpos('v'),
    vim.fn.getpos('.'),
    { type = vim.fn.mode() }
  )[1]
  vim.cmd('help ' .. text)
end, { desc = 'Help for Selection' })


-- Search help for topic
vim.keymap.set('n', '<leader>hs', function()
  local topic = vim.fn.input('Help search: ')
  if topic ~= '' then
    vim.cmd('helpgrep ' .. topic)
  end
end, { desc = 'Search Help' })


-- Open help in vertical split
vim.keymap.set('n', '<leader>hv', function()
  local word = vim.fn.expand('<cword>')
  vim.cmd('vert help ' .. word)
end, { desc = 'Help Vertical' })

Quickfix and Location Lists

Advanced List Operations


-- Quickfix commands:

-- :copen    - Open quickfix window

-- :cclose   - Close quickfix window

-- :cnext    - Next item

-- :cprev    - Previous item

-- :cfirst   - First item

-- :clast    - Last item

-- :cnewer   - Newer quickfix list

-- :colder   - Older quickfix list

-- :cdo      - Execute command on each quickfix entry

-- :cfdo     - Execute command on each file in quickfix


-- Location list (buffer-local quickfix):

-- Same commands with 'l' instead of 'c'

-- :lopen, :lnext, :lprev, etc.


-- Navigate quickfix
vim.keymap.set('n', '[q', ':cprev<CR>', { desc = 'Prev Quickfix' })
vim.keymap.set('n', ']q', ':cnext<CR>', { desc = 'Next Quickfix' })
vim.keymap.set('n', '[Q', ':cfirst<CR>', { desc = 'First Quickfix' })
vim.keymap.set('n', ']Q', ':clast<CR>', { desc = 'Last Quickfix' })


-- Navigate location list
vim.keymap.set('n', '[l', ':lprev<CR>', { desc = 'Prev Location' })
vim.keymap.set('n', ']l', ':lnext<CR>', { desc = 'Next Location' })


-- Toggle quickfix
vim.keymap.set('n', '<leader>co', function()
  if vim.fn.getqflist({winid = 0}).winid ~= 0 then
    vim.cmd('cclose')
  else
    vim.cmd('copen')
  end
end, { desc = 'Toggle Quickfix' })


-- Clear quickfix
vim.keymap.set('n', '<leader>cc', ':cexpr []<CR>', 
  { desc = 'Clear Quickfix' })


-- Execute command on each quickfix entry
vim.api.nvim_create_user_command('Cdo', function(opts)
  vim.cmd('cdo ' .. opts.args)
end, { nargs = '+' })


-- Example: Replace in all quickfix files

-- :vimgrep /pattern/ **/*.lua

-- :Cdo s/old/new/g | update


-- Save/restore quickfix list
vim.keymap.set('n', '<leader>cs', function()
  local qf = vim.fn.getqflist()
  vim.g.saved_qflist = qf
  print('Quickfix list saved')
end, { desc = 'Save Quickfix' })

vim.keymap.set('n', '<leader>cr', function()
  if vim.g.saved_qflist then
    vim.fn.setqflist(vim.g.saved_qflist)
    vim.cmd('copen')
    print('Quickfix list restored')
  end
end, { desc = 'Restore Quickfix' })

Command-Line Window

The Secret Command Editor


-- Open command-line window:

-- q:  - Command history window

-- q/  - Search forward history window

-- q?  - Search backward history window

-- <C-f> in command mode - Open command window


-- In the command window:

-- - Edit commands like normal text

-- - Press Enter to execute

-- - Press <C-c> to cancel


-- Better command-line window
vim.keymap.set('n', 'q:', function()
  vim.cmd('nohlsearch')
  vim.cmd('call feedkeys("q:", "n")')
end, { desc = 'Command History' })


-- Configure command window
vim.api.nvim_create_autocmd('CmdwinEnter', {
  callback = function()
    vim.keymap.set('n', '<CR>', '<CR>', { buffer = true })
    vim.keymap.set('n', 'q', ':q<CR>', { buffer = true })
    vim.keymap.set('n', '<Esc>', ':q<CR>', { buffer = true })
  end,
})


-- Quick command editing workflow:

-- 1. Press q:

-- 2. Navigate to command you want

-- 3. Edit it (use normal Vim commands)

-- 4. Press Enter to execute

-- 5. Or press q to cancel

The Black Hole Register

"_ - The Deletion That Doesn’t Yank


-- Delete without affecting registers

-- "_d - Delete without yanking

-- "_c - Change without yanking

-- "_x - Delete char without yanking


-- Make these the default for certain operations
vim.keymap.set('n', 'x', '"_x', { desc = 'Delete Char (No Yank)' })
vim.keymap.set('n', 'X', '"_X', { desc = 'Delete Char Back (No Yank)' })
vim.keymap.set('v', 'x', '"_x', { desc = 'Delete Char (No Yank)' })


-- Change without yanking
vim.keymap.set('n', 'c', '"_c', { desc = 'Change (No Yank)' })
vim.keymap.set('n', 'C', '"_C', { desc = 'Change Line (No Yank)' })
vim.keymap.set('v', 'c', '"_c', { desc = 'Change (No Yank)' })


-- Keep original behavior with leader
vim.keymap.set('n', '<leader>c', 'c', { desc = 'Change (Yank)' })
vim.keymap.set('n', '<leader>C', 'C', { desc = 'Change Line (Yank)' })

Diff Mode

Built-in Diff Capabilities


-- Diff commands:

-- :diffsplit file  - Open file in diff mode

-- :diffthis        - Make current window part of diff

-- :diffoff         - Turn off diff mode

-- do               - Obtain diff changes

-- dp               - Put diff changes

-- ]c               - Next change

-- [c               - Previous change

-- :diffupdate      - Update diff highlighting


-- Easy diff operations
vim.keymap.set('n', '<leader>dt', ':diffthis<CR>', { desc = 'Diff This' })
vim.keymap.set('n', '<leader>do', ':diffoff<CR>', { desc = 'Diff Off' })
vim.keymap.set('n', '<leader>du', ':diffupdate<CR>', { desc = 'Diff Update' })


-- Diff navigation
vim.keymap.set('n', '[c', '[c', { desc = 'Prev Change' })
vim.keymap.set('n', ']c', ']c', { desc = 'Next Change' })


-- Quick diff with buffer
vim.keymap.set('n', '<leader>db', function()
  local bufnr = vim.fn.input('Buffer number: ')
  if bufnr ~= '' then
    vim.cmd('vert diffsplit #' .. bufnr)
  end
end, { desc = 'Diff Buffer' })


-- Diff with file
vim.keymap.set('n', '<leader>df', function()
  local file = vim.fn.input('File: ', '', 'file')
  if file ~= '' then
    vim.cmd('vert diffsplit ' .. file)
  end
end, { desc = 'Diff File' })


-- Diff with git HEAD
vim.keymap.set('n', '<leader>dg', function()
  local file = vim.fn.expand('%')
  vim.cmd('vert diffsplit !git show HEAD:' .. file)
end, { desc = 'Diff Git HEAD' })

The = Motion (Auto-indent)

Powerful Formatting Motion


-- = is a motion, so it works with:

-- ==    - Format current line

-- =G    - Format to end of file

-- =gg   - Format to start of file

-- =ap   - Format paragraph

-- =a{   - Format {} block

-- v=    - Format selection


-- Format entire function
vim.keymap.set('n', '=f', 'va{=', { desc = 'Format Function' })


-- Format inside parentheses
vim.keymap.set('n', '=i', 'vi(=', { desc = 'Format () Block' })


-- Smart format (LSP or =)
vim.keymap.set('n', '<leader>=', function()
  if vim.lsp.buf.format then
    vim.lsp.buf.format()
  else
    vim.cmd('normal! gg=G``')
  end
end, { desc = 'Format Buffer' })


-- Format and save
vim.keymap.set('n', '<leader>w=', function()
  vim.cmd('normal! gg=G``')
  vim.cmd('write')
end, { desc = 'Format and Save' })

The . Command (Repeat)

Making the Dot Command More Powerful


-- The dot command repeats the last change

-- Make your changes "dot-repeatable"


-- Example: Delete word and repeat with .

-- dw.... (deletes 4 words)


-- Example: Change word and repeat

-- cw<new><Esc>.... (changes 4 words to "new")


-- Make macro repeatable with dot
vim.keymap.set('n', 'Q', '@qj', { desc = 'Execute Macro q and Move Down' })

-- Now . will repeat @q on next line


-- Create "change inside quotes" that's dot-repeatable
vim.keymap.set('n', '<leader>ci"', 'ci"', { desc = 'Change Inside "' })
vim.keymap.set('n', "<leader>ci'", "ci'", { desc = "Change Inside '" })


-- Make plugin commands dot-repeatable (example)
vim.api.nvim_create_user_command('RepeatableChange', function()

  -- Do something
  vim.cmd([[normal! ciw]])

  -- Make it repeatable
  vim.fn['repeat#set']('\\<Plug>RepeatableChange')
end, {})

Command Ranges

Powerful Range Specifications


-- Range patterns:

-- :5,10d         - Lines 5-10

-- :5,$d          - Line 5 to end

-- :%d            - All lines

-- :.d            - Current line

-- :.,+5d         - Current line and next 5

-- :.-5,.d        - Previous 5 lines to current

-- :/pattern/d    - Next line matching pattern

-- :?pattern?d    - Previous line matching pattern

-- :'<,'>d        - Visual selection

-- :g/pattern/d   - All lines matching pattern


-- Examples with custom commands
vim.api.nvim_create_user_command('CopyRange', function(opts)
  local lines = vim.api.nvim_buf_get_lines(0, 
    opts.line1 - 1, opts.line2, false)
  vim.fn.setreg('+', table.concat(lines, '\n'))
  print('Copied ' .. #lines .. ' lines')
end, { range = true })


-- Use it: :'<,'>CopyRange or :5,10CopyRange


-- Delete lines in range matching pattern
vim.api.nvim_create_user_command('DeleteMatching', function(opts)
  local pattern = opts.args
  vim.cmd(opts.line1 .. ',' .. opts.line2 .. 'g/' .. pattern .. '/d')
end, { range = '%', nargs = 1 })


-- Use it: :%DeleteMatching TODO


-- Execute command on range with offset

-- :.,.+10s/old/new/g  - Current line + next 10

-- :$-5,$s/old/new/g   - Last 5 lines

The @: Command

Repeat Last Command


-- @: repeats the last Ex command

-- @@ repeats the last @ command


-- Quick repeat last command
vim.keymap.set('n', '<leader>.', '@:', { desc = 'Repeat Last Command' })


-- Repeat last substitution
vim.keymap.set('n', '<leader>&', ':&&<CR>', { desc = 'Repeat Substitution' })


-- Repeat last substitution with flags
vim.keymap.set('n', '<leader>&&', ':&&g<CR>', 
  { desc = 'Repeat Substitution (global)' })


-- Store frequently used commands in registers
vim.keymap.set('n', '<leader>1', function()
  vim.fn.setreg('1', ':source %\n')
  print('Stored :source % in register 1')
end, { desc = 'Store Source Command' })

vim.keymap.set('n', '<leader>!', '@1', { desc = 'Execute Register 1' })

The :read and :write Commands

Lesser-Known File Operations


-- :read can:

-- :read file        - Insert file contents

-- :read !command    - Insert command output

-- :0read file       - Insert at top

-- :$read file       - Insert at bottom

-- :read !ls         - Insert directory listing


-- :write can:

-- :write !command   - Pipe buffer to command

-- :write !sudo tee %  - Save as root

-- :'<,'>write file    - Write selection to file

-- :'<,'>write >> file - Append selection to file


-- Insert file contents
vim.keymap.set('n', '<leader>rf', function()
  local file = vim.fn.input('Read file: ', '', 'file')
  if file ~= '' then
    vim.cmd('read ' .. file)
  end
end, { desc = 'Read File' })


-- Insert command output
vim.keymap.set('n', '<leader>rc', function()
  local cmd = vim.fn.input('Command: ')
  if cmd ~= '' then
    vim.cmd('read !' .. cmd)
  end
end, { desc = 'Read Command' })


-- Write selection to new file
vim.keymap.set('v', '<leader>wf', function()
  local file = vim.fn.input('Write to: ', '', 'file')
  if file ~= '' then
    vim.cmd("'<,'>write " .. file)
  end
end, { desc = 'Write Selection' })


-- Filter buffer through command
vim.keymap.set('n', '<leader>!', function()
  local cmd = vim.fn.input('Filter through: ')
  if cmd ~= '' then
    vim.cmd('%!' .. cmd)
  end
end, { desc = 'Filter Buffer' })


-- Filter selection through command
vim.keymap.set('v', '<leader>!', function()
  local cmd = vim.fn.input('Filter through: ')
  if cmd ~= '' then
    vim.cmd("'<,'>!" .. cmd)
  end
end, { desc = 'Filter Selection' })

The Power of :substitute Flags

Advanced Substitution Options


-- Substitute flags:

-- g - Replace all occurrences in line

-- c - Confirm each replacement

-- i - Case insensitive

-- I - Case sensitive

-- n - Report number of matches (don't replace)

-- e - No error if pattern not found

-- & - Reuse flags from previous substitute


-- Count matches without replacing
vim.keymap.set('n', '<leader>sc', function()
  local pattern = vim.fn.input('Count pattern: ')
  if pattern ~= '' then
    vim.cmd('%s/' .. pattern .. '//gn')
  end
end, { desc = 'Count Pattern' })


-- Interactive substitute
vim.keymap.set('n', '<leader>si', function()
  local old = vim.fn.input('Old: ')
  if old == '' then return end
  local new = vim.fn.input('New: ')
  vim.cmd('%s/' .. old .. '/' .. new .. '/gc')
end, { desc = 'Interactive Substitute' })


-- Substitute with confirmation in range
vim.keymap.set('v', '<leader>s', function()
  local old = vim.fn.input('Old: ')
  if old == '' then return end
  local new = vim.fn.input('New: ')
  vim.cmd("'<,'>s/" .. old .. '/' .. new .. '/gc')
end, { desc = 'Substitute (confirm)' })

Summary: Chapter #37 – Lesser-Known Features

This chapter explored powerful but underutilized Neovim features:

  1. Advanced Text Objects – Custom motions and selections

  2. g Commands – Powerful operations starting with g

  3. :g Global Command – Batch operations on matching lines

  4. :norm Command – Programmatic normal mode execution

  5. Expression Register – Calculations and evaluations

  6. Advanced Marks – Sophisticated position management

  7. Help System – Advanced help navigation

  8. Quickfix Lists – Powerful list management

  9. Command-Line Window – Secret command editor

  10. Black Hole Register – Clean deletions

  11. Diff Mode – Built-in comparison tools

  12. Ranges and Commands – Advanced command execution

These features exist in vanilla Neovim and require no plugins. Mastering them can significantly enhance your editing capabilities and understanding of Vim’s design philosophy.


Chapter #38: Performance Optimization and Profiling

Introduction

Neovim’s extensibility comes with a performance cost—each plugin, autocommand, and custom function adds overhead. This chapter teaches you to identify bottlenecks, optimize configurations, and maintain a responsive editing experience even with hundreds of plugins loaded.

Philosophy: Measure first, optimize second. Premature optimization wastes time; informed optimization based on profiling data yields real improvements.


Startup Time Profiling

Basic Startup Measurement

Generate a startup profile:

nvim --startuptime startup.log +quit

Analyze the output:

# View slowest operations
sort -k2 -n startup.log | tail -20

# Or use a more readable format
cat startup.log | awk '{print $2, $NF}' | sort -n | tail -20

Understanding the log format:

004.123 002.456: sourcing /path/to/plugin.lua ^^^ ^^^ | Time spent in this operation (ms) Total time elapsed

Identifying Common Culprits

Typical slow operations:

  1. Plugin loading – Too many plugins loaded at startup

  2. TreeSitter parsing – Large grammars loaded unnecessarily

  3. LSP attachment – Servers starting for every filetype

  4. Colorscheme – Complex themes with many highlight groups

  5. Autocommands – Excessive event handlers

Example analysis:

045.234 015.123: sourcing telescope.lua 060.456 012.345: TreeSitter loading parsers 075.789 010.234: LSP client initialization

This shows Telescope takes 15ms, TreeSitter 12ms, LSP 10ms—prioritize optimizing Telescope first.


Lazy Loading Strategies

Lazy.nvim Advanced Patterns

Load on specific events:


-- Load telescope only when needed
{
  'nvim-telescope/telescope.nvim',
  cmd = 'Telescope',  -- Load on :Telescope command
  keys = {            -- Load on keybind
    { '<leader>ff', '<cmd>Telescope find_files<cr>' },
    { '<leader>/', '<cmd>Telescope live_grep<cr>' },
  },
}

Lazy load LSP configurations:

{
  'neovim/nvim-lspconfig',
  event = { 'BufReadPre', 'BufNewFile' },  -- Load before editing
  dependencies = {
    { 'williamboman/mason.nvim', config = true },
  },
}

Conditional loading based on file size:


-- Disable heavy plugins for large files
vim.api.nvim_create_autocmd('BufReadPre', {
  callback = function()
    local max_size = 100 * 1024 -- 100KB
    local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(0))
    if ok and stats and stats.size > max_size then
      vim.b.large_file = true
      vim.cmd('syntax off')
      vim.opt_local.foldmethod = 'manual'
      vim.opt_local.spell = false

      -- Prevent LSP attachment (covered later)
    end
  end,
})

Module-based lazy loading:


-- Instead of requiring at startup:

-- local utils = require('my.utils')


-- Use on-demand loading:
local function get_utils()
  return require('my.utils')
end


-- Call only when needed
vim.keymap.set('n', '<leader>x', function()
  get_utils().execute_file()
end)

Measuring Lazy Loading Impact


-- Before lazy loading
vim.cmd([[
  profile start /tmp/nvim-profile.log
  profile func *
  profile file *
]])


-- After configuration loads
vim.defer_fn(function()
  print("Startup time: " .. vim.fn.reltimefloat(vim.fn.reltime(vim.g.start_time)) .. "s")
end, 100)


-- Set start time in init.lua
vim.g.start_time = vim.fn.reltime()

Runtime Performance Monitoring

Built-in Profiling

Profile a specific operation:

:profile start /tmp/profile.log
:profile func *
:profile file *

" Perform the slow operation
:Telescope find_files

:profile pause
:noautocmd qall!

Analyze the profile log:

# Find functions taking most time
grep "FUNCTION" /tmp/profile.log -A 3 | grep "Total time" | sort -k3 -n | tail -10

# Find most-called functions
grep "FUNCTION" /tmp/profile.log -A 3 | grep "Called" | sort -k2 -n | tail -10

Custom Performance Tracking


-- ~/.config/nvim/lua/performance.lua
local M = {}


-- Simple timer utility
function M.measure(name, fn)
  local start = vim.loop.hrtime()
  local result = fn()
  local elapsed = (vim.loop.hrtime() - start) / 1e6  -- Convert to ms
  
  print(string.format("%s took %.2fms", name, elapsed))
  return result
end


-- Track function execution times
M.timings = {}

function M.track(name)
  return function(fn)
    return function(...)
      local start = vim.loop.hrtime()
      local results = { fn(...) }
      local elapsed = (vim.loop.hrtime() - start) / 1e6
      
      M.timings[name] = M.timings[name] or { count = 0, total = 0 }
      M.timings[name].count = M.timings[name].count + 1
      M.timings[name].total = M.timings[name].total + elapsed
      
      return unpack(results)
    end
  end
end


-- Display timing report
function M.report()
  print("\n=== Performance Report ===")
  local sorted = {}
  for name, stats in pairs(M.timings) do
    table.insert(sorted, {
      name = name,
      avg = stats.total / stats.count,
      total = stats.total,
      count = stats.count,
    })
  end
  
  table.sort(sorted, function(a, b) return a.total > b.total end)
  
  for _, stat in ipairs(sorted) do
    print(string.format(
      "%s: %.2fms total, %.2fms avg, %d calls",
      stat.name, stat.total, stat.avg, stat.count
    ))
  end
end

return M

Usage:

local perf = require('performance')


-- Wrap a function to track it
local original_func = some_plugin.expensive_operation
some_plugin.expensive_operation = perf.track('expensive_op')(original_func)


-- Later, view the report
vim.keymap.set('n', '<leader>pr', function()
  require('performance').report()
end, { desc = 'Performance report' })

LSP Performance Tuning

Selective Server Attachment


-- ~/.config/nvim/lua/lsp/performance.lua
local M = {}


-- Check if LSP should attach based on context
function M.should_attach(client, bufnr)
  local buf_name = vim.api.nvim_buf_get_name(bufnr)
  

  -- Don't attach to large files
  if vim.b[bufnr].large_file then
    return false
  end
  

  -- Skip certain paths (node_modules, build directories)
  local skip_patterns = {
    'node_modules',
    '%.min%.js$',
    '%.min%.css$',
    'dist/',
    'build/',
  }
  
  for _, pattern in ipairs(skip_patterns) do
    if buf_name:match(pattern) then
      return false
    end
  end
  
  return true
end


-- Optimize server capabilities
function M.optimize_capabilities(client)

  -- Disable semantic tokens if not needed
  if client.name == 'tsserver' then
    client.server_capabilities.semanticTokensProvider = nil
  end
  

  -- Reduce document sync for specific servers
  if client.name == 'pyright' then
    client.server_capabilities.documentFormattingProvider = false
  end
end

return M

Apply optimizations:


-- In your LSP on_attach function
local lsp_perf = require('lsp.performance')

local on_attach = function(client, bufnr)
  if not lsp_perf.should_attach(client, bufnr) then
    vim.lsp.buf_detach_client(bufnr, client.id)
    return
  end
  
  lsp_perf.optimize_capabilities(client)
  

  -- Rest of on_attach logic...
end

Debouncing LSP Operations


-- Debounce document updates
local function debounce(fn, ms)
  local timer = vim.loop.new_timer()
  return function(...)
    local args = { ... }
    timer:start(ms, 0, vim.schedule_wrap(function()
      fn(unpack(args))
    end))
  end
end


-- Apply to diagnostics updates
vim.lsp.handlers['textDocument/publishDiagnostics'] = vim.lsp.with(
  vim.lsp.diagnostic.on_publish_diagnostics,
  {
    update_in_insert = false,  -- Don't update while typing
    virtual_text = {
      spacing = 4,
      prefix = '●',
    },
  }
)


-- Debounce formatting on save
vim.api.nvim_create_autocmd('BufWritePre', {
  pattern = '*.lua',
  callback = debounce(function()
    vim.lsp.buf.format({ async = false })
  end, 200),
})

Reduce LSP Memory Usage


-- Limit workspace folders
vim.lsp.start_client({
  root_dir = require('lspconfig').util.root_pattern('.git'),
  workspace_folders = nil,  -- Don't automatically add folders
  on_init = function(client)

    -- Limit to single folder
    client.workspace_folders = {
      {
        uri = vim.uri_from_fname(client.config.root_dir),
        name = vim.fn.fnamemodify(client.config.root_dir, ':t'),
      },
    }
  end,
})


-- Disable workspace symbol caching for large projects
vim.lsp.handlers['workspace/symbol'] = function(_, result, ctx)

  -- Don't cache, process immediately
  vim.lsp.handlers['workspace/symbol'](nil, result, ctx)
end

TreeSitter Optimization

Selective Parser Loading

require('nvim-treesitter.configs').setup({

  -- Only install parsers you actually use
  ensure_installed = { 'lua', 'python', 'javascript', 'rust' },
  

  -- Don't auto-install missing parsers
  auto_install = false,
  
  highlight = {
    enable = true,
    

    -- Disable for large files
    disable = function(lang, buf)
      local max_filesize = 100 * 1024 -- 100 KB
      local ok, stats = pcall(vim.loop.fs_stat, vim.api.nvim_buf_get_name(buf))
      if ok and stats and stats.size > max_filesize then
        return true
      end
    end,
    

    -- Limit parsing for very long lines
    additional_vim_regex_highlighting = false,
  },
  

  -- Disable incremental selection for large files
  incremental_selection = {
    enable = true,
    disable = function(lang, buf)
      return vim.b[buf].large_file
    end,
  },
})

Async Parsing Strategy


-- Parse large files asynchronously
local function parse_async(bufnr)
  vim.defer_fn(function()
    if vim.api.nvim_buf_is_loaded(bufnr) then
      local parser = vim.treesitter.get_parser(bufnr)
      if parser then
        parser:parse()
      end
    end
  end, 100)  -- Delay parsing by 100ms
end

vim.api.nvim_create_autocmd('BufReadPost', {
  callback = function(args)
    if vim.b[args.buf].large_file then
      vim.opt_local.syntax = 'off'
      parse_async(args.buf)
    end
  end,
})

Memory Management

Buffer Lifecycle Management


-- Auto-close hidden buffers after timeout
local function setup_buffer_cleanup()
  local timer = vim.loop.new_timer()
  local check_interval = 60000  -- 1 minute
  
  timer:start(check_interval, check_interval, vim.schedule_wrap(function()
    local buffers = vim.api.nvim_list_bufs()
    local current_time = os.time()
    
    for _, buf in ipairs(buffers) do
      if vim.api.nvim_buf_is_loaded(buf) and not vim.api.nvim_buf_get_option(buf, 'modified') then

        -- Check if buffer hasn't been accessed recently
        local last_used = vim.b[buf].last_used or 0
        if current_time - last_used > 600 then  -- 10 minutes

          -- Check if not visible in any window
          local windows = vim.fn.win_findbuf(buf)
          if #windows == 0 then
            vim.api.nvim_buf_delete(buf, { force = false })
          end
        end
      end
    end
  end))
end


-- Track buffer access
vim.api.nvim_create_autocmd({ 'BufEnter', 'CursorHold' }, {
  callback = function(args)
    vim.b[args.buf].last_used = os.time()
  end,
})

setup_buffer_cleanup()

Shada (Shared Data) Optimization


-- Limit what gets saved to shada
vim.opt.shada = {
  "!",        -- Save global variables (starting with uppercase)
  "'100",     -- Marks for last 100 files
  "<50",      -- Lines per register (default 50)
  "s10",      -- Max item size 10KB
  "h",        -- Disable hlsearch on load
}


-- Clear old data periodically
vim.api.nvim_create_autocmd('VimLeavePre', {
  callback = function()
    vim.cmd('wshada!')  -- Force write, clearing old data
  end,
})

Swap File Configuration


-- Use RAM for swap (faster but lost on crash)
vim.opt.directory = '/tmp/nvim-swap//'


-- Or disable swap entirely for speed (risky)

-- vim.opt.swapfile = false


-- Reduce swap write frequency
vim.opt.updatetime = 300  -- Write swap after 300ms of inactivity (default 4000)
vim.opt.updatecount = 100  -- Write swap after 100 characters typed

Async Operations with vim.loop

Background Job Processing


-- ~/.config/nvim/lua/async.lua
local M = {}


-- Run shell command asynchronously
function M.run_async(cmd, on_complete)
  local stdout = vim.loop.new_pipe(false)
  local stderr = vim.loop.new_pipe(false)
  local output = {}
  local errors = {}
  
  local handle
  handle = vim.loop.spawn('sh', {
    args = { '-c', cmd },
    stdio = { nil, stdout, stderr },
  }, vim.schedule_wrap(function(code, signal)
    stdout:close()
    stderr:close()
    handle:close()
    
    if on_complete then
      on_complete(code, output, errors)
    end
  end))
  
  vim.loop.read_start(stdout, function(err, data)
    if data then
      table.insert(output, data)
    end
  end)
  
  vim.loop.read_start(stderr, function(err, data)
    if data then
      table.insert(errors, data)
    end
  end)
end


-- Batch file operations
function M.process_files(files, processor, on_complete)
  local completed = 0
  local total = #files
  local results = {}
  
  for i, file in ipairs(files) do
    vim.schedule(function()
      results[i] = processor(file)
      completed = completed + 1
      
      if completed == total and on_complete then
        on_complete(results)
      end
    end)
  end
end

return M

Usage example:

local async = require('async')


-- Run linter in background
vim.keymap.set('n', '<leader>l', function()
  async.run_async('eslint .', function(code, output, errors)
    if code == 0 then
      print('Linting complete: ' .. table.concat(output))
    else
      vim.notify('Linting failed:\n' .. table.concat(errors), vim.log.levels.ERROR)
    end
  end)
end)

Throttling Expensive Operations


-- Create a throttled version of a function
local function throttle(fn, ms)
  local last_call = 0
  local pending_timer = nil
  
  return function(...)
    local args = { ... }
    local now = vim.loop.now()
    
    if now - last_call >= ms then
      last_call = now
      fn(unpack(args))
    else
      if pending_timer then
        pending_timer:stop()
      end
      
      pending_timer = vim.defer_fn(function()
        last_call = vim.loop.now()
        fn(unpack(args))
        pending_timer = nil
      end, ms - (now - last_call))
    end
  end
end


-- Apply to autocommands
local update_diagnostics = throttle(function()
  vim.diagnostic.setqflist({ open = false })
end, 500)

vim.api.nvim_create_autocmd('DiagnosticChanged', {
  callback = update_diagnostics,
})

Plugin-Specific Optimizations

Telescope Performance

require('telescope').setup({
  defaults = {

    -- Limit results for faster searching
    file_ignore_patterns = {
      "node_modules",
      "%.git/",
      "dist/",
      "build/",
      "%.jpg",
      "%.png",
    },
    

    -- Use faster sorting
    sorting_strategy = "ascending",
    

    -- Reduce preview window overhead
    preview = {
      treesitter = false,  -- Disable treesitter in preview
      timeout = 200,       -- Limit preview render time
    },
    

    -- Optimize layout
    layout_config = {
      prompt_position = "top",
      width = 0.8,
      height = 0.8,
    },
  },
  
  pickers = {
    find_files = {

      -- Use faster find implementation
      find_command = { 'rg', '--files', '--hidden', '--glob', '!.git/*' },
    },
    
    live_grep = {

      -- Limit results
      max_results = 1000,
      additional_args = function()
        return { "--max-count=1000" }
      end,
    },
  },
  
  extensions = {
    fzf = {
      fuzzy = true,
      override_generic_sorter = true,
      override_file_sorter = true,
      case_mode = "smart_case",
    },
  },
})


-- Load fzf-native for faster sorting
require('telescope').load_extension('fzf')

Completion Performance (nvim-cmp)

local cmp = require('cmp')

cmp.setup({
  performance = {
    debounce = 100,           -- Wait 100ms before showing suggestions
    throttle = 50,            -- Limit updates to every 50ms
    fetching_timeout = 200,   -- Cancel slow sources after 200ms
    max_view_entries = 20,    -- Limit visible entries
  },
  
  sources = cmp.config.sources({
    { name = 'nvim_lsp', priority = 10, max_item_count = 20 },
    { name = 'luasnip', priority = 8, max_item_count = 10 },
    { 
      name = 'buffer',
      priority = 5,
      max_item_count = 10,
      option = {

        -- Only search visible buffers
        get_bufnrs = function()
          local bufs = {}
          for _, win in ipairs(vim.api.nvim_list_wins()) do
            bufs[vim.api.nvim_win_get_buf(win)] = true
          end
          return vim.tbl_keys(bufs)
        end,
      },
    },
  }),
  

  -- Reduce formatting overhead
  formatting = {
    format = function(entry, vim_item)
      vim_item.abbr = string.sub(vim_item.abbr, 1, 40)  -- Truncate
      vim_item.menu = ({
        nvim_lsp = "[LSP]",
        luasnip = "[Snip]",
        buffer = "[Buf]",
      })[entry.source.name]
      return vim_item
    end,
  },
})

Disabling Unused Features

Built-in Provider Cleanup


-- Disable providers you don't use
vim.g.loaded_node_provider = 0
vim.g.loaded_perl_provider = 0
vim.g.loaded_python3_provider = 0  -- Only if you don't use Python plugins
vim.g.loaded_ruby_provider = 0


-- Disable built-in plugins
local disabled_built_ins = {
  "netrw",
  "netrwPlugin",
  "netrwSettings",
  "netrwFileHandlers",
  "gzip",
  "zip",
  "zipPlugin",
  "tar",
  "tarPlugin",
  "getscript",
  "getscriptPlugin",
  "vimball",
  "vimballPlugin",
  "2html_plugin",
  "logipat",
  "rrhelper",
  "spellfile_plugin",
  "matchit",
}

for _, plugin in pairs(disabled_built_ins) do
  vim.g["loaded_" .. plugin] = 1
end

Selective Syntax Loading


-- Only load syntax for specific filetypes
vim.api.nvim_create_autocmd('FileType', {
  pattern = { 'markdown', 'text', 'help' },
  callback = function()
    vim.opt_local.syntax = 'on'
  end,
})


-- For everything else, rely on TreeSitter
vim.cmd('syntax off')

Benchmarking Framework

Complete Benchmark Suite


-- ~/.config/nvim/lua/benchmark.lua
local M = {}

M.results = {}


-- Benchmark a function
function M.run(name, fn, iterations)
  iterations = iterations or 100
  local times = {}
  

  -- Warmup
  for _ = 1, 10 do
    fn()
  end
  

  -- Actual benchmark
  for i = 1, iterations do
    local start = vim.loop.hrtime()
    fn()
    local elapsed = (vim.loop.hrtime() - start) / 1e6
    table.insert(times, elapsed)
  end
  

  -- Calculate statistics
  table.sort(times)
  local total = 0
  for _, t in ipairs(times) do
    total = total + t
  end
  
  M.results[name] = {
    min = times[1],
    max = times[#times],
    avg = total / #times,
    median = times[math.floor(#times / 2)],
    p95 = times[math.floor(#times * 0.95)],
    iterations = iterations,
  }
end


-- Compare two implementations
function M.compare(name1, fn1, name2, fn2, iterations)
  M.run(name1, fn1, iterations)
  M.run(name2, fn2, iterations)
  
  local r1 = M.results[name1]
  local r2 = M.results[name2]
  
  print(string.format("\n=== Benchmark Comparison ==="))
  print(string.format("%s: avg=%.2fms, p95=%.2fms", name1, r1.avg, r1.p95))
  print(string.format("%s: avg=%.2fms, p95=%.2fms", name2, r2.avg, r2.p95))
  print(string.format("Winner: %s (%.2fx faster)", 
    r1.avg < r2.avg and name1 or name2,
    math.max(r1.avg, r2.avg) / math.min(r1.avg, r2.avg)
  ))
end


-- Print all results
function M.report()
  print("\n=== Benchmark Results ===")
  for name, result in pairs(M.results) do
    print(string.format(
      "%s:\n  avg: %.2fms\n  median: %.2fms\n  p95: %.2fms\n  min/max: %.2f/%.2fms",
      name, result.avg, result.median, result.p95, result.min, result.max
    ))
  end
end

return M

Example usage:

local bench = require('benchmark')


-- Compare two search methods
bench.compare(
  'builtin search',
  function() vim.fn.search('pattern') end,
  'regex search',
  function() vim.fn.searchpos('\\vpattern') end,
  1000
)


-- Benchmark a custom function
bench.run('my_custom_func', function()
  require('my_module').do_something()
end, 500)

bench.report()

Performance Monitoring Dashboard

Real-time Stats Display


-- ~/.config/nvim/lua/perf-monitor.lua
local M = {}

M.enabled = false
M.stats = {
  startup_time = 0,
  loaded_plugins = 0,
  active_lsp_clients = 0,
  buffer_count = 0,
  memory_mb = 0,
}


-- Update stats
function M.update()
  M.stats.loaded_plugins = #vim.tbl_keys(require('lazy').plugins())
  M.stats.active_lsp_clients = #vim.lsp.get_active_clients()
  M.stats.buffer_count = #vim.tbl_filter(
    function(b) return vim.api.nvim_buf_is_loaded(b) end,
    vim.api.nvim_list_bufs()
  )
  

  -- Approximate memory usage (Linux only)
  local status_file = io.open('/proc/self/status', 'r')
  if status_file then
    for line in status_file:lines() do
      local kb = line:match('^VmRSS:%s*(%d+)')
      if kb then
        M.stats.memory_mb = tonumber(kb) / 1024
        break
      end
    end
    status_file:close()
  end
end


-- Display in statusline or floating window
function M.show()
  M.update()
  
  local lines = {
    string.format("Startup: %.2fms", M.stats.startup_time),
    string.format("Plugins: %d", M.stats.loaded_plugins),
    string.format("LSP clients: %d", M.stats.active_lsp_clients),
    string.format("Buffers: %d", M.stats.buffer_count),
    string.format("Memory: %.1fMB", M.stats.memory_mb),
  }
  
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
  
  local width = 25
  local height = #lines
  local win = vim.api.nvim_open_win(buf, false, {
    relative = 'editor',
    width = width,
    height = height,
    row = 1,
    col = vim.o.columns - width - 2,
    style = 'minimal',
    border = 'rounded',
  })
  
  vim.defer_fn(function()
    if vim.api.nvim_win_is_valid(win) then
      vim.api.nvim_win_close(win, true)
    end
  end, 5000)
end


-- Toggle monitoring
function M.toggle()
  M.enabled = not M.enabled
  if M.enabled then
    M.timer = vim.loop.new_timer()
    M.timer:start(0, 5000, vim.schedule_wrap(M.update))
  else
    if M.timer then
      M.timer:stop()
      M.timer = nil
    end
  end
end

return M

Keymap:

vim.keymap.set('n', '<leader>pm', function()
  require('perf-monitor').show()
end, { desc = 'Show performance monitor' })

Summary and Best Practices

Performance Checklist

Before optimization:

  1. ✅ Measure startup time baseline (--startuptime)

  2. ✅ Identify slowest plugins (analyze log)

  3. ✅ Profile runtime operations (:profile)

Optimization priorities:

  1. Lazy load plugins – Biggest impact on startup

  2. Optimize LSP – Affects editing responsiveness

  3. Limit TreeSitter – Reduce parsing overhead

  4. Clean up autocommands – Remove unnecessary events

  5. Manage memory – Buffer cleanup, shada limits

Measurement after changes:

  1. ✅ Re-run --startuptime

  2. ✅ Compare before/after times

  3. ✅ Verify functionality still works

  4. ✅ Check for regressions in specific workflows

Target Metrics

Metric Target Excellent
Startup time < 100ms < 50ms
LSP response < 200ms < 100ms
Completion popup < 100ms < 50ms
File open < 50ms < 20ms
Memory usage < 200MB < 100MB

Common Anti-Patterns

❌ Don’t:

  • Load all plugins at startup

  • Run expensive operations in BufEnter

  • Enable all LSP features unconditionally

  • Keep unlimited undo/shada history

  • Use synchronous operations in hot paths

✅ Do:

  • Lazy load with lazy.nvim

  • Debounce/throttle frequent operations

  • Conditionally attach LSP based on file size

  • Set reasonable history limits

  • Use vim.schedule for async work


Next Steps

In Chapter #39: Testing and Debugging Configurations, we’ll cover:

  • Writing tests for custom Lua modules

  • Debugging techniques for complex configurations

  • Bisecting configuration issues

  • Creating minimal reproducible examples

  • Automated config validation

The performance optimizations in this chapter ensure your editor remains fast even as your configuration grows in complexity. Combined with proper testing (next chapter), you’ll have a robust, production-ready setup.


Chapter #39: Testing and Debugging Configurations

Introduction

A well-tested Neovim configuration prevents regressions, catches errors early, and makes refactoring safe. This chapter covers debugging techniques, testing strategies, and tools for maintaining reliable configurations.

Philosophy: Treat your Neovim configuration as production code—test thoroughly, debug systematically, and document issues.


Debugging Lua Code

Basic Debugging Techniques

1. Print debugging (the reliable classic):


-- Simple print
print("Debug: variable value is", vim.inspect(my_variable))


-- More informative
local function debug_print(label, value)
  print(string.format("[DEBUG] %s: %s", label, vim.inspect(value)))
end

debug_print("Config loaded", { plugin_count = 42, startup_time = 123 })

2. Using vim.inspect() for complex structures:


-- Inspect tables/functions
local config = {
  plugins = { 'telescope', 'lsp' },
  settings = { theme = 'catppuccin' }
}


-- Shallow inspection
print(vim.inspect(config))


-- Deep inspection with options
print(vim.inspect(config, { depth = 3, newline = '\n', indent = '  ' }))

3. Conditional debugging:


-- Enable debug mode via environment variable
local DEBUG = os.getenv('NVIM_DEBUG') == '1'

local function dbg(...)
  if DEBUG then
    print('[DEBUG]', ...)
  end
end


-- Usage
dbg('Loading plugin configuration')
dbg('Current buffer:', vim.api.nvim_get_current_buf())

Advanced Debugging with DAP

Setup for debugging Neovim’s Lua runtime:


-- Install nvim-dap and one-small-step-for-vimkind
{
  'mfussenegger/nvim-dap',
  dependencies = {
    'jbyuki/one-small-step-for-vimkind',
  },
  config = function()
    local dap = require('dap')
    

    -- Configure Lua adapter
    dap.configurations.lua = {
      {
        type = 'nlua',
        request = 'attach',
        name = 'Attach to running Neovim instance',
      }
    }
    
    dap.adapters.nlua = function(callback, config)
      callback({ type = 'server', host = config.host or '127.0.0.1', port = config.port or 8086 })
    end
    

    -- Keymaps
    vim.keymap.set('n', '<F5>', dap.continue)
    vim.keymap.set('n', '<F10>', dap.step_over)
    vim.keymap.set('n', '<F11>', dap.step_into)
    vim.keymap.set('n', '<F12>', dap.step_out)
    vim.keymap.set('n', '<leader>db', dap.toggle_breakpoint)
  end,
}

Debug a Lua module:


-- Set breakpoint in your code
require('dap').toggle_breakpoint()


-- Start debug server
require('osv').launch({ port = 8086 })


-- Now execute the code that hits the breakpoint

Error Handling and Logging

Structured logging system:


-- ~/.config/nvim/lua/logger.lua
local M = {}

M.levels = {
  DEBUG = 1,
  INFO = 2,
  WARN = 3,
  ERROR = 4,
}

M.current_level = M.levels.INFO
M.log_file = vim.fn.stdpath('cache') .. '/nvim-debug.log'


-- Core logging function
function M.log(level, message, context)
  if level < M.current_level then
    return
  end
  
  local level_names = { 'DEBUG', 'INFO', 'WARN', 'ERROR' }
  local timestamp = os.date('%Y-%m-%d %H:%M:%S')
  local level_name = level_names[level] or 'UNKNOWN'
  
  local log_line = string.format(
    '[%s] %s: %s',
    timestamp,
    level_name,
    message
  )
  
  if context then
    log_line = log_line .. '\n  Context: ' .. vim.inspect(context)
  end
  

  -- Write to file
  local file = io.open(M.log_file, 'a')
  if file then
    file:write(log_line .. '\n')
    file:close()
  end
  

  -- Also print to console for errors
  if level >= M.levels.ERROR then
    vim.notify(message, vim.log.levels.ERROR)
  end
end


-- Convenience functions
function M.debug(msg, ctx) M.log(M.levels.DEBUG, msg, ctx) end
function M.info(msg, ctx) M.log(M.levels.INFO, msg, ctx) end
function M.warn(msg, ctx) M.log(M.levels.WARN, msg, ctx) end
function M.error(msg, ctx) M.log(M.levels.ERROR, msg, ctx) end


-- Clear log file
function M.clear()
  local file = io.open(M.log_file, 'w')
  if file then
    file:close()
  end
end


-- View log file
function M.view()
  vim.cmd('edit ' .. M.log_file)
end

return M

Usage in configuration:

local log = require('logger')


-- Set log level via environment variable
if os.getenv('NVIM_LOG_LEVEL') == 'DEBUG' then
  log.current_level = log.levels.DEBUG
end


-- Use throughout config
log.info('Loading LSP configuration')

vim.lsp.start_client({
  name = 'my-lsp',
  on_attach = function(client, bufnr)
    log.debug('LSP attached', { client = client.name, buffer = bufnr })
  end,
  on_error = function(code, err)
    log.error('LSP error', { code = code, error = err })
  end,
})

Keymap Testing and Verification

Detecting Keymap Conflicts


-- ~/.config/nvim/lua/keymap-checker.lua
local M = {}


-- Get all keymaps for a mode
function M.get_keymaps(mode)
  local keymaps = vim.api.nvim_get_keymap(mode)
  local buf_keymaps = vim.api.nvim_buf_get_keymap(0, mode)
  

  -- Merge and organize
  local all_maps = {}
  for _, map in ipairs(keymaps) do
    all_maps[map.lhs] = map
  end
  for _, map in ipairs(buf_keymaps) do
    all_maps[map.lhs] = map
  end
  
  return all_maps
end


-- Find conflicts
function M.find_conflicts()
  local modes = { 'n', 'i', 'v', 'x', 't' }
  local conflicts = {}
  
  for _, mode in ipairs(modes) do
    local maps = M.get_keymaps(mode)
    local seen = {}
    
    for lhs, map in pairs(maps) do
      local key = mode .. ':' .. lhs
      if seen[key] then
        table.insert(conflicts, {
          mode = mode,
          key = lhs,
          original = seen[key],
          override = map,
        })
      else
        seen[key] = map
      end
    end
  end
  
  return conflicts
end


-- Display conflicts in a buffer
function M.show_conflicts()
  local conflicts = M.find_conflicts()
  
  if #conflicts == 0 then
    print('No keymap conflicts found!')
    return
  end
  
  local lines = { '=== Keymap Conflicts ===' , '' }
  
  for _, conflict in ipairs(conflicts) do
    table.insert(lines, string.format('Mode: %s, Key: %s', conflict.mode, conflict.key))
    table.insert(lines, '  Original: ' .. (conflict.original.rhs or '<no rhs>'))
    table.insert(lines, '  Override: ' .. (conflict.override.rhs or '<no rhs>'))
    table.insert(lines, '')
  end
  
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
  vim.api.nvim_buf_set_option(buf, 'modifiable', false)
  vim.api.nvim_buf_set_option(buf, 'filetype', 'text')
  
  vim.cmd('split')
  vim.api.nvim_win_set_buf(0, buf)
end


-- List all keymaps for a mode
function M.list_keymaps(mode)
  local maps = M.get_keymaps(mode)
  local lines = { string.format('=== Keymaps for mode: %s ===', mode), '' }
  
  local sorted = {}
  for lhs, map in pairs(maps) do
    table.insert(sorted, { lhs = lhs, map = map })
  end
  table.sort(sorted, function(a, b) return a.lhs < b.lhs end)
  
  for _, item in ipairs(sorted) do
    local desc = item.map.desc or '<no description>'
    table.insert(lines, string.format('%s -> %s (%s)', 
      item.lhs, 
      item.map.rhs or '<function>',
      desc
    ))
  end
  
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
  vim.api.nvim_buf_set_option(buf, 'modifiable', false)
  
  vim.cmd('split')
  vim.api.nvim_win_set_buf(0, buf)
end

return M

Commands for testing:

vim.api.nvim_create_user_command('KeymapConflicts', function()
  require('keymap-checker').show_conflicts()
end, {})

vim.api.nvim_create_user_command('KeymapList', function(opts)
  local mode = opts.args ~= '' and opts.args or 'n'
  require('keymap-checker').list_keymaps(mode)
end, { nargs = '?' })

Keymap Testing Framework


-- ~/.config/nvim/lua/test-keymaps.lua
local M = {}


-- Simulate keypress
function M.feedkeys(keys, mode)
  mode = mode or 'n'
  vim.api.nvim_feedkeys(
    vim.api.nvim_replace_termcodes(keys, true, false, true),
    mode,
    false
  )
end


-- Test a keymap produces expected result
function M.test_keymap(test_name, keys, setup, verify)
  local success, err = pcall(function()

    -- Setup test environment
    local buf = vim.api.nvim_create_buf(false, true)
    vim.api.nvim_set_current_buf(buf)
    
    if setup then
      setup()
    end
    

    -- Execute keymap
    M.feedkeys(keys)
    

    -- Wait for async operations
    vim.wait(100)
    

    -- Verify result
    local result = verify()
    

    -- Cleanup
    vim.api.nvim_buf_delete(buf, { force = true })
    
    return result
  end)
  
  if success and err then
    print(string.format('✓ %s', test_name))
    return true
  else
    print(string.format('✗ %s: %s', test_name, err or 'assertion failed'))
    return false
  end
end


-- Example test suite
function M.run_tests()
  local passed = 0
  local total = 0
  

  -- Test 1: Leader key functionality
  total = total + 1
  if M.test_keymap(
    'Leader + ff opens telescope',
    '<leader>ff',
    function()

      -- Setup: ensure telescope is available
      require('telescope')
    end,
    function()

      -- Verify: check if telescope picker opened
      local wins = vim.api.nvim_list_wins()
      for _, win in ipairs(wins) do
        local buf = vim.api.nvim_win_get_buf(win)
        local ft = vim.api.nvim_buf_get_option(buf, 'filetype')
        if ft == 'TelescopePrompt' then
          return true
        end
      end
      error('Telescope picker not found')
    end
  ) then
    passed = passed + 1
  end
  

  -- Test 2: Navigation
  total = total + 1
  if M.test_keymap(
    'j moves cursor down',
    'j',
    function()
      vim.api.nvim_buf_set_lines(0, 0, -1, false, { 'line 1', 'line 2', 'line 3' })
      vim.api.nvim_win_set_cursor(0, { 1, 0 })
    end,
    function()
      local cursor = vim.api.nvim_win_get_cursor(0)
      if cursor[1] == 2 then
        return true
      end
      error(string.format('Expected row 2, got %d', cursor[1]))
    end
  ) then
    passed = passed + 1
  end
  
  print(string.format('\nResults: %d/%d tests passed', passed, total))
end

return M

Plugin Debugging

Health Checks

Use built-in :checkhealth:

:checkhealth
:checkhealth telescope
:checkhealth lsp

Create custom health checks:


-- ~/.config/nvim/lua/health/myconfig.lua
local M = {}

function M.check()
  vim.health.start('My Configuration')
  

  -- Check Lua version
  if jit then
    vim.health.ok('LuaJIT ' .. jit.version)
  else
    vim.health.warn('Not using LuaJIT, performance may be degraded')
  end
  

  -- Check for required executables
  local required_tools = { 'rg', 'fd', 'git' }
  for _, tool in ipairs(required_tools) do
    if vim.fn.executable(tool) == 1 then
      vim.health.ok(tool .. ' found')
    else
      vim.health.error(tool .. ' not found', {
        'Install ' .. tool .. ' for full functionality'
      })
    end
  end
  

  -- Check plugin directory
  local plugin_dir = vim.fn.stdpath('data') .. '/lazy'
  if vim.fn.isdirectory(plugin_dir) == 1 then
    local count = #vim.fn.readdir(plugin_dir)
    vim.health.ok(string.format('%d plugins installed', count))
  else
    vim.health.error('Plugin directory not found')
  end
  

  -- Check LSP servers
  local lsp_servers = vim.lsp.get_active_clients()
  if #lsp_servers > 0 then
    vim.health.ok(string.format('%d LSP servers running', #lsp_servers))
    for _, server in ipairs(lsp_servers) do
      vim.health.info('  - ' .. server.name)
    end
  else
    vim.health.warn('No LSP servers running')
  end
end

return M

Run with: :checkhealth myconfig

Analyzing Plugin Load Times


-- ~/.config/nvim/lua/plugin-profiler.lua
local M = {}

M.timings = {}


-- Wrap require to measure load time
local original_require = require
_G.require = function(modname)
  local start = vim.loop.hrtime()
  local result = original_require(modname)
  local elapsed = (vim.loop.hrtime() - start) / 1e6
  
  M.timings[modname] = (M.timings[modname] or 0) + elapsed
  
  return result
end


-- Restore original require
function M.stop()
  _G.require = original_require
end


-- Display report
function M.report()
  local sorted = {}
  for mod, time in pairs(M.timings) do
    table.insert(sorted, { module = mod, time = time })
  end
  table.sort(sorted, function(a, b) return a.time > b.time end)
  
  print('\n=== Plugin Load Times ===')
  for i, item in ipairs(sorted) do
    if i <= 20 then  -- Top 20
      print(string.format('%2d. %-40s %.2fms', i, item.module, item.time))
    end
  end
end

return M

Usage:


-- At the top of init.lua
local profiler = require('plugin-profiler')


-- Your configuration here...


-- At the end of init.lua
vim.defer_fn(function()
  profiler.stop()
  profiler.report()
end, 1000)

Debugging Messages and Logs


-- View all Neovim messages
vim.keymap.set('n', '<leader>dm', '<cmd>messages<cr>', { desc = 'Debug: Show messages' })


-- Clear messages
vim.keymap.set('n', '<leader>dc', '<cmd>messages clear<cr>', { desc = 'Debug: Clear messages' })


-- Capture messages to buffer
vim.api.nvim_create_user_command('MessagesToBuffer', function()
  local messages = vim.split(vim.fn.execute('messages'), '\n')
  local buf = vim.api.nvim_create_buf(false, true)
  vim.api.nvim_buf_set_lines(buf, 0, -1, false, messages)
  vim.cmd('split')
  vim.api.nvim_win_set_buf(0, buf)
end, {})

LSP logging:


-- Enable LSP logging
vim.lsp.set_log_level('debug')


-- View LSP log
vim.keymap.set('n', '<leader>dl', function()
  vim.cmd('edit ' .. vim.lsp.get_log_path())
end, { desc = 'Debug: LSP log' })

Minimal Configuration Testing

Creating Minimal Configs

Minimal init.lua for testing:


-- ~/.config/nvim/minimal.lua

-- Use: nvim -u ~/.config/nvim/minimal.lua


-- Set up package path
local temp_dir = vim.fn.stdpath('cache') .. '/minimal'
local package_root = temp_dir .. '/lazy'
local install_path = package_root .. '/lazy.nvim'


-- Install lazy.nvim if not present
if not vim.loop.fs_stat(install_path) then
  vim.fn.system({
    'git', 'clone', '--filter=blob:none',
    'https://github.com/folke/lazy.nvim.git',
    install_path,
  })
end
vim.opt.rtp:prepend(install_path)


-- Only load the plugin you're testing
require('lazy').setup({
  {
    'nvim-telescope/telescope.nvim',
    dependencies = { 'nvim-lua/plenary.nvim' },
  },
}, {
  root = package_root,
  lockfile = temp_dir .. '/lazy-lock.json',
})


-- Minimal settings
vim.opt.number = true
vim.opt.termguicolors = true


-- Test keymap
vim.keymap.set('n', '<leader>ff', '<cmd>Telescope find_files<cr>')

print('Minimal config loaded - Testing telescope')

Use it:

nvim -u ~/.config/nvim/minimal.lua

Bisecting Configuration Issues

Binary search approach:


-- ~/.config/nvim/bisect.lua

-- Systematically disable half the config to find issues

local M = {}

M.disabled_plugins = {}

function M.disable_plugins(pattern)
  local lazy = require('lazy')
  for _, plugin in ipairs(lazy.plugins()) do
    if plugin.name:match(pattern) then
      table.insert(M.disabled_plugins, plugin.name)
      lazy.disable(plugin.name)
    end
  end
  print('Disabled ' .. #M.disabled_plugins .. ' plugins')
end

function M.enable_all()
  local lazy = require('lazy')
  for _, name in ipairs(M.disabled_plugins) do
    lazy.enable(name)
  end
  M.disabled_plugins = {}
  print('Re-enabled all plugins')
end

return M

Interactive bisection:

vim.api.nvim_create_user_command('BisectStart', function()
  local bisect = require('bisect')
  

  -- Disable half the plugins
  bisect.disable_plugins('.*')  -- All plugins
  
  print([[
  Bisection started. Steps:
  1. Test if the issue still occurs
  2. If YES: The problem is in core config
  3. If NO: Run :BisectContinue to test plugin subsets
  ]])
end, {})

Automated Testing

Unit Testing Custom Modules

Using plenary.nvim’s test harness:


-- ~/.config/nvim/tests/utils_spec.lua
local utils = require('my.utils')

describe('Utils module', function()
  describe('string functions', function()
    it('should split strings correctly', function()
      local result = utils.split('a,b,c', ',')
      assert.are.same({ 'a', 'b', 'c' }, result)
    end)
    
    it('should handle empty strings', function()
      local result = utils.split('', ',')
      assert.are.same({ '' }, result)
    end)
  end)
  
  describe('file operations', function()
    it('should detect if file exists', function()
      local exists = utils.file_exists('/etc/passwd')  -- Unix system file
      assert.is_true(exists)
    end)
    
    it('should return false for non-existent files', function()
      local exists = utils.file_exists('/this/does/not/exist')
      assert.is_false(exists)
    end)
  end)
  
  describe('async operations', function()
    it('should complete async function', function()
      local completed = false
      
      utils.async_operation(function()
        completed = true
      end)
      

      -- Wait for async
      vim.wait(1000, function() return completed end)
      assert.is_true(completed)
    end)
  end)
end)

Run tests:

nvim --headless -c "PlenaryBustedDirectory tests/ { minimal_init = 'minimal.lua' }"

Integration Testing

Test full workflows:


-- ~/.config/nvim/tests/workflow_spec.lua
describe('LSP workflow', function()
  local bufnr
  
  before_each(function()

    -- Create test buffer
    bufnr = vim.api.nvim_create_buf(false, true)
    vim.api.nvim_set_current_buf(bufnr)
  end)
  
  after_each(function()

    -- Cleanup
    if vim.api.nvim_buf_is_valid(bufnr) then
      vim.api.nvim_buf_delete(bufnr, { force = true })
    end
  end)
  
  it('should attach LSP to Lua files', function()
    vim.api.nvim_buf_set_name(bufnr, 'test.lua')
    vim.api.nvim_buf_set_option(bufnr, 'filetype', 'lua')
    

    -- Wait for LSP attachment
    vim.wait(2000, function()
      return #vim.lsp.get_active_clients({ bufnr = bufnr }) > 0
    end)
    
    local clients = vim.lsp.get_active_clients({ bufnr = bufnr })
    assert.is_true(#clients > 0)
    assert.equals('lua_ls', clients[1].name)
  end)
  
  it('should provide completions', function()
    vim.api.nvim_buf_set_option(bufnr, 'filetype', 'lua')
    vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, { 'vim.' })
    

    -- Trigger completion
    local line = vim.api.nvim_get_current_line()
    local col = #line
    
    local items = vim.lsp.buf_request_sync(
      bufnr,
      'textDocument/completion',
      vim.lsp.util.make_position_params(),
      1000
    )
    
    assert.is_not_nil(items)
  end)
end)

Continuous Testing in Development


-- Auto-run tests on save
vim.api.nvim_create_autocmd('BufWritePost', {
  pattern = { '*/tests/*_spec.lua', '*/lua/my/*.lua' },
  callback = function()
    vim.cmd('PlenaryBustedFile %')
  end,
})

Configuration Validation

Schema Validation


-- ~/.config/nvim/lua/validate-config.lua
local M = {}


-- Define expected structure
M.schema = {
  plugins = 'table',
  options = {
    number = 'boolean',
    relativenumber = 'boolean',
    tabstop = 'number',
  },
  keymaps = 'table',
  autocommands = 'table',
}


-- Validate against schema
function M.validate(config, schema)
  for key, expected_type in pairs(schema) do
    local actual_value = config[key]
    
    if type(expected_type) == 'table' then

      -- Nested validation
      if type(actual_value) ~= 'table' then
        return false, string.format('%s must be a table', key)
      end
      local ok, err = M.validate(actual_value, expected_type)
      if not ok then
        return false, key .. '.' .. err
      end
    else

      -- Direct type check
      if type(actual_value) ~= expected_type then
        return false, string.format(
          '%s must be %s, got %s',
          key, expected_type, type(actual_value)
        )
      end
    end
  end
  
  return true
end


-- Validate current configuration
function M.check()
  local config = {
    plugins = require('lazy').plugins(),
    options = {
      number = vim.o.number,
      relativenumber = vim.o.relativenumber,
      tabstop = vim.o.tabstop,
    },
    keymaps = vim.api.nvim_get_keymap('n'),
    autocommands = vim.api.nvim_get_autocmds({}),
  }
  
  local ok, err = M.validate(config, M.schema)
  if ok then
    print('✓ Configuration validation passed')
  else
    print('✗ Configuration validation failed: ' .. err)
  end
end

return M

Runtime Assertions


-- ~/.config/nvim/lua/my/utils.lua with assertions
local M = {}

function M.open_file(path)
  assert(type(path) == 'string', 'path must be string')
  assert(path ~= '', 'path cannot be empty')
  
  local exists = vim.fn.filereadable(path) == 1
  assert(exists, 'file does not exist: ' .. path)
  
  vim.cmd('edit ' .. path)
end

function M.safe_require(module)
  local ok, result = pcall(require, module)
  if not ok then
    vim.notify(
      string.format('Failed to load module %s: %s', module, result),
      vim.log.levels.ERROR
    )
    return nil
  end
  return result
end

return M

Error Recovery

Graceful Degradation


-- ~/.config/nvim/lua/safe-setup.lua
local M = {}


-- Safely setup a plugin
function M.setup(plugin_name, config)
  local ok, plugin = pcall(require, plugin_name)
  if not ok then
    vim.notify(
      string.format('Plugin %s not found, skipping setup', plugin_name),
      vim.log.levels.WARN
    )
    return false
  end
  
  local setup_ok, err = pcall(plugin.setup, config)
  if not setup_ok then
    vim.notify(
      string.format('Failed to setup %s: %s', plugin_name, err),
      vim.log.levels.ERROR
    )
    return false
  end
  
  return true
end


-- Setup multiple plugins with fallback
function M.setup_fallback(primary, fallback, config)
  if not M.setup(primary, config) then
    vim.notify('Falling back to ' .. fallback, vim.log.levels.INFO)
    M.setup(fallback, config)
  end
end

return M

Usage:

local safe = require('safe-setup')


-- Try telescope, fallback to fzf
safe.setup_fallback('telescope', 'fzf-lua', {
  defaults = { -- config
  }
})

Protected Calls Wrapper


-- Wrap all plugin setups
local function protected_setup()
  local success_count = 0
  local fail_count = 0
  
  local plugins = {
    { 'telescope', telescope_config },
    { 'lspconfig', lsp_config },
    { 'cmp', cmp_config },
  }
  
  for _, plugin_info in ipairs(plugins) do
    local name, config = plugin_info[1], plugin_info[2]
    local ok, err = pcall(function()
      require(name).setup(config)
    end)
    
    if ok then
      success_count = success_count + 1
    else
      fail_count = fail_count + 1
      vim.notify(
        string.format('Failed to setup %s: %s', name, err),
        vim.log.levels.ERROR
      )
    end
  end
  
  print(string.format(
    'Plugin setup complete: %d successful, %d failed',
    success_count, fail_count
  ))
end

protected_setup()

Debugging Commands Reference

Essential Debug Commands


-- Create debug command palette
vim.api.nvim_create_user_command('DebugInfo', function()
  print('=== Debug Information ===')
  print('Neovim version: ' .. vim.fn.execute('version'))
  print('Config path: ' .. vim.fn.stdpath('config'))
  print('Data path: ' .. vim.fn.stdpath('data'))
  print('Loaded plugins: ' .. #require('lazy').plugins())
  print('Active LSP clients: ' .. #vim.lsp.get_active_clients())
  print('Current buffer: ' .. vim.api.nvim_buf_get_name(0))
  print('Filetype: ' .. vim.bo.filetype)
end, {})


-- Inspect value under cursor (visual mode)
vim.keymap.set('v', '<leader>di', function()
  local start_pos = vim.fn.getpos("'<")
  local end_pos = vim.fn.getpos("'>")
  local lines = vim.api.nvim_buf_get_lines(
    0,
    start_pos[2] - 1,
    end_pos[2],
    false
  )
  local text = table.concat(lines, '\n')
  

  -- Try to evaluate as Lua
  local chunk, err = loadstring('return ' .. text)
  if chunk then
    local ok, result = pcall(chunk)
    if ok then
      print(vim.inspect(result))
    else
      print('Error evaluating: ' .. result)
    end
  else
    print('Not valid Lua: ' .. err)
  end
end, { desc = 'Debug: Inspect selection' })

Summary

Debugging Workflow

  1. Reproduce the issue – Consistent reproduction is key

  2. Isolate – Use minimal config to eliminate variables

  3. Measure – Add logging/profiling to understand behavior

  4. Hypothesize – Form theories about the cause

  5. Test – Verify theories with targeted changes

  6. Fix – Apply solution and test thoroughly

  7. Document – Record the issue and solution

Testing Checklist

  • ✅ Unit tests for custom Lua modules

  • ✅ Integration tests for plugin interactions

  • ✅ Keymap conflict detection

  • ✅ Health checks for dependencies

  • ✅ Performance regression tests

  • ✅ Configuration validation on startup

Common Issues and Solutions

Issue Debug Approach
Slow startup --startuptime, profile plugins
LSP not attaching Check :LspInfo, review logs
Keymap not working Check :map <key> for conflicts
Plugin error :messages, check :checkhealth
High memory Profile buffer/window count
Unexpected behavior Minimal config + bisection

Next Steps

In Chapter #40: Custom Statusline and UI Components, we’ll explore:

  • Building statuslines from scratch with Lua

  • Creating custom floating windows

  • Implementing custom pickers and selectors

  • Advanced UI patterns with virtual text

  • Performance-optimized UI components

The debugging and testing techniques from this chapter ensure you can maintain complex configurations reliably. Combined with the performance optimizations from Chapter #38, you have a solid foundation for building advanced UI features.