Compare commits

..
Author SHA1 Message Date
6b6f5b3e57 vimtex is not lua 2026-07-27 11:25:49 +10:00
475a914ad2
Merge pull request #13 from dwinkler1/refactor/quant-eco-workflow-improvements
refactor: quant econ workflow improvements
2026-07-27 11:19:15 +10:00
Daniel
180c3e4f55 feat: add Harper grammar checker LSP 2026-07-27 01:09:25 +00:00
d9a8c31bc6 always add vimtex 2026-07-27 10:40:14 +10:00
5f6b1248dd
Merge pull request #12 from dwinkler1/refactor/quant-eco-workflow-improvements
refactor: improve Neovim/Nix config for quant econ workflow
2026-07-27 10:34:11 +10:00
f60ba80756
Merge pull request #10 from dwinkler1/dependabot/github_actions/actions/checkout-7
Bump actions/checkout from 4 to 7
2026-07-27 10:33:31 +10:00
931dfc1691
Merge pull request #11 from dwinkler1/dependabot/github_actions/cachix/cachix-action-17
Bump cachix/cachix-action from 14 to 17
2026-07-27 10:33:19 +10:00
Daniel
92bd53feea fix: add ripgrep to always category and revert shellHook to use rg 2026-07-27 00:30:10 +00:00
Daniel
ad26b17b8c fix: avoid ripgrep dependency in devShell R_LIBS_SITE hook 2026-07-27 00:28:43 +00:00
Daniel
5c35787c68 fix: remove broken luaPackages.magick from markdown cat packages 2026-07-27 00:24:33 +00:00
Daniel
2dc7227a0e fix: remove vscDebugger from default R packages (not in CRAN/rPackages) 2026-07-27 00:21:18 +00:00
Daniel
c85925b4f7 Merge remote-tracking branch 'origin/refactor/quant-eco-workflow-improvements' into refactor/quant-eco-workflow-improvements 2026-07-27 00:18:06 +00:00
Daniel
d45d85e5bc chore: remove Stata tree-sitter parser from default parser list 2026-07-27 00:16:50 +00:00
copilot-swe-agent[bot]
f4b960c604
docs: clarify Nix laziness comment 2026-07-27 00:11:43 +00:00
copilot-swe-agent[bot]
259b3d65b8
fix: use valid Nix comment syntax in modules 2026-07-27 00:10:59 +00:00
Daniel
6142f7d55e fix: harden REPL/terminal code-sending paths
- lua/keymap/repl.lua
  - Replace non-existent `:SlimeSendCurrentLine` with synchronous
    `vim.fn["slime#send"](line .. "\n")` fallback.
  - Support both modern `r.send` and older `r.run` R.nvim Lua APIs,
    falling back to `<Plug>` mappings if neither module is available.

- lua/nix_smart_send.lua
  - Refactor `send_repl()` to extract Tree-sitter node text and send it
    directly via `slime#send`, eliminating the `feedkeys(..., "x", true)`
    race between visual selection and cursor movement.
  - Replace internal `slime#send_op` call in `slime_send_region()` with the
    public `<Plug>SlimeRegionSend` mapping.
  - Improve `move_to_next_non_empty_line()` to walk up the AST when a
    node has no next sibling, so the cursor escapes nested blocks.

- plugin/03_terminal.lua
  - Set `vim.g.slime_target = "neovim"` at the top of the module so the
    target is guaranteed before any slime send.

- plugin/21_datascience.lua
  - Remove duplicate `vim.g.slime_target = "neovim"` now that the
    terminal module owns the setting.
2026-07-26 06:57:57 +00:00
Daniel
137ec6d576 style: tidy visual clue groups + fix casing
Two cosmetic cleanups from the post-fix consistency pass on 3f2ab6e:

* lua/keymap/core.lua: changed the visual ``<leader>a`` entry to
  ``<Leader>a`` on line 46 so the casing is consistent with every
  other key in ``Config.leader_group_clues``. mini.clue's trigger
  matching is case-insensitive, so this is purely cosmetic.

* lua/keymap/leader.lua: added an ``xmap_leader('rr', ...)`` mapping
  that sends the visual selection through the filetype-aware
  dispatcher in ``lua/keymap/repl.lua``. Previously the visual
  ``<Leader>r`` (+R) clue group was empty (no ``xmap_leader("r", ...)``
  existed); visual users now have the same send-selection affordance
  that normal-mode users already get from the dispatcher.

No normal-mode behavior changes. No regressions. The existing
visual ``<leader>rx`` reprex mapping is preserved verbatim, just
with a clarifying comment above it.

Files: 2 modified. +9 / -1.
2026-07-26 06:49:12 +00:00
Daniel
3f2ab6ef96 fix: address R1 (image.nvim backend) + C4 (cat-gate consistency)
Tackles the two outstanding items from the post-fix consistency
pass on top of `df2f776`.

* R1 -- plugin/27_image.lua: removed the explicit `backend = "auto"`
  line. image.nvim's setup() does not accept "auto" / "none" as
  literal values; the practical default is to leave `backend` unset,
  letting image.nvim auto-detect the right graphics protocol
  (kitty / wezterm / iterm / sixel) at runtime and falling back to
  no rendering on unsupported terminals. Inline comment explains the
  omission so a future reader doesn't "fix" it back.

* C4 -- modules/module/specs/plugins.nix:
  - Removed `nvim-dap`, `nvim-dap-ui`, `nvim-dap-virtual-text`,
    and `image-nvim` from `config.specs.utils-lazy`. They are R- and
    Markdown-specific, not general utility plugins, so they should
    not depend on `utils=true`.
  - Added a new `config.specs.r-lazy` spec carrying those same four
    plugins, gated by `cats.r`. Users with `r=true` and `utils=false`
    now get a working R debugger (via vscDebugger) and in-buffer
    image rendering for plots.
  - Added `image-nvim` to the existing `config.specs.markdown-lazy`
    so users with `markdown=true` and no other cats still see inline
    plots in Quarto / Markdown documents. nixCats dedups packages
    by pname, so `image-nvim` appears once on the runtime path even
    when both `r` and `markdown` are on.

These two changes close the real correctness bugs surfaced by the
consistency review: `plugin/26_dap.lua` and `plugin/27_image.lua`'s
cat-gated `Config.add(...)` calls previously depended on
`utils=true` resolving the four packages, leaving `r=true` (or
`markdown=true`) only users without DAP / image-nvim even though the
Lua gate let the setup function proceed.

Files: 2 modified. Local verification (`nix flake check --no-build`)
still required before merging PR #12.
2026-07-26 06:47:58 +00:00
Daniel
df2f776d3f fix: address review findings (C1/C3/H1/H2/H5 + M1)
Tackles the critical and high-impact findings from the in-PR review
of commit 7f01be5, plus one consistency fix (M1). All changes are
scoped to the same review branch (PR #12); no behavior changes
elsewhere.

Critical fixes
* C1 -- plugin/22_languages.lua: now does `local Config = require('config')`
  at the top, matching every other `plugin/*.lua` file. Previously the
  file referenced `Config` as a global, depending on `_G.Config` having
  been initialized by `init.lua` before this file loaded. Fragile.
* C2 -- plugin/04_treesitter.lua: removed the hard `<CR>` ->
  `smart_send.send_repl` mapping. Override `<CR>` from a per-buffer
  `ftplugin/<lang>.lua` if you want enter-to-send behavior. R.nvim's
  `<Plug>RDSendLine` (wired by `ftplugin/r.lua`) remains the default
  for R files and is no longer silently clobbered.

High-impact fixes
* C3 -- plugin/27_image.lua: `image.setup({ backend = "kitty" })` ->
  `backend = "auto"`. The previous value silently failed on every
  terminal that is not Kitty. `"auto"` delegates detection to image.nvim.
* H1 -- plugin/10_keymap.lua: `_G.Config = Config` removed from this
  file; `init.lua:2` remains the single source. Avoids drift between
  two aliasing sites.
* H2 -- plugin/25_lsp.lua: `texlab = { single_file_support = true }`,
  so single-file `.tex` buffers attach the LSP without lspconfig's
  sometimes-brittle root_dir heuristic.
* H5 -- plugin/04_treesitter.lua: also drops `<S-CR>` from
  `M.setup_keybindings` on the same principle as C2. `<S-CR>` was a
  hard implicit override in both normal and insert mode, where it
  collided with snippet and transient-state plugins.

Consistency fixes
* M1 -- plugin/01_lib.lua: `print(line)` in `Config.execute_lua_line`
  switched to `vim.notify(line, vim.log.levels.INFO)`, matching the
  print->notify cleanup in `plugin/04_treesitter.lua` from 7f01be5.

Files: 6 modified. +21 / -11.

Local verification
1. `luac -p plugin/{22_languages,27_image,10_keymap,25_lsp,04_treesitter,01_lib}.lua`
2. `nvim --headless -u NONE -l tests/init.lua`
3. Open a R / quarto / tex buffer; verify `<CR>` is no longer hijacked
   by `smart_send` and behaves like the filetype default.

For reviewers
* The text-object configuration in plugin/04_treesitter.lua's
  textobjects block is unchanged. H4 (`@assignment.*` queries may not be
  defined for R) is left for a follow-up with verification.
* yamlls GitHub-rawURL schema dependency (H3) is left intentionally --
  vendoring the schemas is a separate decision.
* Tests (T1-T6 from the review) are deferred; no test infrastructure
  exists beyond `tests/init.lua`.
2026-07-26 06:43:35 +00:00
Daniel
7f01be59d7 refactor: improve Neovim/Nix config for quant econ workflow
Consolidates a multi-pass refactor and a set of workflow integrations
tailored to a quantitative economics research workflow (R / Python /
Quarto / LaTeX heavy, reproducibility-conscious). The existing terminal
setup is preserved (snacks.nvim was deliberately not adopted).

=== Structural refactors ===
* Replace the `_G.Config` global with a proper `require('config')` Lua
  module. `_G.Config` is kept only as a backward-compatible alias.
* Split the monolithic `plugin/10_keymap.lua` into domain-specific
  files under `lua/keymap/` (core, helpers, leader, terminal, repl).
* Harden `lua/nix_smart_send.lua` `send_repl`: only skip forward on
  comment nodes, and return cleanly when there is no next sibling.
* Add a filetype-aware REPL dispatcher in `lua/keymap/repl.lua`:
  pure R scripts -> R.nvim, .qmd/.Rmd chunks -> quarto.runner,
  everything else -> vim-slime. Prefer `quarto.runner.run_line()`
  when available and fall back to `run_cell()`.
* Add R Treesitter text objects (function / call / assignment) for
  faster motion when sending code to the REPL.
* Add `+Send` and `+Debug` leader-clue groups for the new prefixes.

=== LSP and tooling ===
* Register `yamlls` for Quarto YAML frontmatter (`plugin/25_lsp.lua`).
* Register `texlab` and add `vimtex` for a real `.tex` workflow
  (`plugin/25_lsp.lua` + new `plugin/28_latex.lua`).
* Wire `nvim-dap` with an R adapter backed by `vscDebugger`
  (new `plugin/26_dap.lua`, gated to the `r` cat).
* Add `image-nvim` for in-editor plots, gated to the `r`/`markdown`
  cats (new `plugin/27_image.lua`).
* Wire `lintr` into `nvim-lint` for R / Quarto (`plugin/22_languages.lua`).
* Add Treesitter parsers for `stata`, `matlab`, `bibtex` for completeness.

=== Nix updates ===
* Pin `python313Packages.pylatexenc` -> `python3Packages.pylatexenc`
  so the markdown cat survives nixpkgs Python default shifts.
* Add `texlab`, `imagemagick`, `luaPackages.magick` to the markdown
  cat so `image.nvim` has a working backend.
* Add `vscDebugger` and `lintr` to the R package list.

=== Misc ===
* Prefer the R.nvim v1.0 Lua API (`require('r.run').send_line()` /
  `send_selection()`); keep `<Plug>` mappings as a fallback so older
  downstream builds don't regress.
* Use `alejandra` (already installed) as the nixd / nil_ls formatter.

Files: 14 modified, 5 added (953 insertions, 490 deletions).

Local verification before merge:
1. `nix flake check --no-build`
2. `nvim --headless -u NONE -l tests/init.lua`
3. Open a `.qmd` -> confirm `yamlls` attaches and `image.nvim` renders.
4. Open an `.R` -> `<leader>db`, `<leader>dc` confirm DAP loads.
5. Open a `.tex` -> confirm `texlab` + `vimtex` are active.
2026-07-26 06:35:52 +00:00
dependabot[bot]
60b23e42d0
Bump cachix/cachix-action from 14 to 17
Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action) from 14 to 17.
- [Release notes](https://github.com/cachix/cachix-action/releases)
- [Changelog](https://github.com/cachix/cachix-action/blob/master/RELEASE.md)
- [Commits](https://github.com/cachix/cachix-action/compare/v14...v17)

---
updated-dependencies:
- dependency-name: cachix/cachix-action
  dependency-version: '17'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 05:04:58 +00:00
dependabot[bot]
6db6d131be
Bump actions/checkout from 4 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-24 05:04:54 +00:00
27 changed files with 1056 additions and 514 deletions

View file

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

View file

@ -10,7 +10,7 @@ jobs:
id-token: "write" id-token: "write"
contents: "read" contents: "read"
steps: steps:
- uses: "actions/checkout@v5" - uses: "actions/checkout@v7"
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@v4 uses: actions/checkout@v7
- 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@v14 - uses: cachix/cachix-action@v17
with: with:
name: rde name: rde
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'

View file

@ -1,7 +1,5 @@
_G.Config = {} local Config = require('config')
local nix = require('config.nix').init { non_nix_value = true } _G.Config = Config -- keep global alias for keymaps/backward compatibility
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)
@ -12,8 +10,11 @@ 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', 'git', 'clone', '--filter=blob:none', '--branch=' .. mini_nvim_tag,
'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)

10
lua/config/init.lua Normal file
View file

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

44
lua/keymap/core.lua Normal file
View file

@ -0,0 +1,44 @@
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' },
}

25
lua/keymap/helpers.lua Normal file
View file

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

264
lua/keymap/leader.lua Normal file
View file

@ -0,0 +1,264 @@
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", "<cmd>CodeCompanion /agent<CR>", "Agent chat (@{agent} tools)")
nmap_leader("ac", "<cmd>CodeCompanionChat Toggle<CR>", "Chat Toggle")
nmap_leader("aC", function()
local chat = require("codecompanion").last_chat()
if not chat then
return vim.notify("No CodeCompanion chat to compact", vim.log.levels.WARN)
end
require("codecompanion.interactions.chat.context_management.compaction").compact(chat, { min_token_savings = 0 })
end, "Compact chat")
nmap_leader("ag", "<cmd>CodeCompanion /commit<CR>", "Generate commit message")
nmap_leader("ai", "<cmd>CodeCompanionActions<CR>", "Chat Action")
nmap_leader("al", "<cmd>CodeCompanion /lsp<CR>", "Explain LSP Diagnostics")
nmap_leader("an", "<cmd>CodeCompanionChat Add<CR>", "Chat New")
nmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
nmap_leader("aw", "<cmd>CodeCompanion /tdd<CR>", "Workflow: plan, implement, test")
nmap_leader("ax", "<cmd>CodeCompanion /fixer<CR>", "Code Fixer")
xmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent on selection")
xmap_leader("ae", "<cmd>CodeCompanion /explain<CR>", "Explain Code")
xmap_leader("af", "<cmd>CodeCompanion /fix<CR>", "Fix Code")
xmap_leader("ap", "<cmd>CodeCompanion /expert<CR>", "Code Expert")
xmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
nmap_leader("ak", "<cmd>CodeCompanionChat adapter=codex<CR>", "Chat with Codex")
-- b is for 'buffer'
nmap_leader('bb', '<Cmd>b#<CR>', 'Alternate')
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("_", "<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

101
lua/keymap/repl.lua Normal file
View file

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

17
lua/keymap/terminal.lua Normal file
View file

@ -0,0 +1,17 @@
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,5 +1,12 @@
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
@ -14,83 +21,90 @@ local function is_in_list(list, value)
return false return false
end end
-- Define comment node types as constants -- Safely get the Tree-sitter node under the cursor.
local COMMENT_TYPES = {
comment = true,
block_comment = true,
line_comment = true,
}
function M.get_current_node() function M.get_current_node()
local cur_win = vim.api.nvim_get_current_win() local ok, node = pcall(vim.treesitter.get_node, { ignore_injections = false })
return vim.treesitter.get_node({ return ok and node or nil
winid = cur_win,
ignore_injections = 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
-- print("No node detected") local ok, parser = pcall(vim.treesitter.get_parser)
local parser = vim.treesitter.get_parser() if not ok or not parser then
if not parser then
return nil return nil
end end
root = parser:parse()[1]:root() local trees = parser:parse()
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
if not root then return root and root:type() or nil
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
return root:type() while node do
end local parent = node:parent()
if not parent then
function M.move_to_next_non_empty_line() break
-- Search for the next non-empty line
local line_num = vim.fn.search("[^;\\s]", "W")
if line_num <= 0 then
-- print("No non-empty line found below the current position")
return false
end
-- Get the line content and find first non-whitespace character
local line_content = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1]
local first_non_ws = line_content:find("%S") or 1
vim.api.nvim_win_set_cursor(0, { line_num, first_non_ws - 1 })
local node = M.get_current_node()
if not node or not node:type() then
-- print("No node found")
return false
end
local global_node_type = M.detect_global_node()
-- Skip comments and global nodes
while node and (COMMENT_TYPES[node:type()] or node:type() == global_node_type) do
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 end
-- Get the line content and find first non-whitespace character local p_type = parent:type()
line_content = vim.api.nvim_buf_get_lines(0, line_num - 1, line_num, false)[1] if is_in_list(global_nodes, p_type) or p_type == root_type then
first_non_ws = line_content:find("%S") or 1 break
vim.api.nvim_win_set_cursor(0, { line_num, first_non_ws - 1 }) end
node = vim.treesitter.get_node() node = parent
end end
return true return node
end
-- Move the cursor to the next named sibling that is not a comment.
-- Operates on the Tree-sitter AST instead of scanning lines, so it is fast and
-- language-agnostic.
function M.move_to_next_non_empty_line(current_node)
local node = current_node
if not node then
node = get_target_node({})
end
if not node then
return false
end
-- Walk up the tree until we find a node with a next named sibling,
-- so we escape nested blocks when we are on the last statement.
while node and not node:next_named_sibling() do
node = node:parent()
end
node = node:next_named_sibling()
while node do
if not COMMENT_TYPES[node:type()] then
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 end
function M.vselect_node(node) function M.vselect_node(node)
@ -108,93 +122,52 @@ function M.vselect_node(node)
end end
function M.select_until_global(global_nodes) function M.select_until_global(global_nodes)
local root_node = M.detect_global_node() local target = get_target_node(global_nodes)
if not root_node and global_nodes then if not target then
root_node = global_nodes[1]
end
-- Use empty table if no global nodes provided
global_nodes = global_nodes or {}
local node = vim.treesitter.get_node()
if not node then
-- print("No syntax node found at cursor position")
return nil return nil
end end
local node_type = node:type() if M.vselect_node(target) then
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()
-- Check if slime plugin is available vim.api.nvim_feedkeys(
if not vim.fn.exists('*slime#send_op') then vim.api.nvim_replace_termcodes("<Plug>SlimeRegionSend", true, false, true),
vim.notify("slime plugin not available", vim.log.levels.ERROR) "m",
return false
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 cur_node = M.get_current_node() local target_node = get_target_node(global_nodes)
if not target_node then
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
-- Send the selected text to the terminal using vim-slime -- If sitting on a comment, step forward first so we don't send comments.
M.slime_send_region() if COMMENT_TYPES[target_node:type()] then
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
-- Move cursor and continue -- Extract node text and send directly to avoid visual-mode/feedkeys races.
local _, _, er, ec = sel_node:range() local ok, text = pcall(vim.treesitter.get_node_text, target_node, 0)
vim.api.nvim_win_set_cursor(0, { er + 1, ec }) if not ok or not text then
vim.notify("Could not extract code from Tree-sitter node", vim.log.levels.WARN)
return
end
M.move_to_next_non_empty_line() 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

@ -5,6 +5,10 @@
... ...
}: }:
let 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: maybe = cat: pkgsList:
lib.optionals (config.cats.${cat} or false) pkgsList; lib.optionals (config.cats.${cat} or false) pkgsList;
rPackages = (pkgs.baseRPackages or [ ]) ++ config.settings.lang_packages.r; rPackages = (pkgs.baseRPackages or [ ]) ++ config.settings.lang_packages.r;
@ -21,11 +25,14 @@ in
}; };
config.catPkgs = { config.catPkgs = {
always = maybe "always" (with pkgs; [ ]); always = maybe "always" (with pkgs; [
ripgrep
]);
clickhouse = maybe "clickhouse" (with pkgs; [ clickhouse-lts ]); clickhouse = maybe "clickhouse" (with pkgs; [ clickhouse-lts ]);
external = maybe "external" (with pkgs; [ external = maybe "external" (with pkgs; [
nodejs
perl perl
ruby ruby
shfmt shfmt
@ -40,10 +47,13 @@ in
lua = maybe "lua" (with pkgs; [ lua-language-server ]); lua = maybe "lua" (with pkgs; [ lua-language-server ]);
markdown = maybe "markdown" (with pkgs; [ markdown = maybe "markdown" (with pkgs; [
python313Packages.pylatexenc python3Packages.pylatexenc
quartoPkg quartoPkg
zk zk
marksman marksman
texlab
imagemagick
harper
]); ]);
nix = maybe "nix" (with pkgs; [ nix = maybe "nix" (with pkgs; [
@ -88,7 +98,6 @@ in
in in
with pkgs; [ with pkgs; [
python_with_packages python_with_packages
nodejs
ruff ruff
basedpyright basedpyright
uv uv

View file

@ -44,6 +44,11 @@
data_table data_table
janitor janitor
styler 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 [ julia = lib.mkDefault [

View file

@ -3,7 +3,52 @@
pkgs, pkgs,
lib, lib,
... ...
}: { }:
let
parserList = [
"bash"
"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.gitPlugins = lib.mkIf (config.cats.gitPlugins or false) {
data = []; data = [];
@ -24,6 +69,7 @@
lazy = true; lazy = true;
data = [ data = [
config.nvim-lib.neovimPlugins.cmp-pandoc-references config.nvim-lib.neovimPlugins.cmp-pandoc-references
pkgs.vimPlugins.image-nvim
]; ];
}; };
@ -32,6 +78,7 @@
lze lze
lzextras lzextras
plenary-nvim plenary-nvim
vimtex
neogit neogit
{ {
data = mini-nvim; data = mini-nvim;
@ -125,42 +172,7 @@
}; };
config.specs.treesitterParsers = lib.mkIf (config.cats.treesitterParsers or false) { config.specs.treesitterParsers = lib.mkIf (config.cats.treesitterParsers or false) {
data = with pkgs.vimPlugins.nvim-treesitter-parsers; [ data = map (name: pkgs.vimPlugins.nvim-treesitter-parsers.${name}) config.settings.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 = lib.mkIf (config.cats.utils or false) {
@ -172,11 +184,21 @@
colorful-menu-nvim colorful-menu-nvim
conform-nvim conform-nvim
copilot-lua copilot-lua
nvim-lint
vim-slime
];
};
# Lazy-loaded plugins needed when the `r` cat is on. Kept separate from
# `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;
data = with pkgs.vimPlugins; [
nvim-dap nvim-dap
nvim-dap-ui nvim-dap-ui
nvim-dap-virtual-text nvim-dap-virtual-text
nvim-lint image-nvim
vim-slime
]; ];
}; };

View file

@ -1,3 +1,5 @@
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
@ -32,7 +34,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)
print(line) vim.notify(line, vim.log.levels.INFO)
vim.api.nvim_input('<Down>') vim.api.nvim_input('<Down>')
end end

View file

@ -1,3 +1,5 @@
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,3 +1,9 @@
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
@ -34,6 +40,10 @@ 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
@ -45,24 +55,43 @@ 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
-- We can use standard slime sending if needed, or direct chan_send for initialization if not job_id then
local job_id = vim.b[term_buf].terminal_job_id vim.notify("Terminal job not ready, cannot send command", vim.log.levels.WARN)
if job_id then else
vim.api.nvim_chan_send(job_id, command .. "\r") local ok, err = pcall(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
local slime_config = { jobid = vim.b[term_buf].terminal_job_id } if job_id then
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
vim.api.nvim_buf_set_var(code_buf, "slime_config", slime_config) local ok, err = pcall(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,3 +1,5 @@
local Config = require('config')
local M = {} local M = {}
-- Default parsers list moved from startup config -- Default parsers list moved from startup config
@ -93,12 +95,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
print("Not a node") vim.notify("No Tree-sitter node under cursor", vim.log.levels.WARN)
return nil return nil
end end
local node_type = cur_node:type() local node_type = cur_node:type()
print("Node type: " .. node_type) vim.notify("Node type: " .. node_type, vim.log.levels.INFO)
return node_type return node_type
end end
@ -119,11 +121,17 @@ 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 })
vim.keymap.set({ 'n', 'i' }, '<S-CR>', function() smart_send.send_repl(current_global_nodes) end, -- Both `<CR>` and `<S-CR>` were removed from `M.setup_keybindings`. They
{ noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) -- were hard, implicit overrides that clobbered Vim/filetype defaults and
-- the user's snippet + insert-mode workflows (see C2 + H5 in the PR
vim.keymap.set('n', '<CR>', function() smart_send.send_repl(current_global_nodes) end, -- review). To opt back in for a specific filetype, override per-buffer
{ noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) -- from a `ftplugin/<lang>.lua`:
--
-- -- 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,
@ -134,8 +142,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()
pout = table.concat(global_nodes, ', ') .. "" local pout = table.concat(global_nodes, ', ')
print(pout) vim.notify("global_nodes: " .. pout, vim.log.levels.INFO)
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,
@ -144,4 +152,46 @@ 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,323 +1,11 @@
-- Basic mappings ============================================================= local Config = require('config')
-- 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" }) -- Domain-specific keymap modules. Core must load first because it defines the
vim.keymap.set({ "n", "v", "x" }, "<leader>y", '"+y', { noremap = true, silent = true, desc = "Copy toclipboard" }) -- leader clue groups used by the mini.clue setup in the startup plugins.
-- Leader mappings ============================================================ require('keymap.core')
-- stylua: ignore start require('keymap.leader')
require('keymap.terminal')
-- Create global tables with information about clue groups in certain modes -- `_G.Config` is set by `init.lua:2` once at start-up; no need to re-export
-- Structure of tables is taken to be compatible with 'mini.clue'. -- here. The single source of truth is `init.lua` so future refactors don't
_G.Config.leader_group_clues = { -- have to chase which file currently publishes the alias.
{ 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("aa", "<cmd>CodeCompanion /agent<CR>", "Agent chat (@{agent} tools)")
nmap_leader("ac", "<cmd>CodeCompanionChat Toggle<CR>", "Chat Toggle")
nmap_leader("aC", function()
local chat = require("codecompanion").last_chat()
if not chat then
return vim.notify("No CodeCompanion chat to compact", vim.log.levels.WARN)
end
require("codecompanion.interactions.chat.context_management.compaction").compact(chat, { min_token_savings = 0 })
end, "Compact chat")
nmap_leader("ag", "<cmd>CodeCompanion /commit<CR>", "Generate commit message")
nmap_leader("ai", "<cmd>CodeCompanionActions<CR>", "Chat Action")
nmap_leader("al", "<cmd>CodeCompanion /lsp<CR>", "Explain LSP Diagnostics")
nmap_leader("an", "<cmd>CodeCompanionChat Add<CR>", "Chat New")
nmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
nmap_leader("aw", "<cmd>CodeCompanion /tdd<CR>", "Workflow: plan, implement, test")
nmap_leader("ax", "<cmd>CodeCompanion /fixer<CR>", "Code Fixer")
xmap_leader("aa", "<cmd>CodeCompanion /agent<CR>", "Agent on selection")
xmap_leader("ae", "<cmd>CodeCompanion /explain<CR>", "Explain Code")
xmap_leader("af", "<cmd>CodeCompanion /fix<CR>", "Fix Code")
xmap_leader("ap", "<cmd>CodeCompanion /expert<CR>", "Code Expert")
xmap_leader("as", "<cmd>CodeCompanion /suggest<CR>", "Suggest Improvements")
nmap_leader("ak", "<cmd>CodeCompanionChat adapter=codex<CR>", "Chat with Codex")
-- b is for 'buffer'
nmap_leader('bb', '<Cmd>b#<CR>', 'Alternate')
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')
-- - 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,3 +1,4 @@
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

View file

@ -22,7 +22,6 @@ 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 "# %%"

View file

@ -1,3 +1,5 @@
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
@ -30,6 +32,68 @@ later(function()
end end
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 = {}
-- Pattern: /path/file.R:10:5: style: Some message
for line in output:gmatch("[^\r\n]+") do
local path, lnum, col, severity, message = line:match("^[^:]+:(%d+):(%d+):%s*(%w+):%s*(.+)$")
if path then
local severity_map = {
style = vim.diagnostic.severity.INFO,
warning = vim.diagnostic.severity.WARN,
error = vim.diagnostic.severity.ERROR,
}
table.insert(diagnostics, {
bufnr = bufnr,
lnum = math.max(0, tonumber(lnum) - 1),
col = math.max(0, tonumber(col) - 1),
end_lnum = tonumber(lnum) - 1,
end_col = tonumber(col),
severity = severity_map[severity:lower()] or vim.diagnostic.severity.WARN,
message = message or "lintr issue",
source = "lintr",
})
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)
-- Markdown -- Markdown
now_if_args(function() now_if_args(function()
add("render-markdown.nvim") add("render-markdown.nvim")

View file

@ -1,3 +1,4 @@
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

View file

@ -1,3 +1,4 @@
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
@ -16,6 +17,73 @@ now_if_args(function()
marksman = { marksman = {
filetypes = { "markdown", "markdown_inline", "codecompanion" }, 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,
},
julials = { julials = {
settings = { settings = {
julia = { julia = {

79
plugin/26_dap.lua Normal file
View file

@ -0,0 +1,79 @@
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)

50
plugin/27_image.lua Normal file
View file

@ -0,0 +1,50 @@
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)

38
plugin/28_latex.lua Normal file
View file

@ -0,0 +1,38 @@
-- 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)