Editor: Neovim
This page explains what the Neovim setup in this repo enables, how it is structured, and the workflows that are easy to miss if you only skim the config.
This page is written for IDE-first users (VSCode / JetBrains) who want to understand the practical benefits and adopt useful parts gradually.
What You Get
- A keyboard-first editor with discoverable keymaps (most maps have
desc) - Fast repo search (files, grep, changed-lines grep)
- Tight test loops for JS/TS (Jest) in an editor split
- Git ergonomics (hunks, history search, diffs)
- A set of local plugins that solve specific daily problems
- Project-aware formatting for web files (JS/TS/JSON): prefer Oxfmt when the repo declares it, else Biome, else Prettier
- ESLint and Oxlint diagnostics can coexist; formatting remains single-tool to avoid conflicts
- Markdown and MDX: Prettier (via conform.nvim) is invoked with
--prose-wrap=preserveso body text is not reflowed toprintWidthfrom the editor, even when a project config setsproseWrap: "always". For plainmarkdown(notmdx),unwrap-mdruns after Prettier to unwrap hard-wrapped prose (seeplugins/markdown.lua). Editor soft-wrap usesFileTypepatternsmarkdown*/mdx*so compound types (e.g.markdown.github) are included, and options are applied per window viawin_findbuf(seecore/autocmds.luaandutil/markdown_view.lua). Sessions omitlocaloptionsinsessionoptionsso window/buffer options stay driven by config and filetypes, not replayed session state (seecore/options.lua). Default Neovimviewoptionsdoes not includeoptions, somkviewonBufLeavedoes not persistwrap(only folds/cursor).
Where The Config Lives
- Source (in this repo):
home/dot_config/exact_nvim/ - Install target (on disk):
~/.config/nvim/
This setup uses chezmoi naming conventions where some directories are prefixed with exact_ in the source but are installed without that prefix.
Examples (source -> installed):
home/dot_config/exact_nvim/exact_lua/->~/.config/nvim/lua/home/dot_config/exact_nvim/exact_after/->~/.config/nvim/after/
Leader keys:
mapleaderis space (vim.g.mapleader = " ")maplocalleaderis\
See home/dot_config/exact_nvim/exact_lua/exact_core/readonly_options.lua.
Neovim itself is version-managed via mise:
- Runtime config:
home/dot_config/mise/config.toml.tmpl(neovim = "0.12.2")
Quick Start
- Install the pinned Neovim version:
mise install neovim@0.12.2 - Apply dotfiles:
chezmoi apply - Launch Neovim:
nvim - Open plugin dashboard:
:PackDashboard(or use:PackSyncfor raw report)
Plugin Manager On 0.12
This config now uses Neovim's built-in vim.pack.
Plugin specs are still declared in lazy-style tables, but loading is now trigger-aware in core/plugins.lua: cmd, event, ft, and key-triggered plugins are deferred until first use while always-on specs load at startup.
Note: inc-rename.nvim is loaded on LspAttach rather than cmd = IncRename. The deferred cmd stub in core/plugins.lua re-executes commands with vim.cmd(), which breaks Neovim's command-preview API that inc-rename relies on (you may see E32: No file name or preview errors otherwise).
Pack retention: Orphan cleanup only removes directories for plugins that are no longer present in the merged Lua specs. Plugins skipped at startup because cond is false (for example, when the first buffer is outside a git work tree) still count as managed packs so their install is not deleted and re-fetched on the next session.
Version policy (fast startup): startup does not probe remotes. Instead, PackSync / PackStatus refresh a cached heuristic map under stdpath("state") that decides per-plugin whether to follow release tags or branch tip.
Per-spec version field (lazy.nvim-compatible):
version = "*"→ latest semver tag (translates tovim.version.range("*"); resolves to the greatest tag, regardless of major). Footgun: if a repo carries non-release tags that still parse as semver (for example Telescope'snvim-0.6, which parses as0.6.0and outranks realv0.2.xreleases),*selects that junk tag and pins a stale build. The dashboard flags this as ariskyrow (see below); preferversion = false(branch tip) or an explicit range/tag for such repos.version = "^1.0"/"~1.2"/">=2.3"→ semver range (resolves to the greatest matching tag)version = "v1.2.3"→ exact tagversion = "<commit-sha>"→ exact commitversion = false→ default branch tip, skip tag resolution entirelyversion = nil(field omitted) → use the cached heuristic belowcommit/tag/branchtop-level fields take priority overversion(same spec format as lazy.nvim)
Heuristic default (when version is unset): three gates drive the auto-decision between tags and branch tip:
- Minimum release history: repos with fewer than 3 semver tags can't form a reliable average — if more than 30 commits have landed since the last tag, fall back to branch tip.
- Commit ratio: if the number of commits since the latest tag exceeds the average commits per release (x1.5), fall back to branch tip.
- Absolute cap: more than 150 unreleased commits always means branch tip.
The heuristic is a convenience default for plugins whose spec omits version. To override per-plugin, set version explicitly in the spec — this wins over the heuristic without any global configuration. Run :PackPolicyRebuild (optionally with a plugin name) to clear and recompute the cached heuristic after the plugin set changes or after upstream adds/removes tags; tab-completion suggests managed plugin names.
Practical commands:
:PackDashboard-> compact floating plugin dashboard that opens from cached/known state by default, with:- per-plugin update status
- orphan flag (
O/ trash icon) for installed packs that are no longer declared by any spec; pressCto clean them (selected orphans first, else all orphans, with confirmation) - drift flag (
D/ sync icon) when the on-disk checkout no longer satisfies the specversion(e.g. you change aversion/commit/tagfield and restart).vim.pack.adddoes not re-checkout on a version change, so this is otherwise silent. PressVto heal: re-checks out drifted plugins to the spec/lockfile version viavim.pack.update(..., { offline = true, force = true })(no network, no confirmation tabpage; the affected rows show the unified inline spinner and flip to their healed status), selected drifted rows first, else all. Drift/risky checks run asynchronously after the window opens so dashboard launch stays fast. - risky-pin flag (
W/ warning icon) whenversion = "*"would select a non-release tag that outranks the greatest real release tag (theversion = "*"footgun above); switch the spec toversion = falseor an explicit range/tag. This flag may appear a moment after open when the async tag scan completes - breaking-risk hint (best-effort) from semver delta (
major/minor/patch) plus commit-message signals in the cumulativerev_before..rev_afterrange (for example the uppercase Conventional Commits footerBREAKING CHANGE:/BREAKING-CHANGE:— matched case-sensitively, so lowercase prose like "made a breaking change" is not a marker — plusbreak:,break(scope):,!:,feat,fix,refactor,perf);RKshows!(breaking),+(safe), or-(unknown) with warn/green/muted coloring on update rows. The details popup (K) shows the metadata header followed by a single Pending updates list (one line per pending commit) and highlights in orange-yellow exactly the commits whose message carries a breaking marker. Because the marker often lives in the commit body (e.g.BREAKING CHANGE:or prose) rather than the subject, the popup classifies each pending commit by its full message (subject + body, any case) keyed by hash — not by the visible subject text — so body-only breaking commits are still flagged. When the row is flagged breaking with no marker text anywhere (e.g. a semver-major bump), a⚠ Breaking: <reason>banner is shown in the header so the popup never silently contradicts the row. The popup is colorized for scannability (reusing the dashboard table palette): each field label is highlighted, theStatusvalue takes its status color (update/same/error/orphan/drift/risky),Repo/DiffURLs render as links, and pending commit hashes are accented. For an error row the section is titledError:and the failure text is shown in the error color so failures stand out; the footer key hint is dimmed. Inside the popup:oopens the single commit under the cursor (…/commit/<hash>),Oopens all pending commits as one diff (…/compare/<before>...<after>), andropens the plugin repository - row queue checkbox in
SEL([x]/[ ], toggled with<Space>/xor visual mode): bold cyan when checked — separate fromRKwarn/orange so a queued update does not look like a breaking-risk row - icon-based links column (
diff/repo) with direct compare URL for pending updates - single pending update (
<CR>), selected pending updates (u), update all visible pending rows (U) - inline selection/filter/sort/search and details popup (
?for full key help). When a filter mode (f) and/or search (/) is narrowing the list, a bold warning-coloredFILTERED showing X of Y (filter:…, search:'…')banner appears as the second header line — so a restored saved search (the persisted UI state survives restarts) can never make the dashboard look like onlyXplugins exist. Presscto clear the search,fto cycle the filter back toall. When the filter hides every row, the empty-state line names the active filter and the keys that reset it instead of a generic "no match" - the header is laid out as section-structured, bar-separated
key:valuefields rather than dense single lines: each metadata section (title, optional FILTERED banner, counts, optional live progress, last-result, key help) renders as coloredlabel:valueitems joined by a vertical bar (│, faint), and the items wrap to the dashboard window width at item boundaries (akey:valueis never split mid-token), so nothing is clipped in a narrow split/tmux pane. Each part is independently colored — labels dim, values normal, counts in their status color (update/same/error/orphan/drift/risky/breaking), help keys accented — so a line reads as distinct fields, not one grey blob. A horizontal rule divides the status block (state) from the help block (actions); the heavy table rules still bracket theSEL ST RK PLUGIN…column header (those two rules + the column header stay one line so they keep aligning with the horizontally-scrolling table) - during an online/offline refresh the live
checking updates: fetch N/M/status N/Mprogress is shown on its own dedicated header line (info-colored), placed below the whole status block (after both the icon-counts row and the labeled last-result/update:NN same:NN …row, just above the rule that divides status from help) so it never splits the two count rows; it stays visible the whole time instead of being clipped off the right edge and appearing to vanish. TheN/Mdenominator is the number of rows the refresh actually targets (see below), so a filtered refresh reads e.g.status 2/2, never adone > totallike79/2 - manual async online refresh with
R; async offline/local status withr(both use the unified per-row spinner flow). When a filter/search is narrowing the list,r/Rrefresh only the visible rows (matching the FILTERED banner and the per-row spinners): the in-flight set, the progress denominator, and the plugins actually fetched/re-evaluated are one and the same population. Clear the filter (c/f) first to refresh every managed plugin
:PackSync-> raw onlinevim.packreport (fetch remotes first):PackStatus-> raw offlinevim.packreport (local refs only):PackDashboardStats-> print last raw check counters (update/same/error) plus result, online, offline, and apply timestamps:PackTrace [plugin-name]-> show current load state, trigger metadata, and load reason:PackLoad <plugin-name>-> force-load one plugin by name (useful for debugging):PackLockInfo-> shownvim-pack-lock.jsonpath, plugin count, mtime (the lockfile is maintained byvim.packitself):PackLockExport <path>-> copy the lockfile to any path (for syncing across machines viachezmoi re-addor similar):PackLockImport <path>-> overwrite the lockfile from a path; restart or:PackSyncto apply pinned revisions:PackPolicyRebuild [plugin-name]-> clear and recompute the cached tag/branch heuristic (omit the name for a full rebuild)<localleader>ssor:AutoSession save-> save the current session
Dashboard/trace popup buffers are treated as transient and excluded from session persistence to avoid polluting auto-session restores. Session search integrations are loaded on demand to keep startup leaner. Opening :PackDashboard renders cached/known state immediately, then automatically kicks off a full async online refresh (equivalent to pressing R): each row shows the inline spinner and arrives independently as its plugin is fetched + re-evaluated. Set vim.g.pack_dashboard_refresh_on_open = false to opt out and keep the old cached-only open. You can still press R to refresh again at any time, and r for offline/local status, which does not fetch and may report no remote updates. In-dashboard refreshes and operations do not emit progress/result/success toasts — the dashboard is already on screen, so per-row spinners, the inline status icons, and the header counters/stamps convey progress and outcome. Only genuine failures (fetch/checkout errors, surfaced as error rows plus a single warning), "nothing to do" preconditions, and confirm cancellations still notify. Dashboard check/apply timestamps and last plugin status/version snapshot are persisted under stdpath("state") so they survive Neovim restart. The dashboard header shows last raw check counters from the most recent check, plus separate result, online, offline, and applied stamps so stale offline/cache state is visible. Every long-running dashboard mode shares one unified per-row experience driven by refresh.run_row_op: the affected rows immediately show an inline spinner in their status (ST) cell, the operation runs per plugin, and each row resolves independently — its spinner flips to the new status icon the moment that plugin finishes — so results arrive one row at a time in completion order with no full-table redraw. Exactly two full renders bookend the run (initial spinner paint + a final re-sort/cursor/notify); everything in between is single-line updates. This applies to:
R(online refresh): each plugin is fetched + re-evaluated as its own concurrent unit (concurrency honorsvim.g.pack_dashboard_fetch_concurrency); the header shows livecheck:status:done/totalprogress.r(offline status): same per-row flow without the network fetch.<CR>/u/U(update pending): the slow network fetch runs asynchronously per plugin via the pipeline (no UI freeze, no command-line fetch-progress spam), and the moment each plugin's remote is fetched it is checked out locally with a fastvim.pack.update({ name }, { offline = true, force = true })call; that row then flips to up-to-date. The previous monolithic blockingvim.pack.update(filtered, { force = true })(which froze the UI during the fetch) is gone.V(heal drift): drifted rows spin while re-checked out to spec, then flip once drift/risky flags are re-derived.C(clean orphans): orphan rows spin during the delete, then drop out of the table when it completes. Filter/sort, search text, and selected plugin rows are also restored on the next dashboard open. Useoto open a plugin diff link (with repository fallback), andOfor repository-only open.
Dashboard Tuning (Optional)
The dashboard defaults to an icon-first compact view and can be tuned with globals:
vim.g.pack_dashboard_width_ratio(default0.68)vim.g.pack_dashboard_height_ratio(default0.68)vim.g.pack_dashboard_min_width(default84)vim.g.pack_dashboard_min_height(default18)vim.g.pack_dashboard_margin(default6)vim.g.pack_dashboard_fast_scroll(defaulttrue)vim.g.pack_dashboard_ascii(defaultfalse; whentrue, use ASCII labels/icons)vim.g.pack_dashboard_refresh_on_open(defaulttrue; whenfalse, opening:PackDashboardrenders cached state only instead of auto-starting an online refresh)vim.g.pack_dashboard_fetch_concurrency(default8; max concurrent backgroundgit fetchjobs)vim.g.pack_dashboard_skip_risk_confirm(defaultfalse; whentrue,u/U/<CR>skip the risk confirmation for plugins flagged with a major-bump or breaking-signal)vim.g.pack_dashboard_skip_clean_confirm(defaultfalse; whentrue,Ccleans orphan plugins without asking for confirmation)
Repeated :PackDashboard calls reuse the existing floating window instead of stacking multiple instances; add ! (i.e. :PackDashboard!) to force-close and reopen the dashboard without starting a refresh. Stale cache/UI entries for plugins that were removed from the config are purged automatically on every dashboard open.
Orphan plugins (packs on disk that are no longer declared by any spec) are surfaced as orphan rows in the dashboard rather than silently deleted at startup. This mirrors the lazy.nvim UX: you review what will be removed, then press C to clean. The only auto-mutation that still happens at startup is re-cloning a plugin whose src changed (same name, new remote) — that's a legitimate move, not an orphan.
Version drift is surfaced the same way. Because vim.pack.add only updates the lockfile (not the on-disk ref) when a version field changes, editing a version/commit/tag and restarting does not move the checkout — the change is silent until something re-resolves. The dashboard detects this mismatch (resolved spec version vs the current rev/tags) and shows a drift row; press V to re-checkout to the spec version offline. This is flag-and-heal (like orphans), not an automatic startup mutation. The related risky row warns when version = "*" would resolve to a junk tag that outranks real releases, so you can switch to version = false or an explicit range before it pins a stale build.
Initial version-policy generation and 3-day TTL refresh are deferred to VimEnter + 200ms and processed one plugin per scheduled tick, so neither the first launch after adding plugins nor a cold cache blocks the editor. The sync path used by :PackSync / :PackStatus runs incrementally: only new or missing plugin entries are recomputed when the existing cache is otherwise valid.
Current links column behavior is compact availability:
diffmarker when a compare URL exists- otherwise
repomarker when a repository URL exists -when no URL is available
Tree-sitter: Bundled Parsers And Startup Hangs
Neovim can load tree-sitter parsers from multiple places (runtimepath). In practice, a broken parser under the user "site" directory can hang Neovim at startup, especially if your last session opens a filetype that immediately triggers that parser.
This config prefers Neovim's bundled parser for Markdown to reduce the chance of a bad user-installed parser taking down the editor:
- Loader:
home/dot_config/exact_nvim/exact_lua/exact_plugins/readonly_treesitter.lua - Helper:
home/dot_config/exact_nvim/exact_lua/exact_util/readonly_treesitter.lua
Symptoms you might see:
nvimappears to "freeze" (often when opening*.md)nvim --cleanworks but regularnvimdoes not
Local fix (if you hit this):
ls -la ~/.local/share/nvim/site/parser
rm -f ~/.local/share/nvim/site/parser/markdown.so
Note: the config also treats bundled/runtime parsers as "available" so nvim-treesitter doesn't repeatedly try to auto-install languages that Neovim already ships. Availability is decided by an actual parser library on the runtimepath (parser/<lang>.*), not merely by vim.treesitter.language.add() succeeding — that call returns truthy for a registered language name even when no parser is installed. Query lookups are also guarded with pcall, so a language that ships query files via plugins (for example ruby query files from hlargs.nvim/nvim-treesitter-textobjects) but has no parser yields a cached false instead of throwing No parser for language ... and erroring the FileType autocmd on every matching buffer.
Filetype: *.tmpl Belongs To Chezmoi, Not Go
alker0/chezmoi.vim detects files under $CHEZMOI_SOURCE_DIR and sets composite filetypes like gitconfig.chezmoitmpl, toml.chezmoitmpl, sh.chezmoitmpl, etc. This is what enables the inner-language syntax plus Go-template awareness and is what tree-sitter queries expect.
ray-x/go.nvim ships an ftdetect/filetype.vim that blanket-claims every .tmpl file as Go text-template:
au BufRead,BufNewFile *.tmpl set filetype=gotexttmpl
Our plugin manager sources every plugin's ftdetect/ eagerly at startup (even for lazy-loaded plugins like go.nvim), so that autocmd is already registered the first time a .tmpl buffer is read. Depending on registration order, the gotexttmpl autocmd can win on either the initial BufRead or subsequent :e/:edit!. The resulting gotexttmpl filetype pulls in syntax/go.vim, which defines goCharacter as a '...' region — so a stray apostrophe in a comment (git's) paints everything up to the next ' (often many lines away) as Character.
The defense is intentionally scoped: home/dot_config/exact_nvim/exact_lua/exact_plugins/readonly_chezmoi.lua installs an eager FileType autocmd at startup. Whenever a buffer under the chezmoi source tree is set to a known hijacking filetype (gotexttmpl, gohtmltmpl), it restores the composite filetype that chezmoi.vim already detected (<ft>.chezmoitmpl, stored as b:chezmoi_original_filetype) or falls back to plain chezmoitmpl when there is no inner filetype.
readonly_dot_Brewfile.tmpl is intentionally reclaimed to plain conf, not conf.chezmoitmpl, because the Brewfile source is managed as configuration text in this setup.
It does not delete the global *.tmpl detector. Non-chezmoi .tmpl files can still become gotexttmpl, and .gotext / .gohtml handlers from go.nvim remain.
If you are IDE-first, start by learning:
- moving between files quickly
- searching within a repo
- running tests from inside the editor
How To Discover Keymaps
This config installs which-key:
Most mappings are defined with descriptions in:
If you forget a shortcut, use which-key and your leader mappings as the primary discovery mechanism.
Customization Entry Points
Start here if you want to change behavior without spelunking the entire tree:
- Core options:
home/dot_config/exact_nvim/exact_lua/exact_core/readonly_options.lua - Core keymaps:
home/dot_config/exact_nvim/exact_lua/exact_core/readonly_keymaps.lua - Core autocmds:
home/dot_config/exact_nvim/exact_lua/exact_core/readonly_autocmds.lua(Markdown andmdxusewrap+linebreak+breakindentfor readable prose) - Plugin configs:
home/dot_config/exact_nvim/exact_lua/exact_plugins/
The corresponding installed paths are:
~/.config/nvim/lua/core/options.lua~/.config/nvim/lua/core/keymaps.lua~/.config/nvim/lua/core/autocmds.lua~/.config/nvim/lua/plugins/
How it's organized:
core/is foundational editor behavior (options, keymaps, autocmds)plugins/is plugin configuration grouped by topic/languageplugins_local_src/contains local plugins written specifically for this setup
Local plugins (written in this repo) live under:
- Source:
home/dot_config/exact_nvim/exact_lua/exact_plugins_local_src/ - Loader:
home/dot_config/exact_nvim/exact_lua/exact_plugins_local/
These are the most workflow-specific parts of the config and usually the best place to look when you want to understand why something exists.
The load list is explicitly declared in:
LSP Code Actions (<leader>ca)
Code actions are shown with fzf-lua, not Neovim’s default vim.ui.select prompt. The fzf-lua plugin is loaded on demand (key triggers); its config registers vim.ui.select globally only after that first load. The <leader>ca / <leader>cA mappings therefore call fzf-lua’s lsp_code_actions helper (with packadd when needed) so the fzf picker is used even if you have not used another fzf mapping yet in that session.
Lua LS Workspace Scope
lua_ls root detection is intentionally narrowed for chezmoi paths: a file under the chezmoi source tree uses its own directory as the workspace root instead of the repo .git root. This avoids full-repo scans and the "More than 100000 files have been scanned" startup warning (the chezmoi tree holds ~30k files), which otherwise leaves the workspace preload stuck at 0%.
The root_dir in plugins/lua.lua resolves in priority order: a real lua_ls project marker (.luarc.json/.luarc.jsonc) always wins; otherwise, when the buffer is under the chezmoi source tree (detected via vim.g["chezmoi#source_dir_path"], set by plugins/chezmoi.lua), it roots at the file's own directory; only outside chezmoi does it fall back to formatter/linter markers (.stylua.toml/stylua.toml/selene.toml) and then .git. The chezmoi check must precede the formatter markers because a .stylua.toml lives at the chezmoi repo root and would otherwise pull the root back up to the full tree.
Two Neovim-0.11+ specifics make this work: the native LSP root_dir signature is fun(bufnr, on_dir) (the chosen root is passed to on_dir, not returned — the old lspconfig fun(fname) form is silently ignored), and lazydev's lspconfig integration is disabled in plugins/lazydev.lua because it would otherwise override root_dir with find_workspace, letting the .git root marker win. Disabling that integration does not affect lazydev's library/annotation injection, which runs through its buffer/workspace mechanism.
Jump To Source, Not Target (Chezmoi)
When you edit this config from its chezmoi source tree (~/.local/share/chezmoi/home/...), language servers still resolve symbols against the deployed copies under $HOME. For example, lua_ls resolves require("plugins_local_src.qf") to ~/.config/nvim/lua/plugins_local_src/qf.lua, so a plain gd/gr from inside a source file would jump to the rendered target — the file chezmoi apply silently overwrites (the C1 source-vs-output invariant in AGENTS.md).
util/chezmoi_lsp.lua closes that gap. Wired in plugins/chezmoi.lua, it wraps vim.lsp.buf_request{,_sync} (the chokepoint fzf-lua uses for gd, gD, gI, gy, and gr) and rewrites location results so a destination that is a chezmoi-managed target is swapped for its source path via chezmoi source-path.
Scope is deliberately narrow:
- It only rewrites when the originating buffer is itself a chezmoi source file; editing a deployed target directly keeps normal target-to-target navigation.
- Only location methods are touched (
definition,declaration,typeDefinition,implementation,references); hover, formatting, and code actions pass through untouched. - Targets under the neovim data/plugin dir,
$VIMRUNTIME, or outside$HOMEare skipped before anychezmoiprobe, and resolutions (including misses) are cached, so reference lists stay fast. - Line/column are preserved. For non-template sources the content is byte-identical so the cursor lands exactly; for
.tmplsources the rows may drift but you still land in the correct source file.
LSP Progress In Lualine
Lualine renders native Neovim LspProgress events through a local plugin: loader plugins_local/lsp-progress.lua, implementation plugins_local_src/lsp-progress.lua, and component wiring in plugins/lualine.lua. The statusline shows the client name, an animated spinner, the latest title/message, optional server-provided percentage, and a derived completed-token counter such as (0/1) or (1/1) - done. This replaces lsp-progress.nvim without emitting terminal/tmux OSC progress bars.
Starter Keymaps (High Signal)
Window navigation:
Ctrl-h/j/k/lmoves between splitsleader-|split right,leader--split below
Buffers:
leader-bborleader-<backtick>toggles last buffer[band]bprev/next buffer
Search:
leader-Spacefiles (fzf)leader-sglive grep (fzf)leader-/grep current buffer
Explorer:
leader-eNeo-tree explorer (cwd)leader-geNeo-tree git status
Diagnostics / quickfix:
leader-cdline diagnostics[dand]dprev/next diagnosticleader-xqtoggle quickfix,leader-xltoggle location list
Git:
[hand]hprev/next hunkleader-ghppreview hunk inline
GitHub (Octo):
leader-goaopen Octo actionsleader-goil/leader-goislist/search issuesleader-gopl/leader-gopslist/search pull requestsleader-godllist discussionsleader-gonllist notificationsleader-gosrun GitHub search in Octo
Search And Navigation Workflows
Repo search is centered around fzf-lua:
Useful mappings (all are defined in that file):
leader-sglive grep in cwdleader-segrep in changed lines (git status)leader-sEgrep in changed lines (branch)leader-sfgrep in changed files (git status)leader-sFgrep in changed files (branch)
File explorers:
- Neo-tree:
home/dot_config/exact_nvim/exact_lua/exact_plugins/readonly_neo-tree.lua - Yazi: same file (
mikavilpas/yazi.nvim) - Oil: same file (
stevearc/oil.nvim)
Neo-tree has a couple of "workflow" mappings inside the tree:
leader-nffind in selected directoryleader-nggrep in selected directoryleader-ypcopy relative path
Git Workflows
Hunks, blame, and history search are configured here:
Highlights:
- gitsigns hunk navigation (
[h/]h) and stage/reset hunk mappings underleader-gh* - Diffview mappings under
leader-df* - History search (
AdvancedGitSearch) underleader-ga*
Local Plugins
This config ships a set of small in-repo Lua plugins for testing (Jest in a split), git (commit summarizer), ownership/CODEOWNERS search, TS export refactors, the tmux bridge, source/test toggling, screenshots, and quickfix/window ergonomics. They have their own page:
Small Quality-Of-Life Commands
Defined in home/dot_config/exact_nvim/exact_lua/exact_core/readonly_keymaps.lua:
:LargeFilespopulate quickfix with very large tracked files:WW/:WWWwrite without triggering autocmdsleader-yp/leader-yPcopy path to clipboard (relative / absolute)
What Problems This Setup Focuses On
- Fast navigation and editing with a consistent keyboard-first model
- Tight test loops (run tests, rerun, jump to failures) without leaving editor
- Repeatable refactors that update imports/exports predictably
- Working in large repos (ownership, ripgrep/fzf tooling, git ergonomics)
IDE Translation (Mental Model)
- VSCode/JetBrains tabs map more closely to Neovim buffers.
- IDE "Problems" panel maps well to Neovim's quickfix list.
- Multi-cursor exists, but composition (motions + operators + textobjects) is the main scaling strategy.
If you are used to clicking around panels in an IDE, your first bridge skill is to rely on:
- fzf pickers for file/search navigation
- quickfix for diagnostics and search results
- a file explorer for local context (Neo-tree/Yazi/Oil)
Verification And Troubleshooting
High-signal checks:
nvim --version
mise ls --current | rg neovim
nvim "+PackSync" +qa
nvim "+checkhealth" +qa
Inside Neovim, verify key workflows:
- run
:map <leader>ttand confirm Jest mapping exists. - open quickfix and test
:QFDedupe. - open a git repo file and test gitsigns navigation (
[h/]h).
If keymaps/plugins seem missing:
- confirm
chezmoi applysucceeded forhome/dot_config/exact_nvim/. - confirm plugin sync completed (
:PackSync/:PackStatusoutput). - confirm you are running the expected Neovim binary/version from mise.
Related
- Neovim local plugins — the in-repo Lua plugins (testing, git, ownership, refactors, tmux bridge, quickfix)
- Repo overview and install:
README.md - Neovim local README (short pointer):
home/dot_config/exact_nvim/readonly_README.md - Terminals
- Tmux