disable scraper disabling

This commit is contained in:
Barrett Ruth 2025-10-04 17:45:49 -04:00
parent 0a320945a0
commit aae98a5796
9 changed files with 88 additions and 166 deletions

View file

@ -120,8 +120,8 @@ Here's an example configuration with lazy.nvim: >lua
cmd = 'CP',
build = 'uv sync',
opts = {
contests = {
default = {
platforms = {
cses = {
cpp = {
compile = { 'g++', '{source}', '-o', '{binary}',
'-std=c++17', '-fdiagnostic-colors=always' },
@ -138,7 +138,6 @@ Here's an example configuration with lazy.nvim: >lua
},
snippets = {},
debug = false,
scrapers = { 'atcoder', 'codeforces', 'cses' },
run_panel = {
ansi = true,
diff_mode = 'vim',
@ -156,23 +155,29 @@ Here's an example configuration with lazy.nvim: >lua
<
By default, all contests are configured to use C++ with the g++ compiler and ISO standard
17. Python is also configured with the system executable python as a non-default option. Consult lua/cp/config.lua for
more information.
17. Python is also configured with the system executable python.
For example, to run CodeForces contests with Python, only the following config
is required:
{
contests = {
platforms = {
codeforces = {
default_langauge = 'python'
default_language = 'python'
}
}
}
Any language is supported, provided the proper configuration. For example, to
run CSES problems with Rust:
{
}
*cp.Config*
Fields: ~
{contests} (table<string,ContestConfig>) Contest configurations.
{platforms} (table<string,PlatformContestConfig>) Contest configurations.
{hooks} (|cp.Hooks|) Hook functions called at various stages.
{snippets} (table[]) LuaSnip snippet definitions.
{debug} (boolean, default: false) Show info messages
@ -189,11 +194,12 @@ is required:
Should return full filename with extension.
(default: concatenates contest_id and problem_id, lowercased)
*cp.ContestConfig*
*cp.PlatformConfig*
Fields: ~
{cpp} (|LanguageConfig|) C++ language configuration.
{python} (|LanguageConfig|) Python language configuration.
{default_language} (string, default: "cpp") Default language for contests.
{default_language} (string, default: "cpp") Default language for
platform contests.
*cp.LanguageConfig*
Fields: ~
@ -201,7 +207,6 @@ is required:
{source}, {binary} placeholders.
{test} (string[]) Test execution command template.
{debug} (string[], optional) Debug compile command template.
{extension} (string) File extension (e.g. "cc", "py").
{executable} (string, optional) Executable name for interpreted languages.
*cp.RunPanelConfig*

View file

@ -1,14 +1,12 @@
---@class LanguageConfig
---@class PlatformLanguageConfig
---@field compile? string[] Compile command template
---@field test string[] Test execution command template
---@field debug? string[] Debug command template
---@field executable? string Executable name
---@field version? number Language version
---@field extension? string File extension
---@class ContestConfig
---@field cpp LanguageConfig
---@field python LanguageConfig
---@class PlatformConfig
---@field [string] PlatformLanguageConfig
---@field default_language? string
---@class Hooks
@ -28,22 +26,20 @@
---@field git DiffGitConfig
---@class cp.Config
---@field platforms table<string, ContestConfig>
---@field platforms table<string, PlatformConfig>
---@field snippets any[]
---@field hooks Hooks
---@field debug boolean
---@field scrapers string[]
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
---@field run_panel RunPanelConfig
---@field diff DiffConfig
---@field picker string|nil
---@class cp.PartialConfig
---@field platforms? table<string, ContestConfig>
---@field platforms? table<string, PlatformConfig>
---@field snippets? any[]
---@field hooks? Hooks
---@field debug? boolean
---@field scrapers? string[]
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
---@field run_panel? RunPanelConfig
---@field diff? DiffConfig
@ -64,7 +60,6 @@ local default_contest_config = {
test = { '{source}' },
debug = { '{source}' },
executable = 'python',
extension = 'py',
},
default_language = 'cpp',
}
@ -111,7 +106,6 @@ function M.setup(user_config)
snippets = { user_config.snippets, { 'table', 'nil' }, true },
hooks = { user_config.hooks, { 'table', 'nil' }, true },
debug = { user_config.debug, { 'boolean', 'nil' }, true },
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
filename = { user_config.filename, { 'function', 'nil' }, true },
run_panel = { user_config.run_panel, { 'table', 'nil' }, true },
diff = { user_config.diff, { 'table', 'nil' }, true },
@ -136,22 +130,6 @@ function M.setup(user_config)
end
end
if user_config.scrapers then
for _, platform_name in ipairs(user_config.scrapers) do
if type(platform_name) ~= 'string' then
error(('Invalid scraper value type. Expected string, got %s'):format(type(platform_name)))
end
if not vim.tbl_contains(constants.PLATFORMS, platform_name) then
error(
("Invalid platform '%s' in scrapers config. Valid platforms: %s"):format(
platform_name,
table.concat(constants.PLATFORMS, ', ')
)
)
end
end
end
if user_config.picker then
if not vim.tbl_contains({ 'telescope', 'fzf-lua' }, user_config.picker) then
error(("Invalid picker '%s'. Must be 'telescope' or 'fzf-lua'"):format(user_config.picker))
@ -206,34 +184,9 @@ function M.setup(user_config)
})
for _, platform_config in pairs(config.platforms) do
for lang_name, lang_config in pairs(platform_config) do
if type(lang_config) == 'table' and not lang_config.extension then
if lang_name == 'cpp' then
lang_config.extension = 'cpp'
elseif lang_name == 'python' then
lang_config.extension = 'py'
end
end
end
vim.print(platform_config)
if not platform_config.default_language then
local available_langs = {}
for lang_name, lang_config in pairs(platform_config) do
if type(lang_config) == 'table' and lang_name ~= 'default_language' then
table.insert(available_langs, lang_name)
end
end
if vim.tbl_isemtpy(available_langs) then
error('No language configurations found')
end
vim.print('sorting langs')
--- arbitrarily break ties
table.sort(available_langs)
vim.print(available_langs)
platform_config.default_language = available_langs[1]
-- arbitrarily choose a language
platform_config.default_language = vim.tbl_keys(platform_config)[1]
end
end

View file

@ -24,6 +24,12 @@ M.canonical_filetypes = {
[M.PYTHON] = 'python',
}
---@type table<string, string>
M.canonical_filetype_to_extension = {
[M.CPP] = 'cc',
[M.PYTHON] = 'py',
}
---@type table<number, string>
M.signal_codes = {
[128] = 'SIGILL',

View file

@ -5,15 +5,11 @@ local logger = require('cp.log')
local state = require('cp.state')
function M.restore_from_current_file()
local current_file = vim.fn.expand('%:p')
if current_file == '' then
logger.log('No file is currently open.', vim.log.levels.ERROR)
return false
end
cache.load()
local current_file = vim.fn.expand('%:p')
local file_state = cache.get_file_state(current_file)
if not file_state then
if current_file or not file_state then
logger.log('No cached state found for current file.', vim.log.levels.ERROR)
return false
end
@ -27,13 +23,9 @@ function M.restore_from_current_file()
)
local setup = require('cp.setup')
if not setup.set_platform(file_state.platform) then
return false
end
local _ = setup.set_platform(file_state.platform)
state.set_contest_id(file_state.contest_id)
state.set_problem_id(file_state.problem_id)
setup.setup_contest(file_state.platform, file_state.contest_id, file_state.problem_id)
return true

View file

@ -7,26 +7,28 @@
---@field peak_mb number
---@field signal string|nil
---@class SubstitutableCommand
---@field source string substituted via '{source}'
---@field binary string substitued via '{binary}'
local M = {}
local constants = require('cp.constants')
local logger = require('cp.log')
local utils = require('cp.utils')
local filetype_to_language = constants.filetype_to_language
local function get_language_from_file(source_file, contest_config)
local ext = vim.fn.fnamemodify(source_file, ':e')
return filetype_to_language[ext] or contest_config.default_language
end
---@param cmd_template string[]
---@param substitutions SubstitutableCommand
---@return string[] string normalized with substitutions
local function substitute_template(cmd_template, substitutions)
local out = {}
for _, a in ipairs(cmd_template) do
local s = a
for k, v in pairs(substitutions) do
s = s:gsub('{' .. k .. '}', v)
for _, arg in ipairs(cmd_template) do
if arg == '{source}' and substitutions.source then
table.insert(out, substitutions.source)
elseif arg == '{binary}' and substitutions.binary then
table.insert(out, substitutions.binary)
else
table.insert(out, arg)
end
table.insert(out, s)
end
return out
end
@ -39,12 +41,10 @@ function M.build_command(cmd_template, executable, substitutions)
return cmd
end
function M.compile(language_config, substitutions)
if not language_config.compile then
return { code = 0, stdout = '' }
end
local cmd = substitute_template(language_config.compile, substitutions)
---@param compile_cmd string[]
---@param substitutions SubstitutableCommand
function M.compile(compile_cmd, substitutions)
local cmd = substitute_template(compile_cmd, substitutions)
local sh = table.concat(cmd, ' ') .. ' 2>&1'
local t0 = vim.uv.hrtime()
@ -164,32 +164,19 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
}
end
function M.compile_problem(contest_config, is_debug)
function M.compile_problem()
local state = require('cp.state')
local source_file = state.get_source_file()
if not source_file then
return { success = false, output = 'No source file found.' }
end
local language = get_language_from_file(source_file, contest_config)
local language_config = contest_config[language]
if not language_config then
return { success = false, output = ('No configuration for language %s.'):format(language) }
end
local binary_file = state.get_binary_file()
local substitutions = { source = source_file, binary = binary_file }
local chosen = (is_debug and language_config.debug) and language_config.debug
or language_config.compile
if not chosen then
local config = require('cp.config').get_config()
local platform = state.get_platform() or ''
local language = config.platforms[platform].default_language
local compile_config = config.platforms[platform][language].compile
if not compile_config then
return { success = true, output = nil }
end
local saved = language_config.compile
language_config.compile = chosen
local r = M.compile(language_config, substitutions)
language_config.compile = saved
local substitutions = { source = state.get_source_file(), binary = state.get_binary_file() }
local r = M.compile(compile_config, substitutions)
if r.code ~= 0 then
return { success = false, output = r.stdout or 'unknown error' }

View file

@ -31,8 +31,11 @@
local M = {}
local cache = require('cp.cache')
local config = require('cp.config').get_config()
local constants = require('cp.constants')
local execute = require('cp.runner.execute')
local logger = require('cp.log')
local state = require('cp.state')
---@type RunPanelState
local run_panel_state = {
@ -90,42 +93,35 @@ local function create_sentinal_panel_data(test_cases)
return out
end
---@param language_config LanguageConfig
---@param substitutions table<string, string>
---@param cmd string[]
---@param executable string
---@return string[]
local function build_command(language_config, substitutions)
local execute = require('cp.runner.execute')
return execute.build_command(language_config.test, language_config.executable, substitutions)
local function build_command(cmd, executable, substitutions)
return execute.build_command(cmd, executable, substitutions)
end
---@param contest_config ContestConfig
---@param cp_config cp.Config
---@param test_case RanTestCase
---@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 state = require('cp.state')
local exec = require('cp.runner.execute')
local function run_single_test_case(test_case)
local source_file = state.get_source_file()
local ext = vim.fn.fnamemodify(source_file or '', ':e')
local lang_name = constants.filetype_to_language[ext] or contest_config.default_language
local language_config = contest_config[lang_name]
local binary_file = state.get_binary_file()
local substitutions = { source = source_file, binary = binary_file }
local cmd = build_command(language_config, substitutions)
local platform_config = config.platforms[state.get_platform() or '']
local language_config = platform_config[platform_config.default_language]
local cmd = build_command(language_config.test, language_config.executable, substitutions)
local stdin_content = (test_case.input or '') .. '\n'
local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms) or 0
local memory_mb = run_panel_state.constraints and run_panel_state.constraints.memory_mb or 0
local r = exec.run(cmd, stdin_content, timeout_ms, memory_mb)
local r = execute.run(cmd, stdin_content, timeout_ms, memory_mb)
local ansi = require('cp.ui.ansi')
local out = r.stdout or ''
local highlights = {}
if out ~= '' then
if cp_config.run_panel.ansi then
if config.run_panel.ansi then
local parsed = ansi.parse_ansi_text(out)
out = table.concat(parsed.lines, '\n')
highlights = parsed.highlights
@ -134,7 +130,7 @@ local function run_single_test_case(contest_config, cp_config, test_case)
end
end
local max_lines = cp_config.run_panel.max_output_lines
local max_lines = config.run_panel.max_output_lines
local lines = vim.split(out, '\n')
if #lines > max_lines then
local trimmed = {}
@ -180,9 +176,8 @@ local function run_single_test_case(contest_config, cp_config, test_case)
}
end
---@param state table
---@return boolean
function M.load_test_cases(state)
function M.load_test_cases()
local tcs = cache.get_test_cases(
state.get_platform() or '',
state.get_contest_id() or '',
@ -201,18 +196,16 @@ function M.load_test_cases(state)
return #tcs > 0
end
---@param contest_config ContestConfig
---@param cp_config cp.Config
---@param index number
---@return boolean
function M.run_test_case(contest_config, cp_config, index)
function M.run_test_case(index)
local tc = run_panel_state.test_cases[index]
if not tc then
return false
end
tc.status = 'running'
local r = run_single_test_case(contest_config, cp_config, tc)
local r = run_single_test_case(tc)
tc.status = r.status
tc.actual = r.actual
@ -230,13 +223,11 @@ function M.run_test_case(contest_config, cp_config, index)
return true
end
---@param contest_config ContestConfig
---@param cp_config cp.Config
---@return RanTestCase[]
function M.run_all_test_cases(contest_config, cp_config)
function M.run_all_test_cases()
local results = {}
for i = 1, #run_panel_state.test_cases do
M.run_test_case(contest_config, cp_config, i)
M.run_test_case(i)
results[i] = run_panel_state.test_cases[i]
end
return results

View file

@ -4,7 +4,6 @@ local utils = require('cp.utils')
local function syshandle(result)
if result.code ~= 0 then
vim.print(('<%s>'):format(result.stderr))
local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error')
return { success = false, error = msg }
end

View file

@ -42,19 +42,13 @@ end
---@param contest_id string
---@param problem_id string|nil
function M.setup_contest(platform, contest_id, problem_id)
local config = config_module.get_config()
if not vim.tbl_contains(config.scrapers, platform) then
logger.log(('Scraping disabled for %s.'):format(platform), vim.log.levels.WARN)
return
end
state.set_contest_id(contest_id)
cache.load()
local function proceed(contest_data)
local problems = contest_data.problems
local pid = problems[(problem_id and contest_data.index_map[problem_id] or 1)].id
M.setup_problem(pid, language)
M.setup_problem(pid)
local cached_len = #vim.tbl_filter(function(p)
return not vim.tbl_isempty(cache.get_test_cases(platform, contest_id, p.id))
@ -96,8 +90,7 @@ function M.setup_contest(platform, contest_id, problem_id)
end
---@param problem_id string
---@param language? string
function M.setup_problem(problem_id, language)
function M.setup_problem(problem_id)
local platform = state.get_platform()
if not platform then
logger.log('No platform set.', vim.log.levels.ERROR)
@ -111,10 +104,8 @@ function M.setup_problem(problem_id, language)
vim.schedule(function()
vim.cmd.only({ mods = { silent = true } })
local language = config.platforms[platform].default_language
local source_file = state.get_source_file(language)
if not source_file then
return
end
vim.cmd.e(source_file)
local source_buf = vim.api.nvim_get_current_buf()

View file

@ -90,10 +90,8 @@ function M.toggle_interactive()
vim.cmd(('mksession! %s'):format(state.saved_interactive_session))
vim.cmd('silent only')
local config = config_module.get_config()
local contest_config = config.platforms[state.get_platform() or '']
local execute = require('cp.runner.execute')
local compile_result = execute.compile_problem(contest_config, false)
local compile_result = execute.compile_problem()
if not compile_result.success then
require('cp.runner.run').handle_compilation_failure(compile_result.output)
return
@ -120,7 +118,8 @@ function M.toggle_interactive()
state.set_active_panel('interactive')
end
function M.toggle_run_panel(is_debug)
---@param debug? boolean
function M.toggle_run_panel(debug)
if state.get_active_panel() == 'run' then
if current_diff_layout then
current_diff_layout.cleanup()
@ -191,7 +190,7 @@ function M.toggle_run_panel(is_debug)
if config.hooks and config.hooks.before_run then
config.hooks.before_run(state)
end
if is_debug and config.hooks and config.hooks.before_debug then
if debug and config.hooks and config.hooks.before_debug then
config.hooks.before_debug(state)
end
@ -199,7 +198,7 @@ function M.toggle_run_panel(is_debug)
local input_file = state.get_input_file()
logger.log(('run panel: checking test cases for %s'):format(input_file or 'none'))
if not run.load_test_cases(state) then
if not run.load_test_cases() then
logger.log('no test cases found', vim.log.levels.WARN)
return
end
@ -284,10 +283,9 @@ function M.toggle_run_panel(is_debug)
setup_keybindings_for_buffer(test_buffers.tab_buf)
local execute = require('cp.runner.execute')
local contest_config = config.platforms[state.get_platform() or '']
local compile_result = execute.compile_problem(contest_config, is_debug)
local compile_result = execute.compile_problem()
if compile_result.success then
run.run_all_test_cases(contest_config, config)
run.run_all_test_cases()
else
run.handle_compilation_failure(compile_result.output)
end