1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 02:50:03 +08:00
SpaceVim/lua/spacevim/plugin/runner.lua
2024-07-03 12:07:52 +08:00

682 lines
19 KiB
Lua

--=============================================================================
-- M.lua
-- Copyright (c) 2016-2022 Wang Shidong & Contributors
-- Author: Wang Shidong < wsdjeg@outlook.com >
-- URL: https://spacevim.org
-- License: GPLv3
--=============================================================================
local M = {}
local runners = {}
local logger = require('spacevim.logger').derive('runner')
local job = require('spacevim.api').import('job')
local file = require('spacevim.api').import('file')
local str = require('spacevim.api').import('data.string')
local nt = require('spacevim.api.notify')
local code_runner_bufnr = 0
local winid = -1
local target = ''
local runner_lines = 0
local runner_jobid = 0
local runner_status = {
is_running = false,
has_errors = false,
exit_code = 0,
exit_single = 0,
}
local selected_file = ''
--- @type any[]
local start_time
--- @type any[]
local end_time
local task_status = {}
local task_stdout = {}
local task_stderr = {}
local task_problem_matcher = {}
local selected_language = ''
local function stop_runner()
if runner_status.is_running then
logger.debug('stop runner:' .. runner_jobid)
job.stop(runner_jobid)
end
end
-- tbl_extend should provide default behavior
local function tbl_extend(t1, t2)
return vim.tbl_extend('force', t1, t2)
end
local function close_win()
stop_runner()
if code_runner_bufnr ~= 0 and vim.api.nvim_buf_is_valid(code_runner_bufnr) then
vim.cmd('bd ' .. code_runner_bufnr)
end
end
local function insert()
vim.fn.inputsave()
local input = vim.fn.input('input >')
if vim.fn.empty(input) == 0 and runner_status.is_running then
job.send(runner_jobid, input)
end
vim.cmd('normal! :')
vim.fn.inputrestore()
end
local function open_win()
if
code_runner_bufnr ~= 0
and vim.api.nvim_buf_is_valid(code_runner_bufnr)
and vim.fn.index(vim.fn.tabpagebuflist(), code_runner_bufnr) ~= -1
then
return
end
logger.debug('open code runner windows')
local previous_wind = vim.api.nvim_get_current_win()
vim.cmd('botright split __runner__')
code_runner_bufnr = vim.fn.bufnr('%')
local lines = vim.o.lines * 30 / 100
vim.cmd('resize ' .. lines)
vim.cmd([[
setlocal buftype=nofile bufhidden=wipe nobuflisted nolist noswapfile nowrap cursorline nospell nonu norelativenumber winfixheight nomodifiable
set filetype=SpaceVimRunner
]])
vim.api.nvim_buf_set_keymap(code_runner_bufnr, 'n', 'q', '', {
callback = close_win,
})
vim.api.nvim_buf_set_keymap(code_runner_bufnr, 'n', 'i', '', {
callback = insert,
})
vim.api.nvim_buf_set_keymap(code_runner_bufnr, 'n', '<C-c>', '', {
callback = stop_runner,
})
local id = vim.api.nvim_create_augroup('spacevim_runner', {
clear = true,
})
vim.api.nvim_create_autocmd({ 'BufWipeout' }, {
group = id,
buffer = code_runner_bufnr,
callback = stop_runner,
})
winid = vim.api.nvim_get_current_win()
if vim.g.spacevim_code_runner_focus == 0 then
vim.api.nvim_set_current_win(previous_wind)
end
end
local function extend(t1, t2)
for k, v in pairs(t2) do
t1[k] = v
end
end
local function update_statusline()
vim.cmd('redrawstatus!')
end
local function on_stdout(id, data, event)
if id ~= runner_jobid then
return
end
if vim.api.nvim_buf_is_valid(code_runner_bufnr) then
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(code_runner_bufnr, runner_lines, runner_lines + 1, false, data)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
runner_lines = runner_lines + #data
if winid >= 0 then
vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(code_runner_bufnr), 1 })
end
update_statusline()
end
end
local function on_stderr(id, data, event)
if id ~= runner_jobid then
return
end
runner_status.has_errors = true
if vim.api.nvim_buf_is_valid(code_runner_bufnr) then
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(code_runner_bufnr, runner_lines, runner_lines + 1, false, data)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
runner_lines = runner_lines + #data
if winid >= 0 then
vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(code_runner_bufnr), 1 })
end
update_statusline()
end
end
local function on_exit(id, code, single)
if id ~= runner_jobid then
return
end
end_time = vim.fn.reltime(start_time)
runner_status.is_running = false
runner_status.exit_single = single
runner_status.exit_code = code
local done = {
'',
'[Done] exited with code=' .. code .. ', single=' .. single .. ' in ' .. str.trim(
vim.fn.reltimestr(end_time)
) .. ' seconds',
}
if vim.api.nvim_buf_is_valid(code_runner_bufnr) then
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(code_runner_bufnr, runner_lines, runner_lines + 1, false, done)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
if winid >= 0 then
vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(code_runner_bufnr), 1 })
end
update_statusline()
end
end
local function merge_list(...)
local t = {}
for _, tb in ipairs({ ... }) do
for _, v in ipairs(tb) do
table.insert(t, v)
end
end
return t
end
local function on_compile_exit(id, code, single)
if id ~= runner_jobid then
return
end
if code == 0 and single == 0 then
runner_jobid = job.start(target, {
on_stdout = on_stdout,
on_stderr = on_stderr,
on_exit = on_exit,
})
if runner_jobid > 0 then
runner_status = {
is_running = true,
has_errors = false,
exit_code = 0,
exit_single = 0,
}
end
else
end_time = vim.fn.reltime(start_time)
runner_status.is_running = false
runner_status.exit_code = code
runner_status.exit_single = single
local done = {
'',
'[Done] exited with code=' .. code .. ', single=' .. single .. ' in ' .. str.trim(
vim.fn.reltimestr(end_time)
) .. ' seconds',
}
if vim.api.nvim_buf_is_valid(code_runner_bufnr) then
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(code_runner_bufnr, runner_lines, runner_lines + 1, false, done)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
if winid >= 0 then
vim.api.nvim_win_set_cursor(winid, { vim.api.nvim_buf_line_count(code_runner_bufnr), 1 })
end
update_statusline()
end
end
end
local function async_run(runner, ...)
if type(runner) == 'string' then
local cmd = runner
pcall(function()
local f
if selected_file ~= '' then
f = selected_file
else
f = vim.fn.bufname('%')
end
cmd = vim.fn.printf(runner, f)
end)
logger.info(' cmd:' .. cmd)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(
code_runner_bufnr,
runner_lines,
-1,
false,
{ '[Running] ' .. cmd, '', vim.fn['repeat']('-', 20) }
)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
runner_lines = runner_lines + 3
start_time = vim.fn.reltime()
local opts = select(1, ...) or {}
extend(opts, {
on_stdout = on_stdout,
on_stderr = on_stderr,
on_exit = on_exit,
})
runner_jobid = job.start(cmd, opts)
elseif type(runner) == 'table' and #runner == 2 then
target = file.unify_path(vim.fn.tempname(), ':p')
local dir = vim.fn.fnamemodify(target, ':h')
if vim.fn.isdirectory(dir) == 0 then
vim.fn.mkdir(dir, 'p')
end
local compile_cmd
local usestdin
local compile_cmd_info
if type(runner[1]) == 'table' then
local exe
if type(runner[1].exe) == 'function' then
exe = runner[1].exe()
elseif type(runner[1].exe) == 'string' then
exe = { runner[1].exe }
end
usestdin = runner[1].usestdin or false
compile_cmd = merge_list(exe, { runner[1].targetopt or '' }, { target }, runner[1].opt)
if not usestdin then
local f
if selected_file == '' then
f = vim.fn.bufname('%')
else
f = selected_file
end
compile_cmd = merge_list(compile_cmd, { f })
end
elseif type(runner[1]) == 'string' then
end
if type(compile_cmd) == 'table' then
if usestdin then
compile_cmd_info = vim.inspect(merge_list(compile_cmd, { 'STDIN' }))
else
compile_cmd_info = vim.inspect(compile_cmd)
end
else
if usestdin then
compile_cmd_info = compile_cmd .. ' STDIN'
else
compile_cmd_info = compile_cmd
end
end
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(
code_runner_bufnr,
runner_lines,
-1,
false,
{ '[Compile] ' .. compile_cmd_info, '[Running] ' .. target, '', vim.fn['repeat']('-', 20) }
)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
runner_lines = runner_lines + 4
start_time = vim.fn.reltime()
if
type(compile_cmd) == 'string'
or type(compile_cmd) == 'table' and vim.fn.executable(compile_cmd[1] or '') == 1
then
runner_jobid = job.start(compile_cmd, {
on_stdout = on_stdout,
on_stderr = on_stderr,
on_exit = on_compile_exit,
})
if usestdin and runner_jobid > 0 then
local range = runner[1].range or { 1, '$' }
job.send(runner_jobid, vim.fn.getline(unpack(range)))
job.chanclose(runner_jobid, 'stdin')
job.stop(runner_jobid)
end
else
local exe = compile_cmd[1] or ''
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(
code_runner_bufnr,
runner_lines,
-1,
false,
{ exe .. ' is not executable, make sure ' .. exe .. ' is in your PATH' }
)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
end
elseif type(runner) == 'table' then
local cmd = {}
if type(runner.exe) == 'function' then
cmd = merge_list(cmd, runner.exe())
elseif type(runner.exe) == 'string' then
cmd = { runner.exe }
end
local usestdin = runner.usestdin or false
cmd = merge_list(cmd, runner.opt)
if not usestdin then
if selected_file == '' then
cmd = merge_list(cmd, vim.fn.bufname('%'))
else
cmd = merge_list(cmd, selected_file)
end
end
logger.info(' cmd:' .. vim.inspect(cmd))
local running_command = table.concat(cmd, ' ')
if usestdin then
running_command = running_command .. ' STDIN'
end
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(
code_runner_bufnr,
runner_lines,
-1,
false,
{ '[Running] ' .. running_command, '', vim.fn['repeat']('-', 20) }
)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
runner_lines = runner_lines + 3
start_time = vim.fn.reltime()
if vim.fn.empty(cmd) == 0 and vim.fn.executable(cmd[1]) == 1 then
runner_jobid = job.start(cmd, {
on_stdout = on_stdout,
on_stderr = on_stderr,
on_exit = on_exit,
})
if usestdin and runner_jobid > 0 then
local range = runner.range or { 1, '$' }
-- if selected file is not empty
-- read the context from selected file.
local text
if selected_file == '' then
text = vim.fn.getline(unpack(range))
else
text = vim.fn.readfile(selected_file, '')
end
job.send(runner_jobid, text)
job.chanclose(runner_jobid, 'stdin')
job.stop(runner_jobid)
end
else
local exe = cmd[1] or ''
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', true)
vim.api.nvim_buf_set_lines(
code_runner_bufnr,
runner_lines,
-1,
false,
{ exe .. ' is not executable, make sure ' .. exe .. ' is in your PATH' }
)
vim.api.nvim_buf_set_option(code_runner_bufnr, 'modifiable', false)
end
end
if runner_jobid > 0 then
runner_status = {
is_running = true,
has_errors = false,
exit_code = 0,
exit_single = 0,
}
end
end
function M.open(...)
stop_runner()
runner_jobid = 0
runner_lines = 0
runner_status = {
is_running = false,
has_errors = false,
exit_code = 0,
exit_single = 0,
}
local language = vim.o.filetype
local runner = select(1, ...) or runners[language] or ''
local opts = select(2, ...) or {}
logger.debug('runner is:\n' .. vim.inspect(runner))
logger.debug('opt is:\n' .. vim.inspect(opts))
if vim.fn.empty(runner) == 0 then
open_win()
async_run(runner, opts)
update_statusline()
else
end
end
function M.reg_runner(ft, runner)
runners[ft] = runner
end
function M.status()
local running_nr = 0
local running_done = 0
for _, v in ipairs(task_status) do
if v.is_running then
running_nr = running_nr + 1
else
running_done = running_done + 1
end
end
if runner_status.is_running then
running_nr = running_nr + 1
end
return string.format(' %s running, %s done', running_nr, running_done)
end
function M.close()
close_win()
end
function M.select_file()
runner_lines = 0
runner_status = {
is_running = false,
has_errors = false,
exit_code = 0,
exit_single = 0,
}
if vim.loop.os_uname().sysname == 'Windows_NT' then
-- what the fuck, why need trim?
-- because powershell comamnd output has `\n` at the end, and filetype detection failed.
selected_file = str.trim(vim.fn.system({
'powershell',
"Add-Type -AssemblyName System.windows.forms|Out-Null;$f=New-Object System.Windows.Forms.OpenFileDialog;$f.Filter='Model Files All files (*.*)|*.*';$f.showHelp=$true;$f.ShowDialog()|Out-Null;$f.FileName",
}))
end
if selected_file == '' then
logger.debug('file to get selected filename!')
return
else
logger.debug('selected file is:' .. selected_file)
local ft = vim.filetype.match({ filename = selected_file })
if not ft then
logger.debug('failed to detect filetype of selected file:' .. selected_file)
return
end
local runner = runners[ft]
logger.info(vim.inspect(runner))
if runner then
open_win()
async_run(runner)
update_statusline()
end
end
end
function M.select_language() end
function M.get(ft)
return runners[ft] or ''
end
local function match_problems(output, matcher)
if matcher.pattern then
local pattern = matcher.pattern
local items = {}
for _, line in ipairs(output) do
local rst = vim.fn.matchlist(line, pattern.regexp)
local f_idx = 2
if pattern.file then
f_idx = pattern.file + 1
end
local f = rst[f_idx] or ''
local l_idx = 3
if pattern.line then
l_idx = pattern.line + 1
end
local l = rst[l_idx] or 1
local c_idx = 4
if pattern.column then
c_idx = pattern.column + 1
end
local column = rst[c_idx] or 1
local m_idx = 5
if pattern.message then
m_idx = pattern.message + 1
end
local message = rst[m_idx] or ''
if #f > 0 then
table.insert(items, {
filename = f,
lnum = l,
col = column,
text = message,
})
end
end
vim.fn.setqflist({}, 'r', { title = ' task output', items = items })
vim.cmd('copen')
else
local olderrformat = vim.o.errorformat
pcall(function()
vim.o.errorformat = matcher.errorformat
vim.g._spacevim_task_output = output
vim.cmd('noautocmd cexpr g:_spacevim_task_output')
vim.fn.setqflist({}, 'a', { title = ' task output' })
vim.cmd('copen')
vim.g._spacevim_task_output = nil
end)
vim.o.errorformat = olderrformat
end
end
local function on_backgroud_stdout(id, data, event)
local d = task_stdout['task' .. id] or {}
for _, v in ipairs(data) do
table.insert(d, v)
end
task_stdout['task' .. id] = d
end
local function on_backgroud_stderr(id, data, event)
local d = task_stderr['task' .. id] or {}
for _, v in ipairs(data) do
table.insert(d, v)
end
task_stderr['task' .. id] = d
end
local function on_backgroud_exit(id, code, single)
local status = task_status['task' .. id]
or {
is_running = false,
has_errors = false,
start_time = 0,
exit_code = 0,
}
end_time = vim.fn.reltime(status.start_time)
status.is_running = false
local problem_matcher = task_problem_matcher['task' .. id] or {}
local output
if problem_matcher.useStdout then
output = task_stdout['task' .. id] or {}
else
output = task_stderr['task' .. id] or {}
end
if not vim.tbl_isempty(problem_matcher) and not vim.tbl_isempty(output) then
match_problems(output, problem_matcher)
end
nt.notify(
'task finished with code='
.. code
.. ' in '
.. str.trim(vim.fn.reltimestr(end_time))
.. ' seconds'
)
end
local function run_backgroud(cmd, ...)
local running_nr = 0
local running_done = 0
local opts = select(1, ...) or {}
start_time = vim.fn.reltime()
local problemMatcher = select(2, ...) or {}
if not problemMatcher.errorformat and not problemMatcher.regexp then
problemMatcher = tbl_extend(problemMatcher, { errorformat = vim.o.errorformat })
end
opts.on_stdout = on_backgroud_stdout
opts.on_stderr = on_backgroud_stderr
opts.on_exit = on_backgroud_exit
local task_id = job.start(cmd, opts)
task_problem_matcher = tbl_extend(task_problem_matcher, { ['task' .. task_id] = problemMatcher })
logger.debug('task_problem_matcher is:\n' .. vim.inspect(task_problem_matcher))
task_status = tbl_extend(task_status, {
['task' .. task_id] = {
is_running = true,
has_errors = false,
start_time = start_time,
exit_code = 0,
},
})
for _, v in pairs(task_status) do
if v.is_running then
running_nr = running_nr + 1
else
running_done = running_done + 1
end
end
nt.notify(string.format('tasks: %s running, %s done', running_nr, running_done))
end
function M.run_task(task)
local isBackground = task.isBackground or false
if not vim.tbl_isempty(task) then
local cmd = task.command or ''
local args = task.args or {}
local opts = task.options or {}
if #args > 0 and #cmd > 0 then
cmd = cmd .. ' ' .. table.concat(args, ' ')
end
local opt = {}
if opts.cwd then
opt.cwd = opts.cwd
end
if opts.env then
opt = tbl_extend(opt, { env = opts.env })
end
local problemMatcher = task.problemMatcher or {}
if isBackground then
run_backgroud(cmd, opt, problemMatcher)
else
M.open(cmd, opt, problemMatcher)
end
end
end
return M