Compare commits

..
60 changed files with 1532 additions and 3069 deletions

View file

@ -1,5 +0,0 @@
if (Sys.getenv("RNVIM_TMPDIR") == "") {
options(defaultPackages = c("utils", "grDevices", "graphics", "stats", "methods"))
} else {
options(defaultPackages = c("utils", "grDevices", "graphics", "stats", "methods", "nvimcom"))
}

View file

@ -5,20 +5,12 @@ on:
paths: paths:
- 'flake.lock' - 'flake.lock'
- 'flake.nix' - 'flake.nix'
- 'modules/**' - 'modules'
- 'plugin/**'
- 'lua/**'
- 'overlays/**'
- 'ftplugin/**'
pull_request: pull_request:
paths: paths:
- 'flake.lock' - 'flake.lock'
- 'flake.nix' - 'flake.nix'
- 'modules/**' - 'modules'
- 'plugin/**'
- 'lua/**'
- 'overlays/**'
- 'ftplugin/**'
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
@ -29,7 +21,7 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, macos-latest] os: [ubuntu-latest, macos-latest]
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v4
- uses: wimpysworld/nothing-but-nix@main - uses: wimpysworld/nothing-but-nix@main
if: runner.os == 'Linux' if: runner.os == 'Linux'
with: with:
@ -37,11 +29,11 @@ jobs:
- uses: cachix/install-nix-action@v31 - uses: cachix/install-nix-action@v31
with: with:
github_access_token: ${{ secrets.GH_TOKEN }} github_access_token: ${{ secrets.GH_TOKEN }}
- uses: cachix/cachix-action@v17 - uses: cachix/cachix-action@v14
with: with:
name: rde name: rde
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
extraPullNames: rstats-on-nix, nix-community extraPullNames: rstats-on-nix, nix-community
- run: nix build - run: nix build
- run: nix develop -c echo ok - run: nix develop
- run: nix flake check - run: nix flake check

View file

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

View file

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

3
.gitignore vendored
View file

@ -3,6 +3,3 @@
*.R *.R
.Rlibs .Rlibs
.nvimcom .nvimcom
.commandcode
.commandcode/
.r-*

View file

@ -1,85 +0,0 @@
# 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 6984, 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 (7071, 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

@ -1,191 +0,0 @@
# 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

@ -1,17 +0,0 @@
{
"lastAgent": "agent-local-096f2785-09c8-4487-b71b-f8ac2b81aaaf",
"sessionsByServer": {
"api.letta.com": {
"agentId": "agent-21b21fd6-49e4-4d5c-a1b7-1c6dcd8dacc9",
"conversationId": "default"
},
"local:/Users/daniel/.letta/lc-local-backend": {
"agentId": "agent-local-096f2785-09c8-4487-b71b-f8ac2b81aaaf",
"conversationId": "default"
}
},
"lastSession": {
"agentId": "agent-local-096f2785-09c8-4487-b71b-f8ac2b81aaaf",
"conversationId": "default"
}
}

View file

@ -1,185 +0,0 @@
// omp custom tool: read this machine's running Neovim buffers over its RPC
// socket. Read-only and on-demand — the model calls it when it needs buffer
// content; nothing is injected into prompts automatically.
//
// Install options
// 1. Project scope (this project): keep the file in .omp/tools/ — omp picks it
// up when a session's cwd is inside this repository.
// 2. All projects: mkdir -p ~/.omp/agent/tools
// ln -s "$PWD/.omp/tools/nvim_buffers.mjs" ~/.omp/agent/tools/
// then restart `omp` (custom tools are loaded at session bootstrap).
//
// Socket contract (must match lua/nvim_omp/init.lua):
// path = $NVIM_OMP_SOCKET or <XDG_STATE_HOME|~/.local/state>/nvim/omp.sock
// The nvim instance running this config starts the listener at startup.
// Only one instance can own the socket; a second instance never replaces a
// live owner. Remove a stale socket manually only after confirming no nvim
// process owns it.
import os from "node:os";
import path from "node:path";
// Safe default cap so a large buffer cannot flood the conversation.
const DEFAULT_MAX_LINES = 2000;
export default function (pi) {
// Resolve socket path with the same rule as lua/nvim_omp/init.lua.
const socketPath = () => {
const env = process.env.NVIM_OMP_SOCKET;
if (env) return env;
const state =
process.env.XDG_STATE_HOME ||
path.join(os.homedir(), ".local", "state");
return path.join(state, "nvim", "omp.sock");
};
// Evaluate expr in the remote nvim via `nvim --server`; returns parsed JSON.
const rpc = async (expr, signal) => {
const bin = process.env.NVIM_BIN || "nvim";
const { code, stdout, stderr, killed } = await pi.exec(
bin,
["--server", socketPath(), "--remote-expr", expr],
{ signal },
);
if (killed) throw new Error("cancelled");
if (code !== 0) {
const why = String(stderr || "").trim();
throw new Error(
why.includes("E247")
? `No Neovim RPC socket at ${socketPath()}. ` +
"Start nvim (or vv) first — the 31_nvim_omp.lua plugin binds the socket at startup."
: `nvim RPC failed (${code}): ${why}`,
);
}
const text = String(stdout ?? "").trim();
if (!text) throw new Error("empty response from nvim RPC");
return JSON.parse(text);
};
// Resolve a buffer argument (number, string, or partial path) to a buffer
// number. Returns null when nothing matches.
const resolveBuf = async (buffer, signal) => {
if (buffer === undefined || buffer === null || buffer === "") {
return rpc("json_encode(nvim_get_current_buf())", signal);
}
if (typeof buffer === "number") return buffer;
const bufnum = Number(buffer);
if (Number.isInteger(bufnum) && bufnum > 0) return bufnum;
const list = await rpc(
"json_encode(map(nvim_list_bufs(), {i, v -> " +
"{'bufnr': v, 'name': nvim_buf_get_name(v)}}))",
signal,
);
const q = String(buffer);
const hit =
list.find(
(b) => b.name === q || b.name.endsWith("/" + q) || b.name.endsWith(q),
) ||
list.find((b) => b.name.includes(q));
return hit ? hit.bufnr : null;
};
const tools = [];
// List open buffers -----------------------------------------------
tools.push({
name: "nvim_buffers",
label: "Neovim Buffers",
description:
"List the buffers currently open in the user's running Neovim " +
"(number, name, current/loaded state, modified). Use before " +
"nvim_buffer to pick a buffer.",
parameters: pi.arktype({}),
async execute(_id, _params, _onUpdate, _ctx, _signal) {
const list = await rpc(
"json_encode(map(nvim_list_bufs(), {i, v -> {'nr': v, " +
"'name': nvim_buf_get_name(v), " +
"'current': v == nvim_get_current_buf(), " +
"'loaded': nvim_buf_is_loaded(v), " +
"'modified': getbufvar(v, '&modified')}}))",
);
if (!list.length) {
return { content: [{ type: "text", text: "No buffers open." }] };
}
const lines = list.map(
(b) =>
`${b.nr}\t${b.name || "[no name]"}\t` +
`${b.current ? "current " : ""}` +
`${b.loaded ? "" : "unloaded "}` +
`${b.modified ? "modified" : ""}`.trim(),
);
return {
content: [
{
type: "text",
text: `Open buffers (${list.length}):\n` + lines.join("\n"),
},
],
};
},
});
// Read a single buffer, line-capped -------------------------------
tools.push({
name: "nvim_buffer",
label: "Neovim Buffer",
description:
"Read lines of a Neovim buffer. `buffer` accepts the current buffer " +
"(default), a buffer number from nvim_buffers, or a file name/path " +
"open in Neovim. `maxLines` caps the returned lines (default 2000); " +
"pass a larger value explicitly to read more of a long buffer.",
parameters: pi.arktype({
buffer: "string? | number?",
maxLines: "number?",
}),
async execute(_id, params, _onUpdate, _ctx, signal) {
const buf = await resolveBuf(params.buffer, signal);
if (buf === null) {
return {
content: [
{
type: "text",
text: `No buffer matches '${params.buffer}'. List open buffers with nvim_buffers.`,
},
],
};
}
const maxLines =
Number.isInteger(params.maxLines) && params.maxLines > 0
? Math.min(params.maxLines, 100000)
: DEFAULT_MAX_LINES;
const expr =
`json_encode({'name': nvim_buf_get_name(${buf}), ` +
`'total': nvim_buf_line_count(${buf}), ` +
`'lines': nvim_buf_get_lines(${buf}, 0, ${maxLines}, 0)})`;
const { name, total, lines } = await rpc(expr, signal);
if (!total) {
return {
content: [
{ type: "text", text: `Buffer ${buf} (${name}) is empty.` },
],
};
}
const truncated =
total > lines.length
? `\n... ${total - lines.length} more lines ` +
`(raise maxLines to read them)`
: "";
const numbered = lines.map((l, i) => `${i + 1}: ${l}`).join("\n");
return {
content: [
{
type: "text",
text:
`Buffer ${buf} (${name}), ${total} lines:\n` +
numbered +
truncated,
},
],
};
},
});
return tools;
}

341
README.md
View file

@ -1,341 +0,0 @@
# nvimConfig
Modular Neovim wrapper config. Defines a wrapped `vv` binary with per-category packages, plugins, and environment variables. Available as NixOS module, Home Manager module, or single-starflake import.
## Quick start (imported into another flake)
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rixpkgs.url = "github:dwinkler1/rixpkgs/af2dd3f7b4b172077747c0869d4e30702fb71b0e";
fran = {
url = "github:dwinkler1/fran";
inputs.nixpkgs.follows = "rixpkgs";
};
nvimConfig = {
url = "github:dwinkler1/nvimConfig";
inputs = {
nixpkgs.follows = "nixpkgs";
rixpkgs.follows = "rixpkgs";
fran.follows = "fran";
};
};
};
outputs = { self, nixpkgs, nvimConfig, ... } @ inputs: let
systems = ["aarch64-darwin" "x86_64-linux" "aarch64-linux"];
forAllSystems = nixpkgs.lib.genAttrs systems;
in {
packages = forAllSystems (system: let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ nvimConfig.overlays.dependencies ];
};
evalResult = nvimConfig.lib.eval {
inherit pkgs;
modules = [ projectSettings ]; # see below
};
in {
default = evalResult.config.wrap { inherit pkgs; };
});
devShells = forAllSystems (system: let
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ nvimConfig.overlays.dependencies ];
};
evalResult = nvimConfig.lib.eval {
inherit pkgs;
modules = [ projectSettings ];
};
nv = evalResult.config.wrap { inherit pkgs; };
in {
default = pkgs.mkShell {
packages = [ nv ] ++ nvimConfig.lib.devShellPackages evalResult.config;
shellHook = nvimConfig.lib.shellHook evalResult.config;
};
});
};
}
```
## Home Manager module
```nix
{ inputs, ... }: {
home.packages = [
(inputs.nvimConfig.homeModules.default { inherit pkgs; })
# Or via the overlay:
# pkgs.vv
];
}
```
## NixOS module
```nix
{ inputs, ... }: {
environment.systemPackages = [
(inputs.nvimConfig.nixosModules.default { inherit pkgs; })
];
}
```
## Overriding settings
Define a `projectSettings` attrset and pass it to `nvimConfig.lib.eval` or `nvimConfig.lib.mkWrapper`. The helper injects your overlaid `pkgs` automatically, so downstream consumers do not need to wire `specialArgs` themselves. Every option in `nvimConfig.wrapperConfigs.default` can be overridden here. Local `packages.default`, local `devShells.default`, downstream helper usage, and `pkgs.vv` all resolve the same module defaults unless you override them.
### Basic downstream flake example
This example enables a few cats, adds runtime tools to the shared wrapper/devShell package set, and appends language libraries to the default Python and R environments.
```nix
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
rixpkgs.url = "github:dwinkler1/rixpkgs/af2dd3f7b4b172077747c0869d4e30702fb71b0e";
fran = {
url = "github:dwinkler1/fran";
inputs.nixpkgs.follows = "rixpkgs";
};
nvimConfig = {
url = "github:dwinkler1/nvimConfig";
inputs = {
nixpkgs.follows = "nixpkgs";
rixpkgs.follows = "rixpkgs";
fran.follows = "fran";
};
};
};
outputs = { nixpkgs, nvimConfig, ... }: let
system = "aarch64-darwin";
pkgs = import nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [ nvimConfig.overlays.dependencies ];
};
projectSettings = {
cats = {
python = true;
r = true;
nix = true;
markdown = true;
optional = true;
};
settings.lang_packages = {
python = with pkgs.python3Packages; [
pandas
pyarrow
];
r = with pkgs.rpkgs.rPackages; [
fixest
modelsummary
];
};
catPkgs = {
python = [
(pkgs.python3.withPackages (ps:
ps
++ (with ps; [
pandas
pyarrow
])))
pkgs.ruff
pkgs.basedpyright
pkgs.uv
pkgs.nodejs
];
nix = [
pkgs.alejandra
pkgs.nil
];
optional = [
pkgs.git
pkgs.fd
pkgs.ripgrep
];
};
};
evalResult = nvimConfig.lib.eval {
inherit pkgs;
modules = [ projectSettings ];
};
nv = evalResult.config.wrap { inherit pkgs; };
in {
packages.${system}.default = nv;
devShells.${system}.default = pkgs.mkShell {
packages = [ nv ] ++ nvimConfig.lib.devShellPackages evalResult.config;
shellHook = nvimConfig.lib.shellHook evalResult.config;
};
};
}
```
### Categories
Enable only the cats you need. Defaults are listed in `modules/module/settings/cats.nix`.
```nix
projectSettings = {
cats = {
python = true; # enable Python tooling
r = false; # disable R
nix = true;
lua = false;
markdown = false;
optional = false;
gitPlugins = false;
};
};
```
### Language packages (module defaults)
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`
- Julia: `DataFramesMeta`, `QuackIO`
```nix
projectSettings = {
settings.lang_packages = {
python = with pkgs.python3Packages; [ pandas numpy ];
r = with pkgs.rpkgs.rPackages; [ fixest data_table ];
julia = [ "StatsBase" "Plots" ];
};
};
```
Use `lib.mkForce` to replace rather than append:
```nix
projectSettings = {
settings.lang_packages = {
python = pkgs.lib.mkForce (with pkgs.python3Packages; [ polars duckdb ]);
};
};
```
### Runtime dependencies (per-cat packages)
Override `catPkgs` to change the full runtime tool lists that appear in both the wrapper PATH and the devShell. Use normal assignments to merge list values, or `lib.mkForce` to replace them.
```nix
projectSettings = {
catPkgs = {
nix = [ pkgs.nil pkgs.nixfmt ];
python = [
(pkgs.python3.withPackages (ps: [ ps.pandas ps.duckdb ]))
pkgs.ruff
];
};
};
```
### Direct runtimePkgs override (always-on)
Add packages to `catPkgs.always` for tools that should always be available regardless of other cat toggles.
```nix
projectSettings = {
catPkgs = {
always = [ pkgs.git pkgs.curl pkgs.pre-commit ];
};
};
```
### Runtime env vars
```nix
projectSettings = {
env = {
MY_PROJECT_VAR = "some-value";
};
envDefault = {
EDITOR = "vv";
};
};
```
`env` values are forced; `envDefault` values can be overridden by the user's shell.
### Choosing the right override surface
- Use `cats` to enable or disable a whole language or tooling category.
- Use `settings.lang_packages` to add or replace Python, R, or Julia libraries within a language environment.
- Use `catPkgs` to change the full runtime tool list for a category. `config.runtimePkgs` drives wrapper PATH composition, while `nvimConfig.lib.devShellPackages config` returns the package-only list suitable for `mkShell`.
- Use `env` or `envDefault` for wrapper environment variables.
- Use `specs` for plugin additions or custom runtime behavior.
### Neovim settings
```nix
projectSettings = {
settings = {
colorscheme = "kanagawa";
background = "dark";
wrapRc = true;
config_directory = ./nvim; # path to init.lua + lua/ dir
aliases = [ "v" "nvim" ]; # extra binary names (default: [ "vvim" ])
};
binName = "nv"; # binary name (default: vv)
};
```
## Adding plugins
```nix
projectSettings = {
specs = {
extraPlugins = {
data = with pkgs.vimPlugins; [
telescope-nvim
which-key-nvim
];
};
extraLazy = {
lazy = true;
data = with pkgs.vimPlugins; [ dashboard-nvim ];
};
# Inject Lua config at startup
extraLua = {
data = pkgs.writeText "extra.lua" ''
vim.opt.number = true
vim.opt.relativenumber = true
'';
before = ["INIT_MAIN"];
};
};
};
```
## Architecture
```
settings/lang-packages.nix ──► settings.lang_packages
cats.nix ───────────────► config.cats │
│ │
▼ ▼
cat-packages.nix ───────► config.catPkgs.<cat>
│ │
├──────────────┐ │
▼ ▼ ▼
deps.nix PATH wrapper.config.wrap devShells.default
```
- **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 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.

99
flake.lock generated
View file

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1781337902, "lastModified": 1776413584,
"narHash": "sha256-QjXxUvpOBbnlpSskBOZJ9nUbpYyFRD4cBo4s1aHniGs=", "narHash": "sha256-xqqv46MTveuT4yJH2YihmbHGy5mdLnnLFDebVmUws/E=",
"owner": "dwinkler1", "owner": "dwinkler1",
"repo": "fran", "repo": "fran",
"rev": "30ab141871ede23b245a90afea58e076ef83b515", "rev": "da09626e4dd8f0f57078b3a04e0443a8c20defa1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -22,11 +22,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1785967620, "lastModified": 1776169885,
"narHash": "sha256-IItrdb7Puk05RqOBWZYFC5X6Wl1sJmCfh5MWVHw5iMM=", "narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b7c2ada94fe99c15b0dbcf4d11fd7850b957a436", "rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -36,22 +36,6 @@
"type": "github" "type": "github"
} }
}, },
"plugins-bloocky": {
"flake": false,
"locked": {
"lastModified": 1786042339,
"narHash": "sha256-8QcoC1bS8pRcwBkW3mqD2vgP9Mu0aCdIy6umZO6mUGM=",
"owner": "atiladefreitas",
"repo": "bloocky",
"rev": "a052c3b1a8126e04b194bb3db3c4c6ca641c0e4e",
"type": "github"
},
"original": {
"owner": "atiladefreitas",
"repo": "bloocky",
"type": "github"
}
},
"plugins-cmp-pandoc-references": { "plugins-cmp-pandoc-references": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -68,51 +52,29 @@
"type": "github" "type": "github"
} }
}, },
"plugins-dooing": { "plugins-r": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1785971574, "lastModified": 1776340770,
"narHash": "sha256-GtEC5kWcH0DmiYXkzxzNjjfYLfyEb7zpTLQWdi7n3zs=", "narHash": "sha256-o/8UZIc/Bq9dWTjA+MpSR5uMUpE7KHTErk+TwWID8Ww=",
"owner": "atiladefreitas", "owner": "R-nvim",
"repo": "dooing", "repo": "R.nvim",
"rev": "6748316bc6b4601797fb952a83694cc0a5ef6da2", "rev": "b9cfffeb9b4e484aa9e13f01c0eb80230aada455",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "atiladefreitas", "owner": "R-nvim",
"repo": "dooing", "repo": "R.nvim",
"type": "github"
}
},
"r-nvim-nix": {
"inputs": {
"nixpkgs": [
"rixpkgs"
],
"rnvimsrc": "rnvimsrc"
},
"locked": {
"lastModified": 1784867506,
"narHash": "sha256-5zXVJLG7+Gi9sgb4Dn7P6O/ywQQxpvcjjoHSJXZEuVo=",
"owner": "dwinkler1",
"repo": "r_nvim_nix",
"rev": "1d26452e3c940153271fc57fbad0b295fcb0e04f",
"type": "github"
},
"original": {
"owner": "dwinkler1",
"ref": "v1.0.0",
"repo": "r_nvim_nix",
"type": "github" "type": "github"
} }
}, },
"rixpkgs": { "rixpkgs": {
"locked": { "locked": {
"lastModified": 1785776890, "lastModified": 1771303851,
"narHash": "sha256-CzzgB1teVbroR/QhGAnsQToUYCDXEZZtFTcISAooASM=", "narHash": "sha256-tgveHozOJ2D/mi3LxVy/FcmLFDlM5XKZxsNB2XpvzaM=",
"owner": "dwinkler1", "owner": "dwinkler1",
"repo": "rixpkgs", "repo": "rixpkgs",
"rev": "a54080a58b57d5785dc85901850b817bef2aaf83", "rev": "af2dd3f7b4b172077747c0869d4e30702fb71b0e",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -122,31 +84,12 @@
"type": "github" "type": "github"
} }
}, },
"rnvimsrc": {
"flake": false,
"locked": {
"lastModified": 1783264911,
"narHash": "sha256-FywUL3mV2+kfu+rO6uUFyUv80EflbdgSkYuSnW965UE=",
"owner": "R-nvim",
"repo": "R.nvim",
"rev": "c56ebe0f8445e251673981c40ac2d74659ecd6ed",
"type": "github"
},
"original": {
"owner": "R-nvim",
"ref": "v1.0.0",
"repo": "R.nvim",
"type": "github"
}
},
"root": { "root": {
"inputs": { "inputs": {
"fran": "fran", "fran": "fran",
"nixpkgs": "nixpkgs", "nixpkgs": "nixpkgs",
"plugins-bloocky": "plugins-bloocky",
"plugins-cmp-pandoc-references": "plugins-cmp-pandoc-references", "plugins-cmp-pandoc-references": "plugins-cmp-pandoc-references",
"plugins-dooing": "plugins-dooing", "plugins-r": "plugins-r",
"r-nvim-nix": "r-nvim-nix",
"rixpkgs": "rixpkgs", "rixpkgs": "rixpkgs",
"wrappers": "wrappers" "wrappers": "wrappers"
} }
@ -158,11 +101,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1782135443, "lastModified": 1776375800,
"narHash": "sha256-vAmbArdCyjqpVW+37aCy/PMBOLIqukUXLQuEKLwUhA4=", "narHash": "sha256-/SSAR77Brr9fbapsh1cb2K47JXCbvwS1GjM4yyDxle8=",
"owner": "BirdeeHub", "owner": "BirdeeHub",
"repo": "nix-wrapper-modules", "repo": "nix-wrapper-modules",
"rev": "6e7f66fa2cdf4d63162580b438f7fcf87c28a46f", "rev": "f11469ca69068bac13d9e163b2bd268cc06dff57",
"type": "github" "type": "github"
}, },
"original": { "original": {

246
flake.nix
View file

@ -1,23 +1,17 @@
# Copyright (c) 2026 BirdeeHub & Daniel # Copyright (c) 2026 BirdeeHub
# Licensed under the MIT license # Licensed under the MIT license
{ {
description = "Daniel's NixCats"; description = "Daniel's NixCats";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
#nixpkgs.url = "github:nixos/nixpkgs/nixos-25.11";
wrappers = { wrappers = {
url = "github:BirdeeHub/nix-wrapper-modules"; url = "github:BirdeeHub/nix-wrapper-modules";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
rixpkgs.url = "github:dwinkler1/rixpkgs/nixpkgs"; rixpkgs.url = "github:dwinkler1/rixpkgs/nixpkgs";
r-nvim-nix = {
url = "github:dwinkler1/r_nvim_nix/v1.0.0";
inputs = {
nixpkgs.follows = "rixpkgs";
};
};
fran = { fran = {
url = "github:dwinkler1/fran"; url = "github:dwinkler1/fran";
inputs = { inputs = {
@ -25,20 +19,15 @@
}; };
}; };
"plugins-r" = {
url = "github:R-nvim/R.nvim";
flake = false;
};
"plugins-cmp-pandoc-references" = { "plugins-cmp-pandoc-references" = {
url = "github:jmbuhr/cmp-pandoc-references"; url = "github:jmbuhr/cmp-pandoc-references";
flake = false; flake = false;
}; };
"plugins-bloocky" = {
url = "github:atiladefreitas/bloocky";
flake = false;
};
"plugins-dooing" = {
url = "github:atiladefreitas/dooing";
flake = false;
};
}; };
outputs = { outputs = {
@ -47,42 +36,46 @@
wrappers, wrappers,
... ...
} @ inputs: let } @ inputs: let
devShellCatOrder = [ wrapperSettings = pkgs: let
"always" def = pkgs.lib.mkDefault;
"clickhouse" in
"external" wrapper.config.wrap {
"julia"
"lua"
"markdown"
"nix"
"optional"
"python"
"r"
"treesitterParsers"
];
evalWithPkgs = pkgs: extraModules:
wrappers.lib.evalModules {
specialArgs = {
inherit pkgs; inherit pkgs;
cats = {
clickhouse = def false;
gitPlugins = def true;
julia = def false;
lua = def true;
markdown = def false;
nix = def true;
optional = def false;
python = def false;
r = def false;
}; };
modules =
[ settings = {
module lang_packages = {
] python = with pkgs.python3Packages; [
++ extraModules; duckdb
polars
];
r = with pkgs.rpkgs.rPackages; [
arrow
broom
data_table
janitor
styler
];
julia = ["DataFramesMeta" "QuackIO"];
};
colorscheme = def "cyberdream";
background = def "dark";
wrapRc = def true;
};
binName = def "vv";
}; };
mkDevShellPackages = config:
builtins.concatLists (map (name: config.catPkgs.${name} or []) devShellCatOrder);
mkShellHook = config:
''
echo 'I am a NixShell'
''
+ nixpkgs.lib.optionalString (config.cats.r or false) ''
export R_HOME=$(R RHOME)
export R_LIBS_SITE=$(strings "$(command -v R)" | rg -o '/nix/store/[^:]+/library' | sort -u | paste -sd: -)
export R_LIBS_USER="$PWD/.r-libs"
mkdir -p "$R_LIBS_USER"
'';
systems = [ systems = [
"aarch64-darwin" "aarch64-darwin"
@ -92,49 +85,54 @@
forAllSystems = nixpkgs.lib.genAttrs systems; forAllSystems = nixpkgs.lib.genAttrs systems;
extra_pkg_config = {
# allowUnfree = true;
};
overlayDefs = import ./overlays inputs; overlayDefs = import ./overlays inputs;
dependencyOverlays = overlayDefs.dependencyOverlays;
dependencyOverlay = overlayDefs.dependencyOverlay;
mkPkgs = system: mkPkgs = system:
import nixpkgs { import nixpkgs {
inherit system; inherit system;
config = {allowUnfree = true;}; config = extra_pkg_config;
overlays = [ overlays = [dependencyOverlay];
overlayDefs.dependencyOverlay
];
}; };
module = (import ./modules/neovim.nix) inputs; module = nixpkgs.lib.modules.importApply ./modules/neovim.nix inputs;
wrapper = wrappers.lib.evalModule module;
in { in {
lib = {
eval = {pkgs, modules ? []}: evalWithPkgs pkgs modules;
mkWrapper = {pkgs, modules ? []}: (evalWithPkgs pkgs modules).config.wrap {inherit pkgs;};
devShellPackages = config: mkDevShellPackages config;
shellHook = config: mkShellHook config;
};
overlays = { overlays = {
# overlay `vv` wraps the module with default settings only.
# It is evaluated against the final package set so module defaults can
# depend on overlays such as rixpkgs-backed `pkgs.rpkgs`.
default = nixpkgs.lib.composeManyExtensions [ default = nixpkgs.lib.composeManyExtensions [
overlayDefs.dependencyOverlay dependencyOverlay
(final: prev: { (final: prev: {
vv = (evalWithPkgs final []).config.wrap {pkgs = final;}; vv = wrapper.config.wrap {pkgs = final;};
}) })
]; ];
dependencies = overlayDefs.dependencyOverlay; dependencies = dependencyOverlay;
vv = self.overlays.default;
}; };
wrapperModules.default = module; wrapperModules = {
wrapperConfigs.default = {pkgs, modules ? []}: (self.lib.eval {inherit pkgs modules;}).config; default = module;
neovim = self.wrapperModules.default;
};
wrappers = {
default = wrapper.config;
neovim = self.wrappers.default;
};
packages = forAllSystems ( packages = forAllSystems (
system: let system: let
pkgs = mkPkgs system; pkgs = mkPkgs system;
nvimPkg = wrapperSettings pkgs;
in { in {
default = self.lib.mkWrapper { default = nvimPkg;
inherit pkgs; vv = nvimPkg;
};
} }
); );
@ -148,13 +146,14 @@
devShells = forAllSystems ( devShells = forAllSystems (
system: let system: let
pkgs = mkPkgs system; pkgs = mkPkgs system;
config = (self.lib.eval {inherit pkgs;}).config; nvimPkg = wrapperSettings pkgs;
nvimPkg = config.wrap {inherit pkgs;};
in { in {
default = pkgs.mkShell { default = pkgs.mkShell {
name = "vShell"; name = "vShell";
packages = [nvimPkg] ++ self.lib.devShellPackages config; packages = [nvimPkg];
shellHook = mkShellHook config; nativeBuildInputs = with pkgs; [] ++ (pkgs.lib.optionals self.wrappers.default.cats.optional [devenv]);
inputsFrom = [];
shellHook = "";
}; };
} }
); );
@ -162,34 +161,20 @@
checks = forAllSystems ( checks = forAllSystems (
system: let system: let
pkgs = mkPkgs system; pkgs = mkPkgs system;
defaultConfig = (self.lib.eval {inherit pkgs;}).config; nvimPkg = wrapperSettings pkgs;
defaultNvimPkg = defaultConfig.wrap {inherit pkgs;};
defaultShellHook = mkShellHook defaultConfig;
overrideConfig =
(self.lib.eval {
inherit pkgs;
modules = [
{
cats = {
r = false;
python = true;
};
settings.lang_packages.python = nixpkgs.lib.mkForce (with pkgs.python3Packages; [
pandas
]);
catPkgs.nix = nixpkgs.lib.mkForce [
pkgs.alejandra
];
}
];
}).config;
overrideShellHook = mkShellHook overrideConfig;
in { in {
default = pkgs.runCommand "check-vv" {} '' default = nvimPkg;
BINARY_PATH="${defaultNvimPkg}/bin/vv" module-eval = let
_ = wrapper.config;
in
pkgs.runCommand "check-module-eval" {} ''
echo "Module evaluation successful" > $out
'';
package-build = pkgs.runCommand "check-vv" {} ''
BINARY_PATH="${nvimPkg}/bin/vv"
if [ ! -x "$BINARY_PATH" ]; then if [ ! -x "$BINARY_PATH" ]; then
echo "Error: Binary not found or not executable" echo "Error: Binary n not found or not executable"
exit 1 exit 1
fi fi
@ -202,67 +187,18 @@
cat version_output.txt >> $out cat version_output.txt >> $out
fi fi
''; '';
module-eval = let
_ = (self.lib.eval {inherit pkgs;}).config;
in
pkgs.runCommand "check-module-eval" {} ''
echo "Module evaluation successful" > $out
'';
downstream-overrides = let
overrideNix = builtins.map (p: p.pname or p.name) overrideConfig.catPkgs.nix;
defaultAssertions = [
(defaultConfig.cats.r or false)
(builtins.match ".*R RHOME.*" defaultShellHook != null)
(builtins.length (self.lib.devShellPackages defaultConfig) > 0)
];
overrideAssertions = [
(!(overrideConfig.cats.r or false))
(builtins.length overrideNix == 1)
((builtins.head overrideNix) == "alejandra")
(builtins.match ".*R RHOME.*" overrideShellHook == null)
];
in
pkgs.runCommand "check-downstream-overrides" {
pass =
if builtins.all (x: x) (defaultAssertions ++ overrideAssertions)
then "1"
else "";
} ''
if [ -z "$pass" ]; then
echo "Downstream override assertions failed" >&2
exit 1
fi
echo "Downstream override assertions passed" > $out
'';
lua-test = pkgs.runCommand "lua-test" {
buildInputs = [ pkgs.neovim-unwrapped ];
} ''
nvim --headless -u NONE -c "set runtimepath+=${./.}" -l ${./tests/init.lua}
touch $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
'';
} }
); );
nixosModules.default = wrappers.lib.getInstallModule { nixosModules.default = wrappers.lib.mkInstallModule {
name = "vModule"; name = "vModule";
value = module; value = module;
}; };
homeModules.default = wrappers.lib.getInstallModule { homeModules.default = wrappers.lib.mkInstallModule {
name = "vModule"; name = "vModule";
value = module; value = module;
loc = ["home" "packages"];
}; };
}; };
} }

View file

@ -1,7 +1,5 @@
-- Add the key mappings only for Markdown files in a zk notebook. -- Add the key mappings only for Markdown files in a zk notebook.
local nix = require("config.nix") if require("zk.util").notebook_root(vim.fn.expand('%:p')) ~= nil then
if nix.get_cat("markdown", false) then
if require("zk.util").notebook_root(vim.fn.expand('%:p')) ~= nil then
local map = vim.keymap.set local map = vim.keymap.set
-- Open the link under the caret. -- Open the link under the caret.
map("n", "<CR>", "<Cmd>lua vim.lsp.buf.definition()<CR>", { noremap = true, silent = false, buffer = true }) map("n", "<CR>", "<Cmd>lua vim.lsp.buf.definition()<CR>", { noremap = true, silent = false, buffer = true })
@ -31,5 +29,4 @@ if nix.get_cat("markdown", false) then
map("v", "<leader>za", ":'<,'>lua vim.lsp.buf.range_code_action()<CR>", map("v", "<leader>za", ":'<,'>lua vim.lsp.buf.range_code_action()<CR>",
{ noremap = true, silent = false, buffer = true, desc = "Code actions" }) { noremap = true, silent = false, buffer = true, desc = "Code actions" })
end
end end

View file

@ -1,28 +1,23 @@
local quarto_ok, quarto = pcall(require, 'quarto') local quarto = require('quarto')
if quarto_ok then quarto.setup()
vim.keymap.set('n', '<leader>qp', quarto.quartoPreview, { silent = true, noremap = true, buffer = true }) vim.keymap.set('n', '<leader>qp', quarto.quartoPreview, { silent = true, noremap = true })
end
if vim.bo.filetype == "r" then vim.keymap.set("n", "<Enter>", "<Plug>RDSendLine", { buffer = true })
vim.keymap.set("n", "<Enter>", "<Plug>RDSendLine", { buffer = true }) vim.keymap.set("v", "<Enter>", "<Plug>RSendSelection", { buffer = true })
vim.keymap.set("v", "<Enter>", "<Plug>RSendSelection", { buffer = true })
-- Assignment operator (--) -- Assignment operator (--)
vim.keymap.set("i", "--", "<Cmd>lua MiniTrailspace.trim()<CR><Plug>RInsertAssign", { buffer = true, noremap = true }) vim.keymap.set("i", "--", "<Cmd>lua MiniTrailspace.trim()<CR><Plug>RInsertAssign", { buffer = true, noremap = true })
-- Pipe operator (;;) -- Pipe operator (;;)
vim.keymap.set("i", ";;", "<Cmd>lua MiniTrailspace.trim()<CR><Plug>RInsertPipe<CR>", { buffer = true, noremap = true }) vim.keymap.set("i", ";;", "<Cmd>lua MiniTrailspace.trim()<CR><Plug>RInsertPipe<CR>", { buffer = true, noremap = true })
end
local runner_ok, runner = pcall(require, "quarto.runner") local runner = require("quarto.runner")
if runner_ok then vim.keymap.set("n", "<localleader>a", runner.run_cell, { desc = "run cell", silent = true })
vim.keymap.set("n", "<localleader>a", runner.run_cell, { desc = "run cell", silent = true, buffer = true }) vim.keymap.set("n", "<localleader>A", runner.run_all, { desc = "run all cells", silent = true })
vim.keymap.set("n", "<localleader>A", runner.run_all, { desc = "run all cells", silent = true, buffer = true }) vim.keymap.set("n", "<localleader>RA", function()
vim.keymap.set("n", "<localleader>RA", function()
runner.run_all(true) runner.run_all(true)
end, { desc = "run all cells of all languages", silent = true, buffer = true }) end, { desc = "run all cells of all languages", silent = true })
end

View file

@ -1,5 +1,7 @@
local Config = require('config') _G.Config = {}
_G.Config = Config -- keep global alias for keymaps/backward compatibility local nix = require('config.nix').init { non_nix_value = true }
Config.isNixCats = nix.is_nix
Config.nixConfig = nix
require('lze').register_handlers(require('nixCatsUtils.lzUtils').for_cat) require('lze').register_handlers(require('nixCatsUtils.lzUtils').for_cat)
@ -10,11 +12,8 @@ if not Config.isNixCats then
local mini_path = path_package .. 'pack/deps/start/mini.nvim' local mini_path = path_package .. 'pack/deps/start/mini.nvim'
if not vim.uv.fs_stat(mini_path) then if not vim.uv.fs_stat(mini_path) then
vim.cmd('echo "Installing `mini.nvim`" | redraw') vim.cmd('echo "Installing `mini.nvim`" | redraw')
-- Pin to the stable branch for reproducible non-Nix installs.
-- Change the tag/branch here if you need a newer version.
local mini_nvim_tag = 'stable'
local clone_cmd = { local clone_cmd = {
'git', 'clone', '--filter=blob:none', '--branch=' .. mini_nvim_tag, 'git', 'clone', '--filter=blob:none',
'https://github.com/echasnovski/mini.nvim', mini_path 'https://github.com/echasnovski/mini.nvim', mini_path
} }
vim.fn.system(clone_cmd) vim.fn.system(clone_cmd)

View file

@ -1,10 +0,0 @@
--- Shared configuration / state table.
--- Plugin submodules attach their helpers to this table at runtime.
local M = {}
-- Detect whether this Neovim was launched via nixCats.
local nix = require('config.nix').init { non_nix_value = true }
M.isNixCats = nix.is_nix
M.nixConfig = nix
return M

View file

@ -1,44 +0,0 @@
local Config = require('config')
-- Basic mappings =============================================================
-- NOTE: Most basic mappings come from 'mini.basics'
-- Shorter version of the most frequent way of going outside of terminal window
vim.keymap.set('t', '<C-h>', [[<C-\><C-N><C-w>h]])
-- Select all
-- vim.keymap.set({ "n", "v", "x" }, "<C-a>", "gg3vG$", { noremap = true, silent = true, desc = "Select all" })
-- Escape deletes highlights
vim.keymap.set("n", "<Esc>", "<cmd>nohlsearch<CR>")
-- Paste before/after linewise
local cmd = vim.fn.has('nvim-0.12') == 1 and 'iput' or 'put'
vim.keymap.set({ 'n', 'x' }, '[p', '<Cmd>exe "' .. cmd .. '! " . v:register<CR>', { desc = 'Paste Above' })
vim.keymap.set({ 'n', 'x' }, ']p', '<Cmd>exe "' .. cmd .. ' " . v:register<CR>', { desc = 'Paste Below' })
vim.keymap.set({ "n", "v", "x" }, "<leader>p", '"+p', { noremap = true, silent = true, desc = "Paste from clipboard" })
vim.keymap.set({ "n", "v", "x" }, "<leader>y", '"+y', { noremap = true, silent = true, desc = "Copy toclipboard" })
-- Create global tables with information about clue group in certain modes
-- Structure of tables is taken to be compatible with 'mini.clue'.
Config.leader_group_clues = {
{ mode = 'n', keys = '<Leader>a', desc = '+AI' },
{ mode = 'n', keys = '<Leader>b', desc = '+Buffer' },
{ mode = 'n', keys = '<Leader>e', desc = '+Explore' },
{ mode = 'n', keys = '<Leader>f', desc = '+Find' },
{ mode = 'n', keys = '<Leader>fl', desc = '+LSP' },
{ mode = 'n', keys = '<Leader>fa', desc = '+Git' },
{ mode = 'n', keys = '<Leader>g', desc = '+Git' },
{ mode = 'n', keys = '<Leader>l', desc = '+LSP' },
{ mode = 'n', keys = '<Leader>L', desc = '+Lua/Log' },
{ mode = 'n', keys = '<Leader>o', desc = '+Other' },
{ mode = 'n', keys = '<Leader>r', desc = '+R' },
{ mode = 'n', keys = '<Leader>s', desc = '+Send' },
{ mode = 'n', keys = '<Leader>d', desc = '+Debug' },
{ mode = 'n', keys = '<Leader>t', desc = '+Terminal' },
{ mode = 'n', keys = '<Leader>u', desc = '+UI' },
{ mode = 'n', keys = '<Leader>v', desc = '+Visits' },
{ mode = 'n', keys = '<Leader>w', desc = '+Windows' },
{ mode = 'x', keys = '<Leader>l', desc = '+LSP' },
{ mode = 'x', keys = '<Leader>r', desc = '+R' },
{ mode = 'n', keys = '<Leader>z', desc = '+ZK' },
{ mode = 'n', keys = '<Leader>zr', desc = '+Reviews' },
{ mode = 'x', keys = '<Leader>a', desc = '+AI' },
}

View file

@ -1,25 +0,0 @@
local M = {}
---Create a normal-mode `<Leader>` mapping.
function M.nmap_leader(suffix, rhs, desc, opts)
opts = opts or {}
opts.desc = desc
vim.keymap.set('n', '<Leader>' .. suffix, rhs, opts)
end
---Create a visual-mode `<Leader>` mapping.
function M.xmap_leader(suffix, rhs, desc, opts)
opts = opts or {}
opts.desc = desc
vim.keymap.set('x', '<Leader>' .. suffix, rhs, opts)
end
---Create a normal-mode LSP keymap with an "(LSP)" suffix in the description.
function M.nmap_lsp(keys, func, desc)
if desc then
desc = desc .. "(LSP)"
end
vim.keymap.set("n", keys, func, { desc = desc })
end
return M

View file

@ -1,293 +0,0 @@
local Config = require('config')
local helpers = require('keymap.helpers')
local nmap_leader = helpers.nmap_leader
local xmap_leader = helpers.xmap_leader
local nmap_lsp = helpers.nmap_lsp
-- stylua: ignore start
-- Switch buffers
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")
-- b is for 'buffer'
nmap_leader('bb', '<Cmd>b#<CR>', 'Alternate')
nmap_leader('bd', '<Cmd>lua MiniBufremove.delete()<CR>', 'Delete')
nmap_leader('bD', '<Cmd>lua MiniBufremove.delete(0, true)<CR>', 'Delete!')
nmap_leader('bs', '<Cmd>lua Config.new_scratch_buffer()<CR>', 'Scratch')
nmap_leader('bw', '<Cmd>lua MiniBufremove.wipeout()<CR>', 'Wipeout')
nmap_leader('bW', '<Cmd>lua MiniBufremove.wipeout(0, true)<CR>', 'Wipeout!')
nmap_leader('bq', '<Cmd>qall<CR>', 'Quit all')
-- e is for 'explore' and 'edit'
nmap_leader('ed', '<Cmd>lua MiniFiles.open()<CR>', 'Directory')
nmap_leader('ef', '<Cmd>lua Config.try_opendir()<CR>', 'File directory')
nmap_leader('es', '<Cmd>lua MiniSessions.select()<CR>', 'Sessions')
nmap_leader('eq', '<Cmd>lua Config.toggle_quickfix()<CR>', 'Quickfix')
nmap_leader('ez', '<Cmd>lua MiniFiles.open(os.getenv("ZK_NOTEBOOK_DIR"))<CR>', 'Notes directory')
-- f is for 'fuzzy find'
nmap_leader('f/', '<Cmd>Pick history scope="/"<CR>', '"/" history')
nmap_leader('f:', '<Cmd>Pick history scope=":"<CR>', '":" history')
nmap_leader('f,', '<Cmd>Pick visit_labels<CR>', 'Visit labels')
nmap_leader('faa', '<Cmd>Pick git_hunks scope="staged"<CR>', 'Added hunks (all)')
nmap_leader('faA', '<Cmd>Pick git_hunks path="%" scope="staged"<CR>', 'Added hunks (current)')
nmap_leader('fb', '<Cmd>Pick buffers<CR>', 'Buffers')
nmap_leader(',', '<Cmd>Pick buffers<CR>', 'Buffers')
nmap_leader('fac', '<Cmd>Pick git_commits<CR>', 'Commits (all)')
nmap_leader('faC', '<Cmd>Pick git_commits path="%"<CR>', 'Commits (current)')
nmap_leader('fd', '<Cmd>Pick diagnostic scope="all"<CR>', 'Diagnostic workspace')
nmap_leader('fD', '<Cmd>Pick diagnostic scope="current"<CR>', 'Diagnostic buffer')
nmap_leader('ff', '<Cmd>Pick files<CR>', 'Files')
nmap_leader('fg', '<Cmd>Pick grep_live<CR>', 'Grep live')
nmap_leader('fG', '<Cmd>Pick grep pattern="<cword>"<CR>', 'Grep current word')
nmap_leader('fh', '<Cmd>Pick help<CR>', 'Help tags')
nmap_leader('fH', '<Cmd>Pick hl_groups<CR>', 'Highlight groups')
nmap_leader('fj', '<Cmd>Pick buf_lines scope="all"<CR>', 'Lines (all)')
nmap_leader('fJ', '<Cmd>Pick buf_lines scope="current"<CR>', 'Lines (current)')
nmap_leader('fam', '<Cmd>Pick git_hunks<CR>', 'Modified hunks (all)')
nmap_leader('faM', '<Cmd>Pick git_hunks path="%"<CR>', 'Modified hunks (current)')
nmap_leader('fm', '<Cmd>Pick marks<CR>', 'Marks')
nmap_leader('fn', '<cmd>ZkNotes<CR>', "Notes")
nmap_leader('fk', '<Cmd>Pick keymaps<CR>', 'Keymaps')
nmap_leader('fR', '<Cmd>Pick resume<CR>', 'Resume')
nmap_leader('fp', '<Cmd>Pick files<CR>', 'Files')
nmap_leader('fq', '<Cmd>Pick list scope="quickfix"<CR>', 'Quickfix')
nmap_leader('fr', '<Cmd>Pick lsp scope="references"<CR>', 'References (LSP)')
nmap_leader('flr', '<Cmd>Pick lsp scope="references"<CR>', 'References (LSP)')
nmap_leader('fS', '<Cmd>Pick lsp scope="workspace_symbol"<CR>', 'Symbols workspace (LSP)')
nmap_leader('flS', '<Cmd>Pick lsp scope="workspace_symbol"<CR>', 'Symbols workspace (LSP)')
nmap_leader('fs', '<Cmd>Pick lsp scope="document_symbol"<CR>', 'Symbols buffer (LSP)')
nmap_leader('fls', '<Cmd>Pick lsp scope="document_symbol"<CR>', 'Symbols buffer (LSP)')
nmap_leader('fld', '<Cmd>Pick lsp scope="definition"<CR>', 'Definition (LSP)')
nmap_leader('flD', '<Cmd>Pick lsp scope="declaration"<CR>', 'Declaration (LSP)')
nmap_leader('flt', '<Cmd>Pick lsp scope="type_definition"<CR>', 'Type Definition (LSP)')
nmap_leader('fv', '<Cmd>Pick visit_paths cwd=""<CR>', 'Visit paths (all)')
nmap_leader('fV', '<Cmd>Pick visit_paths<CR>', 'Visit paths (cwd)')
-- g is for git
local git_log_cmd = [[Git log --pretty=format:\%h\ \%as\ │\ \%s --topo-order]]
nmap_leader('gc', '<Cmd>Git commit<CR>', 'Commit')
nmap_leader('gC', '<Cmd>Git commit --amend<CR>', 'Commit amend')
nmap_leader('gd', '<Cmd>Git diff<CR>', 'Diff')
nmap_leader('gD', '<Cmd>Git diff -- %<CR>', 'Diff buffer')
nmap_leader("gg", "<cmd>Neogit<cr>", "Open Neogit UI")
nmap_leader('gl', '<Cmd>' .. git_log_cmd .. '<CR>', 'Log')
nmap_leader('gL', '<Cmd>' .. git_log_cmd .. ' --follow -- %<CR>', 'Log buffer')
nmap_leader('go', '<Cmd>lua MiniDiff.toggle_overlay()<CR>', 'Toggle overlay')
nmap_leader('gp', '<Cmd>Git pull<CR>', 'Pull')
nmap_leader('gP', '<Cmd>Git push<CR>', 'Push')
nmap_leader('gs', '<Cmd>lua MiniGit.show_at_cursor()<CR>', 'Show at cursor')
xmap_leader('gs', '<Cmd>lua MiniGit.show_at_cursor()<CR>', 'Show at selection')
-- j/k navigate quickfix
nmap_leader("j", '<cmd>cnext<CR>zz', "Quickfix next")
nmap_leader("k", '<cmd>cprev<CR>zz', "Quickfix prev")
-- l is for 'LSP' (Language Server Protocol)
vim.keymap.set({ 'n' }, 'grd', '<Cmd>lua vim.lsp.buf.definition()<CR>', { desc = 'Definition' })
vim.keymap.set({ 'n' }, 'grk', '<Cmd>lua vim.lsp.buf.hover()<CR>', { desc = 'Documentation' })
vim.keymap.set({ 'n' }, 'gre', '<Cmd>lua vim.diagnostic.open_float()<CR>', { desc = 'Diagnostics' })
nmap_lsp("K", '<Cmd>lua vim.lsp.buf.hover()<CR>', "Documentation")
local formatting_cmd = '<Cmd>lua require("conform").format({ lsp_format = "fallback" })<CR>'
nmap_leader('la', '<Cmd>lua vim.lsp.buf.code_action()<CR>', 'Actions')
nmap_leader('le', '<Cmd>lua vim.diagnostic.open_float()<CR>', 'Diagnostics popup')
nmap_leader('lf', formatting_cmd, 'Format')
nmap_leader('lk', '<Cmd>lua vim.lsp.buf.hover()<CR>', 'Documentation')
nmap_leader('li', '<Cmd>lua vim.lsp.buf.implementation()<CR>', 'Information')
-- use ]d and [d
--nmap_leader('lj', '<Cmd>lua vim.diagnostic.goto_next()<CR>', 'Next diagnostic')
--nmap_leader('lk', '<Cmd>lua vim.diagnostic.goto_prev()<CR>', 'Prev diagnostic')
nmap_leader('lR', '<Cmd>lua vim.lsp.buf.references()<CR>', 'References')
nmap_leader('lr', '<Cmd>lua vim.lsp.buf.rename()<CR>', 'Rename')
nmap_leader('ls', '<Cmd>lua vim.lsp.buf.definition()<CR>', 'Source definition')
xmap_leader('lf', formatting_cmd, 'Format selection')
-- L is for 'Lua'
nmap_leader('Lc', '<Cmd>lua Config.log_clear()<CR>', 'Clear log')
nmap_leader('LL', '<Cmd>luafile %<CR><Cmd>echo "Sourced lua"<CR>', 'Source buffer')
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 free
-- o is for 'other'
local trailspace_toggle_command = '<Cmd>lua vim.b.minitrailspace_disable = not vim.b.minitrailspace_disable<CR>'
nmap_leader('oh', '<Cmd>normal gxiagxila<CR>', 'Move arg left')
nmap_leader('ol', '<Cmd>normal gxiagxina<CR>', 'Move arg right')
nmap_leader('or', '<Cmd>lua MiniMisc.resize_window()<CR>', 'Resize to default width')
nmap_leader('ot', '<Cmd>lua MiniTrailspace.trim()<CR>', 'Trim trailspace')
nmap_leader('oT', trailspace_toggle_command, 'Trailspace hl toggle')
nmap_leader('oz', '<Cmd>lua MiniMisc.zoom()<CR>', 'Zoom toggle')
nmap_leader('ow',
"<Cmd>lua MiniSessions.write(vim.fn.input('Session name: ', string.match(vim.fn.getcwd(), \"[^/]+$\") .. '-session.vim'))<CR>",
'Write session')
-- r is for 'R'
nmap_leader('rc', '<Cmd>RSend devtools::check()<CR>', 'Check')
nmap_leader('rC', '<Cmd>RSend devtools::test_coverage()<CR>', 'Coverage')
nmap_leader('rd', '<Cmd>RSend devtools::document()<CR>', 'Document')
nmap_leader('ri', '<Cmd>RSend devtools::install(keep_source=TRUE)<CR>', 'Install')
nmap_leader('rk', '<Cmd>RSend quarto::quarto_preview("%")<CR>', 'Knit file')
nmap_leader('rl', '<Cmd>RSend devtools::load_all()<CR>', 'Load all')
nmap_leader('rL', '<Cmd>RSend devtools::load_all(recompile=TRUE)<CR>', 'Load all recompile')
nmap_leader('rm', '<Cmd>RSend Rcpp::compileAttributes()<CR>', 'Run examples')
nmap_leader('rT', '<Cmd>RSend testthat::test_file("%")<CR>', 'Test file')
nmap_leader('rt', '<Cmd>RSend devtools::test()<CR>', 'Test')
-- Visual `<leader>r` bindings (dispatched by the filetype-aware REPL
-- runner in `lua/keymap/repl.lua`). Previously the visual clue group was
-- empty, so this restores parity between the visual and normal `r`-group.
xmap_leader('rr', function() require('keymap.repl').send_selection() end, 'Send selection to REPL')
-- - Copy to clipboard and make reprex (which itself is loaded to clipboard)
xmap_leader('rx', '"+y :RSend reprex::reprex()<CR>', 'Reprex selection')
-- s is for 'send' (Send text to the active REPL/runner)
nmap_leader('s', function() require('keymap.repl').send_line() end, 'Send to REPL')
xmap_leader('s', function() require('keymap.repl').send_selection() end, 'Send selection to REPL')
-- d is for 'debug' (nvim-dap)
nmap_leader('db', '<Cmd>lua require("dap").toggle_breakpoint()<CR>', 'Toggle breakpoint')
nmap_leader('dB', '<Cmd>lua require("dap").set_breakpoint(vim.fn.input("Condition: "))<CR>', 'Conditional breakpoint')
nmap_leader('dc', '<Cmd>lua require("dap").continue()<CR>', 'Continue')
nmap_leader('do', '<Cmd>lua require("dap").step_over()<CR>', 'Step over')
nmap_leader('di', '<Cmd>lua require("dap").step_into()<CR>', 'Step into')
nmap_leader('dO', '<Cmd>lua require("dap").step_out()<CR>', 'Step out')
nmap_leader('dr', '<Cmd>lua require("dap").repl.open()<CR>', 'Open DAP REPL')
nmap_leader('du', '<Cmd>lua require("dapui").toggle()<CR>', 'Toggle DAP UI')
nmap_leader('dK', '<Cmd>lua require("dapui").eval()<CR>', 'Evaluate expression')
-- u is for UI
nmap_leader('ut', '<Cmd>TSContext toggle<CR>', 'Toggle TScontext')
nmap_leader('ua', '<Cmd>Copilot toggle<CR>', 'Toggle AI completion')
-- v is for 'visits'
nmap_leader('vv', '<Cmd>lua MiniVisits.add_label("core")<CR>', 'Add "core" label')
nmap_leader('vV', '<Cmd>lua MiniVisits.remove_label("core")<CR>', 'Remove "core" label')
nmap_leader('vl', '<Cmd>lua MiniVisits.add_label()<CR>', 'Add label')
nmap_leader('vL', '<Cmd>lua MiniVisits.remove_label()<CR>', 'Remove label')
local map_pick_core = function(keys, cwd, desc)
local rhs = function()
local sort_latest = MiniVisits.gen_sort.default({ recency_weight = 1 })
MiniExtra.pickers.visit_paths({
cwd = cwd,
filter = 'core',
sort = sort_latest
}, { source = { name = desc } })
end
nmap_leader(keys, rhs, desc)
end
map_pick_core('vc', '', 'Core visits (all)')
map_pick_core('vC', nil, 'Core visits (cwd)')
-- w is for 'windows'
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 })
nmap_leader("wd", "<C-W>c", "Delete Window", { remap = true })
nmap_leader("wo", "<C-W>o", "Delete Other Windows", { remap = true })
-- z is for 'ZettelKasten'
nmap_leader("zo", '<Cmd>ZkNotes<CR>', "Notes")
nmap_leader("zt", '<Cmd>ZkTags<cr>', "Tags")
nmap_leader(
"zrd",
'<Cmd>ZkNew { group = "dreviews" }<CR>',
"Daily Review"
)
nmap_leader(
"zrw",
'<Cmd>ZkNew { group = "wreviews" }<CR>',
"Weekly Review"
)
nmap_leader(
"zn",
'<Cmd>ZkNew { group = "inbox", title = vim.fn.input("Title: ") }<CR>',
"New"
)
nmap_leader(
"zp",
"<Cmd>ZkNew { group = 'permanent', title = vim.fn.input('Title: ') }<CR>",
"Permanent"
)
nmap_leader(
"zl",
"<Cmd>ZkNew { group = 'literature', title = vim.fn.input('Title: '), extra.author = vim.fn.input('Author: '), extra.year = vim.fn.input('Year: ') }<CR>",
"Literature"
)
nmap_leader(
"zd",
"<Cmd>ZkNew { group = 'dashboard', title = vim.fn.input('Title: ') }<CR>",
"Dashboard"
)
nmap_leader(
"zP",
"<Cmd>ZkNew { group = 'project', title = vim.fn.input('Title: ')}<CR>",
"Project"
)
-- stylua: ignore end

View file

@ -1,101 +0,0 @@
--- Filetype-aware REPL/runner dispatcher.
--- Keeps the existing terminal setup intact; it only decides *which* runner
--- to use for the current buffer/filetype.
local M = {}
--- Resolve the filetype to use for dispatching.
--- Quarto buffers report `quarto`; R buffers report `r`. Fallback to the
--- actual filetype if no special handling is needed.
local function dispatch_ft()
local ft = vim.bo.filetype
if ft == "quarto" or ft == "rmd" or ft == "markdown" then
return "quarto"
end
return ft
end
--- Send the current line to the active REPL/runner.
function M.send_line()
local ft = dispatch_ft()
if ft == "r" then
-- R.nvim v1+ exposes a Lua API; try the modern `r.send` module first,
-- then fall back to the older `r.run` module, and finally to <Plug>.
local ok, rmod = pcall(require, "r.send")
if not ok or not rmod then
ok, rmod = pcall(require, "r.run")
end
if ok and rmod then
if type(rmod.line) == "function" then
rmod.line()
return
elseif type(rmod.send_line) == "function" then
rmod.send_line()
return
end
end
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Plug>RDSendLine", true, false, true),
"m",
false
)
return
end
if ft == "quarto" then
local ok, runner = pcall(require, "quarto.runner")
if ok and runner then
if runner.run_line then
runner.run_line()
elseif runner.run_cell then
runner.run_cell()
end
return
end
end
-- Default: vim-slime (terminal).
local line = vim.api.nvim_get_current_line()
vim.fn["slime#send"](line .. "\n")
-- Move to the next line, matching the previous behaviour.
vim.cmd("normal! j")
end
--- Send the current visual selection to the active REPL/runner.
function M.send_selection()
local ft = dispatch_ft()
if ft == "r" then
-- Prefer R.nvim v1+ Lua API; try the modern `r.send` module first,
-- then fall back to the older `r.run` module, and finally to <Plug>.
local ok, rmod = pcall(require, "r.send")
if not ok or not rmod then
ok, rmod = pcall(require, "r.run")
end
if ok and rmod then
if type(rmod.selection) == "function" then
rmod.selection()
return
elseif type(rmod.send_selection) == "function" then
rmod.send_selection()
return
end
end
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Plug>RSendSelection", true, false, true),
"m",
false
)
return
end
-- For Quarto/others, fall back to vim-slime's visual send.
vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Plug>SlimeRegionSend", true, false, true),
"m",
false
)
end
return M

View file

@ -1,17 +0,0 @@
local Config = require('config')
local helpers = require('keymap.helpers')
local nmap_leader = helpers.nmap_leader
-- Exit terminal insert mode with <Esc>
vim.keymap.set("t", "<Esc>", [[<C-\><C-n>]], { desc = "Exit terminal mode" })
-- t is for 'terminal'
nmap_leader("tc", '<Cmd>lua Config.terminal.open_clickhouse_client()<CR>', 'Open Clickhouse client')
nmap_leader("tl", '<Cmd>lua Config.terminal.open_clickhouse_local()<CR>', 'Open Clickhouse local')
nmap_leader("tp", '<Cmd>lua Config.terminal.open_python()<CR>', 'Open Python')
nmap_leader("tj", '<Cmd>lua Config.terminal.open_julia()<CR>', 'Open Julia')
nmap_leader("td", '<Cmd>lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()<CR>', 'Open DuckDB')
nmap_leader("tx", '<Cmd>lua Config.terminal.open_in_terminal()<CR>', 'Terminal Command')
nmap_leader("tt", '<Cmd>lua Config.terminal.open_shell()<CR>', 'Terminal')
nmap_leader("tb", '<Cmd>lua Config.terminal.toggle_bracket()<CR>', 'Toggle bracketed paste')
nmap_leader("up", '<Cmd>lua Config.terminal.toggle_bracket()<CR>', 'Toggle bracketed paste')

View file

@ -1,12 +1,5 @@
local M = {} local M = {}
-- Define comment node types as constants
local COMMENT_TYPES = {
comment = true,
block_comment = true,
line_comment = true,
}
-- Helper function to check if value exists in list (optimized with early return) -- Helper function to check if value exists in list (optimized with early return)
local function is_in_list(list, value) local function is_in_list(list, value)
if not list or not value then if not list or not value then
@ -21,153 +14,183 @@ local function is_in_list(list, value)
return false return false
end end
-- Safely get the Tree-sitter node under the cursor. -- Define comment node types as constants
local COMMENT_TYPES = {
comment = true,
block_comment = true,
line_comment = true,
}
function M.get_current_node() function M.get_current_node()
local ok, node = pcall(vim.treesitter.get_node, { ignore_injections = false }) local ts_utils = require('nvim-treesitter.ts_utils')
return ok and node or nil local cur_win = vim.api.nvim_get_current_win()
return ts_utils.get_node_at_cursor(cur_win, true)
end end
-- Detect the root node type of the current buffer's Tree-sitter tree.
function M.detect_global_node() function M.detect_global_node()
local cur_node = M.get_current_node() local cur_node = M.get_current_node()
local root local root
if not cur_node then if not cur_node then
local ok, parser = pcall(vim.treesitter.get_parser) -- print("No node detected")
if not ok or not parser then local parser = vim.treesitter.get_parser()
if not parser then
return nil return nil
end end
local trees = parser:parse() root = parser:parse()[1]:root()
if not trees or not trees[1] then
return nil
end
root = trees[1]:root()
else else
root = cur_node:root() root = cur_node:root()
end end
return root and root:type() or nil if not root then
end
-- Ascend the tree from the current node until we hit a node whose parent is a
-- "global" node (or the root). This is the unit of code we want to send.
local function get_target_node(global_nodes)
local root_type = M.detect_global_node()
global_nodes = global_nodes or {}
local node = M.get_current_node()
if not node then
return nil return nil
end end
while node do return root:type()
local parent = node:parent()
if not parent then
break
end
local p_type = parent:type()
if is_in_list(global_nodes, p_type) or p_type == root_type then
break
end
node = parent
end
return node
end end
-- Move the cursor to the next named sibling that is not a comment. function M.move_to_next_non_empty_line()
-- Operates on the Tree-sitter AST instead of scanning lines, so it is fast and local ts_utils = require('nvim-treesitter.ts_utils')
-- language-agnostic. -- Search for the next non-empty line
function M.move_to_next_non_empty_line(current_node) local line_num = vim.fn.search("[^;\\s]", "W")
local node = current_node
if not node then if line_num <= 0 then
node = get_target_node({}) -- print("No non-empty line found below the current position")
end
if not node then
return false return false
end end
-- Walk up the tree until we find a node with a next named sibling, -- Get the line content and find first non-whitespace character
-- so we escape nested blocks when we are on the last statement. local line_content = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1]
while node and not node:next_named_sibling() do local first_non_ws = line_content:find("%S") or 1
node = node:parent() vim.api.nvim_win_set_cursor(0, { line_num, first_non_ws - 1 })
end
node = node:next_named_sibling() local node = M.get_current_node()
while node do if not node or not node:type() then
if not COMMENT_TYPES[node:type()] then -- print("No node found")
local start_row, start_col = node:range()
pcall(vim.api.nvim_win_set_cursor, 0, { start_row + 1, start_col })
return true, node
end
node = node:next_named_sibling()
end
return false
end
function M.vselect_node(node)
if not node then
return false return false
end end
local start_row, _, end_row, _ = node:range() local global_node_type = M.detect_global_node()
vim.api.nvim_win_set_cursor(0, { start_row + 1, 0 }) -- Skip comments and global nodes
vim.cmd("normal! V") while node and (COMMENT_TYPES[node:type()] or node:type() == global_node_type) do
vim.api.nvim_win_set_cursor(0, { end_row + 1, 0 }) line_num = line_num + 1
local max_lines = vim.api.nvim_buf_line_count(0)
if line_num > max_lines then
-- print("Reached end of buffer")
return false
end
-- Get the line content and find first non-whitespace character
line_content = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1]
first_non_ws = line_content:find("%S") or 1
vim.api.nvim_win_set_cursor(0, { line_num, first_non_ws - 1 })
node = ts_utils.get_node_at_cursor()
end
return true return true
end end
function M.vselect_node(node)
local ts_utils = require('nvim-treesitter.ts_utils')
if not node then
return false
end
local cur_buf = vim.api.nvim_get_current_buf()
ts_utils.update_selection(cur_buf, node, "V")
return true
end
function M.select_until_global(global_nodes) function M.select_until_global(global_nodes)
local target = get_target_node(global_nodes) local ts_utils = require('nvim-treesitter.ts_utils')
if not target then local root_node = M.detect_global_node()
if not root_node and global_nodes then
root_node = global_nodes[1]
end
-- Use empty table if no global nodes provided
global_nodes = global_nodes or {}
local node = ts_utils.get_node_at_cursor()
if not node then
-- print("No syntax node found at cursor position")
return nil return nil
end end
if M.vselect_node(target) then local node_type = node:type()
return target
if node_type == root_node then
-- print("Cursor is on the root " .. root_node .. " node or in an empty area.")
return nil
end
-- Check if current node is a global
if is_in_list(global_nodes, node_type) then
if M.vselect_node(node) then
return node
end
end
-- Traverse up the tree until we find a global node or reach the root
local parent = node:parent()
local parent_type = parent:type() or ""
if parent and is_in_list(global_nodes, parent_type) then
if M.vselect_node(node) then
return node
end
end
while parent and not is_in_list(global_nodes, parent:type()) do
node = parent
parent = node:parent()
end
if M.vselect_node(node) then
return node
end end
return nil return nil
end end
function M.slime_send_region() function M.slime_send_region()
vim.api.nvim_feedkeys( -- Check if slime plugin is available
vim.api.nvim_replace_termcodes("<Plug>SlimeRegionSend", true, false, true), if not vim.fn.exists('*slime#send_op') then
"m", vim.notify("slime plugin not available", vim.log.levels.ERROR)
false return
) end
local slime_command = ":<C-u>call slime#send_op(visualmode(), 1)<CR>"
local termcodes = vim.api.nvim_replace_termcodes(slime_command, true, true, true)
vim.api.nvim_feedkeys(termcodes, "x", true)
end end
function M.send_repl(global_nodes) function M.send_repl(global_nodes)
local target_node = get_target_node(global_nodes) local ts_utils = require('nvim-treesitter.ts_utils')
if not target_node then local cur_node = M.get_current_node()
if not cur_node then
M.move_to_next_non_empty_line()
else
local cur_type = cur_node:type()
if COMMENT_TYPES[cur_type] or is_in_list(global_nodes, cur_type) then
M.move_to_next_non_empty_line()
end
end
local sel_node = M.select_until_global(global_nodes)
if not sel_node then
-- print("No node selected for REPL")
return return
end end
-- If sitting on a comment, step forward first so we don't send comments. -- Send the selected text to the terminal using vim-slime
if COMMENT_TYPES[target_node:type()] then M.slime_send_region()
local moved, next_node = M.move_to_next_non_empty_line(target_node)
if not moved or not next_node then
return
end
target_node = next_node
end
-- Extract node text and send directly to avoid visual-mode/feedkeys races. -- Move cursor and continue
local ok, text = pcall(vim.treesitter.get_node_text, target_node, 0) ts_utils.goto_node(sel_node, true)
if not ok or not text then M.move_to_next_non_empty_line()
vim.notify("Could not extract code from Tree-sitter node", vim.log.levels.WARN)
return
end
vim.fn["slime#send"](text .. "\n")
-- Jump to the next relevant AST node instead of scanning lines
M.move_to_next_non_empty_line(target_node)
end end
return M return M

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

@ -1,120 +0,0 @@
{
config,
pkgs,
lib,
...
}:
let
# Include packages from a category only if that category is enabled.
# 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.lang_packages.r;
rWrapperPackages = rPackages;
quartoPkg =
if config.cats.r or false
then pkgs.rpkgs.quarto.override { extraRPackages = rPackages; }
else pkgs.quarto;
in
{
options.catPkgs = lib.mkOption {
type = lib.types.attrsOf (lib.types.listOf lib.types.package);
description = "Per-cat package lists, gated by cat toggles. Single source of truth for runtimeDeps and devShells.";
};
config.catPkgs = {
always = maybe "always" (with pkgs; [
ripgrep
]);
clickhouse = maybe "clickhouse" (with pkgs; [ clickhouse-lts ]);
external = maybe "external" (with pkgs; [
nodejs
perl
ruby
shfmt
sqlfluff
tree-sitter
]);
julia = maybe "julia" [
(pkgs.julia-bin.withPackages config.settings.lang_packages.julia)
];
lua = maybe "lua" (with pkgs; [ lua-language-server ]);
markdown = maybe "markdown" (with pkgs; [
python3Packages.pylatexenc
quartoPkg
zk
marksman
texlab
imagemagick
harper
]);
nix = maybe "nix" (with pkgs; [
alejandra
nix-doc
nixd
]);
optional = maybe "optional" (with pkgs; [
bat
broot
devenv
dust
fd
fzf
gawk
gh
git
hunspell
hunspellDicts.de-at
hunspellDicts.en-us
ispell
jq
just
lazygit
man
ncdu
pigz
poppler
ripgrep
tokei
wget
yq
]);
python = maybe "python" (let
python_packages_fn =
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; [
python_with_packages
ruff
basedpyright
uv
]);
r = maybe "r" [
(pkgs.rpkgs.rWrapper.override { packages = rWrapperPackages; })
pkgs.rpkgs.radianWrapper
quartoPkg
pkgs.air-formatter
pkgs.yaml-language-server
];
# cats without packages get empty lists
general = [ ];
gitPlugins = [ ];
treesitterParsers = [ ];
utils = [ ];
};
}

View file

@ -13,8 +13,8 @@
Set a category to `false` to skip its dependency/plugin specs. Set a category to `false` to skip its dependency/plugin specs.
Available categories: Available categories:
- always: always-on packages (not gated by a toggle)
- clickhouse: Clickhouse client and tools - clickhouse: Clickhouse client and tools
- customPlugins: local plugin specs
- external: external tools and integrations - external: external tools and integrations
- general: core Neovim plugins/features - general: core Neovim plugins/features
- gitPlugins: git-related plugins - gitPlugins: git-related plugins
@ -25,24 +25,26 @@
- optional: optional tools and utilities - optional: optional tools and utilities
- python: Python tooling and plugins - python: Python tooling and plugins
- r: R tooling and plugins - r: R tooling and plugins
- test: test-only tooling (disabled by default)
- treesitterParsers: Treesitter parsers - treesitterParsers: Treesitter parsers
- utils: general utilities - utils: general utilities
''; '';
}; };
config.cats = { config.cats = {
always = lib.mkDefault true;
clickhouse = lib.mkDefault false; clickhouse = lib.mkDefault false;
customPlugins = lib.mkDefault true;
external = lib.mkDefault true; external = lib.mkDefault true;
general = lib.mkDefault true; general = lib.mkDefault true;
gitPlugins = lib.mkDefault true; gitPlugins = lib.mkDefault true;
julia = lib.mkDefault false; julia = lib.mkDefault false;
lua = lib.mkDefault true; lua = lib.mkDefault true;
markdown = lib.mkDefault true; markdown = lib.mkDefault false;
nix = lib.mkDefault true; nix = lib.mkDefault true;
optional = lib.mkDefault false; optional = lib.mkDefault false;
python = lib.mkDefault false; python = lib.mkDefault false;
r = lib.mkDefault true; r = lib.mkDefault false;
test = lib.mkDefault false;
treesitterParsers = lib.mkDefault true; treesitterParsers = lib.mkDefault true;
utils = lib.mkDefault true; utils = lib.mkDefault true;
}; };

View file

@ -14,6 +14,10 @@
# Enable RC wrapping (allows neovim to find the config) # Enable RC wrapping (allows neovim to find the config)
config.settings.wrapRc = lib.mkDefault true; config.settings.wrapRc = lib.mkDefault true;
# Lua packages available to neovim (for :lua require())
config.settings.nvim_lua_env = lp:
lib.optionals (config.cats.general or false) [ lp.tiktoken_core ];
# Binary name for the wrapper # Binary name for the wrapper
config.binName = lib.mkDefault "vv"; config.binName = lib.mkDefault "vv";
@ -21,17 +25,11 @@
config.settings.block_normal_config = true; config.settings.block_normal_config = true;
# Don't symlink the config (we wrap it instead) # Don't symlink the config (we wrap it instead)
config.settings.dont_link = lib.mkDefault false; config.settings.dont_link = false;
# Create additional aliases for the binary # Create additional aliases for the binary
config.settings.aliases = lib.mkDefault [ "vvim" ]; config.settings.aliases = [ "vvim" ];
# Enable wrapper handling of spec runtimeDeps (template pattern). # Enable wrapper handling of spec runtimeDeps (template pattern).
config.settings.autowrapRuntimeDeps = true; config.settings.autowrapRuntimeDeps = true;
# The wrapper library currently emits runtime PATH additions via `suffixVar`,
# which lets host-level tools in `/usr/local/bin` win inside `nix run`.
# Mirror those additions into `prefixVar` so wrapped Neovim resolves the
# Nix-provided toolchain first while preserving existing wrapper behavior.
config.prefixVar = lib.mkAfter config.suffixVar;
} }

View file

@ -11,11 +11,12 @@
UV_PYTHON_DOWNLOADS = "never"; UV_PYTHON_DOWNLOADS = "never";
UV_PYTHON = pkgs.python.interpreter; UV_PYTHON = pkgs.python.interpreter;
}) })
# 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.
]; ];
# Environment variables with defaults (can be overridden by user) # Environment variables with defaults (can be overridden by user)
config.envDefault = lib.mkMerge [ ]; config.envDefault = lib.mkMerge [
(lib.mkIf (config.cats.r or false) {
R_LIBS_USER = "./.Rlibs";
})
];
} }

View file

@ -4,10 +4,6 @@
lib, lib,
... ...
}: }:
let
rPackages = (pkgs.baseRPackages or [ ]) ++ config.settings.lang_packages.r;
rWrapperPkg = pkgs.rpkgs.rWrapper.override { packages = rPackages ++ [pkgs.nvimcom]; };
in
{ {
config.hosts = lib.mkMerge [ config.hosts = lib.mkMerge [
{ {
@ -19,11 +15,21 @@ in
nvim-host.enable = true; nvim-host.enable = true;
nvim-host.package = "${pkgs.neovide}/bin/neovide"; nvim-host.package = "${pkgs.neovide}/bin/neovide";
nvim-host.argv0 = "neovide"; nvim-host.argv0 = "neovide";
nvim-host.flags."--neovim-bin" = "${builtins.placeholder "out"}/bin/${config.binName}"; nvim-host.flags."--neovim-bin" = "${placeholder "out"}/bin/${config.binName}";
}; };
m = {
nvim-host.enable = false;
nvim-host.package = "${pkgs.uv}/bin/uv";
nvim-host.argv0 = "uv";
nvim-host.addFlag = [
"run"
"marimo"
"edit"
];
};
} }
(lib.mkIf (config.cats.julia or false) { (lib.mkIf (config.cats.julia or true) {
jl = { jl = {
nvim-host.enable = true; nvim-host.enable = true;
nvim-host.package = "${pkgs.julia-bin}/bin/julia"; nvim-host.package = "${pkgs.julia-bin}/bin/julia";
@ -33,13 +39,13 @@ in
]; ];
}; };
}) })
(lib.mkIf (config.cats.python or false) { (lib.mkIf (config.cats.python or true) {
python3.nvim-host.enable = true; python3.nvim-host.enable = true;
}) })
(lib.mkIf (config.cats.r or false) { (lib.mkIf (config.cats.r or true) {
r = { r = {
nvim-host.enable = true; nvim-host.enable = true;
nvim-host.package = "${rWrapperPkg}/bin/R"; nvim-host.package = "${pkgs.rWrapper}/bin/R";
nvim-host.argv0 = "R"; nvim-host.argv0 = "R";
nvim-host.addFlag = [ nvim-host.addFlag = [
"--no-save" "--no-save"

View file

@ -1,6 +1,5 @@
{ {
config, config,
pkgs,
lib, lib,
... ...
}: }:
@ -27,33 +26,14 @@
}; };
default = { }; default = { };
description = '' description = ''
Language-specific package defaults and downstream overrides appended to each Language-specific package overrides appended to each language spec's extraPackages.
language spec's runtime packages. Intended for flake.nix overrides via wrapper.config.wrap.
''; '';
}; };
config.settings.lang_packages = { config.settings.lang_packages = {
python = lib.mkDefault (with pkgs.python3Packages; [ python = lib.mkDefault [ ];
duckdb r = lib.mkDefault [ ];
polars julia = lib.mkDefault [ ];
]);
r = lib.mkDefault (
(with pkgs.rpkgs.rPackages; [
arrow
broom
data_table
janitor
styler
# vscDebugger is not on CRAN/Bioconductor, so it is not available in
# pkgs.rpkgs.rPackages. Install it manually in your R library if you
# want to use the nvim-dap R adapter (see plugin/26_dap.lua).
# vscDebugger
lintr
])
);
julia = lib.mkDefault [
"DataFramesMeta"
"QuackIO"
];
}; };
} }

View file

@ -0,0 +1,35 @@
{
config,
lib,
...
}:
let
collect_runtime_packages = runtime_deps_type:
config.specCollect
(acc: spec:
let
is_enabled = if spec ? enable then spec.enable else true;
has_runtime_deps = (spec.runtimeDeps or false) == runtime_deps_type;
packages = spec.extraPackages or [ ];
in
acc ++ lib.optionals (is_enabled && has_runtime_deps) packages
)
[ ];
prefix_packages = collect_runtime_packages "prefix";
suffix_packages = collect_runtime_packages "suffix";
to_path_specs = packages: [
{
data = [
"PATH"
":"
"${lib.makeBinPath packages}"
];
}
];
in
{
config.prefixVar = lib.optionals (prefix_packages != [ ]) (to_path_specs prefix_packages);
config.suffixVar = lib.optionals (suffix_packages != [ ]) (to_path_specs suffix_packages);
}

View file

@ -0,0 +1,51 @@
{ config, lib, ... }:
{
# This module implements category-based enabling of specs.
# It runs early (order 200) so other specMaps can see the enable flags.
#
# How it works:
# 1. For each spec, extract its name (removing -lazy suffix if present)
# 2. Check if there's a corresponding cats.<name> toggle
# 3. Set spec.value.enable based on the cats toggle (default: true)
# 4. This allows specs to be conditionally included based on config.cats settings
#
# Example: If config.cats.python = false, then specs.python.enable = false
config.specMaps = lib.mkOrder 200 [
{
name = "CATS_ENABLE";
data =
list:
map (
v:
if v.type == "spec" || v.type == "parent" then
let
# Extract spec name, handling lazy specs (remove -lazy suffix)
specName =
if v.name == null then
null
else if lib.hasSuffix "-lazy" v.name then
lib.removeSuffix "-lazy" v.name
else
v.name;
# Check if this spec has a corresponding cat toggle
catEnabled =
if specName != null && builtins.hasAttr specName config.cats then
config.cats.${specName}
else
true; # Default to enabled if no cat toggle exists
in
v
// {
value = v.value // {
# Use explicit enable if set, otherwise use cat toggle
enable = if v.value ? enable then v.value.enable else catEnabled;
};
}
else
v
) list;
}
];
}

View file

@ -5,78 +5,184 @@
wlib, wlib,
... ...
}: { }: {
# ============================================================================
# SPEC MODULE DEFAULTS
# ============================================================================
# Define default options available to all specs
config.specMods = {parentSpec ? null, ...}: { config.specMods = {parentSpec ? null, ...}: {
options.runtimePkgs = lib.mkOption { options.extraPackages = lib.mkOption {
type = lib.types.listOf wlib.types.stringable; type = lib.types.listOf wlib.types.stringable;
default = []; default = [];
description = "a runtimePkgs spec field to put packages to suffix to the PATH"; description = "a extraPackages spec field to put packages to suffix to the PATH";
}; };
}; };
config.specs.always = lib.mkIf (config.cats.always or false) { # ============================================================================
data = lib.mkDefault null; # EXTERNAL TOOLS SPEC
runtimeDeps = "prefix"; # ============================================================================
runtimePkgs = config.catPkgs.always; # Core system tools and utilities
};
config.specs.external = lib.mkIf (config.cats.external or false) { config.specs.external = {
data = lib.mkDefault null; data = lib.mkDefault null;
before = ["INIT_MAIN"]; before = ["INIT_MAIN"];
config = '' config = ''
vim.o.shell = "${pkgs.zsh}/bin/zsh" vim.o.shell = "${pkgs.zsh}/bin/zsh"
''; '';
runtimeDeps = "prefix"; runtimeDeps = "prefix";
runtimePkgs = config.catPkgs.external; extraPackages = with pkgs; [
perl
ruby
shfmt
sqlfluff
tree-sitter
];
}; };
config.specs.optional = lib.mkIf (config.cats.optional or false) { # ============================================================================
# OPTIONAL TOOLS SPEC
# ============================================================================
config.specs.optional = lib.mkIf (config.cats.optional or true) {
data = lib.mkDefault null; data = lib.mkDefault null;
runtimeDeps = "prefix"; runtimeDeps = "prefix";
before = ["INIT_MAIN"]; before = ["INIT_MAIN"];
runtimePkgs = config.catPkgs.optional; extraPackages = with pkgs; [
bat
broot
devenv
dust
fd
fzf
gawk
gh
git
hunspell
hunspellDicts.de-at
hunspellDicts.en-us
ispell
jq
just
lazygit
man
ncdu
pigz
poppler
ripgrep
tokei
wget
yq
zathura
];
}; };
config.specs.markdown = lib.mkIf (config.cats.markdown or false) { # ============================================================================
# MARKDOWN SPEC
# ============================================================================
config.specs.markdown = lib.mkIf (config.cats.markdown or true) {
data = lib.mkDefault null; data = lib.mkDefault null;
runtimeDeps = "prefix"; runtimeDeps = "prefix";
runtimePkgs = config.catPkgs.markdown; extraPackages = with pkgs; [
python313Packages.pylatexenc
quarto
zk
];
}; };
config.specs.nix = lib.mkIf (config.cats.nix or false) { # ============================================================================
# NIX SPEC
# ============================================================================
config.specs.nix = lib.mkIf (config.cats.nix or true) {
data = lib.mkDefault null; data = lib.mkDefault null;
runtimeDeps = "prefix"; runtimeDeps = "prefix";
runtimePkgs = config.catPkgs.nix; extraPackages = with pkgs; [
alejandra
nix-doc
nixd
];
}; };
config.specs.lua = lib.mkIf (config.cats.lua or false) { # ============================================================================
# LUA SPEC
# ============================================================================
config.specs.lua = lib.mkIf (config.cats.lua or true) {
data = lib.mkDefault null; data = lib.mkDefault null;
runtimeDeps = "prefix"; runtimeDeps = "prefix";
runtimePkgs = config.catPkgs.lua; extraPackages = with pkgs; [
lua-language-server
];
}; };
config.specs.python = lib.mkIf (config.cats.python or false) { # ============================================================================
# PYTHON SPEC
# ============================================================================
config.specs.python = lib.mkIf (config.cats.python or true) {
data = lib.mkDefault null; data = lib.mkDefault null;
runtimeDeps = "prefix"; runtimeDeps = "prefix";
runtimePkgs = config.catPkgs.python; extraPackages = let
python_packages_fn =
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; [
python_with_packages
nodejs
ruff
basedpyright
uv
];
}; };
config.specs.r = lib.mkIf (config.cats.r or false) { # ============================================================================
# R SPEC
# ============================================================================
config.specs.r = lib.mkIf (config.cats.r or true) {
data = lib.mkDefault null; data = lib.mkDefault null;
runtimeDeps = "prefix"; runtimeDeps = "prefix";
runtimePkgs = config.catPkgs.r; extraPackages = let
r_packages = (pkgs.baseRPackages or []) ++ config.settings.lang_packages.r;
in
with pkgs; [
(rWrapper.override {packages = r_packages;})
radianWrapper
(quarto.override {extraRPackages = r_packages;})
air-formatter
yaml-language-server
updateR
];
}; };
config.specs.julia = lib.mkIf (config.cats.julia or false) { # ============================================================================
# JULIA SPEC
# ============================================================================
config.specs.julia = lib.mkIf (config.cats.julia or true) {
data = lib.mkDefault null; data = lib.mkDefault null;
runtimeDeps = "prefix"; runtimeDeps = "prefix";
runtimePkgs = config.catPkgs.julia; extraPackages = let
julia_with_packages =
pkgs.julia-bin.withPackages config.settings.lang_packages.julia;
in [julia_with_packages];
}; };
config.specs.clickhouse = lib.mkIf (config.cats.clickhouse or false) { # ============================================================================
# CLICKHOUSE SPEC
# ============================================================================
config.specs.clickhouse = lib.mkIf (config.cats.clickhouse or true) {
data = lib.mkDefault null; data = lib.mkDefault null;
runtimeDeps = "prefix"; runtimeDeps = "prefix";
runtimePkgs = config.catPkgs.clickhouse; extraPackages = with pkgs; [
clickhouse-lts
];
}; };
config.runtimePkgs = config.specCollect (acc: v: acc ++ (v.runtimePkgs or [])) []; config.extraPackages = config.specCollect (acc: v: acc ++ (v.extraPackages or [])) [];
} }

View file

@ -4,59 +4,14 @@
lib, lib,
... ...
}: }:
let {
parserList = [ config.specs.gitPlugins = {
"bash" data = [ ];
"bibtex"
"c"
"cpp"
"csv"
"diff"
"dockerfile"
"git_config"
"git_rebase"
"gitattributes"
"gitcommit"
"gitignore"
"html"
"javascript"
"json"
"julia"
"latex"
"lua"
"luadoc"
"make"
"markdown"
"markdown_inline"
"matlab"
"nix"
"python"
"query"
"r"
"rnoweb"
"regex"
"sql"
"toml"
"vim"
"vimdoc"
"xml"
"yaml"
"zig"
];
in {
options.settings.treesitter_parsers = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = parserList;
description = "Tree-sitter parser names to install when the treesitterParsers category is enabled.";
}; };
config.specs.gitPlugins = lib.mkIf (config.cats.gitPlugins or false) { config.specs.r = {
data = [];
};
config.specs.r = lib.mkIf (config.cats.r or false) {
data = with pkgs.vimPlugins; [ data = with pkgs.vimPlugins; [
pkgs.r-nvim config.nvim-lib.neovimPlugins.r
quarto-nvim quarto-nvim
{ {
data = otter-nvim; data = otter-nvim;
@ -65,23 +20,19 @@ in {
]; ];
}; };
config.specs.markdown-lazy = lib.mkIf (config.cats.markdown or false) { config.specs.markdown-lazy = {
lazy = true; lazy = true;
data = [ data = [
config.nvim-lib.neovimPlugins.cmp-pandoc-references config.nvim-lib.neovimPlugins.cmp-pandoc-references
pkgs.vimPlugins.image-nvim
]; ];
}; };
config.specs.general = lib.mkIf (config.cats.general or false) { config.specs.general = {
data = with pkgs.vimPlugins; [ data = with pkgs.vimPlugins; [
lze lze
lzextras lzextras
plenary-nvim plenary-nvim
vimtex
neogit neogit
config.nvim-lib.neovimPlugins.bloocky
config.nvim-lib.neovimPlugins.dooing
{ {
data = mini-nvim; data = mini-nvim;
pname = "mini.nvim"; pname = "mini.nvim";
@ -129,7 +80,7 @@ in {
]; ];
}; };
config.specs.lua = lib.mkIf (config.cats.lua or false) { config.specs.lua = {
data = with pkgs.vimPlugins; [ data = with pkgs.vimPlugins; [
luvit-meta luvit-meta
{ {
@ -139,7 +90,7 @@ in {
]; ];
}; };
config.specs.markdown = lib.mkIf (config.cats.markdown or false) { config.specs.markdown = {
data = with pkgs.vimPlugins; [ data = with pkgs.vimPlugins; [
quarto-nvim quarto-nvim
render-markdown-nvim render-markdown-nvim
@ -154,28 +105,59 @@ in {
]; ];
}; };
config.specs.utils = lib.mkIf (config.cats.utils or false) { config.specs.utils = {
data = with pkgs.vimPlugins; [ data = with pkgs.vimPlugins; [
blink-cmp blink-cmp
nvim-lspconfig nvim-lspconfig
nvim-treesitter-context nvim-treesitter-context
nvim-treesitter-textobjects nvim-treesitter-textobjects
{ {
data = pkgs.vimPlugins.nvim-treesitter; data = pkgs.codecompanion-nvim;
pname = "nvim-treesitter"; pname = "codecompanion";
} }
{ ];
data = pkgs.vimPlugins.opencode-nvim;
pname = "opencode.nvim";
}
] ++ builtins.attrValues pkgs.vimPlugins.nvim-treesitter.queries;
}; };
config.specs.treesitterParsers = lib.mkIf (config.cats.treesitterParsers or false) { config.specs.treesitterParsers = {
data = map (name: pkgs.vimPlugins.nvim-treesitter-parsers.${name}) config.settings.treesitter_parsers; data = with pkgs.vimPlugins.nvim-treesitter-parsers; [
bash
c
cpp
csv
diff
dockerfile
git_config
git_rebase
gitattributes
gitcommit
gitignore
html
javascript
json
julia
latex
lua
luadoc
make
markdown
markdown_inline
nix
python
query
r
rnoweb
regex
sql
toml
vim
vimdoc
xml
yaml
zig
];
}; };
config.specs.utils-lazy = lib.mkIf (config.cats.utils or false) { config.specs.utils-lazy = {
lazy = true; lazy = true;
data = with pkgs.vimPlugins; [ data = with pkgs.vimPlugins; [
blink-compat blink-compat
@ -184,26 +166,16 @@ in {
colorful-menu-nvim colorful-menu-nvim
conform-nvim conform-nvim
copilot-lua copilot-lua
nvim-dap
nvim-dap-ui
nvim-dap-virtual-text
nvim-lint nvim-lint
vim-slime vim-slime
]; ];
}; };
# Lazy-loaded plugins needed when the `r` cat is on. Kept separate from config.specs.gitPlugins-lazy = {
# `utils-lazy` so users with `r=true` and `utils=false` still get the
# nvim-dap R adapter and in-buffer image rendering for plots.
config.specs.r-lazy = lib.mkIf (config.cats.r or false) {
lazy = true; lazy = true;
data = with pkgs.vimPlugins; [ data = [ ];
nvim-dap
nvim-dap-ui
nvim-dap-virtual-text
image-nvim
];
};
config.specs.gitPlugins-lazy = lib.mkIf (config.cats.gitPlugins or false) {
lazy = true;
data = [];
}; };
} }

View file

@ -7,18 +7,29 @@ inputs:
... ...
}: }:
{ {
# ============================================================================
# IMPORTS
# ============================================================================
# Import the base neovim wrapper module and all configuration modules
imports = [ imports = [
wlib.wrapperModules.neovim wlib.wrapperModules.neovim
./module/settings/cat-packages.nix
./module/specs/deps.nix ./module/specs/deps.nix
./module/specs/plugins.nix ./module/specs/plugins.nix
./module/specs/cats-enable.nix
./module/settings/core.nix ./module/settings/core.nix
./module/settings/cats.nix ./module/settings/cats.nix
./module/settings/env.nix ./module/settings/env.nix
./module/settings/hosts.nix ./module/settings/hosts.nix
./module/settings/lang-packages.nix ./module/settings/lang-packages.nix
./module/settings/runtime-path.nix
]; ];
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
# Utilities for working with plugin inputs
options.nvim-lib.neovimPlugins = lib.mkOption { options.nvim-lib.neovimPlugins = lib.mkOption {
readOnly = true; readOnly = true;
type = lib.types.attrsOf wlib.types.stringable; type = lib.types.attrsOf wlib.types.stringable;
@ -47,6 +58,11 @@ inputs:
]; ];
}; };
# ============================================================================
# CONFIGURATION
# ============================================================================
# Pass cats configuration to neovim and expose metadata
config.settings.cats = config.cats; config.settings.cats = config.cats;
config.info.cats = config.cats; config.info.cats = config.cats;
config.info.nixCats_config_location = config.settings.config_directory; config.info.nixCats_config_location = config.settings.config_directory;

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

@ -3,33 +3,39 @@ let
lib = nixpkgs.lib; lib = nixpkgs.lib;
rOverlay = import ./r.nix {inherit inputs;}; rOverlay = import ./r.nix {inherit inputs;};
rNvimNixOverlay = inputs.r-nvim-nix.overlays.default; franOverlay = inputs.fran.overlays.default;
pythonOverlay = import ./python.nix {inherit inputs;}; pythonOverlay = import ./python.nix inputs;
pluginsOverlay = import ./plugins.nix {inherit inputs;}; pluginsOverlay = import ./plugins.nix inputs;
dependencyOverlays = [ dependencyOverlays = [
rOverlay rOverlay
rNvimNixOverlay
pythonOverlay pythonOverlay
pluginsOverlay pluginsOverlay
]; ];
dependencyOverlay = lib.composeManyExtensions dependencyOverlays; dependencyOverlay = lib.composeManyExtensions dependencyOverlays;
# franOverlay provides R-specific tooling (radianWrapper, air-formatter).
# It is scoped to rixpkgs (via overlays/r.nix) rather than the global
# package set, since it only applies to R package derivations.
franOverlay = inputs.fran.overlays.default;
in in
{ {
inherit inherit
rOverlay rOverlay
rNvimNixOverlay
franOverlay franOverlay
pythonOverlay pythonOverlay
pluginsOverlay pluginsOverlay
dependencyOverlays dependencyOverlays
dependencyOverlay; dependencyOverlay;
# Named exports for downstream composition.
default = dependencyOverlay; default = dependencyOverlay;
dependencies = dependencyOverlays; dependencies = dependencyOverlays;
overlays = {
inherit
rOverlay
franOverlay
pythonOverlay
pluginsOverlay
dependencyOverlays
dependencyOverlay;
default = dependencyOverlay;
dependencies = dependencyOverlays;
};
} }

View file

@ -1,6 +1,22 @@
{ ... }: { ... }:
final: prev: 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 { zk-nvim = prev.vimPlugins.zk-nvim.overrideAttrs {
nvimSkipModules = [ nvimSkipModules = [
"zk.pickers.fzf_lua" "zk.pickers.fzf_lua"

View file

@ -1,11 +1,52 @@
{inputs, ...}: final: prev: let # R packages overlay (rix)
#
# This overlay provides access to R packages from rstats-on-nix.
#
# rstats-on-nix maintains snapshots of CRAN packages built with Nix:
# - Provides reproducible R package versions
# - Ensures binary cache availability for faster builds
# - Maintained by the rstats-on-nix community
#
# Available attributes after applying this overlay:
# - pkgs.rpkgs: R packages from rstats-on-nix
# - pkgs.rpkgs.rPackages: All CRAN packages
# - pkgs.rpkgs.quarto: Quarto publishing system
# - pkgs.rpkgs.rWrapper: R with package management
# - pkgs.rWrapper: R wrapper with standard packages pre-configured
# - pkgs.quarto: Quarto with R integration and standard packages
#
# Custom R packages and tools (radianWrapper, air-formatter) come from
# the fran overlay which should be applied separately.
#
# To use specific R packages, reference them via:
# with pkgs.rpkgs.rPackages; [ package1 package2 ]
#
# Update the R snapshot date in flake.nix inputs section:
# rixpkgs.url = "github:rstats-on-nix/nixpkgs/YYYY-MM-DD"
{
inputs,
...
}: final: prev: let
# R packages from rstats-on-nix for the current system
rpkgs = import inputs.rixpkgs { rpkgs = import inputs.rixpkgs {
system = prev.stdenv.hostPlatform.system; system = prev.stdenv.hostPlatform.system;
overlays = [inputs.fran.overlays.default inputs.r-nvim-nix.overlays.default]; overlays = [inputs.fran.overlays.default];
}; }; # rixpkgs.legacyPackages.${prev.stdenv.hostPlatform.system};
# Standard R packages used by default in rWrapper and quarto
reqPkgs = with rpkgs.rPackages; [
# languageserver
];
in { in {
inherit rpkgs; inherit rpkgs;
baseRPackages = [rpkgs.nvimcom rpkgs.rPackages.btw]; baseRPackages = reqPkgs;
rWrapper = rpkgs.rWrapper.override {packages = [];};
quarto = rpkgs.quarto.override {extraRPackages = [];}; # R wrapper with standard packages
rWrapper = rpkgs.rWrapper.override {packages = reqPkgs;};
# Quarto with R integration
quarto = rpkgs.quarto.override {extraRPackages = reqPkgs;};
# Update helper for rix
updateR = import ../scripts/updater.nix {pkgs = final;};
} }

View file

@ -128,20 +128,27 @@ local diagnostic_opts = {
-- Don't update diagnostics when typing -- Don't update diagnostics when typing
update_in_insert = false, update_in_insert = false,
} }
later(function() later(function() vim.diagnostic.config(diagnostic_opts) end)
vim.diagnostic.config(diagnostic_opts)
-- Relax diagnostics for markdown (many false positives)
vim.api.nvim_create_autocmd('FileType', { -- Custom autocommands ========================================================
local augroup = vim.api.nvim_create_augroup('CustomSettings', {})
vim.api.nvim_create_autocmd('FileType', {
pattern = { 'markdown' }, pattern = { 'markdown' },
group = augroup,
callback = function() callback = function()
vim.diagnostic.config({ vim.diagnostic.config({
signs = { severity = { min = 'WARN', max = 'ERROR' } }, signs = {
virtual_text = { severity = { min = 'HINT', max = 'ERROR' }, current_line = false }, severity = { min = 'WARN', max = 'ERROR' }
},
virtual_text = {
current_line = false,
severity = { min = 'HINT', max = 'ERROR' }
}
}) })
end, end
}) })
end)
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
desc = "remove formatoptions", desc = "remove formatoptions",
@ -150,6 +157,17 @@ vim.api.nvim_create_autocmd("FileType", {
vim.b.minitrailspace_disable = true -- Don't highlight trailing space by default vim.b.minitrailspace_disable = true -- Don't highlight trailing space by default
end, end,
}) })
vim.api.nvim_create_autocmd('FileType', {
group = augroup,
callback = function()
-- Don't auto-wrap comments and don't insert comment leader after hitting 'o'
-- If don't do this on `FileType`, this keeps reappearing due to being set in
-- filetype plugins.
vim.cmd('setlocal formatoptions-=r formatoptions-=o')
end,
desc = [[Ensure proper 'formatoptions']],
})
-- Neovide ============================================== -- Neovide ==============================================
if vim.g.neovide then if vim.g.neovide then
vim.g.neovide_cursor_vfx_mode = "pixiedust" vim.g.neovide_cursor_vfx_mode = "pixiedust"
@ -158,7 +176,7 @@ if vim.g.neovide then
vim.g.neovide_cursor_short_animation_length = 0 vim.g.neovide_cursor_short_animation_length = 0
vim.g.neovide_font_hinting = 'none' vim.g.neovide_font_hinting = 'none'
vim.g.neovide_font_edging = 'subpixelantialias' vim.g.neovide_font_edging = 'subpixelantialias'
vim.o.guifont = 'JetBrainsMono Nerd Font:h14:#e-subpixelantialias:#h-none' vim.o.guifont = 'Iosevka Nerd Font,Symbols Nerd Font:h14:#e-subpixelantialias:#h-none'
vim.g.neovide_floating_corner_radius = 0.35 vim.g.neovide_floating_corner_radius = 0.35
vim.keymap.set("n", "<leader>nf", "<cmd>NeovideFullscreen<CR>", { desc = "Toggle Neovide Fullscreen" }) vim.keymap.set("n", "<leader>nf", "<cmd>NeovideFullscreen<CR>", { desc = "Toggle Neovide Fullscreen" })
end end

View file

@ -1,5 +1,3 @@
local Config = require('config')
-- Global Functions -- Global Functions
Config.new_scratch_buffer = function() vim.api.nvim_win_set_buf(0, vim.api.nvim_create_buf(true, true)) end Config.new_scratch_buffer = function() vim.api.nvim_win_set_buf(0, vim.api.nvim_create_buf(true, true)) end
@ -14,7 +12,7 @@ end
Config.log = {} Config.log = {}
Config.log_buf_id = Config.log_buf_id or nil Config.log_buf_id = Config.log_buf_id or nil
Config.start_hrtime = Config.start_hrtime or vim.uv.hrtime() Config.start_hrtime = Config.start_hrtime or vim.loop.hrtime()
Config.log_print = function() Config.log_print = function()
if Config.log_buf_id == nil or not vim.api.nvim_buf_is_valid(Config.log_buf_id) then if Config.log_buf_id == nil or not vim.api.nvim_buf_is_valid(Config.log_buf_id) then
@ -26,7 +24,7 @@ end
Config.log_clear = function() Config.log_clear = function()
Config.log = {} Config.log = {}
Config.start_hrtime = vim.uv.hrtime() Config.start_hrtime = vim.loop.hrtime()
vim.cmd('echo "Cleared log"') vim.cmd('echo "Cleared log"')
end end
@ -34,7 +32,7 @@ end
Config.execute_lua_line = function() Config.execute_lua_line = function()
local line = 'lua ' .. vim.api.nvim_get_current_line() local line = 'lua ' .. vim.api.nvim_get_current_line()
vim.api.nvim_command(line) vim.api.nvim_command(line)
vim.notify(line, vim.log.levels.INFO) print(line)
vim.api.nvim_input('<Down>') vim.api.nvim_input('<Down>')
end end

View file

@ -1,5 +1,3 @@
local Config = require('config')
local M = {} local M = {}
-- Helper function to normalize input to a list -- Helper function to normalize input to a list

View file

@ -1,9 +1,3 @@
local Config = require('config')
-- vim-slime target: use Neovim's built-in terminal.
-- Must be set before any slime send happens.
vim.g.slime_target = "neovim"
local M = {} local M = {}
-- Configuration -- Configuration
@ -18,7 +12,7 @@ local defaults = {
duckdb = "duckdb", duckdb = "duckdb",
julia = "julia", julia = "julia",
python = "ipython", python = "ipython",
shell = "echo 'Hello " .. (vim.env.USER or "user") .. "!'", shell = "echo 'Hello " .. vim.env.USER .. "!'",
} }
-- Registry of terminal commands -- Registry of terminal commands
@ -40,10 +34,6 @@ function M.split_and_open_terminal()
-- Set buffer-local variables for vim-slime -- Set buffer-local variables for vim-slime
local job_id = vim.b[term_buf].terminal_job_id local job_id = vim.b[term_buf].terminal_job_id
if not job_id then
vim.notify("Terminal job id not available", vim.log.levels.WARN)
return term_buf
end
vim.b[term_buf].slime_config = { jobid = job_id } vim.b[term_buf].slime_config = { jobid = job_id }
return M.opt_term return M.opt_term
@ -55,43 +45,24 @@ function M.open_in_terminal(cmd)
local current_window = vim.api.nvim_get_current_win() local current_window = vim.api.nvim_get_current_win()
local code_buf = vim.api.nvim_get_current_buf() local code_buf = vim.api.nvim_get_current_buf()
if not vim.api.nvim_buf_is_valid(code_buf) then
vim.notify("Code buffer is not valid", vim.log.levels.ERROR)
return
end
-- Open terminal and get buffer -- Open terminal and get buffer
local term_buf = M.split_and_open_terminal() local term_buf = M.split_and_open_terminal()
if not term_buf or not vim.api.nvim_buf_is_valid(term_buf) then
vim.notify("Failed to open terminal buffer", vim.log.levels.ERROR)
return
end
-- Send command if provided -- Send command if provided
local job_id = vim.b[term_buf].terminal_job_id
if command ~= "" then if command ~= "" then
if not job_id then -- We can use standard slime sending if needed, or direct chan_send for initialization
vim.notify("Terminal job not ready, cannot send command", vim.log.levels.WARN) local job_id = vim.b[term_buf].terminal_job_id
else if job_id then
local ok, err = pcall(vim.api.nvim_chan_send, job_id, command .. "\r") vim.api.nvim_chan_send(job_id, command .. "\r")
if not ok then
vim.notify("Failed to send command to terminal: " .. tostring(err), vim.log.levels.ERROR)
end
end end
end end
-- Configure slime for the ORIGINAL code buffer to point to this new terminal -- Configure slime for the ORIGINAL code buffer to point to this new terminal
-- This makes "Send to Terminal" work immediately -- This makes "Send to Terminal" work immediately
if job_id then local slime_config = { jobid = vim.b[term_buf].terminal_job_id }
local slime_config = { jobid = job_id }
-- Fix: Set the variable on the captured code buffer, not the current (terminal) buffer -- Fix: Set the variable on the captured code buffer, not the current (terminal) buffer
local ok, err = pcall(vim.api.nvim_buf_set_var, code_buf, "slime_config", slime_config) vim.api.nvim_buf_set_var(code_buf, "slime_config", slime_config)
if not ok then
vim.notify("Failed to set slime_config on code buffer: " .. tostring(err), vim.log.levels.ERROR)
end
end
-- Switch back to code buffer -- Switch back to code buffer
vim.api.nvim_set_current_win(current_window) vim.api.nvim_set_current_win(current_window)

View file

@ -1,5 +1,3 @@
local Config = require('config')
local M = {} local M = {}
-- Default parsers list moved from startup config -- Default parsers list moved from startup config
@ -95,12 +93,12 @@ end
function M.get_type() function M.get_type()
local cur_node = smart_send.get_current_node() local cur_node = smart_send.get_current_node()
if not cur_node then if not cur_node then
vim.notify("No Tree-sitter node under cursor", vim.log.levels.WARN) print("Not a node")
return nil return nil
end end
local node_type = cur_node:type() local node_type = cur_node:type()
vim.notify("Node type: " .. node_type, vim.log.levels.INFO) print("Node type: " .. node_type)
return node_type return node_type
end end
@ -121,17 +119,11 @@ function M.setup_keybindings(global_nodes)
vim.keymap.set('n', '<localleader>a', function() smart_send.send_repl(current_global_nodes) end, vim.keymap.set('n', '<localleader>a', function() smart_send.send_repl(current_global_nodes) end,
{ noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) { noremap = true, silent = true, desc = "Send node to REPL", buffer = true })
-- Both `<CR>` and `<S-CR>` were removed from `M.setup_keybindings`. They vim.keymap.set({ 'n', 'i' }, '<S-CR>', function() smart_send.send_repl(current_global_nodes) end,
-- were hard, implicit overrides that clobbered Vim/filetype defaults and { noremap = true, silent = true, desc = "Send node to REPL", buffer = true })
-- the user's snippet + insert-mode workflows (see C2 + H5 in the PR
-- review). To opt back in for a specific filetype, override per-buffer vim.keymap.set('n', '<CR>', function() smart_send.send_repl(current_global_nodes) end,
-- from a `ftplugin/<lang>.lua`: { noremap = true, silent = true, desc = "Send node to REPL", buffer = true })
--
-- -- e.g. ftplugin/r.lua or ftplugin/quarto.lua
-- vim.keymap.set('n', '<CR>', function()
-- require('config').treesitter_helpers.setup_keybindings(global_nodes)
-- require('nix_smart_send').send_repl(global_nodes)
-- end, { buffer = true, desc = 'Send node to REPL' })
vim.keymap.set('n', '<localleader>n', vim.keymap.set('n', '<localleader>n',
function() current_global_nodes = M.add_global_node(current_global_nodes) end, function() current_global_nodes = M.add_global_node(current_global_nodes) end,
@ -142,8 +134,8 @@ function M.setup_keybindings(global_nodes)
{ noremap = true, silent = true, desc = "Remove node under cursor from globals", buffer = true }) { noremap = true, silent = true, desc = "Remove node under cursor from globals", buffer = true })
vim.keymap.set('n', '<localleader>o', function() vim.keymap.set('n', '<localleader>o', function()
local pout = table.concat(global_nodes, ', ') pout = table.concat(global_nodes, ', ') .. ""
vim.notify("global_nodes: " .. pout, vim.log.levels.INFO) print(pout)
end, { noremap = true, silent = true, desc = "Print globals", buffer = true }) end, { noremap = true, silent = true, desc = "Print globals", buffer = true })
vim.keymap.set('n', '<localleader>p', function() M.get_type() end, vim.keymap.set('n', '<localleader>p', function() M.get_type() end,
@ -152,46 +144,4 @@ end
Config.treesitter_helpers = M Config.treesitter_helpers = M
-- Tree-sitter text objects: functions, calls, and assignments are especially
-- useful when editing R/tidyverse pipelines (e.g. `df |> mutate(...)`).
local ts_ok, treesitter = pcall(require, "nvim-treesitter.configs")
if ts_ok then
treesitter.setup({
textobjects = {
select = {
enable = true,
lookahead = true,
keymaps = {
["af"] = "@function.outer",
["if"] = "@function.inner",
["ac"] = "@call.outer",
["ic"] = "@call.inner",
["aa"] = "@assignment.outer",
["ia"] = "@assignment.inner",
},
selection_modes = {
["@function.outer"] = "V",
["@function.inner"] = "V",
["@call.outer"] = "v",
["@call.inner"] = "v",
["@assignment.outer"] = "v",
["@assignment.inner"] = "v",
},
},
move = {
enable = true,
set_jumps = true,
goto_next_start = {
["]f"] = "@function.outer",
["]c"] = "@call.outer",
},
goto_previous_start = {
["[f"] = "@function.outer",
["[c"] = "@call.outer",
},
},
},
})
end
return M return M

View file

@ -1,11 +1,318 @@
local Config = require('config') -- Basic mappings =============================================================
-- NOTE: Most basic mappings come from 'mini.basics'
-- Shorter version of the most frequent way of going outside of terminal window
vim.keymap.set('t', '<C-h>', [[<C-\><C-N><C-w>h]])
-- Select all
-- vim.keymap.set({ "n", "v", "x" }, "<C-a>", "gg3vG$", { noremap = true, silent = true, desc = "Select all" })
-- Escape deletes highlights
vim.keymap.set("n", "<Esc>", "<cmd>nohlsearch<CR>")
-- Paste before/after linewise
local cmd = vim.fn.has('nvim-0.12') == 1 and 'iput' or 'put'
vim.keymap.set({ 'n', 'x' }, '[p', '<Cmd>exe "' .. cmd .. '! " . v:register<CR>', { desc = 'Paste Above' })
vim.keymap.set({ 'n', 'x' }, ']p', '<Cmd>exe "' .. cmd .. ' " . v:register<CR>', { desc = 'Paste Below' })
-- Domain-specific keymap modules. Core must load first because it defines the vim.keymap.set({ "n", "v", "x" }, "<leader>p", '"+p', { noremap = true, silent = true, desc = "Paste from clipboard" })
-- leader clue groups used by the mini.clue setup in the startup plugins. vim.keymap.set({ "n", "v", "x" }, "<leader>y", '"+y', { noremap = true, silent = true, desc = "Copy toclipboard" })
require('keymap.core') -- Leader mappings ============================================================
require('keymap.leader') -- stylua: ignore start
require('keymap.terminal')
-- `_G.Config` is set by `init.lua:2` once at start-up; no need to re-export -- Create global tables with information about clue groups in certain modes
-- here. The single source of truth is `init.lua` so future refactors don't -- Structure of tables is taken to be compatible with 'mini.clue'.
-- have to chase which file currently publishes the alias. _G.Config.leader_group_clues = {
{ mode = 'n', keys = '<Leader>a', desc = '+AI' },
{ mode = 'n', keys = '<Leader>b', desc = '+Buffer' },
{ mode = 'n', keys = '<Leader>e', desc = '+Explore' },
{ mode = 'n', keys = '<Leader>f', desc = '+Find' },
{ mode = 'n', keys = '<Leader>fl', desc = '+LSP' },
{ mode = 'n', keys = '<Leader>fa', desc = '+Git' },
{ mode = 'n', keys = '<Leader>g', desc = '+Git' },
{ mode = 'n', keys = '<Leader>l', desc = '+LSP' },
{ mode = 'n', keys = '<Leader>L', desc = '+Lua/Log' },
{ mode = 'n', keys = '<Leader>o', desc = '+Other' },
{ mode = 'n', keys = '<Leader>r', desc = '+R' },
{ mode = 'n', keys = '<Leader>t', desc = '+Terminal' },
{ mode = 'n', keys = '<Leader>u', desc = '+UI' },
{ mode = 'n', keys = '<Leader>v', desc = '+Visits' },
{ mode = 'n', keys = '<Leader>w', desc = '+Windows' },
{ mode = 'x', keys = '<Leader>l', desc = '+LSP' },
{ mode = 'x', keys = '<Leader>r', desc = '+R' },
{ mode = 'n', keys = '<Leader>z', desc = '+ZK' },
{ mode = 'n', keys = '<Leader>zr', desc = '+Reviews' },
{ mode = 'x', keys = '<leader>a', desc = '+AI' },
}
-- Create `<Leader>` mappings
local nmap_leader = function(suffix, rhs, desc, opts)
opts = opts or {}
opts.desc = desc
vim.keymap.set('n', '<Leader>' .. suffix, rhs, opts)
end
local xmap_leader = function(suffix, rhs, desc, opts)
opts = opts or {}
opts.desc = desc
vim.keymap.set('x', '<Leader>' .. suffix, rhs, opts)
end
-- Other mappings
local nmap_lsp = function(keys, func, desc)
if desc then
desc = desc .. "(LSP)"
end
vim.keymap.set("n", keys, func, { desc = desc })
end
-- Switch buffers
nmap_leader('<Tab>', '<Cmd>bnext<CR>', 'Next buffer')
nmap_leader('<S-Tab>', '<Cmd>bprev<CR>', 'Prev buffer')
-- a is for 'AI'
nmap_leader("ac", "<cmd>CodeCompanionChat Toggle<CR>", "Chat Toggle")
-- nmap_leader("ae", "<cmd>CodeCompanion /explain<CR>", "Explain Code")
-- nmap_leader("af", "<cmd>CodeCompanion /fix<CR>", "Fix Code")
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("ax", "<cmd>CodeCompanion /fixer<CR>", "Code Fixer")
nmap_leader("ax", "<cmd>CodeCompanion /fixer<CR>", "Code Fixer")
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 Fixer")
xmap_leader("ap", "<cmd>CodeCompanion /expert<CR>", "Code Fixer")
xmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
-- b is for 'buffer'
nmap_leader('bb', '<Cmd>b#<CR>', 'Alternate')
nmap_leader('bd', '<Cmd>lua MiniBufremove.delete()<CR>', 'Delete')
nmap_leader('bD', '<Cmd>lua MiniBufremove.delete(0, true)<CR>', 'Delete!')
nmap_leader('bs', '<Cmd>lua Config.new_scratch_buffer()<CR>', 'Scratch')
nmap_leader('bw', '<Cmd>lua MiniBufremove.wipeout()<CR>', 'Wipeout')
nmap_leader('bW', '<Cmd>lua MiniBufremove.wipeout(0, true)<CR>', 'Wipeout!')
nmap_leader('bq', '<Cmd>qall<CR>', 'Quit all')
-- e is for 'explore' and 'edit'
nmap_leader('ed', '<Cmd>lua MiniFiles.open()<CR>', 'Directory')
nmap_leader('ef', '<Cmd>lua Config.try_opendir()<CR>', 'File directory')
nmap_leader('es', '<Cmd>lua MiniSessions.select()<CR>', 'Sessions')
nmap_leader('eq', '<Cmd>lua Config.toggle_quickfix()<CR>', 'Quickfix')
nmap_leader('ez', '<Cmd>lua MiniFiles.open(os.getenv("ZK_NOTEBOOK_DIR"))<CR>', 'Notes directory')
-- f is for 'fuzzy find'
nmap_leader('f/', '<Cmd>Pick history scope="/"<CR>', '"/" history')
nmap_leader('f:', '<Cmd>Pick history scope=":"<CR>', '":" history')
nmap_leader('f,', '<Cmd>Pick visit_labels<CR>', 'Visit labels')
nmap_leader('faa', '<Cmd>Pick git_hunks scope="staged"<CR>', 'Added hunks (all)')
nmap_leader('faA', '<Cmd>Pick git_hunks path="%" scope="staged"<CR>', 'Added hunks (current)')
nmap_leader('fb', '<Cmd>Pick buffers<CR>', 'Buffers')
nmap_leader(',', '<Cmd>Pick buffers<CR>', 'Buffers')
nmap_leader('fac', '<Cmd>Pick git_commits<CR>', 'Commits (all)')
nmap_leader('faC', '<Cmd>Pick git_commits path="%"<CR>', 'Commits (current)')
nmap_leader('fd', '<Cmd>Pick diagnostic scope="all"<CR>', 'Diagnostic workspace')
nmap_leader('fD', '<Cmd>Pick diagnostic scope="current"<CR>', 'Diagnostic buffer')
nmap_leader('ff', '<Cmd>Pick files<CR>', 'Files')
nmap_leader('fg', '<Cmd>Pick grep_live<CR>', 'Grep live')
nmap_leader('fG', '<Cmd>Pick grep pattern="<cword>"<CR>', 'Grep current word')
nmap_leader('fh', '<Cmd>Pick help<CR>', 'Help tags')
nmap_leader('fH', '<Cmd>Pick hl_groups<CR>', 'Highlight groups')
nmap_leader('fj', '<Cmd>Pick buf_lines scope="all"<CR>', 'Lines (all)')
nmap_leader('fJ', '<Cmd>Pick buf_lines scope="current"<CR>', 'Lines (current)')
nmap_leader('fam', '<Cmd>Pick git_hunks<CR>', 'Modified hunks (all)')
nmap_leader('faM', '<Cmd>Pick git_hunks path="%"<CR>', 'Modified hunks (current)')
nmap_leader('fm', '<Cmd>Pick marks<CR>', 'Marks')
nmap_leader('fn', '<cmd>ZkNotes<CR>', "Notes")
nmap_leader('fk', '<Cmd>Pick keymaps<CR>', 'Keymaps')
nmap_leader('fR', '<Cmd>Pick resume<CR>', 'Resume')
nmap_leader('fp', '<Cmd>Pick projects<CR>', 'Projects')
nmap_leader('fq', '<Cmd>Pick list scope="quickfix"<CR>', 'Quickfix')
nmap_leader('fr', '<Cmd>Pick lsp scope="references"<CR>', 'References (LSP)')
nmap_leader('flr', '<Cmd>Pick lsp scope="references"<CR>', 'References (LSP)')
nmap_leader('fS', '<Cmd>Pick lsp scope="workspace_symbol"<CR>', 'Symbols workspace (LSP)')
nmap_leader('flS', '<Cmd>Pick lsp scope="workspace_symbol"<CR>', 'Symbols workspace (LSP)')
nmap_leader('fs', '<Cmd>Pick lsp scope="document_symbol"<CR>', 'Symbols buffer (LSP)')
nmap_leader('fls', '<Cmd>Pick lsp scope="document_symbol"<CR>', 'Symbols buffer (LSP)')
nmap_leader('fld', '<Cmd>Pick lsp scope="definition"<CR>', 'Definition (LSP)')
nmap_leader('flD', '<Cmd>Pick lsp scope="declaration"<CR>', 'Declaration (LSP)')
nmap_leader('flt', '<Cmd>Pick lsp scope="type_definition"<CR>', 'Type Definition (LSP)')
nmap_leader('fv', '<Cmd>Pick visit_paths cwd=""<CR>', 'Visit paths (all)')
nmap_leader('fV', '<Cmd>Pick visit_paths<CR>', 'Visit paths (cwd)')
-- g is for git
local git_log_cmd = [[Git log --pretty=format:\%h\ \%as\ │\ \%s --topo-order]]
nmap_leader('gc', '<Cmd>Git commit<CR>', 'Commit')
nmap_leader('gC', '<Cmd>Git commit --amend<CR>', 'Commit amend')
nmap_leader('gd', '<Cmd>Git diff<CR>', 'Diff')
nmap_leader('gD', '<Cmd>Git diff -- %<CR>', 'Diff buffer')
nmap_leader('gg', '<Cmd>lua require("neogit").open()<CR>', 'Git tab')
nmap_leader('gl', '<Cmd>' .. git_log_cmd .. '<CR>', 'Log')
nmap_leader('gL', '<Cmd>' .. git_log_cmd .. ' --follow -- %<CR>', 'Log buffer')
nmap_leader('go', '<Cmd>lua MiniDiff.toggle_overlay()<CR>', 'Toggle overlay')
nmap_leader('gp', '<Cmd>Git pull<CR>', 'Pull')
nmap_leader('gP', '<Cmd>Git push<CR>', 'Push')
nmap_leader('gs', '<Cmd>lua MiniGit.show_at_cursor()<CR>', 'Show at cursor')
xmap_leader('gs', '<Cmd>lua MiniGit.show_at_cursor()<CR>', 'Show at selection')
-- j/k navigate quickfix
nmap_leader("j", '<cmd>cnext<CR>zz', "Quickfix next")
nmap_leader("k", '<cmd>cprev<CR>zz', "Quickfix prev")
-- l is for 'LSP' (Language Server Protocol)
vim.keymap.set({ 'n' }, 'grd', '<Cmd>lua vim.lsp.buf.definition()<CR>', { desc = 'Definition' })
vim.keymap.set({ 'n' }, 'grk', '<Cmd>lua vim.lsp.buf.hover()<CR>', { desc = 'Documentation' })
vim.keymap.set({ 'n' }, 'gre', '<Cmd>lua vim.diagnostic.open_float()<CR>', { desc = 'Diagnostics' })
nmap_lsp("K", '<Cmd>lua vim.lsp.buf.hover()<CR>', "Documentation")
local formatting_cmd = '<Cmd>lua require("conform").format({ lsp_fallback = true })<CR>'
nmap_leader('la', '<Cmd>lua vim.lsp.buf.code_action()<CR>', 'Actions')
nmap_leader('le', '<Cmd>lua vim.diagnostic.open_float()<CR>', 'Diagnostics popup')
nmap_leader('lf', formatting_cmd, 'Format')
nmap_leader('lk', '<Cmd>lua vim.lsp.buf.hover()<CR>', 'Documentation')
nmap_leader('li', '<Cmd>lua vim.lsp.buf.implementation()<CR>', 'Information')
-- use ]d and [d
--nmap_leader('lj', '<Cmd>lua vim.diagnostic.goto_next()<CR>', 'Next diagnostic')
--nmap_leader('lk', '<Cmd>lua vim.diagnostic.goto_prev()<CR>', 'Prev diagnostic')
nmap_leader('lR', '<Cmd>lua vim.lsp.buf.references()<CR>', 'References')
nmap_leader('lr', '<Cmd>lua vim.lsp.buf.rename()<CR>', 'Rename')
nmap_leader('ls', '<Cmd>lua vim.lsp.buf.definition()<CR>', 'Source definition')
xmap_leader('lf', formatting_cmd, 'Format selection')
-- L is for 'Lua'
nmap_leader('Lc', '<Cmd>lua Config.log_clear()<CR>', 'Clear log')
nmap_leader('LL', '<Cmd>luafile %<CR><Cmd>echo "Sourced lua"<CR>', 'Source buffer')
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 free
-- o is for 'other'
local trailspace_toggle_command = '<Cmd>lua vim.b.minitrailspace_disable = not vim.b.minitrailspace_disable<CR>'
nmap_leader('od', '<Cmd>Neogen<CR>', 'Document')
nmap_leader('oh', '<Cmd>normal gxiagxila<CR>', 'Move arg left')
nmap_leader('ol', '<Cmd>normal gxiagxina<CR>', 'Move arg right')
nmap_leader('or', '<Cmd>lua MiniMisc.resize_window()<CR>', 'Resize to default width')
nmap_leader('oS', '<Cmd>lua Config.insert_section()<CR>', 'Section insert')
nmap_leader('ot', '<Cmd>lua MiniTrailspace.trim()<CR>', 'Trim trailspace')
nmap_leader('oT', trailspace_toggle_command, 'Trailspace hl toggle')
nmap_leader('oz', '<Cmd>lua MiniMisc.zoom()<CR>', 'Zoom toggle')
nmap_leader('ow',
"<Cmd>lua MiniSessions.write(vim.fn.input('Session name: ', string.match(vim.fn.getcwd(), \"[^/]+$\") .. '-session.vim'))<CR>",
'Write session')
-- r is for 'R'
nmap_leader('rc', '<Cmd>RSend devtools::check()<CR>', 'Check')
nmap_leader('rC', '<Cmd>RSend devtools::test_coverage()<CR>', 'Coverage')
nmap_leader('rd', '<Cmd>RSend devtools::document()<CR>', 'Document')
nmap_leader('ri', '<Cmd>RSend devtools::install(keep_source=TRUE)<CR>', 'Install')
nmap_leader('rk', '<Cmd>RSend quarto::quarto_preview("%")<CR>', 'Knit file')
nmap_leader('rl', '<Cmd>RSend devtools::load_all()<CR>', 'Load all')
nmap_leader('rL', '<Cmd>RSend devtools::load_all(recompile=TRUE)<CR>', 'Load all recompile')
nmap_leader('rm', '<Cmd>RSend Rcpp::compileAttributes()<CR>', 'Run examples')
nmap_leader('rT', '<Cmd>RSend testthat::test_file("%")<CR>', 'Test file')
nmap_leader('rt', '<Cmd>RSend devtools::test()<CR>', 'Test')
-- - Copy to clipboard and make reprex (which itself is loaded to clipboard)
xmap_leader('rx', '"+y :RSend reprex::reprex()<CR>', 'Reprex selection')
-- s is for 'send' (Send text to neoterm buffer)
nmap_leader('s', '<Cmd>SlimeSendCurrentLine<CR>j', 'Send to terminal')
-- - In simple visual mode send text and move to the last character in
-- selection and move to the right. Otherwise (like in line or block visual
-- mode) send text and move one line down from bottom of selection.
xmap_leader('s', '<Plug>SlimeRegionSend<CR>', 'Send to terminal')
-- t is for 'terminal'
vim.keymap.set("t", "<Esc>", [[<C-\><C-n>]], { desc = "Exit terminal mode" })
vim.keymap.set("n", "<leader>tc", '<Cmd>lua Config.terminal.open_clickhouse_client()<CR>',
{ desc = "Open Clickhouse client" })
vim.keymap.set("n", "<leader>tl", '<Cmd>lua Config.terminal.open_clickhouse_local()<CR>',
{ desc = "Open Clickhouse local" })
vim.keymap.set("n", "<leader>tp", '<Cmd>lua Config.terminal.open_python()<CR>', { desc = "Open Python" })
vim.keymap.set("n", "<leader>tj", '<Cmd>lua Config.terminal.open_julia()<CR>', { desc = "Open Julia" })
vim.keymap.set("n", "<leader>td", '<Cmd>lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()<CR>',
{ desc = "Open DuckDB" })
vim.keymap.set("n", "<leader>tx", '<Cmd>lua Config.terminal.open_in_terminal()<CR>', { desc = "Terminal Command" })
vim.keymap.set("n", "<leader>tt", '<Cmd>lua Config.terminal.open_shell()<CR>', { desc = "Terminal" })
nmap_leader("tb", '<Cmd>lua Config.terminal.toggle_bracket()<CR>', "Toggle bracketed paste")
nmap_leader("up", '<Cmd>lua Config.terminal.toggle_bracket()<CR>', "Toggle bracketed paste")
-- u is for UI
nmap_leader('ut', '<Cmd>TSContext toggle<CR>', 'Toggle TScontext')
nmap_leader('ua', '<Cmd>Copilot toggle<CR>', 'Toggle AI completion')
-- v is for 'visits'
nmap_leader('vv', '<Cmd>lua MiniVisits.add_label("core")<CR>', 'Add "core" label')
nmap_leader('vV', '<Cmd>lua MiniVisits.remove_label("core")<CR>', 'Remove "core" label')
nmap_leader('vl', '<Cmd>lua MiniVisits.add_label()<CR>', 'Add label')
nmap_leader('vL', '<Cmd>lua MiniVisits.remove_label()<CR>', 'Remove label')
local map_pick_core = function(keys, cwd, desc)
local rhs = function()
local sort_latest = MiniVisits.gen_sort.default({ recency_weight = 1 })
MiniExtra.pickers.visit_paths({
cwd = cwd,
filter = 'core',
sort = sort_latest
}, { source = { name = desc } })
end
nmap_leader(keys, rhs, desc)
end
map_pick_core('vc', '', 'Core visits (all)')
map_pick_core('vC', nil, 'Core visits (cwd)')
-- w is for 'windows'
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("_", "<C-W>s", "Split Window Below", { remap = true })
nmap_leader("|", "<C-W>v", "Split Window Right", { remap = true })
nmap_leader("wd", "<C-W>c", "Delete Window", { remap = true })
nmap_leader("wo", "<C-W>o", "Delete Other Windows", { remap = true })
-- z is for 'ZettelKasten'
nmap_leader("zo", '<Cmd>ZkNotes<CR>', "Notes")
nmap_leader("zt", '<Cmd>ZkTags<cr>', "Tags")
nmap_leader(
"zrd",
'<Cmd>ZkNew { group = "dreviews" }<CR>',
"Daily Review"
)
nmap_leader(
"zrw",
'<Cmd>ZkNew { group = "wreviews" }<CR>',
"Weekly Review"
)
nmap_leader(
"zn",
'<Cmd>ZkNew { group = "inbox", title = vim.fn.input("Title: ") }<CR>',
"New"
)
nmap_leader(
"zp",
"<Cmd>ZkNew { group = 'permanent', title = vim.fn.input('Title: ') }<CR>",
"Permanent"
)
nmap_leader(
"zl",
"<Cmd>ZkNew { group = 'literature', title = vim.fn.input('Title: '), extra.author = vim.fn.input('Author: '), extra.year = vim.fn.input('Year: ') }<CR>",
"Literature"
)
nmap_leader(
"zd",
"<Cmd>ZkNew { group = 'dashboard', title = vim.fn.input('Title: ') }<CR>",
"Dashboard"
)
nmap_leader(
"zP",
"<Cmd>ZkNew { group = 'project', title = vim.fn.input('Title: ')}<CR>",
"Project"
)
-- stylua: ignore end

View file

@ -1,4 +1,3 @@
local Config = require('config')
local now = MiniDeps.now local now = MiniDeps.now
local later = MiniDeps.later local later = MiniDeps.later
local now_if_args = Config.now_if_args local now_if_args = Config.now_if_args
@ -191,7 +190,7 @@ end)
-- Treesitter -- Treesitter
now_if_args(function() 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 { require 'treesitter-context'.setup {
enable = true, enable = true,
@ -209,37 +208,112 @@ now_if_args(function()
on_attach = nil, -- (fun(buf: integer): boolean) return false to disable attaching on_attach = nil, -- (fun(buf: integer): boolean) return false to disable attaching
} }
local ts_filetypes = {
"c", "cpp", "lua", "nix", "python", "r", "markdown", "query", local ok_configs, configs = pcall(require, "nvim-treesitter.configs")
"vim", "vimdoc", "yaml", "json", "toml", "rust", "go",
"javascript", "typescript", "tsx", "html", "css", "sql", if ok_configs and configs.setup then
"julia", "rnoweb", "latex", "gitcommit", "gitignore", local opts = {
"git_config", "git_rebase", "diff", "dockerfile", highlight = { enable = true },
"make", "xml", "zig", "regex", "csv", "bash", indent = { enable = false },
"markdown_inline", "quarto", "rmd", textobjects = {
move = {
enable = true,
set_jumps = true,
goto_next_start = {
["]a"] = "@parameter.inner", -- fixed typo
["]f"] = "@function.outer",
["]o"] = "@loop.*",
["]s"] = { query = "@local.scope", desc = "Next scope" },
["]z"] = { query = "@fold", desc = "Next fold" },
},
goto_next_end = {
["]M"] = "@function.outer",
["]["] = "@class.outer",
},
goto_previous_start = {
["[a"] = "@parameter.inner",
["[f"] = "@function.outer",
["[o"] = "@loop.*",
["[s"] = { query = "@local.scope", query_group = "locals", desc = "Prev. scope" },
["[z"] = { query = "@fold", query_group = "folds", desc = "Prev. fold" },
},
goto_previous_end = {
["[M"] = "@function.outer",
["[]"] = "@class.outer",
},
goto_next = {
["]e"] = "@conditional.outer",
},
goto_previous = {
["[e"] = "@conditional.outer",
},
},
swap = {
enable = true,
swap_next = {
["<leader>x"] = "@parameter.inner",
},
swap_previous = {
["<leader>X"] = "@parameter.inner",
},
},
lsp_interop = {
enable = true,
border = "none",
floating_preview_opts = {},
peek_definition_code = {
["<leader>lm"] = "@function.outer",
["<leader>lM"] = "@class.outer",
},
},
},
} }
local function start_treesitter(buf, filetype)
local lang = vim.treesitter.language.get_lang(filetype) or filetype
if #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".*", false) == 0 then -- Manual parser check for non-Nix users
return if not Config.isNixCats then
local installed_check = function(lang)
return #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".*", false) == 0
end end
vim.treesitter.start(buf, lang) local to_install = vim.tbl_filter(installed_check, opts.ensure_installed)
vim.bo[buf].indentexpr = "v:lua.require('nvim-treesitter').indentexpr()" if #to_install > 0 then
require("nvim-treesitter").install(to_install)
end
end
-- Environment-specific Overrides
if not Config.isNixCats then
opts.auto_install = true
opts.ensure_installed = Config.treesitter_helpers.default_parsers
else
opts.auto_install = false
-- Nix handles installation, so ensure_installed is skipped/empty
end
configs.setup(opts)
return
end end
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
pattern = ts_filetypes, pattern = "*",
callback = function(ev) callback = function(args)
start_treesitter(ev.buf, vim.bo[ev.buf].filetype) -- Use explicit buffer + filetype to avoid any ambiguity
local ok = pcall(vim.treesitter.start, args.buf, args.match)
vim.bo.syntax = 'on'
vim.bo.indentexpr = "v:lua.require'nvim-treesitter'.indentexpr()"
vim.wo[0][0].foldexpr = 'v:lua.vim.treesitter.foldexpr()'
vim.wo[0][0].foldmethod = 'expr'
end, end,
}) })
-- FileType may have fired before this deferred setup ran. -- Textobjects: require plugin and bail out quietly if missing
if vim.bo.filetype ~= "" then local ok_nto, nto = pcall(require, "nvim-treesitter-textobjects")
start_treesitter(0, vim.bo.filetype) if not ok_nto then
return
end end
require("nvim-treesitter-textobjects").setup({ vim.g.no_plugin_maps = true
nto.setup({
move = { move = {
set_jumps = true, set_jumps = true,
}, },
@ -248,41 +322,74 @@ now_if_args(function()
local move = require("nvim-treesitter-textobjects.move") local move = require("nvim-treesitter-textobjects.move")
local swap = require("nvim-treesitter-textobjects.swap") local swap = require("nvim-treesitter-textobjects.swap")
vim.keymap.set("n", "]a", function() move.goto_next_start("@parameter.inner", "textobjects") end, { desc = "Next parameter" }) -- Map motion function names to actual functions
vim.keymap.set("n", "[a", function() move.goto_previous_start("@parameter.inner", "textobjects") end, { desc = "Prev parameter" }) local move_fns = {
vim.keymap.set("n", "]f", function() move.goto_next_start("@function.outer", "textobjects") end, { desc = "Next function" }) goto_next_start = move.goto_next_start,
vim.keymap.set("n", "[f", function() move.goto_previous_start("@function.outer", "textobjects") end, { desc = "Prev function" }) goto_next_end = move.goto_next_end,
vim.keymap.set("n", "]o", function() move.goto_next_start("@loop.*", "textobjects") end, { desc = "Next loop" }) goto_previous_start = move.goto_previous_start,
vim.keymap.set("n", "[o", function() move.goto_previous_start("@loop.*", "textobjects") end, { desc = "Prev loop" }) goto_previous_end = move.goto_previous_end,
vim.keymap.set("n", "]s", function() move.goto_next_start("@local.scope", "textobjects") end, { desc = "Next scope" }) goto_next = move.goto_next,
vim.keymap.set("n", "[s", function() move.goto_previous_start("@local.scope", "locals") end, { desc = "Prev scope" }) goto_previous = move.goto_previous,
vim.keymap.set("n", "]z", function() move.goto_next_start("@fold", "folds") end, { desc = "Next fold" }) }
vim.keymap.set("n", "[z", function() move.goto_previous_start("@fold", "folds") end, { desc = "Prev fold" })
vim.keymap.set("n", "]M", function() move.goto_next_end("@function.outer", "textobjects") end, { desc = "Next function end" })
vim.keymap.set("n", "][", function() move.goto_next_end("@class.outer", "textobjects") end, { desc = "Next class end" })
vim.keymap.set("n", "[M", function() move.goto_previous_end("@function.outer", "textobjects") end, { desc = "Prev function end" })
vim.keymap.set("n", "[]", function() move.goto_previous_end("@class.outer", "textobjects") end, { desc = "Prev class end" })
vim.keymap.set("n", "]e", function() move.goto_next("@conditional.outer", "textobjects") end, { desc = "Next conditional" })
vim.keymap.set("n", "[e", function() move.goto_previous("@conditional.outer", "textobjects") end, { desc = "Prev conditional" })
vim.keymap.set("n", "<leader>x", function() swap.swap_next("@parameter.inner", "textobjects") end, { desc = "Swap parameter next" }) -- All motions defined in one place
vim.keymap.set("n", "<leader>X", function() swap.swap_previous("@parameter.inner", "textobjects") end, { desc = "Swap parameter prev" }) -- spec = { query_or_list, query_group, desc }
local move_maps = {
goto_next_start = {
["]a"] = { "@parameter.inner", "textobjects", "Next parameter" },
["]f"] = { "@function.outer", "textobjects", "Next function start" },
["]o"] = { { "@loop.inner", "@loop.outer" }, "textobjects", "Next loop" },
["]s"] = { "@local.scope", "locals", "Next scope" },
["]z"] = { "@fold", "folds", "Next fold" },
},
goto_next_end = {
["]M"] = { "@function.outer", "textobjects", "Next function end" },
["]["] = { "@class.outer", "textobjects", "Next class end" },
},
goto_previous_start = {
["[a"] = { "@parameter.inner", "textobjects", "Previous parameter" },
["[f"] = { "@function.outer", "textobjects", "Previous function start" },
["[o"] = { { "@loop.inner", "@loop.outer" }, "textobjects", "Previous loop" },
["[s"] = { "@local.scope", "locals", "Previous scope" },
["[z"] = { "@fold", "folds", "Previous fold" },
},
goto_previous_end = {
["[M"] = { "@function.outer", "textobjects", "Previous function end" },
["[]"] = { "@class.outer", "textobjects", "Previous class end" },
},
goto_next = {
["]e"] = { "@conditional.outer", "textobjects", "Next conditional" },
},
goto_previous = {
["[e"] = { "@conditional.outer", "textobjects", "Previous conditional" },
},
}
if not Config.isNixCats then -- Generate motion keymaps
local installed_check = function(lang) for fn_name, maps in pairs(move_maps) do
return #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".*", false) == 0 local fn = move_fns[fn_name]
end if fn then
local default_parsers = Config.treesitter_helpers.default_parsers for lhs, spec in pairs(maps) do
local to_install = vim.tbl_filter(installed_check, default_parsers) local query_or_list, group, desc = spec[1], spec[2], spec[3]
if #to_install > 0 then vim.keymap.set({ "n", "x", "o" }, lhs, function()
require("nvim-treesitter").install(to_install) fn(query_or_list, group)
end, { desc = desc })
end end
end end
end
-- Swap keymaps (unchanged, but minimal)
vim.keymap.set("n", "<leader>x", function()
swap.swap_next("@parameter.inner")
end, { desc = "Swap with next parameter" })
vim.keymap.set("n", "<leader>X", function()
swap.swap_previous("@parameter.inner")
end, { desc = "Swap with previous parameter" })
end) end)
-- zk -- zk
now_if_args(function() now_if_args(function()
if nix.get_cat("markdown", false) then
require("zk").setup({ require("zk").setup({
picker = "minipick", picker = "minipick",
lsp = { lsp = {
@ -302,5 +409,4 @@ now_if_args(function()
}, },
}) })
end
end) end)

View file

@ -5,23 +5,24 @@ local add = Config.add
local nix = require('config.nix') local nix = require('config.nix')
if not Config.isNixCats then if not Config.isNixCats then
local add = MiniDeps.add local m_add = MiniDeps.add
now(function() now(function()
add({ source = "R-nvim/R.nvim" }) m_add({ source = "R-nvim/R.nvim" })
end) end)
now_if_args(function() now_if_args(function()
add({ source = "jmbuhr/otter.nvim" }) m_add({ source = "jmbuhr/otter.nvim" })
end) end)
later(function() later(function()
add({ source = "jpalardy/vim-slime" }) m_add({ source = "jpalardy/vim-slime" })
end) end)
end end
-- terminal -- terminal
later(function() later(function()
vim.g.slime_target = "neovim"
vim.g.slime_no_mappings = true vim.g.slime_no_mappings = true
add("vim-slime") add("vim-slime")
vim.g.slime_cell_delimiter = vim.g.slime_cell_delimiter or "# %%" vim.g.slime_cell_delimiter = vim.g.slime_cell_delimiter or "# %%"
@ -54,21 +55,7 @@ now(function()
min_editor_width = 80, min_editor_width = 80,
rconsole_height = 20, rconsole_height = 20,
nvimpager = "split_h", nvimpager = "split_h",
pdfviewer = "", pdfviewer = "zathura",
-- Use R.nvim's built-in rnvimserver-backed language server. Do not
-- configure the external R languageserver through plugin/25_lsp.lua.
r_ls = {
completion = true,
hover = true,
signature = true,
definition = true,
references = true,
implementation = true,
document_symbol = true,
workspace_symbol = true,
document_highlight = true,
rename = true,
},
}) })
end end
end) end)
@ -76,7 +63,8 @@ end)
-- Quarto -- Quarto
now(function() now(function()
if nix.get_cat({ "r", "markdown" }, false) then vim.treesitter.language.register("markdown", { "quarto", "rmd" })
vim.api.nvim_create_autocmd("FileType", { vim.api.nvim_create_autocmd("FileType", {
pattern = { "quarto" }, pattern = { "quarto" },
callback = function() callback = function()
@ -93,11 +81,9 @@ now(function()
write_to_disk = true, write_to_disk = true,
}, },
}) })
end
end) end)
later(function() later(function()
if nix.get_cat({ "r", "markdown" }, false) then
require("quarto").setup({ require("quarto").setup({
lspFeatures = { lspFeatures = {
enabled = true, enabled = true,
@ -116,5 +102,4 @@ later(function()
default_method = "slime", default_method = "slime",
}, },
}) })
end
end) end)

View file

@ -1,21 +1,17 @@
local Config = require('config')
local add = Config.add local add = Config.add
local now_if_args = Config.now_if_args local now_if_args = Config.now_if_args
local later = MiniDeps.later local later = MiniDeps.later
local nix = require('config.nix')
if not Config.isNixCats then if not Config.isNixCats then
local add = MiniDeps.add local m_add = MiniDeps.add
later(function() later(function()
add({ source = "Bilal2453/luvit-meta" }) m_add({ source = "Bilal2453/luvit-meta" })
add({ source = "folke/lazydev.nvim" }) m_add({ source = "folke/lazydev.nvim" })
end) end)
end end
-- lua -- lua
later(function() later(function()
if nix.get_cat("lua", false) then
add("luvit-meta") add("luvit-meta")
add("lazydev") add("lazydev")
require("lazydev").setup({ require("lazydev").setup({
@ -29,72 +25,6 @@ later(function()
{ path = "${3rd}/luv/library", words = { "vim%.uv" } }, { path = "${3rd}/luv/library", words = { "vim%.uv" } },
}, },
}) })
end
end)
-- Linting (via nvim-lint)
later(function()
Config.add("nvim-lint")
local lint_ok, lint = pcall(require, "lint")
if not lint_ok then
return
end
-- R code style via lintr (must be available in the R runtime).
-- lintr::lint() returns a "lints" object; format() turns it into the
-- standard "file:line:col: severity: message" lines.
lint.linters.lintr = {
cmd = "Rscript",
stdin = false,
args = {
"-e",
"args <- commandArgs(trailingOnly=TRUE); l <- lintr::lint(args[1]); if (length(l) > 0) cat(paste(format(l), collapse='\\n'), '\\n')",
},
append_fname = true,
stream = "both",
ignore_exitcode = true,
parser = function(output, bufnr, linter_cwd)
local diagnostics = {}
-- Expected format: /path/file.R:10:5: style: Some message
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,
severity = severity_map[severity:lower()] or vim.diagnostic.severity.WARN,
message = message or "lintr issue",
source = "lintr",
})
end
end
return diagnostics
end,
}
lint.linters_by_ft = {
r = { "lintr" },
rmd = { "lintr" },
quarto = { "lintr" },
}
vim.api.nvim_create_autocmd({ "BufReadPost", "BufWritePost", "InsertLeave" }, {
group = vim.api.nvim_create_augroup("LintOnEvents", { clear = true }),
callback = function()
lint.try_lint()
end,
})
end) end)
-- Markdown -- Markdown
@ -102,7 +32,7 @@ now_if_args(function()
add("render-markdown.nvim") add("render-markdown.nvim")
require('render-markdown').setup({ require('render-markdown').setup({
-- completions = { blink = { enabled = true } }, -- completions = { blink = { enabled = true } },
file_types = { 'markdown', }, file_types = { 'markdown', 'codecompanion', },
link = { link = {
wiki = { wiki = {
body = function(ctx) body = function(ctx)

View file

@ -2,10 +2,10 @@ local later = MiniDeps.later
local add = Config.add local add = Config.add
if not Config.isNixCats then if not Config.isNixCats then
local add = MiniDeps.add local m_add = MiniDeps.add
later(function() later(function()
add("stevearc/conform.nvim") m_add("stevearc/conform.nvim")
end) end)
end end
@ -23,6 +23,20 @@ later(function()
rmd = { "injected" }, rmd = { "injected" },
quarto = { "injected" }, quarto = { "injected" },
}, },
default_format_opts = {
lsp_format = "fallback",
},
formatters = {
my_styler = {
command = "R",
-- A list of strings, or a function that returns a list of strings
-- Return a single string instead of a list to run the command in a shell
args = { "-s", "-e", "styler::style_file(commandArgs(TRUE)[1])", "--args", "$FILENAME" },
stdin = false,
},
},
}) })
end) end)
@ -37,12 +51,7 @@ later(function()
end) end)
later(function() later(function()
require("mini.align").setup({ require("mini.align").setup()
mappings = {
start = "gA",
start_with_preview = "g<C-A>",
},
})
end) end)
later(function() later(function()
@ -218,8 +227,6 @@ later(function()
local minikeymap = require("mini.keymap") local minikeymap = require("mini.keymap")
minikeymap.setup() minikeymap.setup()
local map_multistep = minikeymap.map_multistep 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 = { local tab_steps = {
"blink_next", "blink_next",
"pmenu_next", "pmenu_next",

View file

@ -1,9 +1,11 @@
local Config = require('config')
local add = Config.add local add = Config.add
local later = MiniDeps.later local later = MiniDeps.later
local now = MiniDeps.now local now = MiniDeps.now
local now_if_args = Config.now_if_args local now_if_args = Config.now_if_args
-- Constants
local BLINK_VERSION = "v1.4.1"
-- Plugin sources configuration -- Plugin sources configuration
local PLUGIN_SOURCES = { local PLUGIN_SOURCES = {
"hrsh7th/cmp-cmdline", "hrsh7th/cmp-cmdline",
@ -11,7 +13,7 @@ local PLUGIN_SOURCES = {
"zbirenbaum/copilot.lua", "zbirenbaum/copilot.lua",
"jmbuhr/cmp-pandoc-references", "jmbuhr/cmp-pandoc-references",
"fang2hou/blink-copilot", "fang2hou/blink-copilot",
"nickjvandyke/opencode.nvim", "olimorris/codecompanion.nvim"
} }
local PLUGIN_ADDS = { local PLUGIN_ADDS = {
@ -21,36 +23,175 @@ local PLUGIN_ADDS = {
"cmp-pandoc-references", "cmp-pandoc-references",
} }
-- Helper functions
local function create_system_prompt(role_description)
return function(context)
return "I want you to act as a senior " .. context.filetype .. " developer. " .. role_description
end
end
local function get_code_block(context)
local text = require("codecompanion.helpers.actions").get_code(context.start_line, context.end_line)
return "```" .. context.filetype .. "\n" .. text .. "\n```"
end
local function create_common_opts(mapping, short_name)
return {
mapping = mapping,
modes = { "v" },
short_name = short_name,
auto_submit = true,
stop_context_insertion = true,
user_prompt = true,
}
end
local function get_mini_icons_highlight(ctx) local function get_mini_icons_highlight(ctx)
local _, hl, _ = require("mini.icons").get("lsp", ctx.kind) local _, hl, _ = require("mini.icons").get("lsp", ctx.kind)
return hl return hl
end end
local function get_blink_fuzzy_setting() local function get_blink_fuzzy_setting()
return { local setting = {
sorts = { "exact", "score", "sort_text" }, sorts = { "exact", "score", "sort_text" }
use_proximity = true,
} }
if not Config.isNixCats then
setting.prebuilt_binary = { force_version = BLINK_VERSION }
end
return setting
end end
-- Plugin loading -- Plugin loading
if not Config.isNixCats then if not Config.isNixCats then
local add = MiniDeps.add local m_add = MiniDeps.add
now_if_args(function() now_if_args(function()
add({ m_add({
source = "saghen/blink.cmp", source = "saghen/blink.cmp",
depends = { "rafamadriz/friendly-snippets" }, depends = { "rafamadriz/friendly-snippets" },
checkout = BLINK_VERSION,
}) })
end) end)
later(function() later(function()
for _, source in ipairs(PLUGIN_SOURCES) do for _, source in ipairs(PLUGIN_SOURCES) do
add({ source = source }) m_add({ source = source })
end end
end) end)
end end
local function get_codecompanion_config()
return {
interactions = {
chat = {
adapter = {
name = "copilot",
model = "gemini-3.1-pro-preview",
},
opts = {
completion_provider = "blink",
},
},
inline = {
adapter = {
name = "copilot",
model = "gemini-3.1-pro-preview",
}
},
keymaps = {
accept_change = {
modes = { n = "ga" },
description = "Accept the suggested change",
},
reject_change = {
modes = { n = "gr" },
opts = { nowait = true },
description = "Reject the suggested change",
},
},
},
display = {
chat = {
show_settings = false,
window = {
layout = "horizontal",
position = "bottom",
height = 0.33,
},
},
},
prompt_library = {
["expert"] = {
interaction = "chat",
description = "Get expert advice from an LLM",
--opts = create_common_opts("<localleader>ae", "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 = create_common_opts("<localleader>af", "afixer"),
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 = create_common_opts("<localleader>as", "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 },
},
},
},
}
}
end
-- Batch add simple plugins -- Batch add simple plugins
later(function() later(function()
for _, plugin in ipairs(PLUGIN_ADDS) do for _, plugin in ipairs(PLUGIN_ADDS) do
@ -102,33 +243,19 @@ later(function()
}) })
end) end)
later(function() later(function()
vim.g.opencode_opts = { add("codecompanion.nvim")
events = {
reload = true, -- now use function
permissions = { require("codecompanion").setup(get_codecompanion_config())
enabled = true, vim.cmd([[cab cc CodeCompanion]])
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")
end) end)
now_if_args(function() now_if_args(function()
add("blink.cmp") add("blink.cmp")
require("blink.cmp").setup({ 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 = { keymap = {
preset = "default", preset = "default",
["<C-space>"] = { "show", "select_next" }, ["<C-space>"] = { "show", "select_next" },
@ -165,7 +292,6 @@ now_if_args(function()
}, },
completion = { completion = {
menu = { menu = {
border = "rounded",
draw = { draw = {
treesitter = { "lsp" }, treesitter = { "lsp" },
components = { components = {
@ -185,13 +311,12 @@ now_if_args(function()
list = { list = {
selection = { preselect = false, auto_insert = true } selection = { preselect = false, auto_insert = true }
}, },
ghost_text = { enabled = true, show_with_menu = true }, documentation = { auto_show = true },
documentation = { auto_show = true, window = { border = "rounded" } },
trigger = { show_in_snippet = false }, trigger = { show_in_snippet = false },
}, },
snippets = { preset = "mini_snippets" }, snippets = { preset = "mini_snippets" },
sources = { sources = {
default = { "references", "lsp", "path", "snippets", "buffer", "omni", "copilot" }, default = { "references", "lsp", "path", "snippets", "buffer", "omni", "copilot", "codecompanion" },
providers = { providers = {
path = { path = {
score_offset = 50, score_offset = 50,
@ -210,12 +335,22 @@ now_if_args(function()
score_offset = 10, score_offset = 10,
opts = { cmp_name = "cmdline" } opts = { cmp_name = "cmdline" }
}, },
cmp_r = {
name = "cmp_r",
module = "blink.compat.source",
},
copilot = { copilot = {
name = "copilot", name = "copilot",
module = "blink-copilot", module = "blink-copilot",
score_offset = 45, score_offset = 45,
async = true, async = true,
}, },
codecompanion = {
name = "CodeCompanion",
module = "codecompanion.providers.completion.blink",
score_offset = 45,
async = true,
},
references = { references = {
name = "pandoc_references", name = "pandoc_references",
module = "cmp-pandoc-references.blink", module = "cmp-pandoc-references.blink",

View file

@ -1,4 +1,3 @@
local Config = require('config')
local now_if_args = Config.now_if_args local now_if_args = Config.now_if_args
if not Config.isNixCats then if not Config.isNixCats then
@ -9,81 +8,24 @@ if not Config.isNixCats then
end end
now_if_args(function() now_if_args(function()
-- R.nvim owns the R `r_ls` client and starts its bundled rnvimserver from
-- its own plugin directory. Keep R out of this generic server registry.
local servers = { local servers = {
clangd = {},
basedpyright = {}, basedpyright = {},
ruff = {}, ruff = {},
marksman = { marksman = {
filetypes = { "markdown", "markdown_inline" }, filetypes = { "markdown", "markdown_inline", "codecompanion" },
},
harper_ls = {
cmd = { "harper-ls", "--stdio" },
filetypes = { "markdown", "quarto", "text", "tex", "typst" },
root_markers = { ".git", ".harper" },
settings = {
["harper-ls"] = {
linters = {
SpellCheck = true,
SpelledNumbers = false,
AnA = true,
SentenceCapitalization = true,
UnclosedQuotes = true,
WrongApostrophe = false,
LongSentences = true,
RepeatedWords = true,
Spaces = true,
CorrectNumberSuffix = true,
},
codeActions = {
ForceStable = false,
},
markdown = {
IgnoreLinkTitle = false,
},
diagnosticSeverity = "hint",
isolateEnglish = false,
dialect = "American",
maxFileLength = 120000,
},
},
},
nil_ls = {
settings = {
["nil"] = {
formatting = {
command = { "alejandra" },
},
},
},
},
nixd = {
settings = {
nixd = {
formatting = {
command = { "alejandra" },
},
options = {
-- Downstream flakes can override these via lib.mkMerge on the lsp config.
nixos = { expr = "(builtins.getFlake \"/etc/nixos\").nixosConfigurations.\"default\".options" },
home_manager = { expr = "(builtins.getFlake \"/etc/nixos\").homeConfigurations.\"default\".options" },
},
},
},
},
yamlls = {
settings = {
yaml = {
schemas = {
["https://raw.githubusercontent.com/quarto-dev/quarto-cli/main/src/resources/schema/project.json"] = "**/_quarto.yml",
["https://raw.githubusercontent.com/quarto-dev/quarto-cli/main/src/resources/schema/document-quarto.json"] = "**/*.qmd",
},
},
},
},
texlab = {
single_file_support = true,
}, },
-- r_language_server = {
-- filetypes = { 'r', 'rmd', 'rmarkdown' },
-- settings = {
-- ['r_language_server'] = {
-- lsp = {
-- rich_documentation = true,
-- enable = true,
-- },
-- },
-- }
-- },
julials = { julials = {
settings = { settings = {
julia = { julia = {

View file

@ -1,79 +0,0 @@
local Config = require('config')
local later = MiniDeps.later
local nix = require('config.nix')
later(function()
-- Only pull in the DAP packages when R (the only cat that has a real
-- adapter for now) is enabled; otherwise the lazy load + autoload chain
-- still works but those three would sit unused on the runtime path.
if not nix.get_cat("r", false) then
return
end
Config.add("nvim-dap")
Config.add("nvim-dap-ui")
Config.add("nvim-dap-virtual-text")
end)
later(function()
if not nix.get_cat("r", false) then
return
end
local dap_ok, dap = pcall(require, "dap")
if not dap_ok then
vim.notify("nvim-dap not available", vim.log.levels.WARN)
return
end
local dapui_ok, dapui = pcall(require, "dapui")
if dapui_ok then
dapui.setup()
dap.listeners.after.event_initialized["dapui_config"] = function()
dapui.open()
end
dap.listeners.before.event_terminated["dapui_config"] = function()
dapui.close()
end
dap.listeners.before.event_exited["dapui_config"] = function()
dapui.close()
end
end
local vt_ok, _ = pcall(require, "nvim-dap-virtual-text")
if vt_ok then
-- Default setup is enough; virtual text is enabled automatically.
end
-- R adapter via vscDebugger (https://github.com/cwida/vscDebugger)
if nix.get_cat("r", false) then
dap.adapters.r = {
type = "executable",
command = "R",
args = {
"--quiet",
"--no-save",
"-e",
"vscDebugger::main()",
},
}
dap.configurations.r = {
{
type = "r",
name = "Debug current R script",
request = "launch",
program = "${file}",
debugMode = "function",
},
{
type = "r",
name = "Attach to R process",
request = "attach",
hostName = "localhost",
port = 18721,
},
}
end
end)

View file

@ -1,50 +0,0 @@
local Config = require('config')
local later = MiniDeps.later
local nix = require('config.nix')
-- Only load image-nvim when a cat that benefits from in-buffer plots is on.
later(function()
if not nix.get_cat({ "r", "markdown" }, false) then
return
end
Config.add("image-nvim")
end)
later(function()
if not nix.get_cat({ "r", "markdown" }, false) then
return
end
local ok, image = pcall(require, "image")
if not ok then
vim.notify("image.nvim not available", vim.log.levels.DEBUG)
return
end
image.setup({
-- Backend is intentionally NOT set so image.nvim auto-detects the
-- graphics protocol (kitty / wezterm / iterm / sixel) at runtime and
-- falls back to no rendering on unsupported terminals. The literal
-- strings "auto" / "none" are not accepted by image.nvim.setup(), so
-- setting either would silently disable rendering everywhere.
integrations = {
markdown = {
enabled = true,
clear_in_insert_mode = false,
download_remote_images = true,
only_render_image_at_cursor = false,
filetypes = { "markdown", "quarto" },
},
},
max_width = nil,
max_height = nil,
max_width_window_percentage = nil,
max_height_window_percentage = 50,
window_overlap_clear_enabled = false,
window_overlap_clear_ft_ignore = { "cmp_menu", "cmp_docs" },
editor_only_render_when_focused = false,
hijack_file_patterns = { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp" },
})
end)

View file

@ -1,38 +0,0 @@
-- vimtex integration for raw .tex workflows (paper drafts, AEA submissions,
-- beamer slides). Rides in the `markdown` cat because Quarto users routinely
-- also write standalone .tex.
local Config = require('config')
local later = MiniDeps.later
local nix = require('config.nix')
later(function()
if not nix.get_cat("markdown", false) then
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")
-- 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", {
pattern = { "*.tex", "*.sty", "*.cls" },
callback = function(args)
local buf = args.buf
if vim.api.nvim_buf_is_loaded(buf) then
vim.bo[buf].filetype = "tex"
end
end,
})
end)

View file

@ -1,43 +0,0 @@
-- Bloocky: timeblocking calendar (day/week/month views), persisted to JSON.
-- Loaded via the `general` cat spec (nix mode) or MiniDeps (non-nix).
-- Default global toggle is <leader>tb, which collides with the terminal map
-- (toggle bracketed paste), so it is moved to the calendar/tasks group <leader>c.
local Config = require('config')
if not Config.isNixCats then
local later = MiniDeps.later
later(function()
MiniDeps.add({ source = 'atiladefreitas/bloocky' })
end)
end
local nix = require('config.nix')
local later = MiniDeps.later
later(function()
if not nix.get_cat('general', false) then
return
end
require('bloocky').setup({
week_start = "monday",
window = {
-- Width per view: fraction of the editor width (or absolute columns if > 1).
-- A single number applies to every view.
width = {
month = 0.99,
week = 0.99,
day = 0.8,
},
border = "rounded",
},
integrations = {
dooing = {
enabled = true, -- show Dooing todos on their due date
show_done = false, -- also show completed todos
},
},
keymaps = {
toggle = '<leader>cb',
},
})
end)

View file

@ -1,44 +0,0 @@
-- Dooing: minimalist todo list manager with a floating window, persisted to JSON.
-- Loaded via the `general` cat spec (nix mode) or MiniDeps (non-nix).
-- Default globals <leader>td / <leader>tN occupy the terminal group, so the
-- todo toggles are moved to the calendar/tasks group <leader>c.
local Config = require('config')
if not Config.isNixCats then
local later = MiniDeps.later
later(function()
MiniDeps.add({ source = 'atiladefreitas/dooing' })
end)
end
local nix = require('config.nix')
local later = MiniDeps.later
local now = MiniDeps.now
later(function()
if not nix.get_cat('general', false) then
return
end
local dooing = require('dooing')
require("dooing").setup({
keymaps = {
toggle_window = "<leader>cd",
open_project_todo = "<leader>cD",
show_due_notification = "<leader>cN",
create_nested_task = "<leader>cn", -- Create nested subtask under current todo
toggle_priority = "a",
},
calendar = {
week_start_day = "monday",
},
})
-- Remove Dooing's old defaults.
vim.keymap.del("n", "<leader>td")
vim.keymap.del("n", "<leader>tD")
vim.keymap.del("n", "<leader>tN")
-- Restore mappings overwritten by Dooing.
local helpers = require('keymap.helpers')
local nmap_leader = helpers.nmap_leader
-- t is for 'terminal'
nmap_leader("td", '<Cmd>lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()<CR>', 'Open DuckDB')
end)

View file

@ -1,8 +0,0 @@
-- omp bridge entrypoint: start the RPC socket as early as possible so the omp
-- harness can read this instance's buffers on demand (see lua/nvim_omp/init.lua
-- and .omp/tools/nvim_buffers.mjs). Safe to fail silently — the socket is a
-- convenience, not a dependency of the editor.
local ok, nvim_omp = pcall(require, "nvim_omp")
if ok then
nvim_omp.start()
end

14
scripts/updater.nix Normal file
View file

@ -0,0 +1,14 @@
{pkgs}:
pkgs.writeShellApplication {
name = "updateR";
# Tools your script needs at runtime
runtimeInputs = [
pkgs.wget
pkgs.gnused
pkgs.coreutils
];
# Keep script in separate file, but embed contents
text = builtins.readFile ./updater.sh;
}

22
scripts/updater.sh Normal file
View file

@ -0,0 +1,22 @@
echo "📡 Fetching latest R version from rstats-on-nix..."
RVER=$( wget -qO- 'https://raw.githubusercontent.com/ropensci/rix/refs/heads/main/inst/extdata/available_df.csv' | tail -n 1 | head -n 1 | cut -d',' -f4 | tr -d '"' )
# Validate RVER matches YYYY-MM-DD format
if [[ ! "$RVER" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]]; then
echo "❌ Error: Failed to fetch valid R version date. Got: '$RVER'"
exit 1
fi
echo "✅ R date is $RVER"
# Create backup of flake.nix before modifying
cp flake.nix flake.nix.backup
# Update rixpkgs date in flake.nix
if sed -i "s|rixpkgs.url = \"github:rstats-on-nix/nixpkgs/[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\";|rixpkgs.url = \"github:rstats-on-nix/nixpkgs/$RVER\";|" flake.nix; then
echo "✅ Updated rixpkgs date in flake.nix"
rm flake.nix.backup
else
echo "⚠️ Warning: Failed to update flake.nix, restoring backup"
mv flake.nix.backup flake.nix
fi

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