All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
nvim-config update dap-adaptersinstalled nothing. It handed Neovim+MasonInstall <package>through an unquoted array, so the shell split each one in two: Neovim ran:MasonInstallwith no package (E471: Argument required: MasonInstall, hidden by2>/dev/null), took the package name for a file to open, exited 0, and the CLI printed "Done". Each command is now one argument. In headless mode:MasonInstallwaits for the install and exits non-zero on an invalid package name. Checked with the real Mason in an isolated data directory, the same arguments both ways: the old form installed nothing, the new one installedjs-debug-adapter. A test runs the command with a fakenvimthat records its argumentsprofile infoprinted stray lines under the language servers and debug packages (,between two names and},after the last one). The function that reads a list from a profile file (_extract_lua_list) returned everything after each quote, so the text between two strings came back as if it were a name; the same lines went into the list of packages thatupdate dap-adaptersinstalls. It now returns only the quoted names, on one line or on several. A test compares, for every profile, whatprofile infoprints with thelsp_serversanddap_mason_packagesthat Neovim reads from the same file. The defect dates from 0.9.0 at the latest
- Pinned plugins refreshed in
lazy-lock.json: the six plugins that follow their branch and were behind, 20 commits in all.nvim-lspconfig(6: thetofu_lsfiletype, Copilot telemetry off by default, docs and CI),mason-lspconfig.nvim(4, regenerated mapping data),nvim-treesitter(1, new parser revisions),gitsigns.nvim(2: deleted lines at the end of a file, detached worktrees),schemastore.nvim(6, catalogue updates) andmini.icons(1, an icon forProjects). None of the commits is a breaking change. The three plugins that follow tags (yazi.nvimv14.0.2,mini.aiv0.18.0,toggleterm.nvimv2.13.1) are already on their latest tag, so the commits:Lazy checkcounts for them are all after the tag and are left alone. Checked at the new commits:tests/full-startupstarts the configuration, and, with the real Mason, mason-lspconfig, nvim-lspconfig and nvim-treesitter,:NvimProfile shellinstallsbash-language-serverandbashlsattaches, the parser menu and the startup install both install and colour a Go and a TOML buffer. Run:Lazy restoreto bring your own checkouts to these commits
- The parsers installed at startup are usable in the session that installs them. On a machine where Neovim's
sitedirectory did not exist at startup (the first run on a fresh machine), Neovim lists it on theruntimepathbut does not search it until the option is assigned again, so a parser installed in that session was on disk yet not found, and files opened before the install stayed uncoloured until a restart. The startup install now goes throughlua/treesitter_install.lua, shared with the parser menu: once it is over, whatever it actually installed makes the install directory visible and starts highlighting in the buffers already open; it does nothing when nothing was installed, and ignores what failed. Checked with the real nvim-treesitter from an empty data directory with a Go and a TOML buffer already open: the previous call installed both parsers and left both buffers uncoloured, the new one coloured both. This is the same cause as the fix made for the parser menu in 0.11.0 .nvim-profilecan name several profiles in the CLI, as it already could for Neovim. Neovim reads the first line of the file as a comma-separated list (web,php), butprofile infoanswered "Profile file not found: .../web,php.lua",profile listmarked no profile as active, andupdate dap-adaptersfailed the same way. They now read the file as Neovim does (first line, spaces around the names ignored, empty and repeated names dropped) and show or use every pinned profile;statusshows the normalised list. A name with no profile file is reported by name while the others are still shownprofile setaccepts a list and checks the names.profile set web,phpwritesweb,php(spaces and duplicates removed); an unknown name, an empty list, or the profile manager and picker files (init,picker) are refused with exit 2 and the list of valid profiles, also under--dry-run. Before,profile set wbewrote the file without a word and Neovim then listedwbeas active while it did nothing
0.11.0 - 2026-10-26
- Neovim offers to install a missing treesitter parser (
lua/treesitter_prompt.lua). Opening a file whose language nvim-treesitter has a parser for, and that is not installed, shows a menu: Install, Not now (also what dismissing it means: silent until the next session) or Never for , which is written totreesitter-declinedin the state directory (one language per line; delete the line to be asked again). Install downloads and compiles the grammar, says so, and starts highlighting in the buffers already open, with no restart. Nothing is asked when the installation could not work (thetree-sitterCLI, a C compiler,curlortarmissing), for windows that are not files, for the languages the configuration installs itself at startup, or without a user interface; a language is asked about once per session, and a failed install is reported once. Checked with the real nvim-treesitter:godownloaded, compiled and installed, highlighting active on the open buffer, the syntax tree parsed without error. That run also found that on a machine whosesitedirectory did not exist when Neovim started, Neovim lists it on theruntimepathyet does not search it until the option is assigned again, so a parser installed in that session was not found; the module assigns it again after an install nvim-config doctorhas a section "Language servers that need npm": it lists the servers Neovim only requests whennpmis onPATHand says whether npm was found, with the way to get it (install deps --with-node) when it was not. Until now the skipping was silent at startup and only the runtime:NvimProfilenotice named the servers. The list is read fromlua/lsp_requirements.lua, the one Neovim uses, so a server added there shows up indoctorwith no other change. The section is left out on Termux, where Neovim installs no language server, and a list that cannot be read gives a warning instead of stopping the check- Language servers for the
shellanddevopsprofiles, which had none:bash-language-server(bashls) forshell, andyaml-language-server(yamlls) anddockerfile-language-server(dockerls) fordevops. All three are npm packages, so they are requested only whennpmis onPATH; without it the profiles are exactly what they were (shellcheck and shfmt; hadolint and actionlint). Checked with the real servers and this repository'slsp.lua:bashlsruns with its shellcheck integration off (bashIde.shellcheckPath = ''). With it on, the server reports the sameSC2086that nvim-lint already reports; with it off it attaches and adds only completion, hover, definitions and symbolsyamllstakes its schemas from SchemaStore.nvim, not from its own catalogue. It attaches toyamlandyaml.docker-composebuffers but not to workflow files (yaml.github), which stay with actionlintdockerlsleaves to hadolint the three rules both check (severalCMD,ENTRYPOINTorHEALTHCHECK) and keeps its own, such as the case of instructions. Every rule is named in the settings because the server treats one it is not told about as off: a partial block silenced it completely
scripts/extract-changelog.sh <X.Y.Z> [changelog-file]prints the CHANGELOG section of a released version without its heading, the starting point of the release notes (gh release create 0.10.0 --notes-file <(scripts/extract-changelog.sh 0.10.0)). It stops at the next version or at the closing block of link references, trims the blank lines, and fails with exit 1 and a clear message for an unknown version, an empty section or a missing file, and with exit 2 for a missing,v-prefixed or malformed version- Release guard rails in
tests/release.bats:VERSION, the CHANGELOG sections and its compare links must agree.[Unreleased]comes first, the newest released section isVERSION, versions and dates are in descending order and real dates, every version has itscompare/<previous>...<version>link (the oldest one its release tag),[Unreleased]compares<VERSION>...HEAD, and there is no stray link. Each kind of inconsistency is tested on a broken copy, so a release commit that forgets to move a link is stopped by CI - A test requires the CI lint step to run
shellcheckon every script underscripts/, so the next helper cannot escape it
- Switching a profile on with
:NvimProfilenow installs its language servers. It used to enable them only, so the first buffer of that type tried to start a binary that was not there (installing happened at the next startup, throughensure_installed). A server already installed is enabled at once; a missing one is installed by Mason and mason-lspconfig, which listens forpackage:install:success, enables it when done, so it attaches to buffers that are already open. The notice lists the servers being installed, a package already being installed is not started twice (Mason raises an error if asked), a server Mason has no package for is enabled as before, and nothing is installed on Termux. Checked with the real Mason in an isolated data directory::NvimProfile shellinstalledbash-language-server, andbashlsattached to a.shbuffer; the same run on the previous code installed nothing and nothing attached - One mechanism decides which language servers are requested,
lua/lsp_requirements.lua: a server whose Mason package is installed by npm is asked for only whennpmis onPATH. It replaces the check that existed forjsonlsalone and now coversts_ls,vue_ls,svelte(web),intelephense(php) andpyright(python) too, which until now made Mason fail on every startup on a machine without Node.js. Switching a profile on with:NvimProfileenables only the servers that can be installed and says which ones it left out. A test compares the list with the Mason registry when it has been downloaded: every listed server must be an npm package and every npm server oflsp.luaand of the profiles must be listed tests/runhands bats a minimal environment (env -i) instead of the caller's. OnlyPATH,HOME,TERM,TMPDIR(when set) and the documentedNVIM_HEADLESS_TESTS_SKIP(when set) get through, and the locale isC.UTF-8, as on a CI runner. Before, it only unset the fourXDG_*variables, so anything else your shell exports could make a test pass locally and fail in CI, or the reverse: that is howXDG_CONFIG_HOMEand thenNVM_DIR(exported by GitHub's runners) each broke CI once. The whole suite passes under the minimal environment, so no test was relying on the machine. Tests check what the runner passes on and what it drops, and each rule was checked to fail when removed- CI pins brought up to date.
actions/checkoutgoes from v4.4.0 to v7.0.1: the v4 line runs on Node 20, which GitHub deprecates, and every run printed a warning (v5 and newer run on Node 24; the v7 change that blocks checking out fork pull requests underpull_request_targetdoes not concern this workflow, which only usespull_request). Neovim in theTestjob and in the newestStartupentry goes from v0.12.5 to v0.12.6; its SHA-256 was recomputed on the downloaded archive and matches the digest GitHub publishes for the release. Both were run locally first:tests/run679 passing on 0.12.6, andtests/full-startup. The oldestStartupentry stays on v0.11.0, the minimum. Newtests/ci.batsguards: every action is pinned to a full commit SHA with its version in a comment,actions/checkoutis v5 or newer, and theTestjob and the newestStartupentry pin the same Neovim, checksum and step name (the procedure asks to change them together; nothing checked it)
0.10.0 - 2026-10-20
]t/[tjump to the next / previous TODO / FIXME / HACK / NOTE comment (todo-comments, declared as lazy keys so the first press loads the plugin). They replace Neovim's built-in tag-stack motions (:tnext/:tprevious), which this config does not use; documented in the usage guide and the keymaps reference, described in which-key. Checked with the real plugin on a sample file:]tvisits the tags in order and stops on the last one with the plugin's own message,[tgoes back- yazi integration (
lua/plugins/yazi.lua, yazi.nvim v14.0.2 pinned inlazy-lock.json):<Leader>-opens yazi, with image / video / PDF previews, at the current file in a floating window (:Yazi cwdfor the working directory). It complements neo-tree and oil, which keep opening directories (open_for_directories = false). Loaded only when theyazibinary is onPATH, tested once at startup, and never installed for you. The plugin's suggested<Leader>cwand<C-Up>are not used here (the first is the CodeCompanion group, the second resizes windows). Verified with the real plugin: without yazi there is no:Yazi, no mapping and no message; with yazi 26.9.1 both are present and the plugin's own:checkhealth yaziis green.nvim-config doctorlists yazi among the optional tools. lazy.nvim clones the plugin even when yazi is absent (condonly prevents loading it) :Cheatextension points (lua/cheatsheets.lua). Topics now come from the built-indocs/cheatsheets/, from thecheatsheetsdirectories declared by the active profiles, and from your own directories:~/.config/nvim-cheatsheets/and every directory of$NVIM_CHEATSHEET_DIRS(colon-separated). On a name clash the later source wins (user over profile over built-in), so a built-in sheet can be replaced. Computed on each call, so a profile switched on with:NvimProfileshows its sheets at once. Theshellanddevopsprofiles each ship a sheet (docs/profile-cheatsheets/), visible only while they are activetests/full-startup: starts the real configuration with all its plugins (cloned at the commits pinned inlazy-lock.json, in a throwaway directory) and fails on any error, or if the config was not actually read. Not part oftests/run(network, about 200 MB). Checked to pass on real 0.11.0 and 0.12.5 and to fail on 0.10.4 and on an injected error in a plugin config. CI runs it as theStartupjob on the oldest supported Neovim (0.11.0) and on 0.12.5; it is deliberately not a required checkscripts/ci-install-neovim: installs an official Neovim release for CI after verifying its SHA-256, and refuses moving refs such asstableand anything whose checksum differs- Neovim version guard:
init.luanow starts withlua/minimum_version.luaand, on a Neovim older than 0.11, printsThis configuration requires Neovim 0.11 or newer (found 0.10.4)and stops before loading anything, instead of failing with a Lua error from the LSP setup. Verified on real 0.10.4 (message, nothing loaded) and 0.11.0 (normal start). The minimum lives in one place and a test checks it agrees with the README
- The CLI follows the XDG base directories (
compatibility.md). Before, it wrote under$HOMEwhateverXDG_*said. Now: the log directory is${XDG_STATE_HOME:-~/.local/state}/nvim-config/logs;install configanduninstall configuse${XDG_CONFIG_HOME:-~/.config}/nvim, which is where Neovim reads its configuration (withXDG_CONFIG_HOMEset, the link in~/.config/nvimwas never used); completions go to${XDG_DATA_HOME:-~/.local/share}/bash-completion/completions(bash),${XDG_CONFIG_HOME:-~/.config}/fish/completions(fish) and${XDG_DATA_HOME:-~/.local/share}/zsh/completions(zsh); the fish file offered for mise activation is${XDG_CONFIG_HOME:-~/.config}/fish/config.fish. With noXDG_*variable set, bash, fish, the log directory and the config link keep their previous paths.NVIM_CONFIG_LOG_DIRand--log-dirstill win overXDG_STATE_HOME - Migration, zsh completion: it is no longer installed in
~/.zfunc, a directory nothing of this configuration puts onfpath, but in${XDG_DATA_HOME:-~/.local/share}/zsh/completions. Runnvim-config --install-completion zshagain and put that directory on yourfpath(README, Shell Completion).--install-completion zshwarns when a leftover~/.zfunc/_nvim-configexists; remove it - The daily log file is appended to instead of overwritten.
nvim-config-YYYY-MM-DD.logused to keep only the last run of the day. It now keeps them all, and each run starts with a lineRun: nvim-config <version> <arguments>, written to the file only (never on the terminal), so the runs can be told apart --dry-runno longer talks about the simulated action in the past tense.install configsaysWould link ...instead ofLinked ...,uninstall configsaysWould remove ...instead ofRemoved ..., anduninstall miseends withWould remove the mise binary ...instead ofmise binary removed .... A real run keeps its past-tense messages. A sweep test runs every mutating command under--dry-runon a throwaway copy of the project, checks none claims a finished action and that no file changed- CI no longer installs Neovim
stablewith an unverifiedsudo tar: theTestjob uses v0.12.5 with its pinned SHA-256, and bats-core is fetched by commit (6636e2c, v1.9.0) rather than by a tag that can be moved. The trade-off is that CI no longer follows new stable releases by itself: moving the pin is a deliberate pull request (procedure in CONTRIBUTING.md).shellcheckin theLintjob also covers the new scripts.tests/ci.batsguards all of it, including that the oldest Startup version equals the minimum enforced byinit.lua - CI no longer runs twice on the same commit:
pushis limited tomain, and a topic branch is tested through its pull request (on the result of merging it intomain). Before, the branch prefixes underpushpluspull_requeststarted two identical runs a few seconds apart.mainis still tested after every merge,workflow_dispatchstays, and theLint/Testjob names required by themainruleset are unchanged. A branch pushed without a pull request is no longer tested. Newtests/ci.batsguards the triggers and those job names tests/runclearsXDG_CONFIG_HOME,XDG_DATA_HOME,XDG_STATE_HOMEandXDG_CACHE_HOMEbefore running bats, so a local run behaves like CI. On a developer machine they usually point at the installed config (~/.config/nvimis a symlink to this repository), which let a test pass locally and fail on a clean runner — what broke CI during 0.9.0. A test checks the runner does it
install tmuxno longer writes through a symlinked~/.tmux.conf. With the file managed by a dotfiles repository, thesource-fileline was appended to that repository's file behind its back. It now stops with an error (also under--dry-run) that names the target and gives the line to add by hand. A symlink whose target already sources this repository is still recognised, and a regular file is appended to as beforeinstall deps --with-nodechecked~/.nvmwhile nvm's installer installs into$XDG_CONFIG_HOME/nvmwhen that variable is set, so a second nvm could be installed, or the laternvm.shnot be found. The directory is now worked out once (an exportedNVM_DIR, else an existing~/.nvm, else the one nvm itself would choose) and handed to the installer- Docs / code consistency audit (CLI surface, profile tables, 68 documented
<Leader>keys and 66 defined ones, referenced paths, apt packages, env vars). Fixed what disagreed:docs/usage.mdcreditedtodo-comments(and, in the neotest section, "if configured") with]t/[tthat did not exist: in Neovim 0.11+ those keys are the built-in tag-stack motions (:tnext/:tprevious). The mapping now exists for real, see Added; the table also lists:TodoQuickFixand:TodoLocList, and the neotest sentence points at the summary panel- the swap keys
]a/[ashadow Neovim's built-in argument-list motions (:next/:previous); they had been described as free. The editing cheatsheet now says what they replace install allalso fetches the tree-sitter CLI, but--help, the zsh and fish completion descriptions and the README only said "nvim, deps, then config"- the usage guide listed the debug adapters of web, php and rust but not python's
debugpy - CONTRIBUTING asked to run shellcheck on
bin/nvim-configonly, while CI checks three files - the README did not document the log file (location, one file per day),
--log-dir,--no-log,NVIM_CONFIG_LOG_DIRorNO_COLORNew tests keep these aligned: every profile debug package must appear in the usage guide, CONTRIBUTING's shellcheck command must cover the CI lint files, and the help / completion descriptions are checked
- The zsh completion assigned
update_subcmds,profile_subcmds,ui_subcmdsandtheme_subcmdswithout declaring themlocal, leaking four variables into the user's shell, and assignedcommandstwice (the first assignment was dead code). A test checks every array it assigns is local --install-completion <shell> --dry-runcreated the completion directory and wrote the file even though--dry-runpromises to change nothing, and then saidInstalled .... It now printsWould install <shell> completion to ...and touches nothing:Cheat <topic>built the file name from what you typed, so:Cheat ../../notesread any.mdfile reachable from the cheatsheet directory. Topics are now looked up in the index of known sheets and anything else answers "Cheatsheet not found"- The documented minimum Neovim version was wrong. The 0.9.0 release notes and the README said
0.10+, a figure derived only from native commenting. The LSP setup uses
vim.lsp.configandvim.lsp.enable, which arrived in 0.11, so on 0.10 the configuration fails at startup (attempt to call field 'config' (a nil value)). Checked by starting the real config, with its plugins, on 0.10.4 (fails), 0.11.0, 0.11.7 and 0.12.0 (clean start). The README now says 0.11+ with 0.12 recommended: themainbranch of nvim-treesitter officially requires 0.12. The test suite itself passes on 0.10.4, so it cannot catch this kind of mismatch; a test now ties the stated minimum to the APIslua/uses
0.9.0 - 2026-10-19
devopsprofile: hadolint on Dockerfiles and actionlint on GitHub Actions workflows (nvim-lint), plus thedockerfiletreesitter parser. Auto-detected from aDockerfile,compose.y*ml,docker-compose.y*mlor.github/workflows/; otherwisenvim-config profile set devops. No language server for now (the YAML and Dockerfile servers are npm packages)- Profiles can declare
mason_packages, a generic field for Mason-installed tools that are not in apt (newprofiles.get_mason_packages()andinstall_mason_packages()). They are installed in the background at startup and when a profile is switched on with:NvimProfile, and skipped on Termux. A test checks every package name declared by a profile exists in the Mason registry nvim-config doctor: a "DevOps tools" section for actionlint and hadolint. It also looks in Neovim's Masonbindirectory, which is on Neovim'sPATHbut never on the shell'sshellprofile: shellcheck diagnostics (nvim-lint) and shfmt formatting (conform) forshbuffers — Neovim files.sh,.bash,.batsand shebang scripts all under that one filetype. Auto-detected from.shellcheckrc, a root-level*.sh/*.bash, ortests/*.bats; otherwisenvim-config profile set shell. Both tools are expected onPATH(sudo apt install shellcheck shfmt) andnvim-config doctornow lists them. No language server for now:bash-language-serveris an npm package and would duplicate shellcheck's diagnostics (recorded as deferred)docs/usage.md: "Extending completion to other Lua projects" subsection under## LSP— generalizes this repo's ownlua_lsworkspace.librarypattern (already used for its own Neovim API completion) into a reusable recipe for any Lua-based tool with LuaCATS type stubs, with WezTerm (DrKJeff16/wezterm-types) as a worked exampledocs/cheatsheets/basics.md— foundational vanilla Vim/Neovim reference (modes, leader key, cursor movement, basic editing, windows/tabs, command-line mode), not specific to this config; fills a real gap for newcomers since every other cheatsheet assumes prior Vim knowledge and only documents this config's own additions on top of it. Picked up automatically by:Cheat(topics are discovered dynamically from the directory); linked from README's "Documentation" section as the suggested starting pointdocs/cheatsheets/editing.md: new "Commenting (native Neovim, no plugin)" section —gcc,5gcc(count), visualgc,gc{motion}(gc2j,gcip,gc}) anddgc; commenting had no documentation anywhere before- Profiles can declare
treesitter_parsers(newprofiles.get_treesitter_parsers()), installed at startup alongside the global list, and — new — also when a profile is switched on at runtime with:NvimProfile(previously a restart was needed for anything beyond LSP/null-ls). Declared:php→php(which also lets native commenting pick<!-- -->inside HTML regions of mixed PHP/HTML files — the html parser comes in as a dependency),laravel→blade,web→tsx/vue/svelte,rust→rust/toml,python→python/toml. Without a parser a language gets no treesitter highlighting, folds or textobjects (af,]m,]a…); a new test checks every declared name exists in nvim-treesitter - Profiles documentation brought in line with
lua/profiles/: the README,docs/usage.mdanddocs/cheatsheets/profiles.mdlisted onlyweb/php/laravel/rust, whilepython,termuxandaialready existed. Detection markers (python files,TERMUX_VERSION,aibeing opt-in) and a per-profile table (LSP, parsers, debug adapter) are now documented, and a test fails if a profile file is missing from any of the three docs nvim-config profile createtemplate now includes thetreesitter_parsersfieldssh_configadded to the global treesitter parser list (thesshconfigfiletype maps to it automatically)docs/cheatsheets/editing.md: new "Treesitter textobjects" section documenting select (af/if,ac/ic), move (]m/[m,]M/[M,]]/[[,][/[]), and swap (]a/[a) — none of theselua/config/treesitter.luakeymaps were documented anywhere before
- GitHub Actions workflow files (
.github/workflows/*.yml|yaml) now have the compound filetypeyaml.githubinstead ofyaml, so actionlint can be attached to them and not to every YAML file (it reports errors on anything that is not a workflow).yamllinters and formatters, including the web profile's prettier, still apply: nvim-lint and conform look tools up per dot-separated part of the filetype, and treesitter maps it to theyamlparser - conform no longer formats
shbuffers on save (shfmt stays available on demand with<Space>f): shfmt's defaults rewrite most of an existing script on first save — measured onbin/nvim-config, ~970 of 1930 lines with-i 2, ~2470 with the tab default that applies when a.editorconfigexists but has no section matching the file uninstall nvim/uninstall misewith--dry-runnow print the removal plan (themise unuse/mise uninstall,snap removeorapt removecommand, the binary and the shell rc cleanup) instead of "Skipped — … left installed". A dry-run executes nothing, so the consent prompt is skipped there; without a TTY or with--quieta real run still declines.install's prompts are unchanged: they choose between alternatives, so declining still leads somewhere informative. Previously this logic could only be checked by reading the source; fakemise/snaptests now cover each detection path- Commenting now uses Neovim's native
gc/gccinstead ofComment.nvim(removed:lua/plugins/comment.lua).gc(visual/operator),gccand counts keep working;gb/gbc(block comments) andgco/gcO/gcAare gone — none of the maintained alternatives offer block-comment toggling either. Minimum supported Neovim goes from 0.9 to 0.10 (README updated) since native commenting was added in 0.10. Run:Lazy cleanto drop the plugin and itslazy-lock.jsonentry - php buffers use
// %sascommentstring(lua/options.lua): Neovim's own default is/* %s */, which would wrap every commented line in a block comment —Comment.nvimused//for php, so this keeps that behaviour lua/config/treesitter.lua: treesitter textobjects swap keymaps moved from<Leader>a/<Leader>Ato]a/[a—<Leader>ais also the which-key group prefix for the AI (Avante) submenu (<Leader>aa,<Leader>ae, …), and having a real keymap on the exact prefix key meant pressing<Leader>aeither fired the swap immediately or raced the AI submenu against Vim'stimeoutlen, depending on typing speed. Bracket motions match the convention already used for other treesitter textobject moves in the same file ([m/]m,[[/]],[h/]h) and fully resolve the ambiguity
nvim-config profile detectignored thepythonandtermuxprofiles that the Lua detection has; it now mirrors all detectors (andshell)nvim-config update(andupdate tree-sitter) exited with status 1 and no message when the GitHub API call for the latest tree-sitter release failed (offline, or the anonymous 60 requests/hour rate limit): the failingcurl | grep | sedpipeline sits in a plain assignment, which ends the script underset -ebefore the error could be logged. It now logs "Could not fetch latest tree-sitter release tag" and returns 1.install allwas unaffected only because errexit is suppressed in its call tree--dry-runno longer fails when that API is unreachable: it warns that the release could not be queried and succeeds, instead of making a dry run depend on the network (this made four unrelatedcli.batstests fail whenever the quota was exhausted)- The tree-sitter download is now checked step by step (
curl -f,gunzip,chmod): a 404 or a failed download used to be written to disk as the "archive", fall through, and end with a "tree-sitter updated" message when run frominstall all - A vendored
tree-sitterbinary that cannot run (wrong architecture, corrupt) is now treated as outdated and replaced, instead of ending the script silently uninstall nvimno longer stops silently after the confirmation whenmise current neovimfails; it falls through to the existing "could not determine the installed neovim version" warninggc/gccfailed with a bare[Comment.nvim] nilnotification (nothing got commented) in every buffer whose filetype has no treesitter parser — ssh config, and php unless a parser was installed. On Neovim 0.12vim.treesitter.get_parser()returnsnilinstead of raising, which Comment.nvim'sft.calculatedoesn't expect (attempt to index local 'tree'); its error handler then printserr.msg, which isnilfor a plain string error, hiding the real cause. Comment.nvim has had no commit since June 2024, so no upstream fix; native commenting falls back tocommentstringinsteaddocs/cheatsheets/editing.md: removed a stale<C-z>→ Undo entry — that binding was intentionally removed in an earlier change (conflicts with the shell'sSIGTSTP, see<C-s>/<C-z>/<C-r>keymap cleanup in this CHANGELOG's history) but the cheatsheet was never updated to match
0.8.0 - 2026-09-10
nvim-config uninstall [config|nvim|mise|deps|all]— reverses the correspondinginstallsubcommand(s): removes the~/.config/nvimsymlink (only if it points at this repository, matchinginstall config's own safety check), removes Neovim (detects mise/snap/apt the same wayupdate nvimdoes, asks for confirmation), removes the mise binary and the shell activation linesinstall miseadded (via a new_remove_mise_activationhelper, matched by the same marker comment_offer_mise_activationwrites; other tools mise manages are left untouched).uninstall depsnever runsapt remove— the packagesinstall depsinstalls (git,make,gcc,curl,ripgrep,fd-find,xsel,xclip,wl-clipboard,lolcat,gnupg) are shared system dependencies other tools may also rely on, so it only prints the manual removal command;uninstall allfollows the same policy and excludesdeps.uninstall nvim/uninstall miseare gated behind_confirm, so they auto-decline under--dry-run/--quiet/outside a TTY like every other prompt in this CLI. Shell completion (bash/zsh/fish) and--helpupdated; README gained an "Uninstalling" section
- CI (
.github/workflows/ci.yml) no longer runs on every push and every ref:pushis now scoped tomain/developand the conventional branch prefixes (feat/,fix/,chore/,docs/,test/,release/,hotfix/), tag pushes no longer trigger a run (a tag should already point at a commit CI validated onmain), andpull_requestis scoped to PRs targetingmain. Addedworkflow_dispatchfor manual runs and aconcurrencygroup withcancel-in-progressto stop superseded runs, both already mandated by this project's own CI conventions but missing from the actual workflow - A new
detect-changesjob determines whether a push/PR touched only documentation (**/*.md,docs/**,.editorconfig);lint/testare skipped via job-levelif:when it did. The workflow itself still always triggers and reports — a job skipped byif:satisfies a required status check, whereas a workflow that never runs at all leaves a PR stuck waiting indefinitely, which is why this isn't implemented as a trigger-levelpaths-ignore
.private/andvendor/(the downloadedtree-sitterCLI) were only kept untracked by the maintainer's personal global~/.gitignore, not the project's own.gitignore— invisible to anyone else cloning the repo, e.g.git add -Awould have picked them up. Both are now explicitly ignored at the project level, alongside a missing.nvim-profileentry (the third local per-project override file —.nvim-ui/.nvim-themewere already covered) and standard OS/editor artifacts
0.7.1 - 2026-09-08
SECURITY.md— vulnerability reporting policy, scoped to the CLI's privileged/network operations (sudo installs, mise's GPG-verified installer, tree-sitter CLI download, shell rc writes)CONTRIBUTING.md— contribution workflow (branch/commit conventions, TDD requirement, test/shellcheck steps); linked from a new README "Contributing" section
- Shell completion:
nvim-config ui set <TAB>andnvim-config theme set <TAB>incorrectly offeredlist detect set unset create info(profile's subcommands) instead ofclassic modern/ the 14 theme names. Bash completion matched on the immediately preceding word only, andset/unset/infoare shared betweenprofile/ui/theme, so theprofilepattern matched first — a dedicatedclassic|modern)case existed but was unreachable dead code. Bash completion now dispatches on the top-level command first, then disambiguates within it; zsh and fish gained the missing third-level completion (theme names) for parity with theui/classic/moderncase fish already had
0.7.0 - 2026-09-08
install miseCLI subcommand — installs the mise version manager via curl, verifying the official release GPG key (24853EC9F655CE80B48E6C3A8B81C9D17413A06D) before executing the installer; idempotent (skips ifmiseis already present)install nvim(no explicit flag) now cascades interactively: usemiseif present → else prompt to install it → else usesnapif present → else prompt to installsnapd→ else fall back toapt; prompts auto-decline outside a TTY, under--quiet, or under--dry-run(never blocks CI or scripted runs)update nvimprefersmiseautomatically when Neovim was installed through it (mise use -g neovim@latest), falling back to the existing snap/apt auto-detection otherwise--mise,--snap,--aptflags now bypass the interactive cascade entirely and fail fast with a clear error if the corresponding tool is missing, instead of silently attempting to run itnvim-config doctor—miseadded to the optional tools check- After a fresh
install mise, the CLI checks whether mise's shell activation line is already present in the user's shell rc (~/.bashrc,~/.zshrc, or~/.config/fish/config.fish, based on$SHELL) and, if not, prompts to add it and reminds the user to reload their shell — required formiseand mise-managed tools (likenvim) to be found onPATH, per mise's activation docs --activate/--no-activateflags — bypass the mise shell-activation prompt in either direction without needing an interactive terminal:--activateadds the line unattended,--no-activateskips it entirely and just logs the manual command. Useful for automated/scripted installs, where the default prompt already auto-declines (no TTY) but an automation author may still want the line added on purposeinstall depsnow also installswl-clipboard, alongside the existingxsel/xclip, for Wayland clipboard support — previously only X11 tools were installed, so system clipboard integration silently didn't work on Wayland sessions (the default on Ubuntu 22.04+). Both are installed unconditionally rather than auto-detected at install time, sinceinstall depsmay run over SSH or before any graphical session starts, when session-type detection would be unreliable; Neovim's own clipboard provider already picks the right tool at runtime
install nvim/install allno longer crash withsudo: snap: command not foundon systems without snap (e.g. WSL distros without snapd) — the missing case is now handled by the interactive cascade instead of being assumed availablelog_message:ERROR-level messages were printed twice — once colored on stdout, once plain on stderr. Now printed exactly once, on stderr only, per the project's error-handling conventioninstall nvimauto mode no longer aborts the whole cascade when the optionalmise(orsnapd) install step fails (e.g.gpgmissing) — it now logs a warning and falls through to the next fallback (snap, then apt) instead of failing outright, matchingmise's "optional" statusinstall allno longer silently skipsinstall deps/install configwhen thenvimstep fails — each step now runs independently and the command reports overall failure via its exit code instead of aborting on the first error (a side effect ofset -ecombined with the unguardedrun_install_nvimcall)install depsnow installsgnupg, required to verify themiseinstaller signature (previouslyinstall misecould fail on a fresh machine withgpg is requiredeven after runninginstall deps)install nvimauto mode: accepting the "install mise?" prompt whencurland/orgnupgare missing used to fail with a baregpg is requirederror and silently fall through to snap/apt, defeating the user's explicit choice of mise. The prompt now detects and lists every missing prerequisite up front (e.g. "mise is not installed and requires: gnupg. Install gnupg + mise now?") and installs it via apt before installing mise, so answering "yes" actually results in mise being usedrun_install_mise: fixedtmp_dir: unbound variablecrash at script exit after a successful (or failed) mise install. The cleanuptraponEXIT INT TERMreferenced alocalvariable that goes out of scope once the function returns; since traps are process-global, the stale reference blew up later when the whole script exited (e.g. at the end ofinstall all, afterinstall confighad already succeeded). The trap is now explicitly cleared (trap - EXIT INT TERM) before every return path- All
apt installcalls the CLI runs on the user's behalf (deps, nvim via apt, mise prerequisites, snapd) now pass-y— the user already confirmed the action (by invoking the command or answering an interactive prompt), so apt's own confirmation prompt was redundant friction, not an extra safety check install nvim --mise/install all --miseused to error immediately ("mise is required — run 'install mise' first") and leave bothmiseandnvimuninstalled, requiring a separate manualinstall miserun — even though passing--miseis already an explicit, unambiguous choice. It now installs mise on the spot (missing prerequisites likegnupgincluded, no extra prompt — the flag itself is the consent) before using it.--snapgets the same treatment: installssnapdvia apt first if missing, instead of erroring.--aptis unchanged (still errors ifaptitself is absent — that signals the wrong OS, nothing to bootstrap)- Fixed a
set -e/errexitgotcha that letinstall nvim --miseattemptmise use -g neovim@latesteven after the mise install had already failed (e.g. on missinggnupg), producing a rawmise: command not foundinstead of stopping cleanly. Root cause:install allcallsrun_install_nvim || { ...; }— in bash, using a function call as the left side of||disableserrexitfor its entire call subtree, not just its own top-level exit status, so a failingrun_install_misedeep inside_install_nvim_viano longer aborted anything on its own. Every fallible step in_install_nvim_via, the_install_nvim_autocascade, andrun_install_depsis now explicitly guarded (||/&&) instead of relying on implicit propagation run_install_mise: even after a fully successful install,mise use -g neovim@lateststill failed withmise: command not foundin the same run — the installer places the binary at~/.local/bin/mise, which usually isn't onPATHuntil a login shell picks it up, so the process runningnvim-confignever saw it. The CLI now adds~/.local/binto its ownPATHright after a successful install (falling back to a clear error ifmisestill can't be found), so it can usemiseimmediately without waiting for a shell reload- The "reload your shell" reminder after adding mise's activation line used to appear once, mid-run, right before a wall of
install depsapt output — easy to miss.install nvim/install mise/install allnow repeat it once more at the very end of the command if activation was added during that run - The generated activation line used bare
mise activate bash, which still didn't work even aftersource ~/.bashrc: that line has to runmiseto produce its own output, butmiseisn't resolvable viaPATHuntil activation has already run — a chicken-and-egg problem. The line now uses the absolute path to the mise binary (eval "$(/home/user/.local/bin/mise activate bash)"), exactly as mise's own installer recommends, and the rc file also gets an explicit~/.local/binPATH guard as defense in depth - That PATH guard line itself is now idempotent at shell-startup time, not just at file-write time: for bash/zsh it's a
case ":$PATH:" in ...check that only exports if~/.local/binisn't already present, and for fish it uses the built-infish_add_path(which deduplicates automatically). Previously it was an unconditionalexport PATH=..., so every manualsource ~/.bashrcwould prepend another duplicate entry install depsnow installsgit— it was documented as an assumed prerequisite but never actually installed, so a machine without it hit a raw, unreadable Neovim crash on startup (E5113: ... E475: Invalid value for argument cmd: 'git' is not executable) instead of a clean error.lua/plugins.luaalso now checksgitis executable before attempting the lazy.nvim bootstrap clone and, if missing, prints one clear actionable message (git is required ... — nvim-config install deps or sudo apt install git) instead of letting the rawE475propagate and silently skip the rest of plugin/bootstrap loadingnoice.nvim: list-style command output (:scriptnames,:marks,:registers,:highlight,:map,:autocmd,:command,:buffers, ...) shared the samenotify-view route as:echo/:echomsg, withopts.replace = true. Sincenvim-notifyauto-dismisses after its configured timeout, calling the same (or another) list-style command again after the notification had already closed tried to replace an ID that no longer existed and silently showed nothing.list_cmd-kind messages are now routed to asplitview instead, which has no such lifecycle and is a better fit for long, browsable output anywayinstall depsnow installsmake— it was checked bynvim-config doctorunder "Required binaries" but never actually installed, sotelescope-fzf-native.nvim's native build silently failed on a fresh machine (/bin/bash: line 1: make: command not found), same class of oversight as thegitfix abovemakealone isn't enough to buildtelescope-fzf-native.nvim: its Makefile also callscc(make: cc: No such file or directory).install depsnow installsgcctoo, andnvim-config doctormovesgccfrom the optional "Build tools" section to "Required binaries" (cargostays optional — only needed for Rust-profile DAP tooling) sincetelescope-fzf-native.nvimis a baseline plugin (loaded by the default "classic" UI variant, not profile-gated)- Removed
vim.opt.clipboard = 'unnamedplus': with it set, every yank/delete/put (x,dd,p,y, ...) shelled out to the system clipboard tool (xclip/xsel/wl-copy) on every single keystroke. On slower setups (e.g. containers) this lagged enough that holding a buffer-modifying key down would only register the first press — repeat presses were silently dropped while Neovim waited on the external process (confirmed: reproduced withx/p, absent with~/u, which don't touch a register; disappeared entirely with:set clipboard=). The config already provides explicit, on-demand system-clipboard access viaYY/XX/PP(yank/cut/paste, seedocs/keymaps.md), making the automatic sync both redundant and the source of the lag — normal editing no longer touches the system clipboard at all jsonlsauto-install is now gated onnpmbeing executable —json-lspis an npm package installed by Mason vianpm install, which failed on every Neovim startup with a Mason error (Could not find executable "npm" in PATH) on any machine without Node.js. Node.js is intentionally opt-in in this config (install deps --with-node), sojsonlsis now simply skipped whennpmis missing instead of repeatedly retrying a known failure;lua_ls(a standalone binary, no npm needed) is unaffected and always auto-installsnvim-treesitterfailed to build parsers on first launch (Error during "tree-sitter build": ... ENOENT ... 'tree-sitter'): it shells out to a baretree-sittercommand with no way to configure its path, and whilenvim-config update tree-sitteralready downloaded the CLI intovendor/tree-sitter/, nothing ever added that directory to Neovim'sPATH, and the command itself was never mentioned in the README's setup instructions.lua/config/treesitter.luanow prependsvendor/tree-sittertoPATHwhen the vendored binary is present, andinstall allnow also runsupdate tree-sitterautomatically (idempotent — downloads only if missing/outdated), so parser installs work out of the box on a fresh install without a separate manual step
- Update pinned plugin commits in
lazy-lock.json(routine:Lazy sync) — 30 plugins bumped to their latest tracked commit, no plugin added or removed
0.6.1 - 2026-09-01
- Harden GitHub Actions CI workflow: pin
actions/checkoutto a full commit SHA instead of the mutablev4tag, addpermissions: contents: readat workflow level (least privilege), disablepersist-credentialson checkout, pinbats-coreinstall to a tagged release instead of the default branch, and fail closed on HTTP errors when downloading Neovim (curl --fail)
- Update pinned plugin commits in
lazy-lock.json(routine:Lazy sync); note thatneotest-rust's pin was dropped as a side effect of syncing without therustprofile active — the plugin spec inlua/profiles/rust.luais unchanged and will simply reinstall unpinned on the next sync with that profile active
0.6.0 - 2026-05-18
aiprofile (lua/profiles/ai.lua) — opt-in flag profile activating the full AI tooling stack; never auto-detected; activate via.nvim-profileornvim-config profile set ai; all five AI plugins (avante, codecompanion, codeium, mcphub, mcp-diagnostics) now guard theircondwithis_active('ai') and not is_active('termux')mcp-server-filesystemandmcp-server-fetchadded tolua/config/mcphub.lua: filesystem gives AI scoped read/write access to CWD; fetch retrieves web pages as Markdown (both official Anthropic servers)- Python profile (
lua/profiles/python.lua) — auto-detected frompyproject.toml,setup.py,setup.cfg, orrequirements.txt; LSP:pyright; formatter:ruff_formatthenblack; linters:ruff,mypy; DAP:debugpy(Mason); neotest:neotest-pythonwith two launch configurations (file and module) nvim-config doctor: Python tools section (python3,pyright,ruff,black,mypy) shown as optional checks
0.5.0 - 2026-05-12
- Termux profile (
lua/profiles/termux.lua) — Android/ARM environment profile; auto-detected via$TERMUX_VERSION; disables DAP adapters, avante, and codeium; LSP limited tolua_ls;install depsroutes topkg install(nosudo);doctorshows Termux-specific binaries mcphub.nvim— MCP client hub;:MCPHubUI;<Leader>am; integrates with avante and codecompanion; disabled on Termux; build installsmcp-hubvia npm- MCP servers:
mcp-server-git(git log/diff/blame viauvx),Context7(versioned library docs vianpx @upstash/context7-mcp),mcp-diagnostics(LSP diagnostics via bundled Node.js server) mcp-diagnostics.nvim— exposes Neovim LSP diagnostics to AI via MCP; loaded onLspAttach; disabled on Termuxcodecompanion.nvim— AI chat + inline assistant alongside avante for evaluation; default adapterclaude_code(CLI, no OAuth); also supportsanthropicandgemini_cli; native MCP via mcphub; honoursCLAUDE.md/.cursor/rules;<Leader>cctoggle,<Leader>cxactions; disabled on Termuxmini.icons(echasnovski/mini.icons) — lightweight icon provider required by which-key health check<Leader>tl— switch to last accessed tab; tab index tracked viaTabLeaveautocmd- Bracketed paste: auto-enable
pastemode on xterm\e[200~/ auto-disable on\e[201~; works in SSH and tmux sessions
lua/profiles/web.lua: remove stalenull_ls_sourcesfordiagnostics.eslintandformatting.prettier— both migrated to nvim-lint/conform;diagnostics.eslintremoved from none-ls builtins in recent versions causing nil-index crash on startuplua/config/lsp.lua: skipmason-lspconfigensure_installedon Termux — Mason ARM binaries often unavailable; servers must be installed manuallyrender-markdown.nvim: removeheading.signandcode.sign(invalid in current version, causedtable - expected: nil, got: tablehealth ERROR); addlatex = { enabled = false }to suppress LaTeX tool warningsnoice.lua: enablelsp.overrideforconvert_input_to_markdown_lines,stylize_markdown,cmp.entry.get_documentation— improves markdown rendering and eliminates 3 health warnings; hover and signature remain nativeoptions.lua: disable unused language providers (python3,ruby,perl,node) — eliminates 4:checkhealthwarningswhich-key.lua: addicons.keys = false— eliminatesmini.icons not installedwarningtreesitter.lua: addregexparser — required by noice.nvim for cmdline regex highlightingbin/nvim-config: resolve symlinks inSCRIPT_DIR(readlink -f) —PROJECT_DIRwas resolving to$HOMEwhen invoked via~/bin/nvim-configsymlink, causinginstall configto link~/.config/nvim → $HOMEcommands.lua: skip trailing whitespace trim on non-modifiable / readonly buffers (E21 on:win:checkhealthand help buffers)
0.4.1 - 2026-05-05
nvim-config update tree-sitter— downloads the latest tree-sitter CLI binary (Linux x64/arm64) intovendor/tree-sitter/; skips when already up to date; integrated intoupdate all; bash/zsh/fish completions updated.luarc.json: configures lua-language-server for the Neovim environment (LuaJIT runtime, snap runtime library path,vimdeclared as global) — eliminates 200+ false-positive "Undefined globalvim" diagnostics
- treesitter: parser compilation failed with "subcommand 'build' not recognized" — vendored
tree-sitterCLI was 0.20.6; nvim-treesitter v1.0 requires >= 0.22; updated to 0.26.8 - LSP:
ts_lsattached to.kdlfiles (Zellij config) because the extension had no registered filetype; addedvim.filetype.add({ extension = { kdl = 'kdl' } })so ts_ls ignores them - avante:
<Leader>aPpicker threw E5108 "Failed to find provider: X" when switching to ACP providers (claude-code, gemini-cli, codex) — avante'srefresh()looks them up in the API provider module table where they don't exist;Config.provideris already updated before the error, so the switch succeeds; wrapped inpcalland suppressed the known error
docs/keymaps.md: add missing sections for nvim-ufo (zR/zM/Kwith async fold note) and marks.nvim (m[a-z],]'/[',]`/[`); clarify<space>froutes through conform.nvim
0.4.0 - 2026-04-27
conform.nvim: formatter replacing none-ls formatting sources; profile-drivenformatters_by_ft;format_on_save(1 s timeout, LSP fallback);<Space>fglobal keymap; local binary resolution (node_modules/.bin,vendor/bin) evaluated at format timenvim-lint: linter replacing none-ls diagnostic sources; profile-drivenlinters_by_ft; event-driven (BufWritePost,InsertLeave); local binary resolution evaluated at lint time; phpmd ruleset configured to match prior none-ls settings- Profiles:
conform_formatters()andlint_linters()fields added to web (prettier/eslint), php (php_cs_fixer/phpstan+phpcs+phpmd), laravel (blade_formatter), rust (rustfmt) profiles.get_conform_formatters()andprofiles.get_lint_linters()inlua/profiles/init.lua
- GitHub Actions CI workflow (
.github/workflows/ci.yml):lintjob (shellcheck onbin/nvim-config) andtestjob (bats-core + Neovim stable); triggers onpushandpull_request - CI status badge in
README.md tests/run: post-run summary — total / passing / failing counts and elapsed time; failing tests listed by file (alphabetical) with tests sorted within each filenvim-ufo: LSP/treesitter-aware folding; provider chain lsp → indent;zRopen all folds,zMclose all folds;Kpeeks folded lines under cursor and falls back to LSP hover when not foldable; fold options (foldlevel=99,foldlevelstart=99) set inoptions.luamarks.nvim: visual marks in the sign column;m[a-z]to set,]'/['to navigate by line,]`/[`by column; builtin marks (. < > ^) displayedoil.nvim: edit the filesystem as a buffer (rename, move, delete via normal editing);-opens the parent directory of the current file; coexists with neo-tree (default_file_explorer = false)noice.nvim: floating cmdline (cmdline_popupcentered at 40% row), popup messages, LSP progress indicator;notify.enableddisabled in modern variant (snacks.nvim ownsvim.notify); LSP hover/signature kept native to avoid conflictsnvim-config status— shows active Neovim version, UI variant, theme, and profile at a glancenvim-config doctor— checks required binaries (git, make, rg, fdfind, node, npm) and optional tools (lazygit, tmux, claude, codex, gemini, cargo, gcc)nvim-config theme info— prints the active colorscheme;theme listlists all 14 themes and marks the active one with*nvim-config ui info— prints the active UI variantnvim-config profile info— prints active profile with its LSP servers and DAP packages parsed from the profile Lua filenvim-config profile listnow marks the active profile with*and skips internal files (init, picker)nvim-config update dap-adapters— reads the active profile'sdap_mason_packagesand triggers Mason install headlesslynvim-config keymaps— opensdocs/keymaps.mdin$PAGER- bash/zsh/fish completions updated for all new commands (
status,doctor,keymaps) and subcommands (theme info/list,ui info,profile info,update dap-adapters) - Session restore:
persistence.nvimsaves session per directory on exit and auto-restores on startup when Neovim is opened with no arguments;<Leader>qsrestore,<Leader>qdstop persistence;<Leader>qgroup annotated as Session in which-key :Cheatwith no argument opens avim.ui.selectpicker (works in both UI variants) instead of showing a usage message:w!!sudo save — write system files without relaunching Neovim as root (cabbrev w!!→w !sudo tee % > /dev/null)nvim-spectre: project-wide search/replace with regex and preview;<Leader>sropen,<Leader>swsearch word under cursorzen-mode.nvim: distraction-free fullscreen mode;<Leader>tztogglelazygitintegration viatoggleterm.nvim:<Leader>ggopens lazygit in a floating terminal; requireslazygitinstalled on the systemneotest+neotest-bash: test runner integrated in Neovim;<Leader>Trrun nearest,<Leader>Tfrun file,<Leader>Tssummary panel,<Leader>Tooutput panel;<Leader>TTesting group in which-key- neotest profile-driven adapters:
neotest-vitest(web, vitest detection vianode_modules/.bin/vitest),neotest-phpunit(php/laravel),neotest-rust(rust); loaded conditionally viaprofiles.get_neotest_adapters()— same pattern as LSP servers and null-ls sources nvim-dap+nvim-dap-ui+nvim-dap-virtual-text: interactive debugger with floating UI and inline variable values;<Leader>Dgroup;one-small-step-for-vimkindLua adapter built-in- DAP profile-driven adapters:
pwa-node/JS-TS (web, Masonjs-debug-adapter), Xdebug/PHP (php/laravel, Masonphp-debug-adapter), codelldb/Rust (rust, Masoncodelldb); registered viaprofiles.setup_dap(dap) - DAP Mason auto-install:
profiles.get_dap_mason_packages()+ Mason registry inlua/config/lsp.lua; missing packages installed at startup alongside LSP servers avante.nvim: Cursor-like AI assistant (chat + inline edits); CLI-first providers (claude-code default, gemini-cli, codex) require no OAuth at startup; API providers (claude, claude-opus, gemini) available via<Leader>aPpicker;<Leader>awhich-key group- Gemini CLI:
install geminiCLI subcommand (@google/gemini-clivia npm); dedicated toggleterm terminal<Leader>tG; bash/zsh/fish completions updated codeium.nvim: free inline AI completion; integrated as nvim-cmp source ([AI]label); activate with:Codeium Authon first usemini.ai: extended text objects — smartera/ifor function args, brackets, quotes across multiple lines;n_lines=500toggleterm.nvim: persistent floating terminal (<C-\>);<Leader>ttgeneral toggle;<Leader>tCdedicated Claude Code session;<Leader>tXdedicated Codex CLI sessionharpoon(v2): per-project file bookmarks;<Leader>haadd,<Leader>hhmenu,<C-1..4>jump to file 1-4todo-comments.nvim: colorisesTODO/FIXME/HACK/NOTE/WARNin code;<Leader>ftopens full list via Telescopediffview.nvim: enhanced diff view (<Leader>gv) and per-file git history (<Leader>gH); lazy-loaded on commands; complements gitsignsvim-illuminate: auto-highlights all occurrences of the word under cursor; 200ms delay; disabled in neo-tree, Telescope, Masonindent-blankline.nvim(v3/ibl): visual indent guides with│character and scope highlightingfriendly-snippets: pre-built snippet library for LuaSnip — already wired as LuaSnip dependency withlazy_load()install claudeCLI subcommand — installs Claude Code CLI (@anthropic-ai/claude-codevia npm); skips if already present; errors if npm is missinginstall codexCLI subcommand — installs OpenAI Codex CLI (@openai/codexvia npm); same guards- Fish shell completion:
--show-completion fish/--install-completion fish; installs to~/.config/fish/completions/nvim-config.fish; all commands and subcommands covered - Theme system: 14 dark colorschemes (nightfox variants, catppuccin, tokyonight, kanagawa, gruvbox-material) with persistent selection via
.nvim-theme(gitignored) lua/ui/theme.lua: source of truth for valid theme names;get()reads.nvim-themewithnightfoxfallback:Theme [name]user command: applies and persists a colorscheme; no args shows current; tab-completion on all 14 namesbin/nvim-config theme set|unset [<name>]CLI subcommand: mirrorsuicommand pattern; bash and zsh completions updatedlua/plugins/catppuccin.lua: catppuccin-mocha, catppuccin-macchiato, catppuccin-frappe (dark variants, transparent background)lua/plugins/tokyonight.lua: tokyonight-night, tokyonight-storm, tokyonight-moon (transparent)lua/plugins/kanagawa.lua: kanagawa-wave, kanagawa-dragon (transparent)lua/plugins/gruvbox-material.lua: gruvbox-material (dark, medium contrast)theme-initlazy.nvim inline spec (priority = 0): applies the active colorscheme after allpriority = 1000colorscheme plugins have completed setup — prevents flashrender-markdown.nvim: in-buffer markdown rendering (headings, code blocks, bullets, checkboxes, tables); lazy-loaded onft = markdown; no Node.js/browser required- Bash shebang detection: extensionless files with
#!/.../bin/bashor#!/.../env bashshebang auto-detected asshfiletype viavim.filetype.addwithpriority = -math.huge <Leader>tn: cycle line numbers (absolute → relative → none), new<Leader>tToggle group in which-keygitsigns.nvim: inline diff signs in gutter,]h/[hhunk navigation,<Leader>gs/gr/gp/gb/gdstage/reset/preview/blame/diff — buffer-local, active in git repos onlydocs/cheatsheets/git.md: git cheatsheet (5th topic in:Cheat)<Leader>ggroup annotated as Git in which-key<Leader><CR>: clear search highlight without moving cursor- Auto-save: all modified regular buffers saved automatically on
FocusLostandBufLeave(guarded: skips scratch/prompt/input buffers) :Cheat [topic]user command: opens a cheatsheet in a centered floating window (q/Escto close); tab-completion on topics; auto-discovers topics fromdocs/cheatsheets/*.md<Leader>hckeymap: opens:Cheatwith topic prompt;<Leader>hgroup annotated as Help in which-keydocs/cheatsheets/: four light quick-reference files (editing,lsp,plugins,profiles)scrolloff=7: 7-line scroll margin inoptions.lua0remapped to^(first non-blank character of line) inkeymaps.lua- Visual
*/#: search forward / backward on the current selection (literal, very-nomagic) <Leader>pp: paste mode toggle (manual fallback for SSH / terminals without bracketed paste)<Leader>cd: change CWD to the directory of the current file<Leader>ss/sn/sp/sa/s?: spell checking toggle and navigation;<Leader>sgroup annotated in which-key- UI variant system: two interchangeable variants (
classic,modern) selected at startup via.nvim-uiat the config root (gitignored, defaults toclassic) lua/ui/variant.lua: reads.nvim-uiand returns the active variant namelua/ui/shared.lua: plugin specs common to both variants (nightfox, lualine, bufferline, nvim-navic)lua/ui/classic.lua: classic variant specs (telescope, which-key, trouble)lua/ui/modern.lua: modern variant specs (snacks.nvim — picker, notifier, dashboard)- CLI
uicommand:set classic|modernwrites.nvim-ui;unsetremoves it .nvim-uiadded to.gitignore- Split
tests/nvim-config.batsinto domain-specific files:cli.bats,lsp.bats,ui.bats,docs.bats,profiles.bats :NvimProfile [name]user command: activates a profile at runtime (LSP + null-ls immediate, plugins require restart); no argument opens Telescope picker<Leader>fpkeymap: opens profile picker (which-key annotated)lua/profiles/picker.lua: Telescope picker showing active (●) / inactive (○) profileslua/profiles/init.lua:activate()function for runtime profile switching- Project profile system:
lua/profiles/with additive profilesweb,php,laravel(extends php),rust; auto-detected frompackage.json,composer.json,Cargo.toml; overridable via.nvim-profileat project root lua/profiles/init.lua: profile manager withdetect,is_active,get_plugins,get_lsp_servers,get_null_ls_sources- CLI
profilecommand:list,detect,set,unset,createsubcommands - Bash and zsh completions updated with
profile,ui, andthemecommands and subcommands emmet-vimandtelescope-node_modulesmoved from global plugins towebprofile- none-ls sources are now profile-driven: PHP QA tools (phpstan, phpcs, phpmd, php-cs-fixer), Prettier/ESLint, rustfmt, blade-formatter
- Profile-driven LSP:
ts_ls,volar,svelte-language-server(web);intelephense(php);rust_analyzer(rust) install configsubcommand: symlinks~/.config/nvimto the repository; aborts safely if target exists and is not a symlink to this repoinstall depssubcommand: installs apt packages onlyinstall all(default): runs deps then config- Bash completion for
installsubcommands (deps,config,all) - Tests: expanded suite now at 230+ tests covering CLI, LSP, UI, profiles, docs, and startup integrity
commands.lua: replace deprecatedvim.api.nvim_execwithvim.api.nvim_create_autocmdfordisable-comments-on-new-linesandtrim-white-space-on-saveautocmd groups; cursor position is now preserved across the trailing-whitespace substitutionlua_ls: addruntimeandworkspace.library(configlua/+VIMRUNTIME) to thelua_lsLSP settings so LuaLS resolvesrequire('plugins.foo')relative tolua/(correct module root) and not the project root, eliminating "same file required with different names" warnings<C-s>added in normal mode (:w);<C-z>removed from normal and insert modes (conflicts with shellSIGTSTP);<C-r>removed from insert mode (overwrote native "insert register" — redo remains on<C-r>in normal mode)nvim-treesitterv1.0 migration: rewrotelua/config/treesitter.lua—nvim-treesitter.configsremoved; highlight via FileType autocmd +vim.treesitter.start(), folds viavim.treesitter.foldexpr(), textobjects migrated tonvim-treesitter-textobjectsv2 explicit keymaps- which-key v3: disabled automatic keymap icons (
icons.mappings = false) to avoid rendering issues without a Nerd Font avante.nvimauth_typecorrected from"pro"to"max"— avante only recognises"api"and"max";"max"covers both Pro and Max Claude subscriptions- nvim-ufo:
provider_selectorreduced to{ 'lsp', 'indent' }— API only accepts a{main, fallback}pair; three-value table caused an error on every file open - avante: auth input field closed on focus return — root cause was the
"native"input provider callingvim.ui.select()(a picker that closes onBufLeave); fixed by settinginput.provider = "dressing"to use a propervim.ui.inputbuffer - avante:
<Leader>aPprovider picker —AvanteSwitchProviderVim command hasnargs=0(E488 on any argument); replaced with directrequire('avante.api').switch_provider()Lua call; rebuilt picker usingtelescope.pickers.new()to fix invisible-on-second-use regression withvim.ui.select - avante: keymaps (
<Leader>at,<Leader>aa, etc.) not responding — avante'ssafe_keymap_setskips any key that lazy.nvim has registered as a handler (Keys:have()check); string entries inkeysspec blocked all avante keymap definitions; reverted toVeryLazyloading (no OAuth prompt at init — default provider isclaude-code, ACP-based);<Leader>aPusesvim.keymap.setdirectly and is unaffected - avante: ACP providers (claude-code, codex) stuck on "generating" — avante default
argsfor ACP providers use{ '-y', '-g', '<pkg>' }; in npm 11-gmeans "from global install only" so the package is never downloaded; replaced-gwith--(end-of-options separator) for claude-code and codex; also replaced deprecated@zed-industries/claude-code-acpwith@agentclientprotocol/claude-agent-acp - avante: provider switch routes requests to wrong ACP subprocess —
sidebar.acp_clientis cached after first connection and reused for all subsequent requests regardless of active provider;switch_provider()does not clear the cache; fixed by resettingsidebar.acp_clientandsidebar.chat_history.acp_session_idin the<Leader>aPpicker after the switch - avante: sidebar not opening after provider selection —
open_sidebar({ ask = false })now called afterswitch_provider()in the<Leader>aPpicker - avante: gemini-cli demanding API key —
auth_method = falseskipped auth entirely, causingcreate_sessionto fail because gemini CLI requires an explicit auth type; the correct Google OAuth method id is"oauth-personal"(confirmed via ACPinitializeresponse); setauth_method = "oauth-personal"to reuse the OAuth token already stored by the gemini CLI - LSP:
K → vim.lsp.buf.hover()removed fromon_attachbuffer-local keymaps — conflicts with nvim-ufo's globalK(peek fold / LSP hover fallback); nvim-ufo handles both cases correctly - LSP:
<C-h> → signature helpmoved from normal mode to insert mode inon_attach— was shadowing the global<C-h>window-navigation keymap in normal mode - auto-save:
BufLeaveautocmd now guards onbuftype('' or 'acwrite'only) — previously interfered with dressing input buffers via trailing-whitespaceBufWritePrehooks, causing the auth input field to close
lua/plugins.lua: UI plugins (telescope, lualine, bufferline, which-key, trouble, nvim-navic, nightfox) are now loaded vialua/ui/instead of inline requires- Plugin loading deferred with lazy.nvim triggers:
neo-treeandtroubleoncmd;telescopeoncmd;treesitter,lsp,null-lsonBufReadPre/BufNewFile;nvim-cmponInsertEnter;commentandvim-surroundonBufReadPost;which-keyonVeryLazy;emmeton web filetypes only;bufferlineexplicitlylazy = false <Space>f: moved fromon_attachbuffer-local LSP keymap to a global keymap inlua/config/conform.lua; behavior unchanged (falls back to LSP when no conform formatter is configured)- avante: model updated to
claude-sonnet-4-6(replaces deprecatedclaude-sonnet-4-5-20250929) andclaude-opus-4-7; provider set expanded toclaude-code(CLI, default),gemini-cli(CLI),codex(CLI),claude(API, OAuth),claude-opus(API, OAuth),gemini(API key) timeoutlen = 500(explicit, was implicit 1000ms);ttimeoutlen = 10(was 0, caused intermittent terminal escape-sequence issues):Cheatauto-discovers topics by scanningdocs/cheatsheets/*.mdinstead of a hardcoded list — adding a file to that directory makes it immediately available in the picker and tab-completion
0.3.0 - 2026-03-30
nvim-navic(LSP-based breadcrumb) replaces archivednvim-gps; attached in LSPon_attachand rendered in lualineneo-tree.nvim(v3.x) replacesnvim-tree.luaas the file explorer- Regression tests for LSP bootstrap, nvim-cmp mapping API, neo-tree migration, Trouble v3 keymaps, and Telescope extension loading
cmp.mapping.close()replaced withcmp.mapping.abort()(current API)<Tab>/<S-Tab>cmp mappings now declare{ 'i', 's' }modes so LuaSnip jump works in select moderequire('nvim-navic')in lualine config guarded withpcallto prevent crash when plugin is not yet installed- Trouble keymaps migrated from v1 API (
workspace_diagnostics,document_diagnostics,quickfix) to v3 (diagnostics,qflist) - Telescope trouble integration updated from
open_with_troubletotrouble.sources.telescope(v3 API), guarded withpcall - Telescope
fzfextension now explicitly loaded viaload_extension - Telescope
node_modulesextension load wrapped inpcallto tolerate missing build - Bufferline offset filetype updated from
NvimTreetoneo-tree
nvim-gps(archived upstream, superseded bynvim-navic)lsp-colors.nvim(redundant with Neovim 0.9+ built-in diagnostic highlight groups)lua/config/nvim-tree.lua(replaced bylua/config/neo-tree.lua)
0.2.0 - 2026-03-30
.editorconfigwith per-language indentation rules (Lua, shell, bats, Markdown)LICENSE.md(MIT)tests/startup.batswith staticrequire()integrity checks forinit.lua,plugins.lua, andbootstrap.lua- Headless Neovim smoke tests for
init.lua,options.lua, andkeymaps.lua; skipped automatically under Snap, overridable withNVIM_HEADLESS_TESTS_SKIP=0 - Colored terminal output in
bin/nvim-config: INFO (white), WARN (yellow), ERROR (red), DEBUG (cyan); respectsNO_COLORenv var and TTY detection - Supported platforms section in
README.md(Ubuntu 22.04/24.04, Bash 5+, Neovim 0.9+)
CHANGELOG.mdreformatted to Keep a Changelog with typed sections and bracketed version headers- ERROR-level log messages now also written to stderr
0.1.0 - 2026-03-26
bin/nvim-configCLI withinstall,--help,--version,--dry-run, logging, and bash completion supportbatstest suite and a dedicated test runner undertests/
- Replaced the old
install.shbootstrap script withbin/nvim-config - Replaced the old
TODOnote with a structured roadmap inTODO.md
- Exploratory Lua files that were not part of the runtime config