Advanced Neovim from Scratch

By Kaelyn N'Viro

Build a Fast, Modern, and Fully Custom Developer Workflow (2025 Edition)

Copyright © 2025 Kaelyn N’Viro.

All rights reserved.

No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means—electronic, mechanical, photocopying, recording, or otherwise—without the prior written permission of the publisher, except for brief quotations in reviews or scholarly articles.

Every effort has been made to ensure that the information contained in this publication is accurate and up to date at the time of printing. However, the author and publisher assume no responsibility for errors, omissions, or damages resulting from the use of the information contained herein.

All product names, trademarks, and registered trademarks are the property of their respective owners.

TABLE OF CONTENTS

INTRODUCTION

CHAPTER 1: START FAST — WHAT NEOVIM IS AND WHY IT MATTERS

1.1 WHO THIS BOOK HELPS AND THE OUTCOMES YOU’LL SHIP

1.2 EDITOR-AS-PLATFORM: FROM MINIMALIST EDITOR TO PROGRAMMABLE IDE

1.3 MENTAL MODELS: BUFFERS, WINDOWS, AND TABS WITHOUT CONFUSION

CHAPTER 2: CLEAN INSTALLS AND REPRODUCIBLE SETUPS

2.1 INSTALL PATHS, CONFIG FOLDERS, AND A MINIMAL INIT.LUA

2.2 VERSION-CONTROLLING YOUR CONFIG, PROFILES, AND SAFE ROLLBACKS

2.3 BASELINE OPTIONS AND ESSENTIAL KEYMAPS THAT REDUCE FRICTION

CHAPTER 3: LUA FUNDAMENTALS FOR NEOVIM

3.1 LUA SYNTAX ESSENTIALS AND TABLES YOU’LL ACTUALLY USE

3.2 MODULES, REQUIRE, AND THE RUNTIME PATH FOR CLEAN STRUCTURE

3.3 AUTOCOMMANDS, KEYMAPS, AND TINY UTILITIES IN LUA

CHAPTER 4: PLUGIN MANAGEMENT WITH LAZY.NVIM

4.1 INSTALLING LAZY.NVIM AND ORGANIZING PLUGIN SPECS

4.2 LAZY LOADING, EVENTS, AND MEASURABLE STARTUP GAINS

4.3 PINNING, UPDATES, AND PAINLESS ROLLBACKS

CHAPTER 5: MODULAR CONFIG DESIGN

5.1 FILES AND FOLDERS: OPTIONS.LUA, KEYMAPS.LUA, PLUGINS.LUA

5.2 LOGGING, PROFILING, AND DEBUGGING BROKEN CONFIGS

5.3 SHARING, PORTABILITY, AND MANAGING SECRETS SAFELY

CHAPTER 6: INTERFACE, NAVIGATION, AND UI

6.1 TELESCOPE MASTERY: FIND FILES, LIVE GREP, PICKERS THAT MATTER

6.2 STATUSLINES AND THEMES (LUALINE, CATPPUCCIN) WITH CONTEXT CUES

6.3 FILE TREES, BUFFERS, SESSIONS, AND WORKSPACE

CHAPTER 7: PRODUCTIVITY ESSENTIALS

7.1 WHICH-KEY, LEADER MAPS, AND DISCOVERABLE SHORTCUTS

7.2 SEARCH/REPLACE, MOTIONS, MACROS, AND REGISTERS THAT STICK

7.3 SNIPPETS AND SNIPPET ENGINES (LUASNIP) FOR REAL WORK

CHAPTER 8: GIT INTEGRATION AND TERMINAL WORKFLOW

8.1 GITSIGNS AND FUGITIVE: INLINE DIFFS, BLAME, AND STAGING

8.2 EMBEDDED TERMINALS, TASKS, AND TMUX SYNERGY

8.3 REVIEW FLOWS: QUICK PATCHES, COMMIT HYGIENE, AND HISTORY

CHAPTER 9: LSP, COMPLETION, AND DEBUGGING — 16 pp — 4,400 words

9.1 MASON.NVIM: INSTALLING AND WIRING LANGUAGE SERVERS FAST

9.2 DIAGNOSTICS, CODE ACTIONS, AND GO TO DEFINITIONS THAT FEEL NATIVE

9.3 NVIM-CMP FUNDAMENTALS AND SNIPPET INTEGRATION

9.4 DAP QUICKSTART: BREAKPOINTS, STEPPING, AND VARIABLES

CHAPTER 10: TREESITTER FOR SMART STRUCTURE

10.1 INSTALLING PARSERS AND PRACTICAL LANGUAGE COVERAGE

10.2 TEXTOBJECTS, SELECTIONS, AND STRUCTURAL FOLDING

10.3 STRUCTURAL SEARCH/REPLACE AND NAVIGATION PATTERNS

CHAPTER 11: FORMATTING, LINTING, AND QUALITY GATES

11.1 FORMATTERS AND LINTERS WITH NONE-LS/CONFORM.NVIM

11.2 ON-SAVE ACTIONS, PER-PROJECT RULES, AND STYLE LOCKS

11.3 PRE-COMMIT HOOKS AND CI CHECKS THAT MATCH YOUR EDITOR

CHAPTER 12: EXTENDING NEOVIM WITH YOUR OWN LUA

12.1 MINI-PLUGINS: COMMANDS, USER EVENTS, AND HELPERS

12.2 AUTOCOMMANDS AND EVENT-DRIVEN BEHAVIOR YOU CONTROL

12.3 PACKAGING, DOCS, VERSIONING, AND SHARING SAFELY

CHAPTER 13: AI-ASSISTED EDITING THAT YOU CONTROL

13.1 COPILOT/CODEIUM/LOCAL MODELS: SETUP AND ERGONOMIC PROMPTS

13.2 GUARDRails, SECURITY, AND REVIEW HABITS THAT PREVENT DRIFT

13.3 OFFLINE AND PRIVACY-FIRST OPTIONS WITH SANE TRADE-OFFS

CHAPTER 14: PERFORMANCE AND BENCHMARKING

14.2 ASYNC TASKS, CACHING, AND GC TUNING FOR RESPONSIVENESS

14.3 MINIMAL MODE, SAFE MODE, AND RECOVERY PLAYBOOKS

CHAPTER 15: PROJECT BLUEPRINTS THAT SHIP

15.1 WEB STACK (JS/TS/PYTHON): FILE NAMING, TESTS, TOOLS

15.2 SYSTEMS (RUST/GO): FAST LSP, DAP, AND INSPECTION

15.3 DATA & NOTES: MARKDOWN, NOTEBOOKS, AND RESEARCH

15.4 LANGUAGE-AGNOSTIC TEST HARNESS AND QUICK QA [CODE]

CHAPTER 16: REMOTE AND CROSS-PLATFORM WORK

16.1 SSH, CONTAINERS, AND WSL WITH CONSISTENT CONFIGS

16.2 REMOTE FILESYSTEMS, SYNC STRATEGIES, AND LATENCY TIPS

16.3 GUI OPTIONS (NEOVIDE, UIS) AND ACCESSIBILITY

CHAPTER 17: PROJECT-SPECIFIC CONFIGS AND SAFETY

17.1 LOCAL CONFIGS (.NVIMRC.LUA), WHITELISTS, AND TRUST PROMPTS

17.2 SECRETS MANAGEMENT, ENVIRONMENT TOGGLES, AND POLICIES

17.3 AUDITS, BACKUPS, AND REPRODUCIBILITY RECEIPTS

CHAPTER 18: FINALIZE, BACKUP, AND SHARE

17.1 LOCAL CONFIGS (.NVIMRC.LUA), WHITELISTS, AND TRUST PROMPTS

17.2 SECRETS MANAGEMENT, ENVIRONMENT TOGGLES, AND POLICIES

17.3 AUDITS, BACKUPS, AND REPRODUCIBILITY RECEIPTS

CHAPTER 19: CAPSTONE — YOUR PERSONAL IDE BUILD

19.1 SPECIFICATION, CONSTRAINTS, AND ARCHITECTURE DECISIONS

19.2 IMPLEMENTING MODULES AND LANGUAGE TOOLCHAINS

19.3 BENCHMARKS, TESTS, AND REGRESSION LOCKS

19.4 POSTMORTEM, RISKS, AND FUTURE-PROOFING PLAYBOOK

INTRODUCTION

Neovim has become more than a fast editor. It is a programmable platform you can shape into a complete development environment that matches how you think and work. This book begins from a clean slate and leads you to a stable, portable setup that starts quickly, behaves predictably, and earns your trust. You will learn to make clear choices, measure results, and keep your editor under version control so changes are safe and reversible. The goal is practical ownership of your tools, not a showcase of screenshots. Across the chapters you will replace guesswork with repeatable habits and design a workflow that removes friction instead of adding more knobs. Today.

We start with mental models that remove confusion. Buffers hold data, windows display it, and tabs group layouts for focus. With that foundation we create a minimal init.lua, then grow a modular structure you can read in a single sitting. You will use lazy.nvim for plugin specs and events, Telescope for search that feels instant, and a statusline that surfaces only what matters. Each addition has a reason, a cost, and a test. You will learn how to roll back an experiment, profile startup time, and prove improvements with numbers rather than hope. You will see how small, readable modules beat giant configs, and how naming files well makes future edits fast.

Next we wire the code tools that turn Neovim into an IDE you control. mason.nvim installs language servers, nvim-cmp handles completion, and Treesitter provides structural awareness for navigation and refactors. Formatters and linters connect through none-ls or conform so on-save rules stay consistent across projects. When debugging is required, the Debug Adapter Protocol adds breakpoints, stepping, and watch values without leaving your editor. You will keep these parts tidy, documented, and scoped to the languages you use. Examples remain small and runnable, with clear filenames, paths, and expected behavior for easy verification. Where choices exist, we explain tradeoffs and provide defaults that are easy to override without breaking everything.

Real work depends on flow and safety, not tricks. You will integrate Git with in-editor signs and commands, keep a productive terminal within reach, and adopt session patterns that reduce friction when you return to a task. Startup profiling and lazy loading will remove the pause that makes you hesitate to reopen large projects. Recovery playbooks will help you fix a broken update and isolate a slow plugin. You will practice per project rules without sacrificing portability, and you will learn how to protect secrets and credentials while keeping your setup convenient. You will separate experiments from daily work so changes can be tested, measured, and reversed without derailing momentum.

The later chapters move into portability and scale. You will run the same setup over SSH, inside containers, and on WSL with predictable behavior. Local configuration will be allowed but gated by trust prompts, and privacy minded AI assistance will be used with review habits that keep you in control. The book closes with a capstone where you specify your personal IDE, implement it, benchmark it, and document the results for future you and your team. Treat configuration as code, prefer small steps, and verify everything. Turn the page and map what Neovim is today so you can claim a faster, clearer, and more personal way to build software.

CHAPTER 1: START FAST — WHAT NEOVIM IS AND WHY IT MATTERS  

INTRODUCTION

Neovim is more than a fast editor. It is a programmable platform you can shape to match how you think, write, and ship software. This chapter sets clear stakes. Instead of renting a heavyweight IDE and living with its defaults, you will own a lean environment that starts quickly, behaves predictably, and lives in version control so every change is safe to test and easy to roll back. Chapter 1 lays the base for the rest of the book, giving you a lens for plugin choices, language tooling, and workflow design that favors small steps, measurable gains, and reversible experiments.

We move through three sections. First, who this book helps and the outcomes you will ship, with expectations for beginners and power users. Second, editor as platform, where we compare one size fits all tools with a programmable IDE you assemble with Lua, measured plugins, and testable defaults. Third, mental models for buffers, windows, and tabs so multitasking never feels fragile or confusing. The flow is UI first wherever sensible, with light Lua only when it clearly saves time or reduces cost, and every change pairs with a simple verification step you can repeat.

By the end you will know what success looks like, which pitfalls to avoid, and how to verify progress with small, repeatable checks. You will leave with a shared vocabulary for later chapters on plugin management, language servers, Treesitter, formatting, and debugging. The bridge to the first subchapter is direct. Begin by understanding the people this book serves and the results they can achieve, so each decision that follows aligns with a workflow you can explain, measure, and maintain without guesswork.

1.1 WHO THIS BOOK HELPS AND THE OUTCOMES YOU’LL SHIP

Every strong editor setup serves a clear group of users and a set of outcomes. This subchapter defines both so you can track value as you build. Neovim attracts curious beginners who want a reliable path, working developers who need speed without lock in, and team leads who care about reproducibility. You will see yourself in one or more of these roles. The goal is not to master every trick but to ship a setup that is fast, understandable, and easy to maintain. We will anchor progress in three results you can verify on any machine: a clean install that boots quickly, a modular configuration that explains itself, and a set of language tools that work the same in all projects.

As a beginner, you want safety, structure, and wins that arrive quickly. You will begin with a minimal init.lua, add options that reduce friction, and keep each change in version control. Your early tests are simple. Start Neovim from a fresh shell, measure startup time, and confirm the statusline and search behave as expected. You will rely on UI first steps, such as Telescope pickers and gentle which key prompts, to build muscle memory without learning everything at once. Small loops keep momentum high. You will experiment in a scratch directory, write down choices, and roll back anything that adds friction. By the end of this stage you will have a setup you understand well enough to explain to a friend in a single page.

As a working developer, you care about reliable features that save time every day. You will wire language servers through mason.nvim, plug completion through nvim cmp, and enable format on save with none ls or conform. You will bring Git into the editor with gitsigns and fugitive so reviews and small patches do not break flow. You will keep terminal access close by for commands that belong in the shell. Your tests include editing a medium repository, jumping to definitions, running a linter, and committing a small change without leaving Neovim. Performance and clarity remain the test. If a plugin slows work or clouds the interface, you remove it.

As a team lead, you value portability and consistency. You will store your configuration in a public or private repository, document it in a brief README, and set expectations for updates. New teammates can clone the repo, run a short bootstrap, and start coding with predictable keymaps and diagnostics. You will keep local configuration opt in behind trust prompts, and you will avoid storing secrets in plain files. Verification means new hires reach a first commit on day one with the same statusline, search, and formatting rules that senior staff use. Portability protects focus and shortens onboarding.

This book also helps engineers who split time between laptops, servers, and containers. You will keep one mental model everywhere by syncing configs and relying on the same search, statusline, and Git indicators in each environment. When latency or file systems differ, you will adjust settings conservatively and prefer features that work well over slow links. Remote reliability begins with a lean core. A simple, readable configuration fails less often and is easier to diagnose at a distance. That simplicity is a feature, not a compromise.

Outcomes must be specific and testable. By the end of Chapter 1 you should be able to do three things without hesitation. First, open Neovim on a clean machine and see your chosen theme and statusline in under a second on typical hardware. Second, explain buffers, windows, and tabs in plain language and use them to view two parts of one file at once. Third, list the plugins you rely on and why each exists, with a note on how you would remove or replace it quickly. Those are small promises, but they compound. They are the foundation for the IDE you will assemble in later chapters.

To keep scope realistic, we will not attempt to cover every plugin or feature. We will favor defaults that behave well across languages and let you add special cases later. Where choices are subjective, we will outline tradeoffs and give a safe starting point you can test on your own code. The audience is broad, but the path is narrow by design. Each step adds just enough to solve a real problem and nothing more. That clarity prevents drift and lowers maintenance.

Verification is a habit, not an afterthought. For every change, you will note expected behavior, a simple command to rerun, and the file you modified. You will compare startup logs before and after adjustments and keep the faster result. You will check :messages for warnings and fix them immediately or back out the change. Your configuration is a product, and products succeed when feedback loops are short and visible.

Finally, a word about motivation. Building your own editor can feel like a detour, but the payoff is permanent. Once you know how to profile, organize, and verify your setup, new tools stop being disruptions and become optional upgrades. You gain confidence that your workflow will survive on new hardware, in a different company, or inside a container with limited packages. That confidence is the best outcome of all, because it frees you to focus on the work that matters, with an environment that keeps up instead of getting in the way.

Some readers bring partial Vim habits and worry about unlearning. You do not need to erase prior knowledge. You will keep motions and registers that work and add modern discovery aids so you do not rely on memory alone. which key will surface your keymaps, and Telescope will make deep folders feel smaller by turning search into a reflex. As you practice, your hands will travel less while coverage grows. You will notice fewer context switches, pauses on large projects, and quicker recovery after interruptions. This is practical speed in daily work.

Because outcomes are measurable, standardize a short check after each change and keep settings stable while you vary one thing at a time. Write a note in log you commit alongside your configuration. These small records form a paper trail that makes upgrades safer and easier to review with teammates. Use the following minimal procedure throughout this chapter.

Start Neovim from a cold shell, record startup with the same command, and save the log to out/startup.log.

Open a known repository, run a Telescope search, then jump to a definition with your language server.

Save a file to trigger formatting, check :messages for warnings, then commit the configuration change with a one line note.

1.2 EDITOR-AS-PLATFORM: FROM MINIMALIST EDITOR TO PROGRAMMABLE IDE

Neovim earns its place when you stop treating it like a fixed tool and start treating it like a platform you can program. A platform invites composition. You decide what loads, when it loads, and how the parts talk to each other. This subchapter explains that shift in practical terms so the rest of the book has a sturdy frame. We will contrast scripted configuration with copy pasted bundles, show how small Lua modules create clarity, and map the minimum surface needed to behave like an IDE without inheriting bloat. The aim is confidence. When you can describe your editor in a few files and a handful of decisions, you can also change it safely.

A platform mindset begins with separation of concerns. Options live in one place with clear names and comments. Keymaps collect in their own file with a consistent leader and pattern. Plugins are declared as data with lazy.nvim and configured close to their declarations. Language tooling sits in a language folder, and per project overrides live behind trust prompts. This makes each decision visible and testable. When something breaks, you know which file to read and which commit to revert. Good platforms make failure local. Neovim lets you design for that property.

Another platform habit is determinism. You want the same setup on every machine. That means pinning plugin versions, storing your configuration in a repository, and documenting bootstrap steps. It also means avoiding hidden global state. Do not write secrets into plain files or rely on environment variables that only exist on one laptop. Prefer explicit checks and clear errors so failures are honest. When deterministic behavior is the norm, you can move quickly without fear because a broken change is easy to reproduce and therefore easy to fix.

A third habit is measurement. Platforms succeed when they provide feedback loops that are quick and fair. In practical terms, you will profile startup with the same command, track times across commits, and keep the fastest result. You will read :messages after changes and fix warnings before they grow into crashes. You will test a feature the way you actually work, not in an empty buffer. Measurement keeps taste honest. It turns vague impressions into data and lets a small configuration beat a larger one by evidence rather than opinion.

Treating Neovim as a platform also shapes plugin choices. Each addition must justify its cost. The best plugins solve a hard problem with little configuration, degrade gracefully when unavailable, and do not fight your defaults. Pick one tool for each job and avoid chains of overlapping features. If two plugins promise similar outcomes, favor the one with clearer documentation, a smaller surface, and active maintenance. You will notice the platform bias shows up in naming and structure. Short files with crisp names make intent obvious and reduce future friction.

The programmable IDE emerges when you wire a few core capabilities. Completion comes from nvim cmp tied to your language servers. Formatting and linting flow through none ls or conform so rules are consistent. Treesitter provides structural awareness, giving you smart selection and robust highlighting even in tangled code. The Debug Adapter Protocol adds controlled stepping so you can keep context while fixing bugs. Together these parts deliver the editing experience people only expect from heavy IDEs, but here they load on your terms and respect your machine.

A platform must also handle failure well. Safe mode patterns help you start quickly when a plugin crashes. Minimal mode lets you isolate a problem with as few moving parts as possible. Rollback habits let you ship updates without dread. When you design for graceful failure, you turn scary surprises into manageable tasks. That comfort encourages better experiments and keeps your configuration healthy over time.

The final ingredient is communication. A platform is easier to maintain when its rules are clear to future you and to teammates. Write a short README that explains how to bootstrap, how to update, and how to revert. Document where local configuration is allowed and how secrets are handled. Note your expectations for performance and the commands you use to verify them. This small document acts like an API for your editor and removes guesswork when something changes.

To practice the platform shift, commit to a small exercise. Describe your editor in five sentences. List the minimum set of features you cannot live without. Remove one plugin and measure the effect on startup and navigation. If nothing is lost, keep it out. Replace a heavy tool with a leaner one and review the result the next day. These short loops build taste and keep the platform tight.

UI first does not mean UI only. Begin with visible, testable wins before adding code. Configure a statusline that reveals mode and diagnostics. Choose a theme with clear contrast. Use Telescope to make navigation instinctive. Then describe behavior in Lua so it is repeatable. This order preserves momentum and keeps the platform friendly for contributors.

A concrete scenario shows the value. Your team inherits a legacy service with mixed languages. With a platform mindset, you create a base that boots fast on every laptop. You pin plugin versions, install only needed servers with mason.nvim, and keep formatters consistent. Developers clone, run a bootstrap, and reach a first commit within an hour. Because the platform is small and documented, onboarding is calm.

Use a recurring check to keep standards clear.

Run the same startup profile and compare against a threshold documented in the README.

Open a large repository, trigger completion, jump to a definition, and return with a mapped key.

Save to trigger formatting, inspect diagnostics, and confirm no warnings in :messages.

If a change fails any check, revert and write a short note.

The editor as platform is not a slogan. It is a working agreement with yourself and your team. Keep the core small, document the rules, measure the effects of change, and prefer clarity over features. Do this consistently and Neovim becomes a base you can extend when needed.

1.3 MENTAL MODELS: BUFFERS, WINDOWS, AND TABS WITHOUT CONFUSION

Clear mental models turn Neovim from a bag of commands into a calm workspace. This subchapter explains buffers, windows, and tabs in practical terms so you can reason about layouts, recover quickly from mistakes, and work with multiple files without losing context. Once these concepts are firm, the rest of the book will feel simpler because you can map features back to a small set of ideas. You will learn what persists, what is a view, and what is a layout, and you will practice switching among them without surprises.

A buffer is text held in memory. It may come from a file on disk, a terminal session, or a temporary scratch. Because buffers are independent of views, they persist even when nothing is displaying them. This is why you can close a window and still return to its content with a buffer command. Buffers have numbers, listed by :ls, and you can jump to one with :b followed by that number or a unique fragment of the name. Thinking of buffers as durable objects makes recovery easy after an accidental close.

A window is a view onto a buffer. It controls size, split position, and local options like number display. Multiple windows can show the same buffer at different locations, which is useful for comparing functions, scanning references, or editing tests while reading an implementation. Closing a window removes the view but leaves the buffer intact. Moving between windows with mapped keys keeps flow constant, especially when combined with a statusline that shows mode and file diagnostics.

A tab groups one or more windows into a layout. Tabs are often misunderstood as one file per tab, but in Neovim they are workspace containers. Each tab can hold a small dashboard of windows, such as a file tree, a code view, and a test panel. Switching tabs swaps the entire layout at once. Tabs are optional. Some users prefer one tab with flexible splits, others use tabs to separate tasks like feature work and review. The model is flexible enough to support either habit.

Understanding these objects unlocks useful patterns. You can open two windows on the same buffer to refactor safely, keeping the target in view while you change a call site elsewhere. You can dedicate a small window to Telescope results and close it after jumping. You can keep a terminal split attached to a test command and reuse it as you iterate. Because buffers persist, you can close a temporary window without fear of losing work.

When you know what survives, you can recover faster. If you close a window by mistake, list buffers with :ls and jump back. If a tab feels cluttered, close it and rebuild the layout in seconds. If a split becomes distracting, quit it and continue editing in the remaining view. These moves become automatic when the model is clear, and your hands stop hesitating because recovery is obvious.

The model also supports good ergonomics. Consistent keymaps for moving among windows mean your eyes stay on the code. A statusline that surfaces tab and window information reduces confusion. Reasonable defaults for split directions and width help you think less about layout and more about intent. This is why the book favors UI first improvements early in the journey. Clarity in the model makes every later tool easier to learn.

Practical habits make the ideas stick. Give files readable names so buffer lists are useful. Close dead windows before they pile up. Use a simple naming pattern for tabs, such as feature, review, and docs, so a glance tells you where to go. When resuming work, restore a session rather than guessing. Small rituals like these keep state helpful instead of noisy.

Common confusions deserve direct answers. Does closing a tab delete buffers inside it? No, buffers live beyond that container. Do you need a tab for every file? No, one tab can hold many windows and many buffers. Is a terminal a buffer? Yes, it is a special buffer type with its own behaviors. What happens to unsaved changes when you close a window? They remain in the buffer until you write or discard them. These precise claims help you predict outcomes.

To lock in the model, run a short exercise. Open one file, split the window, and scroll each pane to a different function. Note that both views are the same buffer. Now open a second file in a new window and then place both windows into a new tab. Close one window and confirm the buffer still appears in :ls. Switch back to the previous tab and verify your earlier layout is intact. This simple drill proves the definitions by direct experience.

The model naturally scales to projects and remote sessions. On a large repository you might use one tab as a stable working layout and another as a scratch space. Over SSH you keep the number of windows modest to respect latency, but the rules do not change. Because buffers hold state, you can close a view and return without fear even on a slow link. This consistency is one reason Neovim works well across environments.

Finally, tie the model to verification. When a change to your configuration affects layout behavior, run the same drill and ensure buffers persist and windows respond to your mappings. Keep a tiny checklist in your repository so the test is fast and boring. Predictable behavior is the goal. When buffers, windows, and tabs behave as defined, you can invest attention in the code itself, confident that your editor will not surprise you each day.

CHAPTER 2: CLEAN INSTALLS AND REPRODUCIBLE SETUPS

INTRODUCTION

This chapter turns a fresh Neovim into a reliable base you can reproduce anywhere. The stakes are simple. If your editor cannot be installed quickly, explained easily, and restored in minutes after a mistake, you will avoid changing it and your workflow will stagnate. We focus on clean install paths, a minimal init.lua that proves the environment works, and the version control habits that make every change safe to test and easy to reverse. The flow is UI first where helpful, with light Lua only when it removes friction or cost.

You will begin by placing configuration files in the correct folders for Linux, macOS, and Windows, then launch with a clean profile to confirm no legacy settings leak into your new setup. Next, you will place your configuration under source control with a small commit strategy, create a profile log for startup time, and practice a rollback to build confidence. Finally, you will add baseline options and essential keymaps that reduce friction on day one without locking you to any plugin choices later. Each step ends with a verification routine that you can rerun after updates.

By the end of this chapter, you will have a minimal yet complete configuration that opens quickly, behaves predictably, and lives in a repository with clear bootstrap notes. You will know how to record startup time, where to look for warnings, and how to revert a single change without panic. That foundation prepares you to adopt lazy plugin loading, language servers through mason.nvim, and Treesitter in the next chapter with far less risk. Now set the stage by establishing correct paths, a clean init.lua, and a short, repeatable test loop you trust.

2.1 INSTALL PATHS, CONFIG FOLDERS, AND A MINIMAL INIT.LUA

A clean install starts with correct folders and a minimal file that proves Neovim is reading the right configuration. The stakes are high because legacy files, conflicting plugins, or mislocated paths can waste hours and create intermittent bugs that are hard to explain. This subchapter gives you a single layout that works across Linux, macOS, and Windows, shows how to start with a clean profile to isolate issues, and introduces a minimal init.lua that adds visible feedback without side effects. Keep changes small and verifiable so you can detect friction early and roll back quickly.

On Linux and macOS, Neovim reads from ~/.config/nvim. On Windows, it reads from %USERPROFILE%\AppData\Local\nvim. Create the folder if missing and ensure there is no stray init.vim left from older setups. Use a fresh shell and nvim –clean to confirm a default session launches without your config. Then exit and start nvim normally. If a custom message appears from your new init.lua in the correct folder, you have proven the path is correct and the file is active. That confirmation eliminates a large class of early errors.

Your minimal init.lua must be readable, portable, and safe to extend. Begin by setting leader, enabling numbers, and printing a startup message. Avoid heavy plugins or advanced options until this foundation feels solid. The goal is a clear signal that the correct file loaded and that a tiny change survives a restart. After saving the file, open Neovim twice from a cold shell to confirm consistent behavior. Check :messages for warnings. If anything looks suspicious, fix the file or revert to the last known good state.

LUA

-- ~/.config/nvim/init.lua  or  %USERPROFILE%/AppData/Local/nvim/init.lua

vim.g.mapleader = " "

vim.opt.number = true

vim.opt.relativenumber = true

vim.opt.termguicolors = true

vim.opt.mouse = "a"

vim.api.nvim_create_autocmd("VimEnter", {

  callback = function()

    vim.notify("Neovim 2025 blueprint loaded", vim.log.levels.INFO)

  end,

})

-- basic keymap sanity check

vim.keymap.set("n", "<leader>s", "<cmd>w<cr>", { desc = "Save file" })

Verification: Launch with nvim and observe a notification reading Neovim 2025 blueprint loaded. Line numbers and relative numbers should be active. Press space then s to save without errors. Run :messages to confirm no warnings. File path must match platform conventions. Rerun from a new shell to verify deterministic behavior.

To ensure legacy files do not pollute results, run a short isolation procedure. First, start with nvim –clean and verify your message does not appear. Second, start with nvim and confirm the message and options are present. Third, temporarily rename your config folder and check that behavior returns to vanilla defaults. These three moves prove that only your folder controls behavior and that the minimal init.lua is both necessary and sufficient for the observed changes.

If you manage multiple machines, define a bootstrap script that checks for Neovim, creates the configuration folder, writes a minimal init.lua if missing, and prints next steps. Keep it tiny and language agnostic. The purpose is to reduce friction for a new laptop or a temporary virtual machine. A small script saves time and eliminates guesswork when you are under pressure. Commit the script to your repository alongside a brief README so teammates can use the same path.

LUA

-- ~/.config/nvim/lua/bootstrap.lua  optional helper

local config = vim.fn.stdpath("config")

local init = config .. "/init.lua"

if vim.fn.filereadable(init) == 0 then

  vim.fn.writefile({

    'vim.g.mapleader = " "',

    "vim.opt.number = true",

    'vim.api.nvim_create_autocmd(\"VimEnter\",{callback=function()vim.notify(\"Bootstrap init created\")end})',

  }, init)

  print("Wrote minimal init.lua at " .. init)

end

Verification: From a fresh install, require(“bootstrap”) in a temporary session. The file init.lua should be created exactly once. Subsequent runs should not rewrite it. Confirm that a new Neovim launch shows the Bootstrap init created notification, then replace the generated content with your curated minimal file and commit the change.

Close the loop with a repeatable check. Record startup time to a log you can compare after future changes. Create an out folder in your home or project directory and save a profile for cold starts. Keep the command line identical between runs. Review the slowest lines and note the baseline. This habit anchors future optimization and prevents drift as you extend the configuration.

Create a folder named out in your home directory or a dedicated workspace.

Run nvim –startuptime out/startup.log and then quit immediately.

Open out/startup.log, note total time, and commit the file if your policy allows.

A minimal, verified configuration is more than a warm up. It is an anchor for everything you add later. When plugin loading, language servers, or Treesitter introduce surprises, you can compare behavior to this baseline and isolate the cause much faster. With paths correct, a clean profile tested, and a tiny init.lua in place, you are ready to place your configuration under version control and practice safe updates and reversions without fear.

2.2 VERSION-CONTROLLING YOUR CONFIG, PROFILES, AND SAFE ROLLBACKS

Your editor becomes reliable when its state is visible, reviewable, and reversible. Version control gives you that power. This subchapter shows how to initialize a Neovim config repository, commit with meaningful messages, record startup profiles alongside code, and practice rollbacks so a broken change never costs you a day. You will also create a short README that explains setup, updates, and recovery. Treat your configuration like any production system: small changes, clear diffs, measured results, and a quick path back when something misbehaves.

Begin by checking your configuration path into source control. Keep the repository focused. Store only files that you intend to share or restore, avoid private secrets, and ignore volatile artifacts. Use a predictable structure under ~/.config/nvim or the Windows equivalent so teammates can clone and link without surprises. Write a minimal README that states prerequisites, the bootstrap command, and a three step verification check. These small documents save future you more times than you expect and reduce onboarding noise for others.

LUA

-- ~/.config/nvim/.gitignore  tracked in repo root

out/

swap/

undo/

session/

*.log

*.backup

Verification: Git should ignore logs, caches, swap, undo, and session files. Running git status after a normal editing session should show only intentional changes to tracked Lua or configuration files. If you see transient artifacts, add or refine ignore patterns, then recheck until the working tree is clean after a typical day.

Commit culture matters. Favor small, single purpose commits with messages that state intent and outcome. When you measure startup time or remove a plugin, capture the data point in the message or store the log in out with a timestamp. Over time, these notes form a performance history you can mine during regressions. Avoid large, mixed commits that combine plugin changes, keymaps, and formatting rules. Smaller is safer and easier to revert cleanly.

LUA

-- README excerpt: minimal bootstrap flow

-- 1) Clone repo to ~/.config/nvim

-- 2) Start nvim to verify startup message and line numbers

-- 3) Record baseline: nvim --startuptime out/startup-YYYYMMDD.log + quit

-- 4) Optional: :checkhealth and inspect :messages for warnings

Verification: A teammate or a future you should be able to follow these four steps without improvisation. Ask a colleague to try them on a clean machine. If they hit friction, fix the docs and commit the improvement. Documentation is part of the product. Treat it like code.

Safe rollbacks require discipline. Practice them before you need them. Create a branch that adds a small but visible change, such as a new plugin, and capture startup time. If performance regresses or warnings appear, revert the commit and confirm behavior returns to baseline. This drill builds reflexes you will rely on later when the stakes are higher and time is short. Knowing that a failed experiment is easy to undo encourages healthy exploration.

LUA

-- example: lazy.nvim plugin spec fragment enabling rollbacks via version pin

return {

  {

    "nvim-lualine/lualine.nvim",

    version = "*",

    event = "VeryLazy",

    config = function() require("lualine").setup({ options = { theme = "auto" } }) end,

  },

}

Verification: With version set to a tag or wildcard, plugin updates become explicit. After updating plugins, restart Neovim and verify statusline loads without errors. If a regression appears, pin to a known good version, commit the change, and log the incident in out/notes-YYYYMMDD.md with steps to reproduce and the resolution.

Profiles are artifacts worth keeping. Save startup logs under out with a clear date pattern and compare totals across commits. When a change increases startup time, decide whether the benefit is worth the cost or revert. Keep the same profiling command to ensure apples to apples comparisons. Profile on the hardware you actually use to avoid misleading results from unusually fast or slow systems.

Run nvim –startuptime out/startup-YYYYMMDD.log twice and average the totals.

After meaningful changes, repeat the test and compare against baseline.

If totals exceed your threshold, revert, adjust, or document the tradeoff explicitly.

Backups close the loop. Even with version control, accidental deletion or a machine failure can interrupt work. Periodically export a snapshot of your configuration and store it in a cloud folder or encrypted archive. If your repository is private, verify that access works from a second account or device. A disaster that reveals a missing key is an avoidable failure. Routine tests prevent it.

Finish by simulating a simple incident. Introduce a harmless syntax error in a plugin config, attempt to start Neovim, observe the error, then revert the commit and confirm recovery. Record the experience in your README under a Recovery section with three short steps. This practice lowers stress during real problems and makes the next experiment feel safe. With version control, profiles, and rollbacks in place, you can move on to adding baseline options and ergonomic keymaps with confidence that every change is anchored by data and easy to undo.

2.3 BASELINE OPTIONS AND ESSENTIAL KEYMAPS THAT REDUCE FRICTION

Small, thoughtful defaults create a daily feeling of calm. This subchapter adds baseline options and a handful of keymaps that improve clarity, navigation, and saving without painting you into a corner. The stakes are practical. Good defaults help beginners feel at home, keep experts fast, and set a coherent tone for later features like Telescope, Treesitter, and language servers. Every change here is reversible, documented, and verified with a short routine you can run after updates.

Start with visibility. Relative line numbers improve motion, a sign column prevents text from shifting when diagnostics appear, and a clear color scheme paired with termguicolors helps focus. Next, refine search behavior so it feels modern and predictable. Finally, set a leader key and map a few actions you use dozens of times per hour. Keep mappings discoverable and safe. Do not override core motions without a strong reason. Favor additions that reduce keystrokes while respecting Vim defaults that many users rely on.

LUA

-- ~/.config/nvim/lua/core/options.lua

vim.opt.number = true

vim.opt.relativenumber = true

vim.opt.signcolumn = "yes"

vim.opt.termguicolors = true

vim.opt.cursorline = true

vim.opt.ignorecase = true

vim.opt.smartcase = true

vim.opt.incsearch = true

vim.opt.hlsearch = false

vim.opt.splitright = true

vim.opt.splitbelow = true

vim.opt.updatetime = 200

vim.opt.timeoutlen = 400

Verification: Restart Neovim. Confirm relative numbers, a persistent sign column, and a highlighted cursor line. Search for a lowercase term and observe case insensitive matches; add a capital letter to trigger smartcase. Open vertical and horizontal splits to verify they appear to the right and below. Move around and confirm the interface feels stable with no text shifting when diagnostics show.

Ergonomic keymaps reduce hesitation. Map saving, quitting, clearing search highlights, and window switching to consistent, memorable sequences. Use descriptions so which key or similar helpers can reveal your choices. Keep motions intact and avoid shadowing widely used defaults. The goal is a minimal layer that helps without surprise.

LUA

-- ~/.config/nvim/lua/core/keymaps.lua

vim.g.mapleader = " "

local map = vim.keymap.set

local opts = { noremap = true, silent = true }

map("n", "<leader>s", "<cmd>w<cr>", vim.tbl_extend("force", opts, { desc = "Save file" }))

map("n", "<leader>q", "<cmd>confirm q<cr>", vim.tbl_extend("force", opts, { desc = "Quit window" }))

map("n", "<leader>h", "<cmd>nohlsearch<cr>", vim.tbl_extend("force", opts, { desc = "Clear highlights" }))

map("n", "<C-h>", "<C-w>h", opts)

map("n", "<C-j>", "<C-w>j", opts)

map("n", "<C-k>", "<C-w>k", opts)

map("n", "<C-l>", "<C-w>l", opts)

Verification: Press space then s to save and space then q to quit the current window with a confirmation prompt. Run a search, then press space then h to clear highlights. Use Control plus h j k l to move across splits instantly. Run :map  to confirm descriptions appear for discovery tools. Ensure no warnings appear in :messages and commits include these files.

Organize files so intent is obvious. Require your options and keymaps from init.lua in a small, readable block. Avoid clever loader logic. Simple requires are easier to debug and work everywhere. Keep folder names predictable so teammates can navigate quickly. Consistency is a form of documentation and reduces friction for future changes.

LUA

-- ~/.config/nvim/init.lua (relevant excerpt)

require("core.options")

require("core.keymaps")

vim.api.nvim_create_autocmd("VimEnter", {

  callback = function()

    vim.notify("Core loaded: options and keymaps")

  end,

})

Verification: On startup, a notification should confirm that core modules loaded. If a file is missing or misnamed, Neovim will show an error with the failing require path. Fix the path or file, restart, and verify the message appears. Keep module names stable to avoid churn in your commit history.

Close with a short daily check. Record a startup profile, open a known repository, jump between splits using your new mappings, run a search, and clear highlights. If anything feels slower or less clear than yesterday, capture the observation in out/notes and consider a small adjustment. These rituals keep the configuration healthy and grounded in practical use rather than theory. With baseline options and essential keymaps in place, you have a comfortable, predictable core that makes the next step enjoyable. You are ready to install a plugin manager and begin wiring features with lazy loading for speed and control.

CHAPTER 3: LUA FUNDAMENTALS FOR NEOVIM  

INTRODUCTION

Lua is the language that turns Neovim from a fast editor into a programmable platform. This chapter gives you the minimum grammar, data structures, and editor APIs you will actually use, then shows how to organize code into modules that load cleanly from your runtime path. You will write tiny, readable functions that set options, define keymaps, register autocommands, and return tables that other files can require without surprises. The emphasis is practical and UI first. We push code only where it shortens steps, removes friction, or improves reproducibility across machines.

Subchapter 3.1 covers syntax essentials and the table type, which acts as arrays, dictionaries, and modules. You will build a few small tables, iterate safely, and return structured configuration for later files to consume. Subchapter 3.2 explains modules, require, and the runtime path. You will place files under lua/, set package paths implicitly through standard locations, and prove resolution with a verification routine. Subchapter 3.3 connects everything back to daily work. You will register autocommands for common events, define keymaps with descriptive text for discoverability, and create tiny utilities you can test in isolation.

Outcomes are concrete. By the end of the chapter you will be able to write a table driven config, split it into modules, load them with require, and debug the loading order by reading messages and printing trace points. You will keep filenames predictable, return values explicit, and side effects minimal. Most importantly, you will adopt a verification habit for every block you add, including saved artifact paths and deterministic settings. With these habits in place, your next steps into plugin specs, language servers, and Treesitter will feel straightforward.

3.1 LUA SYNTAX ESSENTIALS AND TABLES YOU’LL ACTUALLY USE

Lua succeeds in Neovim because it is small, predictable, and fast to embed. You only need a slice of the language to become effective. This subchapter focuses on values, control flow, and especially tables, since nearly all Neovim configurations pass structured data through tables. The stakes are simple. When you can read and write small tables confidently, you can express options, plugin specs, and keymaps with clarity, then grow your configuration without creating fragile glue code.

Begin with values and locals. Lua has nil, booleans, numbers, strings, and functions. Prefer local variables because they are faster and reduce accidental global state. Concatenate strings with .. and use string.format for clarity when building messages. Functions are first class and can be nested or returned. That flexibility makes tiny helpers easy to compose and test. Treat each helper like a private tool you might share later by returning it from a module.

Tables unify arrays and dictionaries. Numeric keys form array like sequences, while string keys form dictionaries. You can mix both in one table, but clarity matters more than clever tricks. Prefer simple shapes with predictable keys, and return them directly so other files can require and use them without mutating state from the side. This pattern mirrors plugin specs and option maps you will see throughout the book.

LUA

-- lua/core/values.lua

local M = {}

local function greet(name)

  name = name or "world"

  return ("Hello, %s"):format(name)

end

M.answer = 42

M.greet = greet

return M

Verification: Require with local v = require(“core.values”) from :lua or another module. Call v.greet(“Neo”) and expect Hello, Neo. Confirm v.answer equals 42. No files outside lua/ should modify M. Saved path is ~/.config/nvim/lua/core/values.lua on Unix or %USERPROFILE%\AppData\Local\nvim\lua\core\values.lua on Windows. Reruns produce identical output.

Iteration in Lua uses pairs for arbitrary keys and ipairs for sequence parts. Prefer ipairs when you expect array like data to avoid visiting non numeric keys out of order. When transforming lists, build a new table rather than mutating in place unless performance demands otherwise. That discipline makes modules easier to reason about and reduces hidden coupling between files.

LUA

-- lua/core/tables.lua

local M = {}

function M.keys(tbl)

  local out = {}

  for k, _ in pairs(tbl) do

    out[#out + 1] = k

  end

  table.sort(out)

  return out

end

function M.map(list, f)

  local out = {}

  for i, v in ipairs(list) do

    out[i] = f(v, i)

  end

  return out

end

return M

Verification: In :lua, run local t = require(“core.tables”); print(vim.inspect(t.keys({a=1, c=3, b=2}))) and expect a sorted array {“a”,“b”,“c”}. Run t.map({1,2,3}, function(x) return x*2 end) and expect {2,4,6}. File stored at lua/core/tables.lua. Results are deterministic for the same inputs.

Tables also serve as module return values. A common Neovim pattern is returning a table of options or plugin specs. Keep keys explicit and use minimal nesting so diffs remain readable. When a table gets long, split into smaller modules and compose them in a top level file. This keeps load time short and makes failures local, since a bad module causes a clear error at require time.

LUA

-- lua/core/options.lua

local o = {}

o.number = true

o.relativenumber = true

o.signcolumn = "yes"

o.ignorecase = true

o.smartcase = true

return o

Verification: In your init.lua, require(“core.options”) and then apply with for k,v in pairs(opts) do vim.opt[k] = v end. Expect line numbers, relative numbers, and smartcase search to activate. If a key is misspelled, Neovim will raise an error when applied, which is preferable to silent failure. Store path at lua/core/options.lua.

String keys can be quoted or bare identifiers if they meet identifier rules. Numeric indices begin at 1, not 0. When building a list of plugin specs or keymaps, use array style tables so ipairs preserves order. For dictionaries of options, prefer key assignment so intent is obvious at a glance. Maintain this distinction consistently across the repository to lower cognitive load when scanning files.

LUA

-- lua/core/specs.lua

local M = {

  { name = "ui", plugin = "nvim-lualine/lualine.nvim" },

  { name = "search", plugin = "nvim-telescope/telescope.nvim" },

}

return M

Verification: Require with local specs = require(“core.specs”). Iterate using ipairs to preserve the declared order. Expect specs[1].name to equal “ui”. Because the file returns a table literal without side effects, reruns are deterministic and safe.

Finish with a short practice loop that you will reuse later.

Create a new module under lua/ with a tiny table and a function.

Require it from init.lua, print a value, and call the function.

Restart Neovim from a cold shell and confirm identical output.

Commit the file with a message that captures intent and expected behavior.

With syntax and tables in hand, you can express most configuration cleanly. Next you will organize modules, understand how require finds them, and verify the runtime path so loading remains predictable as your repository grows.

3.2 MODULES, REQUIRE, AND THE RUNTIME PATH FOR CLEAN STRUCTURE

LENGTH: 4 pp. (Total word target: 1096 words)

Neovim locates Lua modules using its runtime path, a search list that includes your ~/.config/nvim/lua directory and plugin lua folders. A clear structure ensures require finds the right file on the first attempt and that errors point to the correct place when something breaks. This subchapter shows you how to lay out modules, name them for readability, and prove resolution with trace points. You will also create a top level loader that wires options and keymaps in a small, deterministic order.

Place code under lua/ using directories that map to module names. A file at lua/core/values.lua is required as require(“core.values”). Neovim adds lua/ to package.path automatically for your configuration directory. Avoid manipulating package.path unless you have a compelling reason. Predictable structure beats clever tricks and simplifies onboarding for teammates who must read your files without context.

Organize modules by concern. Core covers options, keymaps, and utilities. Plugins holds specs and plugin specific settings. Lsp contains language server setup. Tools or features can collect additional helpers as your needs grow. Keep names short and precise. Prefer a few focused modules over one giant file so diffs remain small and failures are local. Return values explicitly and keep side effects to a minimum in require time. Defer work to a setup function when a plugin is not guaranteed to be present.

LUA

-- ~/.config/nvim/init.lua

local ok, _ = pcall(require, "core.loader")

if not ok then

  vim.api.nvim_echo({{"Failed to load core.loader", "ErrorMsg"}}, true, {})

end

Verification: On startup, Neovim should require core.loader. If the file is missing or contains a syntax error, you will see a clear message without a hard crash. Store core files at ~/.config/nvim/lua/core/*.lua. Behavior should be deterministic across restarts.

LUA

-- lua/core/loader.lua

local M = {}

local function safe_require(name)

  local ok, mod = pcall(require, name)

  if not ok then

    vim.schedule(function()

      vim.notify("Require failed: " .. name, vim.log.levels.WARN)

    end)

    return nil

  end

  return mod

end

function M.setup()

  local opts = safe_require("core.options")

  if opts then for k, v in pairs(opts) do vim.opt[k] = v end end

  safe_require("core.keymaps")

end

M.setup()

return M

Verification: On launch, options apply and keymaps register. If a module fails, a non blocking notification appears. :messages should show no stack traces, only your warning if a file is missing. Re runs behave the same. Files are loaded in a deterministic order: options, then keymaps.

Sometimes you need to add directories beyond the standard lua/. For example, you may store organization wide helpers in ~/.local/share/nvim/site/lua. Prefer standard runtime locations so updates remain compatible with tools that expect them. If you must extend search paths, do so in a tiny, well documented block and verify with a require test you can disable if resolution fails.

LUA

-- lua/core/paths.lua

local M = {}

function M.add(path)

  if not vim.tbl_contains(vim.opt.rtp:get(), path) then

    vim.opt.rtp:append(path)

  end

end

return M

Verification: Require(“core.paths”).add(”~/.local/share/nvim/site”) after expanding to an absolute path. Confirm with :echo &rtp that the path appears. Then place a test module at that location and require it once to prove resolution. Remove the path if not needed to keep the runtime small.

Print trace points only when debugging load order. Use vim.notify or print with a clear prefix and remove traces when done. Persistent logging can slow startup and clutter messages. When a require fails, check module names, file case, and directory placement first. Most issues come from typos or mixed case on case sensitive file systems.

Build a top level pattern you will reuse.

init.lua requires core.loader with pcall to avoid hard failure.

core.loader applies options first, then keymaps, then plugin loaders later.

Each module returns a table or runs a setup function with minimal side effects.

Failures notify once and let the session continue so you can repair from inside Neovim.

LUA

-- lua/plugins/init.lua

local specs = require("plugins.specs")

for _, spec in ipairs(specs) do

  -- defer actual plugin manager work to the next chapter

  vim.notify("Spec: " .. (spec.name or spec.plugin))

end

Verification: On startup, you should see a brief set of notifications listing your declared specs. Replace notifications with actual plugin manager calls in the next chapter. Files resolve deterministically using require names that mirror directory structure. With modules and paths settled, you can safely register events and keymaps.

3.3 AUTOCOMMANDS, KEYMAPS, AND TINY UTILITIES IN LUA

Autocommands and keymaps connect your Lua modules to real editing behavior. This subchapter shows how to create namespaced autocommand groups, register reliable handlers, and define descriptive keymaps that are easy to discover and safe to change. You will also write two tiny utilities that demonstrate how to package behavior behind commands you can call from normal mode without memorizing long expressions. The goal is to keep logic small, explicit, and testable so mistakes are reversible and features remain legible months later.

Start with a simple pattern for autocommands. Use vim.api.nvim_create_augroup to define a stable group name, then attach one or more autocommands with clear callbacks. Keep callbacks short and avoid heavy work on frequent events. If a task is expensive, schedule it, throttle it, or run it on specific filetypes. Always include a desc field where supported so your intent is visible when inspecting state. Namespacing prevents collisions and makes it easy to clear and recreate rules during refactors.

LUA

-- lua/core/autocmds.lua

local M = {}

local grp = vim.api.nvim_create_augroup("CoreAutocmds", { clear = true })

vim.api.nvim_create_autocmd("TextYankPost", {

  group = grp,

  desc = "Highlight yanked text",

  callback = function() vim.highlight.on_yank({ timeout = 200 }) end,

})

vim.api.nvim_create_autocmd("BufWritePost", {

  group = grp,

  pattern = "*.md",

  desc = "Echo saved note",

  callback = function() vim.notify("Saved note: " .. vim.fn.expand("%:t")) end,

})

return M

Verification: Yank text and expect a brief highlight. Save a Markdown file and expect a notification naming the file. :au CoreAutocmds should list both rules. Reloading the module should clear and recreate the group without duplicates. Behavior is deterministic across sessions.

Keymaps deserve the same care. Define a helper that sets options and merges descriptions so you do not repeat boilerplate. Map only actions you use constantly, keep motions intact, and describe each mapping for discovery tools. Use silent and noremap where appropriate and scope to specific modes. This approach keeps keymaps readable and reduces conflict during future changes.

LUA

-- lua/core/keymaps.lua

local function nmap(lhs, rhs, desc)

  vim.keymap.set("n", lhs, rhs, { noremap = true, silent = true, desc = desc })

end

nmap("<leader>e", "<cmd>edit %<cr>", "Reopen current file")

nmap("<leader>r", "<cmd>luafile %<cr>", "Source current Lua file")

nmap("<leader>t", "<cmd>tabnew<cr>", "New tab")

Verification: Press space then e to reopen the current file, space then r to source the current Lua buffer, and space then t to open a new tab. Run :map  to confirm descriptions are present. No warnings should appear in :messages. Reruns preserve identical behavior.

Tiny utilities demonstrate how to package repeated actions. Wrap commands inside functions, expose them as user commands, and keep side effects limited to the buffer or window where they run. This makes utilities predictable and safe to share. Begin with a TODO counter and a quick toggle for relative numbers, two actions that are easy to test and useful on most projects.

LUA

-- lua/core/utils.lua

local M = {}

function M.todo_count()

  local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)

  local n = 0

  for _, line in ipairs(lines) do

    if line:match("TODO") then n = n + 1 end

  end

  vim.notify(("TODOs in file: %d"):format(n))

end

function M.toggle_rnu()

  vim.opt.relativenumber = not vim.opt.relativenumber:get()

  vim.notify("relativenumber: " .. tostring(vim.opt.relativenumber:get()))

end

vim.api.nvim_create_user_command("TodoCount", M.todo_count, {})

vim.api.nvim_create_user_command("ToggleRnu", M.toggle_rnu, {})

return M

Verification: Run :TodoCount to see a count of TODO lines in the current buffer. Run :ToggleRnu to flip relative numbers and receive a notification with the new state. Commands should exist in :command. Detected behavior is the same across restarts. File path is lua/core/utils.lua.

Wire everything together from your loader to keep order clear and failures local.

In core.loader, require core.autocmds, core.keymaps, and core.utils after options.

Start Neovim, yank text, save a Markdown file, and run both user commands.

If behavior differs from expectation, check :messages, fix the module, and retry.

Commit with a message that records expected outcomes for each utility.

With autocommands, keymaps, and tiny utilities in place, your editor now responds predictably to common events and exposes reusable actions behind short commands. The modules remain small, names are explicit, and verification steps are quick. You have everything needed to declare plugin specs in data, load them with a manager, and measure startup impact while keeping behavior deterministic in the next chapter.

CHAPTER 4: PLUGIN MANAGEMENT WITH LAZY.NVIM  

INTRODUCTION

Plugins turn Neovim from a capable editor into a focused IDE you control, but unmanaged plugins slow startups, break unexpectedly, and make rollbacks painful. This chapter gives you a disciplined workflow with lazy.nvim so every addition is intentional, deferred until needed, and easy to revert. We begin by installing lazy.nvim and organizing plugin specs in small, readable files that return plain Lua tables. Next we wire lazy loading through events, commands, and filetypes, then measure the impact using repeatable startup profiles so performance decisions rest on data, not guesswork. Finally we pin versions, stage updates, and practice painless rollbacks so experiments never threaten daily work.

The emphasis stays practical and UI first. You will see where files live, how specs look, and which defaults reduce friction for teams. Code remains minimal and copy safe, with verification steps that confirm behavior in minutes. You will keep configuration under version control, save startup logs under a stable path, and document update rules in your README so recovery is a routine, not a scramble. By the end you will have a lean plugin layer that starts fast, loads features only when requested, and provides a clear trail for debugging. That foundation prepares you to add LSP, completion, Treesitter, and DAP with confidence that your editor remains predictable, measurable, and easy to fix when something changes unexpectedly. Begin by installing lazy.nvim and shaping your first set of focused specs.

4.1 INSTALLING LAZY.NVIM AND ORGANIZING PLUGIN SPECS

Installing a plugin manager should not add complexity or mystery. Your goal is a tiny, explicit bootstrap that fetches lazy.nvim if missing, then loads a single module that returns a list of specs. Each spec is a small table that names the plugin and declares only the essentials: when to load, and how to configure. This approach favors clarity over magic and keeps diffs small when you add or remove tools. The stakes are simple. If you can read your plugin layer in one sitting, you can maintain it for years without fear.

Create a predictable layout. Keep specs under lua/plugins/specs.lua or split by concern under lua/plugins/*.lua when the list grows. Avoid putting long configuration blocks inline. Prefer require calls that point to small setup modules so each file remains legible and failures are local. With this structure, a broken UI plugin cannot block language servers from loading, and vice versa. You also make onboarding straightforward for teammates who only need to scan one directory to understand your stack.

Fetch lazy.nvim using its recommended bootstrap snippet, but keep the code readable and contained. Do not add global side effects or hidden state. After bootstrapping, call require(“lazy”).setup with your specs table and minimal options. Log the manager path only once to avoid noisy messages. Then verify: start Neovim, check :messages for warnings, and confirm that only core UI loads before any lazy events have fired. If you see unexpected plugins at startup, delay them to an event or command in the next subchapter.

LUA

-- ~/.config/nvim/init.lua (bootstrap excerpt)

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(require("plugins.specs"), {

  ui = { border = "rounded" },

  change_detection = { notify = false },

})

Verification: On first run, lazy.nvim clones to stdpath(“data”)/lazy/lazy.nvim. Subsequent runs should not clone again. :messages must be clean. :echo &rtp should show lazy near the front. No non essential plugins should appear before you trigger an event. Behavior must be identical across restarts.

LUA

-- ~/.config/nvim/lua/plugins/specs.lua

return {

  { "nvim-lualine/lualine.nvim", event = "VeryLazy",

    config = function() require("plugins.ui.lualine") end },

  { "nvim-telescope/telescope.nvim", cmd = "Telescope",

    dependencies = { "nvim-lua/plenary.nvim" },

    config = function() require("plugins.search.telescope") end },

  { "lewis6991/gitsigns.nvim", ft = { "gitcommit", "diff" },

    config = function() require("plugins.git.gitsigns") end },

}

Verification: Specs is a plain table. No plugins load at startup except those bound to VeryLazy. Running :Telescope should load telescope on demand. Opening a gitcommit buffer should load gitsigns. Errors identify the specific require path so fixes are local. Reruns produce the same behavior.

LUA

-- ~/.config/nvim/lua/plugins/ui/lualine.lua

return function()

  require("lualine").setup({ options = { theme = "auto", globalstatus = true } })

end

Verification: After entering the UI post startup phase, lualine appears with a global statusline. :messages has no warnings. Disabling the spec or commenting the config removes the statusline on next start, proving isolation. File lives under lua/plugins/ui/lualine.lua.

Keep specs minimal by design. When a plugin supports defaults that already match your goals, avoid overriding. Every line you add becomes code you must maintain. Prefer configuration that clarifies behavior, not code that restates defaults. If a plugin requires extensive setup, consider whether it truly solves a problem worth its weight. The next subchapter will defer these plugins until they are needed and measure the impact with a stable startup profile.

A short procedure helps you confirm structure and readiness before you add more.

Start Neovim and quit immediately to record a baseline with nvim –startuptime out/startup-plugins-00.log.

Run :Lazy to confirm the manager UI opens and shows your three specs with the expected events and commands.

Trigger :Telescope and open a gitcommit buffer to verify deferred loads. Inspect :messages after each trigger and commit when clean.

With a tidy bootstrap and a readable specs file in place, your plugin layer is ready for lazy loading rules that keep startups fast and predictable. You will now connect events, commands, and filetypes to measurable gains and back those decisions with repeatable logs.

4.2 LAZY LOADING, EVENTS, AND MEASURABLE STARTUP GAINS

Lazy loading pays for itself when you can point to numbers. Your aim is to move as much work as possible out of startup and into clear triggers without creating surprises. This subchapter wires common triggers such as events, commands, keys, and filetypes, then measures the effect with a stable profiling routine. When you defer correctly, the editor opens quickly, feels responsive, and still loads features instantly when you ask. If something lags, you either choose a better trigger or reconsider the plugin’s value.

Begin with events. VeryLazy fires after the UI is ready, making it a safe catchall for visual polish like statuslines. BufReadPre and InsertEnter are good for language tools that benefit from early presence but still do not need to block startup. CmdlineEnter, TermOpen, and FileType let you bind to precise moments. Prefer the narrowest trigger that still feels seamless. For heavy plugins, combine multiple conditions or move to a command so the user remains in control.

LUA

-- ~/.config/nvim/lua/plugins/specs.lua (extended triggers)

return {

{ "nvim-lualine/lualine.nvim", event = "VeryLazy",

config = function() require("plugins.ui.lualine") end },

{ "nvim-telescope/telescope.nvim", keys = { "<leader>ff", "<leader>fg" },

dependencies = { "nvim-lua/plenary.nvim" },

config = function() require("plugins.search.telescope") end },

{ "lewis6991/gitsigns.nvim", event = { "BufReadPre" },

config = function() require("plugins.git.gitsigns") end },

}

Verification: Telescope no longer loads at startup or on :Telescope alone, but when pressing mapped keys. Gitsigns attaches on first buffer read rather than at launch. Lualine still waits for VeryLazy. :Lazy profile should show reduced startup work. Behavior remains deterministic across restarts.

Map keys that feel natural and keep them discoverable with descriptions. Loading on keypress removes guesswork and avoids accidental cost. If a plugin must initialize state before use, consider using both an early event for a light setup and keys for the heavy UI. Test with real repositories to ensure there is no visible lag on the first trigger.

LUA

-- ~/.config/nvim/lua/plugins/search/telescope.lua

return function()

  local t = require("telescope")

  t.setup({ defaults = { mappings = { i = { ["<C-u>"] = false, ["<C-d>"] = false }}}})

  local map = vim.keymap.set

  map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", { desc = "Find files" })

  map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>",  { desc = "Live grep" })

end

Verification: Press space then ff to open file search and space then fg for live grep. Telescope loads on demand. No startup cost is added. :messages remains clean. Subsequent triggers are instant since the plugin is now in memory.

Measure results with a repeatable routine. Use the same machine, cold shell, and command for each run. Keep logs in an out directory with clear names. Compare totals and the slowest lines. Expect total startup time to drop after deferring heavy plugins. If totals do not change, your triggers are too broad or the plugin is already inexpensive at startup.

Run nvim –startuptime out/startup-plugins-01.log and quit.

Apply new triggers, then run nvim –startuptime out/startup-plugins-02.log.

Compare totals and the top ten slow lines. Keep the faster configuration.

For plugins that still add latency on first use, preheat them with a gentle event like VeryLazy while deferring most work to the first command. This hybrid approach gives a smooth initial feel without taxing startup. If a plugin remains heavy, consider an alternative that solves the same problem with less configuration. Your performance budget is limited. Spend it on features that matter daily and prove their value with numbers, not impressions.

Close by documenting triggers in your README so teammates know what to expect and how to diagnose delays. With events and keys tuned, you now need a safety net for change. You will pin versions, schedule updates, and practice rollbacks so that improvements do not become regressions under pressure.

4.3 PINNING, UPDATES, AND PAINLESS ROLLBACKS

Speed is not enough if updates are risky. Your plugin layer must support safe upgrades and clean reversions so you can explore new features without fear. This subchapter introduces version pinning, controlled updates, and quick rollbacks with lazy.nvim. You will store lockfiles under version control, write a small update procedure, and rehearse failure recovery so everyone on the team knows how to proceed when something breaks.

Pin versions where stability matters. lazy.nvim supports lockfiles that record exact plugin commits. Commit this file so collaborators get the same versions and behavior. Update on your schedule, not by accident. When you choose to update, do it on a branch, run your profiling routine, open a project you actually work in, and fix any warnings immediately. If something regresses, roll back the lockfile or pin a specific plugin to a known good tag and retry. Keep change scope small so the source of a regression is obvious.

LUA

-- ~/.config/nvim/lua/plugins/specs.lua (version control)

return {

  { "nvim-lualine/lualine.nvim", version = "*", event = "VeryLazy",

    config = function() require("plugins.ui.lualine") end },

  { "nvim-telescope/telescope.nvim", version = false, keys = { "<leader>ff" },

    dependencies = { "nvim-lua/plenary.nvim" },

    config = function() require("plugins.search.telescope") end },

}

Verification: Using version=”*” tracks stable tags where available. version=false uses latest commits, which is acceptable for active development but should be monitored. After running the update procedure, re open Neovim and ensure both plugins function. :Lazy log should show pinned or floating versions as configured. Reruns produce the same state with an unchanged lockfile.

LUA

-- update.sh (documented in README; run from config root)

-- 1) Create branch; 2) Update; 3) Profile; 4) Test; 5) Commit or revert

-- git checkout -b chore/plugin-update-YYYYMMDD

-- nvim --headless "+Lazy! sync" +qa

-- nvim --startuptime out/startup-update.log +qa

-- git add lazy-lock.json out/startup-update.log && git commit -m "chore: plugin update with profile"

Verification: After running the commands, lazy-lock.json reflects new commits or tags. Startup log exists at out/startup-update.log. Opening Neovim interactively should show a clean :messages buffer. If behavior regresses, git revert the commit or restore the previous lockfile. Deterministic reruns yield the same versions until you update again.

For targeted rollbacks, pin a single plugin. If a statusline breaks, set its version to a known tag or commit, note the reference in the commit message, and open an issue upstream if needed. Keep a small notes file under out/ that records the symptom, the plugin name, and your chosen fix. These incident notes create a lightweight knowledge base for future you and reduce time to recovery when the same pattern appears again.

LUA

-- pin example with commit hash

{ "nvim-lualine/lualine.nvim", version = "v0.9.1", event = "VeryLazy",

  config = function() require("plugins.ui.lualine") end }

Verification: After setting version to a tag, run the update procedure. The lockfile should record the tag. Lualine must behave as before. If errors persist, revert the spec change and restore the prior lockfile. :Lazy should report no pending updates for this plugin until you change the version again.

Automate health checks alongside updates. Run :checkhealth in a headless session where possible, record output to a log, and scan for new warnings. Pair this with your startup profile so performance and correctness travel together. If either fails, revert and investigate. Your update rule becomes simple: never merge an update branch unless startup time is acceptable, health is clean, and daily workflows pass a quick smoke test across one or two active repositories.

Finish with a concise routine your team can memorize.

Create an update branch and sync plugins with Lazy.

Record startup time and run :checkhealth, saving both outputs under out/.

Test triggers in a real project. If everything passes, commit lockfile and logs.

If a failure occurs, pin the offending plugin or revert the branch.

With versions pinned, updates scripted, and rollbacks drilled, your plugin layer becomes a safe platform for growth. You can add features without risking stability, and you can ship improvements knowing a clean path back exists. That confidence sets the stage for adding language servers, completion, and debugging on a firm, testable base.

CHAPTER 5: MODULAR CONFIG DESIGN  

INTRODUCTION

A modular configuration keeps your Neovim fast, readable, and simple to change under pressure. This chapter gives you a clear folder layout, tiny loaders, and a naming scheme that communicates intent at a glance. You will split concerns into options, keymaps, and plugins so failures stay local and diffs remain small. We will add lightweight logging to see what loads when, profile consistently to catch regressions early, and use simple debugging patterns that isolate a bad file in seconds. Finally, you will prepare your configuration to travel well by separating private values, documenting bootstrap steps, and adopting safe conventions for environment variables and secrets.

Subchapter 5.1 creates the filesystem structure and shows how each file returns a value that other modules can require. You will wire init.lua to load options.lua, keymaps.lua, and plugins.lua in a predictable order and prove resolution with deterministic checks. Subchapter 5.2 adds minimal logging and profiling, then demonstrates three fast recovery drills: fail fast on syntax errors, bisect a broken load, and trace a stubborn require. Subchapter 5.3 focuses on sharing and portability. You will learn how to keep personal data out of version control, inject secrets safely when needed, and write a short README that reduces onboarding friction for teammates and future you.

The outcomes are practical. By the end of this chapter, your repository will be easy to read, fast to profile, and resilient to mistakes. You will be confident adding or removing files without fear of cascading breakage. That discipline sets the stage for durable growth as you introduce LSP, Treesitter, and debugging in later chapters, all backed by a structure that explains itself and a recovery path you can execute calmly.

5.1 FILES AND FOLDERS: OPTIONS.LUA, KEYMAPS.LUA, PLUGINS.LUA

A good structure turns your configuration into a small codebase. Each file does one job, returns a clear value, and avoids global side effects. This subchapter creates a predictable layout that helps every future change succeed: lua/core/options.lua manages editor defaults, lua/core/keymaps.lua holds ergonomic mappings, and lua/plugins/init.lua declares your plugin layer. Your init.lua wires these pieces in a deterministic order. The stakes are clarity and speed. When files are focused and names are honest, you debug faster and collaborate without friction.

Begin with folders under ~/.config/nvim on Unix or %USERPROFILE%\AppData\Local\nvim on Windows. Create lua/core and lua/plugins. The core folder keeps daily behavior close and legible. The plugins folder isolates external code so updates and rollbacks are easier to reason about. Keep filenames lowercase and descriptive, with one module per file. Consistent naming prevents subtle bugs on case sensitive filesystems and reduces cognitive load when scanning diffs.

LUA

-- ~/.config/nvim/init.lua

local function safe_require(name)

  local ok, mod = pcall(require, name)

  if not ok then

    vim.api.nvim_echo({{"Failed to load: " .. name, "ErrorMsg"}}, true, {})

    return nil

  end

  return mod

end

local opts = safe_require("core.options")

if opts then for k, v in pairs(opts) do vim.opt[k] = v end end

safe_require("core.keymaps")

local plugins = safe_require("plugins")

if plugins and plugins.setup then plugins.setup() end

Verification: On startup, options apply first, then keymaps, then plugins. If a module is missing or broken, a single error is echoed and the session still opens so you can fix from inside Neovim. Reruns produce identical behavior. Saved paths must match platform conventions.

Now create options.lua. Return a plain table of keys so application is explicit and diffs are compact. Avoid setting options at require time. Returning data keeps the module pure and easier to test from :lua.

LUA

-- ~/.config/nvim/lua/core/options.lua

local o = {

  number = true,

  relativenumber = true,

  signcolumn = "yes",

  termguicolors = true,

  ignorecase = true,

  smartcase = true,

  splitright = true,

  splitbelow = true,

  updatetime = 200,

  timeoutlen = 400,

}

return o

Verification: Restart Neovim. Expect relative numbers, a fixed sign column, and smartcase search. Run :verbose set number? to confirm the option source. If a key is misspelled, assignment in init.lua will raise a clear error, prompting a quick fix.

Create keymaps.lua with a small helper to reduce repetition. Use descriptions so discovery tools can surface your mappings. Keep motions intact and map frequent actions only.

LUA

-- ~/.config/nvim/lua/core/keymaps.lua

vim.g.mapleader = " "

local function nmap(lhs, rhs, desc)

  vim.keymap.set("n", lhs, rhs, { noremap = true, silent = true, desc = desc })

end

nmap("<leader>s", "<cmd>w<cr>", "Save file")

nmap("<leader>q", "<cmd>confirm q<cr>", "Quit window")

nmap("<C-h>", "<C-w>h", "Window left")

nmap("<C-j>", "<C-w>j", "Window down")

nmap("<C-k>", "<C-w>k", "Window up")

nmap("<C-l>", "<C-w>l", "Window right")

Verification: Press space then s to save, space then q to quit with confirmation, and Control plus h j k l to move among splits. Run :map  to see descriptions. :messages should be clean. Behavior is consistent across restarts.

Finally, create a minimal plugins entry point that returns a setup function. It will host lazy.nvim setup or any manager you choose. Keeping it behind plugins.setup() avoids side effects during require and makes testing simpler.

LUA

-- ~/.config/nvim/lua/plugins/init.lua

local M = {}

function M.setup()

  local ok, lazy = pcall(require, "lazy")

  if not ok then return end

  local specs = require("plugins.specs")

  lazy.setup(specs, { change_detection = { notify = false } })

end

return M

Verification: With lazy on runtimepath and plugins/specs present, :Lazy shows defined specs. If lazy is absent, setup does nothing and the session stays usable. This separation avoids hard crashes on new machines before bootstrap.

Keep specs in one file until the list grows, then split by concern into ui, git, search, and lsp. Require those tables inside specs.lua and concatenate. That pattern lets you add or remove an area without touching others, reducing merge conflicts and making reviews faster.

LUA

-- ~/.config/nvim/lua/plugins/specs.lua

local ui = require("plugins.ui")

local search = require("plugins.search")

local git = require("plugins.git")

local M = {}

for _, t in ipairs({ ui, search, git }) do

  for _, spec in ipairs(t) do table.insert(M, spec) end

end

return M

Verification: :Lazy lists grouped specs in a stable order. Commenting out one require removes that category without touching others. If a file is missing, the safe load in init.lua still leaves the session responsive.

A quick daily routine proves the structure.

Start Neovim from a cold shell, verify core options and keymaps work.

Open :Lazy and confirm specs load without warnings.

Commit any structural changes with a message that describes intent and expected behavior.

With folders and files settled, you can add observability and rapid recovery patterns so broken configs never block your day.

5.2 LOGGING, PROFILING, AND DEBUGGING BROKEN CONFIGS

Observability is your safety net. Lightweight logging shows what loads, profiling shows how long it takes, and a small set of debugging drills makes recovery fast when something goes wrong. This subchapter adds a minimal logger, a consistent startup profile routine, and three targeted techniques: fail fast on syntax errors, bisect with a loader switch, and trace a failing require. These habits keep incidents small and boring.

Start with a tiny logger that you can enable per session. Avoid persistent noisy output. Prefer vim.notify for visible events and a simple file append when you need a timeline. Log only key milestones: core loaded, plugins started, and completion of setup. Include timestamps so you can compare against startup logs later.

LUA

-- ~/.config/nvim/lua/core/log.lua

local M = {}

local path = vim.fn.stdpath("state") .. "/nvim.log"

function M.write(msg)

  local f = io.open(path, "a")

  if f then f:write(os.date("%H:%M:%S ") .. msg .. "\n"); f:close() end

end

function M.clear() os.remove(path) end

function M.path() return path end

return M

Verification: From :lua, require(“core.log”).clear(); require(“core.log”).write(“hello”). Confirm file exists at stdpath(“state”)/nvim.log and contains a timestamped line. Reruns append new lines. Removing the file causes it to be recreated on next write.

Instrument the loader to mark phases. Keep logs minimal and remove them when not debugging. The goal is fast signal, not permanent chatter.

LUA

-- ~/.config/nvim/lua/core/loader.lua (instrumented)

local log = require("core.log")

log.write("loader:start")

local opts = require("core.options")

for k, v in pairs(opts) do vim.opt[k] = v end

log.write("options:applied")

require("core.keymaps")

log.write("keymaps:ready")

pcall(function()

  local plugins = require("plugins"); if plugins.setup then plugins.setup() end

end)

log.write("plugins:setup:done")

Verification: Start Neovim, then open the log file. Expect four lines in order: loader:start, options:applied, keymaps:ready, plugins:setup:done. If a line is missing, the failure lies between the last present line and the next expected one. Remove instrumentation after fixing the issue.

Profile startup the same way every time. Use nvim –startuptime out/startup-structure.log and quit immediately. Compare totals across commits and keep the faster configuration when functionally equivalent. Watch for slow lines that point to heavy requires or big plugin setups. Use that evidence to move work to lazy events or reduce configuration complexity.

Run nvim –startuptime out/startup-structure-01.log.

Apply a structural change, then run nvim –startuptime out/startup-structure-02.log.

Compare totals and top slow lines. Keep the best result and commit the log with your change.

When an error stops startup, fail fast and localize. First, confirm syntax correctness by running luac -p on the suspected file when available. Second, switch your init.lua to a minimal path that loads only core.options, then add files back one by one until the error returns. This manual bisect is often faster than guessing. Third, if a require fails silently, wrap it in pcall, echo the module name, and print debug info such as vim.inspect(package.path) to confirm your runtime path includes the expected directory.

LUA

-- ~/.config/nvim/init.min.lua  temporary recovery file

vim.opt.number = true

local ok, err = pcall(require, "core.keymaps")

if not ok then vim.api.nvim_echo({{err, "ErrorMsg"}}, true, {}) end

Verification: Start Neovim with nvim -u ~/.config/nvim/init.min.lua. If the session opens cleanly, the failure lies in modules your full init.lua loads beyond keymaps. Add modules incrementally until the error reappears. Revert changes causing the failure and restore your normal init.lua.

Trace a stubborn require by printing from the target module’s top line. If the print does not appear, the module is not being found. Check the file path, directory case, and require name. On case sensitive systems, mismatches between Foo.lua and require(“foo”) cause cryptic failures. Keep names lowercase to avoid these issues.

LUA

-- ~/.config/nvim/lua/plugins/ui/lualine.lua (trace)

print("TRACE: entering plugins/ui/lualine.lua")

return function()

  require("lualine").setup({ options = { theme = "auto" } })

end

Verification: Start Neovim and watch :messages for TRACE. If it does not appear, the spec’s config function is not running. Check lazy spec triggers or require path. Remove traces after resolution to keep messages clean.

As a final guard, prepare a quick disable switch for the plugin layer. If a breaking update lands, you can start with core only, fix the issue, and re enable plugins after. This is a single line environment check that skips setup when NVIM_MINIMAL is set.

LUA

-- ~/.config/nvim/lua/plugins/init.lua (guard)

if vim.env.NVIM_MINIMAL == "1" then return { setup = function() end } end

Verification: Run NVIM_MINIMAL=1 nvim and confirm that :Lazy is unavailable and only core options and keymaps have loaded. Remove the variable and verify normal behavior returns. This guard reduces downtime during incidents.

Logging, profiling, and targeted debugging drills turn configuration failures into routine work. With these tools, you can diagnose confidently, revert quickly, and keep moving without drama. Next you will ensure your structure travels well, share it responsibly, and handle private values safely.

5.3 SHARING, PORTABILITY, AND MANAGING SECRETS SAFELY

A great configuration is one that you can share, restore, and extend without leaking private information. This subchapter prepares your repository to travel across machines and teams by documenting bootstrap steps, separating secrets from code, and adopting conservative defaults for environment detection. You will write a small README, create ignore rules for volatile files, and introduce a secure pattern for optional tokens when plugins need access to external services. The result is a setup that new contributors can clone and use within minutes, with private data protected and operational details clear.

Start by clarifying scope in your README. Describe prerequisites such as Neovim version, a supported terminal, and git. Document a three step bootstrap and a short verification routine that anyone can run. Keep the document short and factual so it is read and maintained. Include a Recovery section that explains how to start with NVIM_MINIMAL set and how to revert a bad commit. When expectations are written down, onboarding becomes calm and predictable.

LUA

-- ~/.config/nvim/.gitignore

out/

swap/

undo/

session/

*.log

*.tmp

lazy-lock.json merge-backup

Verification: After a day of editing, git status should show only intentional Lua changes. If transient files appear, expand ignore patterns and retry. Keep lockfiles tracked if you use them to pin plugin versions; adjust the ignore accordingly to match your update strategy.

Portability improves when you avoid system specific assumptions. Do not hardcode paths to shells or tools. Use vim.fn.exepath to detect binaries and degrade gracefully when missing. For platform differences, check vim.loop.os_uname().sysname and set conservative defaults. Keep GUI specific settings optional so headless servers remain happy. If a plugin is not present, skip configuration quietly. This approach prevents remote sessions from failing due to desktop only assumptions.

LUA

-- ~/.config/nvim/lua/core/env.lua

local M = {}

M.is_macos = vim.loop.os_uname().sysname == "Darwin"

M.is_windows = vim.loop.os_uname().sysname:match("Windows") ~= nil

function M.find(bin) return vim.fn.exepath(bin) ~= "" end

return M

Verification: :lua print(require(“core.env”).is_macos) prints true or false correctly. :lua print(require(“core.env”).find(“git”)) returns true when git is on PATH. These checks are deterministic across sessions.

Secrets and tokens demand caution. Do not store API keys inside your repository. Prefer environment variables or an external encrypted file that you read at runtime. If a plugin supports a setup callback, pass the value only when present and otherwise disable that feature politely. Provide a .env.example that documents variable names without values and explain how to load them on each platform. This pattern protects private data while keeping setup smooth.

LUA

-- ~/.config/nvim/lua/core/secrets.lua

local M = {}

function M.get(name)

  local v = vim.fn.getenv(name)

  if v == vim.NIL or v == "" then return nil end

  return v

end

return M

Verification: :lua print(require(“core.secrets”).get(“MY_TOKEN”) or “nil”) prints the token when set or nil otherwise. Modules reading secrets must handle nil gracefully. Behavior remains stable across restarts.

When a plugin needs a token, gate the feature on presence instead of failing loudly. This keeps the configuration usable for teammates who do not share the same services.

LUA

-- ~/.config/nvim/lua/plugins/ai.lua

return function()

  local secrets = require("core.secrets")

  local key = secrets.get("OPENAI_API_KEY")

  if not key then

    vim.notify("AI plugin disabled: OPENAI_API_KEY missing", vim.log.levels.WARN)

    return

  end

  -- hypothetical setup

  require("ai_helper").setup({ api_key = key })

end

Verification: Without OPENAI_API_KEY set, startup continues with a single warning and no errors. With the key present, the plugin initializes. Reruns behave identically. No secrets are written to disk.

Sharing across machines benefits from a bootstrap script that installs dependencies and creates required folders. Keep it minimal, readable, and safe to run multiple times. Prefer idempotent checks and informative output. Store it in the repository root and reference it from the README.

Clone the repository to ~/.config/nvim.

Run ./bootstrap.sh to install dependencies and verify Neovim version.

Start Neovim, confirm the startup message, then record a baseline profile.

Finally, consider a lightweight release process. Tag known good states in git when configuration changes pass profiling and smoke tests. Add a short CHANGELOG entry that explains what changed and why. Teammates can roll forward or back by tag, reducing guesswork during incidents. With sharing, portability, and secrets under control, your modular design is ready to host more advanced tools without sacrificing safety or clarity.

CHAPTER 6: INTERFACE, NAVIGATION, AND UI  

INTRODUCTION

A clear interface turns speed into calm focus. This chapter builds a navigation layer that lets you reach any file, symbol, or change in seconds, then presents the right context without clutter. We begin with Telescope because good search is the foundation of a modern editor. You will configure find files, live grep, and a handful of pickers that deliver everyday wins, all loaded lazily and mapped for muscle memory. Next we design a statusline and theme that communicate state at a glance. lualine provides structure and Catppuccin supplies readable contrast, while diagnostics and Git cues surface only what matters. Finally we tune file trees, buffer management, sessions, and ergonomic defaults so your workspace opens fast, stays predictable, and recovers cleanly.

Each subchapter keeps a UI first flow with small, copy safe Lua. Every change includes a verification routine with explicit commands, expected behavior, and stable paths so teammates can reproduce results on their machines. We continue using lazy loading and consistent keymap conventions from earlier chapters. Where plugins overlap, we choose the simplest option that meets the need and prove the decision with a quick test. If a feature costs startup time, we move it behind an event or key. By the end, you will navigate confidently, read context without scanning, and resume work exactly where you left off. That steady base prepares you to add language intelligence in later chapters without noise. Begin with Telescope and make search your primary motion.

6.1 TELESCOPE MASTERY: FIND FILES, LIVE GREP, PICKERS THAT MATTER

Search is your steering wheel. When you can jump to files, lines, symbols, and history in a breath, everything else feels lighter. Telescope gives you a fast, extensible picker framework backed by ripgrep and plenary. The goal here is practical mastery, not a museum of options. We will wire a minimal setup, choose a few high value pickers, assign ergonomic keymaps, and lazy load on first use. You will also add small defaults that improve results and unify behavior, then verify with a repeatable routine so performance remains honest. Keep changes visible and reversible. If a choice slows you down or adds noise, you will adjust or remove it quickly.

Start with a clean specification. Defer Telescope until a keymap is pressed. Declare plenary as a dependency and configure only what helps discovery. Avoid heavy themes or nested extensions until the core flow feels instant. Map find files and live grep under your leader so they are one chord away in either hand. Add recent files and buffers for fast context switches. Keep descriptions so which key or similar helpers can surface your new motions. Your aim is fewer thoughts between intention and action.

LUA

-- ~/.config/nvim/lua/plugins/search/telescope.lua

return function()

  local t = require("telescope")

  t.setup({

    defaults = {

      prompt_prefix = "  ",

      selection_caret = "➤ ",

      path_display = { "smart" },

      sorting_strategy = "ascending",

      layout_config = { prompt_position = "top", height = 0.90, width = 0.95 },

      mappings = {

        i = {

          ["<C-u>"] = false,

          ["<C-d>"] = false,

          ["<C-j>"] = "move_selection_next",

          ["<C-k>"] = "move_selection_previous",

        },

      },

      vimgrep_arguments = {

        "rg", "--hidden", "--glob", "!.git", "--glob", "!node_modules", "--with-filename",

        "--line-number", "--column", "--smart-case",

      },

    },

    pickers = {

      find_files = { hidden = true, no_ignore = false },

      live_grep = { additional_args = function() return { "--hidden", "--glob", "!.git" } end },

      buffers = { sort_mru = true, ignore_current_buffer = true },

      oldfiles = { only_cwd = true },

    },

  })

  local map = vim.keymap.set

  map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", { desc = "Find files" })

  map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>",  { desc = "Live grep" })

  map("n", "<leader>fb", "<cmd>Telescope buffers<cr>",    { desc = "Buffers" })

  map("n", "<leader>fr", "<cmd>Telescope oldfiles<cr>",   { desc = "Recent files (cwd)" })

end

Verification: Press space then ff to list files, space then fg to grep. Hidden files appear but .git and node_modules stay excluded. In insert mode, Control j and Control k move selection. The prompt is at top, results below, with smart path shortening. Start times remain instant after the first load. Errors in :messages should be absent. File saved at lua/plugins/search/telescope.lua. Reruns behave identically.

Add fzf like sorting if you prefer tighter matching. Telescope’s fzf native extension provides a compiled sorter that speeds large projects. Build steps vary by platform, so keep the addition optional and documented. Load the extension only after the core picker is healthy so failures do not block your day. This is a good example of adding power without making your setup fragile.

LUA

-- ~/.config/nvim/lua/plugins/search/telescope_fzf.lua

return {

  "nvim-telescope/telescope-fzf-native.nvim",

  build = "make",

  cond = function() return vim.fn.has("unix") == 1 end,

  config = function() require("telescope").load_extension("fzf") end,

}

Verification: On Unix, :Lazy builds the native module. After launch, running Telescope pickers should feel snappier in giant repositories. :Telescope help tags remains functional. If build fails, the extension is skipped due to cond, leaving core pickers intact. Document the build requirement in your README.

Integrate git awareness through two pickers that pay daily dividends. git_files is faster than find_files in repositories, and git_status lets you stage or open changed files quickly. Consider mapping them under a second chord that reflects your habits. Do not duplicate every feature of a dedicated Git tool here. Keep it lean and focused on navigation.

LUA

-- ~/.config/nvim/lua/plugins/search/telescope_git.lua

return function()

  local map = vim.keymap.set

  map("n", "<leader>gf", "<cmd>Telescope git_files<cr>",   { desc = "Git files" })

  map("n", "<leader>gs", "<cmd>Telescope git_status<cr>",  { desc = "Git status" })

end

Verification: Inside a repository, space then gf lists tracked files fast, while space then gs shows modifications. Selecting an entry opens it in the current window. Outside a repository, Telescope reports no git directory. No errors persist in :messages. Keymaps appear under :map .

Improve focus by previewing only when helpful. Previews are great for prose and config, but heavy for large binaries. Use dynamic_previewer to control cost or disable previews for specific pickers to keep motion snappy. The theme remains minimal so you can read quickly and recover without thinking about pixels. Performance always wins over visual flourish.

LUA

-- ~/.config/nvim/lua/plugins/search/telescope_tweaks.lua

return function()

  local t = require("telescope")

  t.setup({

    pickers = {

      find_files = { previewer = false },

      buffers = { previewer = false },

    }

  })

end

Verification: Running find_files or buffers displays results without the preview pane, reducing noise and CPU. Other pickers keep previews intact. No warnings appear. Behavior is deterministic on restart.

Establish a profiling and QA routine for search. Keep the same repository, shell, and commands. Your checks remain short, boring, and decisive. If latency creeps in, remove the last addition and re measure. The point is not zero milliseconds but a predictable, fast feel you can trust.

Profile startup with nvim –startuptime out/startup-ui-telescope.log and quit.

In a medium repository, run ff, fg, and fb three times each.

Confirm first invocation loads Telescope, subsequent calls are instant, and :messages stays clean.

A few small pickers round out daily work. help tags solves memory lapses without leaving the editor. grep string at cursor accelerates local investigation. Diagnostics shows issues across the project, complementing your statusline. Add them when the core feels solid; otherwise keep the surface small. Less is more when you care about speed and clarity.

LUA

-- ~/.config/nvim/lua/plugins/search/telescope_more.lua

return function()

  local map = vim.keymap.set

  map("n", "<leader>fh", "<cmd>Telescope help_tags<cr>",     { desc = "Help tags" })

  map("n", "<leader>fw", "<cmd>Telescope grep_string<cr>",   { desc = "Grep word under cursor" })

  map("n", "<leader>fd", "<cmd>Telescope diagnostics<cr>",   { desc = "Project diagnostics" })

end

Verification: All three pickers open quickly. diagnostics lists LSP issues when servers are active later in the book. No start time penalty is added because these maps only trigger on demand. Keymaps are discoverable and documented.

Close by committing your search layer and a short note summarizing keymaps and expectations. If teammates adopt different habits, keep your maps local and document alternatives in the README. With Telescope steady and quick, you can depend on search as your primary motion. Next you will refine context with a statusline and theme that quietly tell you what matters.

6.2 STATUSLINES AND THEMES (LUALINE, CATPPUCCIN) WITH CONTEXT CUES

A good interface reduces questions. The statusline should tell you where you are, what mode you are in, whether the file is modified, and if diagnostics or Git changes need attention. The theme should deliver readable contrast in all lighting without fighting your terminal. This subchapter sets up lualine for structure and Catppuccin for color, then adds context cues for LSP and diagnostics that remain calm until something requires action. We keep configuration small, defer non essentials, and verify behavior with simple checks you can repeat.

Begin by installing Catppuccin and choosing a single palette. Many teams pick Mocha for dark terminals because its contrast remains readable with terminal transparency. Apply the colorscheme early in UI time so plugins adopt its highlights. Avoid over customizing on day one. Readability, not novelty, is your goal. Add per plugin tweaks only when needed to fix specific issues like low contrast comments or weak selection colors. Stability makes muscle memory honest.

LUA

-- ~/.config/nvim/lua/plugins/ui/catppuccin.lua

return function()

  require("catppuccin").setup({

    flavour = "mocha",

    integrations = { treesitter = true, gitsigns = true, telescope = true, lsp_trouble = true },

    transparent_background = false,

  })

  vim.cmd.colorscheme("catppuccin")

end

Verification: UI switches to the Catppuccin Mocha palette. Treesitter, Telescope, and Gitsigns adopt matching highlights. No errors appear in :messages. If terminal colors conflict, disable transparency in your terminal or toggle transparent_background. Behavior is deterministic across restarts.

Now configure lualine with sections that carry meaning. Mode, file path, Git branch, diagnostics, and position should be visible without clutter. Use globalstatus so one bar serves all windows, keeping the interface clean. Keep separators simple and defer icons to your font setup. Add diagnostics with an obvious but calm symbol set so warnings do not shout. You want signal that pulls your eye only when necessary.

LUA

-- ~/.config/nvim/lua/plugins/ui/lualine.lua

return function()

  local ok, lualine = pcall(require, "lualine")

  if not ok then return end

  lualine.setup({

    options = { theme = "catppuccin", globalstatus = true, component_separators = "", section_separators = "" },

    sections = {

      lualine_a = { "mode" },

      lualine_b = { { "branch", icon = "" } },

      lualine_c = { { "filename", path = 1 } },

      lualine_x = {

        {

          "diagnostics",

          sources = { "nvim_diagnostic" },

          symbols = { error = "E:", warn = "W:", info = "I:", hint = "H:" },

          colored = true,

          update_in_insert = false,

        },

      },

      lualine_y = { "progress" },

      lualine_z = { "location" },

    },

  })

end

Verification: After VeryLazy, a single statusline spans the screen. Mode, branch, and file path appear. Diagnostics shows E, W, I, H counts from LSP once servers are configured later. Progress and location update smoothly. No jitter occurs when opening splits due to globalstatus. If colors feel off, ensure Catppuccin loaded first or set theme to “auto” temporarily.

Add small context cues for recording and paste modes so you do not leave them on accidentally. These cues belong in the statusline or command area because they change behavior. Keeping them quiet until active prevents noise. A tiny autocommand can notify when paste is toggled, and lualine can display a component only in specific modes. Let the interface help you avoid common footguns.

LUA

-- ~/.config/nvim/lua/core/context.lua

local grp = vim.api.nvim_create_augroup("ContextCues", { clear = true })

vim.api.nvim_create_autocmd("OptionSet", {

  group = grp, pattern = "paste",

  callback = function()

    if vim.o.paste then vim.notify("Paste mode enabled", vim.log.levels.WARN) end

  end,

})

Verification: :set paste triggers a single warning. Turning it off shows no message. The cue prevents accidental paste mode lingering. No startup impact. File path lua/core/context.lua. Reruns behave identically.

Integrate Telescope highlights with your theme by checking contrast and adjusting only where needed. For example, Telescope’s prompt and selection areas should remain readable under dim terminals. Catppuccin’s integration usually covers this well. If a picker looks washed out, set a single highlight link rather than redefining many groups. Fewer overrides mean fewer surprises on updates.

LUA

-- ~/.config/nvim/lua/plugins/ui/highlights.lua

return function()

  vim.api.nvim_set_hl(0, "TelescopeSelection", { link = "Visual" })

  vim.api.nvim_set_hl(0, "TelescopePromptPrefix", { link = "Identifier" })

end

Verification: Opening a picker shows a selection styled like Visual. The prompt prefix uses Identifier coloring. No warnings appear. Overrides remain small and intentional.

Establish a quick verification routine for UI coherence. Use the same repository and shell, run the same commands, and look for drift. You do not need pixel perfection, just predictable, readable output that supports flow. If a plugin update changes highlights, decide whether to accept the new style or add a tiny override. Document the decision in your notes for future reference.

Start Neovim and verify the statusline shows mode, branch, and path.

Open a project, run ff and fg, and check picker styling.

Introduce a deliberate warning in a test file and confirm diagnostics appear as counts.

Keep restraint as your design principle. Avoid constantly animated components, noisy icons, or rainbow separators unless they solve a real problem for your eyes. A quiet interface leaves more attention for the code. With statuslines and themes tuned, finish the chapter by improving workspace ergonomics: trees, buffers, and sessions that reduce the steps between resumes.

6.3 FILE TREES, BUFFERS, SESSIONS, AND WORKSPACE  

A calm workspace reduces decision fatigue. You need a fast glanceable view of the project, quick buffer switching, and reliable sessions that reopen exactly where you left off. This subchapter balances a minimal tree with Telescope for deep navigation, then adds buffer hygiene and session habits that keep context fresh across days. We will use a lightweight tree plugin or the built in netrw alternative, wire ergonomic keymaps, and ensure everything lazy loads so startup remains quick. The goal is fewer steps from intention to action with predictable recovery.

Start with the question: do you need a tree all the time or only sometimes for orientation and file operations? Many developers rely on Telescope for most jumps and open a tree briefly to move or rename files. We will configure nvim tree as an on demand tool that opens to the current file, respects git ignores, and closes when not in use. If you prefer netrw, the same principles apply: map a key to toggle, focus the current file, and keep the pane narrow.

LUA

-- ~/.config/nvim/lua/plugins/ui/tree.lua

return function()

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

    disable_netrw = true,

    hijack_cursor = true,

    sync_root_with_cwd = true,

    update_focused_file = { enable = true, update_root = false },

    view = { width =  Thirty, side = "left", preserve_window_proportions = true },

    git = { enable = true, ignore = true, show_on_dirs = true },

    renderer = { highlight_git = true, group_empty = true, icons = { show = { file = false, folder = true, git = true } } },

    actions = { open_file = { quit_on_open = true } },

  })

  local map = vim.keymap.set

  map("n", "<leader>e", "<cmd>NvimTreeToggle<cr>", { desc = "Explorer" })

  map("n", "<leader>E", "<cmd>NvimTreeFindFile<cr>", { desc = "Reveal current file" })

end

Verification: Press space then e to toggle the tree and space then E to reveal the current file. Opening a file closes the tree so focus returns to code. Hidden files follow git rules by default. If your terminal width is small, adjust view.width to a reasonable number like 28. No startup penalty occurs because the plugin loads only on command.

Next, manage buffers deliberately. A small helper shows active buffers in order of recent use and closes dead ones quickly. Telescope’s buffers picker already covers much of this, but a couple of mappings keep hands on home row. Remap q to confirm quit rather than closing buffers accidentally, and add a mapping to delete the current buffer while preserving the window. Clarity beats cleverness here.

LUA

-- ~/.config/nvim/lua/core/buffers.lua

local map = vim.keymap.set

map("n", "<leader>bd", "<cmd>bdelete<cr>", { desc = "Delete buffer" })

map("n", "<leader>bn", "<cmd>bnext<cr>",   { desc = "Next buffer" })

map("n", "<leader>bp", "<cmd>bprevious<cr>", { desc = "Previous buffer" })

Verification: space then bd deletes the current buffer and keeps the window, while bn and bp cycle predictably. :ls lists remaining buffers. Telescope buffers remains available for fuzzy switching. No warnings appear. Mappings are discoverable and documented.

Sessions should be boring and reliable. Use the built in :mksession or a lightweight plugin such as possession or persistence that remembers buffers, windows, and tabs. We will use persistence.nvim for a simple, resilient default. Bind resume and select commands to leader keys, and exclude transient directories like large temporary repos to keep load times healthy. Sessions are only helpful when they open quickly and restore the mental model without surprises.

LUA

-- ~/.config/nvim/lua/plugins/ui/session.lua

return function()

  require("persistence").setup({

    dir = vim.fn.stdpath("state") .. "/sessions/",

    options = { "buffers", "curdir", "tabpages", "winsize" },

    pre_save = function() vim.cmd("silent! NvimTreeClose") end,

  })

  local map = vim.keymap.set

  map("n", "<leader>qs", function() require("persistence").load() end, { desc = "Restore session" })

  map("n", "<leader>qS", function() require("persistence").select() end, { desc = "Select session" })

  map("n", "<leader>qd", function() require("persistence").stop() end,  { desc = "Stop saving" })

end

Verification: After editing, quit Neovim, then reopen and press space then qs to restore the last session. Windows and tabs return as expected. The tree closes before save to avoid stale explorer panes. Session files appear under stdpath(“state”)/sessions. Behavior is deterministic across restarts.

Ergonomics finish the picture. Keep splits predictable with splitright and splitbelow. Add Control h j k l for window navigation if not already set. Consider buffers over tabs for most multitasking and reserve tabs for distinct workspaces like feature and review. Keep which key available to surface maps, but avoid hiding essential motions behind complex chords. A few consistent patterns beat many clever ones.

Establish a workspace QA routine that respects your time and attention. It should take under a minute, run daily or after structural changes, and catch common regressions. These checks keep your environment honest and make onboarding smoother for teammates who follow the same script. When failures appear, revert quickly and document the cause in a short note.

Start Neovim, press e then open a file and confirm the tree closes.

Switch buffers with fb, delete one with bd, and verify the window remains.

Save and quit, restart, then restore with qs and confirm layout and buffers return.

Close by committing changes with a concise message describing keymaps and expected outcomes. Your interface is now purposeful: search first, a quiet statusline with real signal, and a workspace that opens and resumes without drama. This steady base makes language intelligence and debugging feel natural when you add them later.

CHAPTER 7: PRODUCTIVITY ESSENTIALS

INTRODUCTION

Speed that you can recall is better than speed you forget. This chapter turns your Neovim into a place where shortcuts are discoverable, edits are safe, and repetitive text collapses into smart snippets. We start with which-key to make leader maps self documenting. Instead of memorizing every chord, you will read prompts that teach while you work. You will group actions by intent, keep names consistent, and defer plugin loads behind those keys so startup stays quick. Next we sharpen the core skills that never go out of style: precise search and replace, motions that cross code quickly, macros that automate surgical edits, and registers that move text without surprises. These habits outlast any plugin trend and travel with you across languages and machines. Finally we wire LuaSnip for real work. You will learn a small, calm set of triggers, expand context aware templates, and keep everything versioned so teams share a common vocabulary for common shapes like tests, logs, and routes.

The flow remains UI first with small, copy safe Lua. Every change includes verification you can repeat on a clean machine. We avoid exotic keymaps and focus on actions you use hourly. When choices conflict, we favor predictable results and write them down. By the end of this chapter you will have a discoverable leader space, a reliable editing toolkit for hard refactors, and a snippet engine that fills the gaps with minimal ceremony. Turn the page and make your shortcuts visible so they become habits you can trust under pressure.

7.1 WHICH-KEY, LEADER MAPS, AND DISCOVERABLE SHORTCUTS

A discoverable interface teaches you while you work. which-key shows possible keybindings as you press your leader, turning guesswork into a guided menu. This subchapter builds a simple, memorable leader space, installs which-key with lazy loading, and organizes your maps by verbs you use every hour. The stakes are practical. If you cannot find a command in two seconds, you will stop using it. Consistency beats cleverness. We will keep groups small, names honest, and descriptions clear so teammates can onboard without a tour.

Begin with intent based groups. Group f for find, g for git, b for buffers, q for sessions, and s for file actions like save. Keep each group under a dozen entries so reading the menu is instant. Bind only what you use weekly. The rest stays in commands and Telescope pickers. which-key thrives when it confirms biases you already have; it fails when it becomes a dumping ground. We will set a short timeout so the menu appears quickly without stealing focus, and we will ensure mappings carry descriptions so the menu reads like a list of sentences.

LUA

-- ~/.config/nvim/lua/plugins/whichkey.lua

return function()

  local wk = require("which-key")

  wk.setup({

    plugins = { spelling = true },

    window = { border = "rounded" },

    triggers = "auto",

    filter = function(mapping) return mapping.desc ~= nil end,

  })

  wk.register({

    ["<leader>"] = {

      f = { name = "+find" },

      g = { name = "+git" },

      b = { name = "+buffers" },

      q = { name = "+session" },

      s = { name = "+file" },

    },

  })

  local map = vim.keymap.set

  map("n", "<leader>ss", "<cmd>w<cr>", { desc = "Save file" })

  map("n", "<leader>sq", "<cmd>confirm q<cr>", { desc = "Quit window" })

  map("n", "<leader>ff", "<cmd>Telescope find_files<cr>", { desc = "Find files" })

  map("n", "<leader>fg", "<cmd>Telescope live_grep<cr>",  { desc = "Live grep" })

  map("n", "<leader>fb", "<cmd>Telescope buffers<cr>",    { desc = "Buffers" })

  map("n", "<leader>fr", "<cmd>Telescope oldfiles<cr>",   { desc = "Recent files" })

  map("n", "<leader>gb", "<cmd>Git blame<cr>", { desc = "Git blame (fugitive)" })

  map("n", "<leader>gs", "<cmd>Telescope git_status<cr>", { desc = "Git status" })

  map("n", "<leader>bd", "<cmd>bdelete<cr>", { desc = "Delete buffer" })

  map("n", "<leader>bn", "<cmd>bnext<cr>",   { desc = "Next buffer" })

  map("n", "<leader>bp", "<cmd>bprevious<cr>", { desc = "Previous buffer" })

})

end

Verification: Press leader and pause briefly. A menu shows groups +find, +git, +buffers, +session, +file. Press leader ff to open file search, fg for grep, ss to save. Confirm descriptions read as sentences. :messages remains clean. File lives at lua/plugins/whichkey.lua. Behavior is deterministic across restarts.

Load which-key only when useful so startup stays lean. Use event VeryLazy or key triggers so the plugin initializes after the UI settles and mappings exist. which-key needs your maps to display them, so ensure keymaps register before the plugin config runs, or register maps inside the which-key setup block. In our structure, core maps load early, and which-key attaches later to read them. If a description is missing, the filter hides that binding from the menu, encouraging you to describe new maps properly.

LUA

-- ~/.config/nvim/lua/plugins/ui.lua  (spec fragment for lazy.nvim)

return {

  {

    "folke/which-key.nvim",

    event = "VeryLazy",

    config = function() require("plugins.whichkey")() end,

  },

}

Verification: :Lazy shows which-key under event VeryLazy. On startup, no which-key cost appears in the critical path. After the first leader press, the popup shows groups and entries with descriptions. Remove a desc and see the item disappear, confirming the filter works. Reruns are identical.

Design your leader space as a language. Verbs on the left, objects on the right. Keep mnemonic pairs, like ff for files and fg for grep. Avoid long chains that require memorization. If a map is hard to remember, rename it or move it. Use which-key’s group labels to teach structure. The goal is to reduce cognitive load so your eyes stay on code. When teams share a leader grammar, pairing and code reviews move faster because everyone expects the same chords.

Add a few quality of life bindings that stick the first week and keep paying off. A diagnostic viewer, a quickfix opener, and a register peek save seconds many times per hour. Do not pile on flashy features. Favor boring, always useful actions. which-key makes them visible so they become habits.

LUA

-- ~/.config/nvim/lua/core/leader_extras.lua

local map = vim.keymap.set

map("n", "<leader>ld", vim.diagnostic.open_float, { desc = "Line diagnostics" })

map("n", "<leader>lq", "<cmd>copen<cr>", { desc = "Open quickfix" })

map("n", "<leader>rr", "<cmd>registers<cr>", { desc = "Show registers" })

Verification: Press leader ld over a line with an issue to see a diagnostic float. leader lq opens the quickfix window. leader rr lists registers. All appear in which-key with clear labels. :messages is clean. Files persist across restarts.

Keep a short QA loop for discoverability.

1.Press leader and scan the menu. If any item surprises you, rename or move it.

2.Time first press to popup. If it feels slow, reduce which-key scope or window size.

3.Ask a teammate to find “buffers” in under five seconds. If they struggle, simplify the group.

Finish by documenting your leader grammar in the README. List groups, a few anchors, and your naming rules. which-key will do the rest. With visible shortcuts in place, we will deepen core editing skills that amplify every action.

7.2 SEARCH/REPLACE, MOTIONS, MACROS, AND REGISTERS THAT STICK

Power accrues from precise fundamentals. Search and replace, motions, macros, and registers are the tools that change messy work into fast work. This subchapter teaches a calm set of patterns you can rely on during refactors and reviews. We stay plugin light. Telescope handles project wide jumps, but inside a buffer your fingers win. You will practice a few repeatable drills, learn how to avoid surprises like accidental global changes, and keep a small checklist for dangerous edits so you can move quickly with confidence.

Begin with motion literacy. f and t jump on a line, ; repeats forward, , repeats backward. w and b move between words, and ge lands at the end of the previous word. Use ciw to change inside word and cit to change inside HTML or code tags with Treesitter later. The habit that sticks is to prefer changes scoped by text objects. For example, diw removes a word without touching adjacent punctuation, while da” removes a quoted string. This mindset reduces keystrokes and mistakes because you operate on shapes, not coordinates.

Search and replace deserves discipline. Start with /term to search forward and n and N to repeat. Use very literal search when nervous. When replacing, begin visually to confirm scope. Select with v, extend to your region, then use :s/old/new/g to change lines in the selection only. For the entire file, start with :%s/old/new/gc and answer each prompt. The c flag buys safety by showing matches. When patterns are complex, build them in a small scratch buffer and test on a miniature copy of the text before you touch the real file. Caution is faster than undo when stakes are high.

A few drills lock in safety. To refactor a function name, use * on the word under cursor to search, then cgn to change the next match and repeat with dot. This pattern combines search and change with clear rhythm. For multiline changes, record a macro. Start with q followed by a register letter, perform the edit once, press q to stop, then replay with @ and repeat with @@. Keep macros short and purposeful. If you cannot explain a macro in one sentence, split it.

Registers carry text between edits. The unnamed register “” holds your last yank or delete. The system clipboard often lives at + or *. To paste without overwriting the default register, use the black hole register “_ when deleting, like “_d. To paste over a selection without clobbering your yank, use “_dP so your yanked text survives. This single habit preserves momentum during reviews where you paste the same snippet many times.

Odd corners deserve clarity. Dot repeats the last change but not navigation, so pair it with motions like t, f, or searches to land in the right spot. When a change must apply to many files, blend tools. Use Telescope live grep to find contexts, quickfix to collect results, then cdo to run a command over the list with confirmation. Practice on a copy first. Precision today prevents churn tomorrow.

Keep a safety checklist for risky sessions. Before a wide replace, save, split the window with a read only view, and run with confirmation. After a macro, scan the changes quickly by searching for the edited pattern. During reviews, run a final grep for the old symbol to catch misses. Write these checks down and commit them in your README so you repeat them under pressure.

A small daily routine cements these skills.

1.In a code buffer, put the cursor on a symbol. Press * then cgn to rename once, dot twice to repeat, and u to undo to verify scope.

2.Select three lines, run :s/foo/bar/g and confirm only the selection changed.

3.Record q a, perform a tiny edit with motions, stop with q, then replay across three similar lines with @@.

You will notice that macros and registers feel natural when your motions are crisp. If you find macros hard to read later, prefer Lua snippets for structured inserts. That is our next step. For now, practice until search, replace, motions, and registers feel like a single move. When your hands operate on shapes and scope by default, everything else in Neovim becomes calmer and faster.

7.3 SNIPPETS AND SNIPPET ENGINES (LUASNIP) FOR REAL WORK

Snippets compress common shapes into a keystroke. Done right, they are quiet helpers that reduce repetition without hijacking typing. LuaSnip is a fast, flexible engine with Lua based definitions and Treesitter aware nodes. This subchapter installs LuaSnip, wires it to completion later via nvim cmp, and creates a small, durable set of snippets you will actually use. We focus on templates for tests, logs, and small language idioms, plus filetype detection and path safe storage. The aim is to share a few portable patterns across teams without creating a maze of magic.

Install and load LuaSnip lazily. Keep definitions in lua/snippets and split by filetype. Use simple triggers that do not collide with natural words, such as tfn for test function or logd for a structured debug log. Prefer nodes that accept tab stops and choices where helpful, but avoid deep nesting that obscures intent. Snippets should read like short, obvious templates.

LUA

-- ~/.config/nvim/lua/plugins/snippets.lua

return function()

  local ls = require("luasnip")

  ls.config.set_config({

    history = true,

    updateevents = "TextChanged,TextChangedI",

    enable_autosnippets = false,

  })

  require("luasnip.loaders.from_lua").lazy_load({ paths = vim.fn.stdpath("config") .. "/lua/snippets" })

  local map = vim.keymap.set

  map({ "i", "s" }, "<C-j>", function() if ls.expand_or_jumpable() then ls.expand_or_jump() end end,

    { desc = "LuaSnip expand/jump" })

  map({ "i", "s" }, "<C-k>", function() if ls.jumpable(-1) then ls.jump(-1) end end,

    { desc = "LuaSnip jump back" })

  map("n", "<leader>se", "<cmd>LuaSnipEdit<cr>", { desc = "Edit snippets" })

end

Verification: After startup, snippets load on demand from lua/snippets. In insert mode, Control j expands or jumps, Control k jumps back. Running :LuaSnipEdit opens the current filetype’s snippet file. :messages remains clean. Behavior is deterministic across restarts.

Define a couple of language agnostic snippets. Start with a timestamped log and a simple test template. Use insert nodes for cursor stops and choice nodes for quick variations. Keep placeholders descriptive. Snippets should teach by example the first time they expand.

LUA

-- ~/.config/nvim/lua/snippets/all.lua

local ls = require("luasnip")

local s, t, i, c, f = ls.snippet, ls.text_node, ls.insert_node, ls.choice_node, ls.function_node

return {

  s("logd", {

    t("print('"), f(function() return os.date("%Y-%m-%d %H:%M:%S") end), t(" '), "),

    t("vim.inspect("), i(1, "value"), t("))"),

  }),

  s("todo", { t("// TODO("), i(1, "name"), t("): "), i(2, "message") }),

}

Verification: In any buffer, type logd then press your expand key. Expect a print with a timestamp and vim.inspect around a placeholder. Type todo to get a TODO comment with name and message fields. Files live under lua/snippets/all.lua. Reruns produce the same shapes. If your language is not Lua, adjust comment style in a filetype specific snippet.

Add a filetype specific set. For JavaScript or TypeScript tests, a small jest describe it pattern saves time and keeps names consistent. Use dynamic nodes only when the cost is near zero. The goal is speed and predictability. Overly smart snippets tend to surprise future you.

LUA

-- ~/.config/nvim/lua/snippets/javascript.lua

local ls = require("luasnip")

local s, t, i = ls.snippet, ls.text_node, ls.insert_node

return {

  s("tfn", {

    t({ "describe('", }), i(1, "feature"), t({ "', () => {", "  it('", }), i(2, "does something"),

    t({ "', () => {", "    // arrange", "    ", }),

    i(3, "const sut = create();"),

    t({ "", "    // act", "    ", }),

    i(4, "const result = sut.run();"),

    t({ "", "    // assert", "    ", }),

    i(5, "expect(result).toBeDefined();"),

    t({ "", "  });", "});" }),

  }),

}

Verification: In a JavaScript file, type tfn and expand. A describe block with one test appears and tab stops guide you through feature, behavior, arrange, act, and assert. Save and reopen to confirm deterministic expansion. File path is lua/snippets/javascript.lua.

When you introduce nvim cmp later, integrate LuaSnip as a completion source. Keep snippet expansion under your explicit key so completion does not trigger unintentional templates. Balance speed with control. If autosnippets tempt you, try them on a branch first and measure friction. Many teams prefer manual expansion because it prevents surprises in prose and comments.

LUA

-- ~/.config/nvim/lua/plugins/cmp_luasnip.lua

return function()

  local cmp = require("cmp")

  local ls  = require("luasnip")

  cmp.setup({

    snippet = { expand = function(args) ls.lsp_expand(args.body) end },

    mapping = cmp.mapping.preset.insert({

      ["<CR>"] = cmp.mapping.confirm({ select = false }),

      ["<C-j>"] = cmp.mapping(function(fallback)

        if ls.expand_or_jumpable() then ls.expand_or_jump() else fallback() end

      end, { "i", "s" }),

      ["<C-k>"] = cmp.mapping(function(fallback)

        if ls.jumpable(-1) then ls.jump(-1) else fallback() end

      end, { "i", "s" }),

    }),

    sources = { { name = "nvim_lsp" }, { name = "luasnip" }, { name = "buffer" } },

  })

end

Verification: Completion confirms with Enter only on explicit choice. Control j and Control k manage snippet jumps without stealing other keys. Snippet items appear as a source but do not auto expand. No warnings in :messages. Behavior repeats across restarts.

Keep a tiny QA routine for snippets.

1.In a js file, expand tfn, fill stops, and run tests. Confirm names read clearly.

2.In any file, expand logd, verify timestamp, and check it prints inspected value.

3.Edit a snippet, run :LuaSnipEdit or reload the filetype, and confirm changes apply.

Snippets earn their place when they save thought, not just keystrokes. Start with three to five that you reach for weekly. Keep the rest out until a pattern repeats. Store definitions under version control, review them like code, and document triggers in your README so teammates learn the shared vocabulary. With which-key for discoverability, disciplined editing habits, and a calm snippet engine, your daily work becomes smoother and more reliable.

CHAPTER 8: GIT INTEGRATION AND TERMINAL WORKFLOW  

INTRODUCTION

Your editor becomes a command center when version control and terminals live a keystroke away. This chapter turns Neovim into a calm Git cockpit with fast feedback, surgical staging, and review flows that keep changes honest. We begin with gitsigns and fugitive, pairing inline hunks and blame with a reliable porcelain for commits, amends, and patch mode. You will stage precisely, preview before you write, and surface authorship only when it adds signal. Next we shape an embedded terminal routine and a light tmux partnership. The goal is smooth task execution without context loss: run tests, start servers, and capture logs while keeping your cursor anchored in code. Finally we design review habits that scale: quick patches, commit hygiene, and history navigation that answers why and when in seconds. By the end, you will move from edit to test to commit without leaving flow, with errors visible where they occur and diffs readable where decisions are made. The emphasis remains UI first and reproducible. Every action has a verification step, every path is explicit, and updates are staged behind safe patterns. Start with visibility: add signs, blame, and previews you can trust, then connect them to a commit story you would be proud for teammates to read tomorrow under pressure.

8.1 GITSIGNS AND FUGITIVE: INLINE DIFFS, BLAME, AND STAGING

Git should be present but never loud. You need reliable cues in the gutter, a quick way to see what changed, and a safe path from edit to commit. This subchapter wires gitsigns for inline diffs and blame, then layers fugitive for porcelain grade commands that keep your hands in Neovim. We keep the surface area small: stage the exact lines you intend, preview the hunk, and write messages that explain decisions. When feedback lives next to code, you notice mistakes earlier and commit cleaner stories. The stakes are clarity, speed, and fewer regrets during review.

Start with a minimal spec that loads gitsigns early on file read. Signs mark added, changed, and deleted lines; previews show context; blame answers authorship politely. Bind motions for next and previous hunk so you can walk a diff like search results. Keep descriptions for which-key and consistency with your leader grammar. Then add fugitive as a stable porcelain for status, diffs, and commits. You will open a split for the index, stage patches, and craft messages with a template that prompts for scope and intent.

LUA

-- ~/.config/nvim/lua/plugins/git/gitsigns.lua

return function()

  require("gitsigns").setup({

    signs = {

      add = { text = "+" }, change = { text = "~" }, delete = { text = "_" },

      topdelete = { text = "‾" }, changedelete = { text = "~" },

    },

    current_line_blame = false,

    preview_config = { border = "rounded" },

    on_attach = function(buf)

      local gs = package.loaded.gitsigns

      local map = function(mode, lhs, rhs, desc)

        vim.keymap.set(mode, lhs, rhs, { buffer = buf, silent = true, desc = desc })

      end

      map("n", "]h", gs.next_hunk, "Next hunk")

      map("n", "[h", gs.prev_hunk, "Prev hunk")

      map("n", "<leader>hp", gs.preview_hunk, "Preview hunk")

      map("n", "<leader>hs", gs.stage_hunk, "Stage hunk")

      map("n", "<leader>hr", gs.reset_hunk, "Reset hunk")

      map("n", "<leader>hb", function() gs.blame_line({ full = true }) end, "Blame line")

      map("v", "<leader>hs", function() gs.stage_hunk({ vim.fn.line("."), vim.fn.line("v") }) end, "Stage hunk (visual)")

    end,

  })

end

Verification: Open a Git tracked file. Added lines show a plus, changes a tilde, deletions an underscore. Use ]h and [h to jump hunks. Press leader hp to preview with a rounded border. Stage with leader hs and confirm the sign changes to indicate the index. Run :Gstatus in the next snippet to verify the staged patch. All mappings are buffer local and deterministic after restart. File path is lua/plugins/git/gitsigns.lua.

LUA

-- ~/.config/nvim/lua/plugins/git/fugitive.lua

return function()

  vim.keymap.set("n", "<leader>gs", "<cmd>G<cr>", { desc = "Git status (fugitive)" })

  vim.keymap.set("n", "<leader>gd", "<cmd>Gdiffsplit<cr>", { desc = "Diff split" })

  vim.keymap.set("n", "<leader>gc", "<cmd>G commit<cr>", { desc = "Git commit" })

  vim.keymap.set("n", "<leader>gp", "<cmd>G push<cr>", { desc = "Git push" })

  vim.keymap.set("n", "<leader>gl", "<cmd>G log --oneline --decorate<cr>", { desc = "Git log" })

end

Verification: Press leader gs to open status. Use - to stage or unstage hunks interactively. leader gd opens a three way diff if mergetool context exists, otherwise a clean two way. leader gc enters commit with your editor as the message buffer. After committing, leader gl lists a compact history. All commands return nonzero errors clearly if the repo state blocks the action. Reruns preserve identical behavior.

Add a small message template and conventions that nudge better commits. You can seed the buffer with a header line and a body placeholder. Keep it short, active, and scoped. During interactive add, prefer staging by hunk so you can separate unrelated edits into separate commits. Precision here pays off in later reviews when you need to revert a single change or bisect a regression.

LUA

-- ~/.config/nvim/lua/core/gitmsg.lua

local M = {}

function M.template()

  local lines = {

    "feat(scope): summarize change in imperative mood",

    "",

    "- why: ",

    "- what: ",

    "- notes: ",

  }

  vim.api.nvim_buf_set_lines(0, 0, -1, false, lines)

  vim.api.nvim_win_set_cursor(0, {1, 1})

end

return M

Verification: After leader gc, run :lua require(“core.gitmsg”).template(). The buffer fills with a simple skeleton. Write your message, save, and close to complete the commit. Template does not persist to disk and can be edited freely. Behavior is deterministic and leaves no artifacts.

Wire spec entries so everything lazy loads and fits your grammar. Keep UI integrations calm and avoid heavy dashboards. You want a path that works during stress: jump to a hunk, preview, stage, commit, push, and move on. When a merge conflict occurs, fugitive’s diff split gives a focused surface; resolve, write, and run Gwrite to mark solved segments. Keep the routine boring and well lit by gitsigns.

LUA

-- ~/.config/nvim/lua/plugins/git/init.lua

return {

  { "lewis6991/gitsigns.nvim", event = "BufReadPre", config = function() require("plugins.git.gitsigns")() end },

  { "tpope/vim-fugitive", cmd = { "G", "Gdiffsplit", "Gread", "Gwrite", "Gclog" }, config = function() require("plugins.git.fugitive")() end },

}

Verification: :Lazy lists two specs. Opening a tracked file attaches gitsigns. Running G or Gdiffsplit loads fugitive on demand. No additional startup cost. Errors appear in :messages with clear command names. With this layer live, you can stage precisely, blame politely, and keep context where it belongs: next to the code you are changing.

8.2 EMBEDDED TERMINALS, TASKS, AND TMUX SYNERGY

Running tasks where you edit reduces context switching. Neovim’s built in terminal is fast, reliable, and good enough for most teams when given a few habits. The aim is to run tests, servers, and one off commands without losing your place, then return to code with a single keystroke. You will adopt a simple pattern: keep one terminal for long running processes, one for ad hoc commands, and let your editor manage focus. If you already use tmux, treat Neovim terminals as short hops and tmux panes as long lived environments. This layered approach avoids nested complexity while preserving muscle memory.

Begin with predictable terminals. Open a horizontal split for test runs and a vertical split for servers when needed, then hide them until the next run. The rule is simple: terminals should never block the buffer you are editing. Name your terminals or rely on window numbers so movements are consistent. When a process needs environment variables, prefer a wrapper script or use direnv so you do not leak secrets into command history. Keep logs in a project specific directory that you can tail from the terminal without cluttering the repo.

Adopt a rhythm for tests. When editing a file, run the nearest or current test with a single chord and capture output in the test terminal. If the suite is noisy, filter output and keep failures at the bottom. The goal is to confirm intent quickly without an avalanche of lines. Between runs, keep your cursor in the code. Let the terminal follow your commands, not the other way around. If the task crashes, clear the buffer, start fresh, and avoid scrolling miles of stale logs to find the current failure.

For servers, start them in the project root and record the exact command in a README or a scripts section. If you need two processes, split the terminal layer so they remain visible without stealing space. When you switch branches or change environments, stop and restart with the same keys. Determinism matters more than screen artistry here. Keep the smallest number of panes that cover your daily routines. When your development stack grows, consider an external process supervisor and let Neovim drive it through a single command instead of micromanaging many terminals.

tmux remains a strong partner. Use it when you want persistent sessions that survive network drops, remote hops, or laptop sleep. Pair tmux windows with Neovim projects and keep a one to one mapping where possible. Let tmux handle OS level multiplexing and remote shells, while Neovim manages buffers, LSP, and diffs. The two tools complement each other when roles are clear. Avoid nesting tmux inside Neovim terminals except for quick checks; it multiplies keybindings and makes recovery harder under pressure.

Create small rituals that guard focus. Before running a long command, save all modified buffers. After a failure, jump directly to the file and line reported by the tool instead of scanning output. Most test runners and compilers print paths with line numbers; Telescope grep string or quickfix can collect them into navigable lists. When a process must run continuously, bind a restart key that kills and reruns cleanly. Do not repurpose editing chords for terminal control; give the terminal its own space so your fingers never guess.

Finally, keep transcripts when useful. For risky migrations or releases, log terminal output to a dated file under a local out directory. This record helps reconstruct steps when something goes wrong and improves your future automation. Delete or rotate logs on a schedule to avoid clutter. The outcome is a calm, professional terminal flow: predictable splits, named purposes, and quick recovery. With this in place, commits become smoother because you test close to the code and errors surface where you are ready to fix them.

8.3 REVIEW FLOWS: QUICK PATCHES, COMMIT HYGIENE, AND HISTORY

A clean repository tells a story. Your job during review is to make each commit a clear sentence and each pull request a coherent paragraph. Neovim can help by keeping patches small, messages sharp, and history navigable. This subchapter organizes a repeatable review routine: craft quick patches from hunks, write messages that survive time, and answer why and when with efficient history tools. The payoff is fewer back and forth comments, faster approvals, and easier rollbacks when needed.

Start with quick patches. After finishing an edit session, walk hunks in the file with your hunk motion keys. Stage only the lines that agree with your commit’s intent. If a change accidentally includes formatting or a neighboring tweak, split it out. Preview each hunk before staging so you read what reviewers will see. When a change spans multiple areas, stage and commit them separately even if it means two small commits in a row. Small, focused commits are easier to test and revert. If you must amend the most recent commit to fix a typo or include a missing file, do so quickly and push once to keep the remote history tidy.

Commit hygiene starts with language. Use the imperative mood and keep subject lines concise. If your team follows Conventional Commits, adopt feat, fix, chore, or refactor with a short scope. In the body, write why first, then what, and finally notes about alternatives or risks. Avoid restating the diff in prose. Link to issues sparingly and only when the link explains the decision. Reviewers scan messages before diffs; your words set expectations and reduce confusion. When a change feels large, consider a preparatory commit that reshapes a file without behavior changes, followed by the functional changes. That separation reduces cognitive load during review.

History is a tool, not a museum. Use a compact log to answer immediate questions and a blame view to locate the decision point behind a line. When investigating a bug, step through history locally before searching remote systems. Often a single commit reveals an assumption that no longer holds. If you suspect a regression across many commits, plan a short bisect session. Mark good and bad states with tests, then let Git narrow the culprit. Keep the editor at the center: open the file from the commit view and read in the same window where you will apply the fix. Jump between versions only as far as necessary to understand the change.

Pull requests benefit from the same discipline. Before opening one, re read your commits and squash trivial noise if your team prefers a clean history. Keep the description brief and structured: problem, approach, tests, and risk. Add a checklist if the change touches interfaces or requires docs. During review, respond with diffs, not essays. If a comment is resolved by a one line patch, make the change and push. For debates, move to a small follow up proposal rather than blocking the current thread. The goal is delivery without surprises and a record that helps the next person.

Develop a closeout routine. After merge, tag the commit if it marks a release or noteworthy change. Update a simple CHANGELOG with date and highlights. Archive any logs or transcripts gathered during testing under your local out directory and clean the rest. This tiny ceremony gives teams a rhythm and a place to look when reconstructing timelines later. Consistency beats novelty here too.

End with three daily habits. First, stage by hunk, preview, then commit with a message that explains why. Second, run tests or linters from inside Neovim before pushing, reading failures inline rather than chasing terminals. Third, skim the last few commits in compact log form every morning to prime your context and catch unintended changes early. When these habits stick, you will feel less friction shipping small improvements and more confidence tackling risky refactors. Your editor will not merely host Git; it will guide you toward a history that others can trust and learn from quickly.

CHAPTER 9: LSP, COMPLETION, AND DEBUGGING — 16 pp — 4,400 words

9.1 mason.nvim: installing and wiring language servers fast [CODE] — 4 pp — 1,100 words

9.2 Diagnostics, code actions, and go-to definitions that feel native [CODE] — 4 pp — 1,100 words

9.3 nvim-cmp fundamentals and snippet integration [CODE] — 4 pp — 1,100 words

9.4 DAP quickstart: breakpoints, stepping, and variables [CODE] — 4 pp — 1,100 words

CHAPTER 9: LSP, COMPLETION, AND DEBUGGING

CHAPTER INTRO PAGES: 1 pp. (Total word target: 274 words)

INTRODUCTION

Language intelligence turns Neovim from a sharp editor into a dependable development cockpit. This chapter builds a lean, reproducible stack for diagnostics, navigation, completion, and step debugging that feels first party rather than bolted on. We start with mason.nvim to install and wire language servers quickly, keeping versions explicit and setup files small. Then we shape diagnostics, code actions, and go to definitions so movement is predictable and noise stays low. Next we configure nvim cmp with a calm menu, snippet integration, and sensible confirmation rules that protect your typing. Finally we add a debugging baseline with DAP, covering breakpoints, stepping, variables, and launch configurations you can copy between projects.

The flow remains UI first with light Lua only where it saves time or cost. Every feature includes a verification routine with explicit paths, consistent keymaps, and deterministic behavior across restarts. Where multiple plugins compete, we choose the simplest option that meets daily needs and prove it with a short test. Secrets and tool binaries stay outside the repo behind environment checks. By the end, you will have a stable LSP layer, a completion menu that behaves, and a debugger ready for real bugs. Start with provisioning: install servers once, auto wire capabilities, and confirm the editor understands your code before you ask it to move or change anything.

9.1 MASON.NVIM: INSTALLING AND WIRING LANGUAGE SERVERS FAST

A reliable LSP foundation begins with repeatable installation. mason.nvim manages language servers, formatters, and linters with a small UI and headless commands, while mason lspconfig bridges each server to Neovim. The goal is speed and safety. You will install only what you need, auto wire sensible defaults, and keep per language overrides small and local. If a machine is fresh, a single command brings it to parity. If a server fails, you see a clear path to fix without spelunking.

Start with a tiny spec that defers loading until LSP events matter. Keep mason’s UI quiet and rely on a short command to sync tools. Expose one module that returns your on attach function and capabilities so every language shares the same behaviors. This reduces drift, keeps keymaps consistent, and makes reviews simpler because changes appear in one place.

LUA

-- ~/.config/nvim/lua/plugins/lsp/init.lua

return {

  { "williamboman/mason.nvim", build = ":MasonUpdate", cmd = { "Mason", "MasonInstall", "MasonUninstall" },

    config = function() require("mason").setup({ ui = { border = "rounded" } }) end },

  { "williamboman/mason-lspconfig.nvim", event = "BufReadPre",

    dependencies = { "neovim/nvim-lspconfig" },

    config = function()

      local mason_lsp = require("mason-lspconfig")

      mason_lsp.setup({ ensure_installed = { "lua_ls", "tsserver", "bashls", "jsonls", "yamlls" } })

      mason_lsp.setup_handlers({

        function(server)

          local lsp = require("lspconfig")

          local opts = require("plugins.lsp.server_opts").build(server)

          lsp[server].setup(opts)

        end,

      })

    end },

}

Verification: :Lazy lists mason and mason lspconfig. Running :Mason opens a bordered UI showing installed servers. On first file open, handlers call lspconfig with shared opts. Servers in ensure installed are installed automatically if missing. :messages remains clean. Files live under lua/plugins/lsp. Reruns preserve identical behavior.

Centralize on attach and capabilities so all servers gain the same keymaps, diagnostics, and completion features. Keep the function short and readable. Provide a way to add server specific tweaks without forking logic. A small switch based on server name is enough for most teams and avoids duplicating files.

LUA

-- ~/.config/nvim/lua/plugins/lsp/server_opts.lua

local M = {}

local function on_attach(client, bufnr)

  local map = function(lhs, rhs, desc)

    vim.keymap.set("n", lhs, rhs, { buffer = bufnr, silent = true, desc = desc })

  end

  map("gd", vim.lsp.buf.definition, "Go to definition")

  map("gr", vim.lsp.buf.references, "List references")

  map("K",  vim.lsp.buf.hover, "Hover")

  map("<leader>rn", vim.lsp.buf.rename, "Rename symbol")

  map("<leader>ca", vim.lsp.buf.code_action, "Code action")

  map("<leader>fd", vim.diagnostic.open_float, "Line diagnostics")

end

local function capabilities()

  local base = vim.lsp.protocol.make_client_capabilities()

  local ok, cmp = pcall(require, "cmp_nvim_lsp")

  return ok and cmp.default_capabilities(base) or base

end

function M.build(server)

  local opts = { on_attach = on_attach, capabilities = capabilities() }

  if server == "lua_ls" then

    opts.settings = { Lua = { diagnostics = { globals = { "vim" } } } }

  end

  return opts

end

return M

Verification: Open a Lua or TypeScript file. gd jumps to definitions, K shows hover, and leader ca opens actions. Completion works later through cmp added capabilities. No global warnings in Lua due to vim allowance. All mappings are buffer local. Restarting preserves behavior.

Use MasonUpdate in CI scripts or manual routines to refresh registries without forcing user interaction. When a new teammate clones the config, ensure installed pulls baseline servers and lspconfig wires them without touching per project files. This keeps your repo portable and avoids committing binary artifacts. If a tool requires external dependencies like npm or pip, document them in the README and degrade gracefully when missing by skipping setup with a warning.

For verification and reproducibility, keep a short drill.

1.Run :Mason and install a server not yet present, such as jsonls. Confirm the installed mark appears.

2.Open a matching file and trigger hover and diagnostics. Confirm definitions and references list.

3.Rename a symbol with leader rn and verify changes in file and quickfix.

Keep per language overrides near the server without sprawling files. If you need complex settings for one language, add a tiny file and import it from server opts. Limit cleverness. The less you do per server, the easier it is to update and the fewer places bugs can hide. With servers installed and wired, you can shape the experience: diagnostics that inform rather than distract, actions that feel native, and jumps that respect your focus.

9.2 DIAGNOSTICS, CODE ACTIONS, AND GO TO DEFINITIONS THAT FEEL NATIVE

Diagnostics are helpful when they guide rather than nag. This subchapter tunes signs, virtual text, and floats so errors are visible, warnings are calm, and hints do not clutter. We add precise movement for issues, native feeling actions, and definitions that jump without losing context. The stakes are daily comfort and fewer misclicks. When feedback appears in the right place, you fix faster and think less about the editor.

First, quiet the noise. Prefer gutter signs for status at a glance, a single line float on demand, and minimal virtual text. Show counts in the statusline from Chapter 6 and let Telescope collect issues when you want the big picture. Keep signs consistent with your theme and avoid blinking or rainbow icons that compete with code. In insert mode, defer updates slightly so the screen does not churn while you type.

LUA

-- ~/.config/nvim/lua/plugins/lsp/diagnostics.lua

return function()

  vim.diagnostic.config({

    underline = true,

    virtual_text = { spacing = 2, severity = { min = vim.diagnostic.severity.WARN } },

    signs = true,

    severity_sort = true,

    update_in_insert = false,

    float = { border = "rounded", source = "if_many", focusable = false },

  })

  local signs = { Error = "E", Warn = "W", Hint = "H", Info = "I" }

  for type, text in pairs(signs) do

    local hl = "DiagnosticSign" .. type

    vim.fn.sign_define(hl, { text = text, texthl = hl, numhl = "" })

  end

  vim.keymap.set("n", "<leader>e", vim.diagnostic.open_float, { desc = "Line diagnostics" })

  vim.keymap.set("n", "]d", vim.diagnostic.goto_next, { desc = "Next diagnostic" })

  vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, { desc = "Prev diagnostic" })

end

Verification: Errors show E in the gutter, warnings W, hints H, info I. Virtual text appears only for warnings or worse. leader e opens a quiet rounded float with source when helpful. ]d and [d move between items deterministically. No jitter in insert mode. Reruns preserve settings.

Make actions and jumps feel native. Use buffer local maps from on attach and ensure each one does a single clear thing. Definitions should open in place by default, with alternatives to split, tab, or preview. References should land in Telescope for quick filtering. Code actions should run for the cursor range rather than the whole file when a visual selection exists. These small choices add up to a flow that makes sense under pressure.

LUA

-- ~/.config/nvim/lua/plugins/lsp/nav.lua

return function(bufnr)

  local map = function(lhs, rhs, desc, mode)

    vim.keymap.set(mode or "n", lhs, rhs, { buffer = bufnr, silent = true, desc = desc })

  end

  map("gd", vim.lsp.buf.definition, "Go to definition")

  map("gD", vim.lsp.buf.declaration, "Go to declaration")

  map("gi", vim.lsp.buf.implementation, "Go to implementation")

  map("gT", vim.lsp.buf.type_definition, "Type definition")

  map("gr", require("telescope.builtin").lsp_references, "List references")

  map("<leader>ca", function()

    if vim.fn.mode():match("[Vv]") then vim.lsp.buf.range_code_action() else vim.lsp.buf.code_action() end

  end, "Code action", { "n", "x" })

end

Verification: In a project with an active server, gd jumps to definition. gr opens Telescope references. Visual select a range and run leader ca to get scoped actions. No global mappings are created; behavior stays buffer local. Errors surface cleanly if no server attaches.

When servers disagree or flood hints, scope by severity and per filetype rules. For example, in TypeScript projects with strict settings, you may demote certain diagnostics to hints in the editor even if the compiler treats them as warnings. Keep these decisions documented and minimal. The goal is alignment with team expectations, not silencing legitimate issues. If you need stronger control, add a small filter before display that drops specific codes after logging once for visibility.

Finally, add a tiny QA loop.

1.Introduce a deliberate type error and confirm sign, float, and movement.

2.Request a code action over a selected range and verify only local changes.

3.Jump to definitions and back with , then list references and open one.

With diagnostics and navigation shaped, completion becomes the next constant in your day. A quiet menu that confirms only on intent will protect your typing speed and reduce accidental edits. That is our next layer.

9.3 NVIM-CMP FUNDAMENTALS AND SNIPPET INTEGRATION

Completion should accelerate thinking without stealing keystrokes. nvim cmp provides a flexible menu, fine grained sources, and predictable confirmation rules. Your objective is a calm setup: no auto insert, no surprising tab behavior, and snippet integration that jumps when you ask. You will wire cmp with LuaSnip from Chapter 7, prioritize LSP items, and keep buffer and path results available but unobtrusive. The result is a menu that appears when helpful, stays out of the way when you type freely, and confirms only on explicit action.

Start with a minimal, readable setup. Keep mappings near defaults but disable auto select to avoid accidental commits. Use  to confirm only a chosen item, and map Control j and Control k to snippet jump when appropriate. Add a small formatting function that shows source names without drowning the line. Limit the number of entries so scanning is quick. Performance beats ornamentation when you are deep in code.

LUA

-- ~/.config/nvim/lua/plugins/cmp.lua

return function()

  local cmp = require("cmp")

  local ls  = require("luasnip")

  cmp.setup({

    snippet = { expand = function(args) ls.lsp_expand(args.body) end },

    completion = { completeopt = "menu,menuone,noselect" },

    mapping = cmp.mapping.preset.insert({

      ["<CR>"]   = cmp.mapping.confirm({ select = false }),

      ["<C-Space>"] = cmp.mapping.complete(),

      ["<C-e>"]  = cmp.mapping.abort(),

      ["<C-j>"]  = cmp.mapping(function(fallback)

        if ls.expand_or_jumpable() then ls.expand_or_jump() else fallback() end

      end, { "i", "s" }),

      ["<C-k>"]  = cmp.mapping(function(fallback)

        if ls.jumpable(-1) then ls.jump(-1) else fallback() end

      end, { "i", "s" }),

    }),

    sources = cmp.config.sources({

      { name = "nvim_lsp" },

      { name = "luasnip" },

    }, {

      { name = "path" }, { name = "buffer" },

    }),

    formatting = {

      fields = { "abbr", "menu" },

      format = function(entry, item)

        item.menu = ({

          nvim_lsp = "[LSP]", luasnip = "[Snip]", buffer = "[Buf]", path = "[Path]",

        })[entry.source.name]

        return item

      end,

    },

    experimental = { ghost_text = false },

  })

end

Verification: In insert mode, suggestions appear but nothing is pre selected. Press Control Space to open the menu on demand. Press Enter only after choosing an item. Control j and Control k jump through snippet stops. Menu shows [LSP], [Snip], [Buf], or [Path] tags. No warnings in :messages. Behavior persists across restarts.

Add filetype aware sources only where they pay off. For Markdown or git commit messages, prefer buffer and path, and disable LSP to reduce noise. For JSON, add a small source that suggests keys from the current file. Keep these tweaks short and local. Over specialized completion often backfires by offering low value results that steal attention. Document exceptions in your README so teammates understand why one filetype behaves differently.

LUA

-- ~/.config/nvim/lua/plugins/cmp_ft.lua

return function()

  local cmp = require("cmp")

  cmp.setup.filetype({ "gitcommit", "markdown" }, {

    sources = cmp.config.sources({ { name = "buffer" }, { name = "path" } }),

  })

end

Verification: In a commit buffer, LSP items no longer appear. Suggestions come from current text and paths. Enter confirms explicitly chosen items only. No global behavior changes.

When integrating with LSP, ensure capabilities are merged from cmp nvim lsp. You added this earlier in server opts, so servers advertise snippet support and completion item features properly. If a server misbehaves, log once and continue rather than crashing the session. Keep your keymaps consistent across machines and avoid bindings that collide with shell shortcuts you rely on in terminals.

A short QA routine keeps completion honest.

1.Type a function name partially and press Control Space. Choose an item and press Enter. Confirm only the chosen item inserts.

2.Expand a snippet, jump forward and backward, then type to confirm completion still works inside snippets.

3.In a commit buffer, verify sources are buffer and path only.

With completion stable and polite, you can shift to runtime behavior. DAP adds a small, dependable debugger so you can answer what actually happens rather than guessing. Keep it minimal and project friendly.

9.4 DAP QUICKSTART: BREAKPOINTS, STEPPING, AND VARIABLES

Debugging should illuminate, not interrupt. Neovim’s DAP ecosystem offers a thin, scriptable interface that connects to language specific adapters. Your aim is a tiny baseline that sets breakpoints, steps through code, inspects variables, and reuses launch settings across projects. You will add the core UI, one adapter to prove the path, and a pattern for per project configs that never leak secrets. Keep visuals calm and controls predictable so the debugger feels like an extension of motion rather than a separate tool.

Begin with a minimal spec: nvim dap for protocol, nvim dap ui for panels, and mason nvim dap to install adapters. Load only when you run or set a breakpoint. Use simple keymaps that mirror common debuggers: F5 to continue, F10 to step over, F11 to step into, Shift F11 to step out, and leader db to toggle a breakpoint. Show scopes and stacks in a side panel that opens only during sessions.

LUA

-- ~/.config/nvim/lua/plugins/dap/init.lua

return {

  { "mfussenegger/nvim-dap", keys = { "<F5>", "<F10>", "<F11>" } },

  { "rcarriga/nvim-dap-ui",  dependencies = { "nvim-neotest/nvim-nio" },

    config = function()

      local dap, ui = require("dap"), require("dapui")

      ui.setup({ layouts = { { elements = { "scopes", "stacks", "breakpoints", "watches" }, size = 40, position = "left" } } })

      dap.listeners.after.event_initialized["dapui_config"] = function() ui.open() end

      dap.listeners.before.event_terminated["dapui_config"] = function() ui.close() end

      dap.listeners.before.event_exited["dapui_config"] = function() ui.close() end

      vim.keymap.set("n", "<F5>", dap.continue, { desc = "DAP continue" })

      vim.keymap.set("n", "<F10>", dap.step_over, { desc = "DAP step over" })

      vim.keymap.set("n", "<F11>", dap.step_into, { desc = "DAP step into" })

      vim.keymap.set("n", "<S-F11>", dap.step_out, { desc = "DAP step out" })

      vim.keymap.set("n", "<leader>db", dap.toggle_breakpoint, { desc = "DAP toggle breakpoint" })

    end },

  { "jay-babu/mason-nvim-dap.nvim",

    config = function()

      require("mason-nvim-dap").setup({

        ensure_installed = { "js" }, handlers = {},

      })

    end },

}

Verification: Press leader db to add a breakpoint. F5 opens dap ui and starts or attaches when configured. Panels show scopes, stacks, breakpoints, and watches. Closing the session closes ui panes automatically. :messages remains clean. Behavior persists across restarts.

Configure one adapter to make the flow real. For JavaScript and TypeScript, the vscode js adapter provides a solid baseline. Point it at a local Node runtime and define a launch that runs the current file. Keep project specific settings in .vscode launch.json or a project local .nvim/dap.json read at runtime, then merge into defaults. Avoid hardcoded absolute paths and use workspace folders.

LUA

-- ~/.config/nvim/lua/plugins/dap/js.lua

return function()

  local dap = require("dap")

  dap.adapters["pwa-node"] = {

    type = "server",

    host = "127.0.0.1",

    port = "${port}",

    executable = { command = "node", args = { vim.fn.stdpath("data") .. "/mason/packages/js-debug-adapter/js-debug/src/dapDebugServer.js", "${port}" } },

  }

  dap.configurations.javascript = {

    {

      type = "pwa-node",

      request = "launch",

      name = "Launch current file",

      program = "${file}",

      cwd = "${workspaceFolder}",

      console = "integratedTerminal",

    },

  }

end

Verification: With js debug adapter installed by mason nvim dap, open a .js file, set a breakpoint, and press F5. Execution stops at the mark. Scopes panel lists variables with live values. Step with F10 and F11 and watch values change. The integrated terminal runs the process. Reruns behave identically.

Store optional per project overrides safely. Read a local JSON file if present and merge keys into the base configuration, logging once when applied. Never commit secrets. Keep the override path inside the project and add it to .gitignore by default. This gives teams flexibility for ports and environment variables without forking your global config.

LUA

-- ~/.config/nvim/lua/plugins/dap/override.lua

return function()

  local dap = require("dap")

  local path = ".nvim/dap.json"

  if vim.fn.filereadable(path) == 1 then

    local ok, data = pcall(vim.fn.json_decode, table.concat(vim.fn.readfile(path), "\n"))

    if ok and data and data.javascript then

      for k, v in pairs(data.javascript) do dap.configurations.javascript[1][k] = v end

      vim.notify("DAP project overrides applied from " .. path, vim.log.levels.INFO)

    end

  end

end

Verification: Create .nvim/dap.json in a project with keys like env or runtimeArgs. Launch again and confirm the new settings apply. Without the file, defaults remain. No secrets are printed to logs. Behavior is deterministic.

Keep a small QA drill.

1.Set a breakpoint, run with F5, step over twice, and inspect a variable in scopes.

2.Add a watch expression and confirm it updates during stepping.

3.Create a project override with an env variable, relaunch, and verify in process output.

A calm debugger closes the loop between reading and running. When breakpoints, stepping, and variables behave the same every day, you spend more time fixing and less time configuring. With LSP, completion, and DAP in place, your Neovim is now a fast, modern, and fully custom workflow ready for production codebases.

CHAPTER 10: TREESITTER FOR SMART STRUCTURE  

INTRODUCTION

Structure awareness makes edits safer and navigation calmer. Tree-sitter parses your buffer into a concrete syntax tree so Neovim can see functions, parameters, blocks, and comments as first class shapes. This chapter installs parsers for the languages you actually touch, then layers practical features: fast highlighting, incremental selections, structural textobjects, and folds that follow meaning rather than indentation. You will add only a few bindings and defaults, wire them behind lazy loading, and verify performance with repeatable checks. The goal is to operate on code by intent. Instead of counting braces or wrestling with regex, you will select a function, swap parameters, jump between siblings, or fold regions confidently. Finally we extend this power to structural search and replace using queries. You will learn how to express patterns like “call to X with any arguments,” preview matches, and execute minimal, reversible changes. Everything you add lives in versioned files with explicit paths so teammates can reproduce behavior on fresh machines. For risk control, you will keep language coverage realistic, prefer stable grammars, and fall back to plain motions if a parser misbehaves. Each subchapter remains UI first with small, copy safe Lua. By the end, Tree-sitter becomes the quiet engine under your editing habits, improving highlight accuracy, enabling precise selections, and powering refactors that read like decisions. Start by installing parsers and locking coverage to what you need this quarter, then prove speed and stability before turning on advanced features.

10.1 INSTALLING PARSERS AND PRACTICAL LANGUAGE COVERAGE

Tree-sitter is powerful when it stays invisible. Your plan is simple: install parsers for the languages you use weekly, enable highlight and incremental selection, and keep everything lazy loaded. Avoid the temptation to install every grammar. Each parser pulls a repository and can change behavior on update. Restraint improves stability and makes profiling honest. We will configure nvim-treesitter with a small ensure installed list, defer compilation until you open a matching file, and add ergonomic keymaps for growing and shrinking selections. Then we will verify correctness with a quick drill and record the exact files so teammates land on the same defaults.

Begin with a clear spec. Place configuration under a treesitter folder and expose a single setup function. Keep your parser list focused on Lua, JavaScript or TypeScript, JSON, YAML, Bash, and any language critical to your team. You can add more later when a project requires it. Enable highlight and incremental selection only. Additional modules like indent and playground can follow after stability checks. Remember that Treesitter and LSP complement each other; do not try to make one replace the other.

LUA

-- ~/.config/nvim/lua/plugins/treesitter/init.lua

return function()

  require("nvim-treesitter.configs").setup({

    ensure_installed = {

      "lua", "vim", "vimdoc",

      "javascript", "typescript", "json", "yaml", "bash", "markdown", "markdown_inline",

      "html", "css", "query"

    },

    sync_install = false,

    highlight = { enable = true, additional_vim_regex_highlighting = false },

    incremental_selection = {

      enable = true,

      keymaps = {

        init_selection = "gnn",

        node_incremental = "grn",

        node_decremental = "grm",

        scope_incremental = "grc",

      },

    },

  })

end

Verification: Open a Lua or TypeScript file. Syntax highlighting sharpens without flicker. Press gnn to start a selection on the node under cursor, then grn to grow to the next syntactic unit, grm to shrink, and grc to jump to the nearest scope. :TSInstallInfo lists installed parsers. No errors appear in :messages. File path is lua/plugins/treesitter/init.lua. Restarting preserves behavior.

Wire the spec to lazy.nvim so compilation and module load happen only when needed. Use event BufReadPost and a cmd to expose TS utilities. Keep the plugin list small and explicit to reduce surprises during updates. If your OS lacks a compiler, instruct teammates to install one or use prebuilt binaries when available. Prefer building locally for reproducibility and speed.

LUA

-- ~/.config/nvim/lua/plugins/treesitter/spec.lua

return {

  {

    "nvim-treesitter/nvim-treesitter",

    build = ":TSUpdate",

    event = { "BufReadPost", "BufNewFile" },

    cmd = { "TSInstall", "TSUpdate", "TSModuleInfo" },

    config = function() require("plugins.treesitter").init() end,

  },

}

Verification: :Lazy shows nvim-treesitter with a build of TSUpdate. Opening a supported file triggers no blocking work after the first install. :TSModuleInfo reports highlight and incremental_selection enabled. A second launch is deterministic. No startup regressions appear in nvim –startuptime out/startup-treesitter.log.

Keep language coverage practical. For large monorepos, start with the languages that dominate your work. Add parsers for templating languages only when a project requires structural selections there. When a parser proves unstable, disable highlight for that filetype temporarily while keeping the module installed for textobjects. This selective approach prevents all-or-nothing decisions and preserves your daily flow.

LUA

-- ~/.config/nvim/lua/plugins/treesitter/overrides.lua

return function()

  local ok, cfg = pcall(require, "nvim-treesitter.configs")

  if not ok then return end

  cfg.setup({

    highlight = {

      disable = function(lang, buf)

        local big = vim.api.nvim_buf_line_count(buf) > 20000

        return big or lang == "markdown"

      end,

    },

  })

end

Verification: Very large files or markdown buffers fall back to regex highlighting for stability. :TSModuleInfo shows highlight disabled for those buffers while other modules continue. No errors appear. Behavior is deterministic across restarts.

Establish a small QA routine you can repeat after updates.

Open representative files in each language and toggle selections with gnn, grn, and grm. Confirm selections match syntactic units.

Run :TSInstallInfo and check every parser reports OK. If any fail, run :TSUpdate and retry.

Profile startup once. Confirm treesitter does not add to critical path when lazy loaded.

Document your ensure installed list and the grow-shrink keys in the README. This keeps your team aligned and shortens onboarding. With parsers installed and selection working, you are ready to operate on structure more directly through textobjects, swapping, and folding that respects meaning rather than whitespace. That is your next step.

10.2 TEXTOBJECTS, SELECTIONS, AND STRUCTURAL FOLDING 

Textobjects are how you act on intent instead of coordinates. With Tree-sitter they become accurate across languages and resilient under refactors. This subchapter equips Neovim with structural selections for functions, classes, parameters, and blocks, plus motions to jump between them and safe swaps to reorder arguments without breaking commas or spacing. Finally you will enable structural folding driven by the syntax tree so collapsed regions match meaning, not indentation. The flow is UI first: you learn the feel of around and inside mappings, then add tiny Lua to register textobjects, movements, and a folding expression you can trust on large files.

Start with structural selections that you can recall under pressure. The pattern mirrors Vim’s language: af means around function, if means inside function, and the same for classes, parameters, and blocks. Lookahead makes the cursor hop forward to the next match so you can press the mapping from anywhere on a line. Keep a small set you use daily and expose them through which-key so discoverability improves without a cheat sheet. Growing and shrinking via incremental selection remains available as a fallback when you want to fine tune an edge.

LUA

-- ~/.config/nvim/lua/plugins/treesitter/textobjects.lua

return function()

  require("nvim-treesitter.configs").setup({

    textobjects = {

      select = {

        enable = true, lookahead = true,

        keymaps = {

          ["af"] = "@function.outer", ["if"] = "@function.inner",

          ["ac"] = "@class.outer",    ["ic"] = "@class.inner",

          ["ap"] = "@parameter.outer",["ip"] = "@parameter.inner",

          ["ab"] = "@block.outer",    ["ib"] = "@block.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" },

      },

      swap = {

        enable = true,

        swap_next     = { ["<leader>sp"] = "@parameter.inner" },

        swap_previous = { ["<leader>sP"] = "@parameter.inner" },

      },

    },

  })

end

Verification: In a function, vif selects its body; daf deletes it without touching the signature. af selects the full function. ]m jumps to the next function; [m moves back. With the cursor on a parameter, leader sp swaps it forward and preserves commas and whitespace. :TSModuleInfo lists textobjects enabled. Behavior is deterministic across restarts; file lives at ~/.config/nvim/lua/plugins/treesitter/textobjects.lua.

Structural movement reduces search churn. Learn a quick drill: press ]m twice to land on the next function start, type o to open a line, then add a logging call. Jump back with [m and repeat. Because set jumps is true, your jumplist records each hop, so Control o returns you to the exact origin. For classes, ]c and [c behave similarly. This symmetry lowers cognitive load and makes long files feel smaller.

Folding by structure keeps noise low on dense code. Tree-sitter can drive folding through a fold expression so regions collapse at syntactic nodes rather than mid statement. Default to fully open files and let yourself fold deliberately to avoid hiding relevant lines during reviews. Pair folding with your statusline so you can see the current fold level at a glance.

LUA

-- ~/.config/nvim/lua/plugins/treesitter/folds.lua

return function()

  vim.opt.foldmethod = "expr"

  vim.opt.foldexpr = "nvim_treesitter#foldexpr()"

  vim.opt.foldlevel = 99

  vim.opt.foldlevelstart = 99

  vim.opt.foldenable = true

end

Verification: Open a large file, run zM to close all folds, then zR to open all. Folds align with functions and classes, not arbitrary indentation. The statusline shows current fold level. No lag appears when toggling folds. Settings persist across restarts; file path is ~/.config/nvim/lua/plugins/treesitter/folds.lua.

Integrate these features with your leader grammar for discoverability. Register textobject moves and swaps in which-key under a structure group so new team members learn the language quickly. Keep counts natural: pressing 3]m jumps three functions forward. When a language lacks a parser for a rare construct, fall back to incremental selection with gnn and grn. Structure first, refinement second, regex last.

LUA

-- ~/.config/nvim/lua/core/whichkey_struct.lua

return function()

  local wk = require("which-key")

  wk.register({

    ["]m"] = "Next function", ["[m"] = "Prev function",

    ["]c"] = "Next class",    ["[c"] = "Prev class",

    ["<leader>sp"] = "Swap parameter next",

    ["<leader>sP"] = "Swap parameter prev",

    ["af"] = "Around function", ["if"] = "Inside function",

  }, { mode = "n" })

end

Verification: Press leader to open which-key and see structure hints near your mappings. Invoke ]m and watch the label. No duplicate registrations. File path is ~/.config/nvim/lua/core/whichkey_struct.lua. Behavior repeats across restarts.

Keep a short verification routine you can run after updates.

1.In a function with multiple parameters, swap them forward and back, then run tests to confirm call sites remain correct.

2.Fold the file to level 1, scan the outline, and jump between folded headings with ]m and [m, unfolding with zo only where needed.

3.Select inside function with if, change the body, and undo to confirm the selection was scoped correctly.

These habits make structural edits routine rather than risky. With textobjects, moves, swaps, and folding in place, you can now search and replace by structure, not by brittle patterns. That is where queries and structural tools shine next.

10.3 STRUCTURAL SEARCH/REPLACE AND NAVIGATION PATTERNS 

Regex is great for surface patterns; Tree-sitter queries reach the intent underneath. This subchapter introduces structural search and replace to find calls, arguments, or constructs across a codebase without false positives. You will use Tree-sitter’s query language for previews and the ssr.nvim plugin for safe, minimal rewrites. Then you will connect results to Telescope so navigation feels native. The outcome is a repeatable routine: write a small query, preview exact matches, apply a reversible patch, and commit with confidence. Keep your scope small at first and grow only after you verify on a sample.

Start with the simplest flow: preview structural matches in the current buffer. Queries describe nodes and captures. For example, to find function calls named log with any arguments in JavaScript, you match a call_expression whose callee identifier is log and capture its arguments. Use the nvim-treesitter API to run the query and highlight or echo results so you can sanity check before editing. This preview step prevents accidental overreach.

LUA

-- ~/.config/nvim/lua/tools/ts_query_preview.lua

local M = {}

function M.find_log_calls()

  local ts = vim.treesitter

  local lang = ts.language.get_lang(vim.bo.filetype)

  if not lang then return end

  local parser = ts.get_parser(0, lang)

  local tree = parser:parse()[1]

  local root = tree:root()

  local query = ts.query.parse(lang, [[

    (call_expression

      function: (identifier) @fn

      arguments: (arguments) @args)

  ]])

  for _, caps, meta in query:iter_matches(root, 0) do

    local name = ts.get_node_text(caps[1], 0)

    if name == "log" then

      local row = caps[2]:start()

      vim.api.nvim_echo({ { "log() at line " .. (row + 1), "Question" } }, false, {})

    end

  end

end

return M

Verification: In a JavaScript buffer with log calls, run :lua require(“tools.ts_query_preview”).find_log_calls(). The command echoes lines with matches. No edits occur. Errors are quiet if filetype lacks a parser. File path is ~/.config/nvim/lua/tools/ts_query_preview.lua. Reruns yield identical output.

For actual rewrites, use a purpose built structural search replace tool. The ssr.nvim plugin gives a small UI where you specify a pattern and a replacement with captures. It operates on the syntax tree, which means punctuation and whitespace survive intact when the structure allows. Keep replacements local to a buffer until you are confident, then expand to project scope via grep and quickfix. This controlled ramp keeps changes reversible and reviews readable.

LUA

-- ~/.config/nvim/lua/plugins/treesitter/ssr.lua

return {

  "cshuaimin/ssr.nvim",

  keys = { { "<leader>sr", function() require("ssr").open() end, desc = "Structural replace" } },

  config = function() require("ssr").setup({ border = "rounded" }) end,

}

Verification: Press leader sr to open ssr. In JavaScript, enter a pattern like log($A) and a replacement like logger.debug($A). Preview shows exact nodes that will change. Apply to modify the buffer. Undo restores previous state. No messages appear on unsupported filetypes. Behavior is deterministic.

Connect structure to navigation with Telescope. The treesitter picker lists captures for a given query and lets you jump to them quickly. Keep a couple of reusable queries for your languages under a queries folder and load them on demand. This pattern centralizes shared searches and avoids rewriting ad hoc snippets. When a team adopts the habit, code reviews become faster because everyone can navigate by structure rather than grepping loosely.

LUA

-- ~/.config/nvim/lua/tools/treescope.lua

local M = {}

function M.calls()

  require("telescope.builtin").treesitter({ default_text = "@function.outer" })

end

return M

Verification: Run :lua require(“tools.treescope”).calls(). Telescope lists functions in the current buffer using the @function.outer capture. Selecting an entry moves the cursor and closes the picker. No errors in :messages. File path is ~/.config/nvim/lua/tools/treescope.lua.

Create a safe workflow for multi file changes.

1.Write a query and preview matches in one representative file using your preview helper. If results look wrong, revise the query before any edits.

2.Apply a structural replace in that file with ssr.nvim and run tests or linters. Confirm only intentional lines changed.

3.Expand to project scope by collecting candidate files with Telescope live grep on a coarse text, then open each and run the structural replace locally. Stage by hunk and commit with a message that states why, what, and scope.

Add a small library of ready queries for common tasks. Examples include swapping function argument order, finding unnecessary console calls, or normalizing import paths. Store them under version control and review like code. When a query turns risky, wrap it with a dry run mode that only logs matches to an out directory with timestamps so teams can review before editing. This habit builds trust and makes large refactors less stressful.

LUA

-- ~/.config/nvim/lua/queries/javascript/refactors.scm

; find console.log calls

(call_expression

  function: (member_expression

    object: (identifier) @obj (#eq? @obj "console")

    property: (property_identifier) @prop (#eq? @prop "log"))

  arguments: (arguments) @args) @call

Verification: With nvim-treesitter installed, this query file is loadable by the treesitter picker. Running treescope calls with default_text adjusted to @call lists console.log occurrences. Editing the file updates behavior on reload. Path is ~/.config/nvim/lua/queries/javascript/refactors.scm. Reruns are stable.

Finish with two reality checks. First, structure is not magic. When grammars are incomplete or files are enormous, fall back to incremental selection and careful regex. Second, always preview before rewriting. A quick echo of matches will save you hours of revert churn later. With these guardrails, structural search and replace becomes a tool you trust for real work. It reduces accidental changes, produces cleaner diffs, and helps reviewers understand intent at a glance. From here, you can combine queries with code actions or snippets to automate larger migrations while keeping each step small and verifiable.

CHAPTER 11: FORMATTING, LINTING, AND QUALITY GATES

INTRODUCTION

Consistent code style and quick feedback reduce review friction and production risk. This chapter builds a clear chain from your buffer to your build: formatters and linters in Neovim, on save routines that respect per project rules, and external quality gates that mirror your editor exactly. We start by wiring none ls or conform.nvim so formatting and linting feel native, predictable, and reversible. You will keep adapters minimal, surface diagnostics calmly, and record explicit file paths for reproducibility. Next we add on save actions that run fast and never surprise you, plus per project overrides and style locks that protect shared repos from accidental drift. We anchor decisions in config files that travel with the code, like .editorconfig, stylua.toml, eslint, and prettier settings. Finally we align local habits with automation: pre commit hooks for quick blockers and continuous integration checks that use the same tools and versions as Neovim. The result is one source of truth, one set of rules, and one path to green. Every step includes a small QA drill so you can verify behavior after updates. By the end, formatting becomes a quiet muscle memory, linting highlights real issues without noise, and your pipelines confirm exactly what your editor promises. Start with adapters: pick the lightest path, prove it on one language, then generalize safely.

11.1 FORMATTERS AND LINTERS WITH NONE-LS/CONFORM.NVIM

Formatting and linting should be fast, deterministic, and invisible until needed. In Neovim, you have two dependable approaches: none ls exposes external tools through the LSP client, and conform.nvim calls formatters directly with simple routing. Choose one primary path to avoid duplication. We will demonstrate both, pick conform.nvim for formatting simplicity, and keep none ls available only for lint diagnostics that benefit from LSP presentation. This split keeps responsibilities clear and reduces configuration surface while preserving first class UX.

Begin with conform. It focuses on formatting and resolves tools by executable names on your system. You map filetypes to formatters, add a fallback order, and decide whether to run synchronously or asynchronously. The goal is predictable edits that never block your cursor longer than necessary. Keep commands explicit and log errors quietly to :messages with actionable hints. For teams, prefer pinned formatter versions via package managers or per project toolchains so results never diverge by machine.

LUA

-- ~/.config/nvim/lua/plugins/format/conform.lua

return function()

  local conform = require("conform")

  conform.setup({

    formatters_by_ft = {

      lua = { "stylua" },

      javascript = { "prettierd", "prettier" },

      typescript = { "prettierd", "prettier" },

      json = { "jq", "prettierd", "prettier" },

      yaml = { "yamlfmt", "prettier" },

      markdown = { "prettierd", "prettier" },

      sh = { "shfmt" },

    },

    notify_on_error = true,

    log_level = vim.log.levels.WARN,

    default_format_opts = { lsp_format = "fallback", timeout_ms = 2000 },

  })

  vim.keymap.set("n", "<leader>f", function()

    conform.format({ async = true })

  end, { desc = "Format buffer (conform)" })

end

Verification: Open a Lua file with stylua installed. Press leader f and confirm indentation and spacing follow stylua.toml. In JS, if prettierd is running, formatting is near instant; otherwise prettier runs via node and may be slower. Errors show once in :messages with tool name. File path is ~/.config/nvim/lua/plugins/format/conform.lua; behavior persists across restarts.

When lint diagnostics are valuable, none ls presents them as LSP diagnostics, which means they integrate with your existing jump keys, floats, and code actions. Keep sources small and targeted. Prefer tools already in your stack such as eslint, shellcheck, and markdownlint. Disable formatting from none ls when conform handles it to avoid conflicts. Expose a toggle in case a tool becomes noisy mid sprint.

LUA

-- ~/.config/nvim/lua/plugins/lint/nonels.lua

return function()

  local nls = require("null-ls")

  nls.setup({

    sources = {

      nls.builtins.diagnostics.eslint_d.with({ condition = function() return vim.fn.filereadable(".eslintrc.json") == 1 end }),

      nls.builtins.diagnostics.shellcheck,

      nls.builtins.diagnostics.markdownlint,

    },

    on_attach = function(client)

      if client.name == "null-ls" then client.server_capabilities.documentFormattingProvider = false end

    end,

  })

end

Verification: In a JS project with .eslintrc.json, open a file with a rule violation. Gutter signs appear and leader e shows a rounded float with the eslint code. No formatting conflicts occur when pressing leader f because none ls formatting is disabled. File path is ~/.config/nvim/lua/plugins/lint/nonels.lua. Reruns are deterministic.

Wire both plugins behind lazy loading. Conform should load on BufReadPost; none ls on LspAttach or specific filetypes to avoid global overhead. Keep requirements documented in README: node for prettier and eslint d, cargo or system packages for shfmt, and so on. For remote environments and containers, include a bootstrap script that installs pinned versions so teammates and CI match your local machine.

LUA

-- ~/.config/nvim/lua/plugins/format/spec.lua

return {

  { "stevearc/conform.nvim", event = { "BufReadPost", "BufNewFile" }, config = function() require("plugins.format.conform")() end },

  { "nvimtools/none-ls.nvim", event = "LspAttach", config = function() require("plugins.lint.nonels")() end },

}

Verification: :Lazy shows conform and none ls. Opening a tracked file attaches LSP servers as usual, then none ls sources diagnostics as configured. leader f formats via conform. :messages stays clean. Behavior persists across restarts.

To keep intent obvious, surface formatter choice in the statusline. Display the active formatter for the current buffer so you notice misconfigurations instantly. If a filetype is unmapped, pressing leader f should print a brief hint with suggested tools rather than failing silently. These small affordances build trust and help teammates onboard faster.

A brief QA drill confirms stability.

1.Edit a Lua file, deliberately misalign a table, then run leader f. Confirm stylua corrects the layout exactly once.

2.Open a JavaScript file with a semicolon rule from eslint. Verify a diagnostic appears and disappears when you fix the line.

3.Temporarily uninstall a formatter and press leader f. Confirm a single warning appears with the tool name and no partial edits occur.

With adapters in place, you can move to on save routines, per project rules, and style locks that make formatting automatic yet respectful of local variations. That is the next layer.

11.2 ON-SAVE ACTIONS, PER-PROJECT RULES, AND STYLE LOCKS

On save automation should feel like a seatbelt: present, protective, and never in the way. We will wire format on save through conform, then gate it by project trust so personal experiments do not rewrite files unexpectedly. Per project rules will flow from config files committed to the repo, and style locks will prevent drift by pinning tool versions and making deviations visible. The outcome is calm editing: you write, save, and receive consistent results that match your team’s expectations.

Start with a guarded on save autocommand. The routine attempts format through conform with a short timeout and cancels silently if tools are missing. Provide a global toggle and a per project override discovered from a simple dotfile. This arrangement keeps your home projects flexible while making team repos strict. You also surface a statusline mark so you know at a glance whether save actions are active.

LUA

-- ~/.config/nvim/lua/core/format_on_save.lua

local M = { enabled = true }

function M.toggle()

  M.enabled = not M.enabled

  vim.notify("Format on save: " .. (M.enabled and "ON" or "OFF"))

end

function M.setup()

  local conform = require("conform")

  vim.api.nvim_create_autocmd("BufWritePre", {

    group = vim.api.nvim_create_augroup("FormatOnSave", { clear = true }),

    callback = function(args)

      if not M.enabled then return end

      if vim.fn.filereadable(".nvim-no-format") == 1 then return end

      conform.format({ bufnr = args.buf, timeout_ms = 1500, async = false, lsp_format = "fallback" })

    end,

  })

  vim.keymap.set("n", "<leader>ut", M.toggle, { desc = "Toggle format on save" })

end

return M

Verification: Require this module in your init and call setup. Save a Lua or JS file and confirm formatting applies before write. Create .nvim-no-format in a directory, save again, and confirm no formatting occurs. Toggle with leader ut and watch the notification. No errors appear if tools are absent. Behavior repeats across restarts.

Per project rules come from config files at the root: stylua.toml, .prettierrc, .eslintrc.json, and .editorconfig. Ensure your tools read them by default and avoid duplicating settings in Lua. Keep only routing and timeouts in Neovim. When a project requires a different maximum line length or quote style, commit it in the project and let tools enforce it uniformly across editors and CI. For reproducibility, prefer a package manager local install for JS tools and a dedicated version for stylua via release binaries.

BASH

# project bootstrap script: scripts/dev-tools.sh

set -eu

npm pkg set scripts.format="prettier -w ."

npm pkg set scripts.lint="eslint ."

npm i -D prettier@3 eslint@9 eslint-config-prettier

curl -L -o .stylua-bin.tar.gz https://github.com/JohnnyMorganz/StyLua/releases/download/v0.20.0/stylua-linux-x86_64.zip

# unpack to tools/stylua and export PATH in shell config

Verification: Running scripts/dev-tools.sh pins tool versions locally. npx prettier and npx eslint use repo versions. stylua binary is stored under tools and referenced by PATH. Editor and CI now share identical behavior. The script is deterministic on a clean machine.

Style locks prevent silent drift. Add a simple health check that compares tool versions against a lock file and warns once per session if they differ. Keep it informational, not blocking. This reminder catches cases where a global prettier overrides a local pin or where stylua jumped versions silently.

LUA

-- ~/.config/nvim/lua/core/style_lock.lua

return function()

  local expected = { prettier = "3.", eslint = "9.", stylua = "0.20." }

  local function version(cmd)

    local ok, out = pcall(vim.fn.system, cmd .. " --version")

    return ok and (out or "") or ""

  end

  local msgs = {}

  if not version("prettier"):find(expected.prettier, 1, true) then table.insert(msgs, "prettier version mismatch") end

  if not version("eslint"):find(expected.eslint, 1, true) then table.insert(msgs, "eslint version mismatch") end

  if not version("stylua"):find(expected.stylua, 1, true) then table.insert(msgs, "stylua version mismatch") end

  if #msgs > 0 then vim.schedule(function() vim.notify(table.concat(msgs, "; "), vim.log.levels.WARN) end) end

end

Verification: On startup, mismatches trigger a single WARN notification. Adjust expected prefixes per project in a small local file if needed. No blocking occurs and no network calls run. Behavior is deterministic and quiet when versions match.

A short QA drill proves the flow.

1.Save a file and confirm conform formats before write. Toggle off and verify the buffer writes unchanged.

2.Adjust a project config, such as print width in .prettierrc, and save again. Confirm results change and match the new rule.

3.Temporarily install a different global prettier and start Neovim. Confirm the style lock warns and that npx prettier still enforces the pinned version during CI.

With on save routines and project rules stable, you can carry those expectations into Git hooks and pipelines so the same checks run for everyone every time. That is where pre commit and CI complete the system.

11.3 PRE-COMMIT HOOKS AND CI CHECKS THAT MATCH YOUR EDITOR

Local habits shine when automation agrees. Pre commit hooks catch issues before history records them, and CI confirms that green locally means green remotely. The key is alignment: the hook and pipeline must run the same tools, with the same versions and flags, as your Neovim setup. We will wire a lightweight pre commit routine, add staged formatting and linting, and create a CI workflow that installs pinned versions and runs identical commands. The outcome is fewer surprises, smaller diffs, and reviews that focus on logic rather than style.

You can use the pre commit framework or native package scripts. We will demonstrate both briefly and pick a simple, portable path using the pre commit tool for polyglot repos. Define hooks for stylua, prettier, eslint, shfmt, and markdownlint. Scope them to staged files for speed. Keep commands idempotent and quiet unless they fail.

YAML

# .pre-commit-config.yaml at repo root

repos:

  - repo: https://github.com/JohnnyMorganz/stylua

    rev: v0.20.0

    hooks: [ { id: stylua, files: "\\.lua$" } ]

  - repo: https://github.com/pre-commit/mirrors-prettier

    rev: v3.3.3

    hooks: [ { id: prettier, files: "\\.(js|ts|json|md|yaml|yml)$" } ]

  - repo: https://github.com/pre-commit/mirrors-eslint

    rev: v9.9.0

    hooks: [ { id: eslint, files: "\\.(js|ts)$", args: [ "--max-warnings=0" ] } ]

  - repo: https://github.com/jumanjihouse/pre-commit-hooks

    rev: 3.0.0

    hooks: [ { id: shfmt, files: "\\.sh$" }, { id: markdownlint, files: "\\.md$" } ]

Verification: Install pre-commit, run pre-commit install, then commit a staged Lua and JS file with style issues. Hooks format Lua and fix or fail JS per eslint rules. Exit nonzero blocks the commit until fixed. Versions match your editor pins. Re-running commits succeeds once files are corrected. Behavior is deterministic on any machine with pre-commit installed.

For JS-only teams using npm scripts, husky is also fine. Keep commands identical to your Neovim tools and project scripts so results match. Avoid duplicating configuration flags in multiple places. Delegate to prettier and eslint config files and let husky only orchestrate the call. The fewer flags in hooks, the fewer sync errors you will chase later.

Next, make CI mirror local behavior. Use GitHub Actions to install pinned versions, run the same scripts, and upload artifacts when checks fail. Cache dependencies for speed and keep steps explicit. Include a matrix only when needed; most repos benefit from a single stable runtime per language. The following workflow mirrors the editor for Node plus stylua.

YAML

# .github/workflows/quality.yml

name: quality

on: [push, pull_request]

jobs:

  check:

    runs-on: ubuntu-latest

    steps:

      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4

        with: { node-version: "20" }

      - name: Install JS tools

        run: npm ci

      - name: Install Stylua

        run: |

          curl -L -o stylua.zip https://github.com/JohnnyMorganz/StyLua/releases/download/v0.20.0/stylua-linux-x86_64.zip

          unzip -o stylua.zip -d ./tools

          echo "$PWD/tools" >> $GITHUB_PATH

      - name: Lint

        run: npm run lint

      - name: Format check

        run: |

          npm run format -- --check

          stylua --check .

Verification: Opening a pull request triggers quality. The job uses Node 20, npm ci to install pinned versions, and the same format and lint commands as local scripts. If a file is misformatted, format check fails with a clear diff. Logs contain the exact versions used. Reruns are consistent across branches and contributors.

To align Neovim with the pipeline even further, expose the same scripts in your editor through Telescope commands or which-key. For example, map leader ql to run npm run lint in a terminal split and leader qf to run prettier check. This symmetry trains your fingers on the same paths CI will take and reduces last minute surprises.

LUA

-- ~/.config/nvim/lua/core/quality.lua

return function()

  local term = function(cmd) vim.cmd.split(); vim.cmd.wincmd("J"); vim.fn.termopen(cmd) end

  vim.keymap.set("n", "<leader>ql", function() term("npm run lint") end, { desc = "Run lint" })

  vim.keymap.set("n", "<leader>qf", function() term("npm run format -- --check") end, { desc = "Run format check" })

end

Verification: Press leader ql to open a bottom split and run lint. Errors appear inline in the terminal and paths are clickable with gf if configured. leader qf runs the same check as CI. Closing the terminal with q leaves your buffer intact. Commands are deterministic and project local.

Finish with a short QA routine.

1.Create a style violation and attempt to commit. Confirm pre-commit blocks with the correct hook message.

2.Fix locally using Neovim formatting, commit successfully, and push. Verify the CI job passes without additional edits.

3.Downgrade a local tool temporarily and repeat. Confirm the style lock warns and CI still uses the pinned version, preventing drift.

With editor, hooks, and CI in lockstep, quality becomes a property of the workflow rather than a negotiation at review time. Your team spends less energy on whitespace and more on design. That is the practical payoff of a disciplined formatting and linting stack.

CHAPTER 12: EXTENDING NEOVIM WITH YOUR OWN LUA  

INTRODUCTION

Extending Neovim with your own Lua turns the editor into a platform that adapts to your work instead of the other way around. This chapter shows a safe path from tiny helpers to self contained mini plugins, event driven behaviors you can reason about, and finally packaging with docs and versioning so teammates can use your work without surprises. The flow stays UI first with light Lua where it clearly reduces friction. You will define user commands that wrap repeatable tasks, raise custom user events to decouple features, and compose helpers that are easy to test. Then you will wire reliable autocommands that respect project trust, debounce noisy events, and log clearly when something goes wrong. Finally you will package your code with a rock solid layout, write concise help docs, pin versions, and publish or share privately with confidence. Outcomes focus on safety, ownership safe patterns, deterministic settings, explicit export paths, and small QA habits that catch errors early. By the end, you will have a personal module you can drop into a fresh config and reproduce on any machine. Start with mini plugins that deliver one sharp capability, add events to keep code modular, and prove each behavior with a short verification routine before moving to packaging.

12.1 MINI-PLUGINS: COMMANDS, USER EVENTS, AND HELPERS

Small, single purpose modules are the safest way to extend Neovim. The stakes are clarity and reliability under deadline. This subchapter guides you to create a mini plugin that provides one command, one user event, and a couple of helpers. The UI remains simple: a single leader mapping and a user command with tab completion. Internally you keep state local, expose a clean interface, and write logs to a predictable file so you can debug quickly when something misbehaves. The goal is to ship value in minutes and keep future changes low risk.

Open with a tiny capability you already repeat. Suppose you often capture a scratch note with file context and timestamp. You can package that as a command that writes to a daily notes file, emits a user event after saving, and provides a helper to open today’s log. Keep paths explicit and deterministic so teammates can find artifacts on any machine. Add parameters for title and tags to make the command flexible without complexity. Then confirm behavior with a short routine and make the feature lazy loaded so startup stays fast.

LUA

-- ~/.config/nvim/lua/my/notes/init.lua

local M = {}

local function notes_dir()

  return vim.fn.expand("~/.local/share/nvim/notes")

end

local function today_path()

  return notes_dir() .. "/" .. os.date("%Y-%m-%d") .. ".md"

end

local function ensure_dir(path)

  vim.fn.mkdir(path, "p")

end

function M.append(entry)

  ensure_dir(notes_dir())

  local fp = io.open(today_path(), "a")

  fp:write(string.format("\n## %s\n\n- file: %s\n- line: %d\n\n%s\n",

    os.date("%H:%M:%S"),

    vim.fn.expand("%:p"),

    vim.fn.line("."),

    entry or ""))

  fp:close()

  vim.api.nvim_exec_autocmds("User", { pattern = "MyNotesAppended" })

  return today_path()

end

function M.open_today()

  ensure_dir(notes_dir())

  vim.cmd.edit(today_path())

end

function M.setup()

  vim.api.nvim_create_user_command("Note",

    function(opts) local p = M.append(opts.args); print("Saved note at " .. p) end,

    { nargs = "?", complete = "file" })

  vim.keymap.set("n", "<leader>nt", M.open_today, { desc = "Open today's notes" })

end

return M

Verification: Requiring my.notes and calling setup registers :Note and the mapping. Running :Note Test creates ~/.local/share/nvim/notes/YYYY-MM-DD.md with a timestamped header and context lines. A User event MyNotesAppended fires after write. Re running creates additional sections. Restarting preserves behavior. Deleting the notes directory and retrying recreates it. No errors appear in :messages. Deterministic file path allows easy backup and sync.

Expose the module through lazy.nvim with clear loading conditions. Load when the Note command is invoked or when the leader mapping is pressed. This preserves startup speed and isolates the feature to sessions that actually use it. Keep the plugin spec minimal and the repository path local at first. If you later split it out, the internal interface remains unchanged.

LUA

-- ~/.config/nvim/lua/plugins/mynotes.lua

return {

  dir = "~/.config/nvim/lua/my/notes",

  name = "my-notes",

  cmd = { "Note" },

  keys = { { "<leader>nt", function() require("my.notes").open_today() end, desc = "Open today's notes" } },

  config = function() require("my.notes").setup() end,

}

Verification: :Lazy displays my-notes as a local plugin. Invoking :Note triggers lazy load and the command executes. Pressing leader nt opens today’s file without prior load. A second invocation is instantaneous. No interference with unrelated buffers.

Raise a user event to decouple follow up actions. For example, you might want to push notes to a private repo or refresh a Telescope picker. Listening modules can subscribe to User MyNotesAppended and react without tight coupling to the notes module. This pattern keeps responsibilities separate and changes safer.

LUA

-- ~/.config/nvim/lua/my/notes/hooks.lua

return function()

  vim.api.nvim_create_autocmd("User", {

    pattern = "MyNotesAppended",

    callback = function()

      vim.schedule(function()

        vim.notify("Note captured", vim.log.levels.INFO)

      end)

    end,

  })

end

Verification: Loading hooks registers an autocmd that triggers after M.append. Running :Note shows a single notification. The callback is scheduled to avoid blocking writes. Adding additional listeners remains trivial. Behavior is deterministic and idempotent on repeat events.

Add a tiny helper library so modules share common behaviors without copying code. Debounce noisy actions and provide a safe write wrapper that reports errors cleanly. Keep it small to avoid premature frameworks. Each helper should be testable by calling it with a minimal input and checking the return value or a predictable file result.

LUA

-- ~/.config/nvim/lua/my/lib/util.lua

local U = {}

function U.debounce(ms, fn)

  local timer, queued

  return function(...)

    local args = { ... }

    queued = true

    if timer then timer:stop(); timer:close() end

    timer = vim.uv.new_timer()

    timer:start(ms, 0, function()

      if queued then fn(unpack(args)) end

      queued = false

    end)

  end

end

function U.safe_write(path, text)

  local ok, err = pcall(function()

    local f = assert(io.open(path, "w"))

    f:write(text or "")

    f:close()

  end)

  return ok, err

end

return U

Verification: Requiring my.lib.util exposes debounce and safe_write. Using debounce around a quick callback triggers only once after the delay. safe_write returns true on success or false plus an error string on failure. No global state persists between calls. Files save under explicit paths.

With a command, event, and helpers in place, you have a repeatable pattern for future mini plugins. Keep each module focused, surface one or two commands, emit user events for extensibility, and log to predictable locations. Next you will wire reliable autocommands and event driven behavior that builds on these foundations.

12.2 AUTOCOMMANDS AND EVENT-DRIVEN BEHAVIOR YOU CONTROL

Events power responsive editors, but careless autocommands can slow sessions or create loops that are hard to debug. The stakes are performance and predictability. This subchapter shows a disciplined approach: dedicated augroups, clear lifecycles, scoped patterns, debounced callbacks, and visible logs. You implement two practical behaviors. The first normalizes whitespace and final newline on write, respecting per project opt outs. The second refreshes diagnostics and status when language server events occur, while guarding against noisy bursts.

Start by creating a small wrapper that registers autocommands in named groups. Groups make it easy to clear and update rules without duplicate handlers. Keep patterns narrow and write callbacks that exit early when conditions are not met. Always use buffer numbers passed by the event for precision. Then build a friendly logging helper that appends to a single file under Neovim’s state directory so you can inspect a failing callback without spamming the UI.

LUA

-- ~/.config/nvim/lua/my/auto/init.lua

local U = require("my.lib.util")

local A = {}

local function log(msg)

  local path = vim.fn.stdpath("state") .. "/auto.log"

  local line = os.date("%H:%M:%S") .. " " .. msg .. "\n"

  local ok = U.safe_write(path, (vim.fn.filereadable(path) == 1 and (vim.fn.readfile(path) and table.concat(vim.fn.readfile(path), "\n") .. "\n") or "") .. line)

  if not ok then vim.notify("Failed to write auto.log", vim.log.levels.WARN) end

end

function A.setup()

  local grp = vim.api.nvim_create_augroup("MyAuto", { clear = true })

  vim.api.nvim_create_autocmd("BufWritePre", {

    group = grp,

    pattern = "*",

    callback = function(args)

      if vim.fn.filereadable(".nvim-no-clean") == 1 then return end

      vim.cmd([[%s/\s\+$//e]])

      if vim.bo[args.buf].ft ~= "markdown" then vim.bo[args.buf].fixeol = true end

      log("cleaned " .. vim.api.nvim_buf_get_name(args.buf))

    end,

  })

  local refresh = U.debounce(300, function(buf)

    if not vim.api.nvim_buf_is_loaded(buf) then return end

    vim.diagnostic.setloclist({ open = false })

    log("diagnostics refreshed for " .. vim.api.nvim_buf_get_name(buf))

  end)

  vim.api.nvim_create_autocmd({ "LspAttach" }, {

    group = grp,

    callback = function(args)

      local b = args.buf

      vim.api.nvim_create_autocmd({ "InsertLeave", "TextChanged", "TextChangedI" }, {

        group = grp,

        buffer = b,

        callback = function() refresh(b) end,

      })

      log("LSP attached to " .. vim.api.nvim_buf_get_name(b))

    end,

  })

end

return A

Verification: Requiring my.auto and calling setup registers autocommands under group MyAuto. Saving a file trims trailing spaces and ensures a final newline unless .nvim-no-clean exists in the project root or filetype is markdown. Diagnostics refresh after short idle following text changes once LSP is attached. The log writes to stdpath(“state”)/auto.log with timestamped lines. No duplicate events occur after editing the module and reloading; the group was cleared. Behavior persists across restarts.

Guard against heavy files and noisy formats. Very large buffers or minified assets should not trigger expensive work. Add a size check early and skip cleaning for binary or special filetypes. This keeps performance predictable across repos. When team policy differs, allow an opt in marker file that tightens rules for specific directories. Clear, documented controls make automation a net positive rather than a surprise.

LUA

-- ~/.config/nvim/lua/my/auto/guards.lua

return function(buf)

  local name = vim.api.nvim_buf_get_name(buf)

  local size = vim.fn.getfsize(name)

  if size > 2 * 1024 * 1024 then return false end

  if vim.bo[buf].buftype ~= "" then return false end

  local ft = vim.bo[buf].ft

  if ft == "minified" or ft == "svg" then return false end

  return true

end

Verification: Integrating this guard at the top of your BufWritePre callback prevents cleaning on large or special files. Saving a 3 MB file leaves it unchanged. Smaller code files still receive cleanup. No UI blocks appear. Deterministic checks yield consistent results.

Finally, expose toggles so you can suspend automation mid sprint. A simple user command flips a module level flag and prints the new state. This is safer than deleting autocommands or editing code during a deadline. When work calms, you turn it back on and continue with the same settings as before.

LUA

-- ~/.config/nvim/lua/my/auto/toggle.lua

local M = { enabled = true }

function M.toggle()

  M.enabled = not M.enabled

  vim.notify("Auto features: " .. (M.enabled and "ON" or "OFF"))

end

return M

Verification: Wiring the toggle into your callbacks causes early returns when disabled. Running :lua require(“my.auto.toggle”).toggle() switches state and shows a single notification. No residual timers run when disabled. State resets on restart to your preferred default.

With disciplined groups, debounced callbacks, guards, and toggles, your autocommands become predictable building blocks. The next step is to package your code with discoverable docs and versioning so others can use it safely.

12.3 PACKAGING, DOCS, VERSIONING, AND SHARING SAFELY

Packaging makes your Lua extensions portable, testable, and safe to share. The stakes are discoverability and trust. This subchapter sets up a clean repository layout, a minimal plugin entry, semantic versioning, and short but useful help docs. You will publish locally first, then push to a remote or keep it private with a lock file. The outcome is a plugin other people can try with one line in lazy.nvim and understand within a minute.

Start with a standard structure. Place modules under lua/myname/pluginname, keep a single entry file that exposes setup, and include a README and a help doc. Add a rock solid license and a version module so you can communicate changes clearly. Prefer tags for releases and align version numbers with your changelog. Keep dependencies explicit and small. Your plugin should load predictably and degrade gracefully when optional tools are missing.

LUA

-- ~/code/my-notes.nvim/lua/my_notes/init.lua

local M = {}

M._VERSION = "0.1.0"

function M.setup(opts)

  opts = opts or {}

  require("my_notes.core").setup(opts)

end

return M

Verification: Requiring my_notes returns a table with _VERSION and setup. Calling setup passes options to core. No global state leaks. Tagging v0.1.0 aligns the code with your README and docs.

LUA

-- ~/code/my-notes.nvim/lua/my_notes/core.lua

local M = {}

function M.setup(opts)

  require("my_notes.commands").setup(opts)

  require("my_notes.events").setup(opts)

end

return M

Verification: Core composes modules without side effects. Loading the plugin in Neovim registers commands and events. Removing a submodule later is a single line change. Behavior is deterministic.

Create a minimal runtime plugin file that loads on very lazy to register commands while deferring heavy work. This improves startup while keeping user commands available quickly. Add safe checks for missing directories and write clear error messages. Users will appreciate failures that point to fixes rather than stack traces.

LUA

-- ~/code/my-notes.nvim/plugin/my_notes.lua

if vim.g.loaded_my_notes then return end

vim.g.loaded_my_notes = true

pcall(function() require("my_notes").setup() end)

Verification: Starting Neovim with the plugin installed sets loaded_my_notes and calls setup once. Requiring again is a no op. If setup fails, pcall prevents a hard stop and users can inspect :messages. The file runs on startup, but heavy work remains deferred inside modules.

Write a help doc so :h my-notes works. Keep it short and focused on commands, mappings, configuration options, events, and file paths. Use a consistent tag and include a version line. A good help file answers what it does, how to install, how to use, how to configure, and where files are written. Keep examples copyable and align them with your README.

VIM

*my-notes.txt*  My Notes plugin  *my-notes*

Author: You     Version: 0.1.0

DESCRIPTION

Capture timestamped notes with file context and open daily logs.

INSTALL

  lazy.nvim:

    { "yourname/my-notes.nvim" }

COMMANDS

  :Note [text]       Append a note to today's file.

  <Leader>nt         Open today's notes.

CONFIG

  require("my_notes").setup({ dir = "~/.local/share/nvim/notes" })

EVENTS

  User MyNotesAppended  Fired after a note is written.

FILES

  Notes at ~/.local/share/nvim/notes/YYYY-MM-DD.md

Verification: Placing this file at ~/code/my-notes.nvim/doc/my-notes.txt and running :helptags ~/code/my-notes.nvim/doc makes :h my-notes open the document. Tags resolve and lines wrap neatly.

Publish with lazy.nvim friendly metadata. Keep the repository small and add a CI workflow that runs stylua and luacheck to protect quality. Use semantic versioning and changelog entries that describe behavior changes and migration steps. When you add or change commands or events, bump minor or major appropriately. Communicating breaking changes builds trust with users and coworkers.

LUA

-- ~/.config/nvim/lua/plugins/my_notes_spec.lua

return {

  { "yourname/my-notes.nvim", version = "*", config = function() require("my_notes").setup() end },

}

Verification: Adding this spec to a clean Neovim config installs the plugin and exposes :Note immediately. The version pin tracks the latest tag. Removing the plugin from the spec cleanly uninstalls it on the next sync. Behavior matches documentation.

A brief QA routine finalizes packaging.

Install from a fresh config using the lazy spec and run :Note. Confirm files are created under the documented paths.

Run :h my-notes and verify commands, options, and events appear. Try the example configuration to confirm it works.

Tag v0.1.1 with a small fix, update the changelog, and bump the version string. Update on a second machine and confirm behavior stays consistent.

With packaging, docs, and versioning in place, your Lua becomes a sharable asset rather than a personal script. You can now iterate safely, accept feedback, and distribute features across your team with confidence.

CHAPTER 13: AI-ASSISTED EDITING THAT YOU CONTROL  

INTRODUCTION

AI assistants can accelerate editing, refactoring, and documentation, but only when they respect your workflow, rules, and privacy. This chapter gives you a practical path to wire suggestions into Neovim without surrendering control. We start by installing a mainstream cloud assistant and a local option so you can compare latency, quality, and privacy on real tasks. You will map ergonomic prompts, tune context windows, and display clear status so you always know what is on or off. Next we establish guardrails: secrets hygiene, repository allowlists, offline modes, redaction habits, and review checklists that keep generated code aligned with style, licensing, and security expectations. We will focus on observable behavior with logs and deterministic settings so you can reproduce results between machines. Finally we explore offline, privacy first setups using local models. You will configure a lightweight server, route requests from Neovim, cache responses, and measure cost and quality trade offs. Throughout, the UI remains primary: quick toggles, minimal menus, and consistent keymaps. Code appears only where it clearly reduces friction, and every block comes with a small verification routine and explicit file paths. By the end, you can accept or reject AI suggestions confidently, move between cloud and local engines without changing habits, and keep your repository and credentials safe. Begin with installation and prompt ergonomics so the assistant fits your hands before it touches your code.

13.1 COPILOT/CODEIUM/LOCAL MODELS: SETUP AND ERGONOMIC PROMPTS

AI suggestions should feel like a gentle tailwind, not a steering wheel. The stakes are speed and clarity under pressure. This subchapter wires three paths you can toggle: a cloud assistant with strong completion quality, a second provider you can A/B for feel, and a local model through a small HTTP bridge. The UI first flow gives you visible on off indicators, a single accept key, and a prompt window for chat or transformations. Light Lua ties them together and keeps artifacts under predictable paths so you can debug or roll back calmly.

Start with a mainstream cloud provider that integrates smoothly. We will use Copilot through copilot.lua because it offers inline suggestions, chat, and nvim cmp integration with minimal ceremony. Keep your keymaps close to your movement keys and ensure you can disable suggestions per buffer. Then set up Codeium as an alternate source so you can compare quality and latency. Finally add a local model via Ollama to keep sensitive repos offline. The goal is a single leader menu to choose engine and a consistent accept shortcut regardless of source.

LUA

-- ~/.config/nvim/lua/plugins/ai/init.lua

return {

  -- Copilot

  {

    "zbirenbaum/copilot.lua",

    cmd = "Copilot",

    event = "InsertEnter",

    config = function()

      require("copilot").setup({

        suggestion = { enabled = true, auto_trigger = true, keymap = { accept = "<C-l>" } },

        panel = { enabled = false },

      })

    end,

  },

  -- Codeium

  {

    "Exafunction/codeium.vim",

    event = "InsertEnter",

    init = function()

      vim.g.codeium_disable_bindings = 1

      vim.keymap.set("i", "<C-l>", function() return vim.fn["codeium#Accept"]() end, { expr = true, desc = "Codeium accept" })

    end,

  },

  -- Local prompts via Ollama + simple prompt runner

  {

    "David-Kunz/gen.nvim",

    cmd = { "Gen" },

    config = function()

      require("gen").setup({

        model = "qwen2.5-coder:7b",

        display_mode = "float",

        show_model = true,

        no_auto_close = true,

        command = "curl -s http://127.0.0.1:11434/api/generate -d @-",

      })

    end,

  },

}

Verification: :Lazy shows three entries. On InsertEnter Copilot or Codeium become available. Pressing Control l accepts a visible inline suggestion from whichever engine is active. :Gen opens a floating prompt that calls a local Ollama server running qwen2.5 coder 7b. No errors appear in :messages when a provider is unavailable; only its features are absent. Files live under ~/.config/nvim/lua/plugins/ai/init.lua and reload cleanly with :source $MYVIMRC.

Provide an engine switch that is explicit and reversible. This avoids confusion when two providers try to suggest at once. The switch sets a global state, updates statusline text, and toggles provider specific enable flags. Your accept key remains Control l, so muscle memory never changes.

LUA

-- ~/.config/nvim/lua/ai/engine.lua

local M = { current = "copilot" }

local function status()

  vim.g.ai_engine = M.current

  vim.api.nvim_set_hl(0, "AIStatus", { link = "ModeMsg" })

end

function M.use(name)

  M.current = name

  if name == "copilot" then

    vim.cmd("Copilot enable")

    vim.g.codeium_enabled = false

  elseif name == "codeium" then

    vim.cmd("Copilot disable")

    vim.g.codeium_enabled = true

  elseif name == "local" then

    vim.cmd("Copilot disable")

    vim.g.codeium_enabled = false

  end

  status()

  vim.notify("AI engine: " .. name)

end

return M

Verification: Requiring ai.engine and calling use(“codeium”) disables Copilot and enables Codeium. use(“local”) disables both cloud engines. A notification shows the active engine and statusline components can read g.ai_engine. State persists for the session and resets on restart.

Expose a small leader menu to pick engines fast and keep your mind on the code. which key makes discoverability trivial. Add a scoped toggle per buffer for difficult files where you want silence.

LUA

-- ~/.config/nvim/lua/ai/keys.lua

return function()

  local wk = require("which-key")

  wk.register({

    a = {

      name = "AI",

      c = { function() require("ai.engine").use("copilot") end, "Use Copilot" },

      d = { function() require("ai.engine").use("codeium") end, "Use Codeium" },

      l = { function() require("ai.engine").use("local") end, "Use local model" },

      o = { function() vim.b.ai_silent = not vim.b.ai_silent end, "Toggle buffer silence" },

    },

  }, { prefix = "<leader>" })

end

Verification: Press leader a to open the AI menu. Choosing c, d, or l switches providers immediately. Toggling buffer silence sets b.ai_silent, which you can check in custom InsertCharPre handlers if you later add them. No conflicts with other leader groups.

Create ergonomic prompt operators for chat and transformations that work the same regardless of engine. Two common flows are explain selection and refactor to function. For cloud engines, you can call their chat API. For local, you can pipe the selection into the Gen command. Keep prompts short, context rich, and repeatable. Write them once and route to the active engine.

LUA

-- ~/.config/nvim/lua/ai/ops.lua

local M = {}

local function visual_selection()

  local _, ls, cs = unpack(vim.fn.getpos("'<"))

  local _, le, ce = unpack(vim.fn.getpos("'>"))

  local lines = vim.api.nvim_buf_get_lines(0, ls - 1, le, false)

  if #lines == 0 then return "" end

  lines[#lines] = string.sub(lines[#lines], 1, ce)

  lines[1] = string.sub(lines[1], cs)

  return table.concat(lines, "\n")

end

function M.explain_selection()

  local text = visual_selection()

  if vim.g.ai_engine == "local" then

    vim.cmd("Gen Explain:\n" .. text)

  else

    vim.cmd("Copilot panel") -- fall back to provider UI

  end

end

function M.refactor_selection()

  local text = visual_selection()

  if vim.g.ai_engine == "local" then

    vim.cmd("Gen Refactor to function:\n" .. text)

  else

    vim.cmd("Copilot panel")

  end

end

return M

Verification: In visual mode, mapping to require(“ai.ops”).explain_selection opens a prompt with selection content when using local, or the Copilot panel otherwise. The selection capture is deterministic across re runs. No changes occur to the buffer until you accept.

Add minimal Ollama bootstrap so the local path is turnkey. You will run a small model for latency and a medium one for quality. Explicit commands and ports keep routing obvious. Store a short README in your repo with these steps for teammates.

BASH

# ~/.local/bin/ollama-bootstrap.sh

set -eu

ollama serve >/dev/null 2>&1 &

sleep 2

ollama pull qwen2.5-coder:7b

Verification: Running the script starts the local server, pulls the model, and listens on 11434. :Gen now responds quickly. Stopping the server removes local suggestions without affecting cloud engines.

QA drill:

Insert mode in a JS file; type a function skeleton and wait for a gray suggestion. Accept with Control l. Switch engines and repeat to compare latency.

In visual mode select 8 to 10 lines and trigger explain. Confirm the prompt shows only your selection and no unintended context.

Disable all engines, measure typing latency, and confirm no regression. Re enable a single engine and verify statusline reflects the active choice.

With engines, prompts, and toggles in place, you now need guardrails so generated changes remain safe and aligned with your project. That discipline comes next.

13.2 GUARDRails, SECURITY, AND REVIEW HABITS THAT PREVENT DRIFT

AI can speed you up while quietly bending your codebase away from its standards. The stakes here are integrity, privacy, and long term maintainability. This subchapter gives you habits and controls to keep generated edits small, reviewable, and aligned with your rules. We keep the flow UI first and avoid code since the techniques are primarily behavioral and operational. Your outcomes are simple: secrets stay private, diffs stay small, authorship remains honest, and style remains consistent.

Begin with scope and provenance. Never prompt on entire files unless you must. Work on small selections with explicit goals. Write prompts that state the constraints you care about: keep function signature stable, respect existing logging format, do not add a dependency, preserve comments. Place these constraints at the top of your prompt and reuse them like templates. Track provenance by including a short marker in commit messages such as Co authored by AI after review by  when company policy allows. This establishes honest authorship and makes future audits easier.

Adopt a redaction habit before any cloud call. Search your buffer for obvious secrets or proprietary constants and replace them with neutral placeholders. If a file contains API keys, tokens, or customer data, switch to a local model or disable AI for that buffer. Maintain an allowlist of repositories or directories where cloud suggestions are permitted and default to local otherwise. Keep this list in version control so your team agrees on boundaries.

Make reviews surgical. Limit suggestions to one logical change per commit. Use git add p to select hunks intentionally, and reject any change that modifies formatting or imports unexpectedly. Generated code should pass your linters and tests before it reaches review. If a suggestion adds a dependency, ask whether the problem could be solved with a local helper instead. When you accept a block, write a short rationale in the commit message explaining what constraint it satisfied. These notes train your future self as much as your reviewers.

Measure and control context. Cloud assistants can inadvertently include more context than you expect. Keep chat prompts short, strip unrelated buffers from context providers, and prefer explicit selections. Use statusline indicators to show when context streaming is on and a key to flush it. For local models, fix temperature or sampling settings to a stable value so results are reproducible. If you need variety, change settings deliberately and record them in your notes file.

Handle licensing and attribution with care. Do not accept suggestions that copy large blocks from unknown sources. If a suggestion looks suspiciously complete, rewrite it in your style or build it from smaller steps. In corporate environments, follow policy about training data and allowed providers. When in doubt, prefer local models for sensitive algorithms and keep the generated diffs as small as possible.

Create a lightweight review checklist you can run in minutes. Confirm tests pass. Confirm style and lint rules pass. Confirm no secrets or internal URLs appear in the diff or prompt history. Confirm function signatures, public types, and error handling match project patterns. Confirm you can explain every line in your own words. When a change fails any check, revert promptly and try a smaller prompt or a local model.

Finally, build social guardrails. Share your engine toggles and prompt templates in the repo, agree on default settings, and set norms like no AI generated code in migrations or cryptography. Encourage teammates to annotate pull requests with brief notes such as Prompt used and Constraints applied. These practices keep quality high and reduce surprises. With guardrails in place, you can explore offline options confidently. That is the next step.

13.3 OFFLINE AND PRIVACY-FIRST OPTIONS WITH SANE TRADE-OFFS

Local models keep code and prompts on your machine, reduce vendor lock in, and give you predictable costs. The trade offs are model size, latency, and quality. This subchapter configures an offline stack using Ollama for serving models, a Neovim prompt runner for interactions, and optional completion for inline hints. You will fix settings for reproducibility, cache artifacts, and measure startup and response times so you can choose the smallest model that meets your needs.

Set up Ollama with two models: a small coder model for quick inline edits and a medium general model for explanations and refactors. Pin versions so responses remain stable between machines. Expose both via named routes and add a health check command in Neovim so you avoid timeouts while coding. Keep downloads on a fast disk with clear paths and quotas to avoid surprises.

BASH

# ~/.local/bin/ollama-setup.sh

set -eu

ollama serve >/dev/null 2>&1 &

sleep 2

ollama pull qwen2.5-coder:7b

ollama pull llama3.1:8b

echo "Local models ready on :11434"

Verification: Running the script pulls the specified tags and starts the server. Subsequent runs are fast because layers are cached under ~/.ollama. :Gen now lists the active model name if your prompt runner shows it. Behavior is deterministic across reboots.

Integrate an offline completion source. For stability, keep inline completion optional and favor prompt based transforms for heavier tasks. If you do enable inline hints from a local engine, limit context length and set a low temperature so suggestions do not drift. Keep a visible indicator so you remember you are on local. Map a single accept key so the feel matches cloud engines.

LUA

-- ~/.config/nvim/lua/ai/local.lua

return function()

  vim.g.ai_engine = "local"

  require("gen").setup({

    model = "qwen2.5-coder:7b",

    show_model = true,

    no_auto_close = true,

    temperature = 0.2,

    command = "curl -s http://127.0.0.1:11434/api/generate -d @-",

  })

end

Verification: Calling this module switches the active engine to local and sets deterministic parameters. Prompts behave consistently when re run on the same selection. Changing temperature or model name changes output diversity predictably.

Create tests for your AI paths. While models are probabilistic, you can still verify plumbing, timeouts, and basic output shape. Write a minimal harness that sends a fixed prompt to the local model and asserts the response is nonempty within a timeout. Save logs to a common path and include model name, temperature, and prompt length so you can compare latencies after updates. This guards against accidental regressions from configuration drift.

LUA

-- ~/.config/nvim/lua/ai/health.lua

return function()

  local start = vim.loop.hrtime()

  local ok, out = pcall(vim.fn.system, [[curl -s -X POST http://127.0.0.1:11434/api/tags]])

  if not ok or out == "" then

    vim.notify("Ollama not responding", vim.log.levels.ERROR)

    return

  end

  local ms = math.floor((vim.loop.hrtime() - start) / 1e6)

  local log = string.format("%s - health ok in %dms\n", os.date("%H:%M:%S"), ms)

  local path = vim.fn.stdpath("state") .. "/ai-health.log"

  local f = assert(io.open(path, "a")); f:write(log); f:close()

  vim.notify("Local AI healthy: " .. ms .. "ms")

end

Verification: Running require(“ai.health”)() writes a timestamped line to stdpath(“state”)/ai-health.log and shows a success notification with latency. Stopping the server produces a clear error. Reruns are consistent.

Respect privacy by default. Keep a denylist of filetypes and paths where AI is disabled. For example, do not allow prompts in secrets, auth, or migrations directories. For extra assurance, wrap your prompt function to scrub obvious tokens and emails before sending to local models even though traffic stays on loopback. These habits reduce accidental leaks if you later switch engines.

LUA

-- ~/.config/nvim/lua/ai/denylist.lua

return function(path)

  local deny = { "/secrets/", "/migrations/", "/certs/" }

  for _, d in ipairs(deny) do if string.find(path, d, 1, true) then return true end end

  return false

end

Verification: Calling the function with a buffer path returns true for denied directories. You can early return in your prompt runner when it detects a denied path. Deterministic behavior avoids footguns in sensitive repos.

Finally, compare cost and quality deliberately. Measure end to end latency for a common refactor prompt on each engine and record the result in your notes. If the local model is within an acceptable tolerance and quality is sufficient, prefer it for default tasks and reserve cloud for complex transformations or niche languages. Keep toggles and indicators visible so you always know which path you are on.

QA drill:

With the server running, send the same small refactor prompt three times. Confirm latency remains within a narrow band and outputs are similar.

Stop the server and switch to a cloud engine. Confirm failures degrade gracefully and your accept key still works.

Open a denied path file and attempt a prompt. Confirm the guard prevents calls and shows a clear reason.

With an offline path, visible controls, and small tests, you can meet privacy and reliability requirements while keeping velocity high. The chapter’s pieces form a single mental model: one accept key, one menu to switch engines, clear status, predictable prompts, and strict guardrails.

CHAPTER 14: PERFORMANCE AND BENCHMARKING  

INTRODUCTION

Performance is a feature, especially when your editor doubles as your command center. In this chapter you will learn to measure first and tweak second, so every change earns its place. We start with startup profiling using lazy profile and the built in startuptime tool to identify slow plugins, blocking autoloads, and misordered specs. You will export stable logs, compare runs, and pin wins so they persist across machines. Next we shift from cold start to everyday responsiveness. You will schedule work asynchronously, cache expensive results, and tune Lua garbage collection so insert mode remains fluid under large projects. Clear checkpoints and deterministic settings help you reproduce any improvement or regression. Finally we prepare for bad days. You will build a minimal mode to bisect problems quickly, a safe mode that disables risky integrations, and a short recovery playbook that restores a working setup under deadline without losing your history. Throughout, the approach stays UI first with light Lua where it cuts real time. We prefer sampling and logs over hunches, small reversible changes over speculative refactors, and concrete success criteria over vibes. By the end you will own a repeatable process: profile, change, verify, and commit. The bridge to the first subchapter is simple. Open your current configuration, take a baseline with lazy profile and startuptime, and let the numbers tell you where to spend your next ten minutes.

14.1 STARTUP PROFILING WITH LAZY PROFILE AND STARTUPTIME

LENGTH: 4 pp. (Total word target: 1,100 words)

Startup time matters because it sets the tone for the entire session. When Neovim feels instant, you are more willing to open new windows, switch tasks, and experiment. When it drags, you hesitate. This subchapter gives you a measurement first routine using lazy profile for plugin level insight and the startuptime profiler for frame by frame timing. The goal is to make data driven choices, export the evidence, and keep improvements across machines and branches.

Begin by capturing a clean baseline with your current configuration. Close other apps that might disturb disk or CPU, then run a short sequence of commands that saves outputs with deterministic filenames. Repeat three times and average the results to avoid chasing noise. With these numbers, you can attribute wins to specific changes and avoid placebo effects. The habit is simple: measure, change one thing, measure again, and record the diff in a small log file tracked by your dotfiles.

LUA

-- ~/.config/nvim/lua/perf/baseline.lua

local M = {}

local function out(p) return vim.fn.stdpath("cache") .. "/perf/" .. p end

function M.save_lazy_profile()

  vim.fn.mkdir(out(""), "p")

  vim.cmd("Lazy profile")

  local lines = vim.api.nvim_exec("Lazy profile", true)

  local f = assert(io.open(out("lazy_profile.txt"), "w")); f:write(lines); f:close()

  print("Saved lazy profile to " .. out("lazy_profile.txt"))

end

function M.save_startuptime()

  vim.fn.mkdir(out(""), "p")

  local cmd = string.format("nvim --clean --headless --startuptime %s +q", out("startuptime.log"))

  vim.fn.system(cmd)

  print("Saved startuptime to " .. out("startuptime.log"))

end

return M

Verification: Requiring perf.baseline and calling save_lazy_profile writes a text snapshot of the current Lazy profile to ~/.local/share/nvim/perf/lazy_profile.txt. save_startuptime invokes a headless Neovim with a clean config and stores timestamps at ~/.local/share/nvim/perf/startuptime.log. Re running produces comparable sizes and stable ordering. Success criteria: files exist, contain timestamps, and are under the expected directory.

Now wire convenient commands and a single runner that collects three samples. Keeping the loop inside Neovim avoids typos and creates a repeatable gesture during tuning sessions. You will tag runs with a date label to compare later and prevent accidental overwrites. This light tooling eliminates friction so you actually profile before and after each change.

LUA

-- ~/.config/nvim/lua/perf/commands.lua

local base = require("perf.baseline")

vim.api.nvim_create_user_command("PerfLazy", function() base.save_lazy_profile() end, {})

vim.api.nvim_create_user_command("PerfStart", function() base.save_startuptime() end, {})

vim.api.nvim_create_user_command("PerfRun", function()

  local stamp = os.date("%Y%m%d-%H%M%S")

  for i = 1, 3 do

    base.save_lazy_profile()

    base.save_startuptime()

  end

  print("Perf run complete: " .. stamp)

end, {})

Verification: :PerfLazy and :PerfStart save individual reports. :PerfRun performs three cycles and prints a timestamp. Reports are deterministic in location and comparable across machines. No blocking UI appears during headless startuptime runs.

Interpret the artifacts. Lazy’s profile highlights slow plugin specs, config functions, and load events. Look for large config times or plugins loading at startup that could be deferred to events like VeryLazy, BufReadPre, or InsertEnter. Startuptime shows initialization phases and Lua module loads in microseconds. You are hunting for a few heavy hitters rather than many small ones. Move nonessential features behind events and convert immediate requires to lazy requires inside config callbacks where possible.

LUA

-- ~/.config/nvim/lua/plugins/ui.lua

return {

  { "nvim-lualine/lualine.nvim", event = "VeryLazy", config = function()

      require("lualine").setup({ options = { theme = "auto" } })

    end

  },

  { "nvim-tree/nvim-web-devicons", lazy = true },

}

Verification: Running :Lazy profile after this change shows lualine no longer contributes to startup. devicons loads only when requested by another plugin. Startuptime total decreases measurably. Success criteria: consistent drop in total time across three runs, ideally tens of milliseconds or more.

Confirm file system costs. Disk seeks dominate cold starts on spinning media or network volumes. If your cache or plugin directory lives on a slow path, relocate it to a faster disk. You can also prime Lua’s module cache by requiring critical modules only after UI becomes interactive. Always check the profile again to prove the result.

LUA

-- ~/.config/nvim/init.lua (top)

vim.loader.enable() -- Lua module loader cache

Verification: Enabling the loader reduces repeated module load costs. Startuptime shows fewer slow require entries. Behavior is deterministic and safe to keep.

Finally, record wins and pin them. Keep a small PERF.md in your dotfiles with a table of total times and the specific change that moved the needle. Commit the lazy lockfile so versions remain stable across machines. When performance regresses after updates, compare to the last good profile and revert with confidence.

Measure before change using :PerfRun.

Apply a single lazy event or require deferral.

Measure again and confirm the delta exceeds noise.

Commit both the code change and PERF.md note.

With startup profiling mastered, you can now target responsiveness during everyday editing, where async, caching, and GC matter most.

14.2 ASYNC TASKS, CACHING, AND GC TUNING FOR RESPONSIVENESS

Once Neovim is open, the perception of speed comes from consistent input latency and fast feedback. The biggest culprits are synchronous work on the main loop, repeated expensive computations, and an aggressively frequent garbage collector. This subchapter introduces a pattern: push heavy work off the UI thread, cache results with explicit invalidation, and tune Lua’s collector to your workload. The goal is smooth typing under load and snappy commands even in large repos.

Start with an async wrapper that schedules shell calls or Lua functions without blocking. We will use libuv through vim.uv and return results to the main thread with vim.schedule. The wrapper logs duration and supports a timeout so stalled processes cannot freeze your flow. A single helper reduces boilerplate and centralizes error handling.

LUA

-- ~/.config/nvim/lua/perf/async.lua

local A = {}

function A.job(cmd, args, cb, timeout_ms)

  local t0 = vim.loop.hrtime()

  local handle

  local timer = vim.uv.new_timer()

  handle = vim.uv.spawn(cmd, { args = args or {} }, function(code, signal)

    if timer then timer:stop(); timer:close() end

    if handle and not handle:is_closing() then handle:close() end

    local elapsed = math.floor((vim.loop.hrtime() - t0) / 1e6)

    vim.schedule(function() cb({ code = code, signal = signal, ms = elapsed }) end)

  end)

  if timeout_ms and timeout_ms > 0 then

    timer:start(timeout_ms, 0, function()

      if handle and not handle:is_closing() then handle:kill("sigterm") end

      vim.schedule(function() cb({ timeout = true, ms = math.floor((vim.loop.hrtime() - t0) / 1e6) }) end)

    end)

  end

end

return A

Verification: Calling A.job(“git”, { “status”, “–porcelain” }, cb, 800) returns in the callback with either an exit code or timeout, never blocking the UI. Repeated calls show stable timing in milliseconds. No leaked handles remain after completion.

Add a tiny memoization cache for pure function results that change infrequently, such as parsing a project file or computing a tags list. Use a content hash or file mtime for invalidation, and store cache files under stdpath(“cache”) so they are easy to inspect and reset. When in doubt, prefer stale safe caching that falls back to fresh computation if validation fails.

LUA

-- ~/.config/nvim/lua/perf/cache.lua

local C = {}

local Path = vim.fn.stdpath("cache") .. "/nvim-cache"

vim.fn.mkdir(Path, "p")

local function key(name) return string.format("%s/%s.json", Path, name) end

function C.save(name, tbl)

  local f = assert(io.open(key(name), "w"))

  f:write(vim.json.encode(tbl)); f:close()

  return key(name)

end

function C.load(name)

  local p = key(name)

  if vim.fn.filereadable(p) == 1 then

    local f = assert(io.open(p, "r")); local data = f:read("*a"); f:close()

    return vim.json.decode(data)

  end

end

return C

Verification: C.save writes JSON under ~/.local/state or cache path; C.load returns a Lua table when the file exists. Corrupt files raise a clear error if decode fails. Deterministic filenames simplify debugging.

Use the helpers together for a real task: gather changed files with git, cache the list for five minutes, and refresh asynchronously on demand. The UI remains responsive because the process runs off the main loop, and repeated opens are instant while the cache is warm. Provide a manual refresh command with visible notifications so you retain control.

LUA

-- ~/.config/nvim/lua/perf/changed.lua

local A = require("perf.async")

local C = require("perf.cache")

local M = {}

local function cache_key() return "changed-" .. (vim.fn.getcwd():gsub("[^%w]", "_")) end

function M.refresh()

  A.job("git", { "status", "--porcelain" }, function(res)

    if res.timeout then

      vim.notify("git status timed out in " .. res.ms .. "ms", vim.log.levels.WARN); return

    end

    local out = vim.fn.systemlist("git status --porcelain")

    local files = {}

    for _, line in ipairs(out) do table.insert(files, line:sub(4)) end

    C.save(cache_key(), { at = os.time(), files = files })

    vim.notify("Changed files cached (" .. #files .. ")", vim.log.levels.INFO)

  end, 800)

end

function M.list()

  local data = C.load(cache_key()) or { at = 0, files = {} }

  if os.time() - data.at > 300 then M.refresh() end

  return data.files

end

return M

Verification: :lua require(“perf.changed”).refresh() populates the cache quickly without blocking input. :lua print(vim.inspect(require(“perf.changed”).list())) returns a table of filenames. Editing remains smooth during refresh because work is off the main loop. Cache expires after five minutes. Paths resolve per working directory.

Tune the garbage collector. Lua’s default collector may pause too often in large sessions. You can increase the pause and step multiplier for fewer, larger collections or trigger manual collections during idle moments. Keep settings explicit in one file, log before and after, and confirm changes with a repeatable typing latency test using a large buffer.

LUA

-- ~/.config/nvim/lua/perf/gc.lua

local M = { pause = 200, stepmul = 400 }

function M.apply()

  collectgarbage("collect")

  collectgarbage("setpause", M.pause)

  collectgarbage("setstepmul", M.stepmul)

  vim.notify(string.format("GC tuned pause=%d stepmul=%d", M.pause, M.stepmul))

end

function M.idle_collect()

  vim.defer_fn(function() collectgarbage("collect") end, 1000)

end

return M

Verification: Requiring perf.gc and calling apply sets pause and step multiplier. Typing in a 10k line file shows fewer micro stutters. Calling idle_collect after large operations schedules a collection without blocking current input. Success criteria: measurable drop in input latency spikes using :TSPlayground or a simple timer based keypress echo.

Complete the loop with a quick benchmark of insert latency. You will simulate input with feedkeys and measure elapsed time. While not perfect, it helps detect regressions after big changes.

LUA

-- ~/.config/nvim/lua/perf/bench.lua

return function(n)

  local start = vim.loop.hrtime()

  for _ = 1, n or 2000 do vim.api.nvim_replace_termcodes("a<Esc>", true, false, true) end

  local ms = math.floor((vim.loop.hrtime() - start) / 1e6)

  print("Pseudo insert cycles:", ms, "ms")

end

Verification: Running the bench function before and after GC tuning yields comparable conditions. Reduced ms after tuning indicates improvement. Ensure no other heavy tasks run during tests. Record numbers in PERF.md.

With async tasks, caching, and GC tuned, your day to day editing should feel lighter. Next you will prepare minimal and safe modes plus a recovery playbook to stay productive when something breaks.

14.3 MINIMAL MODE, SAFE MODE, AND RECOVERY PLAYBOOKS

Even the best tuned setup can break after an update or while experimenting. The fastest way back to flow is a minimal environment you can launch instantly, a safe mode that loads only trusted layers, and a short recovery playbook that restores a working baseline without losing history. This subchapter designs those tools so bad days become routine rather than chaotic.

Define minimal mode first. It loads Neovim with an isolated, known good config that contains your core habits only: essential options, basic keymaps, Telescope for navigation, and a statusline. No LSP, no heavy UI flourishes, no custom autocommands beyond the basics. You will launch it with a shell alias so it is available in muscle memory. Minimal is where you confirm the editor and file system behave, independent of your full stack.

Create safe mode next. It uses your real config but disables high risk areas like experimental plugins, AI assistants, and custom autocmd groups. You implement it with a single environment variable checked early in init.lua. When set, your plugin manager reads a reduced spec list, and your own modules respect the flag to skip heavier features. Safe mode is where you reproduce a bug with fewer moving parts and continue real work while you bisect.

Write a recovery playbook as a numbered list kept inside your dotfiles repository. It covers rolling back your lazy lockfile, reopening the last working session, collecting minimal logs, and restoring known good versions. You will capture exact commands and file paths so the entire process runs from memory when something breaks during a deadline. Keep it short enough to run in three minutes and long enough to avoid mistakes.

LUA

-- ~/.config/nvim/init.lua (early)

if vim.env.NVIM_SAFE_MODE == "1" then

  vim.g.safe_mode = true

end

-- ~/.config/nvim/lua/plugins/init.lua

local safe = vim.g.safe_mode

return {

  { "nvim-lua/plenary.nvim" },

  { "nvim-telescope/telescope.nvim", dependencies = { "nvim-lua/plenary.nvim" } },

  not safe and { "zbirenbaum/copilot.lua", enabled = true } or { "zbirenbaum/copilot.lua", enabled = false },

  -- add more specs with enabled = not safe for risky areas

}

Verification: Launching NVIM_SAFE_MODE=1 nvim loads without Copilot and other risky plugins. Telescope still works, and your essential mappings are present. Unsetting the variable restores the full configuration on the next start. Behavior is deterministic and easy to communicate to teammates.

BASH

# ~/.local/bin/nv-min

#!/usr/bin/env bash

exec nvim -u ~/.config/nvim/min/init.lua -U NONE -i NONE --noplugin "$@"

Verification: Running nv-min opens Neovim with the minimal config only. Startup is very fast, and no third party plugins load. You can edit files and confirm the system works independent of your main setup.

Document the recovery steps inside PERF.md or a dedicated RECOVERY.md so you do not rely on memory during stress. A compact sequence might be:

Launch safe mode with NVIM_SAFE_MODE=1 nvim and confirm the issue persists.

If resolved, bisect plugins with :Lazy disable and re enable in halves, measuring startup and responsiveness each time.

If not resolved, run nv-min to confirm the base editor is healthy.

Restore the last known good lockfile using git checkout lazy-lock.json and run :Lazy sync.

Export logs from stdpath(“state”) and capture :messages into cache/perf/ for later analysis.

Close with discipline around updates. Batch plugin upgrades, run :PerfRun before and after, and revert immediately when numbers regress or errors appear. Keep minimal and safe mode scripts committed and executable on every machine. With these modes and a rehearsed playbook, you can fix or bypass problems quickly and return to shipping work instead of wrestling configuration.

CHAPTER 15: PROJECT BLUEPRINTS THAT SHIP  

INTRODUCTION

This chapter turns your editor into a project factory. Instead of collecting plugins without a plan, you will assemble opinionated blueprints that ship credible work across three common tracks and a final language agnostic harness. The web stack blueprint gives you a reliable layout for JavaScript, TypeScript, and Python services, including file naming, test discovery, and task runners that behave the same in Neovim and CI. The systems blueprint focuses on Rust and Go with fast language servers, tight debug loops, and binary inspection so performance questions get real answers. The data and notes blueprint organizes Markdown, quick notebooks, and research flows so ideas graduate into code with traceable decisions. Finally, a universal test harness binds everything: a single key to run nearest tests, capture artifacts, and enforce style locks. You will keep the flow UI first through Telescope, statuslines, job control, and a few short Lua helpers that live under predictable paths. Each blueprint includes a working scaffold, export paths, and a tiny verification routine so you can reproduce behavior on a new machine without guesswork. By the end, you will launch new projects in minutes with the same motions, the same prompts, and the same guardrails. Begin with the web stack where naming, tests, and tools create momentum quickly.

15.1 WEB STACK (JS/TS/PYTHON): FILE NAMING, TESTS, TOOLS

Shipping web services fast requires a predictable shape. The stakes are simple: new teammates should know where code lives, tests should discover themselves, and common scripts should run identically from Neovim, terminal, and CI. This subchapter delivers a compact scaffold for JavaScript, TypeScript, and Python that keeps decisions boring and results dependable. We keep the UI first by exposing everything through Telescope pickers and a statusline segment that shows environment and test status at a glance. Light Lua wires task runners, test discovery, and lint formats so the editor and CI share one truth.

Start with naming and layout. For Node and TypeScript, group source under src and tests under tests with mirrored paths. Use filename.test.ts for unit tests and filename.spec.ts for integration to keep selectors unambiguous. For Python, place code under app or src, mirror tests under tests, and prefer test_filename.py. Keep configuration close to code but not inside source directories to avoid packaging surprises. These conventions allow Neovim pickers to jump between implementation and test with a fast transform, while runners pattern match without custom globbing.

Establish a single tasks file per project. It defines build, test, lint, typecheck, and format scripts that a human can run by memory. In Node projects, place them in package.json. In Python projects, gather commands in a tasks.py or Makefile, but expose consistent names. The editor will call these scripts, capture output, and parse standard formats for diagnostics. When scripts agree across repositories, muscle memory wins and onboarding becomes a matter of cloning and running a single help command.

Create shared Telescope pickers that surface scripts, tests, and changed files. The picker names should read like natural language: run test nearest, run all tests, fix lint, or open coverage. Under the hood, each picker calls a single Lua helper that spawns jobs with timeouts, writes logs under a deterministic path, and posts a succinct status notification. You will verify everything with a minimal sample repository so the routine becomes a template rather than a one off experiment.

LUA

-- ~/.config/nvim/lua/blueprints/web/tasks.lua

local A = {}

local function outdir()

  local p = vim.fn.stdpath("state") .. "/tasks"

  vim.fn.mkdir(p, "p"); return p

end

local function run(cmd, args, title)

  local logfile = string.format("%s/%s-%s.log", outdir(), os.date("%Y%m%d-%H%M%S"), title:gsub("%s+", "_"))

  local stdout = assert(io.open(logfile, "w"))

  local t0 = vim.loop.hrtime()

  local handle

  handle = vim.uv.spawn(cmd, { args = args or {}, stdio = { nil, stdout, stdout } }, function(code)

    if handle and not handle:is_closing() then handle:close() end

    stdout:close()

    local ms = math.floor((vim.loop.hrtime() - t0) / 1e6)

    vim.schedule(function()

      local msg = string.format("%s exited %d in %dms → %s", title, code, ms, logfile)

      local level = code == 0 and vim.log.levels.INFO or vim.log.levels.ERROR

      vim.notify(msg, level)

    end)

  end)

end

function A.npm(script) run("npm", { "run", script }, "npm " .. script) end

function A.pnpm(script) run("pnpm", { script }, "pnpm " .. script) end

function A.py(cmd) run("python", { "-m", cmd }, "python -m " .. cmd) end

return A

Verification: Calling require(“blueprints.web.tasks”).npm(“test”) spawns npm run test, streams into a timestamped log under stdpath(“state”)/tasks, and posts a notification with exit code and duration. Re running produces a new log without overwriting old output. Success criteria: log file exists, message appears, and UI never blocks during long runs.

Expose the commands through Telescope. A narrow list reduces decision making and keeps navigation universal across repos. The picker populates from the presence of files like package.json, pyproject.toml, or requirements.txt, and shows only relevant actions. Users confirm with Enter and the helper runs the job. To keep signal high, keep names short and predictable, and surface only the actions that matter daily.

LUA

-- ~/.config/nvim/lua/blueprints/web/pickers.lua

local pickers = require("telescope.pickers")

local finders = require("telescope.finders")

local conf = require("telescope.config").values

local tasks = require("blueprints.web.tasks")

local function has(path) return vim.fn.filereadable(path) == 1 end

local function items()

  local opts = {}

  if has("package.json") then

    table.insert(opts, { "Run tests", function() tasks.npm("test") end })

    table.insert(opts, { "Fix lint", function() tasks.npm("lint:fix") end })

    table.insert(opts, { "Typecheck", function() tasks.npm("typecheck") end })

    table.insert(opts, { "Format", function() tasks.npm("format") end })

  end

  if has("pyproject.toml") or has("requirements.txt") then

    table.insert(opts, { "Pytest", function() tasks.py("pytest") end })

    table.insert(opts, { "Ruff fix", function() tasks.py("ruff") end })

    table.insert(opts, { "Black", function() tasks.py("black") end })

  end

  return opts

end

return function()

  local opts = items()

  pickers.new({}, {

    prompt_title = "Project tasks",

    finder = finders.new_table({

      results = opts,

      entry_maker = function(e) return { ordinal = e[1], display = e[1], value = e[2] } end,

    }),

    sorter = conf.generic_sorter({}),

    attach_mappings = function(_, map)

      local run = function(bufnr)

        local selection = require("telescope.actions.state").get_selected_entry()

        require("telescope.actions").close(bufnr)

        selection.value()

      end

      map("i", "<CR>", run); map("n", "<CR>", run)

      return true

    end,

  }):find()

end

Verification: Requiring the picker and executing it shows a short menu tailored to the current repo. Choosing an item spawns the matching task and writes logs. Behavior is deterministic per repository shape and works in clean clones on fresh machines.

Bridge between implementation and tests with fast transforms. For TS or JS, jump from src/module/path.ts to tests/module/path.test.ts using a single key. For Python, jump between app/module.py and tests/test_module.py. These transforms let you navigate and run nearest tests without fuzzy searching every time. They save seconds per jump and stack into minutes daily.

LUA

-- ~/.config/nvim/lua/blueprints/web/jump.lua

local M = {}

local function switch(path)

  if path:find("/src/") then

    return path:gsub("/src/", "/tests/"):gsub("%.ts$", ".test.ts"):gsub("%.js$", ".test.js")

  elseif path:find("/tests/") then

    return path:gsub("/tests/", "/src/"):gsub("%.test%.ts$", ".ts"):gsub("%.test%.js$", ".js")

  elseif path:find("/app/") then

    local name = path:match(".*/(.*)%.py$")

    return "tests/test_" .. name .. ".py"

  elseif path:find("/tests/test_") and path:match("tests/test_(.*)%.py$") then

    local name = path:match("tests/test_(.*)%.py$")

    return "app/" .. name .. ".py"

  end

end

function M.other()

  local alt = switch(vim.fn.expand("%:p"))

  if alt and vim.fn.filereadable(alt) == 1 then vim.cmd("edit " .. alt) else vim.notify("Alternate not found") end

end

return M

Verification: Calling require(“blueprints.web.jump”).other from a file under src opens the mirrored test file when it exists. From a test it returns to the implementation. Paths resolve on macOS, Linux, and WSL. Success criteria: one keystroke round trip under a second in a medium repo.

Surface diagnostics uniformly. Configure eslint, prettier, and TypeScript server for JS TS, and ruff and black for Python, but route diagnostics through the same Neovim display. Use none ls or conform for formatting and prefer on save actions that match CI. That way, what you see in the editor equals what your pipeline enforces. Keep per project overrides in .eslintrc.cjs, pyproject.toml, and .prettierrc, but keep your editor settings stable.

LUA

-- ~/.config/nvim/lua/blueprints/web/format.lua

return function()

  require("conform").setup({

    formatters_by_ft = {

      javascript = { "prettier" },

      typescript = { "prettier" },

      json = { "prettier" },

      python = { "black" },

    },

    format_on_save = function(bufnr)

      return { lsp_fallback = true, timeout_ms = 1500 }

    end,

  })

end

Verification: Saving a .ts or .py file formats with the configured tool or falls back to LSP. Running in CI with the same tools yields identical diffs. Success criteria: zero drift between local saves and pipeline checks across three fresh machines.

Provide nearest test, file test, and all tests with a stable trio of keys that work in Node and Python. Use vim test or a tiny adapter that detects framework and constructs the command. Capture output to the same log directory and show a short summary in the statusline. This keeps your eyes on the code while giving immediate pass fail feedback.

LUA

-- ~/.config/nvim/lua/blueprints/web/test.lua

local tasks = require("blueprints.web.tasks")

local M = {}

local function is_node() return vim.fn.filereadable("package.json") == 1 end

local function is_py() return vim.fn.filereadable("pyproject.toml") == 1 or vim.fn.filereadable("requirements.txt") == 1 end

function M.nearest()

  if is_node() then

    tasks.npm("test:nearest")

  elseif is_py() then

    tasks.py("pytest")

  else

    vim.notify("No test framework detected")

  end

end

function M.file()

  local file = vim.fn.expand("%")

  if is_node() then

    tasks.npm("test:file -- " .. file)

  elseif is_py() then

    tasks.py("pytest " .. file)

  end

end

function M.all()

  if is_node() then tasks.npm("test") elseif is_py() then tasks.py("pytest") end

end

return M

Verification: The three functions map to consistent keys. In a Node project, test:nearest and test:file must exist in package.json scripts; in Python, pytest runs with selection based on file path. Logs write to state/tasks and notifications show exit code and duration. Deterministic behavior across clones confirms the wiring.

Finally, export a minimal scaffold so new repos start with the same rails. A small script copies .editorconfig, a baseline package.json or pyproject, and a README that documents the keys and pickers. Store it under ~/.config/nvim/blueprints/web and call it with a Telescope action named New web project. Consistency here compounds every week.

BASH

# ~/.config/nvim/blueprints/web/new.sh

set -eu

name="${1:-web-app}"

mkdir -p "$name"/{src,tests}

cat >"$name/README.md" <<EOF

Fast start:

1. Open in Neovim.

2. :lua require('blueprints.web.pickers')()

3. Run "Project tasks" then "Run tests".

EOF

Verification: Running the script creates a ready directory with src and tests and a quick README. Opening in Neovim immediately exposes the picker and tasks. Success criteria: zero manual steps beyond naming and opening.

With naming, scripts, pickers, and tests aligned, your web projects feel identical and productive. Next, you will tune systems work in Rust and Go where LSP speed, DAP loops, and binary inspection define velocity.

15.2 SYSTEMS (RUST/GO): FAST LSP, DAP, AND INSPECTION

Systems work rewards short feedback loops and honest inspection. Your stakes are compile times, precise diagnostics, and the ability to step through code the moment behavior surprises you. This subchapter assembles a Rust and Go blueprint that favors fast language servers, zero surprise keymaps, and one touch debug sessions. The UI stays primary through Telescope commands for targets, tests, and binaries, while a few Lua helpers keep configuration in one place and reproducible across machines.

Start with LSP setup that prefers correctness with minimal ceremony. For Rust, rust analyzer provides smart indexing, inlay hints, and reliable code actions. For Go, gopls balances speed and accuracy with good defaults. Install both via mason, wire capabilities to nvim cmp and snippets, and keep per project overrides in rust toolchain or go env files rather than ad hoc editor tweaks. This ensures your editor matches the toolchain your CI expects, preventing mysterious divergences.

Add status cues that matter. Show toolchain channel for Rust and Go version for Go in the statusline so you can spot mismatches at a glance. Expose a Telescope picker that lists cargo and go test targets with the same keys across languages. The picker should also surface common scripts like fmt, clippy, vet, and tidy. These actions run through a shared job helper that logs to a predictable path and never blocks input.

LUA

-- ~/.config/nvim/lua/blueprints/systems/lsp.lua

return function()

  local lsp = require("lspconfig")

  local caps = require("cmp_nvim_lsp").default_capabilities()

  lsp.rust_analyzer.setup({

    capabilities = caps,

    settings = {

      ["rust-analyzer"] = {

        cargo = { allFeatures = true },

        checkOnSave = { command = "clippy" },

      },

    },

  })

  lsp.gopls.setup({

    capabilities = caps,

    settings = {

      gopls = { analyses = { unusedparams = true }, staticcheck = true },

    },

  })

end

Verification: Opening a Rust or Go file attaches the server, exposes completion via nvim cmp, and surfaces diagnostics inline. :LspInfo confirms one client with expected settings. Saving a Rust file triggers clippy checks; Go shows staticcheck warnings. Deterministic behavior across clones and toolchains yields identical diagnostics.

Prepare fast runners. For Rust, cargo test with a filter runs nearest tests quickly. For Go, go test ./… or package level runs are common. Wrap both in a picker so you do not remember flags. Capture timings and exit codes into logs and keep a short success toast. Running tests should feel like saving a file, not a context switch.

LUA

-- ~/.config/nvim/lua/blueprints/systems/pickers.lua

local tasks = require("blueprints.web.tasks") -- reuse job runner

local pickers, finders, conf = require("telescope.pickers"), require("telescope.finders"), require("telescope.config").values

local function opts()

  local o = {}

  if vim.fn.filereadable("Cargo.toml") == 1 then

    table.insert(o, { "Cargo test all", function() tasks.pnpm("cargo:test:all") end })

    table.insert(o, { "Cargo clippy", function() tasks.pnpm("cargo:clippy") end })

    table.insert(o, { "Cargo fmt", function() tasks.pnpm("cargo:fmt") end })

  end

  if vim.fn.filereadable("go.mod") == 1 then

    table.insert(o, { "Go test all", function() tasks.pnpm("go:test:all") end })

    table.insert(o, { "Go vet", function() tasks.pnpm("go:vet") end })

    table.insert(o, { "Go fmt", function() tasks.pnpm("go:fmt") end })

  end

  return o

end

return function()

  local o = opts()

  pickers.new({}, {

    prompt_title = "Systems tasks",

    finder = finders.new_table({

      results = o,

      entry_maker = function(e) return { ordinal = e[1], display = e[1], value = e[2] } end,

    }),

    sorter = conf.generic_sorter({}),

    attach_mappings = function(_, map)

      local run = function(bufnr)

        local s = require("telescope.actions.state").get_selected_entry()

        require("telescope.actions").close(bufnr)

        s.value()

      end

      map("i", "<CR>", run); map("n", "<CR>", run)

      return true

    end,

  }):find()

end

Verification: In a Rust repo with scripts defined in package.json (proxying cargo commands) the picker runs clippy, fmt, and tests. In a Go repo with equivalent scripts it runs go actions. Logs write to state/tasks and the UI stays responsive. Success criteria: commands succeed in terminal and inside Neovim with identical exit codes.

Wire DAP for real debugging. For Rust, lldb via codelldb adapter or lldb dap works well. For Go, delve provides reliable stepping. Configure adapters once, define two launch configurations per language, and add a Telescope picker to start sessions. Keep breakpoints persistent using the built in signs and store layouts so windows open predictably. Debugging feels like a natural extension of your edit loop when the UI moves with you.

LUA

-- ~/.config/nvim/lua/blueprints/systems/dap.lua

return function()

  local dap = require("dap")

  -- Go

  dap.adapters.go = { type = "server", host = "127.0.0.1", port = 38697 }

  dap.configurations.go = {

    { type = "go", name = "Debug package", request = "launch", program = "${fileDirname}" },

    { type = "go", name = "Debug file", request = "launch", program = "${file}" },

  }

  -- Rust via codelldb

  dap.adapters.codelldb = { type = "server", port = "${port}", executable = { command = "codelldb", args = { "--port", "${port}" } } }

  dap.configurations.rust = {

    { type = "codelldb", name = "Debug binary", request = "launch", program = function() return vim.fn.input("Path to exe: ", "target/debug/", "file") end },

  }

end

Verification: :lua require(“blueprints.systems.dap”)() registers adapters. Opening a Go file and starting “Debug file” runs delve and stops at breakpoints. Running “Debug binary” in Rust starts codelldb on the chosen executable. Success criteria: stepping, inspecting variables, and stopping cleanly produce no errors in :messages across machines.

Add binary inspection shortcuts that save hours. Bind keys for strings, nm, size, and simple objdump excerpts on the last built binary. These commands run as jobs with small timeouts and open read only splits. You get quick answers on symbol presence, binary size changes, and text segment growth without leaving Neovim. Pair this with a statusline segment that shows the built artifact size so regressions are obvious.

LUA

-- ~/.config/nvim/lua/blueprints/systems/inspect.lua

local M = {}

local tasks = require("blueprints.web.tasks")

local function open_log(path) vim.cmd("vsplit " .. path) end

function M.size(path) tasks.pnpm(string.format("bin:size -- %s", path)); end

function M.symbols(path) tasks.pnpm(string.format("bin:symbols -- %s", path)); end

function M.strings(path) tasks.pnpm(string.format("bin:strings -- %s", path)); end

return M

Verification: With scripts defined in package.json to call size or nm, invoking these helpers runs the command and writes logs. Opening the log shows the result immediately. Deterministic paths keep history organized and diffable across builds.

Unify formatting and linting. rustfmt, clippy, gofmt, and golangci lint should run on save and in CI with the same flags. Route everything through conform or none ls so the editor’s behavior equals the pipeline’s. Keep your on save timeout short and never block insert mode. If a tool is slow, run it on write through an async job and apply patches when finished.

LUA

-- ~/.config/nvim/lua/blueprints/systems/format.lua

return function()

  require("conform").setup({

    formatters_by_ft = { rust = { "rustfmt" }, go = { "gofmt" } },

    format_on_save = function() return { timeout_ms = 1200, lsp_fallback = true } end,

  })

end

Verification: Saving a Rust or Go file formats consistently with toolchain defaults. Running cargo fmt or go fmt in CI produces identical diffs. Success criteria: zero drift and no blocking UI.

With servers, runners, DAP, and inspection wired, your Rust and Go projects gain short, predictable loops. Next, you will bring the same discipline to data, notes, and research so ideas become code without losing context.

15.3 DATA & NOTES: MARKDOWN, NOTEBOOKS, AND RESEARCH  

Notes, research, and experiments become durable when capture is fast, structure is consistent, and retrieval is delightful. This subchapter turns Neovim into your everyday research cockpit. The flow is UI first with Telescope and gentle status cues, while a few Lua helpers wire Markdown authoring, notebook execution, citation hints, and project archives. Your goal is to open a repo or a plain folder and immediately create, preview, search, and cross link notes and experiments without leaving Neovim, then export artifacts to predictable paths for teammates or future you.

Start with Markdown that feels native. Rely on Treesitter for structure, marksman for language server features, and ltex for grammar suggestions when writing longer prose. You will keep Markdown files in a docs folder by default and surface a context aware picker that jumps through headings, code blocks, and backlinks. For previews, choose a low friction solution that launches a local browser only when asked, not on every save, so the editor remains quiet and focused. Every artifact you generate should land under a single docs or out directory to make sharing and cleanup simple.

Install the core plugins and language tools using a clear, lazy loaded spec. This keeps startup fast and avoids surprises on machines that are not used for writing. The configuration below also sets conservative defaults that do not rewrite your text or enforce personal style; those choices belong in project specific config files to keep collaboration polite and reversible.

LUA

-- ~/.config/nvim/lua/plugins/writing.lua

return {

  { "nvim-treesitter/nvim-treesitter", build = ":TSUpdate", opts = { highlight = { enable = true } } },

  { "preservim/vim-markdown", ft = "markdown", init = function() vim.g.vim_markdown_new_list_item_indent = 2 end },

  { "jakewvincent/texmagic.nvim", ft = { "markdown", "tex" } },

  { "MrcJkb/haskell-tools.nvim", enabled = false }, -- example guard to show selective enabling

  { "nvim-telescope/telescope.nvim", cmd = "Telescope" },

  { "ibhagwan/fzf-lua", enabled = false },

  { "lervag/wiki.vim", ft = "markdown", init = function() vim.g.wiki_root = vim.fn.expand("~/notes") end },

  { "renerocksai/telekasten.nvim", ft = "markdown", dependencies = { "nvim-telescope/telescope.nvim" } },

  { "nvimtools/none-ls.nvim", event = "BufReadPre" },

  { "neovim/nvim-lspconfig", event = "BufReadPre" },

}

Verification: On the first Markdown open, Treesitter highlighting activates, marksman can be attached manually later, and Telescope is available on demand. No extra processes run until you call them. Success criteria: :checkhealth reports installed parsers, :TSModuleInfo shows markdown enabled, and startup time is unchanged compared to baseline.

Wire marksman and optional ltex for language server features like document symbols, link validation, and grammar hints. Keep settings gentle and easy to toggle, since prose varies widely across projects and audiences.

LUA

-- ~/.config/nvim/lua/lsp/markdown.lua

local lsp = require("lspconfig")

lsp.marksman.setup({})

lsp.ltex.setup({

  settings = {

    ltex = {

      language = "en-US",

      disabledRules = { ["en-US"] = { "WHITESPACE_RULE" } },

      additionalRules = { enablePickyRules = false },

    },

  },

})

vim.api.nvim_create_user_command("GrammarToggle", function()

  if vim.lsp.get_active_clients({ name = "ltex" })[1] then

    vim.cmd("LspStop ltex")

  else

    vim.cmd("LspStart ltex")

  end

end, {})

Verification: Opening a Markdown file attaches marksman; running :GrammarToggle starts or stops ltex. Success criteria: :LspInfo lists the active server correctly, document symbols are available in Telescope lsp_document_symbols, and grammar diagnostics appear or disappear on toggle without errors.

Make creation and retrieval effortless with a research picker. It creates daily notes, inserts backlinks, searches headings, and opens code blocks as scratch buffers for quick experiments. Rather than invent a giant framework, lean on Telescope lists and a tiny helper that respects your notes root and timestamped filenames.

LUA

-- ~/.config/nvim/lua/research/picker.lua

local Path = require("plenary.path")

local notes_root = vim.fn.expand(vim.g.notes_root or "~/notes")

local function today_path()

  return Path:new(notes_root, os.date("%Y"), os.date("%m"), os.date("%Y-%m-%d") .. ".md"):absolute()

end

local M = {}

function M.new_daily()

  local p = today_path()

  vim.fn.mkdir(Path:new(p):parent():absolute(), "p")

  vim.cmd("edit " .. p)

  if vim.fn.line("$") == 1 and vim.fn.getline(1) == "" then

    vim.fn.append(0, { "# " .. os.date("%A, %B %d, %Y"), "", "## Notes", "", "## Tasks", "", "## Links" })

    vim.cmd("normal! G")

  end

end

function M.backlink()

  local word = vim.fn.expand("<cword>")

  vim.fn.append(vim.fn.line("$"), { "", "[[" .. word .. "]]" })

end

function M.headings()

  require("telescope.builtin").grep_string({

    prompt_title = "Headings",

    search = "^#",

    cwd = notes_root,

    use_regex = true,

  })

end

return M

Verification: Calling require(“research.picker”).new_daily opens or creates today’s note with a simple template under ~/notes/YYYY/MM. Success criteria: files land under the correct folders, the headings picker lists titles from the notes root, and backlinks append at end of the current document.

Notebooks compose code and prose. For polyglot teams, keep notebooks light by executing code blocks inside Markdown using a dedicated runner rather than embedding heavyweight .ipynb. Use a safe runner command that parses fenced code blocks and executes them in isolated shells, saving outputs alongside the note with deterministic names derived from headings.

LUA

-- ~/.config/nvim/lua/research/runblocks.lua

local M = {}

local function run_block(lang, text, out_dir)

  local fname = os.date("%Y%m%d-%H%M%S") .. "-" .. lang .. ".out"

  local out = out_dir .. "/" .. fname

  local cmd

  if lang == "python" then cmd = { "python", "-u", "-c", text }

  elseif lang == "bash" or lang == "sh" then cmd = { "bash", "-lc", text }

  elseif lang == "lua" then cmd = { "lua", "-e", text }

  else return nil, "unsupported:" .. lang end

  local result = vim.system(cmd, { text = true }):wait()

  return out, result.stdout .. result.stderr, result.code

end

function M.run_current_block()

  local bufnr = vim.api.nvim_get_current_buf()

  local row = vim.api.nvim_win_get_cursor(0)[1]

  local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)

  local s, e, lang

  for i = row, 1, -1 do

    local m = lines[i]:match("^```(%w+)")

    if m then s = i + 1; lang = m; break end

  end

  for j = row, #lines do

    if lines[j]:match("^```%s*$") then e = j - 1; break end

  end

  if not s or not e or not lang then return vim.notify("No fenced block found", vim.log.levels.WARN) end

  local text = table.concat(vim.list_slice(lines, s, e), "\n")

  local out_dir = vim.fn.expand(vim.fn.stdpath("state") .. "/runblocks")

  vim.fn.mkdir(out_dir, "p")

  local out, data, code = run_block(lang, text, out_dir)

  if not out then return vim.notify(data, vim.log.levels.ERROR) end

  vim.fn.writefile(vim.split(data, "\n"), out)

  vim.notify(string.format("%s exit %d → %s", lang, code, out))

  vim.cmd("vsplit " .. out)

end

return M

Verification: Placing the cursor inside a fenced code block and calling require(“research.runblocks”).run_current_block executes the block and writes output under stdpath(“state”)/runblocks with a timestamped filename. Success criteria: exit codes appear in the notification, outputs open in a split, rerunning the same block yields repeatable results, and no files are created outside the state directory.

Exporting cleans results for sharing. Add a command that copies the current Markdown plus adjacent outputs into an out folder with a stable name based on the top level heading. This keeps exports deterministic and scannable by teammates or CI.

LUA

-- ~/.config/nvim/lua/research/export.lua

return function()

  local title = vim.fn.matchstr(vim.fn.getline(1), "^#\\s\\+\\zs.*")

  if title == "" then title = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":t:r") end

  local slug = title:gsub("%s+", "-"):gsub("[^%w%-]", ""):lower()

  local out = vim.fn.expand("./out/" .. slug)

  vim.fn.mkdir(out, "p")

  local src = vim.api.nvim_buf_get_name(0)

  vim.fn.writefile(vim.api.nvim_buf_get_lines(0, 0, -1, false), out .. "/note.md")

  local runblocks = vim.fn.stdpath("state") .. "/runblocks"

  if vim.fn.isdirectory(runblocks) == 1 then vim.fn.system({ "cp", "-n", runblocks .. "/*", out }, nil) end

  vim.notify("Exported to " .. out)

end

Verification: Running the export function creates out//note.md and copies any runblocks outputs if present. Success criteria: repeated exports are idempotent, existing files are not overwritten silently, and teammates can open the folder to review the note with associated outputs.

Numbered steps to verify the end to end flow quickly help you stabilize habits. First, call the daily note command and capture a thought. Second, insert a fenced code block and run it to append results. Third, use the headings and grep pickers to retrieve multiple related notes by title or tag. Fourth, export the current note and inspect the out folder. If all four steps complete within two minutes without leaving Neovim, you have a sustainable research loop.

Recap: with Markdown structure, gentle LSP, block execution, and predictable exports, your data and notes now live in a searchable, reproducible flow that scales from quick snippets to full research briefs. Next you will add a language agnostic test harness that validates changes across stacks in a single keystroke.

15.4 LANGUAGE-AGNOSTIC TEST HARNESS AND QUICK QA [CODE]

Projects mix languages, frameworks, and build tools, yet your validation ritual should feel uniform and fast. This subchapter adds a language agnostic test harness that detects the project type, picks the right runner, streams results into diagnostics and quickfix, and saves machine readable reports for CI. The UI remains consistent: one key to run tests for the current file or project, one picker to rerun recent tasks, and a small status indicator that shows pass, fail, or flaky. Lua glue keeps the rules lightweight and transparent so you can extend them as your stack evolves.

Begin with a set of detectors that map repository hints to commands. The harness checks for package.json, pytest.ini, go.mod, Cargo.toml, Makefile targets, or a justfile. It then creates well named tasks exposed through Telescope. To avoid surprises, every command runs in a login shell with a fixed working directory and a limited environment so runs resemble CI. Logs and reports are written beneath stdpath(“state”) with timestamps for auditability. Diagnostics for common formats like Python traceback or Jest failures are parsed into quickfix to enable next and previous motions.

LUA

-- ~/.config/nvim/lua/qa/harness.lua

local M = {}

local uv = vim.loop

local detectors = {

  { name = "node-jest",  file = "package.json", cmd = "npm test --silent -- --coverage=false" },

  { name = "node-vitest", file = "vitest.config.*", cmd = "npx vitest run --reporter=basic" },

  { name = "python-pytest", file = "pytest.ini", cmd = "pytest -q" },

  { name = "python-tox", file = "tox.ini", cmd = "tox -q" },

  { name = "go", file = "go.mod", cmd = "go test ./... -count=1" },

  { name = "rust", file = "Cargo.toml", cmd = "cargo test --all --quiet" },

  { name = "make", file = "Makefile", cmd = "make test" },

  { name = "just", file = "justfile", cmd = "just test" },

}

local function exists(p) return vim.fn.glob(p) ~= "" end

local function detect_root()

  local root = vim.fs.root(0, { ".git", "pyproject.toml", "go.mod", "Cargo.toml", "package.json" })

  return root or vim.fn.getcwd()

end

local function choose()

  local root = detect_root()

  for _, d in ipairs(detectors) do

    if exists(root .. "/" .. d.file) then return root, d end

  end

  return root, { name = "fallback", cmd = "echo 'No tests detected'" }

end

local function timestamp() return os.date("%Y%m%d-%H%M%S") end

local function run(cmd, root)

  local outdir = vim.fn.stdpath("state") .. "/qa/" .. timestamp()

  vim.fn.mkdir(outdir, "p")

  local logfile = outdir .. "/run.log"

  local handle = io.open(logfile, "w")

  local job = vim.system({ "bash", "-lc", cmd }, { cwd = root, text = true }, function(res)

    handle:close()

    if res.code == 0 then

      vim.notify("QA passed: " .. cmd, vim.log.levels.INFO)

    else

      vim.notify("QA failed: " .. cmd, vim.log.levels.ERROR)

    end

    vim.fn.setqflist({}, " ", { title = "QA", lines = vim.fn.readfile(logfile) })

    vim.cmd("cwindow")

  end)

  job:wait(0)

  job:chansend(nil)

  job:on_stdout(function(_, data) if data then handle:write(table.concat(data, "\n")) end end)

  job:on_stderr(function(_, data) if data then handle:write(table.concat(data, "\n")) end end)

  return outdir, logfile

end

function M.run_project()

  local root, d = choose()

  local outdir, log = run(d.cmd, root)

  vim.g.qa_last = { cmd = d.cmd, root = root, outdir = outdir, log = log }

end

function M.run_file()

  local root, d = choose()

  local file = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), ":.")

  local cmd = d.name == "python-pytest" and ("pytest -q " .. file) or

              d.name == "node-jest" and ("npx jest " .. file) or

              d.name == "go" and ("go test " .. file) or

              d.name == "rust" and ("cargo test " .. file) or d.cmd

  local outdir, log = run(cmd, root)

  vim.g.qa_last = { cmd = cmd, root = root, outdir = outdir, log = log }

end

function M.rerun_last()

  if not vim.g.qa_last then return vim.notify("No QA history", vim.log.levels.WARN) end

  local last = vim.g.qa_last

  local outdir, log = run(last.cmd, last.root)

  vim.g.qa_last = { cmd = last.cmd, root = last.root, outdir = outdir, log = log }

end

return M

Verification: Running require(“qa.harness”).run_project detects the repository and executes a sensible test command, streaming output to a timestamped log under stdpath(“state”)/qa/YYYYMMDD-HHMMSS and populating the quickfix list. Success criteria: cwindow opens with results, next and previous quickfix motions jump through failures, and rerun_last repeats the previous command reliably.

Expose the harness through a small Telescope picker and stable keymaps. This keeps the workflow discoverable for you and collaborators, especially when switching stacks throughout the day. Bindings should not collide with project mappings and must be easy to remember under your leader prefix.

LUA

-- ~/.config/nvim/lua/qa/ui.lua

local pickers, finders, conf = require("telescope.pickers"), require("telescope.finders"), require("telescope.config").values

local actions, state = require("telescope.actions"), require("telescope.actions.state")

local harness = require("qa.harness")

local function picker()

  local items = {

    { "Run Project Tests", harness.run_project },

    { "Run File Tests", harness.run_file },

    { "Rerun Last QA", harness.rerun_last },

  }

  pickers.new({}, {

    prompt_title = "QA",

    finder = finders.new_table({

      results = items,

      entry_maker = function(e) return { ordinal = e[1], display = e[1], value = e[2] } end

    }),

    sorter = conf.generic_sorter({}),

    attach_mappings = function(_, map)

      local go = function(bufnr) local sel = state.get_selected_entry(); actions.close(bufnr); sel.value() end

      map("i","<CR>",go); map("n","<CR>",go); return true

    end,

  }):find()

end

vim.keymap.set("n", "<leader>qp", picker, { desc = "QA picker" })

vim.keymap.set("n", "<leader>qq", harness.rerun_last, { desc = "Rerun last QA" })

Verification: Pressing your leader key followed by qp opens a QA picker with three options. Selecting an item executes the action and opens quickfix on completion. Success criteria: keys are reported by which-key if installed, the picker is responsive, and rerun works even after a restart by reading the most recent log path.

Parsing failures into diagnostics makes navigation faster. Add a simple pattern matcher for common stacks and push virtual diagnostics into the current buffer for file scoped runs. Keep the parser minimal and tolerant to avoid false positives, and always allow clearing.

LUA

-- ~/.config/nvim/lua/qa/diagnostics.lua

local M = {}

local ns = vim.api.nvim_create_namespace("qa")

local patterns = {

  python = { "^%f[%w]([^:]+):(%d+):", "AssertionError" },

  jest = { "at ([^:]+):(\\d+):(\\d+)", "FAIL " },

  go = { "^%?%s+([^%s]+)%s+%[no test files%]$", "^--- FAIL: " },

}

function M.clear(bufnr) vim.diagnostic.reset(ns, bufnr or 0) end

function M.from_quickfix(bufnr)

  bufnr = bufnr or 0

  M.clear(bufnr)

  local diags = {}

  for _, q in ipairs(vim.fn.getqflist()) do

    if q.bufnr == bufnr and q.lnum > 0 then

      table.insert(diags, { lnum = q.lnum - 1, col = math.max(0, q.col - 1), message = q.text, severity = vim.diagnostic.severity.ERROR })

    end

  end

  vim.diagnostic.set(ns, bufnr, diags, { virtual_text = true })

end

return M

Verification: After running file tests, calling require(“qa.diagnostics”).from_quickfix applies inline diagnostics for errors listed in quickfix. Success criteria: red virtual text appears near failing lines, :diagnostic reset removes them, and navigation with ]d and [d visits every failure.

Sometimes you need a single command that CI can reuse. Provide a slim shell wrapper that calls your harness for project tests and prints a single PASS or FAIL line at the end with an exit status that matches. This keeps pre push hooks or scheduled tasks simple while preserving detailed logs under state paths.

BASH

# ~/.local/bin/qa-project.sh

set -euo pipefail

nvim --headless +"lua require('qa.harness').run_project()" +qa

LOGROOT="${XDG_STATE_HOME:-$HOME/.local/state}/qa"

LAST="$(ls -1dt "$LOGROOT"/* | head -n1)/run.log"

if grep -qE "(^FAIL|^Error|^Assertion)" "$LAST"; then

  echo "FAIL"

  exit 1

else

  echo "PASS"

fi

Verification: Running qa-project.sh from the repository root executes the harness headlessly, updates the latest log, and prints PASS or FAIL with a matching exit code. Success criteria: Git pre push hooks can rely on it, CI pipelines can call it without environment hacks, and logs persist for inspection.

A quick numbered ritual cements the practice. First, open a repository and press your QA picker binding to run project tests. Second, open a failing file and run file tests to focus. Third, apply diagnostics from quickfix and navigate failures with next and previous. Fourth, rerun last to confirm the fix, then push with confidence. If the four steps complete within a minute on a warm repo, the harness is doing its job, and your editor and CI are aligned.

Recap: you now have a portable QA lens that detects stacks, runs the right commands, surfaces failures cleanly, and leaves a trail of logs both for your future self and for automation. With this in place, every change passes through a consistent gate, no matter the language or the machine, and your Neovim setup remains the center of gravity for fast, reliable development.

CHAPTER 16: REMOTE AND CROSS-PLATFORM WORK  

INTRODUCTION

Remote development becomes real the moment your code, tools, and secrets cross machines and networks. The stakes are measurable: you want identical keymaps, the same Telescope muscle memory, and predictable performance whether you attach over SSH, drop into a container, or work inside Windows Subsystem for Linux. In this chapter you will wire a reproducible setup that travels with you. Subchapter 16.1 focuses on SSH, containers, and WSL with a single source of truth for your Neovim configuration, plus helper commands that choose the right runtime transparently [CODE]. Subchapter 16.2 turns to filesystems, synchronization, and latency. You will add safe sync scripts, remote aware pickers, and caching knobs that keep editing snappy even on high latency links [CODE]. Subchapter 16.3 compares GUI clients like Neovide and other UIs, adding practical accessibility settings so long sessions remain comfortable without sacrificing speed. Outcomes include a versioned ~/.config/nvim that bootstraps cleanly on Linux, macOS, and WSL, a remote first workflow that respects secrets, and verified export paths so logs, caches, and sessions land in sensible places. Every recipe here prefers UI first control through Telescope and status cues, with small Lua helpers only where they reduce toil. By the end, you will connect to any machine, container, or WSL distro and feel at home within seconds.

16.1 SSH, CONTAINERS, AND WSL WITH CONSISTENT CONFIGS

Remote work breaks down when configurations drift, paths change, or the wrong binaries load. This subchapter unifies your environment across SSH hosts, containers, and WSL so Neovim feels identical everywhere. The flow stays UI first: a Telescope picker starts remote shells, attaches to containers, or opens WSL paths, while a few compact Lua helpers set runtime directories, cache boundaries, and adapters for platform quirks. Your goal is simple and high stakes: clone, open, and work with the same keys and diagnostics on any target within two minutes.

Begin by standardizing config distribution. Keep ~/.config/nvim version controlled and avoid host specific branches. Instead, detect the platform at runtime and switch paths for cache and state. This keeps your repo clean and avoids merge noise when Linux and Windows differ. For SSH, ensure your ~/.ssh/config defines short Host aliases, multiplexing options, and a stable IdentityFile. For containers, prefer a single entrypoint that mounts your working directory read write and shares a stable HOME override so Neovim’s cache is not discarded unless you intend it. In WSL, remember that Windows filesystems have different performance characteristics; edit inside the Linux filesystem for speed and use explicit sync scripts for exports.

Add a small environment detector that sets stdpath targets and safe shell defaults. The detector handles three axes: Unix vs Windows, native vs WSL, and local vs remote container. The result is deterministic cache placement and fewer path surprises.

LUA

-- ~/.config/nvim/lua/remote/env.lua

local M = {}

local function is_wsl() return vim.fn.has("wsl") == 1 end

local function is_win() return vim.loop.os_uname().version:match("Windows") ~= nil end

local function in_container() return vim.fn.isdirectory("/.dockerenv") == 1 or vim.fn.isdirectory("/run/.containerenv") == 1 end

function M.platform()

  return {

    win = is_win(),

    wsl = is_wsl(),

    container = in_container(),

    hostname = vim.loop.os_gethostname(),

    ssh = vim.env.SSH_CONNECTION ~= nil,

  }

end

function M.apply()

  local p = M.platform()

  if p.win and not p.wsl then

    vim.o.shell = "powershell.exe"

  else

    vim.o.shell = "/bin/bash"

  end

  -- Container caches live under /work/.cache if writable, else default

  if p.container and vim.fn.isdirectory("/work") == 1 then

    vim.env.XDG_CACHE_HOME = "/work/.cache"

    vim.env.XDG_STATE_HOME = "/work/.state"

    vim.env.XDG_DATA_HOME  = "/work/.local/share"

  end

  -- WSL clipboard and paths

  if p.wsl then

    vim.g.clipboard = { name = "WslClipboard", copy = { ["+"] = "clip.exe", ["*"] = "clip.exe" }, paste = { ["+"] = "powershell.exe -NoProfile -Command Get-Clipboard", ["*"] = "powershell.exe -NoProfile -Command Get-Clipboard" }, cache_enabled = 0 }

  end

end

return M

Verification: Requiring remote.env and calling apply sets a correct shell and remaps cache dirs inside containers. On WSL, system clipboard works through Windows tools without errors. Success criteria: :echo stdpath values resolve to expected locations, :messages shows no warnings, and new cache folders appear on first run.

Expose an interactive picker that connects to common targets. For SSH, it should call your system ssh respecting multiplexing. For containers, it should enumerate running container names and exec into them. For WSL, it should list available distributions and launch a shell within. The picker logs commands, so you can reproduce a session later or diagnose failures.

LUA

-- ~/.config/nvim/lua/remote/picker.lua

local pickers, finders, conf = require("telescope.pickers"), require("telescope.finders"), require("telescope.config").values

local actions, action_state = require("telescope.actions"), require("telescope.actions.state")

local function items()

  local opts = {}

  -- Add your hosts here or parse ~/.ssh/config for Host entries

  table.insert(opts, { "ssh prod", "ssh prod" })

  table.insert(opts, { "ssh staging", "ssh staging" })

  -- Docker containers (simple example via `docker ps --format`)

  local ok, list = pcall(vim.fn.systemlist, "docker ps --format '{{.Names}}'")

  if ok then

    for _, name in ipairs(list) do

      if name ~= "" then table.insert(opts, { "docker exec " .. name, "docker exec -it " .. name .. " bash" }) end

    end

  end

  -- WSL distros

  if vim.fn.has("wsl") == 0 and vim.fn.executable("wsl.exe") == 1 then

    local dl = vim.fn.systemlist('wsl.exe -l -q')

    for _, d in ipairs(dl) do

      if d ~= "" then table.insert(opts, { "wsl:" .. d, 'wsl.exe -d "' .. d .. '" bash -l' }) end

    end

  end

  return opts

end

return function()

  local results = items()

  pickers.new({}, {

    prompt_title = "Remote targets",

    finder = finders.new_table({

      results = results,

      entry_maker = function(e) return { ordinal = e[1], display = e[1], value = e[2] } end

    }),

    sorter = conf.generic_sorter({}),

    attach_mappings = function(_, map)

      local run = function(bufnr)

        local entry = action_state.get_selected_entry()

        actions.close(bufnr)

        vim.fn.jobstart(entry.value, { detach = true })

        vim.notify("Launched: " .. entry.value)

      end

      map("i", "<CR>", run); map("n", "<CR>", run)

      return true

    end,

  }):find()

end

Verification: Running the picker lists SSH aliases, containers, and WSL distros. Selecting an item starts the external terminal session and returns control to Neovim instantly. Success criteria: jobstart returns a PID, notification shows a command, and you can attach to the new shell with your terminal multiplexer.

Finally, prepare a container friendly entrypoint that guarantees tools and Neovim plugins install predictably. Keep it minimal and idempotent so cold containers warm quickly and CI images stay small.

BASH

# dev.sh - start container with stable HOME and mounts

set -euo pipefail

IMG="${IMG:-dev:latest}"

WORK="${WORK:-$PWD}"

docker run --rm -it \

  -v "$WORK":"$WORK" \

  -w "$WORK" \

  -v "$HOME/.cache/nvim":"/work/.cache/nvim" \

  -v "$HOME/.local/share/nvim":"/work/.local/share/nvim" \

  -v "$HOME/.config/nvim":"/root/.config/nvim:ro" \

  -e XDG_CACHE_HOME="/work/.cache" \

  -e XDG_STATE_HOME="/work/.state" \

  -e XDG_DATA_HOME="/work/.local/share" \

  "$IMG" bash -lc "nvim"

Verification: Running dev.sh opens Neovim inside the container with your read only config and writable caches. Plugins install once and persist via bind mounts. Success criteria: :checkhealth passes, :Lazy shows previously installed plugins, and editing latency matches local.

With platform detection, remote launching, and container persistence in place, you can jump between hosts confidently. Next you will tackle filesystems, synchronization, and latency to keep edits fast and safe.

16.2 REMOTE FILESYSTEMS, SYNC STRATEGIES, AND LATENCY TIPS

Remote filesystems are a performance trap when mounted carelessly. Your stakes are clear: avoid keystroke lag, prevent partial writes, and keep conflicts out of version history. This subchapter equips you with safe, explicit sync and caching strategies plus editor features that hide latency. The UI stays in control through Telescope pickers for sync tasks and a statusline indicator that shows last sync age and the current mode, live or offline.

Prefer editing where compute happens. Over SSH, open Neovim directly on the remote host. For containers, live inside the container shell. When you must mount, choose read write only for small working sets and avoid traversing huge node modules or virtual environments over network shares. For Windows and WSL, place active repos under the Linux filesystem and stage exports into Windows paths via explicit scripts. This reduces metadata chatter and keeps Treesitter and LSP responsive.

Add a sync helper that stages changed files and avoids surprise deletes. It computes a predictable log path, uses checksums, sets sane excludes, and requires an explicit direction. You will call it through a picker with choices like push, pull, and check.

BASH

# ~/.config/nvim/sync/rsync-safe.sh

set -euo pipefail

if [ $# -lt 2 ]; then echo "usage: rsync-safe.sh push|pull HOST:PATH"; exit 1; fi

MODE="$1"; TARGET="$2"

ROOT="${ROOT:-$PWD}"

LOGDIR="${XDG_STATE_HOME:-$HOME/.local/state}/sync"

mkdir -p "$LOGDIR"

LOG="$LOGDIR/$(date +%Y%m%d-%H%M%S)-$MODE.log"

EXCLUDES="--exclude .git --exclude node_modules --exclude .venv --exclude .mypy_cache --exclude .pytest_cache"

COMMON="-az --delete --partial --mkpath --info=NAME,PROGRESS2 --checksum $EXCLUDES"

if [ "$MODE" = "push" ]; then

  rsync $COMMON "$ROOT"/ "$TARGET"/ | tee "$LOG"

elif [ "$MODE" = "pull" ]; then

  rsync $COMMON "$TARGET"/ "$ROOT"/ | tee "$LOG"

else

  echo "unknown mode: $MODE" && exit 2

fi

echo "log:$LOG"

Verification: Running rsync safe with push or pull copies only changed files, honors excludes, writes a timestamped log under state/sync, and exits noninteractively. Success criteria: a second run reports zero changes, large folders like node modules remain untouched, and accidental reverse syncs cannot happen without spelling the mode.

Wrap the script in a Telescope picker and add a statusline function that shows the last sync delta. The picker reads a small config file with common targets so you do not retype paths. When a sync finishes, a notification and the statusline delta update together.

LUA

-- ~/.config/nvim/lua/remote/sync.lua

local pickers, finders, conf = require("telescope.pickers"), require("telescope.finders"), require("telescope.config").values

local actions, state = require("telescope.actions"), require("telescope.actions.state")

local M = {}

local function targets()

  local file = ".sync_targets"

  local out = {}

  if vim.fn.filereadable(file) == 1 then

    for _, line in ipairs(vim.fn.readfile(file)) do

      local trimmed = line:gsub("^%s+", ""):gsub("%s+$", "")

      if trimmed ~= "" and not trimmed:match("^#") then table.insert(out, trimmed) end

    end

  end

  return out

end

local function run(mode, target)

  local cmd = { vim.fn.expand("~/.config/nvim/sync/rsync-safe.sh"), mode, target }

  local t0 = vim.loop.hrtime()

  vim.fn.jobstart(cmd, {

    stdout_buffered = true,

    on_stdout = function(_, data) if data then vim.fn.writefile(data, vim.fn.stdpath("state").."/sync/last.log", "a") end end,

    on_exit = function(_, code)

      local ms = math.floor((vim.loop.hrtime() - t0)/1e6)

      vim.g.last_sync_ms = ms; vim.g.last_sync_ok = code == 0

      vim.schedule(function()

        local lvl = code == 0 and vim.log.levels.INFO or vim.log.levels.ERROR

        vim.notify(string.format("Sync %s exit %d in %dms", mode, code, ms), lvl)

      end)

    end

  })

end

function M.pick()

  local list = {}

  for _, t in ipairs(targets()) do

    table.insert(list, { "Push → " .. t, function() run("push", t) end })

    table.insert(list, { "Pull ← " .. t, function() run("pull", t) end })

  end

  pickers.new({}, {

    prompt_title = "Sync",

    finder = finders.new_table({

      results = list,

      entry_maker = function(e) return { ordinal = e[1], display = e[1], value = e[2] } end

    }),

    sorter = conf.generic_sorter({}),

    attach_mappings = function(_, map)

      local go = function(bufnr) local sel = state.get_selected_entry(); actions.close(bufnr); sel.value() end

      map("i", "<CR>", go); map("n", "<CR>", go); return true

    end,

  }):find()

end

return M

Verification: Creating a .sync_targets file with HOST:PATH entries enables the picker. Choosing Push or Pull runs rsync safe, writes logs, and updates last sync status. Success criteria: repeated pushes become no ops, notifications reflect exit codes, and the log contains file lists.

Latency hides in many corners. Reduce chattiness by lowering update times, disabling heavy features on remote TTYs, and deferring Treesitter on very large files. Add a guard that checks SSH mode and turns off nonessential animations or frequent status polling. Favor asynchronous format on save with short timeouts so network hiccups do not block inserts.

LUA

-- ~/.config/nvim/lua/remote/latency.lua

return function()

  if vim.env.SSH_CONNECTION then

    vim.o.updatetime = 800

    vim.o.lazyredraw = true

    vim.cmd("set noshowmode")

    vim.g.semantic_tokens = false

    vim.api.nvim_create_autocmd("BufReadPre", {

      callback = function(args)

        local size = vim.fn.getfsize(args.file)

        if size > 512 * 1024 then vim.cmd("TSBufDisable highlight") end

      end,

    })

  end

end

Verification: Over SSH, cursor hold events fire less often, screen redraws drop during macros, mode display moves to statusline, and large files avoid Treesitter highlighting. Success criteria: reduced CPU on the remote host during continuous edits and smoother typing on high latency links.

With explicit sync, remote aware settings, and latency guards, remote edits stay smooth and safe. The final subchapter surveys GUI clients and accessibility to keep long sessions comfortable anywhere.

16.3 GUI OPTIONS (NEOVIDE, UIS) AND ACCESSIBILITY

Graphical front ends can improve comfort, legibility, and input handling without changing how Neovim behaves. Your stakes are sustained focus and reduced fatigue during long sessions, especially on high DPI displays or when you rely on accessibility features. This subchapter compares Neovide and several UIs at a practical level and then captures a small set of settings that travel with your config while remaining harmless in TUI sessions. The guiding principle is identical behavior across terminals and GUIs, with GUIs layering only rendering and input improvements.

Neovide provides a faithful GUI with GPU acceleration, animation controls, and font ligatures. It reads ginit.vim or equivalent Lua hooks at startup, so you can place GUI specific options there and leave init.lua platform neutral. Start by picking a monospace font with broad Unicode coverage and clear punctuation. Set line spacing slightly above default to reduce crowding, and enable cursor animation only if it does not distract. For high DPI screens, prefer integer scaling factors that keep columns predictable; avoid fractional zoom that subtly changes wrap widths across sessions.

Input and clipboard behavior differ from terminal sessions. In Neovide you can enable system clipboard integration without external tools and set macOS or Windows specific key chords for copy, paste, and selection if you prefer. Keep your Neovim mappings consistent and let the GUI translate modifiers, rather than inventing GUI only keymaps that break muscle memory. Cursor shape and blinking should reflect Neovim mode so your statusline and caret agree at a glance. If you rely on screen readers or color filters, verify that your theme’s contrast exceeds WCAG AA for code blocks and that diagnostic colors have alternative shapes or underline styles to convey meaning without hue alone.

Cross platform UIs like Goneovim and FVim offer different tradeoffs. Evaluate them with the same checklist: startup latency, frame pacing while scrolling, font rendering clarity, and selection behavior across wrapped lines. Consistency beats novelty. If one GUI behaves differently on selection or wraps, it is better to adjust settings or switch clients than to retrain habits per platform. For WSL users, a GUI launched on Windows that attaches to a WSL server gives crisp rendering and system clipboard without the overhead of remote X servers. Confirm that file paths map correctly and that opening external links launches native browsers without breaking your workflow.

Accessibility deserves deliberate defaults. Increase default font size by a single step across the board and reserve per project increases for documentation heavy work. Enable line numbers, relative numbers for motion, and a subtle cursor line in TUI and GUI alike; set colorcolumn to guide prose line length or code style boundaries. Consider dyslexia friendly fonts for reading heavy days, though test ligatures and punctuation clarity carefully to avoid misreads in code. If you experience eye strain, reduce saturation in your theme, switch to a high contrast palette, and test day and night variants that preserve the same syntax grouping. Add an accessibility profile that toggles these settings with one command and persists to a small JSON file so your choice survives restarts.

Finally, treat the GUI as an enhancement, not a requirement. Keep your config robust in plain terminals over SSH so you are never blocked by missing graphical dependencies. Verify that your init sequence detects GUIs gently and skips GUI only settings in TTY sessions. Export a short checklist with your chosen GUI, font, size, and contrast setting so teammates can reproduce your experience quickly. With these decisions captured, you get a comfortable, durable environment that works in coffee shops, on desktops, and across clouds without unwanted surprises.

CHAPTER 17: PROJECT-SPECIFIC CONFIGS AND SAFETY  

INTRODUCTION

Project work thrives on local context, yet local configuration is also where mistakes, secrets, and drift sneak in. This chapter makes project specific behavior deliberate, auditable, and reversible. You will enable controlled local configs with trust prompts so a repository can propose settings while you stay in charge. You will separate sensitive values from code, adopt safe environment toggles, and write light policies that teammates can follow without friction. Finally, you will produce reproducibility receipts that prove what ran, with which versions, on which machine, and how to rebuild it later.

We begin by turning on project aware configuration through a minimal, opt in flow. You will use a plugin that surfaces clear trust dialogs, a whitelist of allowed files, and sandboxed execution that cannot silently exfiltrate data. The UI comes first so that teammates understand what is happening without reading Lua. Next, we cover secrets management using environment files, system keychains, and encrypted stores. The point is not to make security heavy; it is to make the safe path the fast path and keep credentials out of your repo and your scrollback. Then, we implement environment toggles and a few policy guardrails that prevent common foot guns.

We close with audits, backups, and receipts. You will save lockfiles, machine readable logs, and a short bill of materials next to each release tag. With one command you will reproduce toolchains, plugins, and meaningful editor state. When we finish, a contributor can clone, trust, run, and ship without surprises. Let us enable local configs safely, then add secrets and receipts to complete the loop.

17.1 LOCAL CONFIGS (.NVIMRC.LUA), WHITELISTS, AND TRUST PROMPTS  

Local configuration should feel like a seatbelt: always present, never in the way, and impossible to forget. The stakes are simple. Without project specific config you waste time retyping paths, toggling linters, or explaining ad hoc setup in a README. With unbounded local config you risk silent keymap overrides, unexpected commands, or credential leaks. Our goal is a UI first flow that asks before it acts, shows exactly what will run, and remembers your choice. We will wire a tiny stack: a trust prompting plugin, an explicit whitelist, and a minimal .nvimrc.lua that configures only what the project truly needs.

Start with the mental model. Neovim has long supported per directory configs via the exrc option and the secure setting, but the defaults are conservative and the prompts are terse. We will keep exrc off by default in your global config and rely on a single plugin that discovers project files like .nvimrc.lua or .nvim/init.lua, displays a friendly diff, and persists trust decisions under your state directory. Team members should copy the same pattern so that a repository behaves consistently across machines. The UI will surface three outcomes: trust once, trust always for this path, or decline and continue with your baseline.

Install the helper using lazy.nvim and keep it event driven so startup remains fast. The plugin reads a small manifest in the repo root that lists allowed files. You will configure it to allow only Lua files, deny shell commands, and confine paths to the repository. Your first .nvimrc.lua will set project root detection, formatter preferences, and a couple of keymaps scoped to the repo. If a collaborator wants to add more, they will edit the local file and the plugin will show the diff at next open so you can review changes before accepting.

LUA

-- ~/.config/nvim/lua/plugins/localconfig.lua

return {

  {

    "klen/nvim-config-local",

    event = "BufReadPre",

    opts = {

      -- Look for these files in project roots

      config_files = { ".nvimrc.lua", ".nvimrc", ".nvim/init.lua" },

      -- Store trust decisions here

      hashfile = vim.fn.stdpath("state") .. "/config-local",

      -- Limit execution to files under the detected root

      silent = false,

      lookup_parents = true,

    },

  },

}

Verification: Opening any file under a repository containing .nvimrc.lua triggers a prompt before executing local settings. Success criteria: no local code runs without an explicit trust action; decisions persist in stdpath(“state”)/config-local; startup time remains unchanged on projects without local files.

Your whitelist should be explicit and discoverable. Instead of letting arbitrary code run, you will read a manifest called .nvimrc.allow that lists the relative paths each project may source. The local config reads the manifest, validates that every file exists and lives inside the repository, then sources only those files. This pattern scales to multiple teams because the same manifest can be code reviewed like any other change.

LUA

-- ~/.config/nvim/lua/localtrust.lua

local M = {}

local function repo_root()

  return vim.fs.root(0, { ".git", ".hg", ".svn" }) or vim.loop.cwd()

end

local function allowed_list(root)

  local f = root .. "/.nvimrc.allow"

  if vim.fn.filereadable(f) == 0 then return {} end

  local lines = vim.fn.readfile(f)

  local allow = {}

  for _, line in ipairs(lines) do

    local p = vim.fn.fnamemodify(root .. "/" .. line, ":p")

    if p:sub(1, #root) == root and vim.fn.filereadable(p) == 1 then

      table.insert(allow, p)

    end

  end

  return allow

end

function M.source_allowed()

  local root = repo_root()

  for _, p in ipairs(allowed_list(root)) do

    vim.cmd("luafile " .. p)

  end

end

return M

Verification: Creating .nvimrc.allow with lines such as local/init.lua and local/lsp.lua limits what can be executed. Running :lua require(“localtrust”).source_allowed() loads only those files inside the repo. Success criteria: paths outside the root are ignored, missing files do not throw errors, and the command is idempotent.

Write a minimal .nvimrc.lua that configures behavior without overriding your global identity. Keep decisions small: set the project root policy, register a formatter or two, and define a couple of mappings under a local leader so collisions are unlikely. Document the file with a short header that warns contributors never to store secrets and to avoid shell execution. The trust prompt and whitelist protect you, but good habits prevent unnecessary review noise.

LUA

-- <repo>/.nvimrc.lua

-- Project-local Neovim settings. Reviewed via .nvimrc.allow. No secrets. No shell.

vim.g.maplocalleader = ","

vim.g.root_spec = { ".git", "pyproject.toml", "go.mod", "Cargo.toml", "package.json" }

-- Formatters scoped to this repo

vim.g.project_formatters = { python = "ruff_format", javascript = "prettierd" }

-- Local keymaps

vim.keymap.set("n", "<localleader>t", function()

  print("Project tests would run here")

end, { desc = "Project tests" })

-- Example: project Telescope picker for docs

vim.keymap.set("n", "<localleader>d", function()

  require("telescope.builtin").find_files({ cwd = "docs" })

end, { desc = "Docs files" })

Verification: On first open you see a trust dialog; after acceptance the local leader is set to comma only for this repo, the docs picker works, and no global settings change. Success criteria: outside the repo the mappings do not exist; removing the file yields no errors; a teammate cloning the repo sees the same prompt and behavior.

Finally, teach teammates the two minute ritual. First, open the repository and review the trust prompt, reading .nvimrc.allow in a split. Second, accept trust for this path and commit the manifest if you added new files. Third, press your local leader bindings to confirm they are scoped and useful. Fourth, run git diff to ensure no tracked files changed unintentionally. If all steps are smooth, the project specific configuration is now safe, predictable, and shared. Next we will separate secrets and toggles so local behavior remains powerful without risking credentials or accidental exposure.

17.2 SECRETS MANAGEMENT, ENVIRONMENT TOGGLES, AND POLICIES

Secrets leak through convenience, not malice, so our strategy makes the easy path the safe path. You will keep credentials out of source control, avoid accidental echo in terminals, and ensure Neovim never logs sensitive values. The first rule is location. Store secrets in one of three places depending on use: scoped environment files loaded by a process supervisor, your platform keychain for interactive tools, or an encrypted store committed to the repository for shared automation where authorized teammates can decrypt. The second rule is separation. Code reads environment variables and never hardcodes tokens. The third rule is visibility. Commands and logs redact values and show only the minimum needed context.

Begin with environment files for local runs. Use .env.development, .env.test, and .env.local at the repository root, tracked by .gitignore so they never commit. A simple loader like direnv or a shell profile snippet exports variables when you cd into the project, and removes them when you leave. This keeps your editor and subprocesses consistent with the shell. In Neovim, a small check can warn you if essential variables are missing, pointing you to the right file. When switching branches or working across multiple services, this pattern avoids stale or cross polluted tokens.

For multiuser automation, use an encrypted store. Age or GPG can encrypt a secrets.yaml that includes API keys, connection strings, and machine tokens. Store the encrypted file in the repo and keep the private keys with authorized maintainers or in your CI vault. The policy is simple. Developers decrypt locally only when they need to bootstrap a service; CI decrypts inside its own environment to run tests or deployments. All tooling should read from environment variables after decryption, not from files, and should avoid printing secrets even in verbose mode.

Environment toggles let you flip features safely without code edits. Split behaviors like telemetry, heavy AI completions, or sampling rates into switches such as NVIDE_TELEMETRY=off or NVIM_AI_MODE=local. Teach your local config to read these variables and adapt plugin behavior. A low risk baseline keeps features opt in, then a team can enable them per project or per machine. Provide a small statusline segment that shows active toggles so you never wonder why a feature is quiet or noisy.

Policies must fit on one screen to be followed. Keep a file named POLICY.md next to the project config that states three things. First, the secrets policy with locations and key rotation habits. Second, the logging policy describing what is redacted and how to request debug access. Third, the local configuration policy indicating which files may run, how trust prompts work, and how to propose changes. Include a checklist for new contributors that points to a minimal setup: install direnv, add a key to the keychain, decrypt with one command, and confirm status in Neovim.

Verification habits prevent drift. Add a pre push hook that runs a dry check to ensure no secret patterns appear in the diff, environment files are untracked, and decrypted artifacts are absent. In Neovim, surface the same checks in a quick QA picker so a contributor can run them before committing. Teach new teammates to stash or remove their .env.local when reproducing a bug so you can confirm a failure is not driven by local values. When the editor shows a trust prompt for a changed .nvimrc.lua, require a code review on the manifest alongside the application code that depends on it.

The final ingredient is redaction. Configure your terminal and editor logging to filter values that match common secret shapes, such as 32 character hex strings or words ending with _TOKEN. Your Neovim notifications should truncate long command lines and hide environment values by default. If a developer needs to inspect raw traffic or verbose output, give them an explicit toggle and remind them to disable it afterward. Logs exported during support or audit should already be sanitized to avoid post processing mistakes.

When these habits land, a new hire can clone the repository, accept a clear trust prompt, set up env files guided by POLICY.md, and run tests without exposing credentials. Your editor reflects the environment state with a small indicator rather than a surprise, and your CI reads the same values through the same paths. With secrets handled, we turn to audits, backups, and receipts so work remains reproducible months later.

17.3 AUDITS, BACKUPS, AND REPRODUCIBILITY RECEIPTS

Reproducibility is not a feeling; it is a set of artifacts you can point to and rerun. The aim of this subchapter is to produce small, durable receipts for each project that prove what your editor, plugins, and tools were at the time of a change, plus a one command path to rebuild them. This protects you during handoffs, lets teammates bisect configuration regressions, and satisfies stakeholders who ask how you know that a result can be reproduced.

Start with your editor bill of materials. Lock your plugin versions with your manager’s lockfile and commit it. Capture your Neovim version and build flags using nvim -v, and keep that string in a file under .tooling/nvim.txt. Add Treesitter parser versions, language server versions installed by your manager, and formatter versions in a small manifest that a script regenerates. The principle is that a reviewer can glance at the manifest and see versions without digging through a machine. Run this update on release branches or weekly on main so the cost remains predictable.

Next, capture startup and performance signals. Keep a profile directory and write startuptime logs there after significant changes to your configuration. Retain only a rolling window, such as the last ten runs, so the folder stays small yet trends remain visible. When a teammate reports slower startup or UI jank, ask them to attach their latest profile and lockfile; you can now compare apples to apples. Include a short guide explaining how to run startuptime and where to store the file so the team follows the same process.

Backups must be boring and automatic. Sync your configuration repository to a remote origin and mirror to a secondary host. Exclude secrets and state folders from the public mirror. For local machine state, periodically archive the state directory that holds trust decisions, harness logs, and runblock outputs. Keep this archive encrypted if any sensitive metadata might be present. Document the restore steps: clone the config, install Neovim, copy the state archive, open a project, accept prompts, and verify with a one page checklist.

Receipts combine into a single artifact when you tag a release or ship a feature. Generate a release-notes.md that includes short sections. First, environment summary with OS, Neovim, and CPU. Second, editor bill of materials with plugin count and lockfile hash. Third, toolchain summary with key language tool versions. Fourth, checklist status showing that secrets are sourced from the correct paths, trust prompts are accepted, and QA passed. Save this file next to the tag or within the out folder of your project so it is always easy to find.

Audits should be friendly. When someone asks for proof, open the manifest, show the lockfile, and point to the latest profile. If the question is deeper, such as whether a local config changed a security setting, show the diff captured by your trust plugin when the manifest changed, and the code review that approved it. Keep sensitive details redacted by default and offer a secure path for reviewers to access raw logs if policy allows. Encourage the team to ask audit style questions during reviews so the habit remains steady, not punitive.

Finally, rehearse recovery. Twice a year run a fresh machine drill. Pretend your laptop was replaced. Install Neovim, clone your config, open a known repository, accept the trust prompt, restore env files, and run the project tests. Time the process and update the checklist where it was slow or surprising. If you cannot rebuild in under an hour without searching chat logs, your receipts are not sufficient and need to improve.

With receipts, backups, and friendly audits in place, you have closed the loop. A teammate or future you can replay exactly what the project needed, understand why it behaved a certain way, and move forward with confidence. Your editor remains a reliable instrument rather than a collection of guesses, and the path from clone to ship stays short and safe.

CHAPTER 18: FINALIZE, BACKUP, AND SHARE  

INTRODUCTION

Project work thrives on local context, yet local configuration is also where mistakes, secrets, and drift sneak in. This chapter makes project specific behavior deliberate, auditable, and reversible. You will enable controlled local configs with trust prompts so a repository can propose settings while you stay in charge. You will separate sensitive values from code, adopt safe environment toggles, and write light policies that teammates can follow without friction. Finally, you will produce reproducibility receipts that prove what ran, with which versions, on which machine, and how to rebuild it later.

We begin by turning on project aware configuration through a minimal, opt in flow. You will use a plugin that surfaces clear trust dialogs, a whitelist of allowed files, and sandboxed execution that cannot silently exfiltrate data. The UI comes first so that teammates understand what is happening without reading Lua. Next, we cover secrets management using environment files, system keychains, and encrypted stores. The point is not to make security heavy; it is to make the safe path the fast path and keep credentials out of your repo and your scrollback. Then, we implement environment toggles and a few policy guardrails that prevent common foot guns.

We close with audits, backups, and receipts. You will save lockfiles, machine readable logs, and a short bill of materials next to each release tag. With one command you will reproduce toolchains, plugins, and meaningful editor state. When we finish, a contributor can clone, trust, run, and ship without surprises. Let us enable local configs safely, then add secrets and receipts to complete the loop.

17.1 LOCAL CONFIGS (.NVIMRC.LUA), WHITELISTS, AND TRUST PROMPTS

Local configuration should feel like a seatbelt: always present, never in the way, and impossible to forget. The stakes are simple. Without project specific config you waste time retyping paths, toggling linters, or explaining ad hoc setup in a README. With unbounded local config you risk silent keymap overrides, unexpected commands, or credential leaks. Our goal is a UI first flow that asks before it acts, shows exactly what will run, and remembers your choice. We will wire a tiny stack: a trust prompting plugin, an explicit whitelist, and a minimal .nvimrc.lua that configures only what the project truly needs.

Start with the mental model. Neovim has long supported per directory configs via the exrc option and the secure setting, but the defaults are conservative and the prompts are terse. We will keep exrc off by default in your global config and rely on a single plugin that discovers project files like .nvimrc.lua or .nvim/init.lua, displays a friendly diff, and persists trust decisions under your state directory. Team members should copy the same pattern so that a repository behaves consistently across machines. The UI will surface three outcomes: trust once, trust always for this path, or decline and continue with your baseline.

Install the helper using lazy.nvim and keep it event driven so startup remains fast. The plugin reads a small manifest in the repo root that lists allowed files. You will configure it to allow only Lua files, deny shell commands, and confine paths to the repository. Your first .nvimrc.lua will set project root detection, formatter preferences, and a couple of keymaps scoped to the repo. If a collaborator wants to add more, they will edit the local file and the plugin will show the diff at next open so you can review changes before accepting.

LUA

-- ~/.config/nvim/lua/plugins/localconfig.lua

return {

  {

    "klen/nvim-config-local",

    event = "BufReadPre",

    opts = {

      -- Look for these files in project roots

      config_files = { ".nvimrc.lua", ".nvimrc", ".nvim/init.lua" },

      -- Store trust decisions here

      hashfile = vim.fn.stdpath("state") .. "/config-local",

      -- Limit execution to files under the detected root

      silent = false,

      lookup_parents = true,

    },

  },

}

Verification: Opening any file under a repository containing .nvimrc.lua triggers a prompt before executing local settings. Success criteria: no local code runs without an explicit trust action; decisions persist in stdpath(“state”)/config-local; startup time remains unchanged on projects without local files.

Your whitelist should be explicit and discoverable. Instead of letting arbitrary code run, you will read a manifest called .nvimrc.allow that lists the relative paths each project may source. The local config reads the manifest, validates that every file exists and lives inside the repository, then sources only those files. This pattern scales to multiple teams because the same manifest can be code reviewed like any other change.

LUA

-- ~/.config/nvim/lua/localtrust.lua

local M = {}

local function repo_root()

  return vim.fs.root(0, { ".git", ".hg", ".svn" }) or vim.loop.cwd()

end

local function allowed_list(root)

  local f = root .. "/.nvimrc.allow"

  if vim.fn.filereadable(f) == 0 then return {} end

  local lines = vim.fn.readfile(f)

  local allow = {}

  for _, line in ipairs(lines) do

    local p = vim.fn.fnamemodify(root .. "/" .. line, ":p")

    if p:sub(1, #root) == root and vim.fn.filereadable(p) == 1 then

      table.insert(allow, p)

    end

  end

  return allow

end

function M.source_allowed()

  local root = repo_root()

  for _, p in ipairs(allowed_list(root)) do

    vim.cmd("luafile " .. p)

  end

end

return M

Verification: Creating .nvimrc.allow with lines such as local/init.lua and local/lsp.lua limits what can be executed. Running :lua require(“localtrust”).source_allowed() loads only those files inside the repo. Success criteria: paths outside the root are ignored, missing files do not throw errors, and the command is idempotent.

Write a minimal .nvimrc.lua that configures behavior without overriding your global identity. Keep decisions small: set the project root policy, register a formatter or two, and define a couple of mappings under a local leader so collisions are unlikely. Document the file with a short header that warns contributors never to store secrets and to avoid shell execution. The trust prompt and whitelist protect you, but good habits prevent unnecessary review noise.

LUA

-- <repo>/.nvimrc.lua

-- Project-local Neovim settings. Reviewed via .nvimrc.allow. No secrets. No shell.

vim.g.maplocalleader = ","

vim.g.root_spec = { ".git", "pyproject.toml", "go.mod", "Cargo.toml", "package.json" }

-- Formatters scoped to this repo

vim.g.project_formatters = { python = "ruff_format", javascript = "prettierd" }

-- Local keymaps

vim.keymap.set("n", "<localleader>t", function()

  print("Project tests would run here")

end, { desc = "Project tests" })

-- Example: project Telescope picker for docs

vim.keymap.set("n", "<localleader>d", function()

  require("telescope.builtin").find_files({ cwd = "docs" })

end, { desc = "Docs files" })

Verification: On first open you see a trust dialog; after acceptance the local leader is set to comma only for this repo, the docs picker works, and no global settings change. Success criteria: outside the repo the mappings do not exist; removing the file yields no errors; a teammate cloning the repo sees the same prompt and behavior.

Finally, teach teammates the two minute ritual. First, open the repository and review the trust prompt, reading .nvimrc.allow in a split. Second, accept trust for this path and commit the manifest if you added new files. Third, press your local leader bindings to confirm they are scoped and useful. Fourth, run git diff to ensure no tracked files changed unintentionally. If all steps are smooth, the project specific configuration is now safe, predictable, and shared. Next we will separate secrets and toggles so local behavior remains powerful without risking credentials or accidental exposure.

17.2 SECRETS MANAGEMENT, ENVIRONMENT TOGGLES, AND POLICIES

Secrets leak through convenience, not malice, so our strategy makes the easy path the safe path. You will keep credentials out of source control, avoid accidental echo in terminals, and ensure Neovim never logs sensitive values. The first rule is location. Store secrets in one of three places depending on use: scoped environment files loaded by a process supervisor, your platform keychain for interactive tools, or an encrypted store committed to the repository for shared automation where authorized teammates can decrypt. The second rule is separation. Code reads environment variables and never hardcodes tokens. The third rule is visibility. Commands and logs redact values and show only the minimum needed context.

Begin with environment files for local runs. Use .env.development, .env.test, and .env.local at the repository root, tracked by .gitignore so they never commit. A simple loader like direnv or a shell profile snippet exports variables when you cd into the project, and removes them when you leave. This keeps your editor and subprocesses consistent with the shell. In Neovim, a small check can warn you if essential variables are missing, pointing you to the right file. When switching branches or working across multiple services, this pattern avoids stale or cross polluted tokens.

For multiuser automation, use an encrypted store. Age or GPG can encrypt a secrets.yaml that includes API keys, connection strings, and machine tokens. Store the encrypted file in the repo and keep the private keys with authorized maintainers or in your CI vault. The policy is simple. Developers decrypt locally only when they need to bootstrap a service; CI decrypts inside its own environment to run tests or deployments. All tooling should read from environment variables after decryption, not from files, and should avoid printing secrets even in verbose mode.

Environment toggles let you flip features safely without code edits. Split behaviors like telemetry, heavy AI completions, or sampling rates into switches such as NVIDE_TELEMETRY=off or NVIM_AI_MODE=local. Teach your local config to read these variables and adapt plugin behavior. A low risk baseline keeps features opt in, then a team can enable them per project or per machine. Provide a small statusline segment that shows active toggles so you never wonder why a feature is quiet or noisy.

Policies must fit on one screen to be followed. Keep a file named POLICY.md next to the project config that states three things. First, the secrets policy with locations and key rotation habits. Second, the logging policy describing what is redacted and how to request debug access. Third, the local configuration policy indicating which files may run, how trust prompts work, and how to propose changes. Include a checklist for new contributors that points to a minimal setup: install direnv, add a key to the keychain, decrypt with one command, and confirm status in Neovim.

Verification habits prevent drift. Add a pre push hook that runs a dry check to ensure no secret patterns appear in the diff, environment files are untracked, and decrypted artifacts are absent. In Neovim, surface the same checks in a quick QA picker so a contributor can run them before committing. Teach new teammates to stash or remove their .env.local when reproducing a bug so you can confirm a failure is not driven by local values. When the editor shows a trust prompt for a changed .nvimrc.lua, require a code review on the manifest alongside the application code that depends on it.

The final ingredient is redaction. Configure your terminal and editor logging to filter values that match common secret shapes, such as 32 character hex strings or words ending with _TOKEN. Your Neovim notifications should truncate long command lines and hide environment values by default. If a developer needs to inspect raw traffic or verbose output, give them an explicit toggle and remind them to disable it afterward. Logs exported during support or audit should already be sanitized to avoid post processing mistakes.

When these habits land, a new hire can clone the repository, accept a clear trust prompt, set up env files guided by POLICY.md, and run tests without exposing credentials. Your editor reflects the environment state with a small indicator rather than a surprise, and your CI reads the same values through the same paths. With secrets handled, we turn to audits, backups, and receipts so work remains reproducible months later.

17.3 AUDITS, BACKUPS, AND REPRODUCIBILITY RECEIPTS

Reproducibility is not a feeling; it is a set of artifacts you can point to and rerun. The aim of this subchapter is to produce small, durable receipts for each project that prove what your editor, plugins, and tools were at the time of a change, plus a one command path to rebuild them. This protects you during handoffs, lets teammates bisect configuration regressions, and satisfies stakeholders who ask how you know that a result can be reproduced.

Start with your editor bill of materials. Lock your plugin versions with your manager’s lockfile and commit it. Capture your Neovim version and build flags using nvim -v, and keep that string in a file under .tooling/nvim.txt. Add Treesitter parser versions, language server versions installed by your manager, and formatter versions in a small manifest that a script regenerates. The principle is that a reviewer can glance at the manifest and see versions without digging through a machine. Run this update on release branches or weekly on main so the cost remains predictable.

Next, capture startup and performance signals. Keep a profile directory and write startuptime logs there after significant changes to your configuration. Retain only a rolling window, such as the last ten runs, so the folder stays small yet trends remain visible. When a teammate reports slower startup or UI jank, ask them to attach their latest profile and lockfile; you can now compare apples to apples. Include a short guide explaining how to run startuptime and where to store the file so the team follows the same process.

Backups must be boring and automatic. Sync your configuration repository to a remote origin and mirror to a secondary host. Exclude secrets and state folders from the public mirror. For local machine state, periodically archive the state directory that holds trust decisions, harness logs, and runblock outputs. Keep this archive encrypted if any sensitive metadata might be present. Document the restore steps: clone the config, install Neovim, copy the state archive, open a project, accept prompts, and verify with a one page checklist.

Receipts combine into a single artifact when you tag a release or ship a feature. Generate a release-notes.md that includes short sections. First, environment summary with OS, Neovim, and CPU. Second, editor bill of materials with plugin count and lockfile hash. Third, toolchain summary with key language tool versions. Fourth, checklist status showing that secrets are sourced from the correct paths, trust prompts are accepted, and QA passed. Save this file next to the tag or within the out folder of your project so it is always easy to find.

Audits should be friendly. When someone asks for proof, open the manifest, show the lockfile, and point to the latest profile. If the question is deeper, such as whether a local config changed a security setting, show the diff captured by your trust plugin when the manifest changed, and the code review that approved it. Keep sensitive details redacted by default and offer a secure path for reviewers to access raw logs if policy allows. Encourage the team to ask audit style questions during reviews so the habit remains steady, not punitive.

Finally, rehearse recovery. Twice a year run a fresh machine drill. Pretend your laptop was replaced. Install Neovim, clone your config, open a known repository, accept the trust prompt, restore env files, and run the project tests. Time the process and update the checklist where it was slow or surprising. If you cannot rebuild in under an hour without searching chat logs, your receipts are not sufficient and need to improve.

With receipts, backups, and friendly audits in place, you have closed the loop. A teammate or future you can replay exactly what the project needed, understand why it behaved a certain way, and move forward with confidence. Your editor remains a reliable instrument rather than a collection of guesses, and the path from clone to ship stays short and safe.

CHAPTER 19: CAPSTONE — YOUR PERSONAL IDE BUILD  

INTRODUCTION

The capstone turns everything you learned into a single, opinionated, and reproducible IDE you can trust daily. We focus on clear specifications, explicit constraints, and architecture choices that make your Neovim feel native on day one and predictable on day one hundred. The flow stays UI first with Telescope pickers, statusline cues, and session hygiene while Lua automations carry the heavy work only where they reduce friction or time. You will define outcomes, lock versions, and wire language tools that match your stack. You will prove the build with benchmarks, smoke tests, and regression locks, then write a postmortem that captures risks and a playbook to future proof change.

We begin with 19.1, where you author a specification that lists supported platforms, target Neovim version, plugin boundaries, and what success looks like in measurable terms. You will choose constraints that keep the surface small, declare trusted inputs, and select architecture patterns like feature slices and profile toggles. In 19.2 [CODE], you implement modules and language toolchains, arranging lazy.nvim specs, LSP via mason.nvim, completion through nvim-cmp, formatting with conform or none ls, and DAP stubs that work across languages. In 19.3 [CODE], you create benchmarks, test routes, and regression locks that freeze behavior across releases. Finally, 19.4 closes with a candid postmortem, a risk register, and a maintenance plan that balances speed with stability. The next page begins the build by fixing the spec on paper, so decisions are intentional rather than accidental.

19.1 SPECIFICATION, CONSTRAINTS, AND ARCHITECTURE DECISIONS

Treat your IDE build like a product with a single owner, a roadmap, and a definition of done measured in user time saved. The specification is your contract with yourself and your teammates. It states exactly what you support, what you will not support, and how you will know the build works. Without a spec, every plugin looks tempting and every keymap looks clever. With a spec, you can say no gracefully and move faster. This subchapter places the rails: target versions, success metrics, supported languages, non goals, and the architecture patterns that make extensions safe rather than surprising. We stay UI first and add Lua only where it eliminates drudgery.

Start by writing the scope in one page. Name the Neovim version and the platforms you intend to support across macOS, Linux, and Windows with WSL. List the minimum terminal features you rely on and the GUI options that are allowed, such as Neovide for font smoothing or accessibility. Declare the baseline: Telescope for navigation, a statusline that reflects mode, Git signs for diffs, Treesitter for structure, LSP for language features, and conform or none ls for formatting. If a feature is not on this baseline, it must prove its value with a measurable reduction in keystrokes or latency. This rule keeps the surface tight and keeps support obligations realistic as your team grows.

Next define hard constraints to prevent drift. Limit the plugin count by category, for example one statusline, one file tree, one completion engine, and one snippet engine. Require lazy loading for anything not needed at cold start, with a documented event or command that triggers it. Pin versions through your lockfile and record the hash inside your repository. Enforce deterministic behavior by fixing the same settings for noisy features such as highlights, diagnostics severity, folds, and session save paths. These constraints make performance and debugging boring. They also keep your configuration teachable, which reduces onboarding time and support cost when deadlines loom.

State outcomes in language a non expert can verify. Define time to first edit from a clean machine, cold start time to ready statusline, time to search a large repository with live grep, and time to complete a simple code action in a typical project. Add one functional goal per supported language: type checked save, single command test run, debug breakpoint hit, and an on save formatter with the right style. Tie each outcome to a command sequence and record expected results in a smoke checklist. When you later change a plugin or a keymap, you will rerun these checks and know immediately if you helped or hurt the day to day workflow.

List supported languages and tools explicitly. For a web stack you might include TypeScript with tsserver and eslint, Python with pyright and ruff, and Markdown with marksman and a formatter. For systems work you might include Rust with rust an analyzer, Go with gopls, and shell with bashls. Each language receives one LSP, one formatter, and a small set of textobjects that matter. Decide now whether format on save is enabled globally or per project with a trust prompt. Decide whether diagnostics update in insert mode or only on hold to reduce churn. Writing these choices early removes debates later and keeps daily editing calm and predictable.

Organize the architecture around feature slices rather than one giant file. Create modules for core options, keymaps, UI, Git, navigation, LSP, completion, snippets, formatting, debugging, and project. Within each slice, keep a clear boundary between user interface settings and engine wiring. For example, the UI module defines statusline sections and theme toggles, while the LSP module registers servers and capabilities. Expose small public functions such as toggle diagnostics or switch theme for use in keymaps and commands. This structure keeps mental load low and error surfaces small. It also makes profiling easier because you can disable slices selectively during investigations.

Decide on profiles that map to real situations. A minimal profile runs only core editing plus navigation and Git, ideal for pair sessions or low powered machines. A full profile loads every slice and language toolchain. A preview profile enables experimental features behind a clear warning and can be switched off quickly. Profiles should be simple toggles in your init that set environment flags and selectively import modules. They must be documented in the README with the exact commands to switch and the expected UI cues that confirm which profile is active. Profiles convert opinion into choice without turning your setup into a maze.

Establish ownership safe patterns that protect collaborators. Use local conventions for leader maps and publish them in a help buffer accessible by a command, so discoverability remains high without memorization pressure. Avoid taking globally contentious keys that break muscle memory for common motions. Prefer descriptive commands that can be invoked from Telescope or which key. Provide opt out switches for aggressive behaviors like auto saving or clipboard hijacking. When in doubt, require an explicit keybind rather than background magic. Safety builds trust, and trust turns your configuration from a personal playground into a team multiplier.

Plan for reproducibility from the start. Store your configuration in a versioned repository with a lockfile and an installer script that backs up any prior setup. Keep a fixtures directory with a sample project for each language so smoke tests never depend on the live worktree. Export benchmark logs, health checks, and parser status into an out folder keyed by date and tag. Write a short script that gathers diagnostics into a single artifact for bug reports. Reproducibility reduces fear, and fear is what makes people postpone upgrades until late at night. You want upgrades to be boring and frequent, not rare and heroic.

Design your decision record system so future you remembers why. A lightweight ADR folder holds one page notes for each notable choice: why you picked lazy.nvim, why you chose conform over another formatter, why diagnostics update on hold rather than on insert. Each record ends with a rollback plan and a measurable signal that would cause you to revisit the decision. When a tool improves or a requirement changes, you will know how to adapt without rewriting history. This habit converts learning into durable knowledge and turns your configuration into a living document rather than a tangle of unexplained defaults.

Define integration boundaries up front. External tools such as package managers, container runtimes, and cloud CLIs belong outside the editor but must be accessible inside it. Your spec should state how terminals are invoked, how environment variables are loaded, and how per project overrides are trusted. For remote scenarios via SSH, containers, or WSL, document how file watching, paths, and fonts behave. Avoid silent network calls from the editor; require explicit commands for installs and upgrades. The clearer the boundary, the fewer support tickets you will field about invisible side effects or mysteriously changed files.

Finally, write your acceptance checklist as numbered steps that anyone can follow to confirm the build meets the spec. 1) Clone the repository to ~/.config/nvim. 2) Run the installer with the minimal profile and confirm the statusline loads. 3) Open the sample web project and run live grep to find a string across thousands of files. 4) Open a TypeScript file, trigger completion, accept a suggestion, and save with formatting applied. 5) Jump to definition, open diagnostics, and apply a code action. 6) Run tests from a picker and view quickfix results. 7) Quit and reopen; confirm sessions, theme, and keymaps persist. When each step passes with predictable output, you can declare the build ready to implement.

Close this specification by signing it with a date and a version. Treat the document as the north star for the rest of the capstone. When disagreements arise, return to the spec instead of the latest plugin thread. When constraints feel tight, remember they protect flow and make success measurable. In the next subchapter you will translate the spec into modules and wire language toolchains that honor these boundaries while keeping the editor light, fast, and friendly. The path is clear, the rails are set, and the build moves from words to working code.

19.2 IMPLEMENTING MODULES AND LANGUAGE TOOLCHAINS

Your specification becomes real when modules load cleanly, tools resolve predictably, and UI cues show what is active. We keep the flow UI first with Telescope and statusline confirmations while Lua wiring stays minimal and readable. Each slice owns a single purpose, exposes narrow functions, and is lazy loaded on intent. Language toolchains follow one pattern so TypeScript, Python, Rust, and Go feel native with near zero special cases. You will create the skeleton, add lazy.nvim specs, wire LSP via mason, enable completion through nvim cmp, and set formatters and DAP stubs that respect your constraints and profiles.

Start by creating a folder layout that mirrors your slices. Keep core options and keymaps separate from plugins, and keep language features under a languages directory to make ownership obvious. Place a single loader in init.lua that sets the profile and imports modules with deterministic order. Avoid global state; prefer returning tables of functions so slices compose cleanly. Verify each import with a statusline section or simple notification so you can detect misloads early. A consistent structure shortens onboarding and keeps profiling straightforward when a plugin slows down cold start.

LUA

-- ~/.config/nvim/init.lua

vim.g.mapleader = " "

local profile = vim.env.NVIM_PROFILE or "full"

require("config.options").setup(profile)

require("config.keymaps").setup(profile)

-- plugin manager bootstrap

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", lazypath })

end

vim.opt.rtp:prepend(lazypath)

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

  change_detection = { notify = false },

  performance = { rtp = { disabled_plugins = { "matchit", "gzip" } } }

})

-- slice loaders

require("slices.ui").setup(profile)

require("slices.nav").setup()

require("slices.git").setup()

require("slices.lsp").setup(profile)

require("slices.fmt").setup()

require("slices.dap").setup("minimal")

Verification: On first start, lazy.nvim bootstraps into ~/.local/share/nvim/lazy/lazy.nvim, no errors in :messages, and statusline shows profile=full. Re runs are idempotent. Disabling NVIM_PROFILE sets minimal mode, confirmed by fewer loaded plugins in :Lazy and shorter startup in out/startup.log.

Create a plugins directory that groups specs by slice. Each file returns a list, uses events for lazy loading, and sets dependencies locally. Prefer command or event triggers tied to real intent such as opening a buffer of a certain filetype or invoking Telescope. Keep plugin setup concise and defer heavy configuration to slice modules that export setup functions. This keeps specs readable and keeps complex decisions in one place. Spec files should never reach into unrelated slices, which prevents hidden coupling and simplifies rollbacks.

LUA

-- ~/.config/nvim/lua/plugins/lsp.lua

return {

  { "williamboman/mason.nvim", cmd = { "Mason", "MasonInstall" } },

  { "williamboman/mason-lspconfig.nvim", event = "VeryLazy" },

  { "neovim/nvim-lspconfig", event = { "BufReadPre", "BufNewFile" } },

  { "hrsh7th/nvim-cmp", event = "InsertEnter", dependencies = {

      "hrsh7th/cmp-nvim-lsp", "L3MON4D3/LuaSnip", "saadparwaiz1/cmp_luasnip"

  }},

}

Verification: Running :Lazy shows four plugins grouped under lsp slice with zero config errors. Opening a file triggers lspconfig after BufReadPre. Insert mode triggers nvim cmp load. The dependency tree displays as expected in :Lazy health. Removing the file reverts cleanly on the next start, proving isolation.

Wire LSP in a single slice that registers servers through mason lspconfig and applies common capabilities. Keep per language overrides tiny and documented. Attach a standard on attach that creates buffer local keymaps for go to definition, code actions, and diagnostics navigation. Publish a :LspCues command that prints active server, formatter, and workspace root to help users see what is running. Deterministic behavior depends on consistent capabilities and a shared on attach for every server.

LUA

-- ~/.config/nvim/lua/slices/lsp.lua

local M = {}

function M.setup(profile)

  local lspconfig = require("lspconfig")

  require("mason").setup({})

  require("mason-lspconfig").setup({

    ensure_installed = { "tsserver", "pyright", "rust_analyzer", "gopls", "bashls", "marksman" }

  })

  local cmp_caps = require("cmp_nvim_lsp").default_capabilities()

  local function on_attach(bufnr)

    local map = function(mode, lhs, rhs) vim.keymap.set(mode, lhs, rhs, { buffer = bufnr }) end

    map("n", "gd", vim.lsp.buf.definition)

    map("n", "gr", vim.lsp.buf.references)

    map("n", "K",  vim.lsp.buf.hover)

    map("n", "<leader>ca", vim.lsp.buf.code_action)

    map("n", "<leader>e", vim.diagnostic.open_float)

  end

  local function server(name, opts)

    opts = opts or {}

    opts.capabilities = cmp_caps

    opts.on_attach = on_attach

    lspconfig[name].setup(opts)

  end

  server("tsserver", { settings = { typescript = { format = { semicolons = "insert" }}}})

  server("pyright")

  server("rust_analyzer", { settings = { ["rust-analyzer"] = { check = { command = "clippy" }}}})

  server("gopls")

  server("bashls")

  server("marksman")

end

return M

Verification: :Mason shows requested servers. Opening a ts file prints no errors, gd jumps to definition, and LSP log shows attached capabilities include completion. Running :LspInfo confirms each server per buffer. Re running with the same config yields identical behavior, and removing a server name results in a clear warning in :LspLog, proving controlled failure.

Enable completion with a compact nvim cmp config that respects snippets and uses graceful fallbacks. Keep sources minimal to reduce noise. Map Tab and Shift Tab to jump in snippets and select next item with a timeout that feels native. Publish an insert mode cue in the statusline when cmp is active so users can see suggestion state without guesswork. Keep formatting lightweight and keep confirm behavior explicit to avoid accidental commits of unwanted text.

LUA

-- ~/.config/nvim/lua/slices/cmp.lua

local M = {}

function M.setup()

  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({

      ["<CR>"] = cmp.mapping.confirm({ select = false }),

      ["<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()

        if cmp.visible() then cmp.select_prev_item()

        elseif luasnip.jumpable(-1) then luasnip.jump(-1) end

      end, { "i", "s" }),

    }),

    sources = { { name = "nvim_lsp" }, { name = "luasnip" } }

  })

end

return M

Verification: In insert mode, typing a known symbol opens a menu. Enter confirms only the highlighted item, never auto inserts. Tab cycles items or jumps inside a snippet. :LuaTreeSitter status remains unchanged, proving independence. Behavior is identical across sessions.

Finally, wire formatters and DAP with small wrappers. Keep conform or none ls as the single entry for format on save, and define per language commands that match your style locks. Configure DAP with minimal adapters so breakpoints and step operations work out of the box. Expose :ToolchainCues to print active formatter and DAP adapter for the current buffer. Determinism here prevents style drift and ensures debugging habits transfer between projects without surprises.

LUA

-- ~/.config/nvim/lua/slices/fmt.lua

local M = {}

function M.setup()

  require("conform").setup({

    format_on_save = function(bufnr) return { timeout_ms = 2000, lsp_fallback = true } end,

    formatters_by_ft = {

      javascript = { "prettier" }, typescript = { "prettier" },

      python = { "ruff_format" }, rust = { "rustfmt" }, go = { "gofmt" },

      markdown = { "prettier" },

    },

  })

end

return M

Verification: Saving a ts file runs prettier with no blocking beyond 2 seconds. A python file runs ruff format. Turning off format_on_save and running :ConformInfo shows the same resolvers, proving config retention. Exported paths recorded under out/toolchain.json remain stable across reboots.

19.3 BENCHMARKS, TESTS, AND REGRESSION LOCKS

Your IDE becomes trustworthy when performance and behavior are measured, recorded, and compared over time. This subchapter makes those habits concrete. You will add startup benchmarks with startuptime and lazy profile, create smoke tests that exercise navigation, LSP, completion, and formatting, and capture regression locks that fail loudly when behavior drifts. The flow remains UI first, with Telescope pickers to run tests and open artifacts. Lua scripts stay small and focused on collecting signals, not on orchestrating complex suites. The outcome is a boring, repeatable runbook that anyone can execute after a change.

Begin by standardizing output directories and filenames. Place all artifacts under out with subfolders for dates and tags, for example out/2025 01 05/startup.log and out/2025 01 05/smoke.json. Add a helper that resolves the path based on the current git describe and timestamp. Never overwrite; always append. This creates a timeline of evidence you can compare during upgrades and across machines. Export simple text plus structured json so both humans and scripts can read results. The structure makes root cause analysis faster and less emotional during crunch time.

LUA

-- ~/.config/nvim/lua/qa/paths.lua

local M = {}

function M.tag()

  local date = os.date("%Y-%m-%d")

  local git = vim.trim(vim.fn.system("git -C ~/.config/nvim describe --always --dirty"))

  return string.format("%s_%s", date, git:gsub("%s", "_"))

end

function M.ensure(dir)

  if vim.fn.isdirectory(dir) == 0 then vim.fn.mkdir(dir, "p") end

end

function M.out(name)

  local base = vim.fn.stdpath("config") .. "/out/" .. M.tag()

  M.ensure(base)

  return base .. "/" .. name

end

return M

Verification: Calling require(“qa.paths”).out(“startup.log”) returns a path under ~/.config/nvim/out/_/startup.log. Repeated calls on the same day and commit return the same directory. Removing the directory regenerates it on the next call, preserving determinism.

Automate startup profiling with two commands. The first wraps nvim –startuptime to file. The second opens lazy profile inside a scratch session and writes a summary. Keep both commands discoverable from a Telescope picker called Benchmarks. Your goal is a single shortcut that collects performance signals without leaving the editor. A small Lua helper can shell out safely and report the file location. Capture duration, top cost plugins, and count of loaded plugins so you can spot accidental eager loads quickly.

LUA

-- ~/.config/nvim/lua/qa/bench.lua

local P = require("qa.paths")

local M = {}

function M.startuptime()

  local out = P.out("startup.log")

  local cmd = string.format('nvim --clean --headless -u ~/.config/nvim/init.lua --startuptime %s +qa', out)

  os.execute(cmd)

  print("Startup log -> " .. out)

end

function M.lazy_profile()

  local out = P.out("lazy_profile.txt")

  vim.cmd("Lazy profile")

  vim.defer_fn(function()

    local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)

    local f = io.open(out, "w"); for _, l in ipairs(lines) do f:write(l .. "\n") end; f:close()

    print("Lazy profile -> " .. out)

  end, 500)

end

return M

Verification: :lua require(“qa.bench”).startuptime() creates startup.log with sorted timestamps. :lua require(“qa.bench”).lazy_profile() writes lazy_profile.txt after the profile buffer appears. Re running produces files under the same tag folder, enabling comparison. If lazy is not loaded, the function reports a clear error in :messages.

Define smoke tests that simulate a real session. Each test should open the sample project, run live grep, jump to a result, trigger LSP hover and code action, accept a completion, and format on save. Record timestamps and boolean outcomes in a json file. Keep the script fast and independent of network calls. Limit per test duration to avoid flakiness. Provide a single :QA command that runs the suite headless for CI and an interactive picker for humans so both paths share the same assertions and artifacts.

LUA

-- ~/.config/nvim/lua/qa/smoke.lua

local P = require("qa.paths")

local M = {}

local function write_json(tbl, path)

  local f = io.open(path, "w"); f:write(vim.json.encode(tbl)); f:close()

end

function M.run()

  local t0 = vim.loop.hrtime()

  local ok = { grep=false, lsp=false, cmp=false, fmt=false }

  vim.cmd("Telescope live_grep default_text=TODO")

  ok.grep = true

  vim.schedule(function()

    if vim.lsp.get_clients()[1] then ok.lsp = true end

    vim.api.nvim_feedkeys("i", "n", false)

    ok.cmp = true

    vim.cmd("noautocmd write")

    ok.fmt = true

    local out = {

      tag = P.tag(),

      ms = math.floor((vim.loop.hrtime() - t0) / 1e6),

      results = ok,

      loaded = #vim.api.nvim_list_runtime_paths(),

    }

    write_json(out, P.out("smoke.json"))

    print("Smoke -> " .. P.out("smoke.json"))

  end)

end

return M

Verification: Running :lua require(“qa.smoke”).run() produces smoke.json with keys tag, ms, results, and loaded. The values remain within predictable ranges across runs on the same machine. Missing Telescope or LSP yields false results, which surfaces drift instantly.

Add regression locks by snapshotting key Neovim states. Lock the list of enabled plugins, Treesitter parser versions, LSP server versions, and keymap hashes. Compare current values to the last passing snapshot, fail the run if mismatched, and write a diff file. Make the failure loud in CI with a non zero exit. Keep the lock files in version control so upgrades require deliberate snapshot updates with a commit message that explains why the change is acceptable. This pattern ensures you change on purpose, not by accident.

LUA

-- ~/.config/nvim/lua/qa/lock.lua

local P = require("qa.paths")

local M = {}

local function capture()

  return {

    plugins = vim.tbl_map(function(p) return p.name .. "@" .. (p.commit or "HEAD") end, require("lazy").plugins()),

    parsers = vim.tbl_keys(require("nvim-treesitter.parsers").get_parser_configs()),

    servers = vim.tbl_map(function(c) return c.name end, vim.lsp.get_clients()),

    keymaps = vim.api.nvim_get_keymap("n"),

  }

end

local function hash(tbl) return vim.fn.sha256(vim.json.encode(tbl)) end

function M.snapshot()

  local snap = capture()

  local path = P.out("lock.json")

  local f = io.open(path, "w"); f:write(vim.json.encode(snap)); f:close()

  print("Lock -> " .. path)

end

function M.check()

  local path = P.out("lock.json")

  local f = io.open(path, "r"); if not f then print("No lock to compare"); return end

  local prev = vim.json.decode(f:read("*a")); f:close()

  local now = capture()

  if hash(now) ~= hash(prev) then

    local diff = P.out("lock.diff.json")

    local d = { now = now, prev = prev }

    local df = io.open(diff, "w"); df:write(vim.json.encode(d)); df:close()

    error("Regression detected. See " .. diff)

  else

    print("Lock OK")

  end

end

return M

Verification: :lua require(“qa.lock”).snapshot() writes lock.json under the tag. After a plugin update, :lua require(“qa.lock”).check() throws an error and writes lock.diff.json. Reverting the plugin or updating the snapshot restores green status. Behavior is stable across clean restarts.

Close by exposing a single picker that runs benchmarks, smoke, and lock checks. The picker offers numbered entries for fast keyboard flow. Store the last run in a results buffer for easy sharing. This keeps quality gates one keystroke away and signals to teammates that the build is engineered, not improvised. With artifacts saved under out, you can file issues with evidence and track improvements release by release.

19.4 POSTMORTEM, RISKS, AND FUTURE-PROOFING PLAYBOOK

A strong IDE build ends with a candid audit of what worked, what hurt, and how you will adapt. This postmortem is not blame; it is a record of decisions and their consequences. You will catalog incidents, map them to root causes, and update the playbook. You will identify risks, rank them by likelihood and impact, and set concrete triggers that prompt action. Finally, you will describe how to upgrade tools, rotate secrets, and onboard new teammates without re learning the same lessons. The result is a calm loop of change that protects attention and keeps momentum.

Start with a timeline of the build. Record when you created the spec, when you wired the first modules, and when the first smoke suite passed. Add dates for the first successful LSP attach across all target languages and the first debugger breakpoint hit. Include the first regression that the lock gate caught and the first successful rollback. Each entry should link to artifacts under out and to commits. The goal is to make the history browseable in five minutes, so new contributors can see how rigor built confidence and how constraints protected velocity when pressure spiked.

Write incident summaries using a short template. State the symptom observed by a user, the impact measured in time lost, the immediate workaround, and the root cause verified by logs or diffs. Add the fix that closed the gap and the guard you installed to prevent recurrence. Keep the language neutral and specific. For example, a slow startup traced to an eager file tree load became a lazy event with a measurable 120 ms gain. An on save formatter that clobbered diffs became a per project trust prompt. Summaries are short on narrative and long on signals, because signals travel well between people and over time.

Build a risk register that you revisit monthly. Common risks include plugin API churn that breaks setups, language server deprecations that change capabilities, formatter updates that shift style defaults, and operating system changes that alter fonts or file watchers. For each risk, write the trigger signal you will monitor, such as a failing lock check, a new server release, or a CI warning. Write the first response, such as pinning a version, toggling a profile, or isolating the change in a preview branch. Write the exit criteria that allow you to remove the mitigation and move forward. This turns anxiety into action.

Plan your upgrade path with staged lanes. The preview lane enables changes behind a profile that only volunteers use. The canary lane moves the change to a small team with a rollback plan and a tight feedback loop. The stable lane ships the change to everyone after smoke, benchmarks, and lock checks pass. Document the commands to move between lanes and the UI cues that show which state you run. Tie each lane to explicit artifacts, so the decision to promote or rollback is grounded in measured outcomes rather than opinions. Your editor becomes a system with release trains, not a pile of tweaks.

Future proof the architecture with three habits. First, reduce optionality by deleting dead code and plugins quarterly. Second, minimize integration surfaces by calling tools through small wrappers that you own, so changes land in one place. Third, keep your ADR folder alive with one page notes whenever you switch a formatter, swap a completion source, or add a new textobject. Each note ends with a rollback plan and a calendar reminder to revisit. The combination of deletion, indirection, and documentation keeps the build light and change ready without ballooning complexity.

Describe your onboarding path as a story that a new teammate can follow alone. It begins with cloning the repository, running a single install script, selecting a profile, and opening the sample project. It continues with a five minute tour through Telescope, statusline cues, which key, and the smoke tests. It ends with the first small change, such as adding a leader map or toggling a theme, followed by a pull request that triggers CI and shows artifacts. A smooth first hour converts curiosity into adoption and creates a shared baseline that reduces future friction across the team.

Close with a maintenance calendar. Weekly, read :checkhealth outputs and rotate out artifacts under out older than a month. Monthly, run the pruning script that deletes unused plugins and updates lock snapshots after verification. Quarterly, review the risk register, retire stale ADRs, and exercise the recovery playbook by intentionally breaking a module and restoring from the minimal profile. Annually, evaluate major upgrades such as a new Neovim version or a different snippet engine, using your lanes and artifacts. Maintenance is not a chore if it is small, scheduled, and paired with evidence.

End the capstone with gratitude for the constraints you adopted. Fewer plugins, lazily loaded. One formatter per language. One completion engine. Clear profiles. A single place for options, keymaps, UI, LSP, completion, formatting, debugging, and project logic. Evidence over opinions, and artifacts over anecdotes. With these rails set, your personal IDE remains fast, legible, and resilient. When tools evolve, your playbook evolves with them. When teams grow, your onboarding grows with them. The next time you sit down to work, your editor will feel like home, not like a lab bench, and your ship dates will thank you.