Merge branch 'main' into fix/submit-hardening

# Conflicts:
#	scrapers/atcoder.py
#	scrapers/codeforces.py
This commit is contained in:
Barrett Ruth 2026-03-05 14:18:01 -05:00
commit 427d03ec2d
Signed by: barrett
GPG key ID: A6C96C9349D2FC81
21 changed files with 154 additions and 129 deletions

View file

@ -47,26 +47,24 @@ function M.handle_cache_command(cmd)
constants.PLATFORM_DISPLAY_NAMES[cmd.platform], constants.PLATFORM_DISPLAY_NAMES[cmd.platform],
cmd.contest cmd.contest
), ),
vim.log.levels.INFO, { level = vim.log.levels.INFO, override = true }
true
) )
else else
logger.log(("Unknown platform '%s'."):format(cmd.platform), vim.log.levels.ERROR) logger.log(("Unknown platform '%s'."):format(cmd.platform), { level = vim.log.levels.ERROR })
end end
elseif cmd.platform then elseif cmd.platform then
if vim.tbl_contains(platforms, cmd.platform) then if vim.tbl_contains(platforms, cmd.platform) then
cache.clear_platform(cmd.platform) cache.clear_platform(cmd.platform)
logger.log( logger.log(
("Cache cleared for platform '%s'"):format(constants.PLATFORM_DISPLAY_NAMES[cmd.platform]), ("Cache cleared for platform '%s'"):format(constants.PLATFORM_DISPLAY_NAMES[cmd.platform]),
vim.log.levels.INFO, { level = vim.log.levels.INFO, override = true }
true
) )
else else
logger.log(("Unknown platform '%s'."):format(cmd.platform), vim.log.levels.ERROR) logger.log(("Unknown platform '%s'."):format(cmd.platform), { level = vim.log.levels.ERROR })
end end
else else
cache.clear_all() cache.clear_all()
logger.log('Cache cleared', vim.log.levels.INFO, true) logger.log('Cache cleared', { level = vim.log.levels.INFO, override = true })
end end
end end
end end

View file

@ -83,8 +83,6 @@ local function parse_command(args)
else else
return { type = 'action', action = 'interact' } return { type = 'action', action = 'interact' }
end end
elseif first == 'login' or first == 'logout' then
return { type = 'action', action = first, platform = args[2] }
elseif first == 'stress' then elseif first == 'stress' then
return { return {
type = 'action', type = 'action',
@ -245,6 +243,9 @@ local function parse_command(args)
message = 'Too few arguments - specify a contest.', message = 'Too few arguments - specify a contest.',
} }
elseif #args == 2 then elseif #args == 2 then
if args[2] == 'login' or args[2] == 'logout' then
return { type = 'action', action = args[2], platform = first }
end
return { return {
type = 'contest_setup', type = 'contest_setup',
platform = first, platform = first,
@ -287,7 +288,7 @@ function M.handle_command(opts)
local cmd = parse_command(opts.fargs) local cmd = parse_command(opts.fargs)
if cmd.type == 'error' then if cmd.type == 'error' then
logger.log(cmd.message, vim.log.levels.ERROR) logger.log(cmd.message, { level = vim.log.levels.ERROR })
return return
end end
@ -336,7 +337,7 @@ function M.handle_command(opts)
local problem_id = cmd.problem_id local problem_id = cmd.problem_id
if not (platform and contest_id) then if not (platform and contest_id) then
logger.log('No contest is currently active.', vim.log.levels.ERROR) logger.log('No contest is currently active.', { level = vim.log.levels.ERROR })
return return
end end
@ -351,7 +352,7 @@ function M.handle_command(opts)
contest_id, contest_id,
problem_id problem_id
), ),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end

View file

@ -12,7 +12,7 @@ function M.handle_pick_action(language)
if not (config.ui and config.ui.picker) then if not (config.ui and config.ui.picker) then
logger.log( logger.log(
'No picker configured. Set ui.picker = "{telescope,fzf-lua}" in your config.', 'No picker configured. Set ui.picker = "{telescope,fzf-lua}" in your config.',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
@ -25,13 +25,13 @@ function M.handle_pick_action(language)
if not ok then if not ok then
logger.log( logger.log(
'telescope.nvim is not available. Install telescope.nvim xor change your picker config.', 'telescope.nvim is not available. Install telescope.nvim xor change your picker config.',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
local ok_cp, telescope_picker = pcall(require, 'cp.pickers.telescope') local ok_cp, telescope_picker = pcall(require, 'cp.pickers.telescope')
if not ok_cp then if not ok_cp then
logger.log('Failed to load telescope integration.', vim.log.levels.ERROR) logger.log('Failed to load telescope integration.', { level = vim.log.levels.ERROR })
return return
end end
@ -41,13 +41,13 @@ function M.handle_pick_action(language)
if not ok then if not ok then
logger.log( logger.log(
'fzf-lua is not available. Install fzf-lua or change your picker config', 'fzf-lua is not available. Install fzf-lua or change your picker config',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
local ok_cp, fzf_picker = pcall(require, 'cp.pickers.fzf_lua') local ok_cp, fzf_picker = pcall(require, 'cp.pickers.fzf_lua')
if not ok_cp then if not ok_cp then
logger.log('Failed to load fzf-lua integration.', vim.log.levels.ERROR) logger.log('Failed to load fzf-lua integration.', { level = vim.log.levels.ERROR })
return return
end end

View file

@ -13,8 +13,6 @@ M.ACTIONS = {
'race', 'race',
'stress', 'stress',
'submit', 'submit',
'login',
'logout',
} }
M.PLATFORM_DISPLAY_NAMES = { M.PLATFORM_DISPLAY_NAMES = {

View file

@ -7,37 +7,37 @@ local state = require('cp.state')
function M.login(platform) function M.login(platform)
platform = platform or state.get_platform() platform = platform or state.get_platform()
if not platform then if not platform then
logger.log('No platform specified. Usage: :CP login <platform>', vim.log.levels.ERROR) logger.log('No platform specified. Usage: :CP login <platform>', { level = vim.log.levels.ERROR })
return return
end end
vim.ui.input({ prompt = platform .. ' username: ' }, function(username) vim.ui.input({ prompt = platform .. ' username: ' }, function(username)
if not username or username == '' then if not username or username == '' then
logger.log('Cancelled', vim.log.levels.WARN) logger.log('Cancelled', { level = vim.log.levels.WARN })
return return
end end
vim.fn.inputsave() vim.fn.inputsave()
local password = vim.fn.inputsecret(platform .. ' password: ') local password = vim.fn.inputsecret(platform .. ' password: ')
vim.fn.inputrestore() vim.fn.inputrestore()
if not password or password == '' then if not password or password == '' then
logger.log('Cancelled', vim.log.levels.WARN) logger.log('Cancelled', { level = vim.log.levels.WARN })
return return
end end
cache.load() cache.load()
cache.set_credentials(platform, { username = username, password = password }) cache.set_credentials(platform, { username = username, password = password })
logger.log(platform .. ' credentials saved', vim.log.levels.INFO, true) logger.log(platform .. ' credentials saved', { level = vim.log.levels.INFO, override = true })
end) end)
end end
function M.logout(platform) function M.logout(platform)
platform = platform or state.get_platform() platform = platform or state.get_platform()
if not platform then if not platform then
logger.log('No platform specified. Usage: :CP logout <platform>', vim.log.levels.ERROR) logger.log('No platform specified. Usage: :CP logout <platform>', { level = vim.log.levels.ERROR })
return return
end end
cache.load() cache.load()
cache.clear_credentials(platform) cache.clear_credentials(platform)
logger.log(platform .. ' credentials cleared', vim.log.levels.INFO, true) logger.log(platform .. ' credentials cleared', { level = vim.log.levels.INFO, override = true })
end end
return M return M

View file

@ -7,7 +7,7 @@ local logger = require('cp.log')
M.helpers = helpers M.helpers = helpers
if vim.fn.has('nvim-0.10.0') == 0 then if vim.fn.has('nvim-0.10.0') == 0 then
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR) logger.log('Requires nvim-0.10.0+', { level = vim.log.levels.ERROR })
return {} return {}
end end

View file

@ -1,12 +1,27 @@
local M = {} local M = {}
function M.log(msg, level, override) ---@class LogOpts
---@field level? integer
---@field override? boolean
---@field sync? boolean
---@param msg string
---@param opts? LogOpts
function M.log(msg, opts)
local debug = require('cp.config').get_config().debug or false local debug = require('cp.config').get_config().debug or false
level = level or vim.log.levels.INFO opts = opts or {}
local level = opts.level or vim.log.levels.INFO
local override = opts.override or false
local sync = opts.sync or false
if level >= vim.log.levels.WARN or override or debug then if level >= vim.log.levels.WARN or override or debug then
vim.schedule(function() local notify = function()
vim.notify(('[cp.nvim]: %s'):format(msg), level) vim.notify(('[cp.nvim]: %s'):format(msg), level)
end) end
if sync then
notify()
else
vim.schedule(notify)
end
end end
end end

View file

@ -42,24 +42,14 @@ function M.get_platform_contests(platform, refresh)
local picker_contests = cache.get_contest_summaries(platform) local picker_contests = cache.get_contest_summaries(platform)
if refresh or vim.tbl_isempty(picker_contests) then if refresh or vim.tbl_isempty(picker_contests) then
logger.log( local display_name = constants.PLATFORM_DISPLAY_NAMES[platform]
('Loading %s contests...'):format(constants.PLATFORM_DISPLAY_NAMES[platform]), logger.log(('Fetching %s contests...'):format(display_name), { level = vim.log.levels.INFO, override = true, sync = true })
vim.log.levels.INFO,
true
)
local contests = scraper.scrape_contest_list(platform) local contests = scraper.scrape_contest_list(platform)
cache.set_contest_summaries(platform, contests) cache.set_contest_summaries(platform, contests)
picker_contests = cache.get_contest_summaries(platform) picker_contests = cache.get_contest_summaries(platform)
logger.log( logger.log(('Fetched %d %s contests.'):format(#picker_contests, display_name), { level = vim.log.levels.INFO, override = true })
('Loaded %d %s contests.'):format(
#picker_contests,
constants.PLATFORM_DISPLAY_NAMES[platform]
),
vim.log.levels.INFO,
true
)
end end
return picker_contests return picker_contests

View file

@ -22,15 +22,15 @@ end
function M.start(platform, contest_id, language) function M.start(platform, contest_id, language)
if not platform or not vim.tbl_contains(constants.PLATFORMS, platform) then if not platform or not vim.tbl_contains(constants.PLATFORMS, platform) then
logger.log('Invalid platform', vim.log.levels.ERROR) logger.log('Invalid platform', { level = vim.log.levels.ERROR })
return return
end end
if not contest_id or contest_id == '' then if not contest_id or contest_id == '' then
logger.log('Contest ID required', vim.log.levels.ERROR) logger.log('Contest ID required', { level = vim.log.levels.ERROR })
return return
end end
if race_state.timer then if race_state.timer then
logger.log('Race already active. Use :CP race stop first.', vim.log.levels.WARN) logger.log('Race already active. Use :CP race stop first.', { level = vim.log.levels.WARN })
return return
end end
@ -38,7 +38,7 @@ function M.start(platform, contest_id, language)
local start_time = cache.get_contest_start_time(platform, contest_id) local start_time = cache.get_contest_start_time(platform, contest_id)
if not start_time then if not start_time then
logger.log('Fetching contest list...', vim.log.levels.INFO, true) logger.log('Fetching contest list...', { level = vim.log.levels.INFO, override = true })
local contests = scraper.scrape_contest_list(platform) local contests = scraper.scrape_contest_list(platform)
if contests and #contests > 0 then if contests and #contests > 0 then
cache.set_contest_summaries(platform, contests) cache.set_contest_summaries(platform, contests)
@ -52,14 +52,14 @@ function M.start(platform, contest_id, language)
constants.PLATFORM_DISPLAY_NAMES[platform] or platform, constants.PLATFORM_DISPLAY_NAMES[platform] or platform,
contest_id contest_id
), ),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
local remaining = start_time - os.time() local remaining = start_time - os.time()
if remaining <= 0 then if remaining <= 0 then
logger.log('Contest has already started, setting up...', vim.log.levels.INFO, true) logger.log('Contest has already started, setting up...', { level = vim.log.levels.INFO, override = true })
require('cp.setup').setup_contest(platform, contest_id, nil, language) require('cp.setup').setup_contest(platform, contest_id, nil, language)
return return
end end
@ -75,8 +75,7 @@ function M.start(platform, contest_id, language)
contest_id, contest_id,
format_countdown(remaining) format_countdown(remaining)
), ),
vim.log.levels.INFO, { level = vim.log.levels.INFO, override = true }
true
) )
local timer = vim.uv.new_timer() local timer = vim.uv.new_timer()
@ -97,7 +96,7 @@ function M.start(platform, contest_id, language)
race_state.contest_id = nil race_state.contest_id = nil
race_state.language = nil race_state.language = nil
race_state.start_time = nil race_state.start_time = nil
logger.log('Contest started!', vim.log.levels.INFO, true) logger.log('Contest started!', { level = vim.log.levels.INFO, override = true })
require('cp.setup').setup_contest(p, c, nil, l) require('cp.setup').setup_contest(p, c, nil, l)
else else
vim.notify( vim.notify(
@ -116,7 +115,7 @@ end
function M.stop() function M.stop()
local timer = race_state.timer local timer = race_state.timer
if not timer then if not timer then
logger.log('No active race', vim.log.levels.WARN) logger.log('No active race', { level = vim.log.levels.WARN })
return return
end end
timer:stop() timer:stop()
@ -126,7 +125,7 @@ function M.stop()
race_state.contest_id = nil race_state.contest_id = nil
race_state.language = nil race_state.language = nil
race_state.start_time = nil race_state.start_time = nil
logger.log('Race cancelled', vim.log.levels.INFO, true) logger.log('Race cancelled', { level = vim.log.levels.INFO, override = true })
end end
function M.status() function M.status()

View file

@ -11,7 +11,7 @@ function M.restore_from_current_file()
local current_file = (vim.uv.fs_realpath(vim.fn.expand('%:p')) or vim.fn.expand('%:p')) local current_file = (vim.uv.fs_realpath(vim.fn.expand('%:p')) or vim.fn.expand('%:p'))
local file_state = cache.get_file_state(current_file) local file_state = cache.get_file_state(current_file)
if not file_state then if not file_state then
logger.log('No cached state found for current file.', vim.log.levels.ERROR) logger.log('No cached state found for current file.', { level = vim.log.levels.ERROR })
return false return false
end end

View file

@ -52,7 +52,7 @@ function M.compile(compile_cmd, substitutions, on_complete)
r.stdout = ansi.bytes_to_string(r.stdout or '') r.stdout = ansi.bytes_to_string(r.stdout or '')
if r.code == 0 then if r.code == 0 then
logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO) logger.log(('Compilation successful in %.1fms.'):format(dt), { level = vim.log.levels.INFO })
else else
logger.log(('Compilation failed in %.1fms.'):format(dt)) logger.log(('Compilation failed in %.1fms.'):format(dt))
end end

View file

@ -245,7 +245,7 @@ function M.load_test_cases()
state.get_problem_id() state.get_problem_id()
) )
logger.log(('Loaded %d test case(s)'):format(#tcs), vim.log.levels.INFO) logger.log(('Loaded %d test case(s)'):format(#tcs), { level = vim.log.levels.INFO })
return #tcs > 0 return #tcs > 0
end end
@ -259,7 +259,7 @@ function M.run_combined_test(debug, on_complete)
) )
if not combined then if not combined then
logger.log('No combined test found', vim.log.levels.ERROR) logger.log('No combined test found', { level = vim.log.levels.ERROR })
on_complete(nil) on_complete(nil)
return return
end end
@ -330,8 +330,7 @@ function M.run_all_test_cases(indices, debug, on_each, on_done)
if #to_run == 0 then if #to_run == 0 then
logger.log( logger.log(
('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', 0), ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', 0),
vim.log.levels.INFO, { level = vim.log.levels.INFO, override = true }
true
) )
on_done(panel_state.test_cases) on_done(panel_state.test_cases)
return return
@ -349,8 +348,7 @@ function M.run_all_test_cases(indices, debug, on_each, on_done)
if remaining == 0 then if remaining == 0 then
logger.log( logger.log(
('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', total), ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', total),
vim.log.levels.INFO, { level = vim.log.levels.INFO, override = true }
true
) )
on_done(panel_state.test_cases) on_done(panel_state.test_cases)
end end

View file

@ -16,7 +16,7 @@ local function syshandle(result)
end end
local msg = 'Failed to parse scraper output: ' .. tostring(data) local msg = 'Failed to parse scraper output: ' .. tostring(data)
logger.log(msg, vim.log.levels.ERROR) logger.log(msg, { level = vim.log.levels.ERROR })
return { success = false, error = msg } return { success = false, error = msg }
end end
@ -37,7 +37,7 @@ end
local function run_scraper(platform, subcommand, args, opts) local function run_scraper(platform, subcommand, args, opts)
if not utils.setup_python_env() then if not utils.setup_python_env() then
local msg = 'no Python environment available (install uv or nix)' local msg = 'no Python environment available (install uv or nix)'
logger.log(msg, vim.log.levels.ERROR) logger.log(msg, { level = vim.log.levels.ERROR })
if opts and opts.on_exit then if opts and opts.on_exit then
opts.on_exit({ success = false, error = msg }) opts.on_exit({ success = false, error = msg })
end end
@ -125,7 +125,7 @@ local function run_scraper(platform, subcommand, args, opts)
if stdin_pipe and not stdin_pipe:is_closing() then if stdin_pipe and not stdin_pipe:is_closing() then
stdin_pipe:close() stdin_pipe:close()
end end
logger.log('Failed to start scraper process', vim.log.levels.ERROR) logger.log('Failed to start scraper process', { level = vim.log.levels.ERROR })
return { success = false, error = 'spawn failed' } return { success = false, error = 'spawn failed' }
end end
@ -221,7 +221,7 @@ function M.scrape_contest_metadata(platform, contest_id, callback)
constants.PLATFORM_DISPLAY_NAMES[platform], constants.PLATFORM_DISPLAY_NAMES[platform],
contest_id contest_id
), ),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
@ -232,7 +232,7 @@ function M.scrape_contest_metadata(platform, contest_id, callback)
constants.PLATFORM_DISPLAY_NAMES[platform], constants.PLATFORM_DISPLAY_NAMES[platform],
contest_id contest_id
), ),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
@ -251,7 +251,7 @@ function M.scrape_contest_list(platform)
platform, platform,
(result and result.error) or 'unknown' (result and result.error) or 'unknown'
), ),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return {} return {}
end end
@ -261,9 +261,15 @@ end
---@param platform string ---@param platform string
---@param contest_id string ---@param contest_id string
---@param callback fun(data: table)|nil ---@param callback fun(data: table)|nil
function M.scrape_all_tests(platform, contest_id, callback) ---@param on_done fun()|nil
function M.scrape_all_tests(platform, contest_id, callback, on_done)
run_scraper(platform, 'tests', { contest_id }, { run_scraper(platform, 'tests', { contest_id }, {
ndjson = true, ndjson = true,
on_exit = function()
if type(on_done) == 'function' then
vim.schedule(on_done)
end
end,
on_event = function(ev) on_event = function(ev)
if ev.done then if ev.done then
return return
@ -275,7 +281,7 @@ function M.scrape_all_tests(platform, contest_id, callback)
contest_id, contest_id,
ev.error ev.error
), ),
vim.log.levels.WARN { level = vim.log.levels.WARN }
) )
return return
end end

View file

@ -16,7 +16,7 @@ local function apply_template(bufnr, lang_id, platform)
end end
local path = vim.fn.expand(eff.template) local path = vim.fn.expand(eff.template)
if vim.fn.filereadable(path) ~= 1 then if vim.fn.filereadable(path) ~= 1 then
logger.log(('[cp.nvim] template not readable: %s'):format(path), vim.log.levels.WARN) logger.log(('[cp.nvim] template not readable: %s'):format(path), { level = vim.log.levels.WARN })
return return
end end
local lines = vim.fn.readfile(path) local lines = vim.fn.readfile(path)
@ -112,11 +112,12 @@ local function start_tests(platform, contest_id, problems)
return not vim.tbl_isempty(cache.get_test_cases(platform, contest_id, p.id)) return not vim.tbl_isempty(cache.get_test_cases(platform, contest_id, p.id))
end, problems) end, problems)
if cached_len ~= #problems then if cached_len ~= #problems then
local to_fetch = #problems - cached_len
logger.log(('Fetching %s/%s problem tests...'):format(cached_len, #problems)) logger.log(('Fetching %s/%s problem tests...'):format(cached_len, #problems))
scraper.scrape_all_tests(platform, contest_id, function(ev) scraper.scrape_all_tests(platform, contest_id, function(ev)
local cached_tests = {} local cached_tests = {}
if not ev.interactive and vim.tbl_isempty(ev.tests) then if not ev.interactive and vim.tbl_isempty(ev.tests) then
logger.log(("No tests found for problem '%s'."):format(ev.problem_id), vim.log.levels.WARN) logger.log(("No tests found for problem '%s'."):format(ev.problem_id), { level = vim.log.levels.WARN })
end end
for i, t in ipairs(ev.tests) do for i, t in ipairs(ev.tests) do
cached_tests[i] = { index = i, input = t.input, expected = t.expected } cached_tests[i] = { index = i, input = t.input, expected = t.expected }
@ -142,6 +143,8 @@ local function start_tests(platform, contest_id, problems)
require('cp.utils').update_buffer_content(io_state.input_buf, input_lines, nil, nil) require('cp.utils').update_buffer_content(io_state.input_buf, input_lines, nil, nil)
end end
end end
end, function()
logger.log(('Loaded %d test%s.'):format(to_fetch, to_fetch == 1 and '' or 's'), { level = vim.log.levels.INFO, override = true })
end) end)
end end
end end
@ -160,7 +163,7 @@ function M.setup_contest(platform, contest_id, problem_id, language)
if language then if language then
local lang_result = config_module.get_language_for_platform(platform, language) local lang_result = config_module.get_language_for_platform(platform, language)
if not lang_result.valid then if not lang_result.valid then
logger.log(lang_result.error, vim.log.levels.ERROR) logger.log(lang_result.error, { level = vim.log.levels.ERROR })
return return
end end
end end
@ -206,7 +209,7 @@ function M.setup_contest(platform, contest_id, problem_id, language)
token = vim.uv.hrtime(), token = vim.uv.hrtime(),
}) })
logger.log('Fetching contests problems...', vim.log.levels.INFO, true) logger.log('Fetching contests problems...', { level = vim.log.levels.INFO, override = true })
scraper.scrape_contest_metadata( scraper.scrape_contest_metadata(
platform, platform,
contest_id, contest_id,
@ -242,7 +245,7 @@ end
function M.setup_problem(problem_id, language) function M.setup_problem(problem_id, language)
local platform = state.get_platform() local platform = state.get_platform()
if not platform then if not platform then
logger.log('No platform/contest/problem configured.', vim.log.levels.ERROR) logger.log('No platform/contest/problem configured.', { level = vim.log.levels.ERROR })
return return
end end
@ -263,7 +266,7 @@ function M.setup_problem(problem_id, language)
if language then if language then
local lang_result = config_module.get_language_for_platform(platform, language) local lang_result = config_module.get_language_for_platform(platform, language)
if not lang_result.valid then if not lang_result.valid then
logger.log(lang_result.error, vim.log.levels.ERROR) logger.log(lang_result.error, { level = vim.log.levels.ERROR })
return return
end end
end end
@ -275,6 +278,19 @@ function M.setup_problem(problem_id, language)
return return
end end
if vim.fn.filereadable(source_file) == 1 then
local existing = cache.get_file_state(vim.fn.fnamemodify(source_file, ':p'))
if existing and (existing.platform ~= platform or existing.contest_id ~= (state.get_contest_id() or '') or existing.problem_id ~= problem_id) then
logger.log(
('File %q already exists for %s/%s %s.'):format(
source_file, existing.platform, existing.contest_id, existing.problem_id
),
{ level = vim.log.levels.ERROR }
)
return
end
end
local contest_dir = vim.fn.fnamemodify(source_file, ':h') local contest_dir = vim.fn.fnamemodify(source_file, ':h')
local is_new_dir = vim.fn.isdirectory(contest_dir) == 0 local is_new_dir = vim.fn.isdirectory(contest_dir) == 0
vim.fn.mkdir(contest_dir, 'p') vim.fn.mkdir(contest_dir, 'p')
@ -397,7 +413,7 @@ function M.navigate_problem(direction, language)
local contest_id = state.get_contest_id() local contest_id = state.get_contest_id()
local current_problem_id = state.get_problem_id() local current_problem_id = state.get_problem_id()
if not platform or not contest_id or not current_problem_id then if not platform or not contest_id or not current_problem_id then
logger.log('No platform configured.', vim.log.levels.ERROR) logger.log('No platform configured.', { level = vim.log.levels.ERROR })
return return
end end
@ -409,7 +425,7 @@ function M.navigate_problem(direction, language)
constants.PLATFORM_DISPLAY_NAMES[platform], constants.PLATFORM_DISPLAY_NAMES[platform],
contest_id contest_id
), ),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
@ -433,7 +449,7 @@ function M.navigate_problem(direction, language)
if language then if language then
local lang_result = config_module.get_language_for_platform(platform, language) local lang_result = config_module.get_language_for_platform(platform, language)
if not lang_result.valid then if not lang_result.valid then
logger.log(lang_result.error, vim.log.levels.ERROR) logger.log(lang_result.error, { level = vim.log.levels.ERROR })
return return
end end
lang = language lang = language

View file

@ -36,7 +36,7 @@ local function compile_cpp(source, output)
if result.code ~= 0 then if result.code ~= 0 then
logger.log( logger.log(
('Failed to compile %s: %s'):format(source, result.stderr or ''), ('Failed to compile %s: %s'):format(source, result.stderr or ''),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return false return false
end end
@ -76,7 +76,7 @@ function M.toggle(generator_cmd, brute_cmd)
end end
if state.get_active_panel() then if state.get_active_panel() then
logger.log('Another panel is already active.', vim.log.levels.WARN) logger.log('Another panel is already active.', { level = vim.log.levels.WARN })
return return
end end
@ -93,14 +93,14 @@ function M.toggle(generator_cmd, brute_cmd)
if not gen_file then if not gen_file then
logger.log( logger.log(
'No generator found. Pass generator as first arg or add gen.{py,cc,cpp}.', 'No generator found. Pass generator as first arg or add gen.{py,cc,cpp}.',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
if not brute_file then if not brute_file then
logger.log( logger.log(
'No brute solution found. Pass brute as second arg or add brute.{py,cc,cpp}.', 'No brute solution found. Pass brute as second arg or add brute.{py,cc,cpp}.',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
@ -140,7 +140,7 @@ function M.toggle(generator_cmd, brute_cmd)
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.', { level = vim.log.levels.ERROR })
restore_session() restore_session()
return return
end end

View file

@ -19,7 +19,7 @@ local function prompt_credentials(platform, callback)
end end
vim.ui.input({ prompt = platform .. ' username: ' }, function(username) vim.ui.input({ prompt = platform .. ' username: ' }, function(username)
if not username or username == '' then if not username or username == '' then
logger.log('Submit cancelled', vim.log.levels.WARN) logger.log('Submit cancelled', { level = vim.log.levels.WARN })
return return
end end
vim.fn.inputsave() vim.fn.inputsave()
@ -27,7 +27,7 @@ local function prompt_credentials(platform, callback)
vim.fn.inputrestore() vim.fn.inputrestore()
vim.cmd.redraw() vim.cmd.redraw()
if not password or password == '' then if not password or password == '' then
logger.log('Submit cancelled', vim.log.levels.WARN) logger.log('Submit cancelled', { level = vim.log.levels.WARN })
return return
end end
local creds = { username = username, password = password } local creds = { username = username, password = password }
@ -42,13 +42,13 @@ function M.submit(opts)
local problem_id = state.get_problem_id() local problem_id = state.get_problem_id()
local language = (opts and opts.language) or state.get_language() local language = (opts and opts.language) or state.get_language()
if not platform or not contest_id or not problem_id or not language then if not platform or not contest_id or not problem_id or not language then
logger.log('No active problem. Use :CP <platform> <contest> first.', vim.log.levels.ERROR) logger.log('No active problem. Use :CP <platform> <contest> first.', { level = vim.log.levels.ERROR })
return return
end end
local source_file = state.get_source_file() local source_file = state.get_source_file()
if not source_file or vim.fn.filereadable(source_file) ~= 1 then if not source_file or vim.fn.filereadable(source_file) ~= 1 then
logger.log('Source file not found', vim.log.levels.ERROR) logger.log('Source file not found', { level = vim.log.levels.ERROR })
return return
end end
@ -71,12 +71,13 @@ function M.submit(opts)
function(result) function(result)
vim.schedule(function() vim.schedule(function()
if result and result.success then if result and result.success then
logger.log('Submitted successfully', vim.log.levels.INFO, true) logger.log('Submitted successfully', { level = vim.log.levels.INFO, override = true })
else else
logger.log( local err = result and result.error or 'unknown error'
'Submit failed: ' .. (result and result.error or 'unknown error'), if err:match('^Login failed') then
vim.log.levels.ERROR cache.clear_credentials(platform)
) end
logger.log('Submit failed: ' .. err, { level = vim.log.levels.ERROR })
end end
end) end)
end end

View file

@ -90,7 +90,7 @@ local function delete_current_test()
return return
end end
if #edit_state.test_buffers == 1 then if #edit_state.test_buffers == 1 then
logger.log('Problems must have at least one test case.', vim.log.levels.ERROR) logger.log('Problems must have at least one test case.', { level = vim.log.levels.ERROR })
return return
end end
@ -311,7 +311,7 @@ setup_keybindings = function(buf)
end end
if is_tracked then if is_tracked then
logger.log('Test buffer closed unexpectedly. Exiting editor.', vim.log.levels.WARN) logger.log('Test buffer closed unexpectedly. Exiting editor.', { level = vim.log.levels.WARN })
M.toggle_edit() M.toggle_edit()
end end
end) end)
@ -368,7 +368,7 @@ function M.toggle_edit(test_index)
state.get_platform(), state.get_contest_id(), state.get_problem_id() state.get_platform(), state.get_contest_id(), state.get_problem_id()
if not platform or not contest_id or not problem_id then if not platform or not contest_id or not problem_id then
logger.log('No problem context. Run :CP <platform> <contest> first.', vim.log.levels.ERROR) logger.log('No problem context. Run :CP <platform> <contest> first.', { level = vim.log.levels.ERROR })
return return
end end
@ -376,7 +376,7 @@ function M.toggle_edit(test_index)
local test_cases = cache.get_test_cases(platform, contest_id, problem_id) local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
if not test_cases or #test_cases == 0 then if not test_cases or #test_cases == 0 then
logger.log('No test cases available for editing.', vim.log.levels.ERROR) logger.log('No test cases available for editing.', { level = vim.log.levels.ERROR })
return return
end end
@ -389,7 +389,7 @@ function M.toggle_edit(test_index)
if target_index < 1 or target_index > #test_cases then if target_index < 1 or target_index > #test_cases then
logger.log( logger.log(
('Test %d does not exist (only %d tests available)'):format(target_index, #test_cases), ('Test %d does not exist (only %d tests available)'):format(target_index, #test_cases),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end

View file

@ -53,7 +53,7 @@ function M.toggle_interactive(interactor_cmd)
end end
if state.get_active_panel() then if state.get_active_panel() then
logger.log('Another panel is already active.', vim.log.levels.WARN) logger.log('Another panel is already active.', { level = vim.log.levels.WARN })
return return
end end
@ -62,7 +62,7 @@ function M.toggle_interactive(interactor_cmd)
if not platform or not contest_id or not problem_id then if not platform or not contest_id or not problem_id then
logger.log( logger.log(
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.', 'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
@ -74,7 +74,7 @@ function M.toggle_interactive(interactor_cmd)
and contest_data.index_map and contest_data.index_map
and not contest_data.problems[contest_data.index_map[problem_id]].interactive and not contest_data.problems[contest_data.index_map[problem_id]].interactive
then then
logger.log('This problem is not interactive. Use :CP {run,panel}.', vim.log.levels.ERROR) logger.log('This problem is not interactive. Use :CP {run,panel}.', { level = vim.log.levels.ERROR })
return return
end end
@ -103,7 +103,7 @@ function M.toggle_interactive(interactor_cmd)
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.', { level = vim.log.levels.ERROR })
restore_session() restore_session()
return return
end end
@ -117,7 +117,7 @@ function M.toggle_interactive(interactor_cmd)
if vim.fn.executable(interactor) ~= 1 then if vim.fn.executable(interactor) ~= 1 then
logger.log( logger.log(
("Interactor '%s' is not executable."):format(interactor_cmd), ("Interactor '%s' is not executable."):format(interactor_cmd),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
restore_session() restore_session()
return return
@ -354,7 +354,7 @@ function M.ensure_io_view()
if not platform or not contest_id or not problem_id then if not platform or not contest_id or not problem_id then
logger.log( logger.log(
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.', 'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
@ -383,7 +383,7 @@ function M.ensure_io_view()
and contest_data.index_map and contest_data.index_map
and contest_data.problems[contest_data.index_map[problem_id]].interactive and contest_data.problems[contest_data.index_map[problem_id]].interactive
then then
logger.log('This problem is not interactive. Use :CP {run,panel}.', vim.log.levels.ERROR) logger.log('This problem is not interactive. Use :CP {run,panel}.', { level = vim.log.levels.ERROR })
return return
end end
@ -594,12 +594,12 @@ end
function M.run_io_view(test_indices_arg, debug, mode) function M.run_io_view(test_indices_arg, debug, mode)
if io_view_running then if io_view_running then
logger.log('Tests already running', vim.log.levels.WARN) logger.log('Tests already running', { level = vim.log.levels.WARN })
return return
end end
io_view_running = true io_view_running = true
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'), { level = vim.log.levels.INFO, override = true })
mode = mode or 'combined' mode = mode or 'combined'
@ -608,7 +608,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
if not platform or not contest_id or not problem_id then if not platform or not contest_id or not problem_id then
logger.log( logger.log(
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.', 'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
io_view_running = false io_view_running = false
return return
@ -617,7 +617,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
cache.load() cache.load()
local contest_data = cache.get_contest_data(platform, contest_id) local contest_data = cache.get_contest_data(platform, contest_id)
if not contest_data or not contest_data.index_map then if not contest_data or not contest_data.index_map then
logger.log('No test cases available.', vim.log.levels.ERROR) logger.log('No test cases available.', { level = vim.log.levels.ERROR })
io_view_running = false io_view_running = false
return return
end end
@ -634,13 +634,13 @@ function M.run_io_view(test_indices_arg, debug, mode)
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 available', vim.log.levels.ERROR) logger.log('No combined test available', { level = vim.log.levels.ERROR })
io_view_running = false io_view_running = false
return return
end end
else else
if not run.load_test_cases() then if not run.load_test_cases() then
logger.log('No test cases available', vim.log.levels.ERROR) logger.log('No test cases available', { level = vim.log.levels.ERROR })
io_view_running = false io_view_running = false
return return
end end
@ -660,7 +660,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
idx, idx,
#test_state.test_cases #test_state.test_cases
), ),
vim.log.levels.WARN { level = vim.log.levels.WARN }
) )
io_view_running = false io_view_running = false
return return
@ -721,7 +721,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
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', { level = vim.log.levels.ERROR })
io_view_running = false io_view_running = false
return return
end end
@ -730,7 +730,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
run.run_combined_test(debug, function(result) 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', { level = vim.log.levels.ERROR })
io_view_running = false io_view_running = false
return return
end end
@ -771,7 +771,7 @@ function M.toggle_panel(panel_opts)
end end
if state.get_active_panel() then if state.get_active_panel() then
logger.log('another panel is already active', vim.log.levels.ERROR) logger.log('another panel is already active', { level = vim.log.levels.ERROR })
return return
end end
@ -780,7 +780,7 @@ function M.toggle_panel(panel_opts)
if not platform or not contest_id then if not platform or not contest_id then
logger.log( logger.log(
'No platform/contest configured. Use :CP <platform> <contest> [...] first.', 'No platform/contest configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return return
end end
@ -792,7 +792,7 @@ function M.toggle_panel(panel_opts)
and contest_data.index_map and contest_data.index_map
and contest_data.problems[contest_data.index_map[state.get_problem_id()]].interactive and contest_data.problems[contest_data.index_map[state.get_problem_id()]].interactive
then then
logger.log('This is an interactive problem. Use :CP interact instead.', vim.log.levels.WARN) logger.log('This is an interactive problem. Use :CP interact instead.', { level = vim.log.levels.WARN })
return return
end end
@ -803,7 +803,7 @@ function M.toggle_panel(panel_opts)
logger.log(('run panel: checking test cases for %s'):format(input_file or 'none')) logger.log(('run panel: checking test cases for %s'):format(input_file or 'none'))
if not run.load_test_cases() then if not run.load_test_cases() then
logger.log('no test cases found', vim.log.levels.WARN) logger.log('no test cases found', { level = vim.log.levels.WARN })
return return
end end

View file

@ -152,7 +152,7 @@ local function discover_nix_submit_cmd()
:wait() :wait()
if result.code ~= 0 then if result.code ~= 0 then
logger.log('nix build #submitEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN) logger.log('nix build #submitEnv failed: ' .. (result.stderr or ''), { level = vim.log.levels.WARN })
return false return false
end end
@ -160,7 +160,7 @@ local function discover_nix_submit_cmd()
local submit_cmd = store_path .. '/bin/cp-nvim-submit' local submit_cmd = store_path .. '/bin/cp-nvim-submit'
if vim.fn.executable(submit_cmd) ~= 1 then if vim.fn.executable(submit_cmd) ~= 1 then
logger.log('nix submit cmd not executable at ' .. submit_cmd, vim.log.levels.WARN) logger.log('nix submit cmd not executable at ' .. submit_cmd, { level = vim.log.levels.WARN })
return false return false
end end
@ -216,7 +216,7 @@ local function discover_nix_python()
:wait() :wait()
if result.code ~= 0 then if result.code ~= 0 then
logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN) logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), { level = vim.log.levels.WARN })
return false return false
end end
@ -224,7 +224,7 @@ local function discover_nix_python()
local python_path = store_path .. '/bin/python3' local python_path = store_path .. '/bin/python3'
if vim.fn.executable(python_path) ~= 1 then if vim.fn.executable(python_path) ~= 1 then
logger.log('nix python not executable at ' .. python_path, vim.log.levels.WARN) logger.log('nix python not executable at ' .. python_path, { level = vim.log.levels.WARN })
return false return false
end end
@ -270,7 +270,7 @@ function M.setup_python_env()
if result.code ~= 0 then if result.code ~= 0 then
logger.log( logger.log(
'Failed to setup Python environment: ' .. (result.stderr or ''), 'Failed to setup Python environment: ' .. (result.stderr or ''),
vim.log.levels.ERROR { level = vim.log.levels.ERROR }
) )
return false return false
end end
@ -292,7 +292,7 @@ function M.setup_python_env()
logger.log( logger.log(
'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.', 'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.',
vim.log.levels.WARN { level = vim.log.levels.WARN }
) )
return false return false
end end

View file

@ -43,7 +43,6 @@ end, {
vim.list_extend(candidates, platforms) vim.list_extend(candidates, platforms)
table.insert(candidates, 'cache') table.insert(candidates, 'cache')
table.insert(candidates, 'pick') table.insert(candidates, 'pick')
if platform and contest_id then if platform and contest_id then
vim.list_extend(candidates, actions) vim.list_extend(candidates, actions)
local cache = require('cp.cache') local cache = require('cp.cache')
@ -60,10 +59,11 @@ end, {
return filter_candidates(candidates) return filter_candidates(candidates)
elseif num_args == 3 then elseif num_args == 3 then
if vim.tbl_contains(platforms, args[2]) then if vim.tbl_contains(platforms, args[2]) then
local candidates = { 'login', 'logout' }
local cache = require('cp.cache') local cache = require('cp.cache')
cache.load() cache.load()
local contests = cache.get_cached_contest_ids(args[2]) vim.list_extend(candidates, cache.get_cached_contest_ids(args[2]))
return filter_candidates(contests) return filter_candidates(candidates)
elseif args[2] == 'cache' then elseif args[2] == 'cache' then
return filter_candidates({ 'clear', 'read' }) return filter_candidates({ 'clear', 'read' })
elseif args[2] == 'stress' or args[2] == 'interact' then elseif args[2] == 'stress' or args[2] == 'interact' then
@ -103,8 +103,6 @@ end, {
end end
end end
return filter_candidates(candidates) return filter_candidates(candidates)
elseif args[2] == 'login' or args[2] == 'logout' then
return filter_candidates(platforms)
elseif args[2] == 'race' then elseif args[2] == 'race' then
local candidates = { 'stop' } local candidates = { 'stop' }
vim.list_extend(candidates, platforms) vim.list_extend(candidates, platforms)

View file

@ -38,6 +38,11 @@ from .timeouts import (
HTTP_TIMEOUT, HTTP_TIMEOUT,
) )
_LANGUAGE_ID_EXTENSION = {
"6017": "cc",
"6082": "py",
}
MIB_TO_MB = 1.048576 MIB_TO_MB = 1.048576
BASE_URL = "https://atcoder.jp" BASE_URL = "https://atcoder.jp"
ARCHIVE_URL = f"{BASE_URL}/contests/archive" ARCHIVE_URL = f"{BASE_URL}/contests/archive"