fix caching
This commit is contained in:
parent
e6c09a4897
commit
7eb314b02c
7 changed files with 79 additions and 85 deletions
|
|
@ -1,7 +1,5 @@
|
||||||
# cp.nvim
|
# cp.nvim
|
||||||
|
|
||||||
> ⚠️ **Warning**: as of 27/09/25, CodeForces upgraded their anti-scraping technology and support is thus (temporarily) broken. I am actively researching a way around this.
|
|
||||||
|
|
||||||
**The definitive competitive programming environment for Neovim**
|
**The definitive competitive programming environment for Neovim**
|
||||||
|
|
||||||
Scrape problems, run tests, and debug solutions across multiple platforms with zero configuration.
|
Scrape problems, run tests, and debug solutions across multiple platforms with zero configuration.
|
||||||
|
|
@ -29,7 +27,7 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
1. **Find a contest or problem** on the judge website
|
1. **Find a contest or problem** on the judge website
|
||||||
2. **Set up locally** with `:CP <platform> <contest> [<problem>]`
|
2. **Set up locally** with `:CP <platform> <contest> [--{lang=<lang>,debug}]`
|
||||||
|
|
||||||
```
|
```
|
||||||
:CP codeforces 1848
|
:CP codeforces 1848
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ function M.get_contest_data(platform, contest_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
local contest_data = cache_data[platform][contest_id]
|
local contest_data = cache_data[platform][contest_id]
|
||||||
if not contest_data then
|
if not contest_data or vim.tbl_isempty(contest_data) then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,8 @@ local function parse_command(args)
|
||||||
}
|
}
|
||||||
elseif #filtered_args == 3 then
|
elseif #filtered_args == 3 then
|
||||||
return {
|
return {
|
||||||
type = 'full_setup',
|
type = 'error',
|
||||||
platform = first,
|
message = 'Setup contests with :CP <platform> <contest_id> [--{lang=<lang>,debug}]',
|
||||||
contest = filtered_args[2],
|
|
||||||
problem = filtered_args[3],
|
|
||||||
language = language,
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return { type = 'error', message = 'Too many arguments' }
|
return { type = 'error', message = 'Too many arguments' }
|
||||||
|
|
@ -88,16 +85,6 @@ local function parse_command(args)
|
||||||
if state.get_platform() and state.get_contest_id() then
|
if state.get_platform() and state.get_contest_id() then
|
||||||
local cache = require('cp.cache')
|
local cache = require('cp.cache')
|
||||||
cache.load()
|
cache.load()
|
||||||
local contest_data =
|
|
||||||
cache.get_contest_data(state.get_platform() or '', state.get_contest_id() or '')
|
|
||||||
if contest_data and contest_data.problems then
|
|
||||||
local problem_ids = vim.tbl_map(function(prob)
|
|
||||||
return prob.id
|
|
||||||
end, contest_data.problems)
|
|
||||||
if vim.tbl_contains(problem_ids, first) then
|
|
||||||
return { type = 'problem_switch', problem = first, language = language }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
return {
|
return {
|
||||||
type = 'error',
|
type = 'error',
|
||||||
message = ("invalid subcommand '%s'"):format(first),
|
message = ("invalid subcommand '%s'"):format(first),
|
||||||
|
|
@ -149,24 +136,10 @@ function M.handle_command(opts)
|
||||||
if cmd.type == 'contest_setup' then
|
if cmd.type == 'contest_setup' then
|
||||||
local setup = require('cp.setup')
|
local setup = require('cp.setup')
|
||||||
if setup.set_platform(cmd.platform) then
|
if setup.set_platform(cmd.platform) then
|
||||||
setup.setup_contest(cmd.platform, cmd.contest, nil, cmd.language)
|
setup.setup_contest(cmd.platform, cmd.contest, cmd.language, nil)
|
||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if cmd.type == 'full_setup' then
|
|
||||||
local setup = require('cp.setup')
|
|
||||||
if setup.set_platform(cmd.platform) then
|
|
||||||
setup.setup_contest(cmd.platform, cmd.contest, cmd.problem, cmd.language)
|
|
||||||
end
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
if cmd.type == 'problem_switch' then
|
|
||||||
local setup = require('cp.setup')
|
|
||||||
setup.setup_problem(state.get_contest_id() or '', cmd.problem, cmd.language)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ function M.restore_from_current_file()
|
||||||
local file_state = cache.get_file_state(current_file)
|
local file_state = cache.get_file_state(current_file)
|
||||||
if not file_state then
|
if not file_state then
|
||||||
logger.log(
|
logger.log(
|
||||||
'No cached state found for current file. Use :CP <platform> <contest> <problem> [...] first.',
|
'No cached state found for current file. Use :CP <platform> <contest> [--{lang=<lang>,debug}...] first.',
|
||||||
vim.log.levels.ERROR
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
|
|
@ -37,7 +37,12 @@ function M.restore_from_current_file()
|
||||||
state.set_contest_id(file_state.contest_id)
|
state.set_contest_id(file_state.contest_id)
|
||||||
state.set_problem_id(file_state.problem_id)
|
state.set_problem_id(file_state.problem_id)
|
||||||
|
|
||||||
setup.setup_problem(file_state.contest_id, file_state.problem_id, file_state.language)
|
setup.setup_contest(
|
||||||
|
file_state.platform,
|
||||||
|
file_state.contest_id,
|
||||||
|
file_state.language,
|
||||||
|
file_state.problem_id
|
||||||
|
)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,12 @@ function M.set_platform(platform)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local function scrape_contest_problems(platform, contest_id, problems)
|
local function scrape_contest_problems(problems)
|
||||||
cache.load()
|
cache.load()
|
||||||
local missing_problems = {}
|
local missing_problems = {}
|
||||||
|
|
||||||
|
local platform, contest_id = state.get_platform() or '', state.get_contest_id() or ''
|
||||||
|
|
||||||
for _, prob in ipairs(problems) do
|
for _, prob in ipairs(problems) do
|
||||||
local cached_tests = cache.get_test_cases(platform, contest_id, prob.id)
|
local cached_tests = cache.get_test_cases(platform, contest_id, prob.id)
|
||||||
if not cached_tests then
|
if not cached_tests then
|
||||||
|
|
@ -40,7 +42,7 @@ local function scrape_contest_problems(platform, contest_id, problems)
|
||||||
end
|
end
|
||||||
|
|
||||||
if vim.tbl_isempty(missing_problems) then
|
if vim.tbl_isempty(missing_problems) then
|
||||||
logger.log(('All problems already cached for %s contest %s'):format(platform, contest_id))
|
logger.log(('All problems already cached for %s contest %s.'):format(platform, contest_id))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -67,50 +69,60 @@ local function scrape_contest_problems(platform, contest_id, problems)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.setup_contest(platform, contest_id, problem_id, language)
|
function M.setup_contest(platform, contest_id, language, problem_id)
|
||||||
if not state.get_platform() then
|
if not platform then
|
||||||
logger.log('No platform configured. Use :CP <platform> <contest> [...] first.')
|
logger.log('No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug} first.')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local config = config_module.get_config()
|
local config = config_module.get_config()
|
||||||
|
|
||||||
if not vim.tbl_contains(config.scrapers, platform) then
|
if not vim.tbl_contains(config.scrapers, platform) then
|
||||||
logger.log(('Scraping disabled for %s - aborting.'):format(platform), vim.log.levels.WARN)
|
logger.log(('Scraping disabled for %s.'):format(platform), vim.log.levels.WARN)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
state.set_contest_id(contest_id)
|
state.set_contest_id(contest_id)
|
||||||
-- TODO: should check cache here, & other uses of gt_contest_data validate them
|
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||||
logger.log('Fetching contests problems...', vim.log.levels.INFO, true)
|
|
||||||
|
|
||||||
|
if
|
||||||
|
not contest_data
|
||||||
|
or not contest_data.problems
|
||||||
|
or (problem_id and not cache.get_test_cases(platform, contest_id, problem_id))
|
||||||
|
then
|
||||||
|
logger.log('Fetching contests problems...', vim.log.levels.INFO, true)
|
||||||
scraper.scrape_contest_metadata(platform, contest_id, function(result)
|
scraper.scrape_contest_metadata(platform, contest_id, function(result)
|
||||||
local problems = result.problems
|
local problems = result.problems
|
||||||
|
|
||||||
cache.set_contest_data(platform, contest_id, problems)
|
cache.set_contest_data(platform, contest_id, problems)
|
||||||
|
|
||||||
logger.log(('Found %d problems for %s contest %s'):format(#problems, platform, contest_id))
|
logger.log(('Found %d problems for %s contest %s.'):format(#problems, platform, contest_id))
|
||||||
|
|
||||||
local target_problem = problem_id or problems[1].id
|
M.setup_problem(problem_id or problems[1].id, language)
|
||||||
|
|
||||||
M.setup_problem(contest_id, target_problem, language)
|
scrape_contest_problems(problems)
|
||||||
|
|
||||||
scrape_contest_problems(platform, contest_id, problems)
|
|
||||||
end)
|
end)
|
||||||
|
else
|
||||||
|
M.setup_problem(problem_id, language)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.setup_problem(contest_id, problem_id, language)
|
---@param problem_id string
|
||||||
if not state.get_platform() then
|
---@param language? string
|
||||||
logger.log('No platform set. run :CP <platform> <contest> first', vim.log.levels.ERROR)
|
function M.setup_problem(problem_id, language)
|
||||||
|
local platform = state.get_platform()
|
||||||
|
if not platform then
|
||||||
|
logger.log(
|
||||||
|
'No platform set. run :CP <platform> <contest> [--{lang=<lang>,debug}]',
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local config = config_module.get_config()
|
|
||||||
local platform = state.get_platform() or ''
|
|
||||||
|
|
||||||
state.set_contest_id(contest_id)
|
|
||||||
state.set_problem_id(problem_id)
|
state.set_problem_id(problem_id)
|
||||||
|
|
||||||
|
local config = config_module.get_config()
|
||||||
|
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
vim.cmd.only({ mods = { silent = true } })
|
vim.cmd.only({ mods = { silent = true } })
|
||||||
|
|
||||||
|
|
@ -149,18 +161,30 @@ function M.setup_problem(contest_id, problem_id, language)
|
||||||
config.hooks.setup_code(state)
|
config.hooks.setup_code(state)
|
||||||
end
|
end
|
||||||
|
|
||||||
cache.set_file_state(vim.fn.expand('%:p'), platform, contest_id, problem_id, language)
|
cache.set_file_state(
|
||||||
|
vim.fn.expand('%:p'),
|
||||||
|
platform,
|
||||||
|
state.get_contest_id() or '',
|
||||||
|
state.get_problem_id(),
|
||||||
|
language
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.navigate_problem(direction, language)
|
function M.navigate_problem(direction, language)
|
||||||
|
if direction == 0 then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
direction = direction > 0 and 1 or -1
|
||||||
|
|
||||||
local platform = state.get_platform()
|
local platform = state.get_platform()
|
||||||
local contest_id = state.get_contest_id()
|
local contest_id = state.get_contest_id()
|
||||||
local current_problem_id = state.get_problem_id()
|
local current_problem_id = state.get_problem_id()
|
||||||
|
|
||||||
if not platform or not contest_id or not current_problem_id then
|
if not platform or not contest_id or not current_problem_id then
|
||||||
logger.log(
|
logger.log(
|
||||||
'No platform configured. Use :CP <platform> <contest> [...] first.',
|
'No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug}] first.',
|
||||||
vim.log.levels.ERROR
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -169,7 +193,13 @@ function M.navigate_problem(direction, language)
|
||||||
cache.load()
|
cache.load()
|
||||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||||
if not contest_data or not contest_data.problems then
|
if not contest_data or not contest_data.problems then
|
||||||
logger.log('No contest data available', vim.log.levels.ERROR)
|
logger.log(
|
||||||
|
('No data available for %s contest %s.'):format(
|
||||||
|
constants.PLATFORM_DISPLAY_NAMES[platform],
|
||||||
|
contest_id
|
||||||
|
),
|
||||||
|
vim.log.levels.ERROR
|
||||||
|
)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -187,12 +217,7 @@ function M.navigate_problem(direction, language)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local cp = require('cp')
|
M.setup_contest(platform, contest_id, language, problems[new_index].id)
|
||||||
local args = { platform, contest_id, problems[new_index].id }
|
|
||||||
if language then
|
|
||||||
vim.list_extend(args, { '--lang', language })
|
|
||||||
end
|
|
||||||
cp.handle_command({ fargs = args })
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ local logger = require('cp.log')
|
||||||
function M.setup(config)
|
function M.setup(config)
|
||||||
local ok, ls = pcall(require, 'luasnip')
|
local ok, ls = pcall(require, 'luasnip')
|
||||||
if not ok then
|
if not ok then
|
||||||
logger.log('LuaSnip not available - snippets disabled', vim.log.levels.INFO)
|
logger.log('LuaSnip not available - snippets are disabled.', vim.log.levels.INFO, true)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,12 @@ function M.toggle_interactive()
|
||||||
state.saved_interactive_session = nil
|
state.saved_interactive_session = nil
|
||||||
end
|
end
|
||||||
state.set_active_panel(nil)
|
state.set_active_panel(nil)
|
||||||
logger.log('interactive closed')
|
logger.log('Interactive panel closed.')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
if state.get_active_panel() then
|
if state.get_active_panel() then
|
||||||
logger.log('another panel is already active', vim.log.levels.ERROR)
|
logger.log('Another panel is already active.', vim.log.levels.WARN)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ function M.toggle_interactive()
|
||||||
|
|
||||||
if not platform then
|
if not platform then
|
||||||
logger.log(
|
logger.log(
|
||||||
'No platform configured. Use :CP <platform> <contest> [...] first.',
|
'No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug}] first.',
|
||||||
vim.log.levels.ERROR
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -44,7 +44,7 @@ function M.toggle_interactive()
|
||||||
|
|
||||||
if not contest_id then
|
if not contest_id then
|
||||||
logger.log(
|
logger.log(
|
||||||
('No contest %s configured for platform %s. Use :CP <platform> <contest> <problem> to set up first.'):format(
|
('No contest %s configured for platform %s. Use :CP <platform> <contest> [--{lang=<lang>,debug}] to set up first.'):format(
|
||||||
contest_id,
|
contest_id,
|
||||||
platform
|
platform
|
||||||
),
|
),
|
||||||
|
|
@ -62,12 +62,8 @@ function M.toggle_interactive()
|
||||||
local cache = require('cp.cache')
|
local cache = require('cp.cache')
|
||||||
cache.load()
|
cache.load()
|
||||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||||
vim.print('checking cache - contes_data (DLETE ME): ', contest_data)
|
|
||||||
if contest_data and not contest_data.interactive then
|
if contest_data and not contest_data.interactive then
|
||||||
logger.log(
|
logger.log('This is NOT an interactive problem. Use :CP run instead.', vim.log.levels.WARN)
|
||||||
'This is NOT an interactive problem. Use :CP run instead - aborting.',
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -140,7 +136,7 @@ function M.toggle_run_panel(is_debug)
|
||||||
|
|
||||||
if not contest_id then
|
if not contest_id then
|
||||||
logger.log(
|
logger.log(
|
||||||
('No contest %s configured for platform %s. Use :CP <platform> <contest> <problem> to set up first.'):format(
|
('No contest %s configured for platform %s. Use :CP <platform> <contest> [--{lang=<lang>,debug}] to set up first.'):format(
|
||||||
contest_id,
|
contest_id,
|
||||||
platform
|
platform
|
||||||
),
|
),
|
||||||
|
|
@ -159,15 +155,12 @@ function M.toggle_run_panel(is_debug)
|
||||||
cache.load()
|
cache.load()
|
||||||
local contest_data = cache.get_contest_data(platform, contest_id)
|
local contest_data = cache.get_contest_data(platform, contest_id)
|
||||||
if contest_data and contest_data.interactive then
|
if contest_data and contest_data.interactive then
|
||||||
logger.log(
|
logger.log('This is an interactive problem. Use :CP interact instead.', vim.log.levels.WARN)
|
||||||
'This is an interactive problem. Use :CP interact instead - aborting.',
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
('run panel: platform=%s, contest=%s, problem=%s'):format(
|
('Run panel: platform=%s, contest=%s, problem=%s'):format(
|
||||||
tostring(platform),
|
tostring(platform),
|
||||||
tostring(contest_id),
|
tostring(contest_id),
|
||||||
tostring(problem_id)
|
tostring(problem_id)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue