Merge pull request #212 from barrettruth/feat/async
make `:CP {run,panel}` asynchronous
This commit is contained in:
commit
ae7b571b68
4 changed files with 522 additions and 467 deletions
|
|
@ -39,14 +39,14 @@ end
|
||||||
|
|
||||||
---@param compile_cmd string[]
|
---@param compile_cmd string[]
|
||||||
---@param substitutions SubstitutableCommand
|
---@param substitutions SubstitutableCommand
|
||||||
function M.compile(compile_cmd, substitutions)
|
---@param on_complete fun(r: {code: integer, stdout: string})
|
||||||
|
function M.compile(compile_cmd, substitutions, on_complete)
|
||||||
local cmd = substitute_template(compile_cmd, substitutions)
|
local cmd = substitute_template(compile_cmd, substitutions)
|
||||||
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
||||||
|
|
||||||
local t0 = vim.uv.hrtime()
|
local t0 = vim.uv.hrtime()
|
||||||
local r = vim.system({ 'sh', '-c', sh }, { text = false }):wait()
|
vim.system({ 'sh', '-c', sh }, { text = false }, function(r)
|
||||||
local dt = (vim.uv.hrtime() - t0) / 1e6
|
local dt = (vim.uv.hrtime() - t0) / 1e6
|
||||||
|
|
||||||
local ansi = require('cp.ui.ansi')
|
local ansi = require('cp.ui.ansi')
|
||||||
r.stdout = ansi.bytes_to_string(r.stdout or '')
|
r.stdout = ansi.bytes_to_string(r.stdout or '')
|
||||||
|
|
||||||
|
|
@ -56,7 +56,10 @@ function M.compile(compile_cmd, substitutions)
|
||||||
logger.log(('Compilation failed in %.1fms.'):format(dt))
|
logger.log(('Compilation failed in %.1fms.'):format(dt))
|
||||||
end
|
end
|
||||||
|
|
||||||
return r
|
vim.schedule(function()
|
||||||
|
on_complete(r)
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function parse_and_strip_time_v(output)
|
local function parse_and_strip_time_v(output)
|
||||||
|
|
@ -103,7 +106,8 @@ local function parse_and_strip_time_v(output)
|
||||||
return head, peak_mb
|
return head, peak_mb
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.run(cmd, stdin, timeout_ms, memory_mb)
|
---@param on_complete fun(result: ExecuteResult)
|
||||||
|
function M.run(cmd, stdin, timeout_ms, memory_mb, on_complete)
|
||||||
local time_bin = utils.time_path()
|
local time_bin = utils.time_path()
|
||||||
local timeout_bin = utils.timeout_path()
|
local timeout_bin = utils.timeout_path()
|
||||||
|
|
||||||
|
|
@ -117,12 +121,7 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
||||||
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
||||||
|
|
||||||
local t0 = vim.uv.hrtime()
|
local t0 = vim.uv.hrtime()
|
||||||
local r = vim
|
vim.system({ 'sh', '-c', sh }, { stdin = stdin, text = true }, function(r)
|
||||||
.system({ 'sh', '-c', sh }, {
|
|
||||||
stdin = stdin,
|
|
||||||
text = true,
|
|
||||||
})
|
|
||||||
:wait()
|
|
||||||
local dt = (vim.uv.hrtime() - t0) / 1e6
|
local dt = (vim.uv.hrtime() - t0) / 1e6
|
||||||
|
|
||||||
local code = r.code or 0
|
local code = r.code or 0
|
||||||
|
|
@ -143,7 +142,7 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
||||||
or lower:find('enomem', 1, true)
|
or lower:find('enomem', 1, true)
|
||||||
local near_cap = peak_mb >= (0.90 * memory_mb)
|
local near_cap = peak_mb >= (0.90 * memory_mb)
|
||||||
|
|
||||||
local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint and not tled)
|
local mled = (peak_mb >= memory_mb) or near_cap or (oom_hint ~= nil and not tled)
|
||||||
|
|
||||||
if tled then
|
if tled then
|
||||||
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
logger.log(('Execution timed out in %.1fms.'):format(dt))
|
||||||
|
|
@ -155,7 +154,8 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
||||||
logger.log(('Execution successful in %.1fms.'):format(dt))
|
logger.log(('Execution successful in %.1fms.'):format(dt))
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
vim.schedule(function()
|
||||||
|
on_complete({
|
||||||
stdout = cleaned,
|
stdout = cleaned,
|
||||||
code = code,
|
code = code,
|
||||||
time_ms = dt,
|
time_ms = dt,
|
||||||
|
|
@ -163,10 +163,14 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
||||||
mled = mled,
|
mled = mled,
|
||||||
peak_mb = peak_mb,
|
peak_mb = peak_mb,
|
||||||
signal = signal,
|
signal = signal,
|
||||||
}
|
})
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.compile_problem(debug)
|
---@param debug boolean?
|
||||||
|
---@param on_complete fun(result: {success: boolean, output: string?})
|
||||||
|
function M.compile_problem(debug, on_complete)
|
||||||
local state = require('cp.state')
|
local state = require('cp.state')
|
||||||
local config = require('cp.config').get_config()
|
local config = require('cp.config').get_config()
|
||||||
local platform = state.get_platform()
|
local platform = state.get_platform()
|
||||||
|
|
@ -176,17 +180,20 @@ function M.compile_problem(debug)
|
||||||
local compile_config = (debug and eff.commands.debug) or eff.commands.build
|
local compile_config = (debug and eff.commands.debug) or eff.commands.build
|
||||||
|
|
||||||
if not compile_config then
|
if not compile_config then
|
||||||
return { success = true, output = nil }
|
on_complete({ success = true, output = nil })
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local binary = debug and state.get_debug_file() or state.get_binary_file()
|
local binary = debug and state.get_debug_file() or state.get_binary_file()
|
||||||
local substitutions = { source = state.get_source_file(), binary = binary }
|
local substitutions = { source = state.get_source_file(), binary = binary }
|
||||||
local r = M.compile(compile_config, substitutions)
|
|
||||||
|
|
||||||
|
M.compile(compile_config, substitutions, function(r)
|
||||||
if r.code ~= 0 then
|
if r.code ~= 0 then
|
||||||
return { success = false, output = r.stdout or 'unknown error' }
|
on_complete({ success = false, output = r.stdout or 'unknown error' })
|
||||||
|
else
|
||||||
|
on_complete({ success = true, output = nil })
|
||||||
end
|
end
|
||||||
return { success = true, output = nil }
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -101,8 +101,8 @@ end
|
||||||
|
|
||||||
---@param test_case RanTestCase
|
---@param test_case RanTestCase
|
||||||
---@param debug boolean?
|
---@param debug boolean?
|
||||||
---@return { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string, tled: boolean, mled: boolean, rss_mb: number }
|
---@param on_complete fun(result: { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string?, tled: boolean, mled: boolean, rss_mb: number })
|
||||||
local function run_single_test_case(test_case, debug)
|
local function run_single_test_case(test_case, debug, on_complete)
|
||||||
local source_file = state.get_source_file()
|
local source_file = state.get_source_file()
|
||||||
|
|
||||||
local binary_file = debug and state.get_debug_file() or state.get_binary_file()
|
local binary_file = debug and state.get_debug_file() or state.get_binary_file()
|
||||||
|
|
@ -117,8 +117,7 @@ local function run_single_test_case(test_case, debug)
|
||||||
local timeout_ms = (panel_state.constraints and panel_state.constraints.timeout_ms) or 0
|
local timeout_ms = (panel_state.constraints and panel_state.constraints.timeout_ms) or 0
|
||||||
local memory_mb = panel_state.constraints and panel_state.constraints.memory_mb or 0
|
local memory_mb = panel_state.constraints and panel_state.constraints.memory_mb or 0
|
||||||
|
|
||||||
local r = execute.run(cmd, stdin_content, timeout_ms, memory_mb)
|
execute.run(cmd, stdin_content, timeout_ms, memory_mb, function(r)
|
||||||
|
|
||||||
local ansi = require('cp.ui.ansi')
|
local ansi = require('cp.ui.ansi')
|
||||||
local out = r.stdout or ''
|
local out = r.stdout or ''
|
||||||
local highlights = {}
|
local highlights = {}
|
||||||
|
|
@ -162,7 +161,7 @@ local function run_single_test_case(test_case, debug)
|
||||||
status = 'fail'
|
status = 'fail'
|
||||||
end
|
end
|
||||||
|
|
||||||
return {
|
on_complete({
|
||||||
status = status,
|
status = status,
|
||||||
actual = out,
|
actual = out,
|
||||||
actual_highlights = highlights,
|
actual_highlights = highlights,
|
||||||
|
|
@ -175,7 +174,8 @@ local function run_single_test_case(test_case, debug)
|
||||||
tled = r.tled or false,
|
tled = r.tled or false,
|
||||||
mled = r.mled or false,
|
mled = r.mled or false,
|
||||||
rss_mb = r.peak_mb or 0,
|
rss_mb = r.peak_mb or 0,
|
||||||
}
|
})
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return boolean
|
---@return boolean
|
||||||
|
|
@ -199,8 +199,8 @@ function M.load_test_cases()
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param debug boolean?
|
---@param debug boolean?
|
||||||
---@return RanTestCase?
|
---@param on_complete fun(result: RanTestCase?)
|
||||||
function M.run_combined_test(debug)
|
function M.run_combined_test(debug, on_complete)
|
||||||
local combined = cache.get_combined_test(
|
local combined = cache.get_combined_test(
|
||||||
state.get_platform() or '',
|
state.get_platform() or '',
|
||||||
state.get_contest_id() or '',
|
state.get_contest_id() or '',
|
||||||
|
|
@ -209,7 +209,8 @@ function M.run_combined_test(debug)
|
||||||
|
|
||||||
if not combined then
|
if not combined then
|
||||||
logger.log('No combined test found', vim.log.levels.ERROR)
|
logger.log('No combined test found', vim.log.levels.ERROR)
|
||||||
return nil
|
on_complete(nil)
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local ran_test = {
|
local ran_test = {
|
||||||
|
|
@ -228,22 +229,23 @@ function M.run_combined_test(debug)
|
||||||
selected = true,
|
selected = true,
|
||||||
}
|
}
|
||||||
|
|
||||||
local result = run_single_test_case(ran_test, debug)
|
run_single_test_case(ran_test, debug, function(result)
|
||||||
return result
|
on_complete(result)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param index number
|
---@param index number
|
||||||
---@param debug boolean?
|
---@param debug boolean?
|
||||||
---@return boolean
|
---@param on_complete fun(success: boolean)
|
||||||
function M.run_test_case(index, debug)
|
function M.run_test_case(index, debug, on_complete)
|
||||||
local tc = panel_state.test_cases[index]
|
local tc = panel_state.test_cases[index]
|
||||||
if not tc then
|
if not tc then
|
||||||
return false
|
on_complete(false)
|
||||||
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
tc.status = 'running'
|
tc.status = 'running'
|
||||||
local r = run_single_test_case(tc, debug)
|
run_single_test_case(tc, debug, function(r)
|
||||||
|
|
||||||
tc.status = r.status
|
tc.status = r.status
|
||||||
tc.actual = r.actual
|
tc.actual = r.actual
|
||||||
tc.actual_highlights = r.actual_highlights
|
tc.actual_highlights = r.actual_highlights
|
||||||
|
|
@ -257,13 +259,15 @@ function M.run_test_case(index, debug)
|
||||||
tc.mled = r.mled
|
tc.mled = r.mled
|
||||||
tc.rss_mb = r.rss_mb
|
tc.rss_mb = r.rss_mb
|
||||||
|
|
||||||
return true
|
on_complete(true)
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param indices? integer[]
|
---@param indices? integer[]
|
||||||
---@param debug boolean?
|
---@param debug boolean?
|
||||||
---@return RanTestCase[]
|
---@param on_each? fun(index: integer, total: integer)
|
||||||
function M.run_all_test_cases(indices, debug)
|
---@param on_done fun(results: RanTestCase[])
|
||||||
|
function M.run_all_test_cases(indices, debug, on_each, on_done)
|
||||||
local to_run = indices
|
local to_run = indices
|
||||||
if not to_run then
|
if not to_run then
|
||||||
to_run = {}
|
to_run = {}
|
||||||
|
|
@ -272,20 +276,26 @@ function M.run_all_test_cases(indices, debug)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for _, i in ipairs(to_run) do
|
local function run_next(pos)
|
||||||
M.run_test_case(i, debug)
|
if pos > #to_run then
|
||||||
end
|
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
('Finished %s %s test cases.'):format(
|
('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', #to_run),
|
||||||
debug and 'debugging' or 'running',
|
|
||||||
#panel_state.test_cases
|
|
||||||
),
|
|
||||||
vim.log.levels.INFO,
|
vim.log.levels.INFO,
|
||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
on_done(panel_state.test_cases)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
return panel_state.test_cases
|
M.run_test_case(to_run[pos], debug, function()
|
||||||
|
if on_each then
|
||||||
|
on_each(pos, #to_run)
|
||||||
|
end
|
||||||
|
run_next(pos + 1)
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
run_next(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return PanelState
|
---@return PanelState
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
|
local function strwidth(s)
|
||||||
|
return vim.api.nvim_strwidth(s)
|
||||||
|
end
|
||||||
|
|
||||||
local exit_code_names = {
|
local exit_code_names = {
|
||||||
[128] = 'SIGHUP',
|
[128] = 'SIGHUP',
|
||||||
[129] = 'SIGINT',
|
[129] = 'SIGINT',
|
||||||
|
|
@ -26,6 +30,12 @@ local exit_code_names = {
|
||||||
---@param ran_test_case RanTestCase
|
---@param ran_test_case RanTestCase
|
||||||
---@return StatusInfo
|
---@return StatusInfo
|
||||||
function M.get_status_info(ran_test_case)
|
function M.get_status_info(ran_test_case)
|
||||||
|
if ran_test_case.status == 'pending' then
|
||||||
|
return { text = '...', highlight_group = 'CpTestNA' }
|
||||||
|
elseif ran_test_case.status == 'running' then
|
||||||
|
return { text = 'RUN', highlight_group = 'CpTestNA' }
|
||||||
|
end
|
||||||
|
|
||||||
if ran_test_case.ok then
|
if ran_test_case.ok then
|
||||||
return { text = 'AC', highlight_group = 'CpTestAC' }
|
return { text = 'AC', highlight_group = 'CpTestAC' }
|
||||||
end
|
end
|
||||||
|
|
@ -34,7 +44,7 @@ function M.get_status_info(ran_test_case)
|
||||||
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
||||||
elseif ran_test_case.mled then
|
elseif ran_test_case.mled then
|
||||||
return { text = 'MLE', highlight_group = 'CpTestMLE' }
|
return { text = 'MLE', highlight_group = 'CpTestMLE' }
|
||||||
elseif ran_test_case.code > 0 and ran_test_case.code >= 128 then
|
elseif ran_test_case.code and ran_test_case.code >= 128 then
|
||||||
return { text = 'RTE', highlight_group = 'CpTestRTE' }
|
return { text = 'RTE', highlight_group = 'CpTestRTE' }
|
||||||
elseif ran_test_case.code == 0 and not ran_test_case.ok then
|
elseif ran_test_case.code == 0 and not ran_test_case.ok then
|
||||||
return { text = 'WA', highlight_group = 'CpTestWA' }
|
return { text = 'WA', highlight_group = 'CpTestWA' }
|
||||||
|
|
@ -63,24 +73,24 @@ local function compute_cols(test_state)
|
||||||
|
|
||||||
for i, tc in ipairs(test_state.test_cases) do
|
for i, tc in ipairs(test_state.test_cases) do
|
||||||
local prefix = (i == test_state.current_index) and '>' or ' '
|
local prefix = (i == test_state.current_index) and '>' or ' '
|
||||||
w.num = math.max(w.num, #(' ' .. prefix .. i .. ' '))
|
w.num = math.max(w.num, strwidth(' ' .. prefix .. i .. ' '))
|
||||||
w.status = math.max(w.status, #(' ' .. M.get_status_info(tc).text .. ' '))
|
w.status = math.max(w.status, strwidth(' ' .. M.get_status_info(tc).text .. ' '))
|
||||||
local time_str = tc.time_ms and string.format('%.2f', tc.time_ms) or '—'
|
local time_str = tc.time_ms and string.format('%.2f', tc.time_ms) or '—'
|
||||||
w.time = math.max(w.time, #(' ' .. time_str .. ' '))
|
w.time = math.max(w.time, strwidth(' ' .. time_str .. ' '))
|
||||||
w.timeout = math.max(w.timeout, #(' ' .. timeout_str .. ' '))
|
w.timeout = math.max(w.timeout, strwidth(' ' .. timeout_str .. ' '))
|
||||||
local rss_str = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or '—'
|
local rss_str = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or '—'
|
||||||
w.rss = math.max(w.rss, #(' ' .. rss_str .. ' '))
|
w.rss = math.max(w.rss, strwidth(' ' .. rss_str .. ' '))
|
||||||
w.memory = math.max(w.memory, #(' ' .. memory_str .. ' '))
|
w.memory = math.max(w.memory, strwidth(' ' .. memory_str .. ' '))
|
||||||
w.exit = math.max(w.exit, #(' ' .. format_exit_code(tc.code) .. ' '))
|
w.exit = math.max(w.exit, strwidth(' ' .. format_exit_code(tc.code) .. ' '))
|
||||||
end
|
end
|
||||||
|
|
||||||
w.num = math.max(w.num, #' # ')
|
w.num = math.max(w.num, strwidth(' # '))
|
||||||
w.status = math.max(w.status, #' Status ')
|
w.status = math.max(w.status, strwidth(' Status '))
|
||||||
w.time = math.max(w.time, #' Runtime (ms) ')
|
w.time = math.max(w.time, strwidth(' Runtime (ms) '))
|
||||||
w.timeout = math.max(w.timeout, #' Time (ms) ')
|
w.timeout = math.max(w.timeout, strwidth(' Time (ms) '))
|
||||||
w.rss = math.max(w.rss, #' RSS (MB) ')
|
w.rss = math.max(w.rss, strwidth(' RSS (MB) '))
|
||||||
w.memory = math.max(w.memory, #' Mem (MB) ')
|
w.memory = math.max(w.memory, strwidth(' Mem (MB) '))
|
||||||
w.exit = math.max(w.exit, #' Exit Code ')
|
w.exit = math.max(w.exit, strwidth(' Exit Code '))
|
||||||
|
|
||||||
local sum = w.num + w.status + w.time + w.timeout + w.rss + w.memory + w.exit
|
local sum = w.num + w.status + w.time + w.timeout + w.rss + w.memory + w.exit
|
||||||
local inner = sum + 6
|
local inner = sum + 6
|
||||||
|
|
@ -89,7 +99,7 @@ local function compute_cols(test_state)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function center(text, width)
|
local function center(text, width)
|
||||||
local pad = width - #text
|
local pad = width - strwidth(text)
|
||||||
if pad <= 0 then
|
if pad <= 0 then
|
||||||
return text
|
return text
|
||||||
end
|
end
|
||||||
|
|
@ -101,7 +111,7 @@ local function format_num_column(prefix, idx, width)
|
||||||
local num_str = tostring(idx)
|
local num_str = tostring(idx)
|
||||||
local content = (#num_str == 1) and (' ' .. prefix .. ' ' .. num_str .. ' ')
|
local content = (#num_str == 1) and (' ' .. prefix .. ' ' .. num_str .. ' ')
|
||||||
or (' ' .. prefix .. num_str .. ' ')
|
or (' ' .. prefix .. num_str .. ' ')
|
||||||
local total_pad = width - #content
|
local total_pad = width - strwidth(content)
|
||||||
if total_pad <= 0 then
|
if total_pad <= 0 then
|
||||||
return content
|
return content
|
||||||
end
|
end
|
||||||
|
|
@ -314,10 +324,10 @@ function M.render_test_list(test_state)
|
||||||
|
|
||||||
for _, input_line in ipairs(vim.split(tc.input, '\n', { plain = true, trimempty = false })) do
|
for _, input_line in ipairs(vim.split(tc.input, '\n', { plain = true, trimempty = false })) do
|
||||||
local s = input_line or ''
|
local s = input_line or ''
|
||||||
if #s > c.inner then
|
if strwidth(s) > c.inner then
|
||||||
s = string.sub(s, 1, c.inner)
|
s = string.sub(s, 1, c.inner)
|
||||||
end
|
end
|
||||||
local pad = c.inner - #s
|
local pad = c.inner - strwidth(s)
|
||||||
table.insert(lines, '│' .. s .. string.rep(' ', pad) .. '│')
|
table.insert(lines, '│' .. s .. string.rep(' ', pad) .. '│')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -81,15 +81,26 @@ function M.toggle_interactive(interactor_cmd)
|
||||||
|
|
||||||
local execute = require('cp.runner.execute')
|
local execute = require('cp.runner.execute')
|
||||||
local run = require('cp.runner.run')
|
local run = require('cp.runner.run')
|
||||||
local compile_result = execute.compile_problem()
|
|
||||||
|
local function restore_session()
|
||||||
|
if state.saved_interactive_session then
|
||||||
|
vim.cmd.source(state.saved_interactive_session)
|
||||||
|
vim.fn.delete(state.saved_interactive_session)
|
||||||
|
state.saved_interactive_session = nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
execute.compile_problem(false, function(compile_result)
|
||||||
if not compile_result.success then
|
if not compile_result.success then
|
||||||
run.handle_compilation_failure(compile_result.output)
|
run.handle_compilation_failure(compile_result.output)
|
||||||
|
restore_session()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local binary = state.get_binary_file()
|
local binary = state.get_binary_file()
|
||||||
if not binary or binary == '' then
|
if not binary or binary == '' then
|
||||||
logger.log('No binary produced.', vim.log.levels.ERROR)
|
logger.log('No binary produced.', vim.log.levels.ERROR)
|
||||||
|
restore_session()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -104,14 +115,11 @@ function M.toggle_interactive(interactor_cmd)
|
||||||
("Interactor '%s' is not executable."):format(interactor_cmd),
|
("Interactor '%s' is not executable."):format(interactor_cmd),
|
||||||
vim.log.levels.ERROR
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
if state.saved_interactive_session then
|
restore_session()
|
||||||
vim.cmd.source(state.saved_interactive_session)
|
|
||||||
vim.fn.delete(state.saved_interactive_session)
|
|
||||||
state.saved_interactive_session = nil
|
|
||||||
end
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local orchestrator = vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/interact.py', ':p')
|
local orchestrator =
|
||||||
|
vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/interact.py', ':p')
|
||||||
cmdline = table.concat({
|
cmdline = table.concat({
|
||||||
'uv',
|
'uv',
|
||||||
'run',
|
'run',
|
||||||
|
|
@ -139,11 +147,7 @@ function M.toggle_interactive(interactor_cmd)
|
||||||
pcall(vim.fn.jobstop, job)
|
pcall(vim.fn.jobstop, job)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if state.saved_interactive_session then
|
restore_session()
|
||||||
vim.cmd.source(state.saved_interactive_session)
|
|
||||||
vim.fn.delete(state.saved_interactive_session)
|
|
||||||
state.saved_interactive_session = nil
|
|
||||||
end
|
|
||||||
state.interactive_buf = nil
|
state.interactive_buf = nil
|
||||||
state.interactive_win = nil
|
state.interactive_win = nil
|
||||||
state.set_active_panel(nil)
|
state.set_active_panel(nil)
|
||||||
|
|
@ -189,6 +193,7 @@ function M.toggle_interactive(interactor_cmd)
|
||||||
state.interactive_buf = term_buf
|
state.interactive_buf = term_buf
|
||||||
state.interactive_win = term_win
|
state.interactive_win = term_win
|
||||||
state.set_active_panel('interactive')
|
state.set_active_panel('interactive')
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return integer, integer
|
---@return integer, integer
|
||||||
|
|
@ -464,6 +469,158 @@ function M.ensure_io_view()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function render_io_view_results(io_state, test_indices, mode, combined_result, combined_input)
|
||||||
|
local run = require('cp.runner.run')
|
||||||
|
local run_render = require('cp.runner.run_render')
|
||||||
|
local cfg = config_module.get_config()
|
||||||
|
|
||||||
|
run_render.setup_highlights()
|
||||||
|
|
||||||
|
local input_lines = {}
|
||||||
|
local output_lines = {}
|
||||||
|
local verdict_lines = {}
|
||||||
|
local verdict_highlights = {}
|
||||||
|
local formatter = cfg.ui.run.format_verdict
|
||||||
|
local test_state = run.get_panel_state()
|
||||||
|
|
||||||
|
if mode == 'combined' and combined_result then
|
||||||
|
input_lines = vim.split(combined_input, '\n')
|
||||||
|
|
||||||
|
if combined_result.actual and combined_result.actual ~= '' then
|
||||||
|
output_lines = vim.split(combined_result.actual, '\n')
|
||||||
|
end
|
||||||
|
|
||||||
|
local status = run_render.get_status_info(combined_result)
|
||||||
|
local format_data = {
|
||||||
|
index = 1,
|
||||||
|
status = status,
|
||||||
|
time_ms = combined_result.time_ms or 0,
|
||||||
|
time_limit_ms = test_state.constraints and test_state.constraints.timeout_ms or 0,
|
||||||
|
memory_mb = combined_result.rss_mb or 0,
|
||||||
|
memory_limit_mb = test_state.constraints and test_state.constraints.memory_mb or 0,
|
||||||
|
exit_code = combined_result.code or 0,
|
||||||
|
signal = (combined_result.code and combined_result.code >= 128)
|
||||||
|
and require('cp.constants').signal_codes[combined_result.code]
|
||||||
|
or nil,
|
||||||
|
time_actual_width = #string.format('%.2f', combined_result.time_ms or 0),
|
||||||
|
time_limit_width = #tostring(
|
||||||
|
test_state.constraints and test_state.constraints.timeout_ms or 0
|
||||||
|
),
|
||||||
|
mem_actual_width = #string.format('%.0f', combined_result.rss_mb or 0),
|
||||||
|
mem_limit_width = #string.format(
|
||||||
|
'%.0f',
|
||||||
|
test_state.constraints and test_state.constraints.memory_mb or 0
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
local verdict_result = formatter(format_data)
|
||||||
|
table.insert(verdict_lines, verdict_result.line)
|
||||||
|
|
||||||
|
if verdict_result.highlights then
|
||||||
|
for _, hl in ipairs(verdict_result.highlights) do
|
||||||
|
table.insert(verdict_highlights, {
|
||||||
|
line_offset = #verdict_lines - 1,
|
||||||
|
col_start = hl.col_start,
|
||||||
|
col_end = hl.col_end,
|
||||||
|
group = hl.group,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
local max_time_actual, max_time_limit, max_mem_actual, max_mem_limit = 0, 0, 0, 0
|
||||||
|
|
||||||
|
for _, idx in ipairs(test_indices) do
|
||||||
|
local tc = test_state.test_cases[idx]
|
||||||
|
max_time_actual = math.max(max_time_actual, #string.format('%.2f', tc.time_ms or 0))
|
||||||
|
max_time_limit = math.max(
|
||||||
|
max_time_limit,
|
||||||
|
#tostring(test_state.constraints and test_state.constraints.timeout_ms or 0)
|
||||||
|
)
|
||||||
|
max_mem_actual = math.max(max_mem_actual, #string.format('%.0f', tc.rss_mb or 0))
|
||||||
|
max_mem_limit = math.max(
|
||||||
|
max_mem_limit,
|
||||||
|
#string.format('%.0f', test_state.constraints and test_state.constraints.memory_mb or 0)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
local all_outputs = {}
|
||||||
|
for _, idx in ipairs(test_indices) do
|
||||||
|
local tc = test_state.test_cases[idx]
|
||||||
|
for _, line in ipairs(vim.split(tc.input, '\n')) do
|
||||||
|
table.insert(input_lines, line)
|
||||||
|
end
|
||||||
|
if tc.actual then
|
||||||
|
table.insert(all_outputs, tc.actual)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local combined_output = table.concat(all_outputs, '')
|
||||||
|
if combined_output ~= '' then
|
||||||
|
for _, line in ipairs(vim.split(combined_output, '\n')) do
|
||||||
|
table.insert(output_lines, line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
for _, idx in ipairs(test_indices) do
|
||||||
|
local tc = test_state.test_cases[idx]
|
||||||
|
local status = run_render.get_status_info(tc)
|
||||||
|
|
||||||
|
local format_data = {
|
||||||
|
index = idx,
|
||||||
|
status = status,
|
||||||
|
time_ms = tc.time_ms or 0,
|
||||||
|
time_limit_ms = test_state.constraints and test_state.constraints.timeout_ms or 0,
|
||||||
|
memory_mb = tc.rss_mb or 0,
|
||||||
|
memory_limit_mb = test_state.constraints and test_state.constraints.memory_mb or 0,
|
||||||
|
exit_code = tc.code or 0,
|
||||||
|
signal = (tc.code and tc.code >= 128) and require('cp.constants').signal_codes[tc.code]
|
||||||
|
or nil,
|
||||||
|
time_actual_width = max_time_actual,
|
||||||
|
time_limit_width = max_time_limit,
|
||||||
|
mem_actual_width = max_mem_actual,
|
||||||
|
mem_limit_width = max_mem_limit,
|
||||||
|
}
|
||||||
|
|
||||||
|
local result = formatter(format_data)
|
||||||
|
table.insert(verdict_lines, result.line)
|
||||||
|
|
||||||
|
if result.highlights then
|
||||||
|
for _, hl in ipairs(result.highlights) do
|
||||||
|
table.insert(verdict_highlights, {
|
||||||
|
line_offset = #verdict_lines - 1,
|
||||||
|
col_start = hl.col_start,
|
||||||
|
col_end = hl.col_end,
|
||||||
|
group = hl.group,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #output_lines > 0 and #verdict_lines > 0 then
|
||||||
|
table.insert(output_lines, '')
|
||||||
|
end
|
||||||
|
|
||||||
|
local verdict_start = #output_lines
|
||||||
|
for _, line in ipairs(verdict_lines) do
|
||||||
|
table.insert(output_lines, line)
|
||||||
|
end
|
||||||
|
|
||||||
|
local final_highlights = {}
|
||||||
|
for _, vh in ipairs(verdict_highlights) do
|
||||||
|
table.insert(final_highlights, {
|
||||||
|
line = verdict_start + vh.line_offset,
|
||||||
|
col_start = vh.col_start,
|
||||||
|
col_end = vh.col_end,
|
||||||
|
highlight_group = vh.group,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
utils.update_buffer_content(io_state.input_buf, input_lines, nil, nil)
|
||||||
|
local output_ns = vim.api.nvim_create_namespace('cp_io_view_output')
|
||||||
|
utils.update_buffer_content(io_state.output_buf, output_lines, final_highlights, output_ns)
|
||||||
|
end
|
||||||
|
|
||||||
function M.run_io_view(test_indices_arg, debug, mode)
|
function M.run_io_view(test_indices_arg, debug, mode)
|
||||||
logger.log(('%s tests...'):format(debug and 'Debugging' or 'Running'), vim.log.levels.INFO, true)
|
logger.log(('%s tests...'):format(debug and 'Debugging' or 'Running'), vim.log.levels.INFO, true)
|
||||||
|
|
||||||
|
|
@ -544,20 +701,25 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local config = config_module.get_config()
|
local cfg = config_module.get_config()
|
||||||
|
|
||||||
if config.ui.ansi then
|
if cfg.ui.ansi then
|
||||||
require('cp.ui.ansi').setup_highlight_groups()
|
require('cp.ui.ansi').setup_highlight_groups()
|
||||||
end
|
end
|
||||||
|
|
||||||
local execute = require('cp.runner.execute')
|
local execute = require('cp.runner.execute')
|
||||||
local compile_result = execute.compile_problem(debug)
|
|
||||||
|
execute.compile_problem(debug, function(compile_result)
|
||||||
|
if not vim.api.nvim_buf_is_valid(io_state.output_buf) then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if not compile_result.success then
|
if not compile_result.success then
|
||||||
local ansi = require('cp.ui.ansi')
|
local ansi = require('cp.ui.ansi')
|
||||||
local output = compile_result.output or ''
|
local output = compile_result.output or ''
|
||||||
local lines, highlights
|
local lines, highlights
|
||||||
|
|
||||||
if config.ui.ansi then
|
if cfg.ui.ansi then
|
||||||
local parsed = ansi.parse_ansi_text(output)
|
local parsed = ansi.parse_ansi_text(output)
|
||||||
lines = parsed.lines
|
lines = parsed.lines
|
||||||
highlights = parsed.highlights
|
highlights = parsed.highlights
|
||||||
|
|
@ -571,19 +733,8 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local run_render = require('cp.runner.run_render')
|
|
||||||
run_render.setup_highlights()
|
|
||||||
|
|
||||||
local input_lines = {}
|
|
||||||
local output_lines = {}
|
|
||||||
local verdict_lines = {}
|
|
||||||
local verdict_highlights = {}
|
|
||||||
|
|
||||||
local formatter = config.ui.run.format_verdict
|
|
||||||
|
|
||||||
if mode == 'combined' then
|
if mode == 'combined' then
|
||||||
local combined = cache.get_combined_test(platform, contest_id, problem_id)
|
local combined = cache.get_combined_test(platform, contest_id, problem_id)
|
||||||
|
|
||||||
if not combined then
|
if not combined then
|
||||||
logger.log('No combined test found', vim.log.levels.ERROR)
|
logger.log('No combined test found', vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
|
|
@ -591,161 +742,24 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
||||||
|
|
||||||
run.load_test_cases()
|
run.load_test_cases()
|
||||||
|
|
||||||
local result = run.run_combined_test(debug)
|
run.run_combined_test(debug, function(result)
|
||||||
|
|
||||||
if not result then
|
if not result then
|
||||||
logger.log('Failed to run combined test', vim.log.levels.ERROR)
|
logger.log('Failed to run combined test', vim.log.levels.ERROR)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
input_lines = vim.split(combined.input, '\n')
|
if vim.api.nvim_buf_is_valid(io_state.output_buf) then
|
||||||
|
render_io_view_results(io_state, test_indices, mode, result, combined.input)
|
||||||
if result.actual and result.actual ~= '' then
|
|
||||||
output_lines = vim.split(result.actual, '\n')
|
|
||||||
end
|
|
||||||
|
|
||||||
local status = run_render.get_status_info(result)
|
|
||||||
local test_state = run.get_panel_state()
|
|
||||||
|
|
||||||
---@type VerdictFormatData
|
|
||||||
local format_data = {
|
|
||||||
index = 1,
|
|
||||||
status = status,
|
|
||||||
time_ms = result.time_ms or 0,
|
|
||||||
time_limit_ms = test_state.constraints and test_state.constraints.timeout_ms or 0,
|
|
||||||
memory_mb = result.rss_mb or 0,
|
|
||||||
memory_limit_mb = test_state.constraints and test_state.constraints.memory_mb or 0,
|
|
||||||
exit_code = result.code or 0,
|
|
||||||
signal = (result.code and result.code >= 128)
|
|
||||||
and require('cp.constants').signal_codes[result.code]
|
|
||||||
or nil,
|
|
||||||
time_actual_width = #string.format('%.2f', result.time_ms or 0),
|
|
||||||
time_limit_width = #tostring(
|
|
||||||
test_state.constraints and test_state.constraints.timeout_ms or 0
|
|
||||||
),
|
|
||||||
mem_actual_width = #string.format('%.0f', result.rss_mb or 0),
|
|
||||||
mem_limit_width = #string.format(
|
|
||||||
'%.0f',
|
|
||||||
test_state.constraints and test_state.constraints.memory_mb or 0
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
local verdict_result = formatter(format_data)
|
|
||||||
table.insert(verdict_lines, verdict_result.line)
|
|
||||||
|
|
||||||
if verdict_result.highlights then
|
|
||||||
for _, hl in ipairs(verdict_result.highlights) do
|
|
||||||
table.insert(verdict_highlights, {
|
|
||||||
line_offset = #verdict_lines - 1,
|
|
||||||
col_start = hl.col_start,
|
|
||||||
col_end = hl.col_end,
|
|
||||||
group = hl.group,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
else
|
else
|
||||||
run.run_all_test_cases(test_indices, debug)
|
run.run_all_test_cases(test_indices, debug, nil, function()
|
||||||
local test_state = run.get_panel_state()
|
if vim.api.nvim_buf_is_valid(io_state.output_buf) then
|
||||||
|
render_io_view_results(io_state, test_indices, mode, nil, nil)
|
||||||
local max_time_actual = 0
|
|
||||||
local max_time_limit = 0
|
|
||||||
local max_mem_actual = 0
|
|
||||||
local max_mem_limit = 0
|
|
||||||
|
|
||||||
for _, idx in ipairs(test_indices) do
|
|
||||||
local tc = test_state.test_cases[idx]
|
|
||||||
max_time_actual = math.max(max_time_actual, #string.format('%.2f', tc.time_ms or 0))
|
|
||||||
max_time_limit = math.max(
|
|
||||||
max_time_limit,
|
|
||||||
#tostring(test_state.constraints and test_state.constraints.timeout_ms or 0)
|
|
||||||
)
|
|
||||||
max_mem_actual = math.max(max_mem_actual, #string.format('%.0f', tc.rss_mb or 0))
|
|
||||||
max_mem_limit = math.max(
|
|
||||||
max_mem_limit,
|
|
||||||
#string.format('%.0f', test_state.constraints and test_state.constraints.memory_mb or 0)
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
local all_outputs = {}
|
|
||||||
for _, idx in ipairs(test_indices) do
|
|
||||||
local tc = test_state.test_cases[idx]
|
|
||||||
|
|
||||||
for _, line in ipairs(vim.split(tc.input, '\n')) do
|
|
||||||
table.insert(input_lines, line)
|
|
||||||
end
|
end
|
||||||
|
end)
|
||||||
if tc.actual then
|
|
||||||
table.insert(all_outputs, tc.actual)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
local combined_output = table.concat(all_outputs, '')
|
|
||||||
if combined_output ~= '' then
|
|
||||||
for _, line in ipairs(vim.split(combined_output, '\n')) do
|
|
||||||
table.insert(output_lines, line)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for _, idx in ipairs(test_indices) do
|
|
||||||
local tc = test_state.test_cases[idx]
|
|
||||||
local status = run_render.get_status_info(tc)
|
|
||||||
|
|
||||||
---@type VerdictFormatData
|
|
||||||
local format_data = {
|
|
||||||
index = idx,
|
|
||||||
status = status,
|
|
||||||
time_ms = tc.time_ms or 0,
|
|
||||||
time_limit_ms = test_state.constraints and test_state.constraints.timeout_ms or 0,
|
|
||||||
memory_mb = tc.rss_mb or 0,
|
|
||||||
memory_limit_mb = test_state.constraints and test_state.constraints.memory_mb or 0,
|
|
||||||
exit_code = tc.code or 0,
|
|
||||||
signal = (tc.code and tc.code >= 128) and require('cp.constants').signal_codes[tc.code]
|
|
||||||
or nil,
|
|
||||||
time_actual_width = max_time_actual,
|
|
||||||
time_limit_width = max_time_limit,
|
|
||||||
mem_actual_width = max_mem_actual,
|
|
||||||
mem_limit_width = max_mem_limit,
|
|
||||||
}
|
|
||||||
|
|
||||||
local result = formatter(format_data)
|
|
||||||
table.insert(verdict_lines, result.line)
|
|
||||||
|
|
||||||
if result.highlights then
|
|
||||||
for _, hl in ipairs(result.highlights) do
|
|
||||||
table.insert(verdict_highlights, {
|
|
||||||
line_offset = #verdict_lines - 1,
|
|
||||||
col_start = hl.col_start,
|
|
||||||
col_end = hl.col_end,
|
|
||||||
group = hl.group,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
if #output_lines > 0 and #verdict_lines > 0 then
|
|
||||||
table.insert(output_lines, '')
|
|
||||||
end
|
|
||||||
|
|
||||||
local verdict_start = #output_lines
|
|
||||||
for _, line in ipairs(verdict_lines) do
|
|
||||||
table.insert(output_lines, line)
|
|
||||||
end
|
|
||||||
|
|
||||||
local final_highlights = {}
|
|
||||||
for _, vh in ipairs(verdict_highlights) do
|
|
||||||
table.insert(final_highlights, {
|
|
||||||
line = verdict_start + vh.line_offset,
|
|
||||||
col_start = vh.col_start,
|
|
||||||
col_end = vh.col_end,
|
|
||||||
highlight_group = vh.group,
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
utils.update_buffer_content(io_state.input_buf, input_lines, nil, nil)
|
|
||||||
|
|
||||||
local output_ns = vim.api.nvim_create_namespace('cp_io_view_output')
|
|
||||||
utils.update_buffer_content(io_state.output_buf, output_lines, final_highlights, output_ns)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param panel_opts? PanelOpts
|
---@param panel_opts? PanelOpts
|
||||||
|
|
@ -918,16 +932,15 @@ function M.toggle_panel(panel_opts)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
local execute = require('cp.runner.execute')
|
vim.api.nvim_set_current_win(test_windows.tab_win)
|
||||||
local compile_result = execute.compile_problem(panel_opts and panel_opts.debug)
|
state.test_buffers = test_buffers
|
||||||
if compile_result.success then
|
state.test_windows = test_windows
|
||||||
run.run_all_test_cases(nil, panel_opts and panel_opts.debug)
|
state.set_active_panel('run')
|
||||||
else
|
logger.log('test panel opened')
|
||||||
run.handle_compilation_failure(compile_result.output)
|
|
||||||
end
|
|
||||||
|
|
||||||
refresh_panel()
|
refresh_panel()
|
||||||
|
|
||||||
|
local function finalize_panel()
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
if config.ui.ansi then
|
if config.ui.ansi then
|
||||||
require('cp.ui.ansi').setup_highlight_groups()
|
require('cp.ui.ansi').setup_highlight_groups()
|
||||||
|
|
@ -936,12 +949,27 @@ function M.toggle_panel(panel_opts)
|
||||||
update_diff_panes()
|
update_diff_panes()
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
vim.api.nvim_set_current_win(test_windows.tab_win)
|
local execute = require('cp.runner.execute')
|
||||||
state.test_buffers = test_buffers
|
execute.compile_problem(panel_opts and panel_opts.debug, function(compile_result)
|
||||||
state.test_windows = test_windows
|
if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
|
||||||
state.set_active_panel('run')
|
return
|
||||||
logger.log('test panel opened')
|
end
|
||||||
|
|
||||||
|
if compile_result.success then
|
||||||
|
run.run_all_test_cases(nil, panel_opts and panel_opts.debug, function()
|
||||||
|
refresh_panel()
|
||||||
|
end, function()
|
||||||
|
refresh_panel()
|
||||||
|
finalize_panel()
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
run.handle_compilation_failure(compile_result.output)
|
||||||
|
refresh_panel()
|
||||||
|
finalize_panel()
|
||||||
|
end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue