diff --git a/lua/cp/cache.lua b/lua/cp/cache.lua index 58bbaf0..41500e8 100644 --- a/lua/cp/cache.lua +++ b/lua/cp/cache.lua @@ -20,6 +20,7 @@ ---@field test_cases_cached_at? number ---@field timeout_ms? number ---@field memory_mb? number +---@field interactive? boolean ---@class Problem ---@field id string @@ -164,7 +165,16 @@ end ---@param test_cases CachedTestCase[] ---@param timeout_ms? number ---@param memory_mb? number -function M.set_test_cases(platform, contest_id, problem_id, test_cases, timeout_ms, memory_mb) +---@param interactive? boolean +function M.set_test_cases( + platform, + contest_id, + problem_id, + test_cases, + timeout_ms, + memory_mb, + interactive +) vim.validate({ platform = { platform, 'string' }, contest_id = { contest_id, 'string' }, @@ -172,6 +182,7 @@ function M.set_test_cases(platform, contest_id, problem_id, test_cases, timeout_ test_cases = { test_cases, 'table' }, timeout_ms = { timeout_ms, { 'number', 'nil' }, true }, memory_mb = { memory_mb, { 'number', 'nil' }, true }, + interactive = { interactive, { 'boolean', 'nil' }, true }, }) local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id diff --git a/lua/cp/ui/panel.lua b/lua/cp/ui/panel.lua index 8cb1bae..3c99430 100644 --- a/lua/cp/ui/panel.lua +++ b/lua/cp/ui/panel.lua @@ -32,6 +32,44 @@ function M.toggle_interactive() return end + local platform, contest_id = state.get_platform(), state.get_contest_id() + + if not platform then + logger.log( + 'No platform %s configured. Use :CP [...] first.', + vim.log.levels.ERROR + ) + return + end + + if not contest_id then + logger.log( + ('No contest %s configured for platform %s. Use :CP to set up first.'):format( + contest_id, + platform + ), + vim.log.levels.ERROR + ) + return + end + + local problem_id = state.get_problem_id() + if not problem_id then + logger.log(('No problem found for the current problem id %s'):format(problem_id)) + return + end + + local cache = require('cp.cache') + cache.load() + local contest_data = cache.get_contest_data(platform, contest_id) + if contest_data and not contest_data.interactive then + logger.log( + 'This is NOT an interactive problem. Use :CP run instead - aborting.', + vim.log.levels.WARN + ) + return + end + state.saved_interactive_session = vim.fn.tempname() vim.cmd(('mksession! %s'):format(state.saved_interactive_session)) vim.cmd('silent only') @@ -89,9 +127,22 @@ function M.toggle_run_panel(is_debug) return end - if not state.get_platform() then + local platform, contest_id = state.get_platform(), state.get_contest_id() + + if not platform then logger.log( - 'No contest configured. Use :CP to set up first.', + 'No platform %s configured. Use :CP [...] first.', + vim.log.levels.ERROR + ) + return + end + + if not contest_id then + logger.log( + ('No contest %s configured for platform %s. Use :CP to set up first.'):format( + contest_id, + platform + ), vim.log.levels.ERROR ) return @@ -99,11 +150,20 @@ function M.toggle_run_panel(is_debug) local problem_id = state.get_problem_id() if not problem_id then + logger.log(('No problem found for the current problem id %s'):format(problem_id)) return end - local platform = state.get_platform() - local contest_id = state.get_contest_id() + local cache = require('cp.cache') + cache.load() + local contest_data = cache.get_contest_data(platform, contest_id) + if contest_data and contest_data.interactive then + logger.log( + 'This is an interactive problem. Use :CP interact instead - aborting.', + vim.log.levels.WARN + ) + return + end logger.log( ('run panel: platform=%s, contest=%s, problem=%s'):format( diff --git a/scrapers/codeforces.py b/scrapers/codeforces.py index e7e1e4b..5107c5e 100644 --- a/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -199,10 +199,10 @@ def scrape_contest_problems(contest_id: str) -> list[ProblemSummary]: problem_letter: str = href.split("/")[-1].lower() problem_name: str = link.get_text(strip=True) - if problem_letter and problem_name: - problems.append( - ProblemSummary(id=problem_letter, name=problem_name) - ) + if not (problem_letter and problem_name): + continue + + problems.append(ProblemSummary(id=problem_letter, name=problem_name)) seen: set[str] = set() unique_problems: list[ProblemSummary] = [] @@ -283,6 +283,12 @@ class CodeforcesScraper(BaseScraper): soup = BeautifulSoup(response.text, "html.parser") timeout_ms, memory_mb = extract_problem_limits(soup) + problem_statement_div = soup.find("div", class_="problem-statement") + interactive = bool( + problem_statement_div + and "This is an interactive problem" in problem_statement_div.get_text() + ) + if not tests: return self._create_tests_error( f"No tests found for {contest_id} {problem_letter}", problem_id, url @@ -296,6 +302,7 @@ class CodeforcesScraper(BaseScraper): tests=tests, timeout_ms=timeout_ms, memory_mb=memory_mb, + interactive=interactive, ) def _scrape_contest_list_impl(self) -> ContestListResult: diff --git a/scrapers/models.py b/scrapers/models.py index 318404d..d36daf5 100644 --- a/scrapers/models.py +++ b/scrapers/models.py @@ -45,3 +45,4 @@ class TestsResult(ScrapingResult): tests: list[TestCase] timeout_ms: int memory_mb: float + interactive: bool = False diff --git a/uv.lock b/uv.lock index aa9248d..203657d 100644 --- a/uv.lock +++ b/uv.lock @@ -282,11 +282,11 @@ wheels = [ [[package]] name = "pyparsing" -version = "3.2.3" +version = "3.2.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, + { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" }, ] [[package]]