From 3cb872a65f85145e3b688c70a6f6475fbd2a9e3a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:44:04 -0500 Subject: [PATCH 1/7] fix: replace deprecated vim.loop with vim.uv Problem: vim.loop is deprecated since Neovim 0.10 in favour of vim.uv. Five call sites across scraper.lua, setup.lua, utils.lua, and health.lua still referenced the old alias. Solution: replace every vim.loop reference with vim.uv directly. --- lua/cp/health.lua | 2 +- lua/cp/scraper.lua | 2 +- lua/cp/setup.lua | 2 +- lua/cp/utils.lua | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lua/cp/health.lua b/lua/cp/health.lua index d3a3fea..97595d6 100644 --- a/lua/cp/health.lua +++ b/lua/cp/health.lua @@ -13,7 +13,7 @@ local function check() vim.health.error('cp.nvim requires Neovim 0.10.0+') end - local uname = vim.loop.os_uname() + local uname = vim.uv.os_uname() if uname.sysname == 'Windows_NT' then vim.health.error('Windows is not supported') end diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index 73b6177..29cc63e 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -57,7 +57,7 @@ local function run_scraper(platform, subcommand, args, opts) env.CONDA_PREFIX = '' if opts and opts.ndjson then - local uv = vim.loop + local uv = vim.uv local stdout = uv.new_pipe(false) local stderr = uv.new_pipe(false) local buf = '' diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 5130822..0572d55 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -179,7 +179,7 @@ function M.setup_contest(platform, contest_id, problem_id, language) contest_id = contest_id, language = lang, requested_problem_id = problem_id, - token = vim.loop.hrtime(), + token = vim.uv.hrtime(), }) logger.log('Fetching contests problems...', vim.log.levels.INFO, true) diff --git a/lua/cp/utils.lua b/lua/cp/utils.lua index b602940..285ebf8 100644 --- a/lua/cp/utils.lua +++ b/lua/cp/utils.lua @@ -5,7 +5,7 @@ local logger = require('cp.log') local _nix_python = nil local _nix_discovered = false -local uname = vim.loop.os_uname() +local uname = vim.uv.os_uname() local _time_cached = false local _time_path = nil @@ -336,7 +336,7 @@ function M.timeout_capability() end function M.cwd_executables() - local uv = vim.uv or vim.loop + local uv = vim.uv local req = uv.fs_scandir('.') if not req then return {} From d274e0c1170f6bdbe94a34f3084febc201ec6977 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:44:32 -0500 Subject: [PATCH 2/7] fix(cache): replace stale M._cache field with get_raw_cache accessor Problem: M._cache = cache_data captured the initial empty table reference at module load time. After M.load() reassigns cache_data to the decoded JSON, M._cache is permanently stale and returns the wrong table. Solution: remove the field assignment and expose get_raw_cache() which closes over cache_data and always returns the current table. --- lua/cp/cache.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index efdcad7..33342c2 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -346,6 +346,8 @@ function M.get_data_pretty() return vim.inspect(cache_data) end -M._cache = cache_data +function M.get_raw_cache() + return cache_data +end return M From 81f52738406f6c7b29bc911ec165871caaa69490 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:44:34 -0500 Subject: [PATCH 3/7] chore: convert .luarc.json to nested format and add busted library Problem: .luarc.json used the flat dotted-key format which is not the canonical LuaLS schema. The busted library was also missing, so LuaLS could not resolve types in test files. Solution: rewrite .luarc.json using nested objects and add ${3rd}/busted/library to workspace.library. --- .luarc.json | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/.luarc.json b/.luarc.json index 19558f6..e0f7a7c 100644 --- a/.luarc.json +++ b/.luarc.json @@ -1,8 +1,16 @@ { - "runtime.version": "LuaJIT", - "runtime.path": ["lua/?.lua", "lua/?/init.lua"], - "diagnostics.globals": ["vim"], - "workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"], - "workspace.checkThirdParty": false, - "completion.callSnippet": "Replace" + "runtime": { + "version": "LuaJIT", + "path": ["lua/?.lua", "lua/?/init.lua"] + }, + "diagnostics": { + "globals": ["vim"] + }, + "workspace": { + "library": ["$VIMRUNTIME/lua", "${3rd}/luv/library", "${3rd}/busted/library"], + "checkThirdParty": false + }, + "completion": { + "callSnippet": "Replace" + } } From 585cf2a077a813cd071179b365108a248a594a2a Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:54:06 -0500 Subject: [PATCH 4/7] feat(runner): run test cases in parallel Problem: test cases were executed sequentially, each waiting for the previous process to finish before starting the next. On problems with many test cases this meant wall-clock run time scaled linearly. Solution: fan out all test case processes simultaneously. A remaining counter fires on_done once all callbacks have returned. on_each is called per completion as before; callers that pass on_each ignore its arguments so the index semantics change is non-breaking. --- lua/cp/runner/run.lua | 45 ++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 36a560c..4e4a8f6 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -276,26 +276,35 @@ function M.run_all_test_cases(indices, debug, on_each, on_done) end end - local function run_next(pos) - if pos > #to_run then - logger.log( - ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', #to_run), - vim.log.levels.INFO, - true - ) - on_done(panel_state.test_cases) - return - end - - M.run_test_case(to_run[pos], debug, function() - if on_each then - on_each(pos, #to_run) - end - run_next(pos + 1) - end) + if #to_run == 0 then + logger.log( + ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', 0), + vim.log.levels.INFO, + true + ) + on_done(panel_state.test_cases) + return end - run_next(1) + local total = #to_run + local remaining = total + + for _, idx in ipairs(to_run) do + M.run_test_case(idx, debug, function() + if on_each then + on_each(idx, total) + end + remaining = remaining - 1 + if remaining == 0 then + logger.log( + ('Finished %s %d test cases.'):format(debug and 'debugging' or 'running', total), + vim.log.levels.INFO, + true + ) + on_done(panel_state.test_cases) + end + end) + end end ---@return PanelState From ce5648f9cfc1ef467e9126e4ab5d673b539cbdaf Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:54:27 -0500 Subject: [PATCH 5/7] docs: add statusline integration recipes Problem: cp.nvim exposed no documentation showing how to integrate its runtime state into a statusline. Users had to discover the state module API by reading source. Solution: add a STATUSLINE INTEGRATION section to the vimdoc with a state API reference and recipes for vanilla statusline, lualine, and heirline. Also anchors the *cp.State* help tag referenced in prose elsewhere in the doc. --- doc/cp.nvim.txt | 110 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 49cdaf7..27563a1 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -888,6 +888,116 @@ Functions ~ Parameters: ~ {bufnr} (integer) Buffer handle +============================================================================== +STATUSLINE INTEGRATION *cp-statusline* + +cp.nvim exposes its runtime state through a public module that can be queried +from any statusline plugin. Import it with: >lua + local state = require('cp.state') +< +All getters return nil when no problem is active, so guard every value before +use. Calling any getter outside a CP context is safe and has no side effects. + +State API ~ + *cp.State* +The following getters are available for statusline use: + + get_platform() (string?) Platform id. e.g. "codeforces", "atcoder" + get_contest_id() (string?) Contest id. e.g. "1933", "abc324" + get_problem_id() (string?) Problem id. e.g. "A", "B" + get_language() (string?) Language id. e.g. "cpp", "python" + get_base_name() (string?) Derived filename stem. e.g. "1933a" + get_source_file() (string?) Full source filename. e.g. "1933a.cc" + get_active_panel() (string?) Non-nil when the test panel is open. + +Recipe: vanilla statusline ~ + +Set vim.o.statusline from an autocommand so it is recalculated on every +BufEnter: >lua + local function cp_component() + local state = require('cp.state') + local platform = state.get_platform() + if not platform then + return '' + end + local parts = { + platform, + state.get_contest_id(), + state.get_problem_id(), + state.get_language(), + } + local filtered = {} + for _, v in ipairs(parts) do + if v then filtered[#filtered + 1] = v end + end + return '[' .. table.concat(filtered, ' · ') .. ']' + end + + vim.api.nvim_create_autocmd({ 'BufEnter', 'User' }, { + callback = function() + vim.o.statusline = cp_component() .. ' %f %=%l:%c' + end + }) +< + +Recipe: lualine ~ + +Add a custom component to any lualine section. The cond field hides the +component entirely when no problem is active: >lua + local function cp_lualine() + local state = require('cp.state') + local parts = { + state.get_platform(), + state.get_contest_id(), + state.get_problem_id(), + state.get_language(), + } + local filtered = {} + for _, v in ipairs(parts) do + if v then filtered[#filtered + 1] = v end + end + return table.concat(filtered, ' · ') + end + + require('lualine').setup({ + sections = { + lualine_c = { + { + cp_lualine, + cond = function() + return require('cp.state').get_platform() ~= nil + end, + }, + }, + }, + }) +< + +Recipe: heirline ~ + +Build a heirline component using a provider and condition: >lua + local CpComponent = { + condition = function() + return require('cp.state').get_platform() ~= nil + end, + provider = function() + local state = require('cp.state') + local parts = { + state.get_platform(), + state.get_contest_id(), + state.get_problem_id(), + state.get_language(), + } + local filtered = {} + for _, v in ipairs(parts) do + if v then filtered[#filtered + 1] = v end + end + return '[' .. table.concat(filtered, ' · ') .. ']' + end, + } +< +Include CpComponent in your heirline StatusLine spec wherever desired. + ============================================================================== PANEL KEYMAPS *cp-panel-keys* From 2c25ec616afd3c3017e0bb6e46a667ceb8c4778e Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:54:32 -0500 Subject: [PATCH 6/7] feat: add per-language template file support Problem: new solution files were always created empty, requiring users to manually paste boilerplate or rely on editor snippets that fire outside cp.nvim's control. Solution: add an optional template field to the language config. When set to a file path, its contents are written into every newly created solution buffer before the setup_code hook runs. Existing files are never overwritten. --- lua/cp/config.lua | 5 +++++ lua/cp/setup.lua | 53 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 4ed762b..6cf43d9 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -7,6 +7,7 @@ ---@class CpLanguage ---@field extension string ---@field commands CpLangCommands +---@field template? string ---@class CpPlatformOverrides ---@field extension? string @@ -215,6 +216,10 @@ local function validate_language(id, lang) commands = { lang.commands, { 'table' } }, }) + if lang.template ~= nil then + vim.validate({ template = { lang.template, 'string' } }) + end + if not lang.commands.run then error(('[cp.nvim] languages.%s.commands.run is required'):format(id)) end diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 0572d55..86fdee1 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -8,6 +8,25 @@ local logger = require('cp.log') local scraper = require('cp.scraper') local state = require('cp.state') +local function apply_template(bufnr, lang_id, platform) + local config = config_module.get_config() + local eff = config.runtime.effective[platform] + and config.runtime.effective[platform][lang_id] + if not eff or not eff.template then + return + 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 + ) + return + end + local lines = vim.fn.readfile(path) + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) +end + ---Get the language of the current file from cache ---@return string? local function get_current_file_language() @@ -270,14 +289,17 @@ function M.setup_problem(problem_id, language) mods = { silent = true, noautocmd = true, keepalt = true }, }) state.set_solution_win(vim.api.nvim_get_current_win()) - if config.hooks and config.hooks.setup_code and not vim.b[prov.bufnr].cp_setup_done then - local ok = pcall(config.hooks.setup_code, state) - if ok then + if not vim.b[prov.bufnr].cp_setup_done then + apply_template(prov.bufnr, lang, platform) + if config.hooks and config.hooks.setup_code then + local ok = pcall(config.hooks.setup_code, state) + if ok then + vim.b[prov.bufnr].cp_setup_done = true + end + else + helpers.clearcol(prov.bufnr) vim.b[prov.bufnr].cp_setup_done = true end - elseif not vim.b[prov.bufnr].cp_setup_done then - helpers.clearcol(prov.bufnr) - vim.b[prov.bufnr].cp_setup_done = true end cache.set_file_state( vim.fn.fnamemodify(source_file, ':p'), @@ -300,14 +322,21 @@ function M.setup_problem(problem_id, language) local bufnr = vim.api.nvim_get_current_buf() state.set_solution_win(vim.api.nvim_get_current_win()) require('cp.ui.views').ensure_io_view() - if config.hooks and config.hooks.setup_code and not vim.b[bufnr].cp_setup_done then - local ok = pcall(config.hooks.setup_code, state) - if ok then + if not vim.b[bufnr].cp_setup_done then + local is_new = vim.api.nvim_buf_line_count(bufnr) == 1 + and vim.api.nvim_buf_get_lines(bufnr, 0, 1, false)[1] == '' + if is_new then + apply_template(bufnr, lang, platform) + end + if config.hooks and config.hooks.setup_code then + local ok = pcall(config.hooks.setup_code, state) + if ok then + vim.b[bufnr].cp_setup_done = true + end + else + helpers.clearcol(bufnr) vim.b[bufnr].cp_setup_done = true end - elseif not vim.b[bufnr].cp_setup_done then - helpers.clearcol(bufnr) - vim.b[bufnr].cp_setup_done = true end cache.set_file_state( vim.fn.expand('%:p'), From 84d12758c2151ad57e4b7162a260cfcb18296f1f Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Thu, 26 Feb 2026 22:57:18 -0500 Subject: [PATCH 7/7] style(setup): apply stylua formatting --- lua/cp/setup.lua | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 86fdee1..44c993d 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -10,17 +10,13 @@ local state = require('cp.state') local function apply_template(bufnr, lang_id, platform) local config = config_module.get_config() - local eff = config.runtime.effective[platform] - and config.runtime.effective[platform][lang_id] + local eff = config.runtime.effective[platform] and config.runtime.effective[platform][lang_id] if not eff or not eff.template then return 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), vim.log.levels.WARN) return end local lines = vim.fn.readfile(path)