mirror of
https://github.com/dwinkler1/nvimConfig.git
synced 2026-08-22 17:43:13 -04:00
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.
This commit is contained in:
parent
0ea9b249ed
commit
7f01be59d7
23 changed files with 952 additions and 490 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue