fix(submit): use file path over stdin; fix CF CodeMirror textarea (#305)
## Problem After the initial submit hardening, two issues remained: source code was read in Lua and piped as stdin to the scraper (unnecessary roundtrip since the file exists on disk), and CF's `page.fill()` timed out on the hidden `textarea[name="source"]` because CodeMirror owns the editor state. ## Solution Pass the source file path as a CLI arg instead — AtCoder calls `page.set_input_files(file_path)` directly, CF reads it with `Path(file_path).read_text()`. Fix CF source injection via `page.evaluate()` into the CodeMirror instance. Extract `BROWSER_SUBMIT_NAV_TIMEOUT` as a per-platform `defaultdict` (CF defaults to 2× nav timeout). Save the buffer with `vim.cmd.update()` before submitting.
This commit is contained in:
parent
127089c57f
commit
a202725cc5
28 changed files with 269 additions and 168 deletions
|
|
@ -47,26 +47,30 @@ function M.handle_cache_command(cmd)
|
|||
constants.PLATFORM_DISPLAY_NAMES[cmd.platform],
|
||||
cmd.contest
|
||||
),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
{ level = vim.log.levels.INFO, override = true }
|
||||
)
|
||||
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
|
||||
elseif cmd.platform then
|
||||
if vim.tbl_contains(platforms, cmd.platform) then
|
||||
cache.clear_platform(cmd.platform)
|
||||
logger.log(
|
||||
("Cache cleared for platform '%s'"):format(constants.PLATFORM_DISPLAY_NAMES[cmd.platform]),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
{ level = vim.log.levels.INFO, override = true }
|
||||
)
|
||||
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
|
||||
else
|
||||
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
|
||||
|
|
|
|||
|
|
@ -83,8 +83,6 @@ local function parse_command(args)
|
|||
else
|
||||
return { type = 'action', action = 'interact' }
|
||||
end
|
||||
elseif first == 'login' or first == 'logout' then
|
||||
return { type = 'action', action = first, platform = args[2] }
|
||||
elseif first == 'stress' then
|
||||
return {
|
||||
type = 'action',
|
||||
|
|
@ -245,6 +243,9 @@ local function parse_command(args)
|
|||
message = 'Too few arguments - specify a contest.',
|
||||
}
|
||||
elseif #args == 2 then
|
||||
if args[2] == 'login' or args[2] == 'logout' then
|
||||
return { type = 'action', action = args[2], platform = first }
|
||||
end
|
||||
return {
|
||||
type = 'contest_setup',
|
||||
platform = first,
|
||||
|
|
@ -287,7 +288,7 @@ function M.handle_command(opts)
|
|||
local cmd = parse_command(opts.fargs)
|
||||
|
||||
if cmd.type == 'error' then
|
||||
logger.log(cmd.message, vim.log.levels.ERROR)
|
||||
logger.log(cmd.message, { level = vim.log.levels.ERROR })
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -336,7 +337,7 @@ function M.handle_command(opts)
|
|||
local problem_id = cmd.problem_id
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -351,7 +352,7 @@ function M.handle_command(opts)
|
|||
contest_id,
|
||||
problem_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ function M.handle_pick_action(language)
|
|||
if not (config.ui and config.ui.picker) then
|
||||
logger.log(
|
||||
'No picker configured. Set ui.picker = "{telescope,fzf-lua}" in your config.',
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
@ -25,13 +25,13 @@ function M.handle_pick_action(language)
|
|||
if not ok then
|
||||
logger.log(
|
||||
'telescope.nvim is not available. Install telescope.nvim xor change your picker config.',
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
local ok_cp, telescope_picker = pcall(require, 'cp.pickers.telescope')
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -41,13 +41,13 @@ function M.handle_pick_action(language)
|
|||
if not ok then
|
||||
logger.log(
|
||||
'fzf-lua is not available. Install fzf-lua or change your picker config',
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
local ok_cp, fzf_picker = pcall(require, 'cp.pickers.fzf_lua')
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,6 @@ M.ACTIONS = {
|
|||
'race',
|
||||
'stress',
|
||||
'submit',
|
||||
'login',
|
||||
'logout',
|
||||
}
|
||||
|
||||
M.PLATFORM_DISPLAY_NAMES = {
|
||||
|
|
|
|||
|
|
@ -7,37 +7,43 @@ local state = require('cp.state')
|
|||
function M.login(platform)
|
||||
platform = platform or state.get_platform()
|
||||
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
|
||||
end
|
||||
|
||||
vim.ui.input({ prompt = platform .. ' username: ' }, function(username)
|
||||
if not username or username == '' then
|
||||
logger.log('Cancelled', vim.log.levels.WARN)
|
||||
logger.log('Cancelled', { level = vim.log.levels.WARN })
|
||||
return
|
||||
end
|
||||
vim.fn.inputsave()
|
||||
local password = vim.fn.inputsecret(platform .. ' password: ')
|
||||
vim.fn.inputrestore()
|
||||
if not password or password == '' then
|
||||
logger.log('Cancelled', vim.log.levels.WARN)
|
||||
logger.log('Cancelled', { level = vim.log.levels.WARN })
|
||||
return
|
||||
end
|
||||
cache.load()
|
||||
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
|
||||
|
||||
function M.logout(platform)
|
||||
platform = platform or state.get_platform()
|
||||
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
|
||||
end
|
||||
cache.load()
|
||||
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
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ local logger = require('cp.log')
|
|||
M.helpers = helpers
|
||||
|
||||
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 {}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
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
|
||||
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
|
||||
vim.schedule(function()
|
||||
local notify = function()
|
||||
vim.notify(('[cp.nvim]: %s'):format(msg), level)
|
||||
end)
|
||||
end
|
||||
if sync then
|
||||
notify()
|
||||
else
|
||||
vim.schedule(notify)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -42,10 +42,10 @@ function M.get_platform_contests(platform, refresh)
|
|||
local picker_contests = cache.get_contest_summaries(platform)
|
||||
|
||||
if refresh or vim.tbl_isempty(picker_contests) then
|
||||
local display_name = constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
logger.log(
|
||||
('Loading %s contests...'):format(constants.PLATFORM_DISPLAY_NAMES[platform]),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
('Fetching %s contests...'):format(display_name),
|
||||
{ level = vim.log.levels.INFO, override = true, sync = true }
|
||||
)
|
||||
|
||||
local contests = scraper.scrape_contest_list(platform)
|
||||
|
|
@ -53,12 +53,8 @@ function M.get_platform_contests(platform, refresh)
|
|||
picker_contests = cache.get_contest_summaries(platform)
|
||||
|
||||
logger.log(
|
||||
('Loaded %d %s contests.'):format(
|
||||
#picker_contests,
|
||||
constants.PLATFORM_DISPLAY_NAMES[platform]
|
||||
),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
('Fetched %d %s contests.'):format(#picker_contests, display_name),
|
||||
{ level = vim.log.levels.INFO, override = true }
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -22,15 +22,15 @@ end
|
|||
|
||||
function M.start(platform, contest_id, language)
|
||||
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
|
||||
end
|
||||
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
|
||||
end
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ function M.start(platform, contest_id, language)
|
|||
local start_time = cache.get_contest_start_time(platform, contest_id)
|
||||
|
||||
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)
|
||||
if contests and #contests > 0 then
|
||||
cache.set_contest_summaries(platform, contests)
|
||||
|
|
@ -52,14 +52,17 @@ function M.start(platform, contest_id, language)
|
|||
constants.PLATFORM_DISPLAY_NAMES[platform] or platform,
|
||||
contest_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local remaining = start_time - os.time()
|
||||
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)
|
||||
return
|
||||
end
|
||||
|
|
@ -75,8 +78,7 @@ function M.start(platform, contest_id, language)
|
|||
contest_id,
|
||||
format_countdown(remaining)
|
||||
),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
{ level = vim.log.levels.INFO, override = true }
|
||||
)
|
||||
|
||||
local timer = vim.uv.new_timer()
|
||||
|
|
@ -97,7 +99,7 @@ function M.start(platform, contest_id, language)
|
|||
race_state.contest_id = nil
|
||||
race_state.language = 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)
|
||||
else
|
||||
vim.notify(
|
||||
|
|
@ -116,7 +118,7 @@ end
|
|||
function M.stop()
|
||||
local timer = race_state.timer
|
||||
if not timer then
|
||||
logger.log('No active race', vim.log.levels.WARN)
|
||||
logger.log('No active race', { level = vim.log.levels.WARN })
|
||||
return
|
||||
end
|
||||
timer:stop()
|
||||
|
|
@ -126,7 +128,7 @@ function M.stop()
|
|||
race_state.contest_id = nil
|
||||
race_state.language = 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
|
||||
|
||||
function M.status()
|
||||
|
|
|
|||
|
|
@ -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 file_state = cache.get_file_state(current_file)
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ function M.compile(compile_cmd, substitutions, on_complete)
|
|||
r.stdout = ansi.bytes_to_string(r.stdout or '')
|
||||
|
||||
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
|
||||
logger.log(('Compilation failed in %.1fms.'):format(dt))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ function M.load_test_cases()
|
|||
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
|
||||
end
|
||||
|
||||
|
|
@ -259,7 +259,7 @@ function M.run_combined_test(debug, on_complete)
|
|||
)
|
||||
|
||||
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)
|
||||
return
|
||||
end
|
||||
|
|
@ -330,8 +330,7 @@ function M.run_all_test_cases(indices, debug, on_each, on_done)
|
|||
if #to_run == 0 then
|
||||
logger.log(
|
||||
('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', 0),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
{ level = vim.log.levels.INFO, override = true }
|
||||
)
|
||||
on_done(panel_state.test_cases)
|
||||
return
|
||||
|
|
@ -349,8 +348,7 @@ function M.run_all_test_cases(indices, debug, on_each, on_done)
|
|||
if remaining == 0 then
|
||||
logger.log(
|
||||
('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', total),
|
||||
vim.log.levels.INFO,
|
||||
true
|
||||
{ level = vim.log.levels.INFO, override = true }
|
||||
)
|
||||
on_done(panel_state.test_cases)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ local function syshandle(result)
|
|||
end
|
||||
|
||||
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 }
|
||||
end
|
||||
|
||||
|
|
@ -37,7 +37,7 @@ end
|
|||
local function run_scraper(platform, subcommand, args, opts)
|
||||
if not utils.setup_python_env() then
|
||||
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
|
||||
opts.on_exit({ success = false, error = msg })
|
||||
end
|
||||
|
|
@ -125,7 +125,7 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
if stdin_pipe and not stdin_pipe:is_closing() then
|
||||
stdin_pipe:close()
|
||||
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' }
|
||||
end
|
||||
|
||||
|
|
@ -221,7 +221,7 @@ function M.scrape_contest_metadata(platform, contest_id, callback)
|
|||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
contest_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
@ -232,7 +232,7 @@ function M.scrape_contest_metadata(platform, contest_id, callback)
|
|||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
contest_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
@ -251,7 +251,7 @@ function M.scrape_contest_list(platform)
|
|||
platform,
|
||||
(result and result.error) or 'unknown'
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return {}
|
||||
end
|
||||
|
|
@ -261,9 +261,15 @@ end
|
|||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@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 }, {
|
||||
ndjson = true,
|
||||
on_exit = function()
|
||||
if type(on_done) == 'function' then
|
||||
vim.schedule(on_done)
|
||||
end
|
||||
end,
|
||||
on_event = function(ev)
|
||||
if ev.done then
|
||||
return
|
||||
|
|
@ -275,7 +281,7 @@ function M.scrape_all_tests(platform, contest_id, callback)
|
|||
contest_id,
|
||||
ev.error
|
||||
),
|
||||
vim.log.levels.WARN
|
||||
{ level = vim.log.levels.WARN }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
@ -316,15 +322,14 @@ function M.submit(
|
|||
contest_id,
|
||||
problem_id,
|
||||
language,
|
||||
source_code,
|
||||
source_file,
|
||||
credentials,
|
||||
on_status,
|
||||
callback
|
||||
)
|
||||
local done = false
|
||||
run_scraper(platform, 'submit', { contest_id, problem_id, language }, {
|
||||
run_scraper(platform, 'submit', { contest_id, problem_id, language, source_file }, {
|
||||
ndjson = true,
|
||||
stdin = source_code,
|
||||
env_extra = { CP_CREDENTIALS = vim.json.encode(credentials) },
|
||||
on_event = function(ev)
|
||||
if ev.credentials ~= nil then
|
||||
|
|
|
|||
|
|
@ -16,7 +16,10 @@ local function apply_template(bufnr, lang_id, platform)
|
|||
end
|
||||
local path = vim.fn.expand(eff.template)
|
||||
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
|
||||
end
|
||||
local lines = vim.fn.readfile(path)
|
||||
|
|
@ -112,11 +115,15 @@ local function start_tests(platform, contest_id, problems)
|
|||
return not vim.tbl_isempty(cache.get_test_cases(platform, contest_id, p.id))
|
||||
end, problems)
|
||||
if cached_len ~= #problems then
|
||||
local to_fetch = #problems - cached_len
|
||||
logger.log(('Fetching %s/%s problem tests...'):format(cached_len, #problems))
|
||||
scraper.scrape_all_tests(platform, contest_id, function(ev)
|
||||
local cached_tests = {}
|
||||
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
|
||||
for i, t in ipairs(ev.tests) do
|
||||
cached_tests[i] = { index = i, input = t.input, expected = t.expected }
|
||||
|
|
@ -142,6 +149,11 @@ local function start_tests(platform, contest_id, problems)
|
|||
require('cp.utils').update_buffer_content(io_state.input_buf, input_lines, nil, nil)
|
||||
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
|
||||
|
|
@ -160,7 +172,7 @@ function M.setup_contest(platform, contest_id, problem_id, language)
|
|||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
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
|
||||
end
|
||||
end
|
||||
|
|
@ -206,7 +218,7 @@ function M.setup_contest(platform, contest_id, problem_id, language)
|
|||
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(
|
||||
platform,
|
||||
contest_id,
|
||||
|
|
@ -242,7 +254,7 @@ end
|
|||
function M.setup_problem(problem_id, language)
|
||||
local platform = state.get_platform()
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -263,7 +275,7 @@ function M.setup_problem(problem_id, language)
|
|||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
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
|
||||
end
|
||||
end
|
||||
|
|
@ -275,6 +287,29 @@ function M.setup_problem(problem_id, language)
|
|||
return
|
||||
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 is_new_dir = vim.fn.isdirectory(contest_dir) == 0
|
||||
vim.fn.mkdir(contest_dir, 'p')
|
||||
|
|
@ -397,7 +432,7 @@ function M.navigate_problem(direction, language)
|
|||
local contest_id = state.get_contest_id()
|
||||
local current_problem_id = state.get_problem_id()
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -409,7 +444,7 @@ function M.navigate_problem(direction, language)
|
|||
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||
contest_id
|
||||
),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
@ -433,7 +468,7 @@ function M.navigate_problem(direction, language)
|
|||
if language then
|
||||
local lang_result = config_module.get_language_for_platform(platform, language)
|
||||
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
|
||||
end
|
||||
lang = language
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ local function compile_cpp(source, output)
|
|||
if result.code ~= 0 then
|
||||
logger.log(
|
||||
('Failed to compile %s: %s'):format(source, result.stderr or ''),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
|
@ -76,7 +76,7 @@ function M.toggle(generator_cmd, brute_cmd)
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -93,14 +93,14 @@ function M.toggle(generator_cmd, brute_cmd)
|
|||
if not gen_file then
|
||||
logger.log(
|
||||
'No generator found. Pass generator as first arg or add gen.{py,cc,cpp}.',
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
if not brute_file then
|
||||
logger.log(
|
||||
'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
|
||||
end
|
||||
|
|
@ -140,7 +140,7 @@ function M.toggle(generator_cmd, brute_cmd)
|
|||
|
||||
local binary = state.get_binary_file()
|
||||
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()
|
||||
return
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ local function prompt_credentials(platform, callback)
|
|||
end
|
||||
vim.ui.input({ prompt = platform .. ' username: ' }, function(username)
|
||||
if not username or username == '' then
|
||||
logger.log('Submit cancelled', vim.log.levels.WARN)
|
||||
logger.log('Submit cancelled', { level = vim.log.levels.WARN })
|
||||
return
|
||||
end
|
||||
vim.fn.inputsave()
|
||||
|
|
@ -27,7 +27,7 @@ local function prompt_credentials(platform, callback)
|
|||
vim.fn.inputrestore()
|
||||
vim.cmd.redraw()
|
||||
if not password or password == '' then
|
||||
logger.log('Submit cancelled', vim.log.levels.WARN)
|
||||
logger.log('Submit cancelled', { level = vim.log.levels.WARN })
|
||||
return
|
||||
end
|
||||
local creds = { username = username, password = password }
|
||||
|
|
@ -42,20 +42,21 @@ function M.submit(opts)
|
|||
local problem_id = state.get_problem_id()
|
||||
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
|
||||
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
|
||||
end
|
||||
|
||||
local source_file = state.get_source_file()
|
||||
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
|
||||
end
|
||||
|
||||
prompt_credentials(platform, function(creds)
|
||||
local source_lines = vim.fn.readfile(source_file)
|
||||
local source_code = table.concat(source_lines, '\n')
|
||||
|
||||
vim.cmd.update()
|
||||
vim.notify('[cp.nvim] Submitting...', vim.log.levels.INFO)
|
||||
|
||||
require('cp.scraper').submit(
|
||||
|
|
@ -63,7 +64,7 @@ function M.submit(opts)
|
|||
contest_id,
|
||||
problem_id,
|
||||
language,
|
||||
source_code,
|
||||
source_file,
|
||||
creds,
|
||||
function(ev)
|
||||
vim.schedule(function()
|
||||
|
|
@ -73,12 +74,13 @@ function M.submit(opts)
|
|||
function(result)
|
||||
vim.schedule(function()
|
||||
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
|
||||
logger.log(
|
||||
'Submit failed: ' .. (result and result.error or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
local err = result and result.error or 'unknown error'
|
||||
if err:match('^Login failed') then
|
||||
cache.clear_credentials(platform)
|
||||
end
|
||||
logger.log('Submit failed: ' .. err, { level = vim.log.levels.ERROR })
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -90,7 +90,7 @@ local function delete_current_test()
|
|||
return
|
||||
end
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -311,7 +311,10 @@ setup_keybindings = function(buf)
|
|||
end
|
||||
|
||||
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()
|
||||
end
|
||||
end)
|
||||
|
|
@ -368,7 +371,10 @@ function M.toggle_edit(test_index)
|
|||
state.get_platform(), state.get_contest_id(), state.get_problem_id()
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -376,7 +382,7 @@ function M.toggle_edit(test_index)
|
|||
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -389,7 +395,7 @@ function M.toggle_edit(test_index)
|
|||
if target_index < 1 or target_index > #test_cases then
|
||||
logger.log(
|
||||
('Test %d does not exist (only %d tests available)'):format(target_index, #test_cases),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ function M.toggle_interactive(interactor_cmd)
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -62,7 +62,7 @@ function M.toggle_interactive(interactor_cmd)
|
|||
if not platform or not contest_id or not problem_id then
|
||||
logger.log(
|
||||
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
@ -74,7 +74,10 @@ function M.toggle_interactive(interactor_cmd)
|
|||
and contest_data.index_map
|
||||
and not contest_data.problems[contest_data.index_map[problem_id]].interactive
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -103,7 +106,7 @@ function M.toggle_interactive(interactor_cmd)
|
|||
|
||||
local binary = state.get_binary_file()
|
||||
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()
|
||||
return
|
||||
end
|
||||
|
|
@ -117,7 +120,7 @@ function M.toggle_interactive(interactor_cmd)
|
|||
if vim.fn.executable(interactor) ~= 1 then
|
||||
logger.log(
|
||||
("Interactor '%s' is not executable."):format(interactor_cmd),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
restore_session()
|
||||
return
|
||||
|
|
@ -354,7 +357,7 @@ function M.ensure_io_view()
|
|||
if not platform or not contest_id or not problem_id then
|
||||
logger.log(
|
||||
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
@ -383,7 +386,10 @@ function M.ensure_io_view()
|
|||
and contest_data.index_map
|
||||
and contest_data.problems[contest_data.index_map[problem_id]].interactive
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -594,12 +600,15 @@ end
|
|||
|
||||
function M.run_io_view(test_indices_arg, debug, mode)
|
||||
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
|
||||
end
|
||||
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'
|
||||
|
||||
|
|
@ -608,7 +617,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
|||
if not platform or not contest_id or not problem_id then
|
||||
logger.log(
|
||||
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
io_view_running = false
|
||||
return
|
||||
|
|
@ -617,7 +626,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
|||
cache.load()
|
||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||
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
|
||||
return
|
||||
end
|
||||
|
|
@ -634,13 +643,13 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
|||
if mode == 'combined' then
|
||||
local combined = cache.get_combined_test(platform, contest_id, problem_id)
|
||||
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
|
||||
return
|
||||
end
|
||||
else
|
||||
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
|
||||
return
|
||||
end
|
||||
|
|
@ -660,7 +669,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
|||
idx,
|
||||
#test_state.test_cases
|
||||
),
|
||||
vim.log.levels.WARN
|
||||
{ level = vim.log.levels.WARN }
|
||||
)
|
||||
io_view_running = false
|
||||
return
|
||||
|
|
@ -721,7 +730,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
|||
if mode == 'combined' then
|
||||
local combined = cache.get_combined_test(platform, contest_id, problem_id)
|
||||
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
|
||||
return
|
||||
end
|
||||
|
|
@ -730,7 +739,7 @@ function M.run_io_view(test_indices_arg, debug, mode)
|
|||
|
||||
run.run_combined_test(debug, function(result)
|
||||
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
|
||||
return
|
||||
end
|
||||
|
|
@ -771,7 +780,7 @@ function M.toggle_panel(panel_opts)
|
|||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -780,7 +789,7 @@ function M.toggle_panel(panel_opts)
|
|||
if not platform or not contest_id then
|
||||
logger.log(
|
||||
'No platform/contest configured. Use :CP <platform> <contest> [...] first.',
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return
|
||||
end
|
||||
|
|
@ -792,7 +801,10 @@ function M.toggle_panel(panel_opts)
|
|||
and contest_data.index_map
|
||||
and contest_data.problems[contest_data.index_map[state.get_problem_id()]].interactive
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -803,7 +815,7 @@ function M.toggle_panel(panel_opts)
|
|||
logger.log(('run panel: checking test cases for %s'):format(input_file or 'none'))
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -152,7 +152,10 @@ local function discover_nix_submit_cmd()
|
|||
:wait()
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -160,7 +163,7 @@ local function discover_nix_submit_cmd()
|
|||
local submit_cmd = store_path .. '/bin/cp-nvim-submit'
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -216,7 +219,10 @@ local function discover_nix_python()
|
|||
:wait()
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -224,7 +230,7 @@ local function discover_nix_python()
|
|||
local python_path = store_path .. '/bin/python3'
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
|
|
@ -270,7 +276,7 @@ function M.setup_python_env()
|
|||
if result.code ~= 0 then
|
||||
logger.log(
|
||||
'Failed to setup Python environment: ' .. (result.stderr or ''),
|
||||
vim.log.levels.ERROR
|
||||
{ level = vim.log.levels.ERROR }
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
|
@ -292,7 +298,7 @@ function M.setup_python_env()
|
|||
|
||||
logger.log(
|
||||
'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
|
||||
end
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ end, {
|
|||
vim.list_extend(candidates, platforms)
|
||||
table.insert(candidates, 'cache')
|
||||
table.insert(candidates, 'pick')
|
||||
|
||||
if platform and contest_id then
|
||||
vim.list_extend(candidates, actions)
|
||||
local cache = require('cp.cache')
|
||||
|
|
@ -60,10 +59,11 @@ end, {
|
|||
return filter_candidates(candidates)
|
||||
elseif num_args == 3 then
|
||||
if vim.tbl_contains(platforms, args[2]) then
|
||||
local candidates = { 'login', 'logout' }
|
||||
local cache = require('cp.cache')
|
||||
cache.load()
|
||||
local contests = cache.get_cached_contest_ids(args[2])
|
||||
return filter_candidates(contests)
|
||||
vim.list_extend(candidates, cache.get_cached_contest_ids(args[2]))
|
||||
return filter_candidates(candidates)
|
||||
elseif args[2] == 'cache' then
|
||||
return filter_candidates({ 'clear', 'read' })
|
||||
elseif args[2] == 'stress' or args[2] == 'interact' then
|
||||
|
|
@ -103,8 +103,6 @@ end, {
|
|||
end
|
||||
end
|
||||
return filter_candidates(candidates)
|
||||
elseif args[2] == 'login' or args[2] == 'logout' then
|
||||
return filter_candidates(platforms)
|
||||
elseif args[2] == 'race' then
|
||||
local candidates = { 'stop' }
|
||||
vim.list_extend(candidates, platforms)
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ from .timeouts import (
|
|||
BROWSER_NAV_TIMEOUT,
|
||||
BROWSER_SESSION_TIMEOUT,
|
||||
BROWSER_SETTLE_DELAY,
|
||||
BROWSER_SUBMIT_NAV_TIMEOUT,
|
||||
BROWSER_TURNSTILE_POLL,
|
||||
HTTP_TIMEOUT,
|
||||
)
|
||||
|
|
@ -297,7 +298,7 @@ def _ensure_browser() -> None:
|
|||
def _submit_headless(
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
_retried: bool = False,
|
||||
|
|
@ -362,19 +363,12 @@ def _submit_headless(
|
|||
f'select[name="data.LanguageId"] option[value="{language_id}"]'
|
||||
).wait_for(state="attached", timeout=BROWSER_ELEMENT_WAIT)
|
||||
page.select_option('select[name="data.LanguageId"]', language_id)
|
||||
ext = _LANGUAGE_ID_EXTENSION.get(language_id, "txt")
|
||||
page.set_input_files(
|
||||
"#input-open-file",
|
||||
{
|
||||
"name": f"solution.{ext}",
|
||||
"mimeType": "text/plain",
|
||||
"buffer": source_code.encode(),
|
||||
},
|
||||
)
|
||||
page.set_input_files("#input-open-file", file_path)
|
||||
page.wait_for_timeout(BROWSER_SETTLE_DELAY)
|
||||
page.locator('button[type="submit"]').click()
|
||||
page.wait_for_url(
|
||||
lambda url: "/submissions/me" in url, timeout=BROWSER_NAV_TIMEOUT
|
||||
lambda url: "/submissions/me" in url,
|
||||
timeout=BROWSER_SUBMIT_NAV_TIMEOUT["atcoder"],
|
||||
)
|
||||
except Exception as e:
|
||||
submit_error = str(e)
|
||||
|
|
@ -423,7 +417,7 @@ def _submit_headless(
|
|||
return _submit_headless(
|
||||
contest_id,
|
||||
problem_id,
|
||||
source_code,
|
||||
file_path,
|
||||
language_id,
|
||||
credentials,
|
||||
_retried=True,
|
||||
|
|
@ -581,7 +575,7 @@ class AtcoderScraper(BaseScraper):
|
|||
self,
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
) -> SubmitResult:
|
||||
|
|
@ -589,7 +583,7 @@ class AtcoderScraper(BaseScraper):
|
|||
_submit_headless,
|
||||
contest_id,
|
||||
problem_id,
|
||||
source_code,
|
||||
file_path,
|
||||
language_id,
|
||||
credentials,
|
||||
)
|
||||
|
|
@ -651,15 +645,14 @@ async def main_async() -> int:
|
|||
return 0 if contest_result.success else 1
|
||||
|
||||
if mode == "submit":
|
||||
if len(sys.argv) != 5:
|
||||
if len(sys.argv) != 6:
|
||||
print(
|
||||
SubmitResult(
|
||||
success=False,
|
||||
error="Usage: atcoder.py submit <contest_id> <problem_id> <language>",
|
||||
error="Usage: atcoder.py submit <contest_id> <problem_id> <language> <file_path>",
|
||||
).model_dump_json()
|
||||
)
|
||||
return 1
|
||||
source_code = sys.stdin.read()
|
||||
creds_raw = os.environ.get("CP_CREDENTIALS", "{}")
|
||||
try:
|
||||
credentials = json.loads(creds_raw)
|
||||
|
|
@ -667,7 +660,7 @@ async def main_async() -> int:
|
|||
credentials = {}
|
||||
language_id = get_language_id("atcoder", sys.argv[4]) or sys.argv[4]
|
||||
submit_result = await scraper.submit(
|
||||
sys.argv[2], sys.argv[3], source_code, language_id, credentials
|
||||
sys.argv[2], sys.argv[3], sys.argv[5], language_id, credentials
|
||||
)
|
||||
print(submit_result.model_dump_json())
|
||||
return 0 if submit_result.success else 1
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class BaseScraper(ABC):
|
|||
self,
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
) -> SubmitResult: ...
|
||||
|
|
@ -114,14 +114,13 @@ class BaseScraper(ABC):
|
|||
return 0 if result.success else 1
|
||||
|
||||
case "submit":
|
||||
if len(args) != 5:
|
||||
if len(args) != 6:
|
||||
print(
|
||||
self._submit_error(
|
||||
"Usage: <platform> submit <contest_id> <problem_id> <language_id>"
|
||||
"Usage: <platform> submit <contest_id> <problem_id> <language_id> <file_path>"
|
||||
).model_dump_json()
|
||||
)
|
||||
return 1
|
||||
source_code = sys.stdin.read()
|
||||
creds_raw = os.environ.get("CP_CREDENTIALS", "{}")
|
||||
try:
|
||||
credentials = json.loads(creds_raw)
|
||||
|
|
@ -129,7 +128,7 @@ class BaseScraper(ABC):
|
|||
credentials = {}
|
||||
language_id = get_language_id(self.platform_name, args[4]) or args[4]
|
||||
result = await self.submit(
|
||||
args[2], args[3], source_code, language_id, credentials
|
||||
args[2], args[3], args[5], language_id, credentials
|
||||
)
|
||||
print(result.model_dump_json())
|
||||
return 0 if result.success else 1
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ MEMORY_LIMIT_RE = re.compile(
|
|||
)
|
||||
|
||||
|
||||
async def fetch_json(client: httpx.AsyncClient, path: str) -> dict:
|
||||
async def fetch_json(client: httpx.AsyncClient, path: str) -> dict[str, Any]:
|
||||
r = await client.get(BASE_URL + path, headers=HEADERS, timeout=HTTP_TIMEOUT)
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
|
|
@ -256,7 +256,7 @@ class CodeChefScraper(BaseScraper):
|
|||
self,
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
) -> SubmitResult:
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ from .models import (
|
|||
from .timeouts import (
|
||||
BROWSER_NAV_TIMEOUT,
|
||||
BROWSER_SESSION_TIMEOUT,
|
||||
BROWSER_SUBMIT_NAV_TIMEOUT,
|
||||
HTTP_TIMEOUT,
|
||||
)
|
||||
|
||||
|
|
@ -289,7 +290,7 @@ class CodeforcesScraper(BaseScraper):
|
|||
self,
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
) -> SubmitResult:
|
||||
|
|
@ -297,7 +298,7 @@ class CodeforcesScraper(BaseScraper):
|
|||
_submit_headless,
|
||||
contest_id,
|
||||
problem_id,
|
||||
source_code,
|
||||
file_path,
|
||||
language_id,
|
||||
credentials,
|
||||
)
|
||||
|
|
@ -306,13 +307,15 @@ class CodeforcesScraper(BaseScraper):
|
|||
def _submit_headless(
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
_retried: bool = False,
|
||||
) -> SubmitResult:
|
||||
from pathlib import Path
|
||||
|
||||
source_code = Path(file_path).read_text()
|
||||
|
||||
try:
|
||||
from scrapling.fetchers import StealthySession # type: ignore[import-untyped,unresolved-import]
|
||||
except ImportError:
|
||||
|
|
@ -379,12 +382,22 @@ def _submit_headless(
|
|||
problem_id.upper(),
|
||||
)
|
||||
page.select_option('select[name="programTypeId"]', language_id)
|
||||
page.fill('textarea[name="source"]', source_code)
|
||||
page.evaluate(
|
||||
"""(code) => {
|
||||
const cm = document.querySelector('.CodeMirror');
|
||||
if (cm && cm.CodeMirror) {
|
||||
cm.CodeMirror.setValue(code);
|
||||
}
|
||||
const ta = document.querySelector('textarea[name="source"]');
|
||||
if (ta) ta.value = code;
|
||||
}""",
|
||||
source_code,
|
||||
)
|
||||
page.locator("form.submit-form input.submit").click(no_wait_after=True)
|
||||
try:
|
||||
page.wait_for_url(
|
||||
lambda url: "/my" in url or "/status" in url,
|
||||
timeout=BROWSER_NAV_TIMEOUT * 2,
|
||||
timeout=BROWSER_SUBMIT_NAV_TIMEOUT["codeforces"],
|
||||
)
|
||||
except Exception:
|
||||
err_el = page.query_selector("span.error")
|
||||
|
|
@ -441,7 +454,7 @@ def _submit_headless(
|
|||
return _submit_headless(
|
||||
contest_id,
|
||||
problem_id,
|
||||
source_code,
|
||||
file_path,
|
||||
language_id,
|
||||
credentials,
|
||||
_retried=True,
|
||||
|
|
|
|||
|
|
@ -357,10 +357,13 @@ class CSESScraper(BaseScraper):
|
|||
self,
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
) -> SubmitResult:
|
||||
from pathlib import Path
|
||||
|
||||
source_code = Path(file_path).read_text()
|
||||
username = credentials.get("username", "")
|
||||
password = credentials.get("password", "")
|
||||
if not username or not password:
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ class KattisScraper(BaseScraper):
|
|||
self,
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
) -> SubmitResult:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
from collections import defaultdict
|
||||
|
||||
HTTP_TIMEOUT = 15.0
|
||||
|
||||
BROWSER_SESSION_TIMEOUT = 15000
|
||||
BROWSER_NAV_TIMEOUT = 10000
|
||||
BROWSER_SUBMIT_NAV_TIMEOUT: defaultdict[str, int] = defaultdict(
|
||||
lambda: BROWSER_NAV_TIMEOUT
|
||||
)
|
||||
BROWSER_SUBMIT_NAV_TIMEOUT["codeforces"] = BROWSER_NAV_TIMEOUT * 2
|
||||
BROWSER_TURNSTILE_POLL = 5000
|
||||
BROWSER_ELEMENT_WAIT = 10000
|
||||
BROWSER_SETTLE_DELAY = 500
|
||||
|
|
|
|||
|
|
@ -73,8 +73,11 @@ def _parse_results_page(html: str) -> dict[str, list[tuple[str, str]]]:
|
|||
for part in parts:
|
||||
heading_m = DIVISION_HEADING_RE.search(part)
|
||||
if heading_m:
|
||||
current_div = heading_m.group(3).lower()
|
||||
sections.setdefault(current_div, [])
|
||||
div = heading_m.group(3)
|
||||
if div:
|
||||
key = div.lower()
|
||||
current_div = key
|
||||
sections.setdefault(key, [])
|
||||
continue
|
||||
if current_div is not None:
|
||||
for m in PROBLEM_BLOCK_RE.finditer(part):
|
||||
|
|
@ -285,7 +288,7 @@ class USACOScraper(BaseScraper):
|
|||
self,
|
||||
contest_id: str,
|
||||
problem_id: str,
|
||||
source_code: str,
|
||||
file_path: str,
|
||||
language_id: str,
|
||||
credentials: dict[str, str],
|
||||
) -> SubmitResult:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue