nvimConfig/plugin/20_startup.lua
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

306 lines
11 KiB
Lua

local Config = require('config')
local now = MiniDeps.now
local later = MiniDeps.later
local now_if_args = Config.now_if_args
local nix = require('config.nix')
if not Config.isNixCats then
local add = MiniDeps.add
now_if_args(function()
add({
source = "nvim-treesitter/nvim-treesitter",
checkout = "master",
monitor = "main",
hooks = {
post_checkout = function()
vim.cmd("TSUpdate")
end,
},
})
add({
source = "nvim-treesitter/nvim-treesitter-textobjects",
checkout = "main",
})
add({ source = "zk-org/zk-nvim" })
end)
end
-- Mini.nvim
now(function()
local colorschemeName = nix.get_setting("onedark_dark", "colorscheme")
if colorschemeName == 'light' then
local palette = require('mini.hues').make_palette({
background = '#fefcf5',
foreground = '#657b83',
accent = 'bg',
saturation = 'high',
n_hues = 8
})
palette.fg_mid2 = "#586e75"
palette.fg_mid = "#073642"
palette.bg_edge = "#fdf6e3"
palette.accent_bg = "#eee8d5"
require('mini.hues').apply_palette(palette)
else
if colorschemeName == "cyberdream" and vim.o.background == 'light' then
colorschemeName = colorschemeName .. '-light'
end
vim.cmd.colorscheme(colorschemeName)
end
end)
now(function()
require("mini.basics").setup({
options = {
basic = true,
extra_ui = true
},
mappings = {
-- jk linewise, gy/gp system clipboard, gV select last change/yank
basic = true,
-- <C-hjkl> move between windows, <C-arrow> resize
windows = true,
move_with_alt = true,
option_toggle_prefix = "<leader>u"
},
autocommands = {
basic = true,
relnum_in_visual_mode = true
},
})
end)
now(function()
require("mini.icons").setup({
use_file_extension = function(ext, _)
local suf3, suf4 = ext:sub(-3), ext:sub(-4)
return suf3 ~= "scm" and suf3 ~= "txt" and suf3 ~= "yml" and suf4 ~= "json" and suf4 ~= "yaml"
end,
})
later(MiniIcons.mock_nvim_web_devicons)
later(MiniIcons.tweak_lsp_kind)
end)
now(function()
local predicate = function(notif)
if not (notif.data.source == "lsp_progress" and notif.data.client_name == "lua_ls") then
return true
end
-- Filter out some LSP progress notifications from 'lua_ls'
return notif.msg:find("Diagnosing") == nil and notif.msg:find("semantic tokens") == nil
end
local custom_sort = function(notif_arr)
return MiniNotify.default_sort(vim.tbl_filter(predicate, notif_arr))
end
require("mini.notify").setup({ content = { sort = custom_sort } })
vim.notify = MiniNotify.make_notify()
end)
now(function()
require("mini.sessions").setup()
end)
now(function()
local starter = require("mini.starter")
starter.setup({
evaluate_single = true,
items = {
starter.sections.recent_files(5, true),
function()
local section = Config.startup.get_recent_files_by_ft_or_ext({
"r",
"sql",
"julia",
"python",
"lua",
})
return section
end,
starter.sections.pick(),
starter.sections.sessions(5, true),
starter.sections.builtin_actions(),
starter.sections.recent_files(3, false),
},
footer = Config.startup.footer_text,
content_hooks = {
starter.gen_hook.adding_bullet(),
starter.gen_hook.indexing(
"all",
{ "Builtin actions", "Recent files (current directory)", "Recent files", }
),
starter.gen_hook.aligning("center", "center"),
starter.gen_hook.padding(3, 2),
},
})
end)
now(function()
require("mini.statusline").setup()
end)
now(function()
require("mini.tabline").setup()
end)
now(function()
local miniclue = require("mini.clue")
--stylua: ignore
miniclue.setup({
window = {
config = {
width = 'auto'
},
delay = 100,
},
clues = {
Config.leader_group_clues,
miniclue.gen_clues.builtin_completion(),
miniclue.gen_clues.g(),
miniclue.gen_clues.marks(),
miniclue.gen_clues.registers(),
miniclue.gen_clues.windows({ submode_resize = true, submode_move = true }),
miniclue.gen_clues.z(),
},
triggers = {
{ mode = 'n', keys = '<Leader>' }, -- Leader triggers
{ mode = 'n', keys = '<LocalLeader>' }, -- LocalLeader triggers
{ mode = 'x', keys = '<Leader>' },
{ mode = 'x', keys = '<LocalLeader>' },
{ mode = 'n', keys = [[\]] }, -- mini.basics
{ mode = 'n', keys = '[' }, -- mini.bracketed
{ mode = 'n', keys = ']' },
{ mode = 'x', keys = '[' },
{ mode = 'x', keys = ']' },
{ mode = 'i', keys = '<C-x>' }, -- Built-in completion
{ mode = 'n', keys = 'g' }, -- `g` key
{ mode = 'x', keys = 'g' },
{ mode = 'n', keys = '`' },
{ mode = 'x', keys = '`' },
{ mode = 'n', keys = '"' }, -- Registers
{ mode = 'x', keys = '"' },
{ mode = 'i', keys = '<C-r>' },
{ mode = 'c', keys = '<C-r>' },
{ mode = 'n', keys = '<C-w>' }, -- Window commands
{ mode = 'n', keys = 'z' }, -- `z` key
{ mode = 'x', keys = 'z' },
},
})
end)
-- Treesitter
now_if_args(function()
vim.treesitter.language.register("markdown", { "markdown", "codecompanion", "rmd", "quarto" })
require 'treesitter-context'.setup {
enable = true,
multiwindow = false, -- Enable multiwindow support.
max_lines = 30, -- How many lines the window should span. Values <= 0 mean no limit.
min_window_height = 70, -- Minimum editor window height to enable context. Values <= 0 mean no limit.
line_numbers = true,
multiline_threshold = 10, -- Maximum number of lines to show for a single context
trim_scope = 'outer', -- Which context lines to discard if `max_lines` is exceeded. Choices: 'inner', 'outer'
mode = 'cursor', -- Line used to calculate context. Choices: 'cursor', 'topline'
-- Separator between context and content. Should be a single character string, like '-'.
-- When separator is set, the context will only show up when there are at least 2 lines above cursorline.
separator = '-',
zindex = 20, -- The Z-index of the context window
on_attach = nil, -- (fun(buf: integer): boolean) return false to disable attaching
}
local ts_filetypes = {
"c", "cpp", "lua", "nix", "python", "r", "markdown", "query",
"vim", "vimdoc", "yaml", "json", "toml", "rust", "go",
"javascript", "typescript", "tsx", "html", "css", "sql",
"julia", "rnoweb", "latex", "gitcommit", "gitignore",
"git_config", "git_rebase", "diff", "dockerfile",
"make", "xml", "zig", "regex", "csv", "bash",
"markdown_inline", "quarto", "rmd", "codecompanion",
}
local function start_treesitter(buf, filetype)
local lang = vim.treesitter.language.get_lang(filetype) or filetype
if #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".*", false) == 0 then
return
end
vim.treesitter.start(buf, lang)
vim.bo[buf].indentexpr = "v:lua.require('nvim-treesitter').indentexpr()"
end
vim.api.nvim_create_autocmd("FileType", {
pattern = ts_filetypes,
callback = function(ev)
start_treesitter(ev.buf, vim.bo[ev.buf].filetype)
end,
})
-- FileType may have fired before this deferred setup ran.
if vim.bo.filetype ~= "" then
start_treesitter(0, vim.bo.filetype)
end
require("nvim-treesitter-textobjects").setup({
move = {
set_jumps = true,
},
})
local move = require("nvim-treesitter-textobjects.move")
local swap = require("nvim-treesitter-textobjects.swap")
vim.keymap.set("n", "]a", function() move.goto_next_start("@parameter.inner", "textobjects") end, { desc = "Next parameter" })
vim.keymap.set("n", "[a", function() move.goto_previous_start("@parameter.inner", "textobjects") end, { desc = "Prev parameter" })
vim.keymap.set("n", "]f", function() move.goto_next_start("@function.outer", "textobjects") end, { desc = "Next function" })
vim.keymap.set("n", "[f", function() move.goto_previous_start("@function.outer", "textobjects") end, { desc = "Prev function" })
vim.keymap.set("n", "]o", function() move.goto_next_start("@loop.*", "textobjects") end, { desc = "Next loop" })
vim.keymap.set("n", "[o", function() move.goto_previous_start("@loop.*", "textobjects") end, { desc = "Prev loop" })
vim.keymap.set("n", "]s", function() move.goto_next_start("@local.scope", "textobjects") end, { desc = "Next scope" })
vim.keymap.set("n", "[s", function() move.goto_previous_start("@local.scope", "locals") end, { desc = "Prev scope" })
vim.keymap.set("n", "]z", function() move.goto_next_start("@fold", "folds") end, { desc = "Next fold" })
vim.keymap.set("n", "[z", function() move.goto_previous_start("@fold", "folds") end, { desc = "Prev fold" })
vim.keymap.set("n", "]M", function() move.goto_next_end("@function.outer", "textobjects") end, { desc = "Next function end" })
vim.keymap.set("n", "][", function() move.goto_next_end("@class.outer", "textobjects") end, { desc = "Next class end" })
vim.keymap.set("n", "[M", function() move.goto_previous_end("@function.outer", "textobjects") end, { desc = "Prev function end" })
vim.keymap.set("n", "[]", function() move.goto_previous_end("@class.outer", "textobjects") end, { desc = "Prev class end" })
vim.keymap.set("n", "]e", function() move.goto_next("@conditional.outer", "textobjects") end, { desc = "Next conditional" })
vim.keymap.set("n", "[e", function() move.goto_previous("@conditional.outer", "textobjects") end, { desc = "Prev conditional" })
vim.keymap.set("n", "<leader>x", function() swap.swap_next("@parameter.inner", "textobjects") end, { desc = "Swap parameter next" })
vim.keymap.set("n", "<leader>X", function() swap.swap_previous("@parameter.inner", "textobjects") end, { desc = "Swap parameter prev" })
if not Config.isNixCats then
local installed_check = function(lang)
return #vim.api.nvim_get_runtime_file("parser/" .. lang .. ".*", false) == 0
end
local default_parsers = Config.treesitter_helpers.default_parsers
local to_install = vim.tbl_filter(installed_check, default_parsers)
if #to_install > 0 then
require("nvim-treesitter").install(to_install)
end
end
end)
-- zk
now_if_args(function()
if nix.get_cat("markdown", false) then
require("zk").setup({
picker = "minipick",
lsp = {
-- `config` is passed to `vim.lsp.start_client(config)`
config = {
cmd = { "zk", "lsp" },
name = "zk",
-- on_attach = ...
-- etc, see `:h vim.lsp.start_client()`
},
-- automatically attach buffers in a zk notebook that match the given filetypes
auto_attach = {
enabled = true,
filetypes = { "markdown" },
},
},
})
end
end)