Neovim Configuration (LazyVim)

A LazyVim-based setup focused on Go and Zig development, with LSP, debugging, test running, task running, AI chat, database tooling, and a which-key cheat sheet. Everything is lazy-loaded so plugins only activate for the filetype or command that needs them.

Prerequisites

  • Neovim >= 0.11 (developed on 0.12.x; 0.11+ is required for the modern vim.lsp / root_markers API used by some servers)
  • Git
  • A Nerd Font (icons in the explorer, statusline, dadbod UI, etc.)
  • Node.js — TypeScript/JSON/YAML/web LSPs and prettier
  • Go — gopls, delve, gofumpt, goimports
  • Zig — the zig toolchain on $PATH (for LSP, build/run, tests, debug)
  • Python (optional) — pyright + ruff are configured if you use it

Most language servers, linters, formatters, and DAP adapters install automatically through Mason on first launch.

Installation

  1. Back up any existing config:

    mv ~/.config/nvim ~/.config/nvim.bak
    mv ~/.local/share/nvim ~/.local/share/nvim.bak
    mv ~/.local/state/nvim ~/.local/state/nvim.bak
    mv ~/.cache/nvim ~/.cache/nvim.bak
    
  2. Clone:

    git clone https://git.samoneal.io/sam_oneal/nvim-config.git ~/.config/nvim
    
  3. Launch Neovim. lazy.nvim bootstraps itself and installs all plugins.

  4. Install tooling with :Mason (or :MasonInstall <tool>). Check health with :checkhealth and LSP attachment with :checkhealth lsp.

What's included

Base: LazyVim with leader = <Space>, localleader = \, colorscheme catppuccin-mocha.

LazyVim extras enabled (in lua/config/lazy.lua): lang.go, lang.json, lang.yaml, lang.markdown, lang.typescript, lang.tailwind, dap.core.

Languages (LSP + Treesitter): Go, Zig, Lua, TypeScript/JavaScript, HTML, CSS, JSON, YAML, Markdown, SQL, Dockerfile, Python.

Concern Plugin / tool
LSP nvim-lspconfig + Mason (gopls, zls, lua_ls, ts_ls, html, cssls, jsonls, yamlls, marksman, sqlls, dockerls, pyright/ruff)
Completion blink.cmp (LazyVim default; SQL routed to dadbod)
Formatting conform.nvim via LazyVim's format pipeline
Debugging nvim-dap + delve (Go), codelldb (Zig/C/C++/Odin)
Testing neotest (Go, Python, Zig) with in-buffer pass/fail signs
Coverage nvim-coverage (Go)
Task running overseer.nvim (+ user templates)
AI chat agentic.nvim (ACP; OpenCode by default)
Database vim-dadbod + dadbod-ui + dadbod-completion
Git Neogit + diffview, plus LazyVim's lazygit
Explorer/picker snacks.nvim
Symbols aerial.nvim
Diagnostics trouble.nvim
Cheat sheet which-key.nvim (hold <leader>)

AI (agentic.nvim)

AI is intentionally environment-agnostic: agentic.nvim is a chat front-end for any agent that speaks the Agent Client Protocol (ACP). We drive the external opencode CLI through opencode acp, so the model/provider (Anthropic, OpenAI, local models, etc.) is chosen in opencode's own config rather than here. Install the opencode CLI separately and configure your provider there.

Note

Unlike the previous opencode.nvim setup, no version pinning or binary split is needed. agentic spawns opencode acp from your normal PATH, so keep just the one opencode install. Other ACP providers (Claude, Codex, Gemini, Cursor, …) are built in — change provider in lua/plugins/ai.lua, or switch at runtime with <leader>ap. Sessions are owned by the provider, so a chat started in the terminal can be pulled into Neovim and vice-versa (<leader>ar).

New sessions start in the git worktree root (session_cwd → config.git-worktree.git_root), so the agent edits the same checkout Neovim is open in; outside a repo it falls back to Neovim's cwd.

Database (vim-dadbod)

Open the UI with <leader>Du. Add a connection with <leader>Da. Examples:

postgresql://user:pass@localhost:5432/dbname
mysql://user:pass@localhost:3306/dbname
sqlite:path/to/db.sqlite

SQL completion is provided via dadbod inside SQL buffers, and SQL autoformat-on-save is intentionally disabled (format manually with <leader>cf).

Key bindings

Hold <leader> (Space) to pop up the which-key menu — it lists every group below as a live cheat sheet. Mouse is enabled (mouse=a), and the explorer, dadbod UI, Neogit, DAP UI, trouble, aerial, and Overseer pickers are all clickable.

Note the deliberate capitalization: <leader>T* is Terminal (lowercase <leader>t* is the Test group), and <leader>D* is Database (lowercase <leader>d* is Debug).

AI (<leader>a)

Key Action
<leader>aa Open AI chat, adding current file/selection
<leader>ax Add current file or selection to AI context
<leader>at Toggle AI chat
<leader>an New AI session
<leader>al Pick an AI session
<leader>ar Restore an AI session (incl. terminal ones)
<leader>ap Switch ACP provider
<leader>ad Add cursor-line diagnostics to AI context
<leader>aD Add buffer diagnostics to AI context

Inside the chat buffer agentic sets its own keys (see :h agentic): <S-Tab> agent mode, <localLeader>m model, <localLeader>s provider, <localLeader>l sessions, q hide (session keeps running), ]]/[[ headings, ]c/[c diff hunks.

The old go/goo operators and <S-C-u>/<S-C-d> scroll keys are gone — agentic has no Vim operator, and it navigates context/diffs inside its own chat buffer (which is a normal Neovim buffer, not a terminal).

Code › Go (<leader>cg)

Key Action
<leader>cgb Run benchmarks (-bench=. -benchmem)
<leader>cgpc CPU profile
<leader>cgpm Memory profile
<leader>cgpt Trace profile

Code › Zig (<leader>cz)

Key Action
<leader>czb zig build
<leader>czr zig build run
<leader>czt zig build test

Tests (neotest, <leader>t)

Works in Go, Python, and Zig buffers; results show as signs in the gutter.

Key Action
<leader>tt Run nearest test
<leader>tf Run test file
<leader>ts Toggle test summary
<leader>to Open test output
<leader>tc Show coverage (Go)

Debugging (DAP, <leader>d)

Go uses delve; Zig/C/C++/Odin use codelldb. Zig launch configs build with debug symbols first, then run (Launch (zig build) prompts for the exe under zig-out/bin; Launch (current file) compiles the open file). Odin offers Launch (make build-debug) (desi_explorer-style monorepo — runs make -C gui build-debug and points at bin/desi_explorer with the repo's GUI_ENV_FILE), Launch (odin build .) (plain debug odin build), and Launch (current file).

Key Action
<leader>db Toggle breakpoint
<leader>dB Conditional breakpoint
<leader>dc Continue / start
<leader>dC Run to cursor
<leader>di Step into
<leader>dO Step over
<leader>do Step out
<leader>dj / <leader>dk Stack down / up
<leader>dr Toggle REPL
<leader>ds Session
<leader>dt Terminate
<leader>dw Inspect widget (hover)
<leader>du Toggle DAP UI
<leader>de Eval (n/v)
<leader>dT Debug Go test
<leader>dL Debug last Go test

Tasks (Overseer, <leader>o)

Key Action
<leader>or Run a task (picker — includes go: and zig: templates)
<leader>ot Toggle task list

Database (<leader>D)

Key Action
<leader>Du Toggle DB UI
<leader>Da Add connection
<leader>Df Find DB buffer

Git (<leader>g)

Key Action
<leader>gg Lazygit (LazyVim default)
<leader>gn Neogit
<leader>gl Lazygit log
<leader>gf Lazygit file history
<leader>gw Git branches (worktree switcher, Telescope)
<leader>gW Git worktrees (list/jump, Telescope)
<leader>gC Convert current repo to worktree layout

Git worktrees

<leader>gw opens a Telescope picker of every branch in the current project (local + remote) and shows at a glance which ones are checked out on this machine: the current branch is bold, branches with a worktree are highlighted green with their path, and remote-only branches are dimmed with a globe icon.

  • <CR> switch — jumps to the branch's worktree, creating it on demand. Remote-only branches are pulled down as tracking local branches first.
  • <c-f> fetch remotes — runs git fetch --all --prune so new remote branches (e.g. renovate/*) appear in the list, then refreshes.
  • <c-d> remove — deletes the selected worktree (or local-only branch).
  • <c-n> new — prompts for a branch name (+ base) and creates its worktree.
  • <c-r> refresh — re-reads local/remote state.

:GitWorktreeConvert (or <leader>gC) turns a plain single-checkout repo into a worktree layout without losing anything: every local branch gets its own linked worktree under <parent>/.worktrees/<project>/<branch> (main keeps its place), so all branches that were on the machine stay available. Pass --keep=<branch> to choose which branch remains at the main worktree.

The snacks explorer (<C-e>) shows a chip in its title with the current project, branch, and worktree count when inside a git repo.

Title bar

lua/config/title.lua keeps the terminal / OS window title pointed at the project you are in, and the Vim window title alongside it:

Desi Explorer                    # project name only
Desi Explorer  󰐘 feat-api            # project + branch
Desi Explorer   wt-explorer  󰐘 feat-api   # project + worktree + branch

The project name is resolved in this order:

  1. the name field of a .nvim-project.json in the directory nvim was opened in (or the root of its worktree),
  2. the name of the worktree layout's home directory, so every worktree of a repo shares one project name,
  3. the name of the directory itself, when there is no git repo at all.

To name a project explicitly, commit this to its root:

{ "name": "Desi Explorer" }

The worktree segment only appears when you are in a linked worktree (it is the path relative to the layout home, with the .worktrees/ hop dropped). The branch segment is replaced by detached on a detached HEAD, and by bare when nvim sits in a bare home with no working tree checked out. Both are omitted outside a git repo.

The title refreshes on DirChanged, FocusGained, VimResume and on User GitSignsUpdate, which covers :cd, <leader>gw worktree switches, neogit, lazygit and :Git branch without polling. :RefreshTitle forces a recompute.

Note: lualine's title component is left off, so the window title is available to anything reading it but is not printed in the statusline.

Search / Symbols (<leader>s)

Key Action
<leader>so Symbols outline (aerial)
<leader>sh Incoming calls
<leader>sc Outgoing calls

Diagnostics (Trouble, <leader>x)

Key Action
<leader>xx Diagnostics
<leader>xw Workspace diagnostics
<leader>xt TODOs

Terminal (<leader>T)

Key Action
<leader>Th Terminal (horizontal split)
<leader>Tv Terminal (vertical split)
<Esc><Esc> Exit terminal mode

Explorer & windows

Key Action
<C-e> Toggle explorer (snacks)
<C-S-v> / <C-v> Open file in vertical split
<C-S-h> / <C-x> Open file in horizontal split
<C-h/j/k/l> Navigate windows
<C-Up/Down/Left/Right> Resize window

Formatting (LazyVim pipeline)

Key / Command Action
<leader>cf Format buffer
<leader>uf Toggle autoformat (buffer)
<leader>uF Toggle autoformat (global)
:LazyFormat Format now

General editing

Key Action
<C-s> Save file
<S-h> / <S-l> Prev / next buffer
<Esc> Clear search highlight
<A-j> / <A-k> Move line/selection down/up
< / > (visual) Indent, keep selection

Formatters by file type

File type Formatter
Go gofumpt, goimports (-local github.com)
Lua stylua (2-space)
JS/TS/JSX/TSX prettier (2-space, single quotes)
HTML/CSS/SCSS prettier
JSON/JSONC/YAML prettier
Markdown prettier
SQL sql-formatter (postgresql; manual only)

File structure

~/.config/nvim/
├── init.lua                     # Entry point → require("config.lazy")
├── lua/
│   ├── config/
│   │   ├── lazy.lua             # lazy.nvim bootstrap, LazyVim + extras
│   │   ├── options.lua          # Neovim options
│   │   ├── keymaps.lua          # Custom keymaps
│   │   ├── git-worktree.lua     # Worktree-aware branch switching + convert
│   │   ├── title.lua            # Title bar: project name, worktree, branch
│   │   └── autocmds.lua         # Autocommands (yank hl, ft tweaks, SQL no-autoformat…)
│   ├── plugins/
│   │   ├── mason.lua            # Tooling installs (LSP/linters/formatters/DAP)
│   │   ├── lspconfig.lua        # Per-server LSP config (gopls, zls, …)
│   │   ├── treesitter.lua       # Parsers
│   │   ├── formatting.lua       # conform.nvim
│   │   ├── dap.lua              # Debugging (Go via delve, Zig/C/C++/Odin via codelldb)
│   │   ├── neotest.lua          # Test runner (go/python/zig)
│   │   ├── go-coverage.lua      # Go coverage signs
│   │   ├── overseer.lua         # Task runner
│   │   ├── ai.lua               # agentic.nvim
│   │   ├── dadbod.lua           # Database
│   │   ├── git.lua              # Neogit + diffview
│   │   ├── worktree.lua         # Git worktree keymaps (`<leader>gw/gW/gC`)
│   │   ├── editor.lua           # snacks tweaks + which-key groups
│   │   ├── symbols.lua          # aerial
│   │   ├── dashboard.lua        # trouble opts
│   │   ├── devcontainer.lua     # nvim-remote-containers
│   │   ├── performance.lua      # treesitter large-file guard
│   │   ├── theme.lua            # catppuccin
│   │   └── zig.lua              # Zig ft plugin (toolchain wired across files)
│   └── overseer/template/user/  # go_*, odin_{build,run,test}, zig_{build,run,test} task templates

Zig notes

  • LSP is zls (lua/plugins/lspconfig.lua), root markers build.zig / build.zig.zon, with build-on-save and inlay hints.
  • zig build test and project-wide neotest runs require a standard test step in your build.zig; individual .zig files also work.
  • Debugging needs codelldb (:MasonInstall codelldb if it doesn't auto-install). Builds use -O Debug so symbols are present.

Odin notes

  • LSP is ols (Mason + lua/plugins/lspconfig.lua); formatting is odinfmt.
  • Debugging uses codelldb with -o:none -debug builds, so real breakpoints, locals/variables, and REPL evaluation work against Odin DWARF. The monorepo launch config sets GUI_ENV_FILE for you, matching make run.
  • In desi_explorer, make -C gui build-debug must finish before Launch (make build-debug) — the config builds first, so breakpoints set in gui/src/*.odin before launching are hit reliably.

Troubleshooting

  • LSP not attaching: :checkhealth lsp — confirm the server appears and is attached (not just installed in Mason). If root_dir/root_markers look off, the server may not find your project root.
  • Formatting: :ConformInfo.
  • Tests not detected: ensure the Treesitter parser for the language is installed (:TSInstall <lang>); Zig needs a test step in build.zig.
  • Ctrl+Shift keys not firing: some terminals don't transmit them; use the plain-Ctrl fallbacks (<C-v> / <C-x> in the explorer).
  • Nerd Font icons missing: install a Nerd Font and set it in your terminal. The title bar uses nerd font glyphs too, so it shows boxes without one — edit M.config.icons in lua/config/title.lua to "" for plain text.

Known issues (in current repo)

  • agentic.nvim sends @-mentioned files as path references, so the agent reads them from disk: save the buffer before adding a file to the context. A visual selection is inlined from the buffer, so unsaved edits there are fine.
  • OpenCode's interactive question tool is excluded over ACP by default; enable it with OPENCODE_ENABLE_QUESTION_TOOL=1 if you need structured prompts.
S
Description
My own NVIM configuration
Readme Apache-2.0
295 KiB
Languages
Lua 100%