From b679e2b932e7ebcaaa8027690698512d520a52f3 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 15 Sep 2025 13:54:50 -0400 Subject: [PATCH 1/8] feat: remove default contest --- doc/cp.txt | 110 ++++++++++++++++++++++++---------------------- lua/cp/config.lua | 85 ++++++----------------------------- lua/cp/init.lua | 11 ++--- lua/cp/window.lua | 7 ++- readme.md | 2 + 5 files changed, 81 insertions(+), 134 deletions(-) diff --git a/doc/cp.txt b/doc/cp.txt index ff67c4b..26b9477 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -107,6 +107,11 @@ Optional configuration with lazy.nvim: > -- ctx.problem_id, ctx.platform, ctx.source_file, etc. vim.cmd.w() end, + setup_code = function(ctx) + vim.wo.foldmethod = "marker" + vim.wo.foldmarker = "{{{,}}}" + vim.diagnostic.enable(false) + end, }, snippets = { ... }, -- LuaSnip snippets tile = function(source_buf, input_buf, output_buf) ... end, @@ -115,56 +120,63 @@ Optional configuration with lazy.nvim: > } < -Configuration options: +*cp.Config* -contests Dictionary of contest configurations - each contest inherits from 'default'. + Fields: ~ + • {contests} (`table`) Contest configurations. + Each contest inherits from 'default'. + • {hooks} (`cp.Hooks`) Hook functions called at various stages. + • {snippets} (`table[]`) LuaSnip snippet definitions. + • {debug} (`boolean`, default: `false`) Show info messages + during operation. + • {tile}? (`function`) Custom window arrangement function. + `function(source_buf, input_buf, output_buf)` + • {filename}? (`function`) Custom filename generation function. + `function(contest, problem_id, problem_letter)` - cpp C++ language configuration - compile Compile command template with {version}, {source}, {binary} placeholders - run Run command template with {binary} placeholder - debug Debug compile command template - version C++ standard version (e.g. 20, 23) - extension File extension for C++ files (default: "cc") +*cp.ContestConfig* - python Python language configuration - run Run command template with {source} placeholder - debug Debug run command template - extension File extension for Python files (default: "py") + Fields: ~ + • {cpp} (`LanguageConfig`) C++ language configuration. + • {python} (`LanguageConfig`) Python language configuration. + • {default_language} (`string`, default: `"cpp"`) Default language when + `--lang` not specified. + • {timeout_ms} (`number`, default: `2000`) Execution timeout in + milliseconds. - default_language Default language when --lang not specified (default: "cpp") +*cp.LanguageConfig* - timeout_ms Duration (ms) to run/debug before timeout + Fields: ~ + • {compile}? (`string[]`) Compile command template with + `{version}`, `{source}`, `{binary}` placeholders. + • {run} (`string[]`) Run command template. + • {debug}? (`string[]`) Debug compile command template. + • {version}? (`number`) Language version (e.g. 20, 23 for C++). + • {extension} (`string`) File extension (e.g. "cc", "py"). + • {executable}? (`string`) Executable name for interpreted languages. -snippets LuaSnip snippets by contest type +*cp.Hooks* -hooks Functions called at specific events - before_run Called before :CP run - function(ctx) - ctx contains: - - problem_id: string - - platform: string (atcoder/codeforces/cses) - - contest_id: string - - source_file: string (path to source) - - input_file: string (path to .cpin) - - output_file: string (path to .cpout) - - expected_file: string (path to .expected) - - contest_config: table (language configs) - (default: nil, do nothing) - before_debug Called before :CP debug - function(ctx) - Same ctx as before_run - (default: nil, do nothing) + Fields: ~ + • {before_run}? (`function`) Called before `:CP run`. + `function(ctx: HookContext)` + • {before_debug}? (`function`) Called before `:CP debug`. + `function(ctx: HookContext)` + • {setup_code}? (`function`) Called after source file is opened. + Used to configure buffer settings. + `function(ctx: HookContext)` -debug Show info messages during operation - (default: false, silent operation) +*cp.HookContext* -tile Custom function to arrange windows - function(source_buf, input_buf, output_buf) - (default: nil, uses built-in layout) - -filename Custom function to generate filenames - function(contest, problem_id, problem_letter) - (default: nil, uses problem_id + letter + ".cc") + Fields: ~ + • {problem_id} (`string`) Problem identifier (e.g. "a", "b"). + • {platform} (`string`) Platform name (e.g. "codeforces"). + • {contest_id} (`string`) Contest identifier (e.g. "1933"). + • {source_file} (`string`) Path to source file. + • {input_file} (`string`) Path to input file (.cpin). + • {output_file} (`string`) Path to output file (.cpout). + • {expected_file} (`string`) Path to expected output file. + • {contest_config} (`table`) Contest configuration. WORKFLOW *cp-workflow* @@ -272,19 +284,13 @@ maintains proper file associations. SNIPPETS *cp-snippets* -cp.nvim integrates with LuaSnip for automatic template expansion. When you -open a new problem file, type the contest name and press to expand. +cp.nvim integrates with LuaSnip for automatic template expansion. Built-in +snippets include basic C++ and Python templates for each contest type. -Built-in snippets include basic C++ and Python templates for each contest type. -Custom snippets can be added via configuration. +Snippet trigger names must EXACTLY match platform names ("codeforces" for +CodeForces, "cses" for CSES, etc.). -IMPORTANT: Snippet trigger names must exactly match the contest/platform names: -- "codeforces" for Codeforces problems -- "atcoder" for AtCoder problems -- "cses" for CSES problems - -The plugin automatically selects the appropriate template based on the file -extension (e.g., .cc files get C++ templates, .py files get Python templates). +Custom snippets can be added via the `snippets` configuration field. HEALTH CHECK *cp-health* diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 0404ab9..dd0a54a 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -39,6 +39,7 @@ ---@class Hooks ---@field before_run? fun(ctx: HookContext) ---@field before_debug? fun(ctx: HookContext) +---@field setup_code? fun(ctx: HookContext) ---@class cp.Config ---@field contests table @@ -67,78 +68,18 @@ local filetype_to_language = { ---@type cp.Config M.defaults = { - contests = { - default = { - cpp = { - compile = { - "g++", - "-std=c++{version}", - "-O2", - "-DLOCAL", - "-Wall", - "-Wextra", - "{source}", - "-o", - "{binary}", - }, - run = { "{binary}" }, - debug = { - "g++", - "-std=c++{version}", - "-g3", - "-fsanitize=address,undefined", - "-DLOCAL", - "{source}", - "-o", - "{binary}", - }, - executable = nil, - version = 20, - extension = "cc", - }, - python = { - compile = nil, - run = { "{source}" }, - debug = { "{source}" }, - executable = "python3", - extension = "py", - }, - default_language = "cpp", - timeout_ms = 2000, - }, - ---@type PartialContestConfig - atcoder = { - ---@type PartialLanguageConfig - cpp = { version = 23 }, - }, - ---@type PartialContestConfig - codeforces = { - ---@type PartialLanguageConfig - cpp = { version = 23 }, - }, - ---@type PartialContestConfig - cses = { - ---@type PartialLanguageConfig - cpp = { version = 20 }, - }, - }, + contests = {}, snippets = {}, hooks = { before_run = nil, before_debug = nil, + setup_code = nil, }, debug = false, tile = nil, filename = nil, } ----@param base_config table ----@param contest_config table ----@return table -local function extend_contest_config(base_config, contest_config) - local result = vim.tbl_deep_extend("force", base_config, contest_config) - return result -end ---@param user_config cp.UserConfig|nil ---@return cp.Config @@ -161,6 +102,7 @@ function M.setup(user_config) vim.validate({ before_run = { user_config.hooks.before_run, { "function", "nil" }, true }, before_debug = { user_config.hooks.before_debug, { "function", "nil" }, true }, + setup_code = { user_config.hooks.setup_code, { "function", "nil" }, true }, }) end @@ -185,14 +127,6 @@ function M.setup(user_config) end local config = vim.tbl_deep_extend("force", M.defaults, user_config or {}) - - local default_contest = config.contests.default - for contest_name, contest_config in pairs(config.contests) do - if contest_name ~= "default" then - config.contests[contest_name] = extend_contest_config(default_contest, contest_config) - end - end - return config end @@ -218,9 +152,18 @@ local function default_filename(contest, contest_id, problem_id, config, languag end end - local contest_config = config.contests[contest] or config.contests.default + local contest_config = config.contests[contest] local target_language = language or contest_config.default_language local language_config = contest_config[target_language] + + if not language_config then + error(("No language config found for '%s' in contest '%s'"):format(target_language, contest)) + end + + if not language_config.extension then + error(("No extension configured for language '%s' in contest '%s'"):format(target_language, contest)) + end + return full_problem_id .. "." .. language_config.extension end diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 72d0213..f78abb0 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -135,13 +135,10 @@ local function setup_problem(contest_id, problem_id, language) end end - vim.api.nvim_set_option_value("winbar", "", { scope = "local" }) - vim.api.nvim_set_option_value("foldlevel", 0, { scope = "local" }) - vim.api.nvim_set_option_value("foldmethod", "marker", { scope = "local" }) - vim.api.nvim_set_option_value("foldmarker", "{{{,}}}", { scope = "local" }) - vim.api.nvim_set_option_value("foldtext", "", { scope = "local" }) - - vim.diagnostic.enable(false) + if config.hooks and config.hooks.setup_code then + local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config) + config.hooks.setup_code(ctx) + end local source_buf = vim.api.nvim_get_current_buf() local input_buf = vim.fn.bufnr(ctx.input_file, true) diff --git a/lua/cp/window.lua b/lua/cp/window.lua index 72ebe8b..767a6d4 100644 --- a/lua/cp/window.lua +++ b/lua/cp/window.lua @@ -10,6 +10,7 @@ ---@field height integer local M = {} +local languages = require("cp.languages") function M.clearcol() vim.api.nvim_set_option_value("number", false, { scope = "local" }) @@ -78,7 +79,7 @@ function M.restore_layout(state, tile_fn) local source_file if source_files ~= "" then local files = vim.split(source_files, "\n") - local valid_extensions = { "cc", "cpp", "cxx", "c", "py", "py3" } + local valid_extensions = vim.tbl_keys(languages.filetype_to_language) for _, file in ipairs(files) do local ext = vim.fn.fnamemodify(file, ":e") if vim.tbl_contains(valid_extensions, ext) then @@ -87,11 +88,9 @@ function M.restore_layout(state, tile_fn) end end source_file = source_file or files[1] - else - source_file = problem_id .. ".cc" end - if vim.fn.filereadable(source_file) == 0 then + if not source_file or vim.fn.filereadable(source_file) == 0 then return end diff --git a/readme.md b/readme.md index 8cafc5e..0cf652f 100644 --- a/readme.md +++ b/readme.md @@ -4,6 +4,8 @@ neovim plugin for competitive programming. https://private-user-images.githubusercontent.com/62671086/489116291-391976d1-c2f4-49e6-a79d-13ff05e9be86.mp4?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3NTc3NDQ1ODEsIm5iZiI6MTc1Nzc0NDI4MSwicGF0aCI6Ii82MjY3MTA4Ni80ODkxMTYyOTEtMzkxOTc2ZDEtYzJmNC00OWU2LWE3OWQtMTNmZjA1ZTliZTg2Lm1wND9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNTA5MTMlMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjUwOTEzVDA2MTgwMVomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPWI0Zjc0YmQzNWIzNGZkM2VjZjM3NGM0YmZmM2I3MmJkZGQ0YTczYjIxMTFiODc3MjQyMzY3ODc2ZTUxZDRkMzkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0In0.MBK5q_Zxr0gWuzfjwmSbB7P7dtWrATrT5cDOosdPRuQ +[video config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua) + > Sample test data from [codeforces](https://codeforces.com) is scraped via [cloudscraper](https://github.com/VeNoMouS/cloudscraper). Use at your own risk. ## Features From e1ad439781dfd6a10436909e16f5ec3571f4a20c Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 15 Sep 2025 13:55:20 -0400 Subject: [PATCH 2/8] feat(doc): update and remove default contrest mentions --- doc/cp.txt | 1 - lua/cp/config.lua | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/cp.txt b/doc/cp.txt index 26b9477..72ec0fd 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -124,7 +124,6 @@ Optional configuration with lazy.nvim: > Fields: ~ • {contests} (`table`) Contest configurations. - Each contest inherits from 'default'. • {hooks} (`cp.Hooks`) Hook functions called at various stages. • {snippets} (`table[]`) LuaSnip snippet definitions. • {debug} (`boolean`, default: `false`) Show info messages diff --git a/lua/cp/config.lua b/lua/cp/config.lua index dd0a54a..66ba2e7 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -80,7 +80,6 @@ M.defaults = { filename = nil, } - ---@param user_config cp.UserConfig|nil ---@return cp.Config function M.setup(user_config) @@ -153,6 +152,10 @@ local function default_filename(contest, contest_id, problem_id, config, languag end local contest_config = config.contests[contest] + if not contest_config then + error(("No contest config found for '%s'"):format(contest)) + end + local target_language = language or contest_config.default_language local language_config = contest_config[target_language] From 17cdbf0a5036f26e4f86a306ecf0193b48b5772e Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 15 Sep 2025 14:04:31 -0400 Subject: [PATCH 3/8] feat: update outdated variable references --- doc/cp.txt | 24 +++++++++++----------- lua/cp/config.lua | 47 ++++++++------------------------------------ lua/cp/languages.lua | 1 - lua/cp/problem.lua | 27 ++++++++++++++++++++++--- 4 files changed, 45 insertions(+), 54 deletions(-) diff --git a/doc/cp.txt b/doc/cp.txt index 72ec0fd..cb8dc28 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -77,7 +77,7 @@ Optional configuration with lazy.nvim: > opts = { debug = false, contests = { - default = { + codeforces = { cpp = { compile = { 'g++', '-std=c++{version}', '-O2', '-Wall', '-Wextra', @@ -89,7 +89,7 @@ Optional configuration with lazy.nvim: > '-fsanitize=address,undefined', '-DLOCAL', '{source}', '-o', '{binary}', }, - version = 20, + version = 23, extension = "cc", }, python = { @@ -97,9 +97,9 @@ Optional configuration with lazy.nvim: > debug = { 'python3', '{source}' }, extension = "py", }, + default_language = "cpp", timeout_ms = 2000, }, - codeforces = { cpp = { version = 23 } }, }, hooks = { before_run = function(ctx) vim.cmd.w() end, @@ -115,7 +115,7 @@ Optional configuration with lazy.nvim: > }, snippets = { ... }, -- LuaSnip snippets tile = function(source_buf, input_buf, output_buf) ... end, - filename = function(contest, problem_id, problem_letter) ... end, + filename = function(contest, contest_id, problem_id, config, language) ... end, } } < @@ -131,7 +131,9 @@ Optional configuration with lazy.nvim: > • {tile}? (`function`) Custom window arrangement function. `function(source_buf, input_buf, output_buf)` • {filename}? (`function`) Custom filename generation function. - `function(contest, problem_id, problem_letter)` + `function(contest, contest_id, problem_id, config, language)` + Should return full filename with extension. + (default: uses problem_id or contest_id) *cp.ContestConfig* @@ -243,7 +245,7 @@ Example: Setting up and solving AtCoder contest ABC324 3. Start with problem A: > :CP a -< This creates abc324a.cc and scrapes test cases +< This creates a.cc and scrapes test cases 4. Code your solution, then test: > :CP run @@ -270,13 +272,13 @@ FILE STRUCTURE *cp-files* cp.nvim creates the following file structure upon problem setup: - {contest_id}{problem_id}.cc " Source file (e.g. abc324a.cc) + {problem_id}.{ext} " Source file (e.g. a.cc, b.py) build/ - {contest_id}{problem_id}.run " Compiled binary + {problem_id}.run " Compiled binary io/ - {contest_id}{problem_id}.cpin " Test input - {contest_id}{problem_id}.cpout " Program output - {contest_id}{problem_id}.expected " Expected output + {problem_id}.cpin " Test input + {problem_id}.cpout " Program output + {problem_id}.expected " Expected output The plugin automatically manages this structure and navigation between problems maintains proper file associations. diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 66ba2e7..e318db9 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -58,13 +58,7 @@ ---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string local M = {} - -local filetype_to_language = { - cc = "cpp", - c = "cpp", - py = "python", - py3 = "python", -} +local languages = require("cp.languages") ---@type cp.Config M.defaults = { @@ -109,13 +103,13 @@ function M.setup(user_config) for contest_name, contest_config in pairs(user_config.contests) do for lang_name, lang_config in pairs(contest_config) do if type(lang_config) == "table" and lang_config.extension then - if not vim.tbl_contains(vim.tbl_keys(filetype_to_language), lang_config.extension) then + if not vim.tbl_contains(vim.tbl_keys(languages.filetype_to_language), lang_config.extension) then error( ("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format( lang_config.extension, lang_name, contest_name, - table.concat(vim.tbl_keys(filetype_to_language), ", ") + table.concat(vim.tbl_keys(languages.filetype_to_language), ", ") ) ) end @@ -129,45 +123,20 @@ function M.setup(user_config) return config end ----@param contest string ---@param contest_id string ---@param problem_id? string ----@param config cp.Config ----@param language? string ---@return string -local function default_filename(contest, contest_id, problem_id, config, language) +local function default_filename(contest_id, problem_id) vim.validate({ - contest = { contest, "string" }, contest_id = { contest_id, "string" }, problem_id = { problem_id, { "string", "nil" }, true }, - config = { config, "table" }, - language = { language, { "string", "nil" }, true }, }) - local full_problem_id = contest_id:lower() - if contest == "atcoder" or contest == "codeforces" then - if problem_id then - full_problem_id = full_problem_id .. problem_id:lower() - end + if problem_id then + return problem_id:lower() + else + return contest_id:lower() end - - local contest_config = config.contests[contest] - if not contest_config then - error(("No contest config found for '%s'"):format(contest)) - end - - local target_language = language or contest_config.default_language - local language_config = contest_config[target_language] - - if not language_config then - error(("No language config found for '%s' in contest '%s'"):format(target_language, contest)) - end - - if not language_config.extension then - error(("No extension configured for language '%s' in contest '%s'"):format(target_language, contest)) - end - - return full_problem_id .. "." .. language_config.extension end M.default_filename = default_filename diff --git a/lua/cp/languages.lua b/lua/cp/languages.lua index 6884287..134d312 100644 --- a/lua/cp/languages.lua +++ b/lua/cp/languages.lua @@ -8,7 +8,6 @@ M.filetype_to_language = { cc = M.CPP, cxx = M.CPP, cpp = M.CPP, - c = M.CPP, py = M.PYTHON, py3 = M.PYTHON, } diff --git a/lua/cp/problem.lua b/lua/cp/problem.lua index caeac2c..088b908 100644 --- a/lua/cp/problem.lua +++ b/lua/cp/problem.lua @@ -26,9 +26,30 @@ function M.create_context(contest, contest_id, problem_id, config, language) language = { language, { "string", "nil" }, true }, }) - local filename_fn = config.filename or require("cp.config").default_filename - local source_file = filename_fn(contest, contest_id, problem_id, config, language) - local base_name = vim.fn.fnamemodify(source_file, ":t:r") + local contest_config = config.contests[contest] + if not contest_config then + error(("No contest config found for '%s'"):format(contest)) + end + + local target_language = language or contest_config.default_language + local language_config = contest_config[target_language] + if not language_config then + error(("No language config found for '%s' in contest '%s'"):format(target_language, contest)) + end + if not language_config.extension then + error(("No extension configured for language '%s' in contest '%s'"):format(target_language, contest)) + end + + local base_name + if config.filename then + local source_file = config.filename(contest, contest_id, problem_id, config, language) + base_name = vim.fn.fnamemodify(source_file, ":t:r") + else + local default_filename = require("cp.config").default_filename + base_name = default_filename(contest_id, problem_id) + end + + local source_file = base_name .. "." .. language_config.extension return { contest = contest, From b3ffef7341f5ef6d5ed987a32ce4877d78f151a9 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 15 Sep 2025 14:11:12 -0400 Subject: [PATCH 4/8] more typechecking --- doc/cp.txt | 18 +++--------------- lua/cp/config.lua | 20 ++++++-------------- lua/cp/execute.lua | 8 ++++---- lua/cp/init.lua | 4 ++-- 4 files changed, 15 insertions(+), 35 deletions(-) diff --git a/doc/cp.txt b/doc/cp.txt index cb8dc28..62cc2e1 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -160,24 +160,12 @@ Optional configuration with lazy.nvim: > Fields: ~ • {before_run}? (`function`) Called before `:CP run`. - `function(ctx: HookContext)` + `function(ctx: ProblemContext)` • {before_debug}? (`function`) Called before `:CP debug`. - `function(ctx: HookContext)` + `function(ctx: ProblemContext)` • {setup_code}? (`function`) Called after source file is opened. Used to configure buffer settings. - `function(ctx: HookContext)` - -*cp.HookContext* - - Fields: ~ - • {problem_id} (`string`) Problem identifier (e.g. "a", "b"). - • {platform} (`string`) Platform name (e.g. "codeforces"). - • {contest_id} (`string`) Contest identifier (e.g. "1933"). - • {source_file} (`string`) Path to source file. - • {input_file} (`string`) Path to input file (.cpin). - • {output_file} (`string`) Path to output file (.cpout). - • {expected_file} (`string`) Path to expected output file. - • {contest_config} (`table`) Contest configuration. + `function(ctx: ProblemContext)` WORKFLOW *cp-workflow* diff --git a/lua/cp/config.lua b/lua/cp/config.lua index e318db9..d32e26c 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -26,20 +26,10 @@ ---@field default_language? string ---@field timeout_ms? number ----@class HookContext ----@field problem_id string ----@field platform string ----@field contest_id string ----@field source_file string ----@field input_file string ----@field output_file string ----@field expected_file string ----@field contest_config table - ---@class Hooks ----@field before_run? fun(ctx: HookContext) ----@field before_debug? fun(ctx: HookContext) ----@field setup_code? fun(ctx: HookContext) +---@field before_run? fun(ctx: ProblemContext) +---@field before_debug? fun(ctx: ProblemContext) +---@field setup_code? fun(ctx: ProblemContext) ---@class cp.Config ---@field contests table @@ -103,7 +93,9 @@ function M.setup(user_config) for contest_name, contest_config in pairs(user_config.contests) do for lang_name, lang_config in pairs(contest_config) do if type(lang_config) == "table" and lang_config.extension then - if not vim.tbl_contains(vim.tbl_keys(languages.filetype_to_language), lang_config.extension) then + if + not vim.tbl_contains(vim.tbl_keys(languages.filetype_to_language), lang_config.extension) + then error( ("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format( lang_config.extension, diff --git a/lua/cp/execute.lua b/lua/cp/execute.lua index a0874f1..78358bc 100644 --- a/lua/cp/execute.lua +++ b/lua/cp/execute.lua @@ -103,9 +103,9 @@ local function compile_generic(language_config, substitutions) local compile_cmd = substitute_template(language_config.compile, substitutions) logger.log(("compiling: %s"):format(table.concat(compile_cmd, " "))) - local start_time = vim.loop.hrtime() + local start_time = vim.uv.hrtime() local result = vim.system(compile_cmd, { text = true }):wait() - local compile_time = (vim.loop.hrtime() - start_time) / 1000000 + local compile_time = (vim.uv.hrtime() - start_time) / 1000000 if result.code == 0 then logger.log(("compilation successful (%.1fms)"):format(compile_time)) @@ -129,7 +129,7 @@ local function execute_command(cmd, input_data, timeout_ms) logger.log(("executing: %s"):format(table.concat(cmd, " "))) - local start_time = vim.loop.hrtime() + local start_time = vim.uv.hrtime() local result = vim.system(cmd, { stdin = input_data, @@ -137,7 +137,7 @@ local function execute_command(cmd, input_data, timeout_ms) text = true, }):wait() - local end_time = vim.loop.hrtime() + local end_time = vim.uv.hrtime() local execution_time = (end_time - start_time) / 1000000 local actual_code = result.code or 0 diff --git a/lua/cp/init.lua b/lua/cp/init.lua index f78abb0..adb801d 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -41,8 +41,8 @@ local function set_platform(platform) end state.platform = platform - vim.fn.mkdir("build", "p") - vim.fn.mkdir("io", "p") + vim.fs.mkdir("build", { parents = true }) + vim.fs.mkdir("io", { parents = true }) return true end From 95b0381ef5b7abda250e8ea8dabeb3bca6bb456a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 15 Sep 2025 14:12:42 -0400 Subject: [PATCH 5/8] fix(ci): more typecheck --- lua/cp/init.lua | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index adb801d..e076a15 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -176,16 +176,7 @@ local function run_problem() local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config) if config.hooks and config.hooks.before_run then - config.hooks.before_run({ - problem_id = problem_id, - platform = state.platform, - contest_id = state.contest_id, - source_file = ctx.source_file, - input_file = ctx.input_file, - output_file = ctx.output_file, - expected_file = ctx.expected_file, - contest_config = contest_config, - }) + config.hooks.before_run(ctx) end vim.schedule(function() @@ -209,16 +200,7 @@ local function debug_problem() local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config) if config.hooks and config.hooks.before_debug then - config.hooks.before_debug({ - problem_id = problem_id, - platform = state.platform, - contest_id = state.contest_id, - source_file = ctx.source_file, - input_file = ctx.input_file, - output_file = ctx.output_file, - expected_file = ctx.expected_file, - contest_config = contest_config, - }) + config.hooks.before_debug(ctx) end vim.schedule(function() From 3889341335ca26d6c52cf70784293922aa23df3f Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 15 Sep 2025 14:13:47 -0400 Subject: [PATCH 6/8] fix(ci/selene): remove name shadowing --- lua/cp/init.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index e076a15..38ad3de 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -135,8 +135,9 @@ local function setup_problem(contest_id, problem_id, language) end end + local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config, language) + if config.hooks and config.hooks.setup_code then - local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config) config.hooks.setup_code(ctx) end From f743b75a59f591c0d8b9a691e8efd68d82851cfd Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 15 Sep 2025 14:16:45 -0400 Subject: [PATCH 7/8] fix(ci/selene): shadowing --- lua/cp/init.lua | 6 +++--- selene.toml | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 38ad3de..92b78a7 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -89,9 +89,9 @@ local function setup_problem(contest_id, problem_id, language) state.test_cases = cached_test_cases end - local ctx = problem.create_context(state.platform, contest_id, problem_id, config, language) + local scrape_ctx = problem.create_context(state.platform, contest_id, problem_id, config, language) - local scrape_result = scrape.scrape_problem(ctx) + local scrape_result = scrape.scrape_problem(scrape_ctx) if not scrape_result.success then logger.log("scraping failed: " .. (scrape_result.error or "unknown error"), vim.log.levels.WARN) @@ -107,7 +107,7 @@ local function setup_problem(contest_id, problem_id, language) end end - vim.cmd.e(ctx.source_file) + vim.cmd.e(scrape_ctx.source_file) if vim.api.nvim_buf_get_lines(0, 0, -1, true)[1] == "" then local has_luasnip, luasnip = pcall(require, "luasnip") diff --git a/selene.toml b/selene.toml index df08b7c..47b9138 100644 --- a/selene.toml +++ b/selene.toml @@ -1,4 +1 @@ -std = "vim" - -[config] -lua52 = true +std = "lua51+vim" From 5417da9b52b6a045b15583e310fb7f9e26dd6ed1 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Mon, 15 Sep 2025 14:32:44 -0400 Subject: [PATCH 8/8] feat: make autocomplete more sophisticated --- lua/cp/init.lua | 4 ++-- plugin/cp.lua | 9 ++++++--- scrapers/codeforces.py | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 92b78a7..6ea45da 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -41,8 +41,8 @@ local function set_platform(platform) end state.platform = platform - vim.fs.mkdir("build", { parents = true }) - vim.fs.mkdir("io", { parents = true }) + vim.fn.mkdir("build", "p") + vim.fn.mkdir("io", "p") return true end diff --git a/plugin/cp.lua b/plugin/cp.lua index f96db7f..9e19695 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -26,8 +26,10 @@ end, { end, lang_completions) end - if ArgLead == "--lang" then - return { "--lang" } + if ArgLead:match("^%-") and not ArgLead:match("^--lang") then + return vim.tbl_filter(function(completion) + return completion:find(ArgLead, 1, true) == 1 + end, { "--lang" }) end local args = vim.split(vim.trim(CmdLine), "%s+") @@ -43,7 +45,6 @@ end, { if num_args == 2 then local candidates = { "--lang" } - vim.list_extend(candidates, platforms) vim.list_extend(candidates, actions) local cp = require("cp") local context = cp.get_current_context() @@ -56,6 +57,8 @@ end, { table.insert(candidates, problem.id) end end + else + vim.list_extend(candidates, platforms) end return vim.tbl_filter(function(cmd) return cmd:find(ArgLead, 1, true) == 1 diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 4610ae9..8e89b0a 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -30,7 +30,7 @@ def scrape(url: str) -> list[tuple[str, str]]: lines = [div.get_text().strip() for div in divs] text = "\n".join(lines) else: - text = inp_pre.get_text().replace("\r", "") + text = inp_pre.get_text().replace("\r", "").strip() all_inputs.append(text) for out_section in output_sections: @@ -41,7 +41,7 @@ def scrape(url: str) -> list[tuple[str, str]]: lines = [div.get_text().strip() for div in divs] text = "\n".join(lines) else: - text = out_pre.get_text().replace("\r", "") + text = out_pre.get_text().replace("\r", "").strip() all_outputs.append(text) if all_inputs and all_outputs: