Compare commits

..
31 changed files with 772 additions and 845 deletions

View file

@ -29,7 +29,7 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: wimpysworld/nothing-but-nix@main
if: runner.os == 'Linux'
with:
@ -37,7 +37,7 @@ jobs:
- uses: cachix/install-nix-action@v31
with:
github_access_token: ${{ secrets.GH_TOKEN }}
- uses: cachix/cachix-action@v17
- uses: cachix/cachix-action@v14
with:
name: rde
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

View file

@ -10,7 +10,7 @@ jobs:
id-token: "write"
contents: "read"
steps:
- uses: "actions/checkout@v7"
- uses: "actions/checkout@v5"
with:
persist-credentials: false
- uses: "DeterminateSystems/determinate-nix-action@v3"

View file

@ -10,13 +10,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@v4
- uses: wimpysworld/nothing-but-nix@main
with:
hatchet-protocol: 'carve'
- name: Install Determinate Nix
uses: DeterminateSystems/determinate-nix-action@v3
- uses: cachix/cachix-action@v17
- uses: cachix/cachix-action@v14
with:
name: rde
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

View file

@ -0,0 +1,85 @@
# CodeCompanion Revamp Plan
## Context
- Current: CodeCompanion **19.18.0** via nixpkgs `vimPlugins.codecompanion-nvim` (nixpkgs input lastModified ~2026-05-15).
- Latest: **19.20.0**. Key changes since 19.18.0:
- v19.19.0: `claude-sonnet-5` support, async/dynamic model fetching, copilot `top_p` fixes, inline orphaned-keymap fix, background command deregistration.
- v19.20.0: `gemini_interactions` adapter, PDF support for http adapters (`/file` on Anthropic/Copilot/OpenAI/OpenRouter), env vars from files, prompt-library items auto-receive default rule groups, copilot schema options removed.
- **Bug in current setup:** config sets `model = "claude-sonnet-5"` but 19.18.0 predates its support; the model name may not resolve. The nixpkgs bump fixes this.
- Files involved:
- `plugin/24_completion.lua` — `get_codecompanion_config()`, setup, blink integration.
- `plugin/10_keymap.lua` — `<leader>a*` keymaps (lines 69–84, contains duplicates/commented cruft).
- `overlays/plugins.nix` — codecompanion overlay (nvimSkipModules); likely unchanged.
- `modules/module/specs/plugins.nix` — plugin spec; unchanged.
## Decisions (confirmed with user)
1. **Version bump:** `nix flake update nixpkgs` (accepts wider plugin bump; do NOT override src).
2. **Adopt:** agent-mode keymaps, one workflow prompt, minor slash-command config (`/share` token). **No MCP.**
3. **Models:** `claude-sonnet-5` (copilot) for **chat only**; cheaper copilot model for **inline** and **background** (title generation) — use `gpt-5-mini` as placeholder; verify exact model id via `ga` model picker in a chat buffer after the bump and adjust.
4. **Codex (ChatGPT Edu) = heavy agent lane.** Copilot stays the default chat adapter; Codex ACP is launched explicitly for heavy autonomous tasks. Auth via ChatGPT login only (no API key) — user's existing `auth_method = "chatgpt"` is correct per the v19.20.0 adapter source (`"openai-api-key"|"codex-api-key"|"chatgpt"`; the docs page comment `"chat-gpt"` is stale).
## Tasks
1. **Bump nixpkgs**
- Run `nix flake update nixpkgs`.
- Verify: `nix eval --raw nixpkgs#vimPlugins.codecompanion-nvim.version` reports ≥ 19.20.0.
- Rebuild the wrapped Neovim per this repo's normal build (`nix build` / the repo's usual package target) and smoke-test that the editor starts.
2. **`plugin/24_completion.lua` — `get_codecompanion_config()`**
- Keep `interactions.chat.adapter = { name = "copilot", model = "claude-sonnet-5" }`.
- Change `interactions.inline.adapter` to `{ name = "copilot", model = "gpt-5-mini" }` (cheap/fast).
- Add a background adapter so title generation doesn't use Sonnet: set the background chat adapter to copilot/`gpt-5-mini` (config lives under `interactions.background.chat`; confirm exact key against `:h codecompanion` / `:checkhealth codecompanion` after the bump — the docs page "Generating Titles" says an adapter must be configured for background interactions).
- Keep existing context_management, rules, display, shared keymaps unchanged.
- Add `interactions.chat.slash_commands["share"].opts.token = os.getenv("GITHUB_GIST_TOKEN")`.
- **Prompt library additions** (keep `expert`, `fixer`, `suggest`):
- `["agent"]`: interaction "chat", alias `agent`, first user prompt starts with `@{agent}` plus selected code block, so Copilot/Claude gets the file-editing tool group (read_file, insert_edit_into_file, grep_search, run_command, etc., with approvals).
- `["tdd"]` (name flexible): interaction "chat", `opts = { is_workflow = true, alias = "tdd" }` — 3-stage workflow: (1) plan/understand `#buffer`, (2) `@{agent}` implement, (3) `@{run_command}` run the test suite (leverages run_command's test-flag for agentic workflows).
3. **`plugin/10_keymap.lua` — clean up + add**
- Remove duplicated/commented lines (70–71, 77, 81).
- Add:
- `nmap_leader("aa", "<cmd>CodeCompanionChat<CR>", "Agent chat (use @{agent})")` or directly `<cmd>CodeCompanion /agent<CR>`.
- `xmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent on selection")`.
- `nmap_leader("aw", "<cmd>CodeCompanion /tdd<CR>", "Workflow: plan→implement→test")`.
- `nmap_leader("aC", "<cmd>CodeCompanion /compact<CR>", "Compact chat")` (verify `<leader>aC` doesn't clash).
- Keep existing mappings unchanged.
4. **No changes** to `overlays/plugins.nix` or `modules/module/specs/plugins.nix` unless the new version introduces new lazy-module load failures (re-run the build's check phase; `doCheck = false` is already set on the spec entry).
5. **Codex ACP lane (ChatGPT Edu)**
- **Resolved prerequisite:** user has installed `codex-acp` at `~/.nix-profile/bin/codex-acp` (verified on PATH 2026-07-24). The preset adapter's default command (`codex-acp`) now works as-is — **no `commands` override in the Lua config**. Fallback only if the binary misbehaves: override with `commands = { default = { "codex", "acp" } }` (requires the codex CLI's built-in `acp` subcommand; verify with `codex acp --help`).
- **Config change (small):** none strictly required — the existing `extend("codex", { defaults = { auth_method = "chatgpt" } })` block in `plugin/24_completion.lua` is correct. Keep it.
- Auth: prereq is an active `codex login` session with the ChatGPT Edu account (`~/.codex/auth.json` exists — user confirms validity; if expired, re-run `codex login`, browser/ChatGPT-app flow, no API key).
- Keep codex on-demand (do NOT make it the default chat adapter):
- Keep `<leader>ak` (`:CodeCompanionChat adapter=codex`).
- Add comment documenting ACP-only slash commands for this lane: `/resume` (restore past codex session, fresh chat only), `/mode` (switch agent mode), `/command`, `/acp_session_options`, plus `\`-triggered ACP command completion in the chat buffer.
- Do not set a default codex model in config; pick per-session via `ga` / `/acp_session_options` (avoids hardcoding model ids that change with the Edu plan).
- PATH note: Neovim must inherit a PATH containing `~/.nix-profile/bin` so the spawned `codex-acp` resolves — true when nvim is launched from the user's normal shell; call it out if validation fails with "command not found".
6. **Docs/habit notes** (add as comment block above the codecompanion setup, no separate docs file): `/compact`, `/fork`, `/symbols`, `/share`, `/resume`+`/mode` (codex ACP), `gm` (btw), `gty` (YOLO), `gba`/`gbd` (buffer sync), `gd` (debug window).
## Validation
- `nix flake update nixpkgs` then repo build succeeds.
- In Neovim: `:checkhealth codecompanion` clean.
- `:CodeCompanionChat`, press `ga` → copilot adapter lists `claude-sonnet-5`; send a trivial message and confirm response + auto title generation.
- In chat: type `@` → completion shows `agent`, `files`, `memory`, etc.; run `@{agent}` task on a scratch repo and confirm the approval prompt flow works.
- `:CodeCompanion /tdd` starts the workflow stages in order.
- Inline: visual-select code, `:CodeCompanion` prompt → confirm diff shows and `ga`/`gr` accept/reject still work on the cheap model.
- `:CodeCompanion /share` prompts/errors sensibly if `GITHUB_GIST_TOKEN` unset.
- **Codex lane:** `<leader>ak` opens a chat with the codex ACP adapter; the spawned `codex-acp` process initializes without auth errors (ChatGPT method), a trivial prompt gets a response, and `/mode` lists codex session modes. If it fails with command-not-found, check that Neovim inherited `~/.nix-profile/bin` in PATH (`:echo $PATH` inside nvim).
## Risks
- `nix flake update nixpkgs` bumps **all** vimPlugins and Neovim itself; other plugins may break. If the blast radius is too large, fall back to overriding codecompanion `src` to tag `v19.20.0` in `overlays/plugins.nix` (fetchFromGitHub, `doCheck = false`).
- Model ids (`claude-sonnet-5`, `gpt-5-mini`) must match the copilot adapter's choices post-bump; verify via `ga` picker and adjust literals.
- `gpt-5-mini` may not support tool use on copilot — fine, since inline/background don't use tools.
- `codex-acp` lives in the user's nix profile (outside this flake). If the profile is rebuilt/removed, the codex lane breaks — long-term consider adding `codex-acp` to this flake's runtime deps so it's pinned with the rest of the setup (optional follow-up, not required now).
- ChatGPT Edu accounts authenticate codex via browser/ChatGPT-app login; token expiry will surface as ACP auth errors → re-run `codex login`.
## Out of scope
- MCP server integration.
- Custom rules parsers, custom tools, extensions (mcphub/history/vectorcode).

View file

@ -0,0 +1,191 @@
# Neovim + Nix setup review
Scope: `flake.nix`, `overlays/`, `modules/`, `plugin/*.lua`, `lua/*`, `ftplugin/*`, CI/workflows, and cross-check against the pinned versions in `flake.lock` (nixpkgs weekly `241313f4`).
## Pinned package versions (from current lock)
- neovim `0.12.2`
- blink-cmp `1.10.2`
- codecompanion-nvim `19.13.0`
- nvim-treesitter `0.10.0-unstable-2026-04-03` (main-branch rewrite)
- nvim-treesitter-textobjects `0-unstable-2026-04-07` (main-branch rewrite)
- copilot-lua `2.0.3`
- render-markdown-nvim `8.12.0-unstable-2026-05-07`
- zk-nvim `0.4.7-unstable-2026-03-13`
- mini.nvim `0.17.0-unstable-2026-05-12`
- quarto-nvim `2.1.0`
- otter-nvim `2.14.5`
- lspconfig `2.9.0`
---
## CRITICAL: Treesitter is currently non-functional
### Finding
The nixpkgs `nvim-treesitter` package is the **main-branch rewrite** — there is no `lua/nvim-treesitter/configs.lua`. The whole module in `plugin/20_startup.lua` lines 212-294 is dead code:
```lua
local ok_configs, configs = pcall(require, "nvim-treesitter.configs") -- fails
```
The working tree already removed the fallback `vim.treesitter.start()` FileType autocmd (the "fixed treesitter" commit deleted it). Net result: **no treesitter highlighting, indentexpr, foldexpr, or textobjects are configured**.
### Impact
- Syntax highlighting falls back to Neovim's regex-only engine.
- All textobject keymaps (`]a`, `[a`, `]f`, `[f`, `]e`/`[e`, `<leader>x`/`X`, lsp_interop `<leader>lm`) are inert.
- `foldexpr`/`indentexpr` based folding does nothing.
### Fix direction
Rewrite `plugin/20_startup.lua` for main-branch API:
1. FileType autocmd → `vim.treesitter.start()` (and optionally `indentexpr`/`foldexpr`).
2. `require("nvim-treesitter-textobjects").setup({ move = { set_jumps = true } })` + explicit keymaps using `move.goto_next_start(query, "textobjects")` and `swap.swap_next(...)`.
3. The non-Nix `ensure_installed` block should set `opts.ensure_installed` BEFORE the filter (line 278 reads it before line 286 defines it).
Also remove duplicate `vim.treesitter.language.register("markdown", ...)` between `plugin/20_startup.lua` and `plugin/21_datascience.lua`.
---
## CodeCompanion version mismatch
### Finding
Config targets features absent in `19.13.0` (present only in `≥19.19.0`/main):
| Config usage | Present in 19.13? | Present in main (`v19.20.0`) |
|---|---|---|
| `interactions.chat.opts.context_management.editing` | No (flat `trigger`/`enabled` only) | Yes |
| `interactions.chat.opts.context_management.compaction` | No | Yes |
| `<leader>aC` requiring `codecompanion.interactions.chat.context_management.compaction` | **RUNTIME ERROR** | Works |
| `slash_commands.share` (`opts.token = ...`) | No (absent from defaults) | Yes |
| `adapters.acp.codex.defaults.auth_method = "chatgpt"` | Yes | Yes |
| `interactions.chat.adapter = { name = ..., model = ... }` table form | Yes | Yes |
### Risk
On today's lock (`19.13.0`): `/share` is inert, compaction-only keymap throws module-not-found, and `editing`/`compaction` tuning is silently ignored. On `≥19.19.0`/main: everything works.
### Decision needed (BLOCKING)
Choose **exactly one**:
- **A — Upgrade the plugin pin** (`overlays/plugins.nix` fetch to `v19.20.0` or `main`, bump the lock, and keep the current config). Riskiest change but matches what the config is written for.
- **B — Downgrade config call sites** to match `19.13.0`: remove `slash_commands.share`, remove `editing`/`compaction` keys (or collapse to flat `trigger: 0.75`), remove or gate `<leader>aC` on "newer" version.
- **C — Version-gate the config**: keep current code, read the installed version at startup and skip the new features when < 19.19.
I recommend A (the config clearly intends to track upstream main-ish behavior; `nix flake update` already bumped it to `19.18.0` in a prior session and you only reverted because the nightly was still too old — `v19.20.0` is now available).
---
## DEAD / DUPLICATE code
- `plugin/20_startup.lua` — the entire `configs.setup(opts)` block is dead (see Treesitter finding above).
- `plugin/10_keymap.lua`:
- `nmap_leader('od', '<Cmd>Neogen<CR>', ...)` — `neogen` plugin is **not shipped** in `specs/plugins.nix`. Mapping errors on press.
- `nmap_leader('fp', '<Cmd>Pick projects<CR>', ...)` — `MiniExtra.pickers.projects` doesn't exist in mini.extra; would error.
- `nmap_leader('oS', '<Cmd>lua Config.insert_section()<CR>', ...)` — `Config.insert_section` is never defined.
- `vim.lsp.buf.definition()` is bound to `grd`; Neovim 0.11+ defaults include `gr` aliases. Cosmetic, but `]d`/`[d` exist on modern LSP config and would be more idiomatic.
- `plugin/23_editor.lua`:
- `my_styler` formatter (calls `R -s -e styler::...`) is defined but never referenced — dead.
- `plugin/24_completion.lua`:
- `providers.cmp_r` is defined in the blink source list but never enabled in `default` or `per_filetype`; inert.
- `BLINK_VERSION = "v1.4.1"` — only consulted in the non-Nix install path; nixpkgs is `1.10.2`. Pin it to current or drop.
- `get_blink_fuzzy_setting().prebuilt_binary = { force_version = BLINK_VERSION }` — singular key is wrong; blink option is `fuzzy.prebuilt_binaries` (plural). Being skipped in-Nix anyway, but still wrong key.
- `modules/module/specs/plugins.nix` — `specs.utils-lazy` ships `nvim-dap*` and `nvim-lint` but there is zero config/tooling that references them in this repo.
- `ftplugin/quarto.lua`:
- Second top-level `require('quarto').setup()` with **no args** runs _after_ `21_datascience.lua`'s setup and **resets** it to defaults (no `lspFeatures`, no `codeRunner`).
- Top-level `require('quarto')` at FileType load also crashes if the quarto plugin isn't installed (e.g. cats off and a `.qmd`/`.quarto` file is opened): Neovim detects `quarto` ft natively in 0.10+.
- `plugin/00_options.lua`:
- Lines 146-163: two back-to-back FileType autocmds that both remove `r`/`o` from `formatoptions`. The second references an undefined `augroup` variable (nil → autocmd is global, happens to still work).
---
## STALE / WRONG options
- `plugin/10_keymap.lua` line 174:
```lua
require("conform").format({ lsp_fallback = true })
```
`lsp_fallback` is the **deprecated** boolean form of `conform.nvim`; should be `lsp_format = "fallback"` (the form already used in `plugin/23_editor.lua` and `ftplugin/python.lua`). In newer conform this may warn or error.
- `ftplugin/quarto.lua`:
- Sets `<Plug>RDSendLine` and R-style keymaps on _all_ quarto buffers, including Python/Julia chunks. This collides with quarto-runner mappings set in `21_datascience.lua`. Should gate on `vim.bo.filetype == "r"` (the ftplugin already imports quarto.runner for python, but the R keys leak).
- `.github/dependapot.yml` — filename typo. GitHub expects `dependabot.yml`; Dependabot won't run.
- `.github/workflows/check.yml`:
- `nix develop` without a `-c` command just launches an interactive shell. In CI it does nothing useful (or hangs). Replace with `nix develop -c echo ok` or drop.
- Path filter `'modules'` only matches the root directory itself; should be `modules/**` (or `'modules/**'`).
- Changing `plugin/`, `lua/`, `overlays/`, `ftplugin/`, etc. does **not** trigger CI.
- `flake.nix` / `.envrc` shellHook:
```sh
export R_LIBS_SITE=$(strings "$(command -v R)" | grep -oP '/nix/store/[^:]+/library' ...)
```
`grep -oP` (PCRE) is **not available in macOS BSD grep**. On `aarch64-darwin` this silently fails → `R_LIBS_SITE` is empty. Either depend on `ripgrep` regex (`strings ... | rg -o '...'`) or use `gsed` (GNU sed).
- `overlays/plugins.nix` — `zk-nvim` skip list contains `zk.pickers.fzf_lua`; current zk-nvim package likely doesn't load that regardless, but harmless.
- `modules/module/settings/core.nix` — `config.settings.nvim_lua_env` references `lp.tiktoken_core` but `tiktoken_core` is **not in `catPkgs.general` or anywhere else** in these files. If there's an extra Lua/tiktoken module, fine; otherwise this option is placeholder dead code.
- `modules/module/settings/hosts.nix` — host `m` (marimo) is defined with `enable = false` but also configured with `package`, `argv0`, `addFlag`. Dead block.
---
## PORTABILITY / DARWIN issues
- Shell hook `grep -oP` (above) fails on macOS.
- `mkdir -p "$R_LIBS_USER"` fine; but `command -v R` on macOS returns the wrapper; the wrapper path injection still works.
---
## DEAD WEIGHT (shipped but unused)
- `catPkgs.r` includes `pkgs.rnvimserver` — `rnvimserver` is needed by R.nvim only when using the _socket_ transport; `21_datascience.lua` only uses vim-slime. Acceptable, but `rnvimserver` adds to build time. Include if you actually use it? Currently not used.
- `specs.utils-lazy` ships `nvim-dap`, `nvim-dap-ui`, `nvim-dap-virtual-text`, `nvim-lint` — none of these are referenced anywhere in `plugin/` or `lua/`. Consider moving them to a devShell-only cat, or drop them.
- `.gitignore` `*.R` — prevents tracking any new `.R` files. `tests/test.R` is already committed so it isn't actively harmful, but it's surprising for a repo whose default cats include R.
- `.commandcode/` dir is listed in `.gitignore` but is in the worktree; fine, but worth cleaning up if it's an artifact.
---
## IMPROVEMENTS given updated packages
- `catPkgs.markdown` — add `marksman` (it's the LSP used by `render-markdown.nvim` wiki links and configured in LSP). Same binary is needed by `render-markdown` for wiki link ISP.
- `vim.lsp.enable` servers configured but binaries missing in PATH:
- `marksman` (LSP + render-markdown wiki integration).
- `r_ls` (new R language server — package name in nixpkgs is likely `r-languageserver` or `r_ls`; current `catPkgs.r` doesn't ship either).
- `clangd` — not required for this data-science setup; either drop or add to `external`.
- `julials` — require `LanguageServer.jl` in `settings.lang_packages.julia` for it to be useful.
- `blink.cmp` is `1.10.2` but config pins `BLINK_VERSION = "v1.4.1"` for the non-Nix path. Either drop the `BLINK_VERSION` constant (let MiniDeps track HEAD or the tag pinned in flake) or update it to `v1.10.2`.
- `conform.nvim` already uses `lsp_format = "fallback"` in `23_editor.lua` and `ftplugin/python.lua`, but `10_keymap.lua` still calls the deprecated `lsp_fallback = true`. Align to `lsp_format = "fallback"`.
- `.github/workflows/check.yml` — add path filters for `plugin/**`, `lua/**`, `overlays/**`, `ftplugin/**`, `modules/**`.
- `tests/init.lua` smoke-test and `tests/test.R` are present but **not wired into `nix flake check`**. Add a trivial check that runs `lua tests/init.lua` via `nix-shell -A ...`.
---
## CONFLICTS
- `<leader>up` (terminal bracketed paste toggle in `10_keymap.lua`) conflicts with `mini.basics.mappings.option_toggle_prefix = "<leader>u"` (paste toggle), which is set up later in `20_startup.lua` via `now()`. Result: the user's `<leader>up` mapping gets overwritten. Portable workaround: remap to `<leader>tp` (terminal namespace) and keep `<leader>tb` as alternative — both already mapped in 10_keymap.
- `ftplugin/quarto.lua` resets quarto-nvim config (kills `lspFeatures`/`codeRunner` set in `21_datascience.lua`) and injects R plug mappings into non-R quarto chunks.
---
## CLEAN summary for implementer
### Immediate (not version-dependent)
1. Restore treesitter highlighting + textobjects in `plugin/20_startup.lua` using main-branch API.
2. Remove duplicate `vim.treesitter.language.register` in `21_datascience.lua`.
3. Fix `plugin/10_keymap.lua`: remove/resolve `Neogen`, `Pick projects`, `Config.insert_section` dangling mappings.
4. Replace deprecated `lsp_fallback = true` with `lsp_format = "fallback"` in `plugin/10_keymap.lua`.
5. Gate `ftplugin/quarto.lua` to not reset config and to not leak R keys into non-R chunks; guard against missing `quarto` plugin.
6. Remove duplicate `formatoptions` autocmds (or at least dedupe).
7. Dedupe `RNVIM_COMPLDIR` / `TMPDIR` setup (do it in one place).
8. Rename `.github/dependapot.yml` to `.github/dependabot.yml`.
9. Remove or gate dead host `m` in `hosts.nix`.
10. Remove unused `rsplit`? no, irrelevant.
11. Add `marksman` to `catPkgs.markdown` and remove orphan LSP entries or ship the binaries.
12. Remove dead `conform` formatter `my_styler` and dead blink `cmp_r` provider, or wire them up.
### Version-dependent (BLOCKED)
- **Reset the CodeCompanion version decision**: either pin to `v19.20.0` (keep config, fix `slash_commands.share`, drop the `<leader>aC` compatibility shim, keep `editing`/`compaction`) OR downgrade config to match `19.13.0` (remove `share`, collapse `context_management`).
### CI / packaging
- Fix `nix develop` usage and path filters in `.github/workflows/check.yml`.
- Replace `grep -oP` in `flake.nix` shellHook with portable `rg -o` or add `gnused` to `catPkgs.always` and use `gsed`.
- Wire `tests/init.lua` into `flake check`.
### Verification
- `nix flake check` on both `aarch64-darwin` and `x86_64-linux`.
- `nix build .#packages.<system>.default` succeeds (already does).
- Start nvim, verify :TSContext works, treesitter highlighting is on, and `]f`/`[f` textobjects move.
- Open a `.qmd` file with cats off → no ftplugin crash.
- Confirm `<leader>up` still toggles bracketed paste.
- Confirm `:Pick projects` and `:Neogen` and `Config.insert_section` no longer error (or are mapped to valid handlers).

View file

@ -198,8 +198,7 @@ projectSettings = {
### Language packages (module defaults)
Add Python/R/Julia libraries. These are appended to the built-in defaults, which
always apply (see `settings.langPackageDefaults` in `modules/module/settings/lang-packages.nix`):
Add Python/R/Julia libraries that get appended to the module defaults. The built-in defaults are:
- Python: `duckdb`, `polars`
- R: `arrow`, `broom`, `data_table`, `janitor`, `styler`
@ -215,8 +214,7 @@ projectSettings = {
};
```
Use `lib.mkForce` to replace other `lang_packages` assignments; the built-in defaults still apply.
To exclude defaults, override `catPkgs.<cat>` instead:
Use `lib.mkForce` to replace rather than append:
```nix
projectSettings = {
@ -339,5 +337,5 @@ cats.nix ───────────────► config.cats │
- **Overlays** (`overlays/`) inject dependency package sets into nixpkgs (`rpkgs`, `baseRPackages`, `basePythonPackages`, plugins). Top-level `rWrapper` and `quarto` are compatibility conveniences, but `pkgs.rpkgs` is the canonical R surface for downstream configuration.
- **cat-packages.nix** is the single source of truth for per-category packages. Each list is gated by its cat toggle.
- **deps.nix** wires `catPkgs` into the wrapper's runtime PATH.
- **settings.lang_packages** holds project-specific language libraries appended to the built-in `settings.langPackageDefaults`, used by local outputs and downstream module consumers.
- **settings.lang_packages** holds the shared defaults used by local outputs and downstream module consumers.
- **flake.nix** exports `lib.eval`, `lib.mkWrapper`, `lib.devShellPackages`, and `lib.shellHook` as the canonical downstream helpers, and builds `packages.default` and `devShells.default` from the same module config.

24
flake.lock generated
View file

@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1787729487,
"narHash": "sha256-3A5rKxnceQ+inWqeQDUrxoVPXr67JMrdIIfbLStx06s=",
"lastModified": 1781337902,
"narHash": "sha256-QjXxUvpOBbnlpSskBOZJ9nUbpYyFRD4cBo4s1aHniGs=",
"owner": "dwinkler1",
"repo": "fran",
"rev": "b77635aeb1e70fffd09e5823dde5b76abb3df991",
"rev": "30ab141871ede23b245a90afea58e076ef83b515",
"type": "github"
},
"original": {
@ -22,11 +22,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1788179007,
"narHash": "sha256-hn1oU2rue2SYK8dAr8+WNZWtbsz1S2W5mnHlSEuh3bo=",
"lastModified": 1784796856,
"narHash": "sha256-wWFrV5/Qbm+lyt5x20E/bSbfJiGKMo4RCxZV8cl/WZI=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "34ab99075ac4f7e40cf037eef32cb1c360bb85e9",
"rev": "e2587caef70cea85dd97d7daab492899902dbf5d",
"type": "github"
},
"original": {
@ -76,11 +76,11 @@
},
"rixpkgs": {
"locked": {
"lastModified": 1788208780,
"narHash": "sha256-ouq3GjmTOXtWVufLOhMqLAEJXxrlkhDYtZJt3YfN5oU=",
"lastModified": 1782576256,
"narHash": "sha256-KOvpL9DJJmShb64mX9QTjhxHaxE8MizQIqNUAniuJ2E=",
"owner": "dwinkler1",
"repo": "rixpkgs",
"rev": "f3d00cb8f39262e6ca15c2046bd29ca8a52aae31",
"rev": "815afc01bc0cc9a2eba80906645bd08f976a4401",
"type": "github"
},
"original": {
@ -124,11 +124,11 @@
]
},
"locked": {
"lastModified": 1787722691,
"narHash": "sha256-bWBt7v2FhgG6WTLgSi8nYGi1zXaTdA+vTdCRdvR2vcw=",
"lastModified": 1782135443,
"narHash": "sha256-vAmbArdCyjqpVW+37aCy/PMBOLIqukUXLQuEKLwUhA4=",
"owner": "BirdeeHub",
"repo": "nix-wrapper-modules",
"rev": "04ef216559b18214853879df862f493a9e6bb8cf",
"rev": "6e7f66fa2cdf4d63162580b438f7fcf87c28a46f",
"type": "github"
},
"original": {

View file

@ -161,11 +161,11 @@
modules = [
{
cats = {
r = true;
r = false;
python = true;
};
settings.lang_packages.python = nixpkgs.lib.mkForce (with pkgs.python3Packages; [
polars
pandas
]);
catPkgs.nix = nixpkgs.lib.mkForce [
pkgs.alejandra
@ -206,10 +206,10 @@
(builtins.length (self.lib.devShellPackages defaultConfig) > 0)
];
overrideAssertions = [
(overrideConfig.cats.r or false)
(!(overrideConfig.cats.r or false))
(builtins.length overrideNix == 1)
((builtins.head overrideNix) == "alejandra")
(builtins.match ".*R RHOME.*" overrideShellHook != null)
(builtins.match ".*R RHOME.*" overrideShellHook == null)
];
in
pkgs.runCommand "check-downstream-overrides" {
@ -230,48 +230,6 @@
nvim --headless -u NONE -c "set runtimepath+=${./.}" -l ${./tests/init.lua}
touch $out
'';
# Downstream modules assign settings.lang_packages.<lang> plainly. The
# module system replaces same-option defaults, so built-in defaults must
# live in langPackageDefaults and be composed at the consumption sites.
# This check fails if someone moves defaults back into lang_packages:
# then a downstream assignment would silently drop lintr etc.
append-semantics = let
downstreamCfg = (self.lib.eval {
inherit pkgs;
modules = [
{
cats.r = true;
settings.lang_packages.r = [ pkgs.rpkgs.rPackages.fixest ];
}
];
}).config;
rWrapperPkg = builtins.head downstreamCfg.catPkgs.r;
in
pkgs.runCommand "check-lang-packages-append" { } ''
script=$(readlink -f "${rWrapperPkg}/bin/R")
grep -q "r-lintr-" "$script" || {
echo "default R packages were dropped by a downstream lang_packages assignment" >&2
exit 1
}
grep -q "r-fixest-" "$script" || {
echo "downstream lang_packages.r additions were not appended" >&2
exit 1
}
echo "lang_packages append semantics OK" > $out
'';
smoke-test = pkgs.runCommand "smoke-test" {} ''
# The Nix build sandbox has a read-only HOME; point XDG dirs at a
# writable location so vim.lsp/shaDa can write state headlessly.
export XDG_CONFIG_HOME=$TMPDIR/xdg-config
export XDG_STATE_HOME=$TMPDIR/xdg-state
export XDG_CACHE_HOME=$TMPDIR/xdg-cache
export XDG_DATA_HOME=$TMPDIR/xdg-data
BINARY_PATH="${defaultNvimPkg}/bin/vv"
"$BINARY_PATH" --headless -c "luafile ${./tests/smoke.lua}" -c "qa!"
touch $out
'';
}
);

View file

@ -11,39 +11,28 @@ nmap_leader('<Tab>', '<Cmd>bnext<CR>', 'Next buffer')
nmap_leader('<S-Tab>', '<Cmd>bprev<CR>', 'Prev buffer')
-- a is for 'AI'
nmap_leader("aa", function()
require("opencode").ask("@this: ")
end, "Ask OpenCode about this")
nmap_leader("ab", function()
require("opencode").prompt("Analyse @buffer")
end, "Analyse buffer")
nmap_leader("aB", function()
require("opencode").prompt("Analyse @buffers")
end, "Analyse open buffers")
nmap_leader("ad", function()
require("opencode").prompt("Explain @diagnostics and fix the underlying issue")
end, "Explain/fix diagnostics")
nmap_leader("an", function()
require("opencode").command("session.new")
end, "New OpenCode session")
nmap_leader("ap", function()
require("opencode").ask("@")
end, "Ask OpenCode with context")
nmap_leader("as", function()
require("opencode").select()
end, "Select OpenCode action")
xmap_leader("aa", function()
require("opencode").ask("@this: ")
end, "Ask OpenCode about selection")
xmap_leader("af", function()
require("opencode").prompt("Fix the selected code and preserve its surrounding API:\n@this")
end, "Fix selection")
xmap_leader("ap", function()
require("opencode").ask("@")
end, "Ask OpenCode with context")
xmap_leader("as", function()
require("opencode").select()
end, "Select OpenCode action")
nmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent chat (@{agent} tools)")
nmap_leader("ac", "<cmd>CodeCompanionChat Toggle<CR>", "Chat Toggle")
nmap_leader("aC", function()
local chat = require("codecompanion").last_chat()
if not chat then
return vim.notify("No CodeCompanion chat to compact", vim.log.levels.WARN)
end
require("codecompanion.interactions.chat.context_management.compaction").compact(chat, { min_token_savings = 0 })
end, "Compact chat")
nmap_leader("ag", "<cmd>CodeCompanion /commit<CR>", "Generate commit message")
nmap_leader("ai", "<cmd>CodeCompanionActions<CR>", "Chat Action")
nmap_leader("al", "<cmd>CodeCompanion /lsp<CR>", "Explain LSP Diagnostics")
nmap_leader("an", "<cmd>CodeCompanionChat Add<CR>", "Chat New")
nmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
nmap_leader("aw", "<cmd>CodeCompanion /tdd<CR>", "Workflow: plan, implement, test")
nmap_leader("ax", "<cmd>CodeCompanion /fixer<CR>", "Code Fixer")
xmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent on selection")
xmap_leader("ae", "<cmd>CodeCompanion /explain<CR>", "Explain Code")
xmap_leader("af", "<cmd>CodeCompanion /fix<CR>", "Fix Code")
xmap_leader("ap", "<cmd>CodeCompanion /expert<CR>", "Code Expert")
xmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
nmap_leader("ak", "<cmd>CodeCompanionChat adapter=codex<CR>", "Chat with Codex")
-- b is for 'buffer'
nmap_leader('bb', '<Cmd>b#<CR>', 'Alternate')
@ -148,8 +137,7 @@ nmap_leader('LL', '<Cmd>luafile %<CR><Cmd>echo "Sourced lua"<CR>', 'Source buffe
nmap_leader('Ls', '<Cmd>lua Config.log_print()<CR>', 'Show log')
nmap_leader('Lx', '<Cmd>lua Config.execute_lua_line()<CR>', 'Execute `lua` line')
-- m is for 'mark' (checkbox toggling)
nmap_leader('mc', '<Cmd>ToggleCheckbox<CR>', 'Toggle checkbox')
-- m is free
-- o is for 'other'
local trailspace_toggle_command = '<Cmd>lua vim.b.minitrailspace_disable = not vim.b.minitrailspace_disable<CR>'
@ -226,24 +214,6 @@ nmap_leader("wh", "<C-w>h", "Go to Left Window", { remap = true })
nmap_leader("wj", "<C-w>j", "Go to Lower Window", { remap = true })
nmap_leader("wk", "<C-w>k", "Go to Upper Window", { remap = true })
nmap_leader("wl", "<C-w>l", "Go to Right Window", { remap = true })
nmap_leader("w>", "<Cmd>vertical resize +5<CR>", "Increase Window Width")
nmap_leader("w<lt>", "<Cmd>vertical resize -5<CR>", "Decrease Window Width")
local function set_window_width_percent(percent)
return function()
local target_width = math.max(1, math.floor(vim.o.columns * percent / 100 + 0.5))
vim.api.nvim_win_set_width(0, target_width)
end
end
for i = 1, 9 do
local percent = i * 10
nmap_leader(
"w" .. i,
set_window_width_percent(percent),
string.format("Set Window Width to %d%%", percent)
)
end
nmap_leader("_", "<C-W>s", "Split Window Below", { remap = true })
nmap_leader("|", "<C-W>v", "Split Window Right", { remap = true })

View file

@ -1,55 +0,0 @@
-- omp bridge: expose an RPC socket so the omp harness can read this Neovim
-- instance's buffers on demand. Read-only on the agent side; this module only
-- owns the socket lifecycle.
--
-- Contract
-- * Socket path: $NVIM_OMP_SOCKET if set, else <stdpath('state')>/omp.sock
-- (~/.local/state/nvim/omp.sock on macOS, ~/.local/state/nvim/omp.sock
-- elsewhere). The omp-side tool in .omp/tools/nvim_buffers.mjs resolves the
-- *same* path, so the two sides agree without configuration.
-- * The agent reads buffers by evaluating read-only nvim API expressions over
-- the socket with `nvim --server <path> --remote-expr 'json_encode(...)'`.
-- Nothing here writes buffers or executes model-supplied commands.
local M = {}
local SOCKET_NAME = "omp.sock"
-- Resolve the deterministic socket path. Copies the rule on the omp side; keep
-- the two files in sync when changing the fallback or env override.
function M.socket_path()
local env = vim.env.NVIM_OMP_SOCKET
if env and env ~= "" then
return env
end
return vim.fn.stdpath("state") .. "/" .. SOCKET_NAME
end
-- Start the RPC listener. Returns the live socket path, or nil.
-- A second instance cannot bind the same address. This function deliberately
-- never removes an existing socket file: a failed liveness probe must not
-- disconnect another live Neovim instance.
-- If a crash leaves a stale socket, remove it manually only after confirming
-- that no Neovim process owns the path, then restart vv.
function M.start()
if vim.v.headless == 1 then
-- Headless runs (tests, CI) get no socket; nothing should depend on one.
return nil
end
local path = M.socket_path()
local ok, res = pcall(vim.fn.serverstart, path)
if ok and type(res) == "string" and res ~= "" then
-- serverstart returns the bound address string (e.g. "/tmp/omp.sock").
vim.notify("nvim_omp: RPC socket ready at " .. path, vim.log.levels.INFO)
return path
end
vim.notify(
"nvim_omp: could not bind RPC socket at " .. path
.. "; another instance may own it or a stale socket needs manual cleanup.",
vim.log.levels.WARN
)
return nil
end
return M

View file

@ -9,17 +9,14 @@ let
# NOTE: Package list expressions are lazily evaluated, and derivations are
# not built until needed, so keep side-effecting expressions out of these
# lists.
maybe = cat: pkgsList: lib.optionals (config.cats.${cat} or false) pkgsList;
rPackages =
(pkgs.baseRPackages or [ ])
++ config.settings.langPackageDefaults.r
++ config.settings.lang_packages.r;
maybe = cat: pkgsList:
lib.optionals (config.cats.${cat} or false) pkgsList;
rPackages = (pkgs.baseRPackages or [ ]) ++ config.settings.lang_packages.r;
rWrapperPackages = rPackages;
quartoPkg =
if config.cats.r or false then
pkgs.rpkgs.quarto.override { extraRPackages = rPackages; }
else
pkgs.quarto;
if config.cats.r or false
then pkgs.rpkgs.quarto.override { extraRPackages = rPackages; }
else pkgs.quarto;
in
{
options.catPkgs = lib.mkOption {
@ -28,38 +25,28 @@ in
};
config.catPkgs = {
always = maybe "always" (
with pkgs;
[
always = maybe "always" (with pkgs; [
ripgrep
]
);
]);
clickhouse = maybe "clickhouse" (with pkgs; [ clickhouse-lts ]);
external = maybe "external" (
with pkgs;
[
external = maybe "external" (with pkgs; [
nodejs
perl
ruby
shfmt
sqlfluff
tree-sitter
]
);
]);
julia = maybe "julia" [
(pkgs.julia-bin.withPackages (
config.settings.langPackageDefaults.julia ++ config.settings.lang_packages.julia
))
(pkgs.julia-bin.withPackages config.settings.lang_packages.julia)
];
lua = maybe "lua" (with pkgs; [ lua-language-server ]);
markdown = maybe "markdown" (
with pkgs;
[
markdown = maybe "markdown" (with pkgs; [
python3Packages.pylatexenc
quartoPkg
zk
@ -67,21 +54,15 @@ in
texlab
imagemagick
harper
]
);
]);
nix = maybe "nix" (
with pkgs;
[
nix = maybe "nix" (with pkgs; [
alejandra
nix-doc
nixd
]
);
]);
optional = maybe "optional" (
with pkgs;
[
optional = maybe "optional" (with pkgs; [
bat
broot
devenv
@ -106,29 +87,21 @@ in
tokei
wget
yq
]
);
]);
python = maybe "python" (
let
python = maybe "python" (let
python_packages_fn =
if pkgs ? basePythonPackages then
ps:
pkgs.basePythonPackages ps
++ config.settings.langPackageDefaults.python
++ config.settings.lang_packages.python
else
_: config.settings.langPackageDefaults.python ++ config.settings.lang_packages.python;
if pkgs ? basePythonPackages
then ps: pkgs.basePythonPackages ps ++ config.settings.lang_packages.python
else _: config.settings.lang_packages.python;
python_with_packages = pkgs.python3.withPackages python_packages_fn;
in
with pkgs;
[
with pkgs; [
python_with_packages
ruff
basedpyright
uv
]
);
]);
r = maybe "r" [
(pkgs.rpkgs.rWrapper.override { packages = rWrapperPackages; })

View file

@ -8,7 +8,7 @@
config.settings.config_directory = ../../..;
# Default colorscheme and background
config.settings.colorscheme = lib.mkDefault "kanagawa";
config.settings.colorscheme = lib.mkDefault "cyberdream";
config.settings.background = lib.mkDefault "dark";
# Enable RC wrapping (allows neovim to find the config)

View file

@ -14,15 +14,6 @@
# R.nvim v1.x owns its cache and temporary directories and exports
# RNVIM_COMPLDIR/RNVIM_TMPDIR during setup. Do not inject literal `$PWD`
# values into the wrapper environment.
#
# ponytail: with R_LIBS_USER unset-or-empty, loading compiled namespaces
# (S7 via btw) segfaults on macOS for bare-terminal launches outside the
# devShell hook. Any non-empty value avoids it; this store path exists,
# contains no libraries, and stays inert. The devShell hook overrides it
# with $PWD/.r-libs. Upgrade path: teach wlib to emit expandable defaults.
(lib.mkIf (config.cats.r or false) {
R_LIBS_USER = "${pkgs.rpkgs.rWrapper}";
})
];
# Environment variables with defaults (can be overridden by user)

View file

@ -5,10 +5,7 @@
...
}:
let
rPackages =
(pkgs.baseRPackages or [ ])
++ config.settings.langPackageDefaults.r
++ config.settings.lang_packages.r;
rPackages = (pkgs.baseRPackages or [ ]) ++ config.settings.lang_packages.r;
rWrapperPkg = pkgs.rpkgs.rWrapper.override { packages = rPackages ++ [pkgs.nvimcom]; };
in
{

View file

@ -5,30 +5,7 @@
...
}:
{
options.settings = {
# Built-in language libraries composed into every language spec.
# Consumers extend via settings.lang_packages, which is APPENDED to these.
langPackageDefaults = lib.mkOption {
type = lib.types.submodule {
options = {
python = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
};
r = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
};
julia = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
};
};
};
default = { };
};
lang_packages = lib.mkOption {
options.settings.lang_packages = lib.mkOption {
type = lib.types.submodule {
options = {
python = lib.mkOption {
@ -50,18 +27,18 @@
};
default = { };
description = ''
Project-specific language libraries. Appended to settings.langPackageDefaults
in each language spec's runtime packages.
Language-specific package defaults and downstream overrides appended to each
language spec's runtime packages.
'';
};
};
config.settings.langPackageDefaults = {
python = with pkgs.python3Packages; [
config.settings.lang_packages = {
python = lib.mkDefault (with pkgs.python3Packages; [
duckdb
polars
];
r = with pkgs.rpkgs.rPackages; [
]);
r = lib.mkDefault (
(with pkgs.rpkgs.rPackages; [
arrow
broom
data_table
@ -72,8 +49,9 @@
# want to use the nvim-dap R adapter (see plugin/26_dap.lua).
# vscDebugger
lintr
];
julia = [
])
);
julia = lib.mkDefault [
"DataFramesMeta"
"QuackIO"
];

View file

@ -78,7 +78,6 @@ in {
lze
lzextras
plenary-nvim
vimtex
neogit
{
data = mini-nvim;
@ -141,6 +140,7 @@ in {
data = with pkgs.vimPlugins; [
quarto-nvim
render-markdown-nvim
vimtex
{
data = otter-nvim;
pname = "otter";
@ -163,8 +163,10 @@ in {
pname = "nvim-treesitter";
}
{
data = pkgs.vimPlugins.opencode-nvim;
pname = "opencode.nvim";
data = pkgs.codecompanion-nvim.overrideAttrs (old: {
doCheck = false;
});
pname = "codecompanion";
}
] ++ builtins.attrValues pkgs.vimPlugins.nvim-treesitter.queries;
};

View file

@ -1,13 +0,0 @@
WRN 2026-07-31T16:36:01.037 ?.50508 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/TN25Kv/nvim.50508.0
WRN 2026-07-31T16:36:38.634 ?.50644 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/6jT8As/nvim.50644.0
WRN 2026-07-31T16:46:12.752 ?.54395 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/zY36Oi/nvim.54395.0
WRN 2026-07-31T17:01:43.138 ?.58656 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/nDisRA/nvim.58656.0
WRN 2026-07-31T17:01:50.641 ?.58696 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/okDyqb/nvim.58696.0
WRN 2026-07-31T17:02:04.773 ?.58741 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/5wxfRu/nvim.58741.0
WRN 2026-07-31T17:02:29.644 ?.58854 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/OMTfpB/nvim.58854.0
WRN 2026-07-31T17:02:29.644 ?.58852 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/QZvLy2/nvim.58852.0
WRN 2026-07-31T17:14:31.289 ?.64329 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/zh9tLm/nvim.64329.0
WRN 2026-07-31T17:14:40.017 ?.64399 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/pGaDez/nvim.64399.0
WRN 2026-07-31T17:17:48.228 ?.64921 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/ryMpX0/nvim.64921.0
WRN 2026-07-31T17:17:52.664 ?.64953 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/X7RmXh/nvim.64953.0
WRN 2026-07-31T17:18:00.691 ?.64989 server_start:197: Failed to start server: operation not permitted: /tmp/nvim.daniel/C6njjq/nvim.64989.0

View file

@ -1,6 +1,22 @@
{ ... }:
final: prev:
{
codecompanion-nvim = prev.vimPlugins.codecompanion-nvim.overrideAttrs {
checkInputs = with prev.vimPlugins; [
blink-cmp
mini-nvim
];
dependencies = [ prev.vimPlugins.plenary-nvim ];
nvimSkipModules = [
"codecompanion.actions.static"
"codecompanion.actions.init"
"minimal"
"codecompanion.providers.actions.fzf_lua"
"codecompanion.providers.completion.cmp.setup"
"codecompanion.providers.actions.telescope"
"codecompanion.providers.actions.snacks"
];
};
zk-nvim = prev.vimPlugins.zk-nvim.overrideAttrs {
nvimSkipModules = [
"zk.pickers.fzf_lua"

View file

@ -9,14 +9,4 @@ in
{
basePythonPackages = reqPkgs;
python = prev.python3.withPackages reqPkgs;
# pyarrow's test_timezone_absent reads /usr/share/zoneinfo, which is
# blocked by the build sandbox (PermissionError: Operation not permitted).
python3Packages = prev.python3Packages // {
pyarrow = prev.python3Packages.pyarrow.overridePythonAttrs (old: {
disabledTests = (old.disabledTests or [ ]) ++ [
"pyarrow/tests/test_orc.py::test_timezone_absent"
];
});
};
}

View file

@ -5,7 +5,7 @@
};
in {
inherit rpkgs;
baseRPackages = [rpkgs.nvimcom rpkgs.rPackages.btw];
baseRPackages = [rpkgs.nvimcom];
rWrapper = rpkgs.rWrapper.override {packages = [];};
quarto = rpkgs.quarto.override {extraRPackages = [];};
}

View file

@ -119,69 +119,3 @@ Config.add = (function(pkg)
end)
Config.now_if_args = vim.fn.argc(-1) > 0 and MiniDeps.now or MiniDeps.later
-- Checkbox
local checked_character = "x"
local checked_checkbox = "%[" .. checked_character .. "%]"
local unchecked_checkbox = "%[ %]"
local line_contains_unchecked = function(line)
return line:find(unchecked_checkbox)
end
local line_contains_checked = function(line)
return line:find(checked_checkbox)
end
local line_with_checkbox = function(line)
return line:find("^%s*- " .. checked_checkbox)
or line:find("^%s*- " .. unchecked_checkbox)
or line:find("^%s*%d%. " .. checked_checkbox)
or line:find("^%s*%d%. " .. unchecked_checkbox)
end
local checkbox = {
check = function(line)
return line:gsub(unchecked_checkbox, checked_checkbox, 1)
end,
uncheck = function(line)
return line:gsub(checked_checkbox, unchecked_checkbox, 1)
end,
make_checkbox = function(line)
if not line:match("^%s*%-%s") and not line:match("^%s*%d%.%s") then
-- "xxx" -> "- [ ] xxx"
return line:gsub("(%S+)", "- [ ] %1", 1)
else
-- "- xxx" -> "- [ ] xxx", "3. xxx" -> "3. [ ] xxx"
return line:gsub("(%s*- )(.*)", "%1[ ] %2", 1):gsub("(%s*%d%. )(.*)", "%1[ ] %2", 1)
end
end,
}
Config.togglecb = function()
local bufnr = vim.api.nvim_get_current_buf()
local cursor = vim.api.nvim_win_get_cursor(0)
local start_line = cursor[1] - 1
local current_line = vim.api.nvim_buf_get_lines(bufnr, start_line, start_line + 1, false)[1] or ""
-- If the line contains a checked checkbox then uncheck it.
-- Otherwise, if it contains an unchecked checkbox, check it.
local new_line = ""
if not line_with_checkbox(current_line) then
new_line = checkbox.make_checkbox(current_line)
elseif line_contains_unchecked(current_line) then
new_line = checkbox.check(current_line)
elseif line_contains_checked(current_line) then
new_line = checkbox.uncheck(current_line)
end
vim.api.nvim_buf_set_lines(bufnr, start_line, start_line + 1, false, { new_line })
vim.api.nvim_win_set_cursor(0, cursor)
end
vim.api.nvim_create_user_command("ToggleCheckbox", Config.togglecb, {})

View file

@ -18,7 +18,7 @@ local defaults = {
duckdb = "duckdb",
julia = "julia",
python = "ipython",
shell = "echo 'Hello " .. (vim.env.USER or "user") .. "!'",
shell = "echo 'Hello " .. vim.env.USER .. "!'",
}
-- Registry of terminal commands

View file

@ -191,7 +191,7 @@ end)
-- Treesitter
now_if_args(function()
vim.treesitter.language.register("markdown", { "markdown", "rmd", "quarto" })
vim.treesitter.language.register("markdown", { "markdown", "codecompanion", "rmd", "quarto" })
require 'treesitter-context'.setup {
enable = true,
@ -216,7 +216,7 @@ now_if_args(function()
"julia", "rnoweb", "latex", "gitcommit", "gitignore",
"git_config", "git_rebase", "diff", "dockerfile",
"make", "xml", "zig", "regex", "csv", "bash",
"markdown_inline", "quarto", "rmd",
"markdown_inline", "quarto", "rmd", "codecompanion",
}
local function start_treesitter(buf, filetype)
local lang = vim.treesitter.language.get_lang(filetype) or filetype

View file

@ -55,24 +55,21 @@ later(function()
ignore_exitcode = true,
parser = function(output, bufnr, linter_cwd)
local diagnostics = {}
-- Expected format: /path/file.R:10:5: style: Some message
-- Pattern: /path/file.R:10:5: style: Some message
for line in output:gmatch("[^\r\n]+") do
local path, lnum, col, severity, message = line:match("^[^:]+:(%d+):(%d+):%s*(%w+):%s*(.+)$")
if path then
local severity_map = {
style = vim.diagnostic.severity.INFO,
warning = vim.diagnostic.severity.WARN,
error = vim.diagnostic.severity.ERROR,
}
for line in output:gmatch("[^\r\n]+") do
-- Capture the file path as well so lnum/col line up with the numbers.
local path, lnum, col, severity, message = line:match("^(.-):(%d+):(%d+):%s*(%w+):%s*(.+)$")
if path and lnum and col and severity then
local line_num = tonumber(lnum)
local col_num = tonumber(col)
table.insert(diagnostics, {
bufnr = bufnr,
lnum = math.max(0, line_num - 1),
col = math.max(0, col_num - 1),
end_lnum = line_num - 1,
end_col = col_num,
lnum = math.max(0, tonumber(lnum) - 1),
col = math.max(0, tonumber(col) - 1),
end_lnum = tonumber(lnum) - 1,
end_col = tonumber(col),
severity = severity_map[severity:lower()] or vim.diagnostic.severity.WARN,
message = message or "lintr issue",
source = "lintr",
@ -102,7 +99,7 @@ now_if_args(function()
add("render-markdown.nvim")
require('render-markdown').setup({
-- completions = { blink = { enabled = true } },
file_types = { 'markdown', },
file_types = { 'markdown', 'codecompanion', },
link = {
wiki = {
body = function(ctx)

View file

@ -37,12 +37,7 @@ later(function()
end)
later(function()
require("mini.align").setup({
mappings = {
start = "gA",
start_with_preview = "g<C-A>",
},
})
require("mini.align").setup()
end)
later(function()
@ -218,8 +213,6 @@ later(function()
local minikeymap = require("mini.keymap")
minikeymap.setup()
local map_multistep = minikeymap.map_multistep
-- blink_next/blink_prev/blink_accept are blink.cmp actions.
-- Direct blink keymaps (C-space/C-l) are set in 24_completion.lua.
local tab_steps = {
"blink_next",
"pmenu_next",

View file

@ -4,6 +4,9 @@ local later = MiniDeps.later
local now = MiniDeps.now
local now_if_args = Config.now_if_args
-- Constants
local BLINK_VERSION = "v1.10.2"
-- Plugin sources configuration
local PLUGIN_SOURCES = {
"hrsh7th/cmp-cmdline",
@ -11,7 +14,7 @@ local PLUGIN_SOURCES = {
"zbirenbaum/copilot.lua",
"jmbuhr/cmp-pandoc-references",
"fang2hou/blink-copilot",
"nickjvandyke/opencode.nvim",
"olimorris/codecompanion.nvim"
}
local PLUGIN_ADDS = {
@ -21,16 +24,34 @@ local PLUGIN_ADDS = {
"cmp-pandoc-references",
}
-- Helper functions
local function create_system_prompt(role_description)
return function(context)
local lang = context.filetype or "programmer"
return "I want you to act as a senior " .. lang .. " developer. " .. role_description
end
end
local function get_code_block(context)
local text = require("codecompanion.helpers.code").get_code(context.start_line, context.end_line)
return "```" .. context.filetype .. "\n" .. text .. "\n```"
end
local function get_mini_icons_highlight(ctx)
local _, hl, _ = require("mini.icons").get("lsp", ctx.kind)
return hl
end
local function get_blink_fuzzy_setting()
return {
sorts = { "exact", "score", "sort_text" },
use_proximity = true,
local setting = {
sorts = { "exact", "score", "sort_text" }
}
if not Config.isNixCats then
setting.prebuilt_binaries = { force_version = BLINK_VERSION }
end
return setting
end
-- Plugin loading
@ -41,6 +62,7 @@ if not Config.isNixCats then
add({
source = "saghen/blink.cmp",
depends = { "rafamadriz/friendly-snippets" },
checkout = BLINK_VERSION,
})
end)
@ -51,6 +73,241 @@ if not Config.isNixCats then
end)
end
local function get_codecompanion_config()
return {
adapters = {
acp = {
-- Codex = heavy agent lane (ChatGPT Edu login via `codex login`; ~/.codex/auth.json).
-- Requires `codex-acp` on PATH (~/.nix-profile/bin). ACP-only slash commands in the
-- chat buffer: /resume (restore a past session, fresh chat only), /mode (switch agent
-- mode), /command, /acp_session_options (e.g. model per session); `\` triggers ACP
-- command completion (1-5s delay after chat open).
codex = function()
return require("codecompanion.adapters").extend("codex", {
defaults = {
auth_method = "chatgpt",
},
})
end,
},
},
interactions = {
chat = {
adapter = {
name = "copilot",
model = "claude-sonnet-5",
},
slash_commands = {
["share"] = {
opts = {
token = os.getenv("GITHUB_GIST_TOKEN"),
},
},
},
opts = {
completion_provider = "blink",
context_management = {
editing = {
trigger = 0.65,
keep_cycles = 3,
exclude_tools = { "memory" },
},
compaction = {
trigger = 0.85,
min_token_savings = 10000,
},
},
},
},
inline = {
adapter = {
name = "copilot",
model = "gpt-5-mini",
},
},
shared = {
keymaps = {
accept_change = {
modes = { n = "ga" },
description = "Accept the suggested change",
},
reject_change = {
modes = { n = "gr" },
opts = { nowait = true },
description = "Reject the suggested change",
},
},
},
background = {
adapter = {
name = "copilot",
model = "gpt-5-mini",
},
chat = {
callbacks = {
["on_ready"] = {
actions = { "interactions.background.builtin.chat_make_title" },
enabled = true,
},
},
opts = {
enabled = true,
},
},
},
},
display = {
chat = {
show_settings = false,
window = {
layout = "horizontal",
position = "bottom",
height = 0.33,
},
},
diff = {
enabled = true,
threshold_for_chat = 6,
},
},
rules = {
default = {
description = "Collection of common files for all projects",
files = {
".clinerules",
".cursorrules",
".rules",
".github/copilot-instructions.md",
"AGENT.md",
"AGENTS.md",
{ path = "CLAUDE.md", parser = "claude" },
{ path = "CLAUDE.local.md", parser = "claude" },
{ path = "~/.claude/CLAUDE.md", parser = "claude" },
},
},
opts = {
chat = {
autoload = "default",
enabled = true,
},
},
},
prompt_library = {
["expert"] = {
interaction = "chat",
description = "Get expert advice from an LLM",
opts = { alias = "expert" },
prompts = {
{
role = "system",
content = create_system_prompt(
"I will ask you specific questions and I want you to return concise explanations and codeblock examples."
),
},
{
role = "user",
content = function(context)
return "I have the following code:\n\n" .. get_code_block(context) .. "\n\n"
end,
opts = { contains_code = true },
},
},
},
["fixer"] = {
interaction = "chat",
description = "Fix code errors with expert guidance",
opts = { alias = "fixer" },
prompts = {
{
role = "system",
content = create_system_prompt(
"I have a block of code that is not working and will give you a hint about the error. I want you to return the corrected code and a concise explanation of the corrections."
),
},
{
role = "user",
content = function(context)
return "The following code has an error:\n\n" .. get_code_block(context) .. "\n\nThe error is:"
end,
opts = { contains_code = true },
},
},
},
["suggest"] = {
interaction = "chat",
description = "Suggest improvements to the buffer",
opts = { alias = "suggest" },
prompts = {
{
role = "system",
content = create_system_prompt(
"When asked to improve code, follow these steps:\n" ..
"1. Identify the programming language.\n" ..
"2. Think separately for each function or significant block of code and think about possible improvements (e.g., for better readability or speed) in the context of the language.\n" ..
"3. Think about the whole document and think about possible improvements.\n" ..
"4. Provide the improved code.\n" ..
"5. Provide a concise explanation of the improvements."
),
},
{
role = "user",
content = function(context)
return "Please improve the following code:\n\n" .. get_code_block(context)
end,
opts = { contains_code = true },
},
},
},
["agent"] = {
interaction = "chat",
description = "Agentic coding with the @{agent} tool group (read/edit/grep/run)",
opts = { alias = "agent" },
prompts = {
{
role = "user",
content = function(context)
return "@{agent} Work on the following code:\n\n" .. get_code_block(context) .. "\n\n"
end,
opts = { contains_code = true },
},
},
},
["tdd"] = {
interaction = "chat",
description = "Workflow: plan the buffer change, implement it, run the tests",
opts = { alias = "tdd", is_workflow = true },
prompts = {
{
{
role = "user",
content = function(context)
return "Let's work test-driven. First, study #buffer and the relevant parts of the codebase, then propose a concise implementation plan (no code yet).\n\nThe code under discussion:\n\n"
.. get_code_block(context)
.. "\n\nThe task: "
end,
opts = { contains_code = true },
},
},
{
{
role = "user",
content = "Implement the plan now, writing or updating tests alongside the code. @{agent}",
opts = { auto_submit = true },
},
},
{
{
role = "user",
content = "Run the project's test suite with @{run_command} and fix any failures until it passes.",
opts = { auto_submit = true },
},
},
},
},
},
}
end
-- Batch add simple plugins
later(function()
for _, plugin in ipairs(PLUGIN_ADDS) do
@ -102,33 +359,26 @@ later(function()
})
end)
-- CodeCompanion habit notes (chat buffer unless stated):
-- /compact compact history, keep summary /fork fork the conversation
-- /symbols insert symbols for a file /share export chat to a GitHub gist
-- gm toggle "btw" ephemeral message gty YOLO: approve all tool calls
-- gba/gbd buffer sync add/drop gd debug window (adapter/tools info)
-- Codex (ACP) lane: /resume (fresh chat only), /mode, /command, /acp_session_options,
-- `\` ACP command completion. Prompt library: /expert /fixer /suggest /agent /tdd (workflow).
later(function()
vim.g.opencode_opts = {
events = {
reload = true,
permissions = {
enabled = true,
edits = { enabled = true },
},
},
server = {
start = function()
vim.cmd("vsplit term://opencode --port")
vim.cmd("vertical resize " .. math.floor(vim.o.columns * 0.4))
vim.cmd("wincmd p")
end
},
}
add("opencode.nvim")
add("codecompanion.nvim")
-- now use function
require("codecompanion").setup(get_codecompanion_config())
vim.cmd([[cab cc CodeCompanion]])
end)
now_if_args(function()
add("blink.cmp")
require("blink.cmp").setup({
-- Direct blink keymaps (C-space/C-l).
-- Tab/Enter/Up/Down are handled via multistep chains in 23_editor.lua,
-- which chain blink_next/blink_prev/blink_accept with other editor actions.
keymap = {
preset = "default",
["<C-space>"] = { "show", "select_next" },
@ -165,7 +415,6 @@ now_if_args(function()
},
completion = {
menu = {
border = "rounded",
draw = {
treesitter = { "lsp" },
components = {
@ -185,12 +434,14 @@ now_if_args(function()
list = {
selection = { preselect = false, auto_insert = true }
},
ghost_text = { enabled = true, show_with_menu = true },
documentation = { auto_show = true, window = { border = "rounded" } },
documentation = { auto_show = true },
trigger = { show_in_snippet = false },
},
snippets = { preset = "mini_snippets" },
sources = {
per_filetype = {
codecompanion = { "codecompanion" },
},
default = { "references", "lsp", "path", "snippets", "buffer", "omni", "copilot" },
providers = {
path = {
@ -216,6 +467,12 @@ now_if_args(function()
score_offset = 45,
async = true,
},
codecompanion = {
name = "CodeCompanion",
module = "codecompanion.providers.completion.blink",
score_offset = 45,
async = true,
},
references = {
name = "pandoc_references",
module = "cmp-pandoc-references.blink",

View file

@ -15,7 +15,7 @@ now_if_args(function()
basedpyright = {},
ruff = {},
marksman = {
filetypes = { "markdown", "markdown_inline" },
filetypes = { "markdown", "markdown_inline", "codecompanion" },
},
harper_ls = {
cmd = { "harper-ls", "--stdio" },

View file

@ -11,19 +11,24 @@ later(function()
return
end
-- vimtex is a VimL plugin: it does not expose a Lua module. Configure it
-- via globals before loading so the plugin picks them up on startup.
vim.g.vimtex_compiler_method = "latexmk"
vim.g.vimtex_compiler_latexmk = {
-- Keep conservative defaults: no continuous background compilation and no
-- callback chatter. Manual :VimtexCompile still works on demand.
continuous = 0,
callback = 0,
}
-- Let vimtex choose the first available viewer (zathura, Skim, Evince, ...).
Config.add("vimtex")
local ok, vimtex = pcall(require, "vimtex")
if not ok then
vim.notify("vimtex not available", vim.log.levels.WARN)
return
end
-- Keep conservative defaults: latexmk continuous compilation off, single
-- viewer (zathura falls back to Evince on most setups).
vimtex.setup({
enabled = true,
compile_on_save = false,
compiler = "latexmk",
-- Avoid hooking spell/formatting into our global <leader> group;
-- vimtex stashes its own <localleader> mappings automatically.
})
-- Filetype detection is normally on, but force it explicitly so .tex files
-- opened outside Quarto still pick up the LSP + viewer hooks.
vim.api.nvim_create_autocmd("BufReadPost", {

View file

@ -1,71 +0,0 @@
-- Minimal Markdown checkbox toggler: `:ToggleCheckbox` (mapped to <leader>mc).
-- Toggles `- [ ]`/`- [x]` (and numbered `N. [ ]`/`N. [x]`) on the current line;
-- creates a checkbox on lines without one.
local checked_character = "x"
local checked_checkbox = "%[" .. checked_character .. "%]"
local unchecked_checkbox = "%[ %]"
local line_contains_unchecked = function(line)
return line:find(unchecked_checkbox)
end
local line_contains_checked = function(line)
return line:find(checked_checkbox)
end
local line_with_checkbox = function(line)
return line:find("^%s*- " .. checked_checkbox)
or line:find("^%s*- " .. unchecked_checkbox)
or line:find("^%s*%d%. " .. checked_checkbox)
or line:find("^%s*%d%. " .. unchecked_checkbox)
end
local checkbox = {
check = function(line)
return line:gsub(unchecked_checkbox, checked_checkbox, 1)
end,
uncheck = function(line)
return line:gsub(checked_checkbox, unchecked_checkbox, 1)
end,
make_checkbox = function(line)
if not line:match("^%s*%-%s") and not line:match("^%s*%d%.%s") then
-- "xxx" -> "- [ ] xxx"
return line:gsub("(%S+)", "- [ ] %1", 1)
else
-- "- xxx" -> "- [ ] xxx", "3. xxx" -> "3. [ ] xxx"
return line:gsub("(%s*- )(.*)", "%1[ ] %2", 1):gsub("(%s*%d%. )(.*)", "%1[ ] %2", 1)
end
end,
}
local M = {}
M.toggle = function()
local bufnr = vim.api.nvim_get_current_buf()
local cursor = vim.api.nvim_win_get_cursor(0)
local start_line = cursor[1] - 1
local current_line = vim.api.nvim_buf_get_lines(bufnr, start_line, start_line + 1, false)[1] or ""
-- If the line contains a checked checkbox then uncheck it.
-- Otherwise, if it contains an unchecked checkbox, check it.
local new_line = ""
if not line_with_checkbox(current_line) then
new_line = checkbox.make_checkbox(current_line)
elseif line_contains_unchecked(current_line) then
new_line = checkbox.check(current_line)
elseif line_contains_checked(current_line) then
new_line = checkbox.uncheck(current_line)
end
vim.api.nvim_buf_set_lines(bufnr, start_line, start_line + 1, false, { new_line })
vim.api.nvim_win_set_cursor(0, cursor)
end
vim.api.nvim_create_user_command("ToggleCheckbox", M.toggle, {})
return M

1
result
View file

@ -1 +0,0 @@
/nix/store/12g17cxiz4wkn5zy483xhbkybpqwc9k4-neovim-unwrapped-0.12.4

View file

@ -1,268 +0,0 @@
-- Comprehensive CI smoke tests for the Neovim configuration.
-- Run inside the wrapped Neovim binary, e.g.:
-- vv --headless -c "luafile tests/smoke.lua" -c "qa!"
--
-- The script loads the full config (plugin/*.lua files are sourced by Neovim on
-- startup), waits for mini.deps deferred work, then exercises key functionality
-- without user interaction.
local M = {}
-- ---------------------------------------------------------------------------
-- Tiny test harness
-- ---------------------------------------------------------------------------
local failures = {}
local passed = 0
local function fail(msg)
table.insert(failures, msg)
print(" FAIL: " .. msg)
end
local function pass(msg)
passed = passed + 1
print(" PASS: " .. msg)
end
local function assert_eq(a, b, msg)
if a == b then
pass(msg)
else
fail(string.format("%s (expected %s, got %s)", msg, vim.inspect(b), vim.inspect(a)))
end
end
local function assert_true(cond, msg)
if cond then
pass(msg)
else
fail(msg)
end
end
local function assert_loaded(mod, msg)
assert_true(package.loaded[mod] ~= nil, msg or ("module '" .. mod .. "' loaded"))
end
-- ---------------------------------------------------------------------------
-- Wait for deferred mini.deps/later work.
-- We patch MiniDeps.later to count pending callbacks and then drain the event
-- loop until the counter returns to zero (or we time out).
-- ---------------------------------------------------------------------------
local function wait_for_deferred(timeout_ms)
timeout_ms = timeout_ms or 10000
-- Fire VimEnter so startup autocmds run, then drain the event loop so
-- mini.deps' deferred setup closures have a chance to execute before we
-- assert anything.
-- The headless smoke run has no start screen to display. Disabling
-- mini.starter also prevents it from replacing the current buffer and
-- trying to create a swap file in the read-only Nix sandbox.
vim.g.ministarter_disable = true
vim.cmd('doautocmd VimEnter')
vim.wait(timeout_ms, function() return false end, 50)
end
-- ---------------------------------------------------------------------------
-- Helper: check whether a keymap is registered for a given lhs in normal mode.
-- ---------------------------------------------------------------------------
local function has_normal_map(lhs)
-- <leader> is stored as the literal leader key, so test both the raw
-- symbolic form and the expanded form (e.g. "<leader>ff" and " ff").
local expanded = lhs:gsub('^<leader>', vim.g.mapleader or '\\')
if vim.keymap and vim.keymap.get then
local maps = vim.keymap.get('n')
for _, map in ipairs(maps) do
if map.lhs == lhs or map.lhs == expanded then
return true
end
end
end
return vim.fn.maparg(lhs, 'n') ~= '' or vim.fn.maparg(expanded, 'n') ~= ''
end
-- ---------------------------------------------------------------------------
-- Helper: check whether a Treesitter parser is available.
-- ---------------------------------------------------------------------------
local function has_parser(lang)
return #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".*", false) > 0
end
-- ---------------------------------------------------------------------------
-- Helper: check whether an LSP config was registered (Neovim 0.11+ native API).
-- ---------------------------------------------------------------------------
local function has_lsp_config(name)
if vim.lsp and vim.lsp.config then
-- Neovim 0.12 exposes configs through the callable `vim.lsp.config`
-- table; older versions expose a function-like API.
local ok, cfg = pcall(function() return vim.lsp.config[name] end)
if not ok or type(cfg) ~= 'table' or next(cfg) == nil then
ok, cfg = pcall(vim.lsp.config, name)
end
if ok and cfg and next(cfg) ~= nil then
return true
end
end
-- Fallback: inspect lspconfig internal table.
local ok, configs = pcall(require, 'lspconfig.configs')
if ok and configs and configs[name] then
return true
end
return false
end
-- ---------------------------------------------------------------------------
-- Test suites
-- ---------------------------------------------------------------------------
function M.test_core_config()
print("\n=== Core configuration ===")
assert_eq(vim.g.mapleader, ' ', "leader is space")
assert_eq(vim.g.maplocalleader, ',', "localleader is comma")
assert_true(vim.o.backup, "backup option enabled")
assert_true(vim.o.undofile, "undofile enabled")
assert_eq(vim.o.mouse, 'a', "mouse enabled")
end
function M.test_config_module()
print("\n=== Config module ===")
assert_true(_G.Config ~= nil, "global Config table exists")
assert_true(type(Config.edit) == 'function', "Config.edit helper exists")
assert_true(type(Config.terminal) == 'table', "Config.terminal namespace exists")
assert_true(type(Config.treesitter_helpers) == 'table', "Config.treesitter_helpers exists")
end
function M.test_mini_modules()
print("\n=== mini.nvim modules ===")
assert_loaded('mini.basics', 'mini.basics loaded')
assert_loaded('mini.statusline', 'mini.statusline loaded')
assert_loaded('mini.tabline', 'mini.tabline loaded')
assert_loaded('mini.clue', 'mini.clue loaded')
assert_loaded('mini.pick', 'mini.pick loaded')
assert_loaded('mini.notify', 'mini.notify loaded')
end
function M.test_keymaps()
print("\n=== Keymaps ===")
assert_true(has_normal_map('<leader>ff'), "leader ff -> pick files")
assert_true(has_normal_map('<leader>fg'), "leader fg -> live grep")
assert_true(has_normal_map('<leader>bb'), "leader bb -> alternate buffer")
assert_true(has_normal_map('<leader>ed'), "leader ed -> mini.files open")
assert_true(has_normal_map('<Esc>'), "Esc clears search highlight")
end
function M.test_treesitter()
print("\n=== Treesitter parsers ===")
local expected = { 'lua', 'python', 'nix', 'markdown', 'latex', 'r', 'julia' }
for _, lang in ipairs(expected) do
assert_true(has_parser(lang), "parser available: " .. lang)
end
end
function M.test_filetype_detection()
print("\n=== Filetype detection ===")
local test_buf = vim.api.nvim_create_buf(false, true)
local orig_buf = vim.api.nvim_get_current_buf()
vim.api.nvim_set_current_buf(test_buf)
-- Test by manually triggering filetype detection for a couple of languages.
vim.api.nvim_buf_set_name(test_buf, 'test.py')
vim.api.nvim_set_option_value('filetype', 'python', { buf = test_buf })
assert_eq(vim.bo.filetype, 'python', "python filetype set")
vim.api.nvim_buf_set_name(test_buf, 'test.lua')
vim.api.nvim_set_option_value('filetype', 'lua', { buf = test_buf })
assert_eq(vim.bo.filetype, 'lua', "lua filetype set")
vim.api.nvim_set_current_buf(orig_buf)
vim.api.nvim_buf_delete(test_buf, { force = true })
end
function M.test_lsp_config()
print("\n=== LSP server registration ===")
local servers = { 'lua_ls', 'basedpyright', 'ruff', 'nil_ls', 'texlab', 'marksman', 'harper_ls' }
for _, name in ipairs(servers) do
assert_true(has_lsp_config(name), "LSP config registered: " .. name)
end
end
function M.test_plugin_configs()
print("\n=== Plugin-specific configuration ===")
-- vimtex globals should be set when the markdown cat is on.
if vim.g.vimtex_compiler_method then
assert_eq(vim.g.vimtex_compiler_method, 'latexmk', "vimtex compiler is latexmk")
else
pass("vimtex not configured (markdown cat disabled)")
end
-- conform formatters should be registered.
local ok, conform = pcall(require, 'conform')
if ok and conform then
local formatters = require('conform').formatters
assert_true(formatters ~= nil, "conform formatters table exists")
else
pass("conform not loaded (expected if utils cat disabled)")
end
-- blink.cmp keymap preset should be available.
local blink_ok, blink = pcall(require, 'blink.cmp')
if blink_ok and blink then
pass("blink.cmp loaded")
else
pass("blink.cmp not loaded (expected if utils cat disabled)")
end
end
function M.test_nix_cats_helper()
print("\n=== nixCats helper ===")
local ok, nix = pcall(require, 'config.nix')
assert_true(ok and nix ~= nil, "config.nix can be required")
if ok and nix then
local cat = nix.get_cat('general', true)
assert_true(type(cat) == 'boolean', "get_cat returns boolean default")
end
end
-- ---------------------------------------------------------------------------
-- Entry point
-- ---------------------------------------------------------------------------
function M.run()
print("Neovim config CI smoke tests")
local v = vim.version()
print(string.format("Neovim version: %d.%d.%d", v.major, v.minor, v.patch))
-- Drain deferred plugin setup before asserting.
wait_for_deferred()
M.test_core_config()
M.test_config_module()
M.test_mini_modules()
M.test_keymaps()
M.test_treesitter()
M.test_filetype_detection()
M.test_lsp_config()
M.test_plugin_configs()
M.test_nix_cats_helper()
print("\n=== Summary ===")
print(string.format("Passed: %d", passed))
print(string.format("Failed: %d", #failures))
if #failures > 0 then
print("\nFailed tests:")
for _, f in ipairs(failures) do
print(" - " .. f)
end
vim.cmd('cquit 1')
else
print("\nAll smoke tests passed!")
vim.cmd('cquit 0')
end
end
local ok, err = pcall(M.run)
if not ok then
print("CRASH: " .. tostring(err))
vim.cmd('cquit 1')
end