diff --git a/lua/cp/commands/cache.lua b/lua/cp/commands/cache.lua index cec3eba..aba8bf5 100644 --- a/lua/cp/commands/cache.lua +++ b/lua/cp/commands/cache.lua @@ -47,30 +47,26 @@ function M.handle_cache_command(cmd) constants.PLATFORM_DISPLAY_NAMES[cmd.platform], cmd.contest ), - { level = vim.log.levels.INFO, override = true } + vim.log.levels.INFO, + true ) else - logger.log( - ("Unknown platform '%s'."):format(cmd.platform), - { level = vim.log.levels.ERROR } - ) + logger.log(("Unknown platform '%s'."):format(cmd.platform), 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]), - { level = vim.log.levels.INFO, override = true } + vim.log.levels.INFO, + true ) else - logger.log( - ("Unknown platform '%s'."):format(cmd.platform), - { level = vim.log.levels.ERROR } - ) + logger.log(("Unknown platform '%s'."):format(cmd.platform), vim.log.levels.ERROR) end else cache.clear_all() - logger.log('Cache cleared', { level = vim.log.levels.INFO, override = true }) + logger.log('Cache cleared', vim.log.levels.INFO, true) end end end diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 4c594bd..56a473f 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -83,6 +83,8 @@ 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', @@ -243,9 +245,6 @@ 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, @@ -288,7 +287,7 @@ function M.handle_command(opts) local cmd = parse_command(opts.fargs) if cmd.type == 'error' then - logger.log(cmd.message, { level = vim.log.levels.ERROR }) + logger.log(cmd.message, vim.log.levels.ERROR) return end @@ -337,7 +336,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.', { level = vim.log.levels.ERROR }) + logger.log('No contest is currently active.', vim.log.levels.ERROR) return end @@ -352,7 +351,7 @@ function M.handle_command(opts) contest_id, problem_id ), - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return end diff --git a/lua/cp/commands/picker.lua b/lua/cp/commands/picker.lua index e01b680..dc49bea 100644 --- a/lua/cp/commands/picker.lua +++ b/lua/cp/commands/picker.lua @@ -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.', - { level = vim.log.levels.ERROR } + 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.', - { level = vim.log.levels.ERROR } + 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.', { level = vim.log.levels.ERROR }) + logger.log('Failed to load telescope integration.', 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', - { level = vim.log.levels.ERROR } + 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.', { level = vim.log.levels.ERROR }) + logger.log('Failed to load fzf-lua integration.', vim.log.levels.ERROR) return end diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index 21e8f62..5d1c3c5 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -13,6 +13,8 @@ M.ACTIONS = { 'race', 'stress', 'submit', + 'login', + 'logout', } M.PLATFORM_DISPLAY_NAMES = { diff --git a/lua/cp/credentials.lua b/lua/cp/credentials.lua index 76945b6..8f80743 100644 --- a/lua/cp/credentials.lua +++ b/lua/cp/credentials.lua @@ -7,43 +7,37 @@ 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 ', - { level = vim.log.levels.ERROR } - ) + logger.log('No platform specified. Usage: :CP login ', vim.log.levels.ERROR) return end vim.ui.input({ prompt = platform .. ' username: ' }, function(username) if not username or username == '' then - logger.log('Cancelled', { level = vim.log.levels.WARN }) + logger.log('Cancelled', 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', { level = vim.log.levels.WARN }) + logger.log('Cancelled', vim.log.levels.WARN) return end cache.load() cache.set_credentials(platform, { username = username, password = password }) - logger.log(platform .. ' credentials saved', { level = vim.log.levels.INFO, override = true }) + logger.log(platform .. ' credentials saved', vim.log.levels.INFO, 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 ', - { level = vim.log.levels.ERROR } - ) + logger.log('No platform specified. Usage: :CP logout ', vim.log.levels.ERROR) return end cache.load() cache.clear_credentials(platform) - logger.log(platform .. ' credentials cleared', { level = vim.log.levels.INFO, override = true }) + logger.log(platform .. ' credentials cleared', vim.log.levels.INFO, true) end return M diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 4ffb530..088272a 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -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+', { level = vim.log.levels.ERROR }) + logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR) return {} end diff --git a/lua/cp/log.lua b/lua/cp/log.lua index 74a8859..02bc5f4 100644 --- a/lua/cp/log.lua +++ b/lua/cp/log.lua @@ -1,27 +1,12 @@ local M = {} ----@class LogOpts ----@field level? integer ----@field override? boolean ----@field sync? boolean - ----@param msg string ----@param opts? LogOpts -function M.log(msg, opts) +function M.log(msg, level, override) local debug = require('cp.config').get_config().debug or false - opts = opts or {} - local level = opts.level or vim.log.levels.INFO - local override = opts.override or false - local sync = opts.sync or false + level = level or vim.log.levels.INFO if level >= vim.log.levels.WARN or override or debug then - local notify = function() + vim.schedule(function() vim.notify(('[cp.nvim]: %s'):format(msg), level) - end - if sync then - notify() - else - vim.schedule(notify) - end + end) end end diff --git a/lua/cp/pickers/init.lua b/lua/cp/pickers/init.lua index 562c274..c634c64 100644 --- a/lua/cp/pickers/init.lua +++ b/lua/cp/pickers/init.lua @@ -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( - ('Fetching %s contests...'):format(display_name), - { level = vim.log.levels.INFO, override = true, sync = true } + ('Loading %s contests...'):format(constants.PLATFORM_DISPLAY_NAMES[platform]), + vim.log.levels.INFO, + true ) local contests = scraper.scrape_contest_list(platform) @@ -53,8 +53,12 @@ function M.get_platform_contests(platform, refresh) picker_contests = cache.get_contest_summaries(platform) 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 diff --git a/lua/cp/race.lua b/lua/cp/race.lua index b44b9ab..0e303ee 100644 --- a/lua/cp/race.lua +++ b/lua/cp/race.lua @@ -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', { level = vim.log.levels.ERROR }) + logger.log('Invalid platform', vim.log.levels.ERROR) return end if not contest_id or contest_id == '' then - logger.log('Contest ID required', { level = vim.log.levels.ERROR }) + logger.log('Contest ID required', vim.log.levels.ERROR) return end if race_state.timer then - logger.log('Race already active. Use :CP race stop first.', { level = vim.log.levels.WARN }) + logger.log('Race already active. Use :CP race stop first.', 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...', { level = vim.log.levels.INFO, override = true }) + logger.log('Fetching contest list...', vim.log.levels.INFO, true) local contests = scraper.scrape_contest_list(platform) if contests and #contests > 0 then cache.set_contest_summaries(platform, contests) @@ -52,17 +52,14 @@ function M.start(platform, contest_id, language) constants.PLATFORM_DISPLAY_NAMES[platform] or platform, contest_id ), - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return end local remaining = start_time - os.time() if remaining <= 0 then - logger.log( - 'Contest has already started, setting up...', - { level = vim.log.levels.INFO, override = true } - ) + logger.log('Contest has already started, setting up...', vim.log.levels.INFO, true) require('cp.setup').setup_contest(platform, contest_id, nil, language) return end @@ -78,7 +75,8 @@ function M.start(platform, contest_id, language) contest_id, format_countdown(remaining) ), - { level = vim.log.levels.INFO, override = true } + vim.log.levels.INFO, + true ) local timer = vim.uv.new_timer() @@ -99,7 +97,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!', { level = vim.log.levels.INFO, override = true }) + logger.log('Contest started!', vim.log.levels.INFO, true) require('cp.setup').setup_contest(p, c, nil, l) else vim.notify( @@ -118,7 +116,7 @@ end function M.stop() local timer = race_state.timer if not timer then - logger.log('No active race', { level = vim.log.levels.WARN }) + logger.log('No active race', vim.log.levels.WARN) return end timer:stop() @@ -128,7 +126,7 @@ function M.stop() race_state.contest_id = nil race_state.language = nil race_state.start_time = nil - logger.log('Race cancelled', { level = vim.log.levels.INFO, override = true }) + logger.log('Race cancelled', vim.log.levels.INFO, true) end function M.status() diff --git a/lua/cp/restore.lua b/lua/cp/restore.lua index 8b9eeab..fde3fd5 100644 --- a/lua/cp/restore.lua +++ b/lua/cp/restore.lua @@ -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.', { level = vim.log.levels.ERROR }) + logger.log('No cached state found for current file.', vim.log.levels.ERROR) return false end diff --git a/lua/cp/runner/execute.lua b/lua/cp/runner/execute.lua index 41a7b51..9e40b4f 100644 --- a/lua/cp/runner/execute.lua +++ b/lua/cp/runner/execute.lua @@ -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), { level = vim.log.levels.INFO }) + logger.log(('Compilation successful in %.1fms.'):format(dt), vim.log.levels.INFO) else logger.log(('Compilation failed in %.1fms.'):format(dt)) end diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 2b374b0..306a1f8 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -245,7 +245,7 @@ function M.load_test_cases() state.get_problem_id() ) - logger.log(('Loaded %d test case(s)'):format(#tcs), { level = vim.log.levels.INFO }) + logger.log(('Loaded %d test case(s)'):format(#tcs), 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', { level = vim.log.levels.ERROR }) + logger.log('No combined test found', vim.log.levels.ERROR) on_complete(nil) return end @@ -330,7 +330,8 @@ 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), - { level = vim.log.levels.INFO, override = true } + vim.log.levels.INFO, + true ) on_done(panel_state.test_cases) return @@ -348,7 +349,8 @@ 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), - { level = vim.log.levels.INFO, override = true } + vim.log.levels.INFO, + true ) on_done(panel_state.test_cases) end diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index af170d3..6980c7d 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -16,7 +16,7 @@ local function syshandle(result) end local msg = 'Failed to parse scraper output: ' .. tostring(data) - logger.log(msg, { level = vim.log.levels.ERROR }) + logger.log(msg, 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, { level = vim.log.levels.ERROR }) + logger.log(msg, 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', { level = vim.log.levels.ERROR }) + logger.log('Failed to start scraper process', 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 ), - { level = vim.log.levels.ERROR } + 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 ), - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return end @@ -251,7 +251,7 @@ function M.scrape_contest_list(platform) platform, (result and result.error) or 'unknown' ), - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return {} end @@ -261,15 +261,9 @@ end ---@param platform string ---@param contest_id string ---@param callback fun(data: table)|nil ----@param on_done fun()|nil -function M.scrape_all_tests(platform, contest_id, callback, on_done) +function M.scrape_all_tests(platform, contest_id, callback) 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 @@ -281,7 +275,7 @@ function M.scrape_all_tests(platform, contest_id, callback, on_done) contest_id, ev.error ), - { level = vim.log.levels.WARN } + vim.log.levels.WARN ) return end @@ -322,14 +316,15 @@ function M.submit( contest_id, problem_id, language, - source_file, + source_code, credentials, on_status, callback ) local done = false - run_scraper(platform, 'submit', { contest_id, problem_id, language, source_file }, { + run_scraper(platform, 'submit', { contest_id, problem_id, language }, { ndjson = true, + stdin = source_code, env_extra = { CP_CREDENTIALS = vim.json.encode(credentials) }, on_event = function(ev) if ev.credentials ~= nil then diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index eb7ba8c..19fa776 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -16,10 +16,7 @@ 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), - { level = vim.log.levels.WARN } - ) + logger.log(('[cp.nvim] template not readable: %s'):format(path), vim.log.levels.WARN) return end local lines = vim.fn.readfile(path) @@ -115,15 +112,11 @@ 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), - { level = vim.log.levels.WARN } - ) + logger.log(("No tests found for problem '%s'."):format(ev.problem_id), vim.log.levels.WARN) end for i, t in ipairs(ev.tests) do cached_tests[i] = { index = i, input = t.input, expected = t.expected } @@ -149,11 +142,6 @@ 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 @@ -172,7 +160,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, { level = vim.log.levels.ERROR }) + logger.log(lang_result.error, vim.log.levels.ERROR) return end end @@ -218,7 +206,7 @@ function M.setup_contest(platform, contest_id, problem_id, language) token = vim.uv.hrtime(), }) - logger.log('Fetching contests problems...', { level = vim.log.levels.INFO, override = true }) + logger.log('Fetching contests problems...', vim.log.levels.INFO, true) scraper.scrape_contest_metadata( platform, contest_id, @@ -254,7 +242,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.', { level = vim.log.levels.ERROR }) + logger.log('No platform/contest/problem configured.', vim.log.levels.ERROR) return end @@ -275,7 +263,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, { level = vim.log.levels.ERROR }) + logger.log(lang_result.error, vim.log.levels.ERROR) return end end @@ -287,29 +275,6 @@ 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') @@ -432,7 +397,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.', { level = vim.log.levels.ERROR }) + logger.log('No platform configured.', vim.log.levels.ERROR) return end @@ -444,7 +409,7 @@ function M.navigate_problem(direction, language) constants.PLATFORM_DISPLAY_NAMES[platform], contest_id ), - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return end @@ -468,7 +433,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, { level = vim.log.levels.ERROR }) + logger.log(lang_result.error, vim.log.levels.ERROR) return end lang = language diff --git a/lua/cp/stress.lua b/lua/cp/stress.lua index 9063d0d..3e51881 100644 --- a/lua/cp/stress.lua +++ b/lua/cp/stress.lua @@ -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 ''), - { level = vim.log.levels.ERROR } + 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.', { level = vim.log.levels.WARN }) + logger.log('Another panel is already active.', 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}.', - { level = vim.log.levels.ERROR } + 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}.', - { level = vim.log.levels.ERROR } + 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.', { level = vim.log.levels.ERROR }) + logger.log('No binary produced.', vim.log.levels.ERROR) restore_session() return end diff --git a/lua/cp/submit.lua b/lua/cp/submit.lua index 77e3d66..7dc9a71 100644 --- a/lua/cp/submit.lua +++ b/lua/cp/submit.lua @@ -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', { level = vim.log.levels.WARN }) + logger.log('Submit cancelled', 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', { level = vim.log.levels.WARN }) + logger.log('Submit cancelled', vim.log.levels.WARN) return end local creds = { username = username, password = password } @@ -42,21 +42,20 @@ 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 first.', - { level = vim.log.levels.ERROR } - ) + logger.log('No active problem. Use :CP first.', 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', { level = vim.log.levels.ERROR }) + logger.log('Source file not found', vim.log.levels.ERROR) return end prompt_credentials(platform, function(creds) - vim.cmd.update() + local source_lines = vim.fn.readfile(source_file) + local source_code = table.concat(source_lines, '\n') + vim.notify('[cp.nvim] Submitting...', vim.log.levels.INFO) require('cp.scraper').submit( @@ -64,7 +63,7 @@ function M.submit(opts) contest_id, problem_id, language, - source_file, + source_code, creds, function(ev) vim.schedule(function() @@ -74,13 +73,12 @@ function M.submit(opts) function(result) vim.schedule(function() if result and result.success then - logger.log('Submitted successfully', { level = vim.log.levels.INFO, override = true }) + logger.log('Submitted successfully', vim.log.levels.INFO, true) else - 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 }) + logger.log( + 'Submit failed: ' .. (result and result.error or 'unknown error'), + vim.log.levels.ERROR + ) end end) end diff --git a/lua/cp/ui/edit.lua b/lua/cp/ui/edit.lua index 93083aa..20d4e83 100644 --- a/lua/cp/ui/edit.lua +++ b/lua/cp/ui/edit.lua @@ -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.', { level = vim.log.levels.ERROR }) + logger.log('Problems must have at least one test case.', vim.log.levels.ERROR) return end @@ -311,10 +311,7 @@ setup_keybindings = function(buf) end if is_tracked then - logger.log( - 'Test buffer closed unexpectedly. Exiting editor.', - { level = vim.log.levels.WARN } - ) + logger.log('Test buffer closed unexpectedly. Exiting editor.', vim.log.levels.WARN) M.toggle_edit() end end) @@ -371,10 +368,7 @@ 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 first.', - { level = vim.log.levels.ERROR } - ) + logger.log('No problem context. Run :CP first.', vim.log.levels.ERROR) return end @@ -382,7 +376,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.', { level = vim.log.levels.ERROR }) + logger.log('No test cases available for editing.', vim.log.levels.ERROR) return end @@ -395,7 +389,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), - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return end diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index b2ee23d..5e658c4 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -53,7 +53,7 @@ function M.toggle_interactive(interactor_cmd) end if state.get_active_panel() then - logger.log('Another panel is already active.', { level = vim.log.levels.WARN }) + logger.log('Another panel is already active.', 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 [...] first.', - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return end @@ -74,10 +74,7 @@ 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}.', - { level = vim.log.levels.ERROR } - ) + logger.log('This problem is not interactive. Use :CP {run,panel}.', vim.log.levels.ERROR) return end @@ -106,7 +103,7 @@ function M.toggle_interactive(interactor_cmd) local binary = state.get_binary_file() if not binary or binary == '' then - logger.log('No binary produced.', { level = vim.log.levels.ERROR }) + logger.log('No binary produced.', vim.log.levels.ERROR) restore_session() return end @@ -120,7 +117,7 @@ function M.toggle_interactive(interactor_cmd) if vim.fn.executable(interactor) ~= 1 then logger.log( ("Interactor '%s' is not executable."):format(interactor_cmd), - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) restore_session() return @@ -357,7 +354,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 [...] first.', - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return end @@ -386,10 +383,7 @@ 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}.', - { level = vim.log.levels.ERROR } - ) + logger.log('This problem is not interactive. Use :CP {run,panel}.', vim.log.levels.ERROR) return end @@ -600,15 +594,12 @@ end function M.run_io_view(test_indices_arg, debug, mode) if io_view_running then - logger.log('Tests already running', { level = vim.log.levels.WARN }) + logger.log('Tests already running', vim.log.levels.WARN) return end io_view_running = true - logger.log( - ('%s tests...'):format(debug and 'Debugging' or 'Running'), - { level = vim.log.levels.INFO, override = true } - ) + logger.log(('%s tests...'):format(debug and 'Debugging' or 'Running'), vim.log.levels.INFO, true) mode = mode or 'combined' @@ -617,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 logger.log( 'No platform/contest/problem configured. Use :CP [...] first.', - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) io_view_running = false return @@ -626,7 +617,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.', { level = vim.log.levels.ERROR }) + logger.log('No test cases available.', vim.log.levels.ERROR) io_view_running = false return end @@ -643,13 +634,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', { level = vim.log.levels.ERROR }) + logger.log('No combined test available', vim.log.levels.ERROR) io_view_running = false return end else if not run.load_test_cases() then - logger.log('No test cases available', { level = vim.log.levels.ERROR }) + logger.log('No test cases available', vim.log.levels.ERROR) io_view_running = false return end @@ -669,7 +660,7 @@ function M.run_io_view(test_indices_arg, debug, mode) idx, #test_state.test_cases ), - { level = vim.log.levels.WARN } + vim.log.levels.WARN ) io_view_running = false return @@ -730,7 +721,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', { level = vim.log.levels.ERROR }) + logger.log('No combined test found', vim.log.levels.ERROR) io_view_running = false return end @@ -739,7 +730,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', { level = vim.log.levels.ERROR }) + logger.log('Failed to run combined test', vim.log.levels.ERROR) io_view_running = false return end @@ -780,7 +771,7 @@ function M.toggle_panel(panel_opts) end if state.get_active_panel() then - logger.log('another panel is already active', { level = vim.log.levels.ERROR }) + logger.log('another panel is already active', vim.log.levels.ERROR) return end @@ -789,7 +780,7 @@ function M.toggle_panel(panel_opts) if not platform or not contest_id then logger.log( 'No platform/contest configured. Use :CP [...] first.', - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return end @@ -801,10 +792,7 @@ 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.', - { level = vim.log.levels.WARN } - ) + logger.log('This is an interactive problem. Use :CP interact instead.', vim.log.levels.WARN) return end @@ -815,7 +803,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', { level = vim.log.levels.WARN }) + logger.log('no test cases found', vim.log.levels.WARN) return end diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index 48e5dc5..3111fec 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -152,10 +152,7 @@ local function discover_nix_submit_cmd() :wait() if result.code ~= 0 then - logger.log( - 'nix build #submitEnv failed: ' .. (result.stderr or ''), - { level = vim.log.levels.WARN } - ) + logger.log('nix build #submitEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN) return false end @@ -163,7 +160,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, { level = vim.log.levels.WARN }) + logger.log('nix submit cmd not executable at ' .. submit_cmd, vim.log.levels.WARN) return false end @@ -219,10 +216,7 @@ local function discover_nix_python() :wait() if result.code ~= 0 then - logger.log( - 'nix build #pythonEnv failed: ' .. (result.stderr or ''), - { level = vim.log.levels.WARN } - ) + logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN) return false end @@ -230,7 +224,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, { level = vim.log.levels.WARN }) + logger.log('nix python not executable at ' .. python_path, vim.log.levels.WARN) return false end @@ -276,7 +270,7 @@ function M.setup_python_env() if result.code ~= 0 then logger.log( 'Failed to setup Python environment: ' .. (result.stderr or ''), - { level = vim.log.levels.ERROR } + vim.log.levels.ERROR ) return false end @@ -298,7 +292,7 @@ function M.setup_python_env() logger.log( 'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.', - { level = vim.log.levels.WARN } + vim.log.levels.WARN ) return false end diff --git a/plugin/cp.lua b/plugin/cp.lua index 60efb7a..db9a8bd 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -43,6 +43,7 @@ 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') @@ -59,11 +60,10 @@ 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() - vim.list_extend(candidates, cache.get_cached_contest_ids(args[2])) - return filter_candidates(candidates) + local contests = cache.get_cached_contest_ids(args[2]) + return filter_candidates(contests) elseif args[2] == 'cache' then return filter_candidates({ 'clear', 'read' }) elseif args[2] == 'stress' or args[2] == 'interact' then @@ -103,6 +103,8 @@ 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) diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 33d3574..8b3db72 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -33,7 +33,6 @@ from .timeouts import ( BROWSER_NAV_TIMEOUT, BROWSER_SESSION_TIMEOUT, BROWSER_SETTLE_DELAY, - BROWSER_SUBMIT_NAV_TIMEOUT, BROWSER_TURNSTILE_POLL, HTTP_TIMEOUT, ) @@ -298,7 +297,7 @@ def _ensure_browser() -> None: def _submit_headless( contest_id: str, problem_id: str, - file_path: str, + source_code: str, language_id: str, credentials: dict[str, str], _retried: bool = False, @@ -363,12 +362,19 @@ 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) - page.set_input_files("#input-open-file", file_path) + 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.wait_for_timeout(BROWSER_SETTLE_DELAY) page.locator('button[type="submit"]').click() page.wait_for_url( - lambda url: "/submissions/me" in url, - timeout=BROWSER_SUBMIT_NAV_TIMEOUT["atcoder"], + lambda url: "/submissions/me" in url, timeout=BROWSER_NAV_TIMEOUT ) except Exception as e: submit_error = str(e) @@ -417,7 +423,7 @@ def _submit_headless( return _submit_headless( contest_id, problem_id, - file_path, + source_code, language_id, credentials, _retried=True, @@ -575,7 +581,7 @@ class AtcoderScraper(BaseScraper): self, contest_id: str, problem_id: str, - file_path: str, + source_code: str, language_id: str, credentials: dict[str, str], ) -> SubmitResult: @@ -583,7 +589,7 @@ class AtcoderScraper(BaseScraper): _submit_headless, contest_id, problem_id, - file_path, + source_code, language_id, credentials, ) @@ -645,14 +651,15 @@ async def main_async() -> int: return 0 if contest_result.success else 1 if mode == "submit": - if len(sys.argv) != 6: + if len(sys.argv) != 5: print( SubmitResult( success=False, - error="Usage: atcoder.py submit ", + error="Usage: atcoder.py submit ", ).model_dump_json() ) return 1 + source_code = sys.stdin.read() creds_raw = os.environ.get("CP_CREDENTIALS", "{}") try: credentials = json.loads(creds_raw) @@ -660,7 +667,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], sys.argv[5], language_id, credentials + sys.argv[2], sys.argv[3], source_code, language_id, credentials ) print(submit_result.model_dump_json()) return 0 if submit_result.success else 1 diff --git a/scrapers/base.py b/scrapers/base.py index c77e293..ed0636b 100644 --- a/scrapers/base.py +++ b/scrapers/base.py @@ -53,7 +53,7 @@ class BaseScraper(ABC): self, contest_id: str, problem_id: str, - file_path: str, + source_code: str, language_id: str, credentials: dict[str, str], ) -> SubmitResult: ... @@ -114,13 +114,14 @@ class BaseScraper(ABC): return 0 if result.success else 1 case "submit": - if len(args) != 6: + if len(args) != 5: print( self._submit_error( - "Usage: submit " + "Usage: submit " ).model_dump_json() ) return 1 + source_code = sys.stdin.read() creds_raw = os.environ.get("CP_CREDENTIALS", "{}") try: credentials = json.loads(creds_raw) @@ -128,7 +129,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], args[5], language_id, credentials + args[2], args[3], source_code, language_id, credentials ) print(result.model_dump_json()) return 0 if result.success else 1 diff --git a/scrapers/codechef.py b/scrapers/codechef.py index b7f4ec7..c4b9d37 100644 --- a/scrapers/codechef.py +++ b/scrapers/codechef.py @@ -33,7 +33,7 @@ MEMORY_LIMIT_RE = re.compile( ) -async def fetch_json(client: httpx.AsyncClient, path: str) -> dict[str, Any]: +async def fetch_json(client: httpx.AsyncClient, path: str) -> dict: 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, - file_path: str, + source_code: str, language_id: str, credentials: dict[str, str], ) -> SubmitResult: diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 5ac615a..895a511 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -21,7 +21,6 @@ from .models import ( from .timeouts import ( BROWSER_NAV_TIMEOUT, BROWSER_SESSION_TIMEOUT, - BROWSER_SUBMIT_NAV_TIMEOUT, HTTP_TIMEOUT, ) @@ -290,7 +289,7 @@ class CodeforcesScraper(BaseScraper): self, contest_id: str, problem_id: str, - file_path: str, + source_code: str, language_id: str, credentials: dict[str, str], ) -> SubmitResult: @@ -298,7 +297,7 @@ class CodeforcesScraper(BaseScraper): _submit_headless, contest_id, problem_id, - file_path, + source_code, language_id, credentials, ) @@ -307,15 +306,13 @@ class CodeforcesScraper(BaseScraper): def _submit_headless( contest_id: str, problem_id: str, - file_path: str, + source_code: 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: @@ -397,7 +394,7 @@ def _submit_headless( try: page.wait_for_url( lambda url: "/my" in url or "/status" in url, - timeout=BROWSER_SUBMIT_NAV_TIMEOUT["codeforces"], + timeout=BROWSER_NAV_TIMEOUT * 2, ) except Exception: err_el = page.query_selector("span.error") @@ -454,7 +451,7 @@ def _submit_headless( return _submit_headless( contest_id, problem_id, - file_path, + source_code, language_id, credentials, _retried=True, diff --git a/scrapers/cses.py b/scrapers/cses.py index 4df3fcc..7d9f4f0 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -357,13 +357,10 @@ class CSESScraper(BaseScraper): self, contest_id: str, problem_id: str, - file_path: str, + source_code: 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: diff --git a/scrapers/kattis.py b/scrapers/kattis.py index 566473c..2bfd2d6 100644 --- a/scrapers/kattis.py +++ b/scrapers/kattis.py @@ -273,7 +273,7 @@ class KattisScraper(BaseScraper): self, contest_id: str, problem_id: str, - file_path: str, + source_code: str, language_id: str, credentials: dict[str, str], ) -> SubmitResult: diff --git a/scrapers/timeouts.py b/scrapers/timeouts.py index 1862349..a21ad0d 100644 --- a/scrapers/timeouts.py +++ b/scrapers/timeouts.py @@ -1,13 +1,7 @@ -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 diff --git a/scrapers/usaco.py b/scrapers/usaco.py index 099f102..9e4d7da 100644 --- a/scrapers/usaco.py +++ b/scrapers/usaco.py @@ -73,11 +73,8 @@ 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: - div = heading_m.group(3) - if div: - key = div.lower() - current_div = key - sections.setdefault(key, []) + current_div = heading_m.group(3).lower() + sections.setdefault(current_div, []) continue if current_div is not None: for m in PROBLEM_BLOCK_RE.finditer(part): @@ -288,7 +285,7 @@ class USACOScraper(BaseScraper): self, contest_id: str, problem_id: str, - file_path: str, + source_code: str, language_id: str, credentials: dict[str, str], ) -> SubmitResult: