[INIT]
This commit is contained in:
137
lua/repl_plugin/git.lua
Normal file
137
lua/repl_plugin/git.lua
Normal file
@@ -0,0 +1,137 @@
|
||||
local M = {}
|
||||
|
||||
local fn = vim.fn
|
||||
local api = vim.api
|
||||
|
||||
local function job_async(argv, opts)
|
||||
opts = opts or {}
|
||||
local on_stdout = opts.on_stdout
|
||||
local on_stderr = opts.on_stderr
|
||||
local on_exit = opts.on_exit
|
||||
|
||||
local id = fn.jobstart(argv, {
|
||||
stdout_buffered = true,
|
||||
stderr_buffered = true,
|
||||
on_stdout = vim.schedule_wrap(function(_, data, _)
|
||||
if on_stdout then on_stdout(data) end
|
||||
end),
|
||||
on_stderr = vim.schedule_wrap(function(_, data, _)
|
||||
if on_stderr then on_stderr(data) end
|
||||
end),
|
||||
on_exit = vim.schedule_wrap(function(_, code, _)
|
||||
if on_exit then on_exit(code) end
|
||||
end),
|
||||
})
|
||||
return id
|
||||
end
|
||||
|
||||
-- get current branch name of repo_path; cb(err, branch)
|
||||
function M.get_branch(repo_path, cb)
|
||||
cb = cb or function() end
|
||||
if not repo_path or repo_path == '' then
|
||||
return cb('no_repo')
|
||||
end
|
||||
job_async({'git','-C',repo_path,'rev-parse','--abbrev-ref','HEAD'}, {
|
||||
on_stdout = function(data)
|
||||
local out = table.concat(data, '\n'):gsub('\n+$','')
|
||||
cb(nil, out)
|
||||
end,
|
||||
on_stderr = function(data)
|
||||
-- ignore stderr unless needed
|
||||
end,
|
||||
on_exit = function(code)
|
||||
if code ~= 0 then cb('git_error') end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- status porcelain; cb(err, lines_table)
|
||||
function M.status_porcelain(repo_path, cb)
|
||||
cb = cb or function() end
|
||||
job_async({'git','-C',repo_path,'status','--porcelain'}, {
|
||||
on_stdout = function(data)
|
||||
-- data is table of lines
|
||||
cb(nil, data)
|
||||
end,
|
||||
on_exit = function(code)
|
||||
if code ~= 0 then cb('git_error') end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- commit_if_dirty: if dirty and auto_commit true, run add+commit
|
||||
-- opts = { auto_commit = true, message = 'msg' }
|
||||
-- cb(err, result)
|
||||
function M.commit_if_dirty(repo_path, opts, cb)
|
||||
cb = cb or function() end
|
||||
opts = opts or {}
|
||||
local auto_commit = opts.auto_commit
|
||||
local message = opts.message or ("Auto REPL state: " .. os.date("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
M.status_porcelain(repo_path, function(err, lines)
|
||||
if err then return cb('status_error') end
|
||||
-- lines may contain empty string elements
|
||||
local dirty = false
|
||||
for _, l in ipairs(lines) do
|
||||
if l and l:match('%S') then dirty = true; break end
|
||||
end
|
||||
if not dirty then
|
||||
return cb(nil, {committed = false, reason = 'no_changes'})
|
||||
end
|
||||
if not auto_commit then
|
||||
return cb(nil, {committed = false, reason = 'auto_commit_disabled'})
|
||||
end
|
||||
|
||||
-- run git add -A && git commit -m message
|
||||
-- We'll run as a shell command via sh -c to combine commands easily
|
||||
local cmd = {'sh','-c', "git -C " .. fn.shellescape(repo_path) .. " add -A && git -C " .. fn.shellescape(repo_path) .. " commit -m " .. fn.shellescape(message)}
|
||||
local stdout_acc = {}
|
||||
local stderr_acc = {}
|
||||
job_async(cmd, {
|
||||
on_stdout = function(data) for _, ln in ipairs(data) do table.insert(stdout_acc, ln) end end,
|
||||
on_stderr = function(data) for _, ln in ipairs(data) do table.insert(stderr_acc, ln) end end,
|
||||
on_exit = function(code)
|
||||
if code == 0 then
|
||||
cb(nil, {committed = true, success = true, stdout = stdout_acc, stderr = stderr_acc})
|
||||
else
|
||||
cb('commit_failed', {committed = true, success = false, stdout = stdout_acc, stderr = stderr_acc, code = code})
|
||||
end
|
||||
end,
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
function M.branch_exists(repo_path, branch, cb)
|
||||
cb = cb or function() end
|
||||
job_async({'git','-C',repo_path,'rev-parse','--verify','--quiet','refs/heads/' .. branch}, {
|
||||
on_exit = function(code)
|
||||
if code == 0 then cb(nil, true) else cb(nil, false) end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
function M.checkout_branch(repo_path, branch, cb)
|
||||
cb = cb or function() end
|
||||
job_async({'git','-C',repo_path,'checkout',branch}, {
|
||||
on_stdout = function(_) end,
|
||||
on_stderr = function(_) end,
|
||||
on_exit = function(code)
|
||||
if code == 0 then cb(nil, true) else cb('checkout_failed', false) end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- create branch from 'from' (string), prefer using checkout then checkout -b
|
||||
function M.create_branch(repo_path, branch, from, cb)
|
||||
cb = cb or function() end
|
||||
from = from or 'HEAD'
|
||||
-- do two commands in shell: git -C repo checkout <from> && git -C repo checkout -b <branch>
|
||||
local cmd = {'sh','-c', "git -C " .. fn.shellescape(repo_path) .. " checkout " .. fn.shellescape(from) .. " && git -C " .. fn.shellescape(repo_path) .. " checkout -b " .. fn.shellescape(branch)}
|
||||
job_async(cmd, {
|
||||
on_exit = function(code)
|
||||
if code == 0 then cb(nil, true) else cb('create_failed', false) end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
87
lua/repl_plugin/init.lua
Normal file
87
lua/repl_plugin/init.lua
Normal file
@@ -0,0 +1,87 @@
|
||||
local git = require('repl_plugin.git')
|
||||
local runner = require('repl_plugin.runner')
|
||||
local sync = require('repl_plugin.sync')
|
||||
|
||||
local M = {}
|
||||
|
||||
local default_config = {
|
||||
main_repo_path = vim.fn.getcwd(),
|
||||
repl_repo_path = nil,
|
||||
repl_file = nil,
|
||||
poll_interval = 5000,
|
||||
auto_commit = true,
|
||||
output_target = 'buffer', -- buffer or file
|
||||
auto_start = false,
|
||||
log_file = nil,
|
||||
}
|
||||
|
||||
local config = {}
|
||||
|
||||
local function merge_opts(opts)
|
||||
opts = opts or {}
|
||||
for k, v in pairs(default_config) do
|
||||
if opts[k] == nil then opts[k] = v end
|
||||
end
|
||||
return opts
|
||||
end
|
||||
|
||||
function M.setup(opts)
|
||||
config = merge_opts(opts)
|
||||
if not config.repl_repo_path or config.repl_repo_path == '' then
|
||||
config.repl_repo_path = config.main_repo_path
|
||||
end
|
||||
if not config.repl_file or config.repl_file == '' then
|
||||
config.repl_file = config.repl_repo_path .. '/repl.pl'
|
||||
end
|
||||
|
||||
-- pass config to sync module
|
||||
sync.setup({
|
||||
main_repo_path = config.main_repo_path,
|
||||
repl_repo_path = config.repl_repo_path,
|
||||
auto_commit = config.auto_commit,
|
||||
poll_interval = config.poll_interval,
|
||||
})
|
||||
|
||||
-- register commands
|
||||
vim.api.nvim_create_user_command('REPL', function() M.open_repl_file() end, {})
|
||||
vim.api.nvim_create_user_command('RUN', function() M.run_repl() end, {})
|
||||
|
||||
if config.auto_start then M.start_auto_sync() end
|
||||
end
|
||||
|
||||
function M.open_repl_file()
|
||||
local file = config.repl_file
|
||||
if not file or file == '' then
|
||||
vim.notify('No repl_file configured', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
vim.cmd('edit ' .. vim.fn.fnameescape(file))
|
||||
end
|
||||
|
||||
function M.run_repl()
|
||||
local file = config.repl_file
|
||||
if not file or file == '' then
|
||||
vim.notify('No repl_file configured', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
runner.run_perl(file, {
|
||||
repo_base = vim.fn.fnamemodify(config.repl_repo_path, ':t'),
|
||||
open_split = true,
|
||||
output_target = config.output_target,
|
||||
repl_repo_path = config.repl_repo_path,
|
||||
})
|
||||
end
|
||||
|
||||
function M.start_auto_sync()
|
||||
sync.start(config.poll_interval)
|
||||
end
|
||||
|
||||
function M.stop_auto_sync()
|
||||
sync.stop()
|
||||
end
|
||||
|
||||
function M.sync_once(cb)
|
||||
sync.sync_once(cb)
|
||||
end
|
||||
|
||||
return M
|
||||
137
lua/repl_plugin/runner.lua
Normal file
137
lua/repl_plugin/runner.lua
Normal file
@@ -0,0 +1,137 @@
|
||||
local M = {}
|
||||
local fn = vim.fn
|
||||
local api = vim.api
|
||||
|
||||
local function get_buf_name(repo_base)
|
||||
return "REPL Output: " .. (repo_base or "repl")
|
||||
end
|
||||
|
||||
local function find_or_create_buffer(name)
|
||||
-- search for existing buffer with this name in the buffer list
|
||||
for _, bufnr in ipairs(api.nvim_list_bufs()) do
|
||||
if api.nvim_buf_get_name(bufnr) == name then
|
||||
return bufnr
|
||||
end
|
||||
end
|
||||
-- create new buffer
|
||||
local bufnr = api.nvim_create_buf(false, true) -- listed=false, scratch
|
||||
api.nvim_buf_set_name(bufnr, name)
|
||||
api.nvim_buf_set_option(bufnr, 'buftype', 'nofile')
|
||||
api.nvim_buf_set_option(bufnr, 'bufhidden', 'hide')
|
||||
api.nvim_buf_set_option(bufnr, 'swapfile', false)
|
||||
return bufnr
|
||||
end
|
||||
|
||||
local function append_lines_to_buf(bufnr, lines)
|
||||
if not bufnr or not api.nvim_buf_is_valid(bufnr) then return end
|
||||
api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
local current_lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local start = #current_lines
|
||||
api.nvim_buf_set_lines(bufnr, start, start, false, lines)
|
||||
api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
end
|
||||
|
||||
-- run perl file asynchronously and stream to output buffer
|
||||
-- opts: { repo_base = 'name', open_split = true }
|
||||
function M.run_perl(repl_file, opts)
|
||||
opts = opts or {}
|
||||
local target = opts.output_target or 'buffer'
|
||||
local repo_base = opts.repo_base or fn.fnamemodify(repl_file, ':h:t')
|
||||
|
||||
if target == 'file' then
|
||||
-- write output to file under repl_repo_path if provided, else next to repl_file
|
||||
local out_path = opts.out_file or (opts.repl_repo_path and (opts.repl_repo_path .. '/repl_output.log')) or (fn.tempname() .. '_repl_output.log')
|
||||
-- clear/initialize file
|
||||
local f = io.open(out_path, 'w')
|
||||
if f then f:write('-- REPL Run: ' .. os.date('%Y-%m-%d %H:%M:%S') .. '\n') f:close() end
|
||||
|
||||
local function append_lines_to_file(path, lines)
|
||||
local fh, err = io.open(path, 'a')
|
||||
if not fh then return end
|
||||
for _, l in ipairs(lines) do fh:write(l .. '\n') end
|
||||
fh:close()
|
||||
end
|
||||
|
||||
local function on_stdout(data)
|
||||
if data then append_lines_to_file(out_path, data) end
|
||||
end
|
||||
local function on_stderr(data)
|
||||
if data then append_lines_to_file(out_path, data) end
|
||||
end
|
||||
local function on_exit(code)
|
||||
append_lines_to_file(out_path, {'-- REPL exited with code: ' .. tostring(code)})
|
||||
end
|
||||
|
||||
local abs_repl = fn.fnamemodify(repl_file, ':p')
|
||||
local cmd = {'perl', abs_repl}
|
||||
fn.jobstart(cmd, {
|
||||
stdout_buffered = true,
|
||||
stderr_buffered = true,
|
||||
on_stdout = vim.schedule_wrap(function(_, data, _)
|
||||
on_stdout(data)
|
||||
end),
|
||||
on_stderr = vim.schedule_wrap(function(_, data, _)
|
||||
on_stderr(data)
|
||||
end),
|
||||
on_exit = vim.schedule_wrap(function(_, code, _)
|
||||
on_exit(code)
|
||||
end),
|
||||
})
|
||||
|
||||
return out_path
|
||||
end
|
||||
|
||||
-- default buffer target
|
||||
local bufname = get_buf_name(repo_base)
|
||||
local bufnr = find_or_create_buffer(bufname)
|
||||
|
||||
-- clear buffer
|
||||
api.nvim_buf_set_option(bufnr, 'modifiable', true)
|
||||
api.nvim_buf_set_lines(bufnr, 0, -1, false, {"-- REPL Run: " .. os.date('%Y-%m-%d %H:%M:%S')})
|
||||
api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
|
||||
-- open split to show buffer
|
||||
if opts.open_split ~= false then
|
||||
api.nvim_command('belowright split ' .. fn.shellescape(bufname))
|
||||
local win = api.nvim_get_current_win()
|
||||
api.nvim_win_set_buf(win, bufnr)
|
||||
end
|
||||
|
||||
local stdout_acc = {}
|
||||
local stderr_acc = {}
|
||||
|
||||
local function on_stdout(data)
|
||||
if data then
|
||||
append_lines_to_buf(bufnr, data)
|
||||
for _, l in ipairs(data) do table.insert(stdout_acc, l) end
|
||||
end
|
||||
end
|
||||
local function on_stderr(data)
|
||||
if data then
|
||||
append_lines_to_buf(bufnr, data)
|
||||
for _, l in ipairs(data) do table.insert(stderr_acc, l) end
|
||||
end
|
||||
end
|
||||
local function on_exit(code)
|
||||
append_lines_to_buf(bufnr, {"-- REPL exited with code: " .. tostring(code)})
|
||||
api.nvim_buf_set_option(bufnr, 'modifiable', false)
|
||||
end
|
||||
|
||||
-- spawn via jobstart
|
||||
local cmd = {'perl', repl_file}
|
||||
fn.jobstart(cmd, {
|
||||
stdout_buffered = true,
|
||||
stderr_buffered = true,
|
||||
on_stdout = vim.schedule_wrap(function(_, data, _)
|
||||
on_stdout(data)
|
||||
end),
|
||||
on_stderr = vim.schedule_wrap(function(_, data, _)
|
||||
on_stderr(data)
|
||||
end),
|
||||
on_exit = vim.schedule_wrap(function(_, code, _)
|
||||
on_exit(code)
|
||||
end),
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
84
lua/repl_plugin/sync.lua
Normal file
84
lua/repl_plugin/sync.lua
Normal file
@@ -0,0 +1,84 @@
|
||||
local git = require('repl_plugin.git')
|
||||
local M = {}
|
||||
|
||||
local api = vim.api
|
||||
local fn = vim.fn
|
||||
|
||||
local config = {}
|
||||
local last_branch = nil
|
||||
local timer_id = nil
|
||||
|
||||
function M.setup(cfg)
|
||||
config = cfg or {}
|
||||
end
|
||||
|
||||
function M.sync_once(cb)
|
||||
cb = cb or function() end
|
||||
git.get_branch(config.main_repo_path, function(err, branch)
|
||||
if err or not branch or branch == '' then
|
||||
vim.notify('Could not determine main repo branch', vim.log.levels.WARN)
|
||||
return cb('no_branch')
|
||||
end
|
||||
if branch == last_branch then
|
||||
return cb(nil, {changed=false})
|
||||
end
|
||||
|
||||
-- attempt to commit repl repo if dirty
|
||||
git.commit_if_dirty(config.repl_repo_path, {auto_commit = config.auto_commit}, function(cerr, cres)
|
||||
if cerr then
|
||||
vim.notify('Failed to commit REPL repo: ' .. tostring(cerr), vim.log.levels.ERROR)
|
||||
return cb('commit_failed')
|
||||
end
|
||||
-- proceed to ensure branch exists and checkout
|
||||
git.branch_exists(config.repl_repo_path, branch, function(_, exists)
|
||||
if exists then
|
||||
git.checkout_branch(config.repl_repo_path, branch, function(co_err, ok)
|
||||
if co_err then
|
||||
vim.notify('Failed to checkout repl branch: ' .. tostring(co_err), vim.log.levels.ERROR)
|
||||
return cb('checkout_failed')
|
||||
end
|
||||
last_branch = branch
|
||||
vim.notify('Repl switched to branch: ' .. branch, vim.log.levels.INFO)
|
||||
return cb(nil, {changed=true, branch=branch})
|
||||
end)
|
||||
else
|
||||
-- prefer template
|
||||
git.branch_exists(config.repl_repo_path, 'template', function(_, tmpl)
|
||||
local from = tmpl and 'template' or 'HEAD'
|
||||
git.create_branch(config.repl_repo_path, branch, from, function(cr_err, ok)
|
||||
if cr_err then
|
||||
vim.notify('Failed to create repl branch: ' .. tostring(cr_err), vim.log.levels.ERROR)
|
||||
return cb('create_failed')
|
||||
end
|
||||
last_branch = branch
|
||||
vim.notify('Repl branch created: ' .. branch, vim.log.levels.INFO)
|
||||
return cb(nil, {changed=true, branch=branch})
|
||||
end)
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.start(interval)
|
||||
if timer_id then
|
||||
vim.notify('REPL sync already running', vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
interval = interval or config.poll_interval or 5000
|
||||
timer_id = fn.timer_start(interval, function()
|
||||
vim.schedule(function() M.sync_once() end)
|
||||
end, {['repeat'] = -1})
|
||||
vim.notify('Started REPL sync (interval ' .. tostring(interval) .. ' ms)', vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
function M.stop()
|
||||
if timer_id then
|
||||
pcall(fn.timer_stop, timer_id)
|
||||
timer_id = nil
|
||||
vim.notify('Stopped REPL sync', vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
Reference in New Issue
Block a user