Merge pull request #133 from barrett-ruth/fix/typing

mle support
This commit is contained in:
Barrett Ruth 2025-10-03 05:50:56 +02:00 committed by GitHub
commit 6e01714fe1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 509 additions and 537 deletions

View file

@ -422,7 +422,8 @@ Test cases use competitive programming terminology with color highlighting:
AC Accepted (passed) - Green AC Accepted (passed) - Green
WA Wrong Answer (output mismatch) - Red WA Wrong Answer (output mismatch) - Red
TLE Time Limit Exceeded (timeout) - Orange TLE Time Limit Exceeded (timeout) - Orange
RTE Runtime Error (non-zero exit) - Purple MLE Memory Limit Exceeded Error (heuristic) - Orange
RTE Runtime Error (other non-zero exit code) - Purple
< <
============================================================================== ==============================================================================

View file

@ -15,11 +15,6 @@
---@field name string ---@field name string
---@field id string ---@field id string
---@class CacheData
---@field [string] table<string, ContestData>
---@field file_states? table<string, FileState>
---@field contest_lists? table<string, ContestSummary>
---@class Problem ---@class Problem
---@field id string ---@field id string
---@field name? string ---@field name? string

View file

@ -52,7 +52,9 @@
---@field picker? string|nil ---@field picker? string|nil
local M = {} local M = {}
local constants = require('cp.constants') local constants = require('cp.constants')
local utils = require('cp.utils')
local default_contest_config = { local default_contest_config = {
cpp = { cpp = {
@ -249,6 +251,11 @@ function M.setup(user_config)
end end
end end
local ok, err = utils.check_required_runtime()
if not ok then
error('[cp.nvim] ' .. err)
end
return config return config
end end

View file

@ -2,59 +2,64 @@ local M = {}
local utils = require('cp.utils') local utils = require('cp.utils')
local function check_nvim_version() local function check_required()
vim.health.start('cp.nvim [required] ~')
if vim.fn.has('nvim-0.10.0') == 1 then if vim.fn.has('nvim-0.10.0') == 1 then
vim.health.ok('Neovim 0.10.0+ detected') vim.health.ok('Neovim 0.10.0+ detected')
else else
vim.health.error('cp.nvim requires Neovim 0.10.0+') vim.health.error('cp.nvim requires Neovim 0.10.0+')
end end
end
local function check_uv() local uname = vim.loop.os_uname()
if uname.sysname == 'Windows_NT' then
vim.health.error('Windows is not supported')
end
if vim.fn.executable('uv') == 1 then if vim.fn.executable('uv') == 1 then
vim.health.ok('uv executable found') vim.health.ok('uv executable found')
local r = vim.system({ 'uv', '--version' }, { text = true }):wait()
local result = vim.system({ 'uv', '--version' }, { text = true }):wait() if r.code == 0 then
if result.code == 0 then vim.health.info('uv version: ' .. r.stdout:gsub('\n', ''))
vim.health.info('uv version: ' .. result.stdout:gsub('\n', ''))
end end
else else
vim.health.warn('uv not found - install from https://docs.astral.sh/uv/ for problem scraping') vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)')
end end
end
local function check_python_env()
local plugin_path = utils.get_plugin_path() local plugin_path = utils.get_plugin_path()
local venv_dir = plugin_path .. '/.venv' local venv_dir = plugin_path .. '/.venv'
if vim.fn.isdirectory(venv_dir) == 1 then if vim.fn.isdirectory(venv_dir) == 1 then
vim.health.ok('Python virtual environment found at ' .. venv_dir) vim.health.ok('Python virtual environment found at ' .. venv_dir)
else else
vim.health.warn('Python virtual environment not set up - run :CP command to initialize') vim.health.info('Python virtual environment not set up (created on first scrape)')
end
local cap = utils.time_capability()
if cap.ok then
vim.health.ok('GNU time found: ' .. cap.path)
else
vim.health.error('GNU time not found: ' .. (cap.reason or ''))
end end
end end
local function check_luasnip() local function check_optional()
local has_luasnip, luasnip = pcall(require, 'luasnip') vim.health.start('cp.nvim [optional] ~')
local has_luasnip = pcall(require, 'luasnip')
if has_luasnip then if has_luasnip then
vim.health.ok('LuaSnip integration available') vim.health.ok('LuaSnip integration available')
local snippet_count = #luasnip.get_snippets('all')
vim.health.info('LuaSnip snippets loaded: ' .. snippet_count)
else else
vim.health.info('LuaSnip not available - template expansion will be limited') vim.health.info('LuaSnip not available (templates optional)')
end end
end end
function M.check() function M.check()
local version = require('cp.version') local version = require('cp.version')
vim.health.start('cp.nvim health check') vim.health.start('cp.nvim health check ~')
vim.health.info('Version: ' .. version.version) vim.health.info('Version: ' .. version.version)
check_nvim_version() check_required()
check_uv() check_optional()
check_python_env()
check_luasnip()
end end
return M return M

View file

@ -1,44 +1,36 @@
---@class ExecuteResult ---@class ExecuteResult
---@field stdout string ---@field stdout string
---@field stderr string
---@field code integer ---@field code integer
---@field time_ms number ---@field time_ms number
---@field timed_out boolean ---@field tled boolean
---@field mled boolean
---@field peak_mb number
---@field signal string|nil
local M = {} local M = {}
local logger = require('cp.log')
local constants = require('cp.constants') local constants = require('cp.constants')
local logger = require('cp.log')
local utils = require('cp.utils')
local filetype_to_language = constants.filetype_to_language local filetype_to_language = constants.filetype_to_language
---@param source_file string
---@param contest_config table
---@return string
local function get_language_from_file(source_file, contest_config) local function get_language_from_file(source_file, contest_config)
local extension = vim.fn.fnamemodify(source_file, ':e') local ext = vim.fn.fnamemodify(source_file, ':e')
local language = filetype_to_language[extension] or contest_config.default_language return filetype_to_language[ext] or contest_config.default_language
return language
end end
---@param cmd_template string[]
---@param substitutions table<string, string>
---@return string[]
local function substitute_template(cmd_template, substitutions) local function substitute_template(cmd_template, substitutions)
local result = {} local out = {}
for _, arg in ipairs(cmd_template) do for _, a in ipairs(cmd_template) do
local substituted = arg local s = a
for key, value in pairs(substitutions) do for k, v in pairs(substitutions) do
substituted = substituted:gsub('{' .. key .. '}', value) s = s:gsub('{' .. k .. '}', v)
end end
table.insert(result, substituted) table.insert(out, s)
end end
return result return out
end end
---@param cmd_template string[]
---@param executable? string
---@param substitutions table<string, string>
---@return string[]
local function build_command(cmd_template, executable, substitutions) local function build_command(cmd_template, executable, substitutions)
local cmd = substitute_template(cmd_template, substitutions) local cmd = substitute_template(cmd_template, substitutions)
if executable then if executable then
@ -47,247 +39,179 @@ local function build_command(cmd_template, executable, substitutions)
return cmd return cmd
end end
---@param language_config table function M.compile(language_config, substitutions)
---@param substitutions table<string, string>
---@return {code: integer, stdout: string, stderr: string}
function M.compile_generic(language_config, substitutions)
if not language_config.compile then if not language_config.compile then
logger.log('No compilation step required for language - skipping.') return { code = 0, stdout = '' }
return { code = 0, stderr = '' }
end end
local compile_cmd = substitute_template(language_config.compile, substitutions) local cmd = substitute_template(language_config.compile, substitutions)
local redirected_cmd = vim.deepcopy(compile_cmd) local sh = table.concat(cmd, ' ') .. ' 2>&1'
if #redirected_cmd > 0 then
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
end
local start_time = vim.uv.hrtime() local t0 = vim.uv.hrtime()
local result = vim local r = vim.system({ 'sh', '-c', sh }, { text = false }):wait()
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, { text = false }) local dt = (vim.uv.hrtime() - t0) / 1e6
:wait()
local compile_time = (vim.uv.hrtime() - start_time) / 1000000
local ansi = require('cp.ui.ansi') local ansi = require('cp.ui.ansi')
result.stdout = ansi.bytes_to_string(result.stdout or '') r.stdout = ansi.bytes_to_string(r.stdout or '')
result.stderr = ansi.bytes_to_string(result.stderr or '')
if result.code == 0 then if r.code == 0 then
logger.log(('Compilation successful in %.1fms.'):format(compile_time), vim.log.levels.INFO) logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO)
else else
logger.log(('Compilation failed in %.1fms.'):format(compile_time)) logger.log(('Compilation failed in %.1fms.'):format(dt))
end end
return result return r
end end
---@param cmd string[] local function parse_and_strip_time_v(output, memory_mb)
---@param input_data string local lines = vim.split(output or '', '\n', { plain = true })
---@param timeout_ms number
---@return ExecuteResult local timing_idx
local function execute_command(cmd, input_data, timeout_ms) for i = #lines, 1, -1 do
local redirected_cmd = vim.deepcopy(cmd) if lines[i]:match('^%s*Command being timed:') then
if #redirected_cmd > 0 then timing_idx = i
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1' break
end
end
if not timing_idx then
while #lines > 0 and lines[#lines]:match('^%s*$') do
table.remove(lines, #lines)
end
return table.concat(lines, '\n'), 0, false
end end
local start_time = vim.uv.hrtime() local start_idx = timing_idx
local k = timing_idx - 1
while k >= 1 and lines[k]:match('^%s*Command ') do
start_idx = k
k = k - 1
end
local result = vim local peak_mb, mled = 0, false
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, { for j = timing_idx, #lines do
stdin = input_data, local kb = lines[j]:match('Maximum resident set size %(kbytes%):%s*(%d+)')
if kb then
peak_mb = tonumber(kb) / 1024.0
if memory_mb and memory_mb > 0 and peak_mb > memory_mb then
mled = true
end
end
end
for j = #lines, start_idx, -1 do
table.remove(lines, j)
end
while #lines > 0 and lines[#lines]:match('^%s*$') do
table.remove(lines, #lines)
end
return table.concat(lines, '\n'), peak_mb, mled
end
function M.run(cmd, stdin, timeout_ms, memory_mb)
local prog = table.concat(cmd, ' ')
local pre = {}
if memory_mb and memory_mb > 0 then
table.insert(pre, ('ulimit -v %d'):format(memory_mb * 1024))
end
local prefix = (#pre > 0) and (table.concat(pre, '; ') .. '; ') or ''
local time_bin = utils.time_path()
local sh = prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
local t0 = vim.uv.hrtime()
local r = vim
.system({ 'sh', '-c', sh }, {
stdin = stdin,
timeout = timeout_ms, timeout = timeout_ms,
text = true, text = true,
}) })
:wait() :wait()
local dt = (vim.uv.hrtime() - t0) / 1e6
local end_time = vim.uv.hrtime() local code = r.code or 0
local execution_time = (end_time - start_time) / 1000000 local raw = r.stdout or ''
local cleaned, peak_mb = parse_and_strip_time_v(raw)
local tled = code == 124
local actual_code = result.code or 0 local signal = nil
if code >= 128 then
signal = constants.signal_codes[code]
end
if result.code == 124 then local lower = (cleaned or ''):lower()
logger.log(('Execution timed out in %.1fms.'):format(execution_time), vim.log.levels.WARN) local oom_hint = lower:find('std::bad_alloc', 1, true)
elseif actual_code ~= 0 then or lower:find('cannot allocate memory', 1, true)
logger.log( or lower:find('enomem', 1, true)
('Execution failed in %.1fms (exit code %d).'):format(execution_time, actual_code),
vim.log.levels.WARN local near_cap = false
) if memory_mb and memory_mb > 0 then
near_cap = (peak_mb >= (0.90 * memory_mb))
end
local mled = false
if peak_mb >= memory_mb or near_cap or oom_hint then
mled = true
end
if tled then
logger.log(('Execution timed out in %.1fms.'):format(dt))
elseif mled then
logger.log(('Execution memory limit exceeded in %.1fms.'):format(dt))
elseif code ~= 0 then
logger.log(('Execution failed in %.1fms (exit code %d).'):format(dt, code))
else else
logger.log(('Execution successful in %.1fms.'):format(execution_time)) logger.log(('Execution successful in %.1fms.'):format(dt))
end end
return { return {
stdout = result.stdout or '', stdout = cleaned,
stderr = result.stderr or '', code = code,
code = actual_code, time_ms = dt,
time_ms = execution_time, tled = tled,
timed_out = result.code == 124, mled = mled,
peak_mb = peak_mb,
signal = signal,
} }
end end
---@param exec_result ExecuteResult
---@param expected_file string
---@param is_debug boolean
---@return string
local function format_output(exec_result, expected_file, is_debug)
local output_lines = { exec_result.stdout }
local metadata_lines = {}
if exec_result.timed_out then
table.insert(metadata_lines, '[code]: 124 (TIMEOUT)')
elseif exec_result.code >= 128 then
local signal_name = constants.signal_codes[exec_result.code] or 'SIGNAL'
table.insert(metadata_lines, ('[code]: %d (%s)'):format(exec_result.code, signal_name))
else
table.insert(metadata_lines, ('[code]: %d'):format(exec_result.code))
end
table.insert(metadata_lines, ('[time]: %.2f ms'):format(exec_result.time_ms))
table.insert(metadata_lines, ('[debug]: %s'):format(is_debug and 'true' or 'false'))
if vim.fn.filereadable(expected_file) == 1 and exec_result.code == 0 then
local expected_content = vim.fn.readfile(expected_file)
local actual_lines = vim.split(exec_result.stdout, '\n')
while #actual_lines > 0 and actual_lines[#actual_lines] == '' do
table.remove(actual_lines)
end
local ok = #actual_lines == #expected_content
if ok then
for i, line in ipairs(actual_lines) do
if line ~= expected_content[i] then
ok = false
break
end
end
end
table.insert(metadata_lines, ('[ok]: %s'):format(ok and 'true' or 'false'))
end
return table.concat(output_lines, '') .. '\n' .. table.concat(metadata_lines, '\n')
end
---@param contest_config ContestConfig
---@param is_debug? boolean
---@return {success: boolean, output: string?}
function M.compile_problem(contest_config, is_debug) function M.compile_problem(contest_config, is_debug)
local state = require('cp.state') local state = require('cp.state')
local source_file = state.get_source_file() local source_file = state.get_source_file()
if not source_file then if not source_file then
logger.log('No source file found.', vim.log.levels.ERROR)
return { success = false, output = 'No source file found.' } return { success = false, output = 'No source file found.' }
end end
local language = get_language_from_file(source_file, contest_config) local language = get_language_from_file(source_file, contest_config)
local language_config = contest_config[language] local language_config = contest_config[language]
if not language_config then if not language_config then
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
return { success = false, output = ('No configuration for language %s.'):format(language) } return { success = false, output = ('No configuration for language %s.'):format(language) }
end end
local binary_file = state.get_binary_file() local binary_file = state.get_binary_file()
local substitutions = { local substitutions = { source = source_file, binary = binary_file }
source = source_file,
binary = binary_file,
}
local compile_cmd = (is_debug and language_config.debug) and language_config.debug local chosen = (is_debug and language_config.debug) and language_config.debug
or language_config.compile or language_config.compile
if compile_cmd then if not chosen then
language_config.compile = compile_cmd return { success = true, output = nil }
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
return { success = false, output = compile_result.stdout or 'unknown error' }
end
end end
local saved = language_config.compile
language_config.compile = chosen
local r = M.compile(language_config, substitutions)
language_config.compile = saved
if r.code ~= 0 then
return { success = false, output = r.stdout or 'unknown error' }
end
return { success = true, output = nil } return { success = true, output = nil }
end end
---@param contest_config ContestConfig M._util = {
---@param is_debug boolean get_language_from_file = get_language_from_file,
function M.run_problem(contest_config, is_debug) substitute_template = substitute_template,
local state = require('cp.state') build_command = build_command,
local source_file = state.get_source_file() }
local output_file = state.get_output_file()
if not source_file or not output_file then
logger.log(
('Missing required file paths %s and %s'):format(source_file, output_file),
vim.log.levels.ERROR
)
return
end
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
local language = get_language_from_file(source_file, contest_config)
local language_config = contest_config[language]
if not language_config then
vim.fn.writefile({ 'Error: No configuration for language: ' .. language }, output_file)
return
end
local binary_file = state.get_binary_file()
local substitutions = {
source = source_file,
binary = binary_file,
}
local compile_cmd = is_debug and language_config.debug or language_config.compile
if compile_cmd then
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
vim.fn.writefile({ compile_result.stderr }, output_file)
return
end
end
local input_file = state.get_input_file()
local input_data = ''
if input_file and vim.fn.filereadable(input_file) == 1 then
input_data = table.concat(vim.fn.readfile(input_file), '\n') .. '\n'
end
local cache = require('cp.cache')
cache.load()
local platform = state.get_platform()
local contest_id = state.get_contest_id()
local problem_id = state.get_problem_id()
local expected_file = state.get_expected_file()
if not platform or not contest_id or not expected_file then
logger.log('Configure a contest before running a problem', vim.log.levels.ERROR)
return
end
local timeout_ms, _ = cache.get_constraints(platform, contest_id, problem_id)
timeout_ms = timeout_ms or 2000
local run_cmd = build_command(language_config.test, language_config.executable, substitutions)
local exec_result = execute_command(run_cmd, input_data, timeout_ms)
local formatted_output = format_output(exec_result, expected_file, is_debug)
local output_buf = vim.fn.bufnr(output_file)
if output_buf ~= -1 then
local was_modifiable = vim.api.nvim_get_option_value('modifiable', { buf = output_buf })
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = output_buf })
vim.api.nvim_set_option_value('readonly', false, { buf = output_buf })
vim.api.nvim_set_option_value('modifiable', true, { buf = output_buf })
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.split(formatted_output, '\n'))
vim.api.nvim_set_option_value('modifiable', was_modifiable, { buf = output_buf })
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = output_buf })
vim.api.nvim_buf_call(output_buf, function()
vim.cmd.write()
end)
else
vim.fn.writefile(vim.split(formatted_output, '\n'), output_file)
end
end
return M return M

View file

@ -2,7 +2,7 @@
---@field index number ---@field index number
---@field input string ---@field input string
---@field expected string ---@field expected string
---@field status "pending"|"pass"|"fail"|"running"|"timeout" ---@field status "pending"|"pass"|"fail"|"running"|"tle"|"mle"
---@field actual string? ---@field actual string?
---@field actual_highlights? Highlight[] ---@field actual_highlights? Highlight[]
---@field time_ms number? ---@field time_ms number?
@ -12,7 +12,9 @@
---@field code number? ---@field code number?
---@field ok boolean? ---@field ok boolean?
---@field signal string? ---@field signal string?
---@field timed_out boolean? ---@field tled boolean?
---@field mled boolean?
---@field rss_mb number
---@class ProblemConstraints ---@class ProblemConstraints
---@field timeout_ms number ---@field timeout_ms number
@ -28,6 +30,7 @@
---@field constraints ProblemConstraints? ---@field constraints ProblemConstraints?
local M = {} local M = {}
local cache = require('cp.cache')
local constants = require('cp.constants') local constants = require('cp.constants')
local logger = require('cp.log') local logger = require('cp.log')
@ -42,221 +45,159 @@ local run_panel_state = {
constraints = nil, constraints = nil,
} }
---@param index number
---@param input string
---@param expected string
---@return RanTestCase
local function create_test_case(index, input, expected)
return {
index = index,
input = input,
expected = expected,
status = 'pending',
actual = nil,
time_ms = nil,
error = nil,
selected = true,
}
end
---@param platform string ---@param platform string
---@param contest_id string ---@param contest_id string
---@param problem_id string? ---@param problem_id string|nil
---@return RanTestCase[] ---@return ProblemConstraints|nil
local function parse_test_cases_from_cache(platform, contest_id, problem_id)
local cache = require('cp.cache')
cache.load()
local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id) or {}
if vim.tbl_isempty(cached_test_cases) then
return {}
end
local test_cases = {}
for i, test_case in ipairs(cached_test_cases) do
local index = test_case.index or i
local expected = test_case.expected or test_case.output or ''
table.insert(test_cases, create_test_case(index, test_case.input, expected))
end
return test_cases
end
---@param platform string
---@param contest_id string
---@param problem_id string?
---@return ProblemConstraints?
local function load_constraints_from_cache(platform, contest_id, problem_id) local function load_constraints_from_cache(platform, contest_id, problem_id)
local cache = require('cp.cache')
cache.load() cache.load()
local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id) local timeout_ms, memory_mb = cache.get_constraints(platform, contest_id, problem_id)
if timeout_ms and memory_mb then if timeout_ms and memory_mb then
return { return { timeout_ms = timeout_ms, memory_mb = memory_mb }
timeout_ms = timeout_ms,
memory_mb = memory_mb,
}
end end
return nil return nil
end end
---@param test_cases TestCase[]
---@return RanTestCase[]
local function create_sentinal_panel_data(test_cases)
local out = {}
for i, tc in ipairs(test_cases) do
out[i] = {
index = tc.index or i,
input = tc.input or '',
expected = tc.expected or '',
status = 'pending',
selected = false,
}
end
return out
end
---@param language_config LanguageConfig
---@param substitutions table<string, string>
---@return string[]
local function build_command(language_config, substitutions)
local exec_util = require('cp.runner.execute')._util
return exec_util.build_command(language_config.test, language_config.executable, substitutions)
end
---@param contest_config ContestConfig ---@param contest_config ContestConfig
---@param cp_config cp.Config
---@param test_case RanTestCase ---@param test_case RanTestCase
---@return table ---@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 }
local function run_single_test_case(contest_config, cp_config, test_case) local function run_single_test_case(contest_config, cp_config, test_case)
local state = require('cp.state') local state = require('cp.state')
local exec = require('cp.runner.execute')
local source_file = state.get_source_file() local source_file = state.get_source_file()
local ext = vim.fn.fnamemodify(source_file or '', ':e')
local language = vim.fn.fnamemodify(source_file or '', ':e') local lang_name = constants.filetype_to_language[ext] or contest_config.default_language
local language_name = constants.filetype_to_language[language] or contest_config.default_language local language_config = contest_config[lang_name]
local language_config = contest_config[language_name]
local function substitute_template(cmd_template, substitutions)
local result = {}
for _, arg in ipairs(cmd_template) do
local substituted = arg
for key, value in pairs(substitutions) do
substituted = substituted:gsub('{' .. key .. '}', value)
end
table.insert(result, substituted)
end
return result
end
local function build_command(cmd_template, executable, substitutions)
local cmd = substitute_template(cmd_template, substitutions)
if executable then
table.insert(cmd, 1, executable)
end
return cmd
end
local binary_file = state.get_binary_file() local binary_file = state.get_binary_file()
local substitutions = { local substitutions = { source = source_file, binary = binary_file }
source = source_file,
binary = binary_file,
}
if language_config.compile and binary_file and vim.fn.filereadable(binary_file) == 0 then if language_config.compile and binary_file and vim.fn.filereadable(binary_file) == 0 then
logger.log('Binary not found - compiling first.') local cr = exec.compile(language_config, substitutions)
local compile_cmd = substitute_template(language_config.compile, substitutions)
local redirected_cmd = vim.deepcopy(compile_cmd)
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
local compile_result = vim
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, { text = false })
:wait()
local ansi = require('cp.ui.ansi') local ansi = require('cp.ui.ansi')
compile_result.stdout = ansi.bytes_to_string(compile_result.stdout or '') local clean = ansi.bytes_to_string(cr.stdout or '')
compile_result.stderr = ansi.bytes_to_string(compile_result.stderr or '') if cr.code ~= 0 then
if compile_result.code ~= 0 then
return { return {
status = 'fail', status = 'fail',
actual = '', actual = clean,
error = 'Compilation failed: ' .. (compile_result.stdout or 'Unknown error'), actual_highlights = {},
stderr = compile_result.stdout or '', error = 'Compilation failed',
stderr = clean,
time_ms = 0, time_ms = 0,
code = compile_result.code, rss_mb = 0,
code = cr.code,
ok = false, ok = false,
signal = nil, signal = nil,
timed_out = false, tled = false,
actual_highlights = {}, mled = false,
} }
end end
end end
local run_cmd = build_command(language_config.test, language_config.executable, substitutions) local cmd = build_command(language_config, substitutions)
local stdin_content = (test_case.input or '') .. '\n'
local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms)
or 2000
local memory_mb = run_panel_state.constraints and run_panel_state.constraints.memory_mb or nil
local stdin_content = test_case.input .. '\n' local r = exec.run(cmd, stdin_content, timeout_ms, memory_mb)
local start_time = vim.uv.hrtime()
local timeout_ms = run_panel_state.constraints and run_panel_state.constraints.timeout_ms or 2000
local redirected_run_cmd = vim.deepcopy(run_cmd)
redirected_run_cmd[#redirected_run_cmd] = redirected_run_cmd[#redirected_run_cmd] .. ' 2>&1'
local result = vim
.system({ 'sh', '-c', table.concat(redirected_run_cmd, ' ') }, {
stdin = stdin_content,
timeout = timeout_ms,
text = false,
})
:wait()
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
local ansi = require('cp.ui.ansi') local ansi = require('cp.ui.ansi')
local stdout_str = ansi.bytes_to_string(result.stdout or '') local out = (r.stdout or ''):gsub('\n$', '')
local actual_output = stdout_str:gsub('\n$', '')
local actual_highlights = {} local highlights = {}
if out ~= '' then
if actual_output ~= '' then
if cp_config.run_panel.ansi then if cp_config.run_panel.ansi then
local parsed = ansi.parse_ansi_text(actual_output) local parsed = ansi.parse_ansi_text(out)
actual_output = table.concat(parsed.lines, '\n') out = table.concat(parsed.lines, '\n')
actual_highlights = parsed.highlights highlights = parsed.highlights
else else
actual_output = actual_output:gsub('\027%[[%d;]*[a-zA-Z]', '') out = out:gsub('\027%[[%d;]*[a-zA-Z]', '')
end end
end end
local max_lines = cp_config.run_panel.max_output_lines local max_lines = cp_config.run_panel.max_output_lines
local output_lines = vim.split(actual_output, '\n') local lines = vim.split(out, '\n')
if #output_lines > max_lines then if #lines > max_lines then
local trimmed_lines = {} local trimmed = {}
for i = 1, max_lines do for i = 1, max_lines do
table.insert(trimmed_lines, output_lines[i]) table.insert(trimmed, lines[i])
end end
table.insert(trimmed_lines, string.format('... (output trimmed after %d lines)', max_lines)) table.insert(trimmed, string.format('... (output trimmed after %d lines)', max_lines))
actual_output = table.concat(trimmed_lines, '\n') out = table.concat(trimmed, '\n')
end end
local expected_output = test_case.expected:gsub('\n$', '') local expected = (test_case.expected or ''):gsub('\n$', '')
local ok = actual_output == expected_output local ok = out == expected
local signal = r.signal
if not signal and r.code and r.code >= 128 then
signal = constants.signal_codes[r.code]
end
local status local status
local timed_out = result.code == 143 or result.code == 124 if r.tled then
if timed_out then status = 'tle'
status = 'timeout' elseif r.mled then
elseif result.code == 0 and ok then status = 'mle'
elseif ok then
status = 'pass' status = 'pass'
else else
status = 'fail' status = 'fail'
end end
local signal = nil
if result.code >= 128 then
signal = constants.signal_codes[result.code]
end
return { return {
status = status, status = status,
actual = actual_output, actual = out,
actual_highlights = actual_highlights, actual_highlights = highlights,
error = result.code ~= 0 and actual_output or nil, error = (r.code ~= 0 and not ok) and out or '',
stderr = '', stderr = '',
time_ms = execution_time, time_ms = r.time_ms,
code = result.code, code = r.code,
ok = ok, ok = ok,
signal = signal, signal = signal,
timed_out = timed_out, tled = r.tled or false,
mled = r.mled or false,
rss_mb = r.peak_mb,
} }
end end
---@param state table ---@param state table
---@return boolean ---@return boolean
function M.load_test_cases(state) function M.load_test_cases(state)
local test_cases = parse_test_cases_from_cache( local tcs = cache.get_test_cases(
state.get_platform() or '', state.get_platform() or '',
state.get_contest_id() or '', state.get_contest_id() or '',
state.get_problem_id() state.get_problem_id()
) or {} )
-- TODO: re-fetch/cache-populating mechanism to ge the test cases if not in the cache run_panel_state.test_cases = create_sentinal_panel_data(tcs)
run_panel_state.test_cases = test_cases
run_panel_state.current_index = 1 run_panel_state.current_index = 1
run_panel_state.constraints = load_constraints_from_cache( run_panel_state.constraints = load_constraints_from_cache(
state.get_platform() or '', state.get_platform() or '',
@ -264,33 +205,35 @@ function M.load_test_cases(state)
state.get_problem_id() state.get_problem_id()
) )
logger.log(('Loaded %d test case(s)'):format(#test_cases), vim.log.levels.INFO) logger.log(('Loaded %d test case(s)'):format(#tcs), vim.log.levels.INFO)
return #test_cases > 0 return #tcs > 0
end end
---@param contest_config ContestConfig ---@param contest_config ContestConfig
---@param cp_config cp.Config
---@param index number ---@param index number
---@return boolean ---@return boolean
function M.run_test_case(contest_config, cp_config, index) function M.run_test_case(contest_config, cp_config, index)
local test_case = run_panel_state.test_cases[index] local tc = run_panel_state.test_cases[index]
if not test_case then if not tc then
return false return false
end end
test_case.status = 'running' tc.status = 'running'
local r = run_single_test_case(contest_config, cp_config, tc)
local result = run_single_test_case(contest_config, cp_config, test_case) tc.status = r.status
tc.actual = r.actual
test_case.status = result.status tc.actual_highlights = r.actual_highlights
test_case.actual = result.actual tc.error = r.error
test_case.actual_highlights = result.actual_highlights tc.stderr = r.stderr
test_case.error = result.error tc.time_ms = r.time_ms
test_case.stderr = result.stderr tc.code = r.code
test_case.time_ms = result.time_ms tc.ok = r.ok
test_case.code = result.code tc.signal = r.signal
test_case.ok = result.ok tc.tled = r.tled
test_case.signal = result.signal tc.mled = r.mled
test_case.timed_out = result.timed_out tc.rss_mb = r.rss_mb
return true return true
end end
@ -300,9 +243,9 @@ end
---@return RanTestCase[] ---@return RanTestCase[]
function M.run_all_test_cases(contest_config, cp_config) function M.run_all_test_cases(contest_config, cp_config)
local results = {} local results = {}
for i, _ in ipairs(run_panel_state.test_cases) do for i = 1, #run_panel_state.test_cases do
M.run_test_case(contest_config, cp_config, i) M.run_test_case(contest_config, cp_config, i)
table.insert(results, run_panel_state.test_cases[i]) results[i] = run_panel_state.test_cases[i]
end end
return results return results
end end
@ -312,32 +255,35 @@ function M.get_run_panel_state()
return run_panel_state return run_panel_state
end end
function M.handle_compilation_failure(compilation_output) ---@param output string|nil
---@return nil
function M.handle_compilation_failure(output)
local ansi = require('cp.ui.ansi') local ansi = require('cp.ui.ansi')
local config = require('cp.config').setup() local config = require('cp.config').setup()
local clean_text local txt
local highlights = {} local hl = {}
if config.run_panel.ansi then if config.run_panel.ansi then
local parsed = ansi.parse_ansi_text(compilation_output or '') local p = ansi.parse_ansi_text(output or '')
clean_text = table.concat(parsed.lines, '\n') txt = table.concat(p.lines, '\n')
highlights = parsed.highlights hl = p.highlights
else else
clean_text = (compilation_output or ''):gsub('\027%[[%d;]*[a-zA-Z]', '') txt = (output or ''):gsub('\027%[[%d;]*[a-zA-Z]', '')
end end
for _, test_case in ipairs(run_panel_state.test_cases) do for _, tc in ipairs(run_panel_state.test_cases) do
test_case.status = 'fail' tc.status = 'fail'
test_case.actual = clean_text tc.actual = txt
test_case.actual_highlights = highlights tc.actual_highlights = hl
test_case.error = 'Compilation failed' tc.error = 'Compilation failed'
test_case.stderr = '' tc.stderr = ''
test_case.time_ms = 0 tc.time_ms = 0
test_case.code = 1 tc.code = 1
test_case.ok = false tc.ok = false
test_case.signal = nil tc.signal = ''
test_case.timed_out = false tc.tled = false
tc.mled = false
end end
end end

View file

@ -26,22 +26,22 @@ 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 == 'pass' then if ran_test_case.ok then
return { text = 'AC', highlight_group = 'CpTestAC' } return { text = 'AC', highlight_group = 'CpTestAC' }
elseif ran_test_case.status == 'fail' then end
if ran_test_case.timed_out then
return { text = 'TLE', highlight_group = 'CpTestTLE' } if ran_test_case.actual == '' then
elseif ran_test_case.code and ran_test_case.code >= 128 then
return { text = 'RTE', highlight_group = 'CpTestRTE' }
else
return { text = 'WA', highlight_group = 'CpTestWA' }
end
elseif ran_test_case.status == 'timeout' then
return { text = 'TLE', highlight_group = 'CpTestTLE' }
elseif ran_test_case.status == 'running' then
return { text = '...', highlight_group = 'CpTestPending' } return { text = '...', highlight_group = 'CpTestPending' }
end
if ran_test_case.tled then
return { text = 'TLE', highlight_group = 'CpTestTLE' }
elseif ran_test_case.mled then
return { text = 'MLE', highlight_group = 'CpTestMLE' }
elseif ran_test_case.code and ran_test_case.code >= 128 then
return { text = 'RTE', highlight_group = 'CpTestRTE' }
else else
return { text = '', highlight_group = 'CpTestPending' } return { text = 'WA', highlight_group = 'CpTestWA' }
end end
end end
@ -54,18 +54,16 @@ local function format_exit_code(code)
end end
local function compute_cols(test_state) local function compute_cols(test_state)
local w = { num = 5, status = 8, time = 6, timeout = 8, memory = 8, exit = 11 } local w = { num = 5, status = 8, time = 6, timeout = 8, rss = 8, memory = 8, exit = 11 }
local timeout_str = '' local timeout_str = ''
local memory_str = '' local memory_str = ''
if test_state.constraints then if test_state.constraints then
timeout_str = tostring(test_state.constraints.timeout_ms) timeout_str = tostring(test_state.constraints.timeout_ms)
memory_str = string.format('%.0f', test_state.constraints.memory_mb) memory_str = string.format('%.0f', test_state.constraints.memory_mb)
else
timeout_str = ''
memory_str = ''
end end
vim.print(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, #(' ' .. prefix .. i .. ' '))
@ -73,6 +71,8 @@ local function compute_cols(test_state)
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, #(' ' .. time_str .. ' '))
w.timeout = math.max(w.timeout, #(' ' .. timeout_str .. ' ')) w.timeout = math.max(w.timeout, #(' ' .. timeout_str .. ' '))
local rss_str = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or ''
w.rss = math.max(w.rss, #(' ' .. rss_str .. ' '))
w.memory = math.max(w.memory, #(' ' .. memory_str .. ' ')) w.memory = math.max(w.memory, #(' ' .. memory_str .. ' '))
w.exit = math.max(w.exit, #(' ' .. format_exit_code(tc.code) .. ' ')) w.exit = math.max(w.exit, #(' ' .. format_exit_code(tc.code) .. ' '))
end end
@ -81,11 +81,12 @@ local function compute_cols(test_state)
w.status = math.max(w.status, #' Status ') w.status = math.max(w.status, #' Status ')
w.time = math.max(w.time, #' Runtime (ms) ') w.time = math.max(w.time, #' Runtime (ms) ')
w.timeout = math.max(w.timeout, #' Time (ms) ') w.timeout = math.max(w.timeout, #' Time (ms) ')
w.rss = math.max(w.rss, #' RSS (MB) ')
w.memory = math.max(w.memory, #' Mem (MB) ') w.memory = math.max(w.memory, #' Mem (MB) ')
w.exit = math.max(w.exit, #' Exit Code ') w.exit = math.max(w.exit, #' Exit Code ')
local sum = w.num + w.status + w.time + w.timeout + w.memory + w.exit local sum = w.num + w.status + w.time + w.timeout + w.rss + w.memory + w.exit
local inner = sum + 5 local inner = sum + 6
local total = inner + 2 local total = inner + 2
return { w = w, sum = sum, inner = inner, total = total } return { w = w, sum = sum, inner = inner, total = total }
end end
@ -95,32 +96,19 @@ local function center(text, width)
if pad <= 0 then if pad <= 0 then
return text return text
end end
local left = math.floor(pad / 2) local left = math.ceil(pad / 2)
return string.rep(' ', left) .. text .. string.rep(' ', pad - left) return string.rep(' ', left) .. text .. string.rep(' ', pad - left)
end end
local function right_align(text, width)
local content = (' %s '):format(text)
local pad = width - #content
if pad <= 0 then
return content
end
return string.rep(' ', pad) .. content
end
local function format_num_column(prefix, idx, width) local function format_num_column(prefix, idx, width)
local num_str = tostring(idx) local num_str = tostring(idx)
local content local content = (#num_str == 1) and (' ' .. prefix .. ' ' .. num_str .. ' ')
if #num_str == 1 then or (' ' .. prefix .. num_str .. ' ')
content = ' ' .. prefix .. ' ' .. num_str .. ' '
else
content = ' ' .. prefix .. num_str .. ' '
end
local total_pad = width - #content local total_pad = width - #content
if total_pad <= 0 then if total_pad <= 0 then
return content return content
end end
local left_pad = math.floor(total_pad / 2) local left_pad = math.ceil(total_pad / 2)
local right_pad = total_pad - left_pad local right_pad = total_pad - left_pad
return string.rep(' ', left_pad) .. content .. string.rep(' ', right_pad) return string.rep(' ', left_pad) .. content .. string.rep(' ', right_pad)
end end
@ -136,6 +124,8 @@ local function top_border(c)
.. '' .. ''
.. string.rep('', w.timeout) .. string.rep('', w.timeout)
.. '' .. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory) .. string.rep('', w.memory)
.. '' .. ''
.. string.rep('', w.exit) .. string.rep('', w.exit)
@ -153,6 +143,8 @@ local function row_sep(c)
.. '' .. ''
.. string.rep('', w.timeout) .. string.rep('', w.timeout)
.. '' .. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory) .. string.rep('', w.memory)
.. '' .. ''
.. string.rep('', w.exit) .. string.rep('', w.exit)
@ -170,6 +162,8 @@ local function bottom_border(c)
.. '' .. ''
.. string.rep('', w.timeout) .. string.rep('', w.timeout)
.. '' .. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory) .. string.rep('', w.memory)
.. '' .. ''
.. string.rep('', w.exit) .. string.rep('', w.exit)
@ -187,6 +181,8 @@ local function flat_fence_above(c)
.. '' .. ''
.. string.rep('', w.timeout) .. string.rep('', w.timeout)
.. '' .. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory) .. string.rep('', w.memory)
.. '' .. ''
.. string.rep('', w.exit) .. string.rep('', w.exit)
@ -204,6 +200,8 @@ local function flat_fence_below(c)
.. '' .. ''
.. string.rep('', w.timeout) .. string.rep('', w.timeout)
.. '' .. ''
.. string.rep('', w.rss)
.. ''
.. string.rep('', w.memory) .. string.rep('', w.memory)
.. '' .. ''
.. string.rep('', w.exit) .. string.rep('', w.exit)
@ -225,6 +223,8 @@ local function header_line(c)
.. '' .. ''
.. center('Time (ms)', w.timeout) .. center('Time (ms)', w.timeout)
.. '' .. ''
.. center('RSS (MB)', w.rss)
.. ''
.. center('Mem (MB)', w.memory) .. center('Mem (MB)', w.memory)
.. '' .. ''
.. center('Exit Code', w.exit) .. center('Exit Code', w.exit)
@ -238,33 +238,34 @@ local function data_row(c, idx, tc, is_current, test_state)
local time = tc.time_ms and string.format('%.2f', tc.time_ms) or '' local time = tc.time_ms and string.format('%.2f', tc.time_ms) or ''
local exit = format_exit_code(tc.code) local exit = format_exit_code(tc.code)
local timeout = '' local timeout = ''
local memory = '' local memory = ''
if test_state.constraints then if test_state.constraints then
timeout = tostring(test_state.constraints.timeout_ms) timeout = tostring(test_state.constraints.timeout_ms)
memory = string.format('%.0f', test_state.constraints.memory_mb) memory = string.format('%.0f', test_state.constraints.memory_mb)
else
timeout = ''
memory = ''
end end
local rss = (tc.rss_mb and string.format('%.0f', tc.rss_mb)) or ''
local line = '' local line = ''
.. format_num_column(prefix, idx, w.num) .. format_num_column(prefix, idx, w.num)
.. '' .. ''
.. right_align(status.text, w.status) .. center(status.text, w.status)
.. '' .. ''
.. right_align(time, w.time) .. center(time, w.time)
.. '' .. ''
.. right_align(timeout, w.timeout) .. center(timeout, w.timeout)
.. '' .. ''
.. right_align(memory, w.memory) .. center(rss, w.rss)
.. '' .. ''
.. right_align(exit, w.exit) .. center(memory, w.memory)
.. ''
.. center(exit, w.exit)
.. '' .. ''
local hi local hi
if status.text ~= '' then if status.text ~= '' then
local status_pos = line:find(status.text) local status_pos = line:find(status.text, 1, true)
if status_pos then if status_pos then
hi = { hi = {
col_start = status_pos - 1, col_start = status_pos - 1,
@ -354,6 +355,7 @@ function M.get_highlight_groups()
CpTestAC = { fg = '#10b981' }, CpTestAC = { fg = '#10b981' },
CpTestWA = { fg = '#ef4444' }, CpTestWA = { fg = '#ef4444' },
CpTestTLE = { fg = '#f59e0b' }, CpTestTLE = { fg = '#f59e0b' },
CpTestMLE = { fg = '#f59e0b' },
CpTestRTE = { fg = '#8b5cf6' }, CpTestRTE = { fg = '#8b5cf6' },
CpTestPending = { fg = '#6b7280' }, CpTestPending = { fg = '#6b7280' },
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' }, CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },

View file

@ -1,10 +1,10 @@
local M = {} local M = {}
local buffer_utils = require('cp.utils.buffer') local utils = require('cp.utils')
local function create_none_diff_layout(parent_win, expected_content, actual_content) local function create_none_diff_layout(parent_win, expected_content, actual_content)
local expected_buf = buffer_utils.create_buffer_with_options() local expected_buf = utils.create_buffer_with_options()
local actual_buf = buffer_utils.create_buffer_with_options() local actual_buf = utils.create_buffer_with_options()
vim.api.nvim_set_current_win(parent_win) vim.api.nvim_set_current_win(parent_win)
vim.cmd.split() vim.cmd.split()
@ -24,8 +24,8 @@ local function create_none_diff_layout(parent_win, expected_content, actual_cont
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true }) local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true }) local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
buffer_utils.update_buffer_content(expected_buf, expected_lines, {}) utils.update_buffer_content(expected_buf, expected_lines, {})
buffer_utils.update_buffer_content(actual_buf, actual_lines, {}) utils.update_buffer_content(actual_buf, actual_lines, {})
return { return {
buffers = { expected_buf, actual_buf }, buffers = { expected_buf, actual_buf },
@ -40,8 +40,8 @@ local function create_none_diff_layout(parent_win, expected_content, actual_cont
end end
local function create_vim_diff_layout(parent_win, expected_content, actual_content) local function create_vim_diff_layout(parent_win, expected_content, actual_content)
local expected_buf = buffer_utils.create_buffer_with_options() local expected_buf = utils.create_buffer_with_options()
local actual_buf = buffer_utils.create_buffer_with_options() local actual_buf = utils.create_buffer_with_options()
vim.api.nvim_set_current_win(parent_win) vim.api.nvim_set_current_win(parent_win)
vim.cmd.split() vim.cmd.split()
@ -61,8 +61,8 @@ local function create_vim_diff_layout(parent_win, expected_content, actual_conte
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true }) local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true }) local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
buffer_utils.update_buffer_content(expected_buf, expected_lines, {}) utils.update_buffer_content(expected_buf, expected_lines, {})
buffer_utils.update_buffer_content(actual_buf, actual_lines, {}) utils.update_buffer_content(actual_buf, actual_lines, {})
vim.api.nvim_set_option_value('diff', true, { win = expected_win }) vim.api.nvim_set_option_value('diff', true, { win = expected_win })
vim.api.nvim_set_option_value('diff', true, { win = actual_win }) vim.api.nvim_set_option_value('diff', true, { win = actual_win })
@ -88,7 +88,7 @@ local function create_vim_diff_layout(parent_win, expected_content, actual_conte
end end
local function create_git_diff_layout(parent_win, expected_content, actual_content) local function create_git_diff_layout(parent_win, expected_content, actual_content)
local diff_buf = buffer_utils.create_buffer_with_options() local diff_buf = utils.create_buffer_with_options()
vim.api.nvim_set_current_win(parent_win) vim.api.nvim_set_current_win(parent_win)
vim.cmd.split() vim.cmd.split()
@ -109,7 +109,7 @@ local function create_git_diff_layout(parent_win, expected_content, actual_conte
highlight.parse_and_apply_diff(diff_buf, diff_result.raw_diff, diff_namespace) highlight.parse_and_apply_diff(diff_buf, diff_result.raw_diff, diff_namespace)
else else
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true }) local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
buffer_utils.update_buffer_content(diff_buf, lines, {}) utils.update_buffer_content(diff_buf, lines, {})
end end
return { return {
@ -123,9 +123,9 @@ local function create_git_diff_layout(parent_win, expected_content, actual_conte
end end
local function create_single_layout(parent_win, content) local function create_single_layout(parent_win, content)
local buf = buffer_utils.create_buffer_with_options() local buf = utils.create_buffer_with_options()
local lines = vim.split(content, '\n', { plain = true, trimempty = true }) local lines = vim.split(content, '\n', { plain = true, trimempty = true })
buffer_utils.update_buffer_content(buf, lines, {}) utils.update_buffer_content(buf, lines, {})
vim.api.nvim_set_current_win(parent_win) vim.api.nvim_set_current_win(parent_win)
vim.cmd.split() vim.cmd.split()
@ -219,7 +219,7 @@ function M.update_diff_panes(
else else
if desired_mode == 'single' then if desired_mode == 'single' then
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true }) local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
buffer_utils.update_buffer_content( utils.update_buffer_content(
current_diff_layout.buffers[1], current_diff_layout.buffers[1],
lines, lines,
actual_highlights, actual_highlights,
@ -238,7 +238,7 @@ function M.update_diff_panes(
) )
else else
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true }) local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
buffer_utils.update_buffer_content( utils.update_buffer_content(
current_diff_layout.buffers[1], current_diff_layout.buffers[1],
lines, lines,
actual_highlights, actual_highlights,
@ -248,8 +248,8 @@ function M.update_diff_panes(
elseif desired_mode == 'none' then elseif desired_mode == 'none' then
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true }) local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true }) local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
buffer_utils.update_buffer_content(current_diff_layout.buffers[1], expected_lines, {}) utils.update_buffer_content(current_diff_layout.buffers[1], expected_lines, {})
buffer_utils.update_buffer_content( utils.update_buffer_content(
current_diff_layout.buffers[2], current_diff_layout.buffers[2],
actual_lines, actual_lines,
actual_highlights, actual_highlights,
@ -258,8 +258,8 @@ function M.update_diff_panes(
else else
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true }) local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true }) local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
buffer_utils.update_buffer_content(current_diff_layout.buffers[1], expected_lines, {}) utils.update_buffer_content(current_diff_layout.buffers[1], expected_lines, {})
buffer_utils.update_buffer_content( utils.update_buffer_content(
current_diff_layout.buffers[2], current_diff_layout.buffers[2],
actual_lines, actual_lines,
actual_highlights, actual_highlights,

View file

@ -1,10 +1,10 @@
local M = {} local M = {}
local buffer_utils = require('cp.utils.buffer')
local config_module = require('cp.config') local config_module = require('cp.config')
local layouts = require('cp.ui.layouts') local layouts = require('cp.ui.layouts')
local logger = require('cp.log') local logger = require('cp.log')
local state = require('cp.state') local state = require('cp.state')
local utils = require('cp.utils')
local current_diff_layout = nil local current_diff_layout = nil
local current_mode = nil local current_mode = nil
@ -194,7 +194,7 @@ function M.toggle_run_panel(is_debug)
vim.cmd(('mksession! %s'):format(state.saved_session)) vim.cmd(('mksession! %s'):format(state.saved_session))
vim.cmd('silent only') vim.cmd('silent only')
local tab_buf = buffer_utils.create_buffer_with_options() local tab_buf = utils.create_buffer_with_options()
local main_win = vim.api.nvim_get_current_win() local main_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(main_win, tab_buf) vim.api.nvim_win_set_buf(main_win, tab_buf)
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf }) vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf })
@ -224,7 +224,7 @@ function M.toggle_run_panel(is_debug)
run_render.setup_highlights() run_render.setup_highlights()
local test_state = run.get_run_panel_state() local test_state = run.get_run_panel_state()
local tab_lines, tab_highlights = run_render.render_test_list(test_state) local tab_lines, tab_highlights = run_render.render_test_list(test_state)
buffer_utils.update_buffer_content( utils.update_buffer_content(
test_buffers.tab_buf, test_buffers.tab_buf,
tab_lines, tab_lines,
tab_highlights, tab_highlights,

View file

@ -2,6 +2,74 @@ local M = {}
local logger = require('cp.log') local logger = require('cp.log')
local uname = vim.loop.os_uname()
local _time_cached = false
local _time_path = nil
local _time_reason = nil
local function is_windows()
return uname and uname.sysname == 'Windows_NT'
end
local function check_time_is_gnu_time(bin)
local ok = vim.fn.executable(bin) == 1
if not ok then
return false
end
local r = vim.system({ bin, '--version' }, { text = true }):wait()
if r and r.code == 0 and r.stdout and r.stdout:lower():find('gnu time', 1, true) then
return true
end
return false
end
local function find_gnu_time()
if _time_cached then
return _time_path, _time_reason
end
if is_windows() then
_time_cached = true
_time_path = nil
_time_reason = 'unsupported on Windows'
return _time_path, _time_reason
end
local candidates
if uname and uname.sysname == 'Darwin' then
candidates = { 'gtime', '/opt/homebrew/bin/gtime', '/usr/local/bin/gtime' }
else
candidates = { '/usr/bin/time', 'time' }
end
for _, bin in ipairs(candidates) do
if check_time_is_gnu_time(bin) then
_time_cached = true
_time_path = bin
_time_reason = nil
return _time_path, _time_reason
end
end
_time_cached = true
_time_path = nil
_time_reason = 'GNU time not found (install `time` on Linux or `brew install coreutils` on macOS)'
return _time_path, _time_reason
end
---@return string|nil path to GNU time binary
function M.time_path()
local path = find_gnu_time()
return path
end
---@return {ok:boolean, path:string|nil, reason:string|nil}
function M.time_capability()
local path, reason = find_gnu_time()
return { ok = path ~= nil, path = path, reason = reason }
end
---@return string ---@return string
function M.get_plugin_path() function M.get_plugin_path()
local plugin_path = debug.getinfo(1, 'S').source:sub(2) local plugin_path = debug.getinfo(1, 'S').source:sub(2)
@ -28,17 +96,70 @@ function M.setup_python_env()
end end
if vim.fn.isdirectory(venv_dir) == 0 then if vim.fn.isdirectory(venv_dir) == 0 then
logger.log('setting up Python environment for scrapers...') logger.log('Setting up Python environment for scrapers...')
local result = vim.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true }):wait() local result = vim.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true }):wait()
if result.code ~= 0 then if result.code ~= 0 then
logger.log('failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR) logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR)
return false return false
end end
logger.log('Python environment setup complete') logger.log('Python environment setup complete.')
end end
python_env_setup = true python_env_setup = true
return true return true
end end
--- Configure the buffer with good defaults
---@param filetype? string
function M.create_buffer_with_options(filetype)
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf })
vim.api.nvim_set_option_value('readonly', true, { buf = buf })
vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
if filetype then
vim.api.nvim_set_option_value('filetype', filetype, { buf = buf })
end
return buf
end
function M.update_buffer_content(bufnr, lines, highlights, namespace)
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
vim.api.nvim_set_option_value('readonly', false, { buf = bufnr })
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
if highlights and namespace then
local highlight = require('cp.ui.highlight')
highlight.apply_highlights(bufnr, highlights, namespace)
end
end
function M.check_required_runtime()
if is_windows() then
return false, 'Windows is not supported'
end
if vim.fn.has('nvim-0.10.0') ~= 1 then
return false, 'Neovim 0.10.0+ required'
end
local cap = M.time_capability()
if not cap.ok then
return false, 'GNU time not found: ' .. (cap.reason or '')
end
if vim.fn.executable('uv') ~= 1 then
return false, 'uv not found (https://docs.astral.sh/uv/)'
end
if not M.setup_python_env() then
return false, 'failed to set up Python virtual environment'
end
return true
end
return M return M

View file

@ -1,29 +0,0 @@
local M = {}
function M.create_buffer_with_options(filetype)
local buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf })
vim.api.nvim_set_option_value('readonly', true, { buf = buf })
vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
if filetype then
vim.api.nvim_set_option_value('filetype', filetype, { buf = buf })
end
return buf
end
function M.update_buffer_content(bufnr, lines, highlights, namespace)
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
vim.api.nvim_set_option_value('readonly', false, { buf = bufnr })
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
if highlights and namespace then
local highlight = require('cp.ui.highlight')
highlight.apply_highlights(bufnr, highlights, namespace)
end
end
return M