fix: harden REPL/terminal code-sending paths

- lua/keymap/repl.lua
  - Replace non-existent `:SlimeSendCurrentLine` with synchronous
    `vim.fn["slime#send"](line .. "\n")` fallback.
  - Support both modern `r.send` and older `r.run` R.nvim Lua APIs,
    falling back to `<Plug>` mappings if neither module is available.

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

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

- plugin/21_datascience.lua
  - Remove duplicate `vim.g.slime_target = "neovim"` now that the
    terminal module owns the setting.
This commit is contained in:
Daniel 2026-07-26 06:57:57 +00:00
commit 6142f7d55e
4 changed files with 50 additions and 28 deletions

View file

@ -20,12 +20,20 @@ function M.send_line()
local ft = dispatch_ft() local ft = dispatch_ft()
if ft == "r" then if ft == "r" then
-- R.nvim v1+ exposes a Lua API; fall back to the legacy <Plug> mappings -- R.nvim v1+ exposes a Lua API; try the modern `r.send` module first,
-- if a v0.x build is still in use. -- then fall back to the older `r.run` module, and finally to <Plug>.
local ok, rrun = pcall(require, "r.run") local ok, rmod = pcall(require, "r.send")
if ok and rrun and type(rrun.send_line) == "function" then if not ok or not rmod then
rrun.send_line() ok, rmod = pcall(require, "r.run")
end
if ok and rmod then
if type(rmod.line) == "function" then
rmod.line()
return return
elseif type(rmod.send_line) == "function" then
rmod.send_line()
return
end
end end
vim.api.nvim_feedkeys( vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Plug>RDSendLine", true, false, true), vim.api.nvim_replace_termcodes("<Plug>RDSendLine", true, false, true),
@ -48,7 +56,8 @@ function M.send_line()
end end
-- Default: vim-slime (terminal). -- 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. -- Move to the next line, matching the previous behaviour.
vim.cmd("normal! j") vim.cmd("normal! j")
end end
@ -58,11 +67,20 @@ function M.send_selection()
local ft = dispatch_ft() local ft = dispatch_ft()
if ft == "r" then if ft == "r" then
-- Prefer R.nvim v1+ Lua API; fall back to <Plug> if unavailable. -- Prefer R.nvim v1+ Lua API; try the modern `r.send` module first,
local ok, rrun = pcall(require, "r.run") -- then fall back to the older `r.run` module, and finally to <Plug>.
if ok and rrun and type(rrun.send_selection) == "function" then local ok, rmod = pcall(require, "r.send")
rrun.send_selection() 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 return
elseif type(rmod.send_selection) == "function" then
rmod.send_selection()
return
end
end end
vim.api.nvim_feedkeys( vim.api.nvim_feedkeys(
vim.api.nvim_replace_termcodes("<Plug>RSendSelection", true, false, true), vim.api.nvim_replace_termcodes("<Plug>RSendSelection", true, false, true),

View file

@ -88,6 +88,12 @@ function M.move_to_next_non_empty_line(current_node)
return false return false
end 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() node = node:next_named_sibling()
while node do while node do
if not COMMENT_TYPES[node:type()] then if not COMMENT_TYPES[node:type()] then
@ -129,15 +135,11 @@ function M.select_until_global(global_nodes)
end end
function M.slime_send_region() function M.slime_send_region()
if vim.fn.exists('*slime#send_op') == 0 then vim.api.nvim_feedkeys(
vim.notify("slime plugin not available", vim.log.levels.ERROR) vim.api.nvim_replace_termcodes("<Plug>SlimeRegionSend", true, false, true),
return "m",
end false
)
local slime_command = ":<C-u>call slime#send_op(visualmode(), 1)<CR>"
local termcodes = vim.api.nvim_replace_termcodes(slime_command, true, true, true)
vim.api.nvim_feedkeys(termcodes, "x", true)
end end
function M.send_repl(global_nodes) function M.send_repl(global_nodes)
@ -155,15 +157,14 @@ function M.send_repl(global_nodes)
target_node = next_node target_node = next_node
end end
-- Select the target node and send it to the REPL. -- Extract node text and send directly to avoid visual-mode/feedkeys races.
if not M.vselect_node(target_node) then 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 return
end end
M.slime_send_region()
-- Place cursor at end of visual block vim.fn["slime#send"](text .. "\n")
local _, _, er, ec = target_node:range()
vim.api.nvim_win_set_cursor(0, { er + 1, ec })
-- Jump to the next relevant AST node instead of scanning lines -- Jump to the next relevant AST node instead of scanning lines
M.move_to_next_non_empty_line(target_node) M.move_to_next_non_empty_line(target_node)

View file

@ -1,5 +1,9 @@
local Config = require('config') local Config = require('config')
-- vim-slime target: use Neovim's built-in terminal.
-- Must be set before any slime send happens.
vim.g.slime_target = "neovim"
local M = {} local M = {}
-- Configuration -- Configuration

View file

@ -22,7 +22,6 @@ end
-- terminal -- terminal
later(function() later(function()
vim.g.slime_target = "neovim"
vim.g.slime_no_mappings = true vim.g.slime_no_mappings = true
add("vim-slime") add("vim-slime")
vim.g.slime_cell_delimiter = vim.g.slime_cell_delimiter or "# %%" vim.g.slime_cell_delimiter = vim.g.slime_cell_delimiter or "# %%"