From 7f01be59d7a3793513402c57d6069e6488c8756f Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:35:52 +0000 Subject: [PATCH 01/12] 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 `` 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` -> `db`, `dc` confirm DAP loads. 5. Open a `.tex` -> confirm `texlab` + `vimtex` are active. --- init.lua | 11 +- lua/config/init.lua | 10 + lua/keymap/core.lua | 44 +++ lua/keymap/helpers.lua | 25 ++ lua/keymap/leader.lua | 260 +++++++++++++++++ lua/keymap/repl.lua | 83 ++++++ lua/keymap/terminal.lua | 17 ++ lua/nix_smart_send.lua | 204 ++++++------- modules/module/settings/cat-packages.nix | 11 +- modules/module/settings/lang-packages.nix | 2 + modules/module/specs/plugins.nix | 87 +++--- plugin/01_lib.lua | 2 + plugin/02_startup.lua | 2 + plugin/03_terminal.lua | 53 +++- plugin/04_treesitter.lua | 52 +++- plugin/10_keymap.lua | 330 +--------------------- plugin/20_startup.lua | 1 + plugin/22_languages.lua | 62 ++++ plugin/24_completion.lua | 1 + plugin/25_lsp.lua | 35 +++ plugin/26_dap.lua | 79 ++++++ plugin/27_image.lua | 46 +++ plugin/28_latex.lua | 43 +++ 23 files changed, 961 insertions(+), 499 deletions(-) create mode 100644 lua/config/init.lua create mode 100644 lua/keymap/core.lua create mode 100644 lua/keymap/helpers.lua create mode 100644 lua/keymap/leader.lua create mode 100644 lua/keymap/repl.lua create mode 100644 lua/keymap/terminal.lua create mode 100644 plugin/26_dap.lua create mode 100644 plugin/27_image.lua create mode 100644 plugin/28_latex.lua diff --git a/init.lua b/init.lua index 8db9da5..11b9975 100644 --- a/init.lua +++ b/init.lua @@ -1,7 +1,5 @@ -_G.Config = {} -local nix = require('config.nix').init { non_nix_value = true } -Config.isNixCats = nix.is_nix -Config.nixConfig = nix +local Config = require('config') +_G.Config = Config -- keep global alias for keymaps/backward compatibility 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' if not vim.uv.fs_stat(mini_path) then 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 = { - 'git', 'clone', '--filter=blob:none', + 'git', 'clone', '--filter=blob:none', '--branch=' .. mini_nvim_tag, 'https://github.com/echasnovski/mini.nvim', mini_path } vim.fn.system(clone_cmd) diff --git a/lua/config/init.lua b/lua/config/init.lua new file mode 100644 index 0000000..60b80e5 --- /dev/null +++ b/lua/config/init.lua @@ -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 diff --git a/lua/keymap/core.lua b/lua/keymap/core.lua new file mode 100644 index 0000000..1314b06 --- /dev/null +++ b/lua/keymap/core.lua @@ -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', '', [[h]]) +-- Select all +-- vim.keymap.set({ "n", "v", "x" }, "", "gg3vG$", { noremap = true, silent = true, desc = "Select all" }) +-- Escape deletes highlights +vim.keymap.set("n", "", "nohlsearch") +-- Paste before/after linewise +local cmd = vim.fn.has('nvim-0.12') == 1 and 'iput' or 'put' +vim.keymap.set({ 'n', 'x' }, '[p', 'exe "' .. cmd .. '! " . v:register', { desc = 'Paste Above' }) +vim.keymap.set({ 'n', 'x' }, ']p', 'exe "' .. cmd .. ' " . v:register', { desc = 'Paste Below' }) + +vim.keymap.set({ "n", "v", "x" }, "p", '"+p', { noremap = true, silent = true, desc = "Paste from clipboard" }) +vim.keymap.set({ "n", "v", "x" }, "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 = 'a', desc = '+AI' }, + { mode = 'n', keys = 'b', desc = '+Buffer' }, + { mode = 'n', keys = 'e', desc = '+Explore' }, + { mode = 'n', keys = 'f', desc = '+Find' }, + { mode = 'n', keys = 'fl', desc = '+LSP' }, + { mode = 'n', keys = 'fa', desc = '+Git' }, + { mode = 'n', keys = 'g', desc = '+Git' }, + { mode = 'n', keys = 'l', desc = '+LSP' }, + { mode = 'n', keys = 'L', desc = '+Lua/Log' }, + { mode = 'n', keys = 'o', desc = '+Other' }, + { mode = 'n', keys = 'r', desc = '+R' }, + { mode = 'n', keys = 's', desc = '+Send' }, + { mode = 'n', keys = 'd', desc = '+Debug' }, + { mode = 'n', keys = 't', desc = '+Terminal' }, + { mode = 'n', keys = 'u', desc = '+UI' }, + { mode = 'n', keys = 'v', desc = '+Visits' }, + { mode = 'n', keys = 'w', desc = '+Windows' }, + { mode = 'x', keys = 'l', desc = '+LSP' }, + { mode = 'x', keys = 'r', desc = '+R' }, + { mode = 'n', keys = 'z', desc = '+ZK' }, + { mode = 'n', keys = 'zr', desc = '+Reviews' }, + { mode = 'x', keys = 'a', desc = '+AI' }, +} diff --git a/lua/keymap/helpers.lua b/lua/keymap/helpers.lua new file mode 100644 index 0000000..10cdaa2 --- /dev/null +++ b/lua/keymap/helpers.lua @@ -0,0 +1,25 @@ +local M = {} + +---Create a normal-mode `` mapping. +function M.nmap_leader(suffix, rhs, desc, opts) + opts = opts or {} + opts.desc = desc + vim.keymap.set('n', '' .. suffix, rhs, opts) +end + +---Create a visual-mode `` mapping. +function M.xmap_leader(suffix, rhs, desc, opts) + opts = opts or {} + opts.desc = desc + vim.keymap.set('x', '' .. 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 diff --git a/lua/keymap/leader.lua b/lua/keymap/leader.lua new file mode 100644 index 0000000..2543212 --- /dev/null +++ b/lua/keymap/leader.lua @@ -0,0 +1,260 @@ +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('', 'bnext', 'Next buffer') +nmap_leader('', 'bprev', 'Prev buffer') + +-- a is for 'AI' +nmap_leader("aa", "CodeCompanion /agent", "Agent chat (@{agent} tools)") +nmap_leader("ac", "CodeCompanionChat Toggle", "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", "CodeCompanion /commit", "Generate commit message") +nmap_leader("ai", "CodeCompanionActions", "Chat Action") +nmap_leader("al", "CodeCompanion /lsp", "Explain LSP Diagnostics") +nmap_leader("an", "CodeCompanionChat Add", "Chat New") +nmap_leader("as", "CodeCompanion /suggest", "Suggest Improvements") +nmap_leader("aw", "CodeCompanion /tdd", "Workflow: plan, implement, test") +nmap_leader("ax", "CodeCompanion /fixer", "Code Fixer") +xmap_leader("aa", "CodeCompanion /agent", "Agent on selection") +xmap_leader("ae", "CodeCompanion /explain", "Explain Code") +xmap_leader("af", "CodeCompanion /fix", "Fix Code") +xmap_leader("ap", "CodeCompanion /expert", "Code Expert") +xmap_leader("as", "CodeCompanion /suggest", "Suggest Improvements") +nmap_leader("ak", "CodeCompanionChat adapter=codex", "Chat with Codex") + +-- b is for 'buffer' +nmap_leader('bb', 'b#', 'Alternate') +nmap_leader('bd', 'lua MiniBufremove.delete()', 'Delete') +nmap_leader('bD', 'lua MiniBufremove.delete(0, true)', 'Delete!') +nmap_leader('bs', 'lua Config.new_scratch_buffer()', 'Scratch') +nmap_leader('bw', 'lua MiniBufremove.wipeout()', 'Wipeout') +nmap_leader('bW', 'lua MiniBufremove.wipeout(0, true)', 'Wipeout!') +nmap_leader('bq', 'qall', 'Quit all') + +-- e is for 'explore' and 'edit' +nmap_leader('ed', 'lua MiniFiles.open()', 'Directory') +nmap_leader('ef', 'lua Config.try_opendir()', 'File directory') +nmap_leader('es', 'lua MiniSessions.select()', 'Sessions') +nmap_leader('eq', 'lua Config.toggle_quickfix()', 'Quickfix') +nmap_leader('ez', 'lua MiniFiles.open(os.getenv("ZK_NOTEBOOK_DIR"))', 'Notes directory') + +-- f is for 'fuzzy find' +nmap_leader('f/', 'Pick history scope="/"', '"/" history') +nmap_leader('f:', 'Pick history scope=":"', '":" history') +nmap_leader('f,', 'Pick visit_labels', 'Visit labels') +nmap_leader('faa', 'Pick git_hunks scope="staged"', 'Added hunks (all)') +nmap_leader('faA', 'Pick git_hunks path="%" scope="staged"', 'Added hunks (current)') +nmap_leader('fb', 'Pick buffers', 'Buffers') +nmap_leader(',', 'Pick buffers', 'Buffers') +nmap_leader('fac', 'Pick git_commits', 'Commits (all)') +nmap_leader('faC', 'Pick git_commits path="%"', 'Commits (current)') +nmap_leader('fd', 'Pick diagnostic scope="all"', 'Diagnostic workspace') +nmap_leader('fD', 'Pick diagnostic scope="current"', 'Diagnostic buffer') +nmap_leader('ff', 'Pick files', 'Files') +nmap_leader('fg', 'Pick grep_live', 'Grep live') +nmap_leader('fG', 'Pick grep pattern=""', 'Grep current word') +nmap_leader('fh', 'Pick help', 'Help tags') +nmap_leader('fH', 'Pick hl_groups', 'Highlight groups') +nmap_leader('fj', 'Pick buf_lines scope="all"', 'Lines (all)') +nmap_leader('fJ', 'Pick buf_lines scope="current"', 'Lines (current)') +nmap_leader('fam', 'Pick git_hunks', 'Modified hunks (all)') +nmap_leader('faM', 'Pick git_hunks path="%"', 'Modified hunks (current)') +nmap_leader('fm', 'Pick marks', 'Marks') +nmap_leader('fn', 'ZkNotes', "Notes") +nmap_leader('fk', 'Pick keymaps', 'Keymaps') +nmap_leader('fR', 'Pick resume', 'Resume') +nmap_leader('fp', 'Pick files', 'Files') +nmap_leader('fq', 'Pick list scope="quickfix"', 'Quickfix') +nmap_leader('fr', 'Pick lsp scope="references"', 'References (LSP)') +nmap_leader('flr', 'Pick lsp scope="references"', 'References (LSP)') +nmap_leader('fS', 'Pick lsp scope="workspace_symbol"', 'Symbols workspace (LSP)') +nmap_leader('flS', 'Pick lsp scope="workspace_symbol"', 'Symbols workspace (LSP)') +nmap_leader('fs', 'Pick lsp scope="document_symbol"', 'Symbols buffer (LSP)') +nmap_leader('fls', 'Pick lsp scope="document_symbol"', 'Symbols buffer (LSP)') +nmap_leader('fld', 'Pick lsp scope="definition"', 'Definition (LSP)') +nmap_leader('flD', 'Pick lsp scope="declaration"', 'Declaration (LSP)') +nmap_leader('flt', 'Pick lsp scope="type_definition"', 'Type Definition (LSP)') +nmap_leader('fv', 'Pick visit_paths cwd=""', 'Visit paths (all)') +nmap_leader('fV', 'Pick visit_paths', 'Visit paths (cwd)') + +-- g is for git +local git_log_cmd = [[Git log --pretty=format:\%h\ \%as\ │\ \%s --topo-order]] + +nmap_leader('gc', 'Git commit', 'Commit') +nmap_leader('gC', 'Git commit --amend', 'Commit amend') +nmap_leader('gd', 'Git diff', 'Diff') +nmap_leader('gD', 'Git diff -- %', 'Diff buffer') +nmap_leader("gg", "Neogit", "Open Neogit UI") +nmap_leader('gl', '' .. git_log_cmd .. '', 'Log') +nmap_leader('gL', '' .. git_log_cmd .. ' --follow -- %', 'Log buffer') +nmap_leader('go', 'lua MiniDiff.toggle_overlay()', 'Toggle overlay') +nmap_leader('gp', 'Git pull', 'Pull') +nmap_leader('gP', 'Git push', 'Push') +nmap_leader('gs', 'lua MiniGit.show_at_cursor()', 'Show at cursor') + +xmap_leader('gs', 'lua MiniGit.show_at_cursor()', 'Show at selection') + +-- j/k navigate quickfix +nmap_leader("j", 'cnextzz', "Quickfix next") +nmap_leader("k", 'cprevzz', "Quickfix prev") + +-- l is for 'LSP' (Language Server Protocol) +vim.keymap.set({ 'n' }, 'grd', 'lua vim.lsp.buf.definition()', { desc = 'Definition' }) +vim.keymap.set({ 'n' }, 'grk', 'lua vim.lsp.buf.hover()', { desc = 'Documentation' }) +vim.keymap.set({ 'n' }, 'gre', 'lua vim.diagnostic.open_float()', { desc = 'Diagnostics' }) + +nmap_lsp("K", 'lua vim.lsp.buf.hover()', "Documentation") +local formatting_cmd = 'lua require("conform").format({ lsp_format = "fallback" })' +nmap_leader('la', 'lua vim.lsp.buf.code_action()', 'Actions') +nmap_leader('le', 'lua vim.diagnostic.open_float()', 'Diagnostics popup') +nmap_leader('lf', formatting_cmd, 'Format') +nmap_leader('lk', 'lua vim.lsp.buf.hover()', 'Documentation') +nmap_leader('li', 'lua vim.lsp.buf.implementation()', 'Information') +-- use ]d and [d +--nmap_leader('lj', 'lua vim.diagnostic.goto_next()', 'Next diagnostic') +--nmap_leader('lk', 'lua vim.diagnostic.goto_prev()', 'Prev diagnostic') +nmap_leader('lR', 'lua vim.lsp.buf.references()', 'References') +nmap_leader('lr', 'lua vim.lsp.buf.rename()', 'Rename') +nmap_leader('ls', 'lua vim.lsp.buf.definition()', 'Source definition') + +xmap_leader('lf', formatting_cmd, 'Format selection') + +-- L is for 'Lua' +nmap_leader('Lc', 'lua Config.log_clear()', 'Clear log') +nmap_leader('LL', 'luafile %echo "Sourced lua"', 'Source buffer') +nmap_leader('Ls', 'lua Config.log_print()', 'Show log') +nmap_leader('Lx', 'lua Config.execute_lua_line()', 'Execute `lua` line') + +-- m is free + +-- o is for 'other' +local trailspace_toggle_command = 'lua vim.b.minitrailspace_disable = not vim.b.minitrailspace_disable' +nmap_leader('oh', 'normal gxiagxila', 'Move arg left') +nmap_leader('ol', 'normal gxiagxina', 'Move arg right') +nmap_leader('or', 'lua MiniMisc.resize_window()', 'Resize to default width') +nmap_leader('ot', 'lua MiniTrailspace.trim()', 'Trim trailspace') +nmap_leader('oT', trailspace_toggle_command, 'Trailspace hl toggle') +nmap_leader('oz', 'lua MiniMisc.zoom()', 'Zoom toggle') +nmap_leader('ow', + "lua MiniSessions.write(vim.fn.input('Session name: ', string.match(vim.fn.getcwd(), \"[^/]+$\") .. '-session.vim'))", + 'Write session') + +-- r is for 'R' +nmap_leader('rc', 'RSend devtools::check()', 'Check') +nmap_leader('rC', 'RSend devtools::test_coverage()', 'Coverage') +nmap_leader('rd', 'RSend devtools::document()', 'Document') +nmap_leader('ri', 'RSend devtools::install(keep_source=TRUE)', 'Install') +nmap_leader('rk', 'RSend quarto::quarto_preview("%")', 'Knit file') +nmap_leader('rl', 'RSend devtools::load_all()', 'Load all') +nmap_leader('rL', 'RSend devtools::load_all(recompile=TRUE)', 'Load all recompile') +nmap_leader('rm', 'RSend Rcpp::compileAttributes()', 'Run examples') +nmap_leader('rT', 'RSend testthat::test_file("%")', 'Test file') +nmap_leader('rt', 'RSend devtools::test()', 'Test') + +-- - Copy to clipboard and make reprex (which itself is loaded to clipboard) +xmap_leader('rx', '"+y :RSend reprex::reprex()', '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', 'lua require("dap").toggle_breakpoint()', 'Toggle breakpoint') +nmap_leader('dB', 'lua require("dap").set_breakpoint(vim.fn.input("Condition: "))', 'Conditional breakpoint') +nmap_leader('dc', 'lua require("dap").continue()', 'Continue') +nmap_leader('do', 'lua require("dap").step_over()', 'Step over') +nmap_leader('di', 'lua require("dap").step_into()', 'Step into') +nmap_leader('dO', 'lua require("dap").step_out()', 'Step out') +nmap_leader('dr', 'lua require("dap").repl.open()', 'Open DAP REPL') +nmap_leader('du', 'lua require("dapui").toggle()', 'Toggle DAP UI') +nmap_leader('dK', 'lua require("dapui").eval()', 'Evaluate expression') + +-- u is for UI +nmap_leader('ut', 'TSContext toggle', 'Toggle TScontext') +nmap_leader('ua', 'Copilot toggle', 'Toggle AI completion') + +-- v is for 'visits' +nmap_leader('vv', 'lua MiniVisits.add_label("core")', 'Add "core" label') +nmap_leader('vV', 'lua MiniVisits.remove_label("core")', 'Remove "core" label') +nmap_leader('vl', 'lua MiniVisits.add_label()', 'Add label') +nmap_leader('vL', 'lua MiniVisits.remove_label()', '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", "h", "Go to Left Window", { remap = true }) +nmap_leader("wj", "j", "Go to Lower Window", { remap = true }) +nmap_leader("wk", "k", "Go to Upper Window", { remap = true }) +nmap_leader("wl", "l", "Go to Right Window", { remap = true }) + +nmap_leader("_", "s", "Split Window Below", { remap = true }) +nmap_leader("|", "v", "Split Window Right", { remap = true }) +nmap_leader("wd", "c", "Delete Window", { remap = true }) +nmap_leader("wo", "o", "Delete Other Windows", { remap = true }) + +-- z is for 'ZettelKasten' +nmap_leader("zo", 'ZkNotes', "Notes") +nmap_leader("zt", 'ZkTags', "Tags") + +nmap_leader( + "zrd", + 'ZkNew { group = "dreviews" }', + "Daily Review" +) +nmap_leader( + "zrw", + 'ZkNew { group = "wreviews" }', + "Weekly Review" +) +nmap_leader( + "zn", + 'ZkNew { group = "inbox", title = vim.fn.input("Title: ") }', + "New" +) +nmap_leader( + "zp", + "ZkNew { group = 'permanent', title = vim.fn.input('Title: ') }", + "Permanent" +) + +nmap_leader( + "zl", + "ZkNew { group = 'literature', title = vim.fn.input('Title: '), extra.author = vim.fn.input('Author: '), extra.year = vim.fn.input('Year: ') }", + "Literature" +) + +nmap_leader( + "zd", + "ZkNew { group = 'dashboard', title = vim.fn.input('Title: ') }", + "Dashboard" +) +nmap_leader( + "zP", + "ZkNew { group = 'project', title = vim.fn.input('Title: ')}", + "Project" +) +-- stylua: ignore end diff --git a/lua/keymap/repl.lua b/lua/keymap/repl.lua new file mode 100644 index 0000000..f43cb9b --- /dev/null +++ b/lua/keymap/repl.lua @@ -0,0 +1,83 @@ +--- 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; fall back to the legacy mappings + -- if a v0.x build is still in use. + local ok, rrun = pcall(require, "r.run") + if ok and rrun and type(rrun.send_line) == "function" then + rrun.send_line() + return + end + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes("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). + vim.cmd("SlimeSendCurrentLine") + -- 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; fall back to if unavailable. + local ok, rrun = pcall(require, "r.run") + if ok and rrun and type(rrun.send_selection) == "function" then + rrun.send_selection() + return + end + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes("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("SlimeRegionSend", true, false, true), + "m", + false + ) +end + +return M diff --git a/lua/keymap/terminal.lua b/lua/keymap/terminal.lua new file mode 100644 index 0000000..bb89596 --- /dev/null +++ b/lua/keymap/terminal.lua @@ -0,0 +1,17 @@ +local Config = require('config') +local helpers = require('keymap.helpers') +local nmap_leader = helpers.nmap_leader + +-- Exit terminal insert mode with +vim.keymap.set("t", "", [[]], { desc = "Exit terminal mode" }) + +-- t is for 'terminal' +nmap_leader("tc", 'lua Config.terminal.open_clickhouse_client()', 'Open Clickhouse client') +nmap_leader("tl", 'lua Config.terminal.open_clickhouse_local()', 'Open Clickhouse local') +nmap_leader("tp", 'lua Config.terminal.open_python()', 'Open Python') +nmap_leader("tj", 'lua Config.terminal.open_julia()', 'Open Julia') +nmap_leader("td", 'lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()', 'Open DuckDB') +nmap_leader("tx", 'lua Config.terminal.open_in_terminal()', 'Terminal Command') +nmap_leader("tt", 'lua Config.terminal.open_shell()', 'Terminal') +nmap_leader("tb", 'lua Config.terminal.toggle_bracket()', 'Toggle bracketed paste') +nmap_leader("up", 'lua Config.terminal.toggle_bracket()', 'Toggle bracketed paste') diff --git a/lua/nix_smart_send.lua b/lua/nix_smart_send.lua index 1f82383..7906d25 100644 --- a/lua/nix_smart_send.lua +++ b/lua/nix_smart_send.lua @@ -1,5 +1,12 @@ 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) local function is_in_list(list, value) if not list or not value then @@ -14,83 +21,84 @@ local function is_in_list(list, value) return false end --- Define comment node types as constants -local COMMENT_TYPES = { - comment = true, - block_comment = true, - line_comment = true, -} - +-- Safely get the Tree-sitter node under the cursor. function M.get_current_node() - local cur_win = vim.api.nvim_get_current_win() - return vim.treesitter.get_node({ - winid = cur_win, - ignore_injections = true, - }) + local ok, node = pcall(vim.treesitter.get_node, { ignore_injections = false }) + return ok and node or nil end +-- Detect the root node type of the current buffer's Tree-sitter tree. function M.detect_global_node() local cur_node = M.get_current_node() local root if not cur_node then - -- print("No node detected") - local parser = vim.treesitter.get_parser() - if not parser then + local ok, parser = pcall(vim.treesitter.get_parser) + if not ok or not parser then return nil 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 root = cur_node:root() 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 end - return root:type() -end - -function M.move_to_next_non_empty_line() - -- 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 + while node do + local parent = node:parent() + if not parent then + break 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 = vim.treesitter.get_node() + 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 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 + + 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 function M.vselect_node(node) @@ -108,57 +116,20 @@ function M.vselect_node(node) end function M.select_until_global(global_nodes) - 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 = vim.treesitter.get_node() - if not node then - -- print("No syntax node found at cursor position") + local target = get_target_node(global_nodes) + if not target then return nil end - local node_type = node:type() - - 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 + if M.vselect_node(target) then + return target end return nil end function M.slime_send_region() - -- Check if slime plugin is available - if not vim.fn.exists('*slime#send_op') then + if vim.fn.exists('*slime#send_op') == 0 then vim.notify("slime plugin not available", vim.log.levels.ERROR) return end @@ -170,31 +141,32 @@ function M.slime_send_region() end function M.send_repl(global_nodes) - 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") + local target_node = get_target_node(global_nodes) + if not target_node then return 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. + 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 + + -- Select the target node and send it to the REPL. + if not M.vselect_node(target_node) then + return + end M.slime_send_region() - -- Move cursor and continue - local _, _, er, ec = sel_node:range() + -- Place cursor at end of visual block + local _, _, er, ec = target_node:range() vim.api.nvim_win_set_cursor(0, { er + 1, ec }) - M.move_to_next_non_empty_line() + -- Jump to the next relevant AST node instead of scanning lines + M.move_to_next_non_empty_line(target_node) end return M diff --git a/modules/module/settings/cat-packages.nix b/modules/module/settings/cat-packages.nix index 2709ca0..49a33e6 100644 --- a/modules/module/settings/cat-packages.nix +++ b/modules/module/settings/cat-packages.nix @@ -5,6 +5,10 @@ ... }: let + -- Include packages from a category only if that category is enabled. + -- NOTE: The package list expression is still evaluated (packages in Nix are + -- lazy by default, so derivations are not built), 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; @@ -26,6 +30,7 @@ in clickhouse = maybe "clickhouse" (with pkgs; [ clickhouse-lts ]); external = maybe "external" (with pkgs; [ + nodejs perl ruby shfmt @@ -40,10 +45,13 @@ in lua = maybe "lua" (with pkgs; [ lua-language-server ]); markdown = maybe "markdown" (with pkgs; [ - python313Packages.pylatexenc + python3Packages.pylatexenc quartoPkg zk marksman + texlab + imagemagick + luaPackages.magick ]); nix = maybe "nix" (with pkgs; [ @@ -88,7 +96,6 @@ in in with pkgs; [ python_with_packages - nodejs ruff basedpyright uv diff --git a/modules/module/settings/lang-packages.nix b/modules/module/settings/lang-packages.nix index a91d9a5..97ec4e7 100644 --- a/modules/module/settings/lang-packages.nix +++ b/modules/module/settings/lang-packages.nix @@ -44,6 +44,8 @@ data_table janitor styler + vscDebugger + lintr ]) ); julia = lib.mkDefault [ diff --git a/modules/module/specs/plugins.nix b/modules/module/specs/plugins.nix index 63d32d0..4a3324a 100644 --- a/modules/module/specs/plugins.nix +++ b/modules/module/specs/plugins.nix @@ -3,7 +3,53 @@ pkgs, 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" + "stata" + "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) { data = []; @@ -94,6 +140,7 @@ data = with pkgs.vimPlugins; [ quarto-nvim render-markdown-nvim + vimtex { data = otter-nvim; pname = "otter"; @@ -125,42 +172,7 @@ }; config.specs.treesitterParsers = lib.mkIf (config.cats.treesitterParsers or false) { - 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 - ]; + data = map (name: pkgs.vimPlugins.nvim-treesitter-parsers.${name}) config.settings.treesitter_parsers; }; config.specs.utils-lazy = lib.mkIf (config.cats.utils or false) { @@ -177,6 +189,7 @@ nvim-dap-virtual-text nvim-lint vim-slime + image-nvim ]; }; diff --git a/plugin/01_lib.lua b/plugin/01_lib.lua index 64bd37e..ba87825 100644 --- a/plugin/01_lib.lua +++ b/plugin/01_lib.lua @@ -1,3 +1,5 @@ +local Config = require('config') + -- Global Functions Config.new_scratch_buffer = function() vim.api.nvim_win_set_buf(0, vim.api.nvim_create_buf(true, true)) end diff --git a/plugin/02_startup.lua b/plugin/02_startup.lua index c4fa2a9..b4d1c7e 100644 --- a/plugin/02_startup.lua +++ b/plugin/02_startup.lua @@ -1,3 +1,5 @@ +local Config = require('config') + local M = {} -- Helper function to normalize input to a list diff --git a/plugin/03_terminal.lua b/plugin/03_terminal.lua index 95d25b1..9408760 100644 --- a/plugin/03_terminal.lua +++ b/plugin/03_terminal.lua @@ -1,3 +1,5 @@ +local Config = require('config') + local M = {} -- Configuration @@ -31,11 +33,15 @@ function M.split_and_open_terminal() vim.cmd("resize " .. math.floor(vim.fn.winheight(0) * 0.9)) local term_buf = vim.api.nvim_win_get_buf(vim.api.nvim_get_current_win()) M.opt_term = term_buf - + -- Set buffer-local variables for vim-slime 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 } - + return M.opt_term end @@ -44,26 +50,45 @@ function M.open_in_terminal(cmd) local command = cmd or "" local current_window = vim.api.nvim_get_current_win() 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 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 + local job_id = vim.b[term_buf].terminal_job_id if command ~= "" then - -- We can use standard slime sending if needed, or direct chan_send for initialization - local job_id = vim.b[term_buf].terminal_job_id - if job_id then - vim.api.nvim_chan_send(job_id, command .. "\r") + if not job_id then + vim.notify("Terminal job not ready, cannot send command", vim.log.levels.WARN) + else + 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 - + -- Configure slime for the ORIGINAL code buffer to point to this new terminal -- This makes "Send to Terminal" work immediately - local slime_config = { jobid = vim.b[term_buf].terminal_job_id } - - -- 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) - + if job_id then + local slime_config = { jobid = job_id } + + -- 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) + 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 vim.api.nvim_set_current_win(current_window) end diff --git a/plugin/04_treesitter.lua b/plugin/04_treesitter.lua index 5a8d315..c4c0452 100644 --- a/plugin/04_treesitter.lua +++ b/plugin/04_treesitter.lua @@ -1,3 +1,5 @@ +local Config = require('config') + local M = {} -- Default parsers list moved from startup config @@ -93,12 +95,12 @@ end function M.get_type() local cur_node = smart_send.get_current_node() if not cur_node then - print("Not a node") + vim.notify("No Tree-sitter node under cursor", vim.log.levels.WARN) return nil end local node_type = cur_node:type() - print("Node type: " .. node_type) + vim.notify("Node type: " .. node_type, vim.log.levels.INFO) return node_type end @@ -134,8 +136,8 @@ function M.setup_keybindings(global_nodes) { noremap = true, silent = true, desc = "Remove node under cursor from globals", buffer = true }) vim.keymap.set('n', 'o', function() - pout = table.concat(global_nodes, ', ') .. "" - print(pout) + local pout = table.concat(global_nodes, ', ') + vim.notify("global_nodes: " .. pout, vim.log.levels.INFO) end, { noremap = true, silent = true, desc = "Print globals", buffer = true }) vim.keymap.set('n', 'p', function() M.get_type() end, @@ -144,4 +146,46 @@ end 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 diff --git a/plugin/10_keymap.lua b/plugin/10_keymap.lua index 8ae0a1c..9e88a27 100644 --- a/plugin/10_keymap.lua +++ b/plugin/10_keymap.lua @@ -1,323 +1,11 @@ --- 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', '', [[h]]) --- Select all --- vim.keymap.set({ "n", "v", "x" }, "", "gg3vG$", { noremap = true, silent = true, desc = "Select all" }) --- Escape deletes highlights -vim.keymap.set("n", "", "nohlsearch") --- Paste before/after linewise -local cmd = vim.fn.has('nvim-0.12') == 1 and 'iput' or 'put' -vim.keymap.set({ 'n', 'x' }, '[p', 'exe "' .. cmd .. '! " . v:register', { desc = 'Paste Above' }) -vim.keymap.set({ 'n', 'x' }, ']p', 'exe "' .. cmd .. ' " . v:register', { desc = 'Paste Below' }) +local Config = require('config') -vim.keymap.set({ "n", "v", "x" }, "p", '"+p', { noremap = true, silent = true, desc = "Paste from clipboard" }) -vim.keymap.set({ "n", "v", "x" }, "y", '"+y', { noremap = true, silent = true, desc = "Copy toclipboard" }) --- Leader mappings ============================================================ --- stylua: ignore start +-- Domain-specific keymap modules. Core must load first because it defines the +-- leader clue groups used by the mini.clue setup in the startup plugins. +require('keymap.core') +require('keymap.leader') +require('keymap.terminal') --- Create global tables with information about clue groups in certain modes --- Structure of tables is taken to be compatible with 'mini.clue'. -_G.Config.leader_group_clues = { - { mode = 'n', keys = 'a', desc = '+AI' }, - { mode = 'n', keys = 'b', desc = '+Buffer' }, - { mode = 'n', keys = 'e', desc = '+Explore' }, - { mode = 'n', keys = 'f', desc = '+Find' }, - { mode = 'n', keys = 'fl', desc = '+LSP' }, - { mode = 'n', keys = 'fa', desc = '+Git' }, - { mode = 'n', keys = 'g', desc = '+Git' }, - { mode = 'n', keys = 'l', desc = '+LSP' }, - { mode = 'n', keys = 'L', desc = '+Lua/Log' }, - { mode = 'n', keys = 'o', desc = '+Other' }, - { mode = 'n', keys = 'r', desc = '+R' }, - { mode = 'n', keys = 't', desc = '+Terminal' }, - { mode = 'n', keys = 'u', desc = '+UI' }, - { mode = 'n', keys = 'v', desc = '+Visits' }, - { mode = 'n', keys = 'w', desc = '+Windows' }, - { mode = 'x', keys = 'l', desc = '+LSP' }, - { mode = 'x', keys = 'r', desc = '+R' }, - { mode = 'n', keys = 'z', desc = '+ZK' }, - { mode = 'n', keys = 'zr', desc = '+Reviews' }, - { mode = 'x', keys = 'a', desc = '+AI' }, -} - --- Create `` mappings -local nmap_leader = function(suffix, rhs, desc, opts) - opts = opts or {} - opts.desc = desc - vim.keymap.set('n', '' .. suffix, rhs, opts) -end -local xmap_leader = function(suffix, rhs, desc, opts) - opts = opts or {} - opts.desc = desc - vim.keymap.set('x', '' .. 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('', 'bnext', 'Next buffer') -nmap_leader('', 'bprev', 'Prev buffer') - --- a is for 'AI' -nmap_leader("aa", "CodeCompanion /agent", "Agent chat (@{agent} tools)") -nmap_leader("ac", "CodeCompanionChat Toggle", "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", "CodeCompanion /commit", "Generate commit message") -nmap_leader("ai", "CodeCompanionActions", "Chat Action") -nmap_leader("al", "CodeCompanion /lsp", "Explain LSP Diagnostics") -nmap_leader("an", "CodeCompanionChat Add", "Chat New") -nmap_leader("as", "CodeCompanion /suggest", "Suggest Improvements") -nmap_leader("aw", "CodeCompanion /tdd", "Workflow: plan, implement, test") -nmap_leader("ax", "CodeCompanion /fixer", "Code Fixer") -xmap_leader("aa", "CodeCompanion /agent", "Agent on selection") -xmap_leader("ae", "CodeCompanion /explain", "Explain Code") -xmap_leader("af", "CodeCompanion /fix", "Fix Code") -xmap_leader("ap", "CodeCompanion /expert", "Code Expert") -xmap_leader("as", "CodeCompanion /suggest", "Suggest Improvements") -nmap_leader("ak", "CodeCompanionChat adapter=codex", "Chat with Codex") - --- b is for 'buffer' -nmap_leader('bb', 'b#', 'Alternate') -nmap_leader('bd', 'lua MiniBufremove.delete()', 'Delete') -nmap_leader('bD', 'lua MiniBufremove.delete(0, true)', 'Delete!') -nmap_leader('bs', 'lua Config.new_scratch_buffer()', 'Scratch') -nmap_leader('bw', 'lua MiniBufremove.wipeout()', 'Wipeout') -nmap_leader('bW', 'lua MiniBufremove.wipeout(0, true)', 'Wipeout!') -nmap_leader('bq', 'qall', 'Quit all') - --- e is for 'explore' and 'edit' -nmap_leader('ed', 'lua MiniFiles.open()', 'Directory') -nmap_leader('ef', 'lua Config.try_opendir()', 'File directory') -nmap_leader('es', 'lua MiniSessions.select()', 'Sessions') -nmap_leader('eq', 'lua Config.toggle_quickfix()', 'Quickfix') -nmap_leader('ez', 'lua MiniFiles.open(os.getenv("ZK_NOTEBOOK_DIR"))', 'Notes directory') - --- f is for 'fuzzy find' -nmap_leader('f/', 'Pick history scope="/"', '"/" history') -nmap_leader('f:', 'Pick history scope=":"', '":" history') -nmap_leader('f,', 'Pick visit_labels', 'Visit labels') -nmap_leader('faa', 'Pick git_hunks scope="staged"', 'Added hunks (all)') -nmap_leader('faA', 'Pick git_hunks path="%" scope="staged"', 'Added hunks (current)') -nmap_leader('fb', 'Pick buffers', 'Buffers') -nmap_leader(',', 'Pick buffers', 'Buffers') -nmap_leader('fac', 'Pick git_commits', 'Commits (all)') -nmap_leader('faC', 'Pick git_commits path="%"', 'Commits (current)') -nmap_leader('fd', 'Pick diagnostic scope="all"', 'Diagnostic workspace') -nmap_leader('fD', 'Pick diagnostic scope="current"', 'Diagnostic buffer') -nmap_leader('ff', 'Pick files', 'Files') -nmap_leader('fg', 'Pick grep_live', 'Grep live') -nmap_leader('fG', 'Pick grep pattern=""', 'Grep current word') -nmap_leader('fh', 'Pick help', 'Help tags') -nmap_leader('fH', 'Pick hl_groups', 'Highlight groups') -nmap_leader('fj', 'Pick buf_lines scope="all"', 'Lines (all)') -nmap_leader('fJ', 'Pick buf_lines scope="current"', 'Lines (current)') -nmap_leader('fam', 'Pick git_hunks', 'Modified hunks (all)') -nmap_leader('faM', 'Pick git_hunks path="%"', 'Modified hunks (current)') -nmap_leader('fm', 'Pick marks', 'Marks') -nmap_leader('fn', 'ZkNotes', "Notes") -nmap_leader('fk', 'Pick keymaps', 'Keymaps') -nmap_leader('fR', 'Pick resume', 'Resume') -nmap_leader('fp', 'Pick files', 'Files') -nmap_leader('fq', 'Pick list scope="quickfix"', 'Quickfix') -nmap_leader('fr', 'Pick lsp scope="references"', 'References (LSP)') -nmap_leader('flr', 'Pick lsp scope="references"', 'References (LSP)') -nmap_leader('fS', 'Pick lsp scope="workspace_symbol"', 'Symbols workspace (LSP)') -nmap_leader('flS', 'Pick lsp scope="workspace_symbol"', 'Symbols workspace (LSP)') -nmap_leader('fs', 'Pick lsp scope="document_symbol"', 'Symbols buffer (LSP)') -nmap_leader('fls', 'Pick lsp scope="document_symbol"', 'Symbols buffer (LSP)') -nmap_leader('fld', 'Pick lsp scope="definition"', 'Definition (LSP)') -nmap_leader('flD', 'Pick lsp scope="declaration"', 'Declaration (LSP)') -nmap_leader('flt', 'Pick lsp scope="type_definition"', 'Type Definition (LSP)') -nmap_leader('fv', 'Pick visit_paths cwd=""', 'Visit paths (all)') -nmap_leader('fV', 'Pick visit_paths', 'Visit paths (cwd)') - --- g is for git -local git_log_cmd = [[Git log --pretty=format:\%h\ \%as\ │\ \%s --topo-order]] - -nmap_leader('gc', 'Git commit', 'Commit') -nmap_leader('gC', 'Git commit --amend', 'Commit amend') -nmap_leader('gd', 'Git diff', 'Diff') -nmap_leader('gD', 'Git diff -- %', 'Diff buffer') -nmap_leader("gg", "Neogit", "Open Neogit UI") -nmap_leader('gl', '' .. git_log_cmd .. '', 'Log') -nmap_leader('gL', '' .. git_log_cmd .. ' --follow -- %', 'Log buffer') -nmap_leader('go', 'lua MiniDiff.toggle_overlay()', 'Toggle overlay') -nmap_leader('gp', 'Git pull', 'Pull') -nmap_leader('gP', 'Git push', 'Push') -nmap_leader('gs', 'lua MiniGit.show_at_cursor()', 'Show at cursor') - -xmap_leader('gs', 'lua MiniGit.show_at_cursor()', 'Show at selection') - --- j/k navigate quickfix -nmap_leader("j", 'cnextzz', "Quickfix next") -nmap_leader("k", 'cprevzz', "Quickfix prev") - --- l is for 'LSP' (Language Server Protocol) -vim.keymap.set({ 'n' }, 'grd', 'lua vim.lsp.buf.definition()', { desc = 'Definition' }) -vim.keymap.set({ 'n' }, 'grk', 'lua vim.lsp.buf.hover()', { desc = 'Documentation' }) -vim.keymap.set({ 'n' }, 'gre', 'lua vim.diagnostic.open_float()', { desc = 'Diagnostics' }) - -nmap_lsp("K", 'lua vim.lsp.buf.hover()', "Documentation") -local formatting_cmd = 'lua require("conform").format({ lsp_format = "fallback" })' -nmap_leader('la', 'lua vim.lsp.buf.code_action()', 'Actions') -nmap_leader('le', 'lua vim.diagnostic.open_float()', 'Diagnostics popup') -nmap_leader('lf', formatting_cmd, 'Format') -nmap_leader('lk', 'lua vim.lsp.buf.hover()', 'Documentation') -nmap_leader('li', 'lua vim.lsp.buf.implementation()', 'Information') --- use ]d and [d ---nmap_leader('lj', 'lua vim.diagnostic.goto_next()', 'Next diagnostic') ---nmap_leader('lk', 'lua vim.diagnostic.goto_prev()', 'Prev diagnostic') -nmap_leader('lR', 'lua vim.lsp.buf.references()', 'References') -nmap_leader('lr', 'lua vim.lsp.buf.rename()', 'Rename') -nmap_leader('ls', 'lua vim.lsp.buf.definition()', 'Source definition') - -xmap_leader('lf', formatting_cmd, 'Format selection') - --- L is for 'Lua' -nmap_leader('Lc', 'lua Config.log_clear()', 'Clear log') -nmap_leader('LL', 'luafile %echo "Sourced lua"', 'Source buffer') -nmap_leader('Ls', 'lua Config.log_print()', 'Show log') -nmap_leader('Lx', 'lua Config.execute_lua_line()', 'Execute `lua` line') - --- m is free - --- o is for 'other' -local trailspace_toggle_command = 'lua vim.b.minitrailspace_disable = not vim.b.minitrailspace_disable' -nmap_leader('oh', 'normal gxiagxila', 'Move arg left') -nmap_leader('ol', 'normal gxiagxina', 'Move arg right') -nmap_leader('or', 'lua MiniMisc.resize_window()', 'Resize to default width') -nmap_leader('ot', 'lua MiniTrailspace.trim()', 'Trim trailspace') -nmap_leader('oT', trailspace_toggle_command, 'Trailspace hl toggle') -nmap_leader('oz', 'lua MiniMisc.zoom()', 'Zoom toggle') -nmap_leader('ow', - "lua MiniSessions.write(vim.fn.input('Session name: ', string.match(vim.fn.getcwd(), \"[^/]+$\") .. '-session.vim'))", - 'Write session') - --- r is for 'R' -nmap_leader('rc', 'RSend devtools::check()', 'Check') -nmap_leader('rC', 'RSend devtools::test_coverage()', 'Coverage') -nmap_leader('rd', 'RSend devtools::document()', 'Document') -nmap_leader('ri', 'RSend devtools::install(keep_source=TRUE)', 'Install') -nmap_leader('rk', 'RSend quarto::quarto_preview("%")', 'Knit file') -nmap_leader('rl', 'RSend devtools::load_all()', 'Load all') -nmap_leader('rL', 'RSend devtools::load_all(recompile=TRUE)', 'Load all recompile') -nmap_leader('rm', 'RSend Rcpp::compileAttributes()', 'Run examples') -nmap_leader('rT', 'RSend testthat::test_file("%")', 'Test file') -nmap_leader('rt', 'RSend devtools::test()', 'Test') - --- - Copy to clipboard and make reprex (which itself is loaded to clipboard) -xmap_leader('rx', '"+y :RSend reprex::reprex()', 'Reprex selection') - --- s is for 'send' (Send text to neoterm buffer) -nmap_leader('s', 'SlimeSendCurrentLinej', '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', 'SlimeRegionSend', 'Send to terminal') - --- t is for 'terminal' -vim.keymap.set("t", "", [[]], { desc = "Exit terminal mode" }) -vim.keymap.set("n", "tc", 'lua Config.terminal.open_clickhouse_client()', - { desc = "Open Clickhouse client" }) -vim.keymap.set("n", "tl", 'lua Config.terminal.open_clickhouse_local()', - { desc = "Open Clickhouse local" }) -vim.keymap.set("n", "tp", 'lua Config.terminal.open_python()', { desc = "Open Python" }) -vim.keymap.set("n", "tj", 'lua Config.terminal.open_julia()', { desc = "Open Julia" }) -vim.keymap.set("n", "td", 'lua Config.terminal.open_duckdb();Config.terminal.toggle_bracket()', - { desc = "Open DuckDB" }) -vim.keymap.set("n", "tx", 'lua Config.terminal.open_in_terminal()', { desc = "Terminal Command" }) -vim.keymap.set("n", "tt", 'lua Config.terminal.open_shell()', { desc = "Terminal" }) -nmap_leader("tb", 'lua Config.terminal.toggle_bracket()', "Toggle bracketed paste") -nmap_leader("up", 'lua Config.terminal.toggle_bracket()', "Toggle bracketed paste") - --- u is for UI -nmap_leader('ut', 'TSContext toggle', 'Toggle TScontext') -nmap_leader('ua', 'Copilot toggle', 'Toggle AI completion') - --- v is for 'visits' -nmap_leader('vv', 'lua MiniVisits.add_label("core")', 'Add "core" label') -nmap_leader('vV', 'lua MiniVisits.remove_label("core")', 'Remove "core" label') -nmap_leader('vl', 'lua MiniVisits.add_label()', 'Add label') -nmap_leader('vL', 'lua MiniVisits.remove_label()', '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", "h", "Go to Left Window", { remap = true }) -nmap_leader("wj", "j", "Go to Lower Window", { remap = true }) -nmap_leader("wk", "k", "Go to Upper Window", { remap = true }) -nmap_leader("wl", "l", "Go to Right Window", { remap = true }) - -nmap_leader("_", "s", "Split Window Below", { remap = true }) -nmap_leader("|", "v", "Split Window Right", { remap = true }) -nmap_leader("wd", "c", "Delete Window", { remap = true }) -nmap_leader("wo", "o", "Delete Other Windows", { remap = true }) - --- z is for 'ZettelKasten' -nmap_leader("zo", 'ZkNotes', "Notes") -nmap_leader("zt", 'ZkTags', "Tags") - -nmap_leader( - "zrd", - 'ZkNew { group = "dreviews" }', - "Daily Review" -) -nmap_leader( - "zrw", - 'ZkNew { group = "wreviews" }', - "Weekly Review" -) -nmap_leader( - "zn", - 'ZkNew { group = "inbox", title = vim.fn.input("Title: ") }', - "New" -) -nmap_leader( - "zp", - "ZkNew { group = 'permanent', title = vim.fn.input('Title: ') }", - "Permanent" -) - -nmap_leader( - "zl", - "ZkNew { group = 'literature', title = vim.fn.input('Title: '), extra.author = vim.fn.input('Author: '), extra.year = vim.fn.input('Year: ') }", - "Literature" -) - -nmap_leader( - "zd", - "ZkNew { group = 'dashboard', title = vim.fn.input('Title: ') }", - "Dashboard" -) -nmap_leader( - "zP", - "ZkNew { group = 'project', title = vim.fn.input('Title: ')}", - "Project" -) --- stylua: ignore end +-- Re-export the shared config table for backwards compatibility with any +-- external code or keymap strings that still reference the global `Config`. +_G.Config = Config diff --git a/plugin/20_startup.lua b/plugin/20_startup.lua index e8d6693..36bab13 100644 --- a/plugin/20_startup.lua +++ b/plugin/20_startup.lua @@ -1,3 +1,4 @@ +local Config = require('config') local now = MiniDeps.now local later = MiniDeps.later local now_if_args = Config.now_if_args diff --git a/plugin/22_languages.lua b/plugin/22_languages.lua index 70bec23..5f115f7 100644 --- a/plugin/22_languages.lua +++ b/plugin/22_languages.lua @@ -30,6 +30,68 @@ later(function() 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 now_if_args(function() add("render-markdown.nvim") diff --git a/plugin/24_completion.lua b/plugin/24_completion.lua index f6516a1..c2291c7 100644 --- a/plugin/24_completion.lua +++ b/plugin/24_completion.lua @@ -1,3 +1,4 @@ +local Config = require('config') local add = Config.add local later = MiniDeps.later local now = MiniDeps.now diff --git a/plugin/25_lsp.lua b/plugin/25_lsp.lua index 8c40915..31528b9 100644 --- a/plugin/25_lsp.lua +++ b/plugin/25_lsp.lua @@ -1,3 +1,4 @@ +local Config = require('config') local now_if_args = Config.now_if_args if not Config.isNixCats then @@ -16,6 +17,40 @@ now_if_args(function() marksman = { filetypes = { "markdown", "markdown_inline", "codecompanion" }, }, + 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 = {}, julials = { settings = { julia = { diff --git a/plugin/26_dap.lua b/plugin/26_dap.lua new file mode 100644 index 0000000..73deeb8 --- /dev/null +++ b/plugin/26_dap.lua @@ -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) diff --git a/plugin/27_image.lua b/plugin/27_image.lua new file mode 100644 index 0000000..e228190 --- /dev/null +++ b/plugin/27_image.lua @@ -0,0 +1,46 @@ +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 = "kitty", + 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) diff --git a/plugin/28_latex.lua b/plugin/28_latex.lua new file mode 100644 index 0000000..b77b201 --- /dev/null +++ b/plugin/28_latex.lua @@ -0,0 +1,43 @@ +-- 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 + + Config.add("vimtex") + + local ok, vimtex = pcall(require, "vimtex") + if not ok then + vim.notify("vimtex not available", vim.log.levels.WARN) + return + end + + -- Keep conservative defaults: latexmk continuous compilation off, single + -- viewer (zathura falls back to Evince on most setups). + vimtex.setup({ + enabled = true, + compile_on_save = false, + compiler = "latexmk", + -- Avoid hooking spell/formatting into our global group; + -- vimtex stashes its own mappings automatically. + }) + + -- Filetype detection is normally on, but force it explicitly so .tex files + -- opened outside Quarto still pick up the LSP + viewer hooks. + vim.api.nvim_create_autocmd("BufReadPost", { + 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) From df2f776d3ff8ddbb959b281fb5504a2ab954cba2 Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:43:35 +0000 Subject: [PATCH 02/12] 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 `` -> `smart_send.send_repl` mapping. Override `` from a per-buffer `ftplugin/.lua` if you want enter-to-send behavior. R.nvim's `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 `` from `M.setup_keybindings` on the same principle as C2. `` 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 `` 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`. --- plugin/01_lib.lua | 2 +- plugin/04_treesitter.lua | 16 +++++++++++----- plugin/10_keymap.lua | 6 +++--- plugin/22_languages.lua | 2 ++ plugin/25_lsp.lua | 4 +++- plugin/27_image.lua | 2 +- 6 files changed, 21 insertions(+), 11 deletions(-) diff --git a/plugin/01_lib.lua b/plugin/01_lib.lua index ba87825..988905c 100644 --- a/plugin/01_lib.lua +++ b/plugin/01_lib.lua @@ -34,7 +34,7 @@ end Config.execute_lua_line = function() local line = 'lua ' .. vim.api.nvim_get_current_line() vim.api.nvim_command(line) - print(line) + vim.notify(line, vim.log.levels.INFO) vim.api.nvim_input('') end diff --git a/plugin/04_treesitter.lua b/plugin/04_treesitter.lua index c4c0452..88d3cb7 100644 --- a/plugin/04_treesitter.lua +++ b/plugin/04_treesitter.lua @@ -121,11 +121,17 @@ function M.setup_keybindings(global_nodes) vim.keymap.set('n', 'a', function() smart_send.send_repl(current_global_nodes) end, { noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) - vim.keymap.set({ 'n', 'i' }, '', function() smart_send.send_repl(current_global_nodes) end, - { noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) - - vim.keymap.set('n', '', function() smart_send.send_repl(current_global_nodes) end, - { noremap = true, silent = true, desc = "Send node to REPL", buffer = true }) + -- Both `` and `` were removed from `M.setup_keybindings`. They + -- were hard, implicit overrides that clobbered Vim/filetype defaults and + -- 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 + -- from a `ftplugin/.lua`: + -- + -- -- e.g. ftplugin/r.lua or ftplugin/quarto.lua + -- vim.keymap.set('n', '', 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', 'n', function() current_global_nodes = M.add_global_node(current_global_nodes) end, diff --git a/plugin/10_keymap.lua b/plugin/10_keymap.lua index 9e88a27..98074fb 100644 --- a/plugin/10_keymap.lua +++ b/plugin/10_keymap.lua @@ -6,6 +6,6 @@ require('keymap.core') require('keymap.leader') require('keymap.terminal') --- Re-export the shared config table for backwards compatibility with any --- external code or keymap strings that still reference the global `Config`. -_G.Config = Config +-- `_G.Config` is set by `init.lua:2` once at start-up; no need to re-export +-- here. The single source of truth is `init.lua` so future refactors don't +-- have to chase which file currently publishes the alias. diff --git a/plugin/22_languages.lua b/plugin/22_languages.lua index 5f115f7..cd5ebed 100644 --- a/plugin/22_languages.lua +++ b/plugin/22_languages.lua @@ -1,3 +1,5 @@ +local Config = require('config') + local add = Config.add local now_if_args = Config.now_if_args local later = MiniDeps.later diff --git a/plugin/25_lsp.lua b/plugin/25_lsp.lua index 31528b9..e249872 100644 --- a/plugin/25_lsp.lua +++ b/plugin/25_lsp.lua @@ -50,7 +50,9 @@ now_if_args(function() }, }, }, - texlab = {}, + texlab = { + single_file_support = true, + }, julials = { settings = { julia = { diff --git a/plugin/27_image.lua b/plugin/27_image.lua index e228190..18a424a 100644 --- a/plugin/27_image.lua +++ b/plugin/27_image.lua @@ -24,7 +24,7 @@ later(function() end image.setup({ - backend = "kitty", + backend = "auto", integrations = { markdown = { enabled = true, From 3f2ab6ef96f4544d19a3fc2ed8b8551b6849aed3 Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:47:58 +0000 Subject: [PATCH 03/12] 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. --- modules/module/specs/plugins.nix | 14 ++++++++++++-- plugin/27_image.lua | 6 +++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/modules/module/specs/plugins.nix b/modules/module/specs/plugins.nix index 4a3324a..ff072cc 100644 --- a/modules/module/specs/plugins.nix +++ b/modules/module/specs/plugins.nix @@ -70,6 +70,7 @@ in { lazy = true; data = [ config.nvim-lib.neovimPlugins.cmp-pandoc-references + pkgs.vimPlugins.image-nvim ]; }; @@ -184,11 +185,20 @@ in { colorful-menu-nvim conform-nvim 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 + -- R debugger (via vscDebugger) 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-ui nvim-dap-virtual-text - nvim-lint - vim-slime image-nvim ]; }; diff --git a/plugin/27_image.lua b/plugin/27_image.lua index 18a424a..89e0d2f 100644 --- a/plugin/27_image.lua +++ b/plugin/27_image.lua @@ -24,7 +24,11 @@ later(function() end image.setup({ - backend = "auto", + -- 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, From 137ec6d5763a2aee91b2c917af0f3a559272faa4 Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:49:12 +0000 Subject: [PATCH 04/12] 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 ``a`` entry to ``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 ``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 ``rx`` reprex mapping is preserved verbatim, just with a clarifying comment above it. Files: 2 modified. +9 / -1. --- lua/keymap/core.lua | 2 +- lua/keymap/leader.lua | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lua/keymap/core.lua b/lua/keymap/core.lua index 1314b06..8c0abe3 100644 --- a/lua/keymap/core.lua +++ b/lua/keymap/core.lua @@ -40,5 +40,5 @@ Config.leader_group_clues = { { mode = 'x', keys = 'r', desc = '+R' }, { mode = 'n', keys = 'z', desc = '+ZK' }, { mode = 'n', keys = 'zr', desc = '+Reviews' }, - { mode = 'x', keys = 'a', desc = '+AI' }, + { mode = 'x', keys = 'a', desc = '+AI' }, } diff --git a/lua/keymap/leader.lua b/lua/keymap/leader.lua index 2543212..795079f 100644 --- a/lua/keymap/leader.lua +++ b/lua/keymap/leader.lua @@ -163,6 +163,10 @@ nmap_leader('rm', 'RSend Rcpp::compileAttributes()', 'Run examples') nmap_leader('rT', 'RSend testthat::test_file("%")', 'Test file') nmap_leader('rt', 'RSend devtools::test()', 'Test') +-- Visual `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()', 'Reprex selection') From 6142f7d55eaa64cb4d7a62398c2ba881b73d9000 Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Sun, 26 Jul 2026 06:57:57 +0000 Subject: [PATCH 05/12] 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 `` 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 `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. --- lua/keymap/repl.lua | 42 ++++++++++++++++++++++++++++----------- lua/nix_smart_send.lua | 31 +++++++++++++++-------------- plugin/03_terminal.lua | 4 ++++ plugin/21_datascience.lua | 1 - 4 files changed, 50 insertions(+), 28 deletions(-) diff --git a/lua/keymap/repl.lua b/lua/keymap/repl.lua index f43cb9b..35425aa 100644 --- a/lua/keymap/repl.lua +++ b/lua/keymap/repl.lua @@ -20,12 +20,20 @@ function M.send_line() local ft = dispatch_ft() if ft == "r" then - -- R.nvim v1+ exposes a Lua API; fall back to the legacy mappings - -- if a v0.x build is still in use. - local ok, rrun = pcall(require, "r.run") - if ok and rrun and type(rrun.send_line) == "function" then - rrun.send_line() - return + -- 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 . + 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("RDSendLine", true, false, true), @@ -48,7 +56,8 @@ function M.send_line() end -- Default: vim-slime (terminal). - vim.cmd("SlimeSendCurrentLine") + 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 @@ -58,11 +67,20 @@ function M.send_selection() local ft = dispatch_ft() if ft == "r" then - -- Prefer R.nvim v1+ Lua API; fall back to if unavailable. - local ok, rrun = pcall(require, "r.run") - if ok and rrun and type(rrun.send_selection) == "function" then - rrun.send_selection() - return + -- 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 . + 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("RSendSelection", true, false, true), diff --git a/lua/nix_smart_send.lua b/lua/nix_smart_send.lua index 7906d25..51e9611 100644 --- a/lua/nix_smart_send.lua +++ b/lua/nix_smart_send.lua @@ -88,6 +88,12 @@ function M.move_to_next_non_empty_line(current_node) 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 @@ -129,15 +135,11 @@ function M.select_until_global(global_nodes) end function M.slime_send_region() - if vim.fn.exists('*slime#send_op') == 0 then - vim.notify("slime plugin not available", vim.log.levels.ERROR) - return - end - - local slime_command = ":call slime#send_op(visualmode(), 1)" - local termcodes = vim.api.nvim_replace_termcodes(slime_command, true, true, true) - - vim.api.nvim_feedkeys(termcodes, "x", true) + vim.api.nvim_feedkeys( + vim.api.nvim_replace_termcodes("SlimeRegionSend", true, false, true), + "m", + false + ) end function M.send_repl(global_nodes) @@ -155,15 +157,14 @@ function M.send_repl(global_nodes) target_node = next_node end - -- Select the target node and send it to the REPL. - if not M.vselect_node(target_node) then + -- Extract node text and send directly to avoid visual-mode/feedkeys races. + local ok, text = pcall(vim.treesitter.get_node_text, target_node, 0) + if not ok or not text then + vim.notify("Could not extract code from Tree-sitter node", vim.log.levels.WARN) return end - M.slime_send_region() - -- Place cursor at end of visual block - local _, _, er, ec = target_node:range() - vim.api.nvim_win_set_cursor(0, { er + 1, ec }) + 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) diff --git a/plugin/03_terminal.lua b/plugin/03_terminal.lua index 9408760..95d7a29 100644 --- a/plugin/03_terminal.lua +++ b/plugin/03_terminal.lua @@ -1,5 +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 = {} -- Configuration diff --git a/plugin/21_datascience.lua b/plugin/21_datascience.lua index cb496b9..807d8ed 100644 --- a/plugin/21_datascience.lua +++ b/plugin/21_datascience.lua @@ -22,7 +22,6 @@ end -- terminal later(function() - vim.g.slime_target = "neovim" vim.g.slime_no_mappings = true add("vim-slime") vim.g.slime_cell_delimiter = vim.g.slime_cell_delimiter or "# %%" From 259b3d65b8cfa9eeabd46edea835b63adba12839 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:10:59 +0000 Subject: [PATCH 06/12] fix: use valid Nix comment syntax in modules --- modules/module/settings/cat-packages.nix | 8 ++++---- modules/module/specs/plugins.nix | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/module/settings/cat-packages.nix b/modules/module/settings/cat-packages.nix index 49a33e6..fdc7145 100644 --- a/modules/module/settings/cat-packages.nix +++ b/modules/module/settings/cat-packages.nix @@ -5,10 +5,10 @@ ... }: let - -- Include packages from a category only if that category is enabled. - -- NOTE: The package list expression is still evaluated (packages in Nix are - -- lazy by default, so derivations are not built), so keep side-effecting - -- expressions out of these lists. + # Include packages from a category only if that category is enabled. + # NOTE: The package list expression is still evaluated (packages in Nix are + # lazy by default, so derivations are not built), 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; diff --git a/modules/module/specs/plugins.nix b/modules/module/specs/plugins.nix index ff072cc..ffd7cff 100644 --- a/modules/module/specs/plugins.nix +++ b/modules/module/specs/plugins.nix @@ -190,9 +190,9 @@ in { ]; }; - -- 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 - -- R debugger (via vscDebugger) and in-buffer image rendering for plots. + # 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 + # R debugger (via vscDebugger) and in-buffer image rendering for plots. config.specs.r-lazy = lib.mkIf (config.cats.r or false) { lazy = true; data = with pkgs.vimPlugins; [ From f4b960c6040ace861d44509fa7cb0f556bb175e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:11:43 +0000 Subject: [PATCH 07/12] docs: clarify Nix laziness comment --- modules/module/settings/cat-packages.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/module/settings/cat-packages.nix b/modules/module/settings/cat-packages.nix index fdc7145..e68ff93 100644 --- a/modules/module/settings/cat-packages.nix +++ b/modules/module/settings/cat-packages.nix @@ -6,9 +6,9 @@ }: let # Include packages from a category only if that category is enabled. - # NOTE: The package list expression is still evaluated (packages in Nix are - # lazy by default, so derivations are not built), so keep side-effecting - # expressions out of these lists. + # 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; From d45d85e5bca53910f0c5e7d013328f3656645e42 Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:16:50 +0000 Subject: [PATCH 08/12] chore: remove Stata tree-sitter parser from default parser list --- modules/module/specs/plugins.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/module/specs/plugins.nix b/modules/module/specs/plugins.nix index ff072cc..d8bc47d 100644 --- a/modules/module/specs/plugins.nix +++ b/modules/module/specs/plugins.nix @@ -36,7 +36,6 @@ let "rnoweb" "regex" "sql" - "stata" "toml" "vim" "vimdoc" From 2dc7227a0ecf906bfe9369cca8d0f59131c6d6a9 Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:21:18 +0000 Subject: [PATCH 09/12] fix: remove vscDebugger from default R packages (not in CRAN/rPackages) --- modules/module/settings/lang-packages.nix | 5 ++++- modules/module/specs/plugins.nix | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/modules/module/settings/lang-packages.nix b/modules/module/settings/lang-packages.nix index 97ec4e7..3be7a9a 100644 --- a/modules/module/settings/lang-packages.nix +++ b/modules/module/settings/lang-packages.nix @@ -44,7 +44,10 @@ data_table janitor styler - vscDebugger + # 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 ]) ); diff --git a/modules/module/specs/plugins.nix b/modules/module/specs/plugins.nix index d003fbe..7630632 100644 --- a/modules/module/specs/plugins.nix +++ b/modules/module/specs/plugins.nix @@ -191,7 +191,7 @@ in { # 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 - # R debugger (via vscDebugger) and in-buffer image rendering for plots. + # 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; [ From 5c35787c68315479a0caec2c3c6451ac22302433 Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:24:33 +0000 Subject: [PATCH 10/12] fix: remove broken luaPackages.magick from markdown cat packages --- modules/module/settings/cat-packages.nix | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/module/settings/cat-packages.nix b/modules/module/settings/cat-packages.nix index e68ff93..213f85c 100644 --- a/modules/module/settings/cat-packages.nix +++ b/modules/module/settings/cat-packages.nix @@ -51,7 +51,6 @@ in marksman texlab imagemagick - luaPackages.magick ]); nix = maybe "nix" (with pkgs; [ From ad26b17b8caf50bb8c14fc07b555c474ecc4cf0b Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:28:43 +0000 Subject: [PATCH 11/12] fix: avoid ripgrep dependency in devShell R_LIBS_SITE hook --- flake.nix | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flake.nix b/flake.nix index db14f58..d55ec6f 100644 --- a/flake.nix +++ b/flake.nix @@ -69,7 +69,9 @@ '' + 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: -) + # Use R itself to discover the library paths, avoiding a dependency on + # ripgrep/strings/grep in the devShell PATH. + export R_LIBS_SITE=$(Rscript -e 'cat(.libPaths(), sep = ":")') export R_LIBS_USER="$PWD/.r-libs" mkdir -p "$R_LIBS_USER" ''; From 92bd53feeaa483f1686214f2793bf545726bdff6 Mon Sep 17 00:00:00 2001 From: Daniel <22460147+dwinkler1@users.noreply.github.com> Date: Mon, 27 Jul 2026 00:30:10 +0000 Subject: [PATCH 12/12] fix: add ripgrep to always category and revert shellHook to use rg --- flake.nix | 4 +--- modules/module/settings/cat-packages.nix | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index d55ec6f..db14f58 100644 --- a/flake.nix +++ b/flake.nix @@ -69,9 +69,7 @@ '' + nixpkgs.lib.optionalString (config.cats.r or false) '' export R_HOME=$(R RHOME) - # Use R itself to discover the library paths, avoiding a dependency on - # ripgrep/strings/grep in the devShell PATH. - export R_LIBS_SITE=$(Rscript -e 'cat(.libPaths(), sep = ":")') + 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" ''; diff --git a/modules/module/settings/cat-packages.nix b/modules/module/settings/cat-packages.nix index 213f85c..ee1a255 100644 --- a/modules/module/settings/cat-packages.nix +++ b/modules/module/settings/cat-packages.nix @@ -25,7 +25,9 @@ in }; config.catPkgs = { - always = maybe "always" (with pkgs; [ ]); + always = maybe "always" (with pkgs; [ + ripgrep + ]); clickhouse = maybe "clickhouse" (with pkgs; [ clickhouse-lts ]);