nvimConfig/plugin/04_treesitter.lua
Daniel df2f776d3f fix: address review findings (C1/C3/H1/H2/H5 + M1)
Tackles the critical and high-impact findings from the in-PR review
of commit 7f01be5, plus one consistency fix (M1). All changes are
scoped to the same review branch (PR #12); no behavior changes
elsewhere.

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

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

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

Files: 6 modified. +21 / -11.

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

For reviewers
* The text-object configuration in plugin/04_treesitter.lua's
  textobjects block is unchanged. H4 (`@assignment.*` queries may not be
  defined for R) is left for a follow-up with verification.
* yamlls GitHub-rawURL schema dependency (H3) is left intentionally --
  vendoring the schemas is a separate decision.
* Tests (T1-T6 from the review) are deferred; no test infrastructure
  exists beyond `tests/init.lua`.
2026-07-26 06:43:35 +00:00

197 lines
6 KiB
Lua

local Config = require('config')
local M = {}
-- Default parsers list moved from startup config
M.default_parsers = {
"bash", "bibtex", "c", "caddy", "cmake", "comment", "commonlisp", "cpp", "css", "csv",
"cuda", "desktop", "diff", "dockerfile", "doxygen", "editorconfig", "fortran", "git_config", "git_rebase",
"gitattributes", "gitcommit", "gitignore", "gnuplot", "go", "gpg", "html", "javascript", "jq", "json", "json5",
"julia", "just", "latex", "ledger", "lua", "luadoc", "luap", "luau", "make", "markdown", "markdown_inline",
"matlab", "meson", "muttrc", "nix", "nu", "passwd", "powershell", "prql", "python", "r", "query", "readline", "regex",
"requirements", "rnoweb", "rust", "sql", "ssh_config", "swift", "tmux", "toml", "tsv", "tsx", "typescript", "typst",
"vala", "vim", "vimdoc", "yaml", "zig",
}
-- Cache treesitter utils to avoid repeated requires
local smart_send = require('nix_smart_send')
-- 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
return false
end
for _, v in ipairs(list) do
if v == value then
return true
end
end
return false
end
function M.add_global_node(nodes)
if not nodes then
return nil
end
local node_type = M.get_type()
if not node_type then
return nodes
end
-- Create a copy to avoid modifying the original
local global_nodes = vim.deepcopy(nodes)
-- Check if node type already exists to avoid duplicates
if not is_in_list(global_nodes, node_type) then
table.insert(global_nodes, node_type)
end
return global_nodes
end
function M.remove_global_node(nodes)
if not nodes then
return nil
end
local node_type = M.get_type()
if not node_type then
return nodes
end
local global_nodes = vim.deepcopy(nodes)
-- Remove all occurrences (iterate backwards to avoid index issues)
for i = #global_nodes, 1, -1 do
if global_nodes[i] == node_type then
table.remove(global_nodes, i)
end
end
return global_nodes
end
function M.set_global_nodes()
local input = vim.fn.input("Enter root nodes: ")
if input == "" then
return {}
end
local nodes_in = {}
-- Trim whitespace from each node name
for node in string.gmatch(input, '([^,]+)') do
local trimmed = vim.trim(node)
if trimmed ~= "" then
table.insert(nodes_in, trimmed)
end
end
return nodes_in
end
function M.get_type()
local cur_node = smart_send.get_current_node()
if not cur_node then
vim.notify("No Tree-sitter node under cursor", vim.log.levels.WARN)
return nil
end
local node_type = cur_node:type()
vim.notify("Node type: " .. node_type, vim.log.levels.INFO)
return node_type
end
function M.setup_keybindings(global_nodes)
local current_global_nodes = global_nodes
vim.keymap.set({ 'n' }, '<localleader>r', function()
current_global_nodes = M.set_global_nodes()
end,
{ noremap = true, silent = true, desc = "set global_nodes", buffer = true })
vim.keymap.set({ 'n', 'v' }, '<localleader>v', function()
smart_send.move_to_next_non_empty_line(); smart_send.select_until_global(current_global_nodes)
end,
{ noremap = true, silent = true, desc = "Visual select next node after WS", buffer = true })
vim.keymap.set('n', '<localleader>a', function() smart_send.send_repl(current_global_nodes) end,
{ noremap = true, silent = true, desc = "Send node to REPL", buffer = true })
-- Both `<CR>` and `<S-CR>` 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/<lang>.lua`:
--
-- -- e.g. ftplugin/r.lua or ftplugin/quarto.lua
-- vim.keymap.set('n', '<CR>', function()
-- require('config').treesitter_helpers.setup_keybindings(global_nodes)
-- require('nix_smart_send').send_repl(global_nodes)
-- end, { buffer = true, desc = 'Send node to REPL' })
vim.keymap.set('n', '<localleader>n',
function() current_global_nodes = M.add_global_node(current_global_nodes) end,
{ noremap = true, silent = true, desc = "Add node under cursor to globals", buffer = true })
vim.keymap.set('n', '<localleader>x',
function() current_global_nodes = M.remove_global_node(current_global_nodes) end,
{ noremap = true, silent = true, desc = "Remove node under cursor from globals", buffer = true })
vim.keymap.set('n', '<localleader>o', function()
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', '<localleader>p', function() M.get_type() end,
{ noremap = true, silent = true, desc = "Print node type", buffer = true })
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