From 1945999099ae795bd4f1a47553d43efbdea31cbe Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 5 Oct 2025 13:16:14 -0400 Subject: [PATCH 1/5] update docs --- README.md | 1 + doc/cp.nvim.txt | 103 ++++++++++++++++-------------------------------- 2 files changed, 36 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 58efb20..9380c63 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**. ``` :CP next :CP prev + :CP e1 ``` 5. **Submit** on the original website diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index e3afa98..5fb4cf2 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -24,12 +24,6 @@ COMMANDS *cp-commands* :CP *:CP* cp.nvim uses a single :CP command with intelligent argument parsing: - State Restoration ~ - :CP Restore state from current file. - Automatically detects platform, contest, problem, - and language from cached state. Use this after - switching files to restore your CP environment. - Setup Commands ~ :CP {platform} {contest_id} Full setup: set platform and load contest metadata. @@ -39,13 +33,10 @@ COMMANDS *cp-commands* < :CP {platform} {contest_id} Contest setup: set platform, load contest metadata, - and scrape ALL problems in the contest. This creates - source files for every problem and caches all test - cases for efficient bulk setup. Opens the first - problem after completion. + and scrape all test cases in the contest. + Opens the first problem after completion. Example: > :CP atcoder abc324 - :CP codeforces 1951 < Action Commands ~ :CP run Toggle run panel for individual test cases. @@ -70,10 +61,15 @@ COMMANDS *cp-commands* :CP {problem_id} Jump to problem {problem_id} in a contest. Requires that a contest has already been set up. + State Restoration ~ + :CP Restore state from current file. + Automatically detects platform, contest, problem, + and language from cached state. Use this after + switching files to restore your CP environment. + Cache Commands ~ :CP cache clear [contest] - Clear the cache data (contest list, problem - data, file states) for the specified contest, + Clear the cache data for the specified contest, or all contests if none specified. :CP cache read @@ -86,8 +82,6 @@ Template Variables ~ • {source} Source file path (e.g. "abc324a.cpp") • {binary} Output binary path (e.g. "build/abc324a.run") - • {contest} Contest identifier (e.g. "abc324", "1933") - • {problem} Problem identifier (e.g. "a", "b") Example template: > build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' } @@ -98,8 +92,8 @@ Template Variables ~ ============================================================================== CONFIGURATION *cp-config* -Here's an example configuration with lazy.nvim: >lua - +Here's an example configuration with lazy.nvim: +>lua { 'barrett-ruth/cp.nvim', cmd = 'CP', @@ -109,7 +103,8 @@ Here's an example configuration with lazy.nvim: >lua cpp = { extension = 'cc', commands = { - build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' }, + build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}', + '-fdiagnostics-color=always' }, run = { '{binary}' }, debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined', '{source}', '-o', '{binary}' }, @@ -164,21 +159,17 @@ By default, C++ (g++ with ISO C++17) and Python are preconfigured under the default; per-platform overrides can tweak `extension` or `commands`. For example, to run CodeForces contests with Python by default: - >lua { platforms = { codeforces = { - enabled_languages = { 'cpp', 'python' }, default_language = 'python', }, }, } < - Any language is supported provided the proper configuration. For example, to run CSES problems with Rust using the single schema: - >lua { languages = { @@ -198,7 +189,6 @@ run CSES problems with Rust using the single schema: }, } < - *cp.Config* Fields: ~ {languages} (table) Global language registry. @@ -214,9 +204,6 @@ run CSES problems with Rust using the single schema: (default: concatenates contest_id and problem_id, lowercased) {ui} (|CpUI|) UI settings: run panel, diff backend, picker. - *cp.PlatformConfig* - Replaced by |CpPlatform|. Platforms no longer inline language tables. - *CpPlatform* Fields: ~ {enabled_languages} (string[]) Language ids enabled on this platform. @@ -279,7 +266,8 @@ run CSES problems with Rust using the single schema: Hook functions receive the cp.nvim state object (cp.State). See the state module documentation (lua/cp/state.lua) for available methods and fields. - Example usage in hook: >lua + Example usage in hook: +>lua hooks = { setup_code = function(state) print("Setting up " .. state.get_base_name()) @@ -300,24 +288,25 @@ PLATFORM-SPECIFIC USAGE *cp-platforms* AtCoder ~ *cp-atcoder* -URL format: https://atcoder.jp/contests/abc123/tasks/abc123_a +URL format: +https://atcoder.jp/contests/{contest_id}/tasks/{contest_id}_{problem_id} Usage examples: > - :CP atcoder abc324 " Contest setup: load contest metadata only + :CP atcoder abc324 " Set up atcoder.jp/contests/abc324 Codeforces ~ *cp-codeforces* -URL format: https://codeforces.com/contest/1234/problem/A +URL format: https://codeforces.com/contest/{contest_id}/problem/{problem_id} Usage examples: > - :CP codeforces 1934 " Contest setup: load contest metadata only + :CP codeforces 1934 " Set up codeforces.com/contest/1934 CSES ~ *cp-cses* -URL format: https://cses.fi/problemset/task/1068 +URL format: https://cses.fi/problemset/task/{problem_id} Usage examples: > - :CP cses dynamic_programming " Set up ALL problems from DP category + :CP cses dynamic_programming " Set up all problems in dp category ============================================================================== @@ -329,30 +318,26 @@ Example: Setting up and solving AtCoder contest ABC324 2. Set up entire contest (bulk setup): > :CP atcoder abc324 -< This scrapes ALL problems (A, B, C, D, ...), creates source files - for each, downloads all test cases, and opens problem A. +< This scrapes all test case data, downloads all test cases, + and opens the first problem. -3. Alternative: Set up single problem: > - :CP atcoder abc324 a -< This creates only a.cc and scrapes its test cases - -4. Code your solution, then test: > +3. Code your solution, then test: > :CP run < Navigate with j/k, run specific tests with Exit test panel with q or :CP run when done -5. Move to next problem: > +4. Move to next problem: > :CP next -< This automatically sets up problem B +< This automatically sets up the next problem (likely problem B) -6. Continue solving problems with :CP next/:CP prev navigation +5. Continue solving problems with :CP next/:CP prev navigation -7. Switch to another file (e.g. previous contest): > +6. Switch to another file (e.g. previous contest): > :e ~/contests/abc323/a.cpp :CP < Automatically restores abc323 contest context -8. Submit solutions on AtCoder website +7. Submit solutions on AtCoder website ============================================================================== PICKER INTEGRATION *cp-picker* @@ -368,7 +353,7 @@ platform and contest selection using telescope.nvim or fzf-lua. Requires corresponding plugin (telescope.nvim or fzf-lua) to be installed. PICKER KEYMAPS *cp-picker-keys* - Force refresh contest list, bypassing cache. + Force refresh/update contest list. Useful when contest lists are outdated or incomplete ============================================================================== @@ -424,7 +409,7 @@ erroneous config. Most tools (GCC, Python, Clang, Rustc) color stdout based on whether stdout is connected to a terminal. One can usually get aorund this by leveraging flags to force colored output. For example, to force colors with GCC, alter your config as follows: - +>lua { commands = { build = { @@ -434,7 +419,7 @@ alter your config as follows: } } } - +< ============================================================================== HIGHLIGHT GROUPS *cp-highlights* @@ -468,34 +453,16 @@ TERMINAL COLOR INTEGRATION *cp-terminal-colors* ANSI colors automatically use the terminal's color palette through Neovim's vim.g.terminal_color_* variables. -If your colorscheme doesn't set terminal colors, set them like so: >vim - let g:terminal_color_1 = '#ff6b6b' - ... - ============================================================================== HIGHLIGHT CUSTOMIZATION *cp-highlight-custom* -You can customize any highlight group by linking to existing groups or -defining custom colors: >lua - - -- Customize the color of "TLE" text in run panel: - vim.api.nvim_set_hl(0, 'CpTestTLE', { fg = '#ffa500', bold = true }) - - -- ... or the ANSI colors used to display stderr - vim.api.nvim_set_hl(0, 'CpAnsiRed', { - fg = vim.g.terminal_color_1 or '#ef4444' - }) -< - -Place customizations in your init.lua or after the colorscheme loads to -prevent them from being overridden: >lua +Customize highlight groups after your colorscheme loads: +>lua vim.api.nvim_create_autocmd('ColorScheme', { callback = function() - -- Your cp.nvim highlight customizations here vim.api.nvim_set_hl(0, 'CpTestAC', { link = 'String' }) end }) -< ============================================================================== RUN PANEL KEYMAPS *cp-test-keys* From ee88450b3b1f00cee553a4efd4ecc1d013958307 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 5 Oct 2025 13:40:56 -0400 Subject: [PATCH 2/5] feat(scrapers): make scrapers softer --- scrapers/atcoder.py | 73 ++++++++++++++++-------------------------- scrapers/codeforces.py | 21 +++--------- 2 files changed, 31 insertions(+), 63 deletions(-) diff --git a/scrapers/atcoder.py b/scrapers/atcoder.py index 0dc9dce..c5d116f 100644 --- a/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -169,7 +169,7 @@ def _parse_tasks_list(html: str) -> list[dict[str, str]]: return rows -def _extract_limits(html: str) -> tuple[int, float]: +def _extract_problem_info(html: str) -> tuple[int, float, bool]: soup = BeautifulSoup(html, "html.parser") txt = soup.get_text(" ", strip=True) timeout_ms = 0 @@ -180,7 +180,10 @@ def _extract_limits(html: str) -> tuple[int, float]: ms = re.search(r"Memory\s*Limit:\s*(\d+)\s*MiB", txt, flags=re.I) if ms: memory_mb = float(ms.group(1)) * MIB_TO_MB - return timeout_ms, memory_mb + div = soup.select_one("#problem-statement") + txt = div.get_text(" ", strip=True) if div else soup.get_text(" ", strip=True) + interactive = "This is an interactive" in txt + return timeout_ms, memory_mb, interactive def _extract_samples(html: str) -> list[TestCase]: @@ -213,13 +216,16 @@ def _scrape_tasks_sync(contest_id: str) -> list[dict[str, str]]: def _scrape_problem_page_sync(contest_id: str, slug: str) -> dict[str, Any]: html = _fetch(f"{BASE_URL}/contests/{contest_id}/tasks/{slug}") - tests = _extract_samples(html) - timeout_ms, memory_mb = _extract_limits(html) + try: + tests = _extract_samples(html) + except Exception: + tests = [] + timeout_ms, memory_mb, interactive = _extract_problem_info(html) return { "tests": tests, "timeout_ms": timeout_ms, "memory_mb": memory_mb, - "interactive": False, + "interactive": interactive, } @@ -309,47 +315,22 @@ class AtcoderScraper(BaseScraper): slug = row.get("slug") or "" if not letter or not slug: return - try: - data = await asyncio.to_thread( - _scrape_problem_page_sync, category_id, slug - ) - tests: list[TestCase] = data["tests"] - if not tests: - print( - json.dumps( - { - "problem_id": letter, - "error": f"{self.platform_name}: no tests found", - } - ), - flush=True, - ) - return - print( - json.dumps( - { - "problem_id": letter, - "tests": [ - {"input": t.input, "expected": t.expected} - for t in tests - ], - "timeout_ms": data["timeout_ms"], - "memory_mb": data["memory_mb"], - "interactive": bool(data["interactive"]), - } - ), - flush=True, - ) - except Exception as e: - print( - json.dumps( - { - "problem_id": letter, - "error": f"{self.platform_name}: {str(e)}", - } - ), - flush=True, - ) + data = await asyncio.to_thread(_scrape_problem_page_sync, category_id, slug) + tests: list[TestCase] = data.get("tests", []) + print( + json.dumps( + { + "problem_id": letter, + "tests": [ + {"input": t.input, "expected": t.expected} for t in tests + ], + "timeout_ms": data.get("timeout_ms", 0), + "memory_mb": data.get("memory_mb", 0), + "interactive": bool(data.get("interactive")), + } + ), + flush=True, + ) await asyncio.gather(*(emit(r) for r in rows)) diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index 47c08c9..5d5421d 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -244,20 +244,7 @@ class CodeforcesScraper(BaseScraper): for b in blocks: pid = b["letter"].lower() - tests: list[TestCase] = b["tests"] - - if not tests: - print( - json.dumps( - { - "problem_id": pid, - "error": f"{self.platform_name}: no tests found", - } - ), - flush=True, - ) - continue - + tests: list[TestCase] = b.get("tests", []) print( json.dumps( { @@ -265,9 +252,9 @@ class CodeforcesScraper(BaseScraper): "tests": [ {"input": t.input, "expected": t.expected} for t in tests ], - "timeout_ms": b["timeout_ms"], - "memory_mb": b["memory_mb"], - "interactive": bool(b["interactive"]), + "timeout_ms": b.get("timeout_ms", 0), + "memory_mb": b.get("memory_mb", 0), + "interactive": bool(b.get("interactive")), } ), flush=True, From 25fde269435e2d8a94e83f11ed93f6cfba7a6112 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 5 Oct 2025 13:42:06 -0400 Subject: [PATCH 3/5] feat(scrapers): cses soft too --- scrapers/cses.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/scrapers/cses.py b/scrapers/cses.py index 73c5964..0ef9778 100644 --- a/scrapers/cses.py +++ b/scrapers/cses.py @@ -221,23 +221,18 @@ class CSESScraper(BaseScraper): html = await fetch_text(client, task_path(pid)) tests = parse_tests(html) timeout_ms, memory_mb = parse_limits(html) - if not tests: - return { - "problem_id": pid, - "error": f"{self.platform_name}: no tests found", - } - return { - "problem_id": pid, - "tests": [ - {"input": t.input, "expected": t.expected} - for t in tests - ], - "timeout_ms": timeout_ms, - "memory_mb": memory_mb, - "interactive": False, - } - except Exception as e: - return {"problem_id": pid, "error": str(e)} + except Exception: + tests = [] + timeout_ms, memory_mb = 0, 0 + return { + "problem_id": pid, + "tests": [ + {"input": t.input, "expected": t.expected} for t in tests + ], + "timeout_ms": timeout_ms, + "memory_mb": memory_mb, + "interactive": False, + } tasks = [run_one(p.id) for p in problems] for coro in asyncio.as_completed(tasks): From fd550bc654f1ef038a0cae140cd050300332a513 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 5 Oct 2025 13:45:26 -0400 Subject: [PATCH 4/5] feat(setup): warn no tests found --- lua/cp/setup.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 4127c93..4986248 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -57,6 +57,12 @@ function M.setup_contest(platform, contest_id, problem_id, language) logger.log(('Fetching test cases...'):format(cached_len, #problems)) scraper.scrape_all_tests(platform, contest_id, function(ev) local cached_tests = {} + if vim.tbl_isempty(ev.tests) then + 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 } end From cedcd82367bb588d589521004779641924dc48ba Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Sun, 5 Oct 2025 13:50:14 -0400 Subject: [PATCH 5/5] fix: write interaction into cache --- lua/cp/cache.lua | 11 ++++++----- lua/cp/setup.lua | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 06c4c0d..9e96caa 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -160,9 +160,9 @@ end ---@param contest_id string ---@param problem_id string ---@param test_cases TestCase[] ----@param timeout_ms? number ----@param memory_mb? number ----@param interactive? boolean +---@param timeout_ms number +---@param memory_mb number +---@param interactive boolean function M.set_test_cases( platform, contest_id, @@ -185,8 +185,9 @@ function M.set_test_cases( local index = cache_data[platform][contest_id].index_map[problem_id] cache_data[platform][contest_id].problems[index].test_cases = test_cases - cache_data[platform][contest_id].problems[index].timeout_ms = timeout_ms or 0 - cache_data[platform][contest_id].problems[index].memory_mb = memory_mb or 0 + cache_data[platform][contest_id].problems[index].timeout_ms = timeout_ms + cache_data[platform][contest_id].problems[index].memory_mb = memory_mb + cache_data[platform][contest_id].problems[index].interactive = interactive M.save() end diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 4986248..2ed1747 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -72,7 +72,8 @@ function M.setup_contest(platform, contest_id, problem_id, language) ev.problem_id, cached_tests, ev.timeout_ms or 0, - ev.memory_mb or 0 + ev.memory_mb or 0, + ev.interactive ) logger.log('Test cases loaded.') end)