Neovim as the Development Core: Fast, Extensible, and Keyboard-Driven
Deep technical dive into my Neovim configuration: lazy.nvim, native LSP with Blink.cmp, Treesitter syntax parsing, git integration, and keymaps aligned with Colemak Mod-DH. Every plugin, colorscheme tweak, and autocmd documented.
This post continues my Keyboard-Driven Development series — a deep technical dive into the configuration, plugins, and design principles that power my Neovim setup.
In previous posts, we talked about how my Kinesis Advantage 360, Colemak Mod-DH layout, and tmux setup form the physical and terminal layers of my environment.
Now, we’re moving to the heart of it all — Neovim — the editor that powers everything I write, code, or edit.
This is a technical deep-dive that captures every detail of my configuration: from Lua module architecture to LSP diagnostics configuration to plugin-specific tweaks. If you want to understand exactly how my Neovim environment works, this post is for you.
Why Neovim?
I started with Vim years ago and gradually migrated to Neovim when it became clear that it was evolving faster, cleaner, and with modern extensibility in mind.
Neovim fits perfectly into my philosophy of keyboard-driven development because:
- Modal editing: Every action can be performed without leaving the home row
- Composability: The modal editing model (normal, insert, visual, command) keeps my hands efficient
- Integration: Works seamlessly with tmux, zsh, and my Colemak layout
- Hackability: Fully scriptable and automatable via Lua — everything can be customized to my exact workflow
Over time, my Neovim configuration has evolved into not just a text editor, but my complete development IDE — handling code editing, debugging, git workflows, and terminal integration all from the keyboard.
Guiding Principles
I follow a few core principles that keep my configuration minimal yet powerful:
- Discoverability: Every plugin/mapping should be discoverable and documented
- Performance first: Startup time should be instant; even remote servers should feel snappy
- Muscle memory: Key mappings align with my Colemak-DH layout and tmux prefix logic (
Spaceas leader) - Portability: Must work identically on Linux and macOS with minimal system dependencies
- Error recovery: Graceful error handling prevents the entire config from breaking if one module fails
Configuration Architecture
My Neovim configuration uses a modular Lua-based architecture:
~/.config/nvim/
├── init.lua # Main entry point with error handling
├── lua/user/
│ ├── bootstrap.lua # lazy.nvim initialization
│ ├── plugins-table.lua # Plugin declarations
│ ├── options.lua # Global vim options
│ ├── keymaps.lua # All keybindings
│ ├── autocommands.lua # Autocommands & user functions
│ ├── colorscheme.lua # Solarized Spring theme configuration
│ ├── lspconfig.lua # LSP setup with diagnostics
│ ├── treesitter.lua # Treesitter syntax parsing
│ ├── telescope.lua # Fuzzy finder configuration
│ ├── [40+ plugin config files]
│ └── utils.lua # Shared utilities
├── queries/markdown/folds.scm # Custom Treesitter query for Markdown
└── tests/ # Integration tests for major features
Each module follows this pattern:
local M = {}
M.meta = { desc = "...", needs_setup = true }
function M.setup() ... end
return M
Error handling at the top level ensures that if a single module fails to load, the editor still boots:
local res, utils = pcall(require, "user.utils")
if not res then
vim.notify("Error loading user.utils", vim.log.levels.ERROR)
return
end
Plugin Management: lazy.nvim
I use lazy.nvim for declarative plugin management with aggressive lazy-loading.
lazy.nvim provides:
- Lazy-loading on events: Plugins load only when their trigger events fire
- Dependency resolution: Automatic plugin ordering and initialization
- Performance: Startup time stays under 50ms even with 60+ plugins
- Build steps: Automatic compilation of plugins like Treesitter
The plugin specifications are centralized in lua/user/plugins-table.lua, which returns a large Lua table defining all plugins, their dependencies, and load conditions.
Options & Core Settings
My options.lua configures 40+ vim options that define editor behavior:
-- Indentation & Formatting
tabstop = 4, softtabstop = 4, shiftwidth = 4 -- 4-space indents
expandtab = true, smartindent = true -- Convert tabs to spaces
shiftround = true -- Round indent to shiftwidth multiple
-- Search & Highlighting
ignorecase = true, smartcase = true -- Case-insensitive by default, smart when uppercase
hlsearch = true, inccommand = "split" -- Show live preview of `:s///` replacements
-- Display
cursorline = true, colorcolumn = "80" -- Highlight cursor line & 80-char column
number = true, relativenumber = true -- Line numbers + relative offsets
list = true, listchars = "lead:⋅" -- Show leading whitespace as dots
wrap = false -- Don't wrap long lines
-- UI Behavior
splitbelow = true, splitright = true -- New splits go down/right
scrolloff = 5, sidescrolloff = 5 -- 5-line padding when scrolling
completeopt = "menuone,noselect" -- Better autocomplete behavior
laststatus = 3 -- Global statusline (not per-window)
-- Performance
updatetime = 300 -- Faster completion & CursorHold events
undofile = true, undodir = ~/.../undodir -- Persistent undo across sessions
undolevels = 10000 # Unlimited undo history
The options are split into two categories:
set_options()- Direct vim.api callsappend_options()- Appending to comma-separated lists (e.g.,diffopt,iskeyword)
Colorscheme: Solarized Spring
I use a customized Solarized Spring variant with transparent background and selective styling:
solarized.setup {
transparent = { enabled = true },
styles = {
comments = { italic = true, bold = false },
functions = { italic = true },
variables = { italic = false },
},
on_highlights = function(colors, _)
return {
-- Completion menu highlighting
BlinkCmpKindConstant = { fg = colors.red },
BlinkCmpKindField = { fg = colors.violet },
BlinkCmpMenuSelection = { fg = colors.base02, bg = colors.green },
-- Telescope search results
TelescopeMatching = { fg = colors.base02, bg = colors.cyan },
TelescopeSelection = { fg = colors.orange, bg = colors.base02, bold = true },
-- Git signs
GitSignsAddLnInline = { fg = colors.green, bold = true },
GitSignsChangeLnInline = { fg = colors.blue, bold = true },
GitSignsDeleteLnInline = { fg = colors.red, bold = true },
-- Markdown headings
MarkviewHeading1 = { fg = colors.red, bold = true },
MarkviewHeading2 = { fg = colors.green, bold = true },
MarkviewHeading3 = { fg = colors.yellow, bold = true },
}
end
}
The transparent background allows the terminal colors to show through, creating visual continuity with tmux.
Keymaps: Colemak-Aligned Bindings
My keymaps are strategically designed around Colemak Mod-DH ergonomics:
-- Leader key: Space
vim.g.mapleader = " "
-- Disable arrow keys to enforce hjkl navigation
M.keymap("n", "<Up>", "<Nop>")
M.keymap("n", "<Down>", "<Nop>")
M.keymap("n", "<Left>", "<Nop>")
M.keymap("n", "<Right>", "<Nop>")
-- Window resizing with Ctrl+Arrow
M.keymap("n", "<C-Up>", ":resize -2<CR>")
M.keymap("n", "<C-Down>", ":resize +2<CR>")
M.keymap("n", "<C-Left>", ":vertical resize -2<CR>")
M.keymap("n", "<C-Right>", ":vertical resize +2<CR>")
-- Delete without yanking (preserve clipboard)
M.keymap("n", "<leader>d", '"_d') -- Delete into black hole register
M.keymap("n", "x", '"_x') -- Delete char without yank
M.keymap("v", "<leader>d", '"_d') -- Visual delete without yank
-- Keep search results centered on screen
M.keymap("n", "n", "nzzzv") -- Next search result, center + unfold
M.keymap("n", "N", "Nzzzv") -- Previous search result
M.keymap("n", "J", "mzJ`z") -- Join lines, keep position
-- Jump to mark on vertical moves
M.keymap("n", "j", function()
if vim.v.count > 0 then
return "m'" .. vim.v.count .. "j" -- Set mark before large jumps
end
return "j"
end, { expr = true })
Leader sequences:
<leader>ff- Find files (Telescope)<leader>fg- Live grep (Telescope)<leader>fb- Switch buffers (Telescope)<leader>ls- LSP symbols (Telescope)<leader>lf- LSP format current buffer<leader>gs- Git stage hunk<leader>gb- Git blame line
LSP Configuration: Native Language Support
Neovim’s native LSP client (nvim-lspconfig) provides IDE-level features without external dependencies.
-- Setup LSP on_attach keymap callbacks
vim.api.nvim_create_augroup("lsp attach", { clear = true })
vim.api.nvim_create_autocmd("LspAttach", {
group = "lsp attach",
callback = function(args)
local client = vim.lsp.get_client_by_id(args.data.client_id)
-- Define keymaps only for attached LSP clients
if client:supports_method("textDocument/formatting") then
vim.keymap.set("n", "<leader>lf", vim.lsp.buf.format, { buffer = args.buf })
end
if client:supports_method("textDocument/declaration") then
vim.keymap.set("n", "<leader>lD", vim.lsp.buf.declaration, { buffer = args.buf })
end
end,
})
-- Diagnostic display configuration
local signs = {
Error = "",
Hint = "",
Info = "",
Warn = ""
}
for type, icon in pairs(signs) do
local hl = "DiagnosticSign" .. type
vim.fn.sign_define(hl, { text = icon, texthl = hl, numhl = hl })
end
-- LSP diagnostic config
vim.diagnostic.config({
update_in_insert = false,
virtual_text = {
prefix = "●",
spacing = 2,
format = function(diagnostic)
return string.format("%s [%s]", diagnostic.message, diagnostic.source)
end,
},
severity_sort = true,
float = {
focusable = true,
style = "rounded",
border = "rounded",
source = "always",
header = "",
prefix = "",
}
})
Supported languages: Lua, Python, TypeScript, Go, Rust, C/C++, Bash, and more via Mason.
Treesitter: Structured Syntax Analysis
Treesitter provides incremental parsing for ~50 languages, enabling:
- Smarter syntax highlighting than regex-based approaches
- Structural code navigation (
]m,[mfor next/prev function) - Intelligent text objects (
af/iffor function,ac/icfor class) - Consistent folding expressions
require("nvim-treesitter.configs").setup {
ensure_installed = {
"bash", "c", "cmake", "cpp", "css", "dockerfile", "go",
"html", "javascript", "json", "lua", "markdown", "python",
"regex", "rust", "toml", "yaml", "vim"
},
highlight = {
enable = true,
disable = function(_, bufnr)
-- Disable highlighting in very large files
return vim.bo[bufnr].filetype == "BigFile"
end,
additional_vim_regex_highlighting = false,
},
textobjects = {
select = {
enable = true,
keymaps = {
["af"] = "@function.outer",
["if"] = "@function.inner",
["ac"] = "@class.outer",
["ic"] = "@class.inner",
},
},
move = {
enable = true,
set_jumps = true,
goto_next_start = {
["]m"] = "@function.outer",
["]c"] = "@class.outer",
},
goto_previous_start = {
["[m"] = "@function.outer",
["[c"] = "@class.outer",
},
},
},
incremental_selection = {
enable = true,
keymaps = {
init_selection = "<M-/>", -- Alt+/ to start selection
node_incremental = "<M-/>", -- Expand to parent
node_decremental = "<BS>", -- Contract to child
},
},
}
-- Use Treesitter for folding expressions
vim.opt.foldexpr = "nvim_treesitter#foldexpr()"
-- Setup LSP semantic token highlighting fallbacks
local links = {
["@lsp.type.namespace"] = "@namespace",
["@lsp.type.class"] = "@type",
["@lsp.type.function"] = "@function",
["@lsp.type.parameter"] = "@parameter",
}
for newgroup, oldgroup in pairs(links) do
vim.api.nvim_set_hl(0, newgroup, { link = oldgroup, default = true })
end
Autocommands: Workflow Automation
My autocommands.lua contains 20+ autocommands that automate repetitive tasks:
-- Quick-fix window manipulation
vim.api.nvim_create_user_command("QFGrep", function(args)
local all = vim.fn.getqflist()
for i = #all, 1, -1 do
local item = all[i]
if not (vim.fn.bufname(item.bufnr):match(args.args) or item.text:match(args.args)) then
table.remove(all, i)
end
end
vim.fn.setqflist(all)
end, { nargs = "*" })
-- Auto-highlight yanked text
vim.api.nvim_create_autocmd("TextYankPost", {
group = "highlight_yank",
callback = function()
vim.highlight.on_yank { timeout = 300 }
end,
})
-- Auto-format on save (if LSP supports it)
vim.api.nvim_create_autocmd("BufWritePre", {
group = "lsp_format",
callback = function(ev)
if vim.lsp.get_active_clients({ bufnr = ev.buf })[1] then
vim.lsp.buf.format({ async = false })
end
end,
})
-- Auto-set filetype for custom extensions
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
pattern = "*.bazel",
command = "set filetype=starlark",
})
Plugin Ecosystem: Beyond the Core
My full plugin suite includes 60+ carefully selected plugins:
Terminal & Shell:
toggleterm.nvim- Embedded terminal with Lua/shell integrationvim-floaterm- Floating terminal
Completion & Snippets:
blink.cmp- Modern completion engineLuaSnip- Snippet engine with VSCode snippet compatibility
Navigation:
telescope.nvim- Fuzzy finder for files, grep, buffersnvim-tree.lua- File tree browserwhich-key.nvim- Keymap discovery menutelescope-fzf-native.nvim- FZF backend for faster searching
Productivity:
vim-fugitive- Git command wrappergitsigns.nvim- Git diff/blame in editordiffview.nvim- Side-by-side diff viewertodo-comments.nvim- Highlight TODO/FIXME/HACK comments
UI & Aesthetics:
bufferline.nvim- Tabbar with buffer listlualine.nvim- Statusline with git infoindent-blankline.nvim- Vertical indent guidesrainbow-delimiters.nvim- Color-coded bracket pairssmooth-cursor.nvim- Smooth scrolling cursor
Debugging & Testing:
nvim-dap- Debug adapter protocol clientnvim-dap-ui- DAP UI frontend
AI Integration:
codecompanion.nvim- Local LLM integrationneocodeium.nvim- Codeium AI completion
OSC52 Clipboard: Remote Copy/Paste
For seamless copy/paste across SSH sessions:
-- Enable OSC52 clipboard provider
vim.g.clipboard = {
name = "OSC 52",
copy = {
["+"] = require("vim.ui.clipboard.osc52").copy "+",
["*"] = require("vim.ui.clipboard.osc52").copy "*",
},
paste = {
["+"] = require("vim.ui.clipboard.osc52").paste "+",
["*"] = require("vim.ui.clipboard.osc52").paste "*",
},
}
This allows copying text from Neovim on a remote server directly to your local clipboard via tmux’s OSC52 support.
Integration with tmux & Shell
Neovim, tmux, and zsh form an integrated development environment:
Seamless navigation:
" Map Ctrl+hjkl to switch between Neovim splits and tmux panes
nnoremap <C-h> <C-w>h
nnoremap <C-j> <C-w>j
nnoremap <C-k> <C-w>k
nnoremap <C-l> <C-w>l
" (tmux also maps <C-hjkl> to switch panes)
Terminal integration with keymaps:
-- Open shells in new tabs
utils.keymap("n", "<leader>zsh", ":tabnew term://zsh<CR>")
utils.keymap("n", "<leader>bash", ":tabnew term://bash -l<CR>")
Quick access to interactive shells without leaving the editor:
<leader>bash- Open Bash in new tab (with login shell via-l)<leader>zsh- Open Zsh in new tab
Shell environment variables are inherited, and the working directory is synchronized with your current buffer location.
Version control workflow:
Fugitive keymaps (Git operations):
utils.keymap("n", "<leader>gst", "<cmd>tab G<CR>")
utils.keymap("n", "<leader>gfa", "<cmd>Git fetch --all --prune<CR>")
utils.keymap("n", "<leader>glog", "<cmd>GcLog!<Bar>cclose<Bar>tab copen<CR>")
utils.keymap("n", "<leader>glogf", "<cmd>tab Git log --oneline --decorate --graph -- %<CR>")
utils.keymap("n", "<leader>gpulla", "<cmd>Git pull --rebase --autostash<CR>")
Gitsigns keymaps (Hunk-level operations):
utils.keymap("n", "<leader>hp", gs.preview_hunk)
utils.keymap("n", "<leader>gbl", gs.toggle_current_line_blame)
utils.keymap("n", "]h", gs.nav_hunk("next"))
utils.keymap("n", "[h", gs.nav_hunk("prev"))
Git integration keymaps:
<leader>gst- Open git status in new tab (Fugitive)<leader>gfa- Fetch all with prune (Fugitive)<leader>glog- Git log in quickfix tab (Fugitive)<leader>glogf- Git log for current file (Fugitive)<leader>gpulla- Pull with rebase and autostash (Fugitive)<leader>hp- Preview hunk changes (Gitsigns)<leader>gbl- Toggle current line blame (Gitsigns)]h/[h- Navigate between hunks (Gitsigns)
The tab-based workflow keeps your buffer layout intact while giving you quick access to shells and git operations whenever needed.
Extending Vim Motions Beyond the Editor
The keyboard-driven philosophy extends far beyond Neovim. I use several applications that embrace vim-motion keybindings, creating a consistent interface across my entire workflow:
Ranger - Terminal File Manager
Ranger replaces GUI file managers entirely. Browse directories, preview files, and manage your filesystem without touching the mouse. The preview pane shows file contents with syntax highlighting via Neovim integration.
Neomutt - Terminal Email Client
Neomutt integrates with Neovim as the default editor for composing emails. Threading, MIME handling, and GPG encryption are all keyboard-driven.
Sioyek - PDF Viewer
Sioyek replaces PDF.js with a vim-native workflow. Blazing fast navigation through research papers and documentation without GUI chrome.
Newsboat - RSS Feed Reader
Stay updated with RSS feeds entirely in the terminal. Organize feeds with tags, mark articles as read, and open links in your browser—all keyboard-driven.
Surfingkeys - Browser Extension
Surfingkeys brings vim motions to Firefox/Chrome. Browse websites without touching the mouse. Open links, manage tabs, and navigate history—all through keyboard shortcuts.
Unified Workflow
These applications create a seamless ecosystem:
- Ranger → Find files
- Ranger → Open files in Neovim for editing
- Neomutt → Read emails with Neovim for composition
- Newsboat → Subscribe to RSS feeds
- Newsboat → Open articles in Surfingkeys-enabled browser
- Surfingkeys → Research papers → Sioyek for reading
- Sioyek → Extract links → Neovim for notes
Every tool speaks the same language: vim motions. Muscle memory built in one application transfers instantly to another. No context switching, no relearning shortcuts. Just consistency.
Testing & Quality Assurance
My configuration includes comprehensive test coverage using plenary.nvim and Busted (Lua testing framework).
Test Suites
The configuration includes 10 test modules covering all major components:
tests/
├── minimal_init.lua # Test environment bootstrap
├── run_all_tests.sh # Test runner script
├── test_core_behavior.lua # Core options & keymaps
├── test_lsp_behavior.lua # LSP setup & diagnostics
├── test_telescope_behavior.lua # Fuzzy finder integration
├── test_treesitter_behavior.lua # Syntax parsing & folding
├── test_git_behavior.lua # Git integration (Fugitive/Gitsigns)
├── test_dap_behavior.lua # Debugging integration
├── test_ui_behavior.lua # UI components & statusline
└── test_utils_behavior.lua # Utility functions
Running Tests
# Run all tests
cd ~/.config/nvim/tests
./run_all_tests.sh
# Run specific test file
nvim --headless \
-u ./minimal_init.lua \
-c "lua require('plenary.busted').run('./test_core_behavior.lua')" \
-c "qa!"
Test Coverage
Tests verify:
- Core Behavior: Line numbers, spell checking, undo breaks, file saving
- LSP Integration: Language server attachment, diagnostic display, code actions
- Telescope: File search, grep, buffer switching, git commands
- Treesitter: Syntax highlighting, incremental selection, text objects
- Git Workflows: Hunk navigation, blame display, git status
- DAP Support: Debugger initialization, breakpoint handling
- UI Components: Statusline updates, buffer management, window management
- Utilities: Working directory detection, path resolution, error handling
Test Environment
The minimal_init.lua provides a lightweight test environment:
- Loads full Neovim configuration
- Auto-installs plenary.nvim and lazy.nvim if needed
- Sets up mapleader and essential options
- Disables unnecessary plugins (netrw) for faster tests
- Minimal file handling (no swap, backup, writebackup)
Example Test
describe("Core Neovim Behavior", function()
before_each(function()
require("user.keymaps").setup()
require("user.options").setup()
require("user.autocommands").setup()
end)
describe("Line Numbers", function()
it("should display line numbers in new buffers", function()
vim.cmd("new")
assert.is_true(vim.wo.number, "Line numbers should be enabled")
assert.is_true(vim.wo.relativenumber, "Relative line numbers should be enabled")
vim.cmd("bdelete!")
end)
end)
end)
Performance Metrics
My configuration achieves excellent performance:
- Startup time: ~30-40ms (including all plugins)
- First keystroke: <10ms
- Completion latency: ~5-10ms with Blink.cmp
- Syntax highlighting: Instant with Treesitter
- Search speed: <100ms for projects with 10k+ files
Measured with:
:StartupTime " Shows plugin load times
:profile start /tmp/log " Profile arbitrary commands
:profile dump " View profiling results
Repository & Configuration Files
The complete configuration including all 60+ plugins, keymaps, and test files is available on GitHub:
📦 github.com/ragu-manjegowda/config/.config/nvim
Key files:
- init.lua - Entry point
- lua/user/options.lua - Global options
- lua/user/keymaps.lua - All keybindings
- lua/user/plugins-table.lua - Plugin declarations
- tests/ - Integration tests
Summary
Neovim is the core of my keyboard-driven development environment — a fully customizable, lightning-fast editor that integrates seamlessly with tmux, zsh, and my Colemak Mod-DH layout.
Through careful architectural design, strategic plugin selection, and meticulous configuration of every detail — from colorscheme tweaks to LSP diagnostics to autocommands — I’ve built an environment where the tools disappear and only the work remains.
Every keystroke serves a purpose. Every plugin earns its place. Every configuration option aligns with my workflow.
That’s the power of Neovim.