Merge pull request #46 from barrett-ruth/ci/test

Random Plugin Modernizations
This commit is contained in:
Barrett Ruth 2025-09-19 02:22:22 +02:00 committed by GitHub
commit 986e03c974
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 1836 additions and 1662 deletions

13
.busted Normal file
View file

@ -0,0 +1,13 @@
return {
_all = {
coverage = false,
lpath = 'lua/?.lua;lua/?/init.lua',
lua = 'nlua',
},
default = {
verbose = true,
},
tests = {
verbose = true,
},
}

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = true
[*]
insert_final_newline = true
charset = utf-8
[*.lua]
indent_style = space
indent_size = 2

17
.github/workflows/luarocks.yml vendored Normal file
View file

@ -0,0 +1,17 @@
name: Push to Luarocks
on:
push:
tags:
- "*"
workflow_dispatch:
jobs:
luarocks-upload:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: LuaRocks Upload
uses: nvim-neorocks/luarocks-tag-release@v7
env:
LUAROCKS_API_KEY: ${{ secrets.LUAROCKS_API_KEY }}

21
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: Run tests
on:
pull_request: ~
push:
branches:
- main
jobs:
build:
name: Run tests
runs-on: ubuntu-latest
strategy:
matrix:
neovim_version: ['nightly', 'stable']
steps:
- uses: actions/checkout@v4
- name: Run tests
uses: nvim-neorocks/nvim-busted-action@v1
with:
nvim_version: ${{ matrix.neovim_version }}

3
.gitignore vendored
View file

@ -1,4 +1,7 @@
.venv/ .venv/
doc/tags doc/tags
*.log
build
debug
venv/ venv/
CLAUDE.md CLAUDE.md

View file

@ -1,5 +1,5 @@
{ {
"runtime.version": "LuaJIT", "runtime.version": "Lua 5.1",
"runtime.path": [ "runtime.path": [
"lua/?.lua", "lua/?.lua",
"lua/?/init.lua" "lua/?/init.lua"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 Raphael
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -1,6 +1,6 @@
vim.opt_local.number = false vim.opt_local.number = false
vim.opt_local.relativenumber = false vim.opt_local.relativenumber = false
vim.opt_local.statuscolumn = "" vim.opt_local.statuscolumn = ''
vim.opt_local.signcolumn = "no" vim.opt_local.signcolumn = 'no'
vim.opt_local.wrap = true vim.opt_local.wrap = true
vim.opt_local.linebreak = true vim.opt_local.linebreak = true

View file

@ -1,6 +1,6 @@
vim.opt_local.number = false vim.opt_local.number = false
vim.opt_local.relativenumber = false vim.opt_local.relativenumber = false
vim.opt_local.statuscolumn = "" vim.opt_local.statuscolumn = ''
vim.opt_local.signcolumn = "no" vim.opt_local.signcolumn = 'no'
vim.opt_local.wrap = true vim.opt_local.wrap = true
vim.opt_local.linebreak = true vim.opt_local.linebreak = true

View file

@ -1,7 +1,7 @@
vim.opt_local.number = false vim.opt_local.number = false
vim.opt_local.relativenumber = false vim.opt_local.relativenumber = false
vim.opt_local.statuscolumn = "" vim.opt_local.statuscolumn = ''
vim.opt_local.signcolumn = "no" vim.opt_local.signcolumn = 'no'
vim.opt_local.wrap = true vim.opt_local.wrap = true
vim.opt_local.linebreak = true vim.opt_local.linebreak = true
vim.opt_local.modifiable = true vim.opt_local.modifiable = true

View file

@ -1,7 +1,7 @@
vim.opt_local.number = false vim.opt_local.number = false
vim.opt_local.relativenumber = false vim.opt_local.relativenumber = false
vim.opt_local.statuscolumn = "" vim.opt_local.statuscolumn = ''
vim.opt_local.signcolumn = "no" vim.opt_local.signcolumn = 'no'
vim.opt_local.wrap = true vim.opt_local.wrap = true
vim.opt_local.linebreak = true vim.opt_local.linebreak = true
vim.opt_local.foldcolumn = "0" vim.opt_local.foldcolumn = '0'

12
cp.nvim-scm-1.rockspec Normal file
View file

@ -0,0 +1,12 @@
rockspec_format = '3.0'
package = 'cp.nvim'
version = 'scm-1'
source = { url = 'git://github.com/barrett-ruth/cp.nvim' }
build = { type = 'builtin' }
test_dependencies = {
'lua >= 5.1',
'nlua',
'busted >= 2.1.1',
}

View file

@ -1,6 +1,6 @@
vim.filetype.add({ vim.filetype.add({
extension = { extension = {
cpin = "cpin", cpin = 'cpin',
cpout = "cpout", cpout = 'cpout',
}, },
}) })

View file

@ -20,17 +20,17 @@
local M = {} local M = {}
local cache_file = vim.fn.stdpath("data") .. "/cp-nvim.json" local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json'
local cache_data = {} local cache_data = {}
---@param platform string ---@param platform string
---@return number? ---@return number?
local function get_expiry_date(platform) local function get_expiry_date(platform)
vim.validate({ vim.validate({
platform = { platform, "string" }, platform = { platform, 'string' },
}) })
if platform == "cses" then if platform == 'cses' then
return os.time() + (30 * 24 * 60 * 60) return os.time() + (30 * 24 * 60 * 60)
end end
return nil return nil
@ -41,11 +41,11 @@ end
---@return boolean ---@return boolean
local function is_cache_valid(contest_data, platform) local function is_cache_valid(contest_data, platform)
vim.validate({ vim.validate({
contest_data = { contest_data, "table" }, contest_data = { contest_data, 'table' },
platform = { platform, "string" }, platform = { platform, 'string' },
}) })
if platform ~= "cses" then if platform ~= 'cses' then
return true return true
end end
@ -69,7 +69,7 @@ function M.load()
return return
end end
local ok, decoded = pcall(vim.json.decode, table.concat(content, "\n")) local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n'))
if ok then if ok then
cache_data = decoded cache_data = decoded
else else
@ -78,9 +78,9 @@ function M.load()
end end
function M.save() function M.save()
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ":h"), "p") vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
local encoded = vim.json.encode(cache_data) local encoded = vim.json.encode(cache_data)
vim.fn.writefile(vim.split(encoded, "\n"), cache_file) vim.fn.writefile(vim.split(encoded, '\n'), cache_file)
end end
---@param platform string ---@param platform string
@ -88,8 +88,8 @@ end
---@return ContestData? ---@return ContestData?
function M.get_contest_data(platform, contest_id) function M.get_contest_data(platform, contest_id)
vim.validate({ vim.validate({
platform = { platform, "string" }, platform = { platform, 'string' },
contest_id = { contest_id, "string" }, contest_id = { contest_id, 'string' },
}) })
if not cache_data[platform] then if not cache_data[platform] then
@ -113,9 +113,9 @@ end
---@param problems Problem[] ---@param problems Problem[]
function M.set_contest_data(platform, contest_id, problems) function M.set_contest_data(platform, contest_id, problems)
vim.validate({ vim.validate({
platform = { platform, "string" }, platform = { platform, 'string' },
contest_id = { contest_id, "string" }, contest_id = { contest_id, 'string' },
problems = { problems, "table" }, problems = { problems, 'table' },
}) })
if not cache_data[platform] then if not cache_data[platform] then
@ -124,7 +124,7 @@ function M.set_contest_data(platform, contest_id, problems)
cache_data[platform][contest_id] = { cache_data[platform][contest_id] = {
problems = problems, problems = problems,
scraped_at = os.date("%Y-%m-%d"), scraped_at = os.date('%Y-%m-%d'),
expires_at = get_expiry_date(platform), expires_at = get_expiry_date(platform),
} }
@ -135,8 +135,8 @@ end
---@param contest_id string ---@param contest_id string
function M.clear_contest_data(platform, contest_id) function M.clear_contest_data(platform, contest_id)
vim.validate({ vim.validate({
platform = { platform, "string" }, platform = { platform, 'string' },
contest_id = { contest_id, "string" }, contest_id = { contest_id, 'string' },
}) })
if cache_data[platform] and cache_data[platform][contest_id] then if cache_data[platform] and cache_data[platform][contest_id] then
@ -151,12 +151,12 @@ end
---@return CachedTestCase[]? ---@return CachedTestCase[]?
function M.get_test_cases(platform, contest_id, problem_id) function M.get_test_cases(platform, contest_id, problem_id)
vim.validate({ vim.validate({
platform = { platform, "string" }, platform = { platform, 'string' },
contest_id = { contest_id, "string" }, contest_id = { contest_id, 'string' },
problem_id = { problem_id, { "string", "nil" }, true }, problem_id = { problem_id, { 'string', 'nil' }, true },
}) })
local problem_key = problem_id and (contest_id .. "_" .. problem_id) or contest_id local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id
if not cache_data[platform] or not cache_data[platform][problem_key] then if not cache_data[platform] or not cache_data[platform][problem_key] then
return nil return nil
end end
@ -169,13 +169,13 @@ end
---@param test_cases CachedTestCase[] ---@param test_cases CachedTestCase[]
function M.set_test_cases(platform, contest_id, problem_id, test_cases) function M.set_test_cases(platform, contest_id, problem_id, test_cases)
vim.validate({ vim.validate({
platform = { platform, "string" }, platform = { platform, 'string' },
contest_id = { contest_id, "string" }, contest_id = { contest_id, 'string' },
problem_id = { problem_id, { "string", "nil" }, true }, problem_id = { problem_id, { 'string', 'nil' }, true },
test_cases = { test_cases, "table" }, test_cases = { test_cases, 'table' },
}) })
local problem_key = problem_id and (contest_id .. "_" .. problem_id) or contest_id local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id
if not cache_data[platform] then if not cache_data[platform] then
cache_data[platform] = {} cache_data[platform] = {}
end end

View file

@ -48,7 +48,7 @@
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string ---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
local M = {} local M = {}
local constants = require("cp.constants") local constants = require('cp.constants')
---@type cp.Config ---@type cp.Config
M.defaults = { M.defaults = {
@ -68,34 +68,34 @@ M.defaults = {
---@return cp.Config ---@return cp.Config
function M.setup(user_config) function M.setup(user_config)
vim.validate({ vim.validate({
user_config = { user_config, { "table", "nil" }, true }, user_config = { user_config, { 'table', 'nil' }, true },
}) })
if user_config then if user_config then
vim.validate({ vim.validate({
contests = { user_config.contests, { "table", "nil" }, true }, contests = { user_config.contests, { 'table', 'nil' }, true },
snippets = { user_config.snippets, { "table", "nil" }, true }, snippets = { user_config.snippets, { 'table', 'nil' }, true },
hooks = { user_config.hooks, { "table", "nil" }, true }, hooks = { user_config.hooks, { 'table', 'nil' }, true },
debug = { user_config.debug, { "boolean", "nil" }, true }, debug = { user_config.debug, { 'boolean', 'nil' }, true },
scrapers = { user_config.scrapers, { "table", "nil" }, true }, scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
filename = { user_config.filename, { "function", "nil" }, true }, filename = { user_config.filename, { 'function', 'nil' }, true },
}) })
if user_config.hooks then if user_config.hooks then
vim.validate({ vim.validate({
before_run = { before_run = {
user_config.hooks.before_run, user_config.hooks.before_run,
{ "function", "nil" }, { 'function', 'nil' },
true, true,
}, },
before_debug = { before_debug = {
user_config.hooks.before_debug, user_config.hooks.before_debug,
{ "function", "nil" }, { 'function', 'nil' },
true, true,
}, },
setup_code = { setup_code = {
user_config.hooks.setup_code, user_config.hooks.setup_code,
{ "function", "nil" }, { 'function', 'nil' },
true, true,
}, },
}) })
@ -104,16 +104,19 @@ function M.setup(user_config)
if user_config.contests then if user_config.contests then
for contest_name, contest_config in pairs(user_config.contests) do for contest_name, contest_config in pairs(user_config.contests) do
for lang_name, lang_config in pairs(contest_config) do for lang_name, lang_config in pairs(contest_config) do
if type(lang_config) == "table" and lang_config.extension then if type(lang_config) == 'table' and lang_config.extension then
if if
not vim.tbl_contains(vim.tbl_keys(constants.filetype_to_language), lang_config.extension) not vim.tbl_contains(
vim.tbl_keys(constants.filetype_to_language),
lang_config.extension
)
then then
error( error(
("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format( ("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format(
lang_config.extension, lang_config.extension,
lang_name, lang_name,
contest_name, contest_name,
table.concat(vim.tbl_keys(constants.filetype_to_language), ", ") table.concat(vim.tbl_keys(constants.filetype_to_language), ', ')
) )
) )
end end
@ -128,18 +131,20 @@ function M.setup(user_config)
error( error(
("Invalid contest '%s' in scrapers config. Valid contests: %s"):format( ("Invalid contest '%s' in scrapers config. Valid contests: %s"):format(
contest_name, contest_name,
table.concat(constants.PLATFORMS, ", ") table.concat(constants.PLATFORMS, ', ')
) )
) )
end end
if type(enabled) ~= "boolean" then if type(enabled) ~= 'boolean' then
error(("Scraper setting for '%s' must be boolean, got %s"):format(contest_name, type(enabled))) error(
("Scraper setting for '%s' must be boolean, got %s"):format(contest_name, type(enabled))
)
end end
end end
end end
end end
local config = vim.tbl_deep_extend("force", M.defaults, user_config or {}) local config = vim.tbl_deep_extend('force', M.defaults, user_config or {})
return config return config
end end
@ -148,8 +153,8 @@ end
---@return string ---@return string
local function default_filename(contest_id, problem_id) local function default_filename(contest_id, problem_id)
vim.validate({ vim.validate({
contest_id = { contest_id, "string" }, contest_id = { contest_id, 'string' },
problem_id = { problem_id, { "string", "nil" }, true }, problem_id = { problem_id, { 'string', 'nil' }, true },
}) })
if problem_id then if problem_id then

View file

@ -1,10 +1,10 @@
local M = {} local M = {}
M.PLATFORMS = { "atcoder", "codeforces", "cses" } M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
M.ACTIONS = { "test", "next", "prev" } M.ACTIONS = { 'test', 'next', 'prev' }
M.CPP = "cpp" M.CPP = 'cpp'
M.PYTHON = "python" M.PYTHON = 'python'
---@type table<string, string> ---@type table<string, string>
M.filetype_to_language = { M.filetype_to_language = {
@ -17,27 +17,27 @@ M.filetype_to_language = {
---@type table<string, string> ---@type table<string, string>
M.canonical_filetypes = { M.canonical_filetypes = {
[M.CPP] = "cpp", [M.CPP] = 'cpp',
[M.PYTHON] = "python", [M.PYTHON] = 'python',
} }
---@type table<number, string> ---@type table<number, string>
M.signal_codes = { M.signal_codes = {
[128] = "SIGILL", [128] = 'SIGILL',
[130] = "SIGINT", [130] = 'SIGINT',
[131] = "SIGQUIT", [131] = 'SIGQUIT',
[132] = "SIGILL", [132] = 'SIGILL',
[133] = "SIGTRAP", [133] = 'SIGTRAP',
[134] = "SIGABRT", [134] = 'SIGABRT',
[135] = "SIGBUS", [135] = 'SIGBUS',
[136] = "SIGFPE", [136] = 'SIGFPE',
[137] = "SIGKILL", [137] = 'SIGKILL',
[138] = "SIGUSR1", [138] = 'SIGUSR1',
[139] = "SIGSEGV", [139] = 'SIGSEGV',
[140] = "SIGUSR2", [140] = 'SIGUSR2',
[141] = "SIGPIPE", [141] = 'SIGPIPE',
[142] = "SIGALRM", [142] = 'SIGALRM',
[143] = "SIGTERM", [143] = 'SIGTERM',
} }
return M return M

View file

@ -6,9 +6,9 @@
---@field timed_out boolean ---@field timed_out boolean
local M = {} local M = {}
local logger = require("cp.log") local logger = require('cp.log')
local constants = require("cp.constants") local constants = require('cp.constants')
local filetype_to_language = constants.filetype_to_language local filetype_to_language = constants.filetype_to_language
---@param source_file string ---@param source_file string
@ -16,13 +16,13 @@ local filetype_to_language = constants.filetype_to_language
---@return string ---@return string
local function get_language_from_file(source_file, contest_config) local function get_language_from_file(source_file, contest_config)
vim.validate({ vim.validate({
source_file = { source_file, "string" }, source_file = { source_file, 'string' },
contest_config = { contest_config, "table" }, contest_config = { contest_config, 'table' },
}) })
local extension = vim.fn.fnamemodify(source_file, ":e") local extension = vim.fn.fnamemodify(source_file, ':e')
local language = filetype_to_language[extension] or contest_config.default_language local language = filetype_to_language[extension] or contest_config.default_language
logger.log(("detected language: %s (extension: %s)"):format(language, extension)) logger.log(('detected language: %s (extension: %s)'):format(language, extension))
return language return language
end end
@ -31,15 +31,15 @@ end
---@return string[] ---@return string[]
local function substitute_template(cmd_template, substitutions) local function substitute_template(cmd_template, substitutions)
vim.validate({ vim.validate({
cmd_template = { cmd_template, "table" }, cmd_template = { cmd_template, 'table' },
substitutions = { substitutions, "table" }, substitutions = { substitutions, 'table' },
}) })
local result = {} local result = {}
for _, arg in ipairs(cmd_template) do for _, arg in ipairs(cmd_template) do
local substituted = arg local substituted = arg
for key, value in pairs(substitutions) do for key, value in pairs(substitutions) do
substituted = substituted:gsub("{" .. key .. "}", value) substituted = substituted:gsub('{' .. key .. '}', value)
end end
table.insert(result, substituted) table.insert(result, substituted)
end end
@ -52,9 +52,9 @@ end
---@return string[] ---@return string[]
local function build_command(cmd_template, executable, substitutions) local function build_command(cmd_template, executable, substitutions)
vim.validate({ vim.validate({
cmd_template = { cmd_template, "table" }, cmd_template = { cmd_template, 'table' },
executable = { executable, { "string", "nil" }, true }, executable = { executable, { 'string', 'nil' }, true },
substitutions = { substitutions, "table" }, substitutions = { substitutions, 'table' },
}) })
local cmd = substitute_template(cmd_template, substitutions) local cmd = substitute_template(cmd_template, substitutions)
@ -65,7 +65,7 @@ local function build_command(cmd_template, executable, substitutions)
end end
local function ensure_directories() local function ensure_directories()
vim.system({ "mkdir", "-p", "build", "io" }):wait() vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
end end
---@param language_config table ---@param language_config table
@ -73,26 +73,29 @@ end
---@return {code: integer, stderr: string} ---@return {code: integer, stderr: string}
function M.compile_generic(language_config, substitutions) function M.compile_generic(language_config, substitutions)
vim.validate({ vim.validate({
language_config = { language_config, "table" }, language_config = { language_config, 'table' },
substitutions = { substitutions, "table" }, substitutions = { substitutions, 'table' },
}) })
if not language_config.compile then if not language_config.compile then
logger.log("no compilation step required") logger.log('no compilation step required')
return { code = 0, stderr = "" } return { code = 0, stderr = '' }
end end
local compile_cmd = substitute_template(language_config.compile, substitutions) local compile_cmd = substitute_template(language_config.compile, substitutions)
logger.log(("compiling: %s"):format(table.concat(compile_cmd, " "))) logger.log(('compiling: %s'):format(table.concat(compile_cmd, ' ')))
local start_time = vim.uv.hrtime() local start_time = vim.uv.hrtime()
local result = vim.system(compile_cmd, { text = true }):wait() local result = vim.system(compile_cmd, { text = true }):wait()
local compile_time = (vim.uv.hrtime() - start_time) / 1000000 local compile_time = (vim.uv.hrtime() - start_time) / 1000000
if result.code == 0 then if result.code == 0 then
logger.log(("compilation successful (%.1fms)"):format(compile_time)) logger.log(('compilation successful (%.1fms)'):format(compile_time))
else else
logger.log(("compilation failed (%.1fms): %s"):format(compile_time, result.stderr), vim.log.levels.WARN) logger.log(
('compilation failed (%.1fms): %s'):format(compile_time, result.stderr),
vim.log.levels.WARN
)
end end
return result return result
@ -104,20 +107,22 @@ end
---@return ExecuteResult ---@return ExecuteResult
local function execute_command(cmd, input_data, timeout_ms) local function execute_command(cmd, input_data, timeout_ms)
vim.validate({ vim.validate({
cmd = { cmd, "table" }, cmd = { cmd, 'table' },
input_data = { input_data, "string" }, input_data = { input_data, 'string' },
timeout_ms = { timeout_ms, "number" }, timeout_ms = { timeout_ms, 'number' },
}) })
logger.log(("executing: %s"):format(table.concat(cmd, " "))) logger.log(('executing: %s'):format(table.concat(cmd, ' ')))
local start_time = vim.uv.hrtime() local start_time = vim.uv.hrtime()
local result = vim.system(cmd, { local result = vim
.system(cmd, {
stdin = input_data, stdin = input_data,
timeout = timeout_ms, timeout = timeout_ms,
text = true, text = true,
}):wait() })
:wait()
local end_time = vim.uv.hrtime() local end_time = vim.uv.hrtime()
local execution_time = (end_time - start_time) / 1000000 local execution_time = (end_time - start_time) / 1000000
@ -125,16 +130,19 @@ local function execute_command(cmd, input_data, timeout_ms)
local actual_code = result.code or 0 local actual_code = result.code or 0
if result.code == 124 then if result.code == 124 then
logger.log(("execution timed out after %.1fms"):format(execution_time), vim.log.levels.WARN) logger.log(('execution timed out after %.1fms'):format(execution_time), vim.log.levels.WARN)
elseif actual_code ~= 0 then elseif actual_code ~= 0 then
logger.log(("execution failed (exit code %d, %.1fms)"):format(actual_code, execution_time), vim.log.levels.WARN) logger.log(
('execution failed (exit code %d, %.1fms)'):format(actual_code, execution_time),
vim.log.levels.WARN
)
else else
logger.log(("execution successful (%.1fms)"):format(execution_time)) logger.log(('execution successful (%.1fms)'):format(execution_time))
end end
return { return {
stdout = result.stdout or "", stdout = result.stdout or '',
stderr = result.stderr or "", stderr = result.stderr or '',
code = actual_code, code = actual_code,
time_ms = execution_time, time_ms = execution_time,
timed_out = result.code == 124, timed_out = result.code == 124,
@ -147,31 +155,31 @@ end
---@return string ---@return string
local function format_output(exec_result, expected_file, is_debug) local function format_output(exec_result, expected_file, is_debug)
vim.validate({ vim.validate({
exec_result = { exec_result, "table" }, exec_result = { exec_result, 'table' },
expected_file = { expected_file, "string" }, expected_file = { expected_file, 'string' },
is_debug = { is_debug, "boolean" }, is_debug = { is_debug, 'boolean' },
}) })
local output_lines = { exec_result.stdout } local output_lines = { exec_result.stdout }
local metadata_lines = {} local metadata_lines = {}
if exec_result.timed_out then if exec_result.timed_out then
table.insert(metadata_lines, "[code]: 124 (TIMEOUT)") table.insert(metadata_lines, '[code]: 124 (TIMEOUT)')
elseif exec_result.code >= 128 then elseif exec_result.code >= 128 then
local signal_name = constants.signal_codes[exec_result.code] or "SIGNAL" local signal_name = constants.signal_codes[exec_result.code] or 'SIGNAL'
table.insert(metadata_lines, ("[code]: %d (%s)"):format(exec_result.code, signal_name)) table.insert(metadata_lines, ('[code]: %d (%s)'):format(exec_result.code, signal_name))
else else
table.insert(metadata_lines, ("[code]: %d"):format(exec_result.code)) table.insert(metadata_lines, ('[code]: %d'):format(exec_result.code))
end end
table.insert(metadata_lines, ("[time]: %.2f ms"):format(exec_result.time_ms)) table.insert(metadata_lines, ('[time]: %.2f ms'):format(exec_result.time_ms))
table.insert(metadata_lines, ("[debug]: %s"):format(is_debug and "true" or "false")) table.insert(metadata_lines, ('[debug]: %s'):format(is_debug and 'true' or 'false'))
if vim.fn.filereadable(expected_file) == 1 and exec_result.code == 0 then if vim.fn.filereadable(expected_file) == 1 and exec_result.code == 0 then
local expected_content = vim.fn.readfile(expected_file) local expected_content = vim.fn.readfile(expected_file)
local actual_lines = vim.split(exec_result.stdout, "\n") local actual_lines = vim.split(exec_result.stdout, '\n')
while #actual_lines > 0 and actual_lines[#actual_lines] == "" do while #actual_lines > 0 and actual_lines[#actual_lines] == '' do
table.remove(actual_lines) table.remove(actual_lines)
end end
@ -185,10 +193,10 @@ local function format_output(exec_result, expected_file, is_debug)
end end
end end
table.insert(metadata_lines, ("[ok]: %s"):format(ok and "true" or "false")) table.insert(metadata_lines, ('[ok]: %s'):format(ok and 'true' or 'false'))
end end
return table.concat(output_lines, "") .. "\n" .. table.concat(metadata_lines, "\n") return table.concat(output_lines, '') .. '\n' .. table.concat(metadata_lines, '\n')
end end
---@param ctx ProblemContext ---@param ctx ProblemContext
@ -197,15 +205,15 @@ end
---@return boolean success ---@return boolean success
function M.compile_problem(ctx, contest_config, is_debug) function M.compile_problem(ctx, contest_config, is_debug)
vim.validate({ vim.validate({
ctx = { ctx, "table" }, ctx = { ctx, 'table' },
contest_config = { contest_config, "table" }, contest_config = { contest_config, 'table' },
}) })
local language = get_language_from_file(ctx.source_file, contest_config) local language = get_language_from_file(ctx.source_file, contest_config)
local language_config = contest_config[language] local language_config = contest_config[language]
if not language_config then if not language_config then
logger.log("No configuration for language: " .. language, vim.log.levels.ERROR) logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
return false return false
end end
@ -215,15 +223,19 @@ function M.compile_problem(ctx, contest_config, is_debug)
version = tostring(language_config.version), version = tostring(language_config.version),
} }
local compile_cmd = (is_debug and language_config.debug) and language_config.debug or language_config.compile local compile_cmd = (is_debug and language_config.debug) and language_config.debug
or language_config.compile
if compile_cmd then if compile_cmd then
language_config.compile = compile_cmd language_config.compile = compile_cmd
local compile_result = M.compile_generic(language_config, substitutions) local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then if compile_result.code ~= 0 then
logger.log("compilation failed: " .. (compile_result.stderr or "unknown error"), vim.log.levels.ERROR) logger.log(
'compilation failed: ' .. (compile_result.stderr or 'unknown error'),
vim.log.levels.ERROR
)
return false return false
end end
logger.log(("compilation successful (%s)"):format(is_debug and "debug mode" or "test mode")) logger.log(('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'))
end end
return true return true
@ -231,9 +243,9 @@ end
function M.run_problem(ctx, contest_config, is_debug) function M.run_problem(ctx, contest_config, is_debug)
vim.validate({ vim.validate({
ctx = { ctx, "table" }, ctx = { ctx, 'table' },
contest_config = { contest_config, "table" }, contest_config = { contest_config, 'table' },
is_debug = { is_debug, "boolean" }, is_debug = { is_debug, 'boolean' },
}) })
ensure_directories() ensure_directories()
@ -242,7 +254,7 @@ function M.run_problem(ctx, contest_config, is_debug)
local language_config = contest_config[language] local language_config = contest_config[language]
if not language_config then if not language_config then
vim.fn.writefile({ "Error: No configuration for language: " .. language }, ctx.output_file) vim.fn.writefile({ 'Error: No configuration for language: ' .. language }, ctx.output_file)
return return
end end
@ -261,9 +273,9 @@ function M.run_problem(ctx, contest_config, is_debug)
end end
end end
local input_data = "" local input_data = ''
if vim.fn.filereadable(ctx.input_file) == 1 then if vim.fn.filereadable(ctx.input_file) == 1 then
input_data = table.concat(vim.fn.readfile(ctx.input_file), "\n") .. "\n" input_data = table.concat(vim.fn.readfile(ctx.input_file), '\n') .. '\n'
end end
local run_cmd = build_command(language_config.run, language_config.executable, substitutions) local run_cmd = build_command(language_config.run, language_config.executable, substitutions)
@ -272,12 +284,12 @@ function M.run_problem(ctx, contest_config, is_debug)
local output_buf = vim.fn.bufnr(ctx.output_file) local output_buf = vim.fn.bufnr(ctx.output_file)
if output_buf ~= -1 then if output_buf ~= -1 then
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.split(formatted_output, "\n")) vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.split(formatted_output, '\n'))
vim.api.nvim_buf_call(output_buf, function() vim.api.nvim_buf_call(output_buf, function()
vim.cmd.write() vim.cmd.write()
end) end)
else else
vim.fn.writefile(vim.split(formatted_output, "\n"), ctx.output_file) vim.fn.writefile(vim.split(formatted_output, '\n'), ctx.output_file)
end end
end end

View file

@ -1,88 +1,88 @@
local M = {} local M = {}
local function check_nvim_version() local function check_nvim_version()
if vim.fn.has("nvim-0.10.0") == 1 then if vim.fn.has('nvim-0.10.0') == 1 then
vim.health.ok("Neovim 0.10.0+ detected") vim.health.ok('Neovim 0.10.0+ detected')
else else
vim.health.error("cp.nvim requires Neovim 0.10.0+") vim.health.error('cp.nvim requires Neovim 0.10.0+')
end end
end end
local function check_uv() local function check_uv()
if vim.fn.executable("uv") == 1 then if vim.fn.executable('uv') == 1 then
vim.health.ok("uv executable found") vim.health.ok('uv executable found')
local result = vim.system({ "uv", "--version" }, { text = true }):wait() local result = vim.system({ 'uv', '--version' }, { text = true }):wait()
if result.code == 0 then if result.code == 0 then
vim.health.info("uv version: " .. result.stdout:gsub("\n", "")) vim.health.info('uv version: ' .. result.stdout:gsub('\n', ''))
end end
else else
vim.health.warn("uv not found - install from https://docs.astral.sh/uv/ for problem scraping") vim.health.warn('uv not found - install from https://docs.astral.sh/uv/ for problem scraping')
end end
end end
local function check_python_env() local function check_python_env()
local plugin_path = debug.getinfo(1, "S").source:sub(2) local plugin_path = debug.getinfo(1, 'S').source:sub(2)
plugin_path = vim.fn.fnamemodify(plugin_path, ":h:h:h") plugin_path = vim.fn.fnamemodify(plugin_path, ':h:h:h')
local venv_dir = plugin_path .. "/.venv" local venv_dir = plugin_path .. '/.venv'
if vim.fn.isdirectory(venv_dir) == 1 then if vim.fn.isdirectory(venv_dir) == 1 then
vim.health.ok("Python virtual environment found at " .. venv_dir) vim.health.ok('Python virtual environment found at ' .. venv_dir)
else else
vim.health.warn("Python virtual environment not set up - run :CP command to initialize") vim.health.warn('Python virtual environment not set up - run :CP command to initialize')
end end
end end
local function check_scrapers() local function check_scrapers()
local plugin_path = debug.getinfo(1, "S").source:sub(2) local plugin_path = debug.getinfo(1, 'S').source:sub(2)
plugin_path = vim.fn.fnamemodify(plugin_path, ":h:h:h") plugin_path = vim.fn.fnamemodify(plugin_path, ':h:h:h')
local scrapers = { "atcoder.py", "codeforces.py", "cses.py" } local scrapers = { 'atcoder.py', 'codeforces.py', 'cses.py' }
for _, scraper in ipairs(scrapers) do for _, scraper in ipairs(scrapers) do
local scraper_path = plugin_path .. "/scrapers/" .. scraper local scraper_path = plugin_path .. '/scrapers/' .. scraper
if vim.fn.filereadable(scraper_path) == 1 then if vim.fn.filereadable(scraper_path) == 1 then
vim.health.ok("Scraper found: " .. scraper) vim.health.ok('Scraper found: ' .. scraper)
else else
vim.health.error("Missing scraper: " .. scraper) vim.health.error('Missing scraper: ' .. scraper)
end end
end end
end end
local function check_luasnip() local function check_luasnip()
local has_luasnip, luasnip = pcall(require, "luasnip") local has_luasnip, luasnip = pcall(require, 'luasnip')
if has_luasnip then if has_luasnip then
vim.health.ok("LuaSnip integration available") vim.health.ok('LuaSnip integration available')
local snippet_count = #luasnip.get_snippets("all") local snippet_count = #luasnip.get_snippets('all')
vim.health.info("LuaSnip snippets loaded: " .. snippet_count) vim.health.info('LuaSnip snippets loaded: ' .. snippet_count)
else else
vim.health.info("LuaSnip not available - template expansion will be limited") vim.health.info('LuaSnip not available - template expansion will be limited')
end end
end end
local function check_config() local function check_config()
vim.health.ok("Plugin ready") vim.health.ok('Plugin ready')
local cp = require("cp") local cp = require('cp')
local context = cp.get_current_context() local context = cp.get_current_context()
if context.platform then if context.platform then
local info = context.platform local info = context.platform
if context.contest_id then if context.contest_id then
info = info .. " " .. context.contest_id info = info .. ' ' .. context.contest_id
if context.problem_id then if context.problem_id then
info = info .. " " .. context.problem_id info = info .. ' ' .. context.problem_id
end end
end end
vim.health.info("Current context: " .. info) vim.health.info('Current context: ' .. info)
else else
vim.health.info("No contest context set") vim.health.info('No contest context set')
end end
end end
function M.check() function M.check()
local version = require("cp.version") local version = require('cp.version')
vim.health.start("cp.nvim health check") vim.health.start('cp.nvim health check')
vim.health.info("Version: " .. version.version) vim.health.info('Version: ' .. version.version)
check_nvim_version() check_nvim_version()
check_uv() check_uv()

View file

@ -1,14 +1,14 @@
local M = {} local M = {}
local config_module = require("cp.config") local cache = require('cp.cache')
local snippets = require("cp.snippets") local config_module = require('cp.config')
local scrape = require("cp.scrape") local logger = require('cp.log')
local logger = require("cp.log") local problem = require('cp.problem')
local problem = require("cp.problem") local scrape = require('cp.scrape')
local cache = require("cp.cache") local snippets = require('cp.snippets')
if not vim.fn.has("nvim-0.10.0") then if not vim.fn.has('nvim-0.10.0') then
vim.notify("[cp.nvim]: requires nvim-0.10.0+", vim.log.levels.ERROR) vim.notify('[cp.nvim]: requires nvim-0.10.0+', vim.log.levels.ERROR)
return {} return {}
end end
@ -28,19 +28,22 @@ local state = {
test_panel_active = false, test_panel_active = false,
} }
local constants = require("cp.constants") local constants = require('cp.constants')
local platforms = constants.PLATFORMS local platforms = constants.PLATFORMS
local actions = constants.ACTIONS local actions = constants.ACTIONS
local function set_platform(platform) local function set_platform(platform)
if not vim.tbl_contains(platforms, platform) then if not vim.tbl_contains(platforms, platform) then
logger.log(("unknown platform. Available: [%s]"):format(table.concat(platforms, ", ")), vim.log.levels.ERROR) logger.log(
('unknown platform. Available: [%s]'):format(table.concat(platforms, ', ')),
vim.log.levels.ERROR
)
return false return false
end end
state.platform = platform state.platform = platform
vim.fn.mkdir("build", "p") vim.fn.mkdir('build', 'p')
vim.fn.mkdir("io", "p") vim.fn.mkdir('io', 'p')
return true return true
end end
@ -49,12 +52,12 @@ end
---@param language? string ---@param language? string
local function setup_problem(contest_id, problem_id, language) local function setup_problem(contest_id, problem_id, language)
if not state.platform then if not state.platform then
logger.log("no platform set. run :CP <platform> <contest> first", vim.log.levels.ERROR) logger.log('no platform set. run :CP <platform> <contest> first', vim.log.levels.ERROR)
return return
end end
local problem_name = state.platform == "cses" and contest_id or (contest_id .. (problem_id or "")) local problem_name = state.platform == 'cses' and contest_id or (contest_id .. (problem_id or ''))
logger.log(("setting up problem: %s"):format(problem_name)) logger.log(('setting up problem: %s'):format(problem_name))
local ctx = problem.create_context(state.platform, contest_id, problem_id, config, language) local ctx = problem.create_context(state.platform, contest_id, problem_id, config, language)
@ -62,7 +65,7 @@ local function setup_problem(contest_id, problem_id, language)
local metadata_result = scrape.scrape_contest_metadata(state.platform, contest_id) local metadata_result = scrape.scrape_contest_metadata(state.platform, contest_id)
if not metadata_result.success then if not metadata_result.success then
logger.log( logger.log(
"failed to load contest metadata: " .. (metadata_result.error or "unknown error"), 'failed to load contest metadata: ' .. (metadata_result.error or 'unknown error'),
vim.log.levels.WARN vim.log.levels.WARN
) )
end end
@ -77,23 +80,26 @@ local function setup_problem(contest_id, problem_id, language)
local scrape_result = scrape.scrape_problem(ctx) local scrape_result = scrape.scrape_problem(ctx)
if not scrape_result.success then if not scrape_result.success then
logger.log("scraping failed: " .. (scrape_result.error or "unknown error"), vim.log.levels.ERROR) logger.log(
'scraping failed: ' .. (scrape_result.error or 'unknown error'),
vim.log.levels.ERROR
)
return return
end end
local test_count = scrape_result.test_count or 0 local test_count = scrape_result.test_count or 0
logger.log(("scraped %d test case(s) for %s"):format(test_count, scrape_result.problem_id)) logger.log(('scraped %d test case(s) for %s'):format(test_count, scrape_result.problem_id))
state.test_cases = scrape_result.test_cases state.test_cases = scrape_result.test_cases
if scrape_result.test_cases then if scrape_result.test_cases then
cache.set_test_cases(state.platform, contest_id, problem_id, scrape_result.test_cases) cache.set_test_cases(state.platform, contest_id, problem_id, scrape_result.test_cases)
end end
else else
logger.log(("scraping disabled for %s"):format(state.platform)) logger.log(('scraping disabled for %s'):format(state.platform))
state.test_cases = nil state.test_cases = nil
end end
vim.cmd("silent only") vim.cmd('silent only')
state.contest_id = contest_id state.contest_id = contest_id
state.problem_id = problem_id state.problem_id = problem_id
@ -101,13 +107,13 @@ local function setup_problem(contest_id, problem_id, language)
vim.cmd.e(ctx.source_file) vim.cmd.e(ctx.source_file)
local source_buf = vim.api.nvim_get_current_buf() local source_buf = vim.api.nvim_get_current_buf()
if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == "" then if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == '' then
local has_luasnip, luasnip = pcall(require, "luasnip") local has_luasnip, luasnip = pcall(require, 'luasnip')
if has_luasnip then if has_luasnip then
local filetype = vim.api.nvim_get_option_value("filetype", { buf = source_buf }) local filetype = vim.api.nvim_get_option_value('filetype', { buf = source_buf })
local language_name = constants.filetype_to_language[filetype] local language_name = constants.filetype_to_language[filetype]
local canonical_language = constants.canonical_filetypes[language_name] or language_name local canonical_language = constants.canonical_filetypes[language_name] or language_name
local prefixed_trigger = ("cp.nvim/%s.%s"):format(state.platform, canonical_language) local prefixed_trigger = ('cp.nvim/%s.%s'):format(state.platform, canonical_language)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { prefixed_trigger }) vim.api.nvim_buf_set_lines(0, 0, -1, false, { prefixed_trigger })
vim.api.nvim_win_set_cursor(0, { 1, #prefixed_trigger }) vim.api.nvim_win_set_cursor(0, { 1, #prefixed_trigger })
@ -117,13 +123,13 @@ local function setup_problem(contest_id, problem_id, language)
if luasnip.expandable() then if luasnip.expandable() then
luasnip.expand() luasnip.expand()
else else
vim.api.nvim_buf_set_lines(0, 0, 1, false, { "" }) vim.api.nvim_buf_set_lines(0, 0, 1, false, { '' })
vim.api.nvim_win_set_cursor(0, { 1, 0 }) vim.api.nvim_win_set_cursor(0, { 1, 0 })
end end
vim.cmd.stopinsert() vim.cmd.stopinsert()
end) end)
else else
vim.api.nvim_input(("i%s<c-space><esc>"):format(state.platform)) vim.api.nvim_input(('i%s<c-space><esc>'):format(state.platform))
end end
end end
@ -131,13 +137,13 @@ local function setup_problem(contest_id, problem_id, language)
config.hooks.setup_code(ctx) config.hooks.setup_code(ctx)
end end
logger.log(("switched to problem %s"):format(ctx.problem_name)) logger.log(('switched to problem %s'):format(ctx.problem_name))
end end
local function get_current_problem() local function get_current_problem()
local filename = vim.fn.expand("%:t:r") local filename = vim.fn.expand('%:t:r')
if filename == "" then if filename == '' then
logger.log("no file open", vim.log.levels.ERROR) logger.log('no file open', vim.log.levels.ERROR)
return nil return nil
end end
return filename return filename
@ -146,18 +152,18 @@ end
local function toggle_test_panel(is_debug) local function toggle_test_panel(is_debug)
if state.test_panel_active then if state.test_panel_active then
if state.saved_session then if state.saved_session then
vim.cmd(("source %s"):format(state.saved_session)) vim.cmd(('source %s'):format(state.saved_session))
vim.fn.delete(state.saved_session) vim.fn.delete(state.saved_session)
state.saved_session = nil state.saved_session = nil
end end
state.test_panel_active = false state.test_panel_active = false
logger.log("test panel closed") logger.log('test panel closed')
return return
end end
if not state.platform then if not state.platform then
logger.log( logger.log(
"No contest configured. Use :CP <platform> <contest> <problem> to set up first.", 'No contest configured. Use :CP <platform> <contest> <problem> to set up first.',
vim.log.levels.ERROR vim.log.levels.ERROR
) )
return return
@ -169,37 +175,37 @@ local function toggle_test_panel(is_debug)
end end
local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config) local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config)
local test_module = require("cp.test") local test_module = require('cp.test')
if not test_module.load_test_cases(ctx, state) then if not test_module.load_test_cases(ctx, state) then
logger.log("no test cases found", vim.log.levels.WARN) logger.log('no test cases found', vim.log.levels.WARN)
return return
end end
state.saved_session = vim.fn.tempname() state.saved_session = vim.fn.tempname()
vim.cmd(("mksession! %s"):format(state.saved_session)) vim.cmd(('mksession! %s'):format(state.saved_session))
vim.cmd("silent only") vim.cmd('silent only')
local tab_buf = vim.api.nvim_create_buf(false, true) local tab_buf = vim.api.nvim_create_buf(false, true)
local expected_buf = vim.api.nvim_create_buf(false, true) local expected_buf = vim.api.nvim_create_buf(false, true)
local actual_buf = vim.api.nvim_create_buf(false, true) local actual_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = tab_buf }) vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = tab_buf })
vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = expected_buf }) vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = expected_buf })
vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = actual_buf }) vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = actual_buf })
local main_win = vim.api.nvim_get_current_win() local main_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(main_win, tab_buf) vim.api.nvim_win_set_buf(main_win, tab_buf)
vim.api.nvim_set_option_value("filetype", "cptest", { buf = tab_buf }) vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf })
vim.cmd.split() vim.cmd.split()
vim.api.nvim_win_set_buf(0, actual_buf) vim.api.nvim_win_set_buf(0, actual_buf)
vim.api.nvim_set_option_value("filetype", "cptest", { buf = actual_buf }) vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_buf })
vim.cmd.vsplit() vim.cmd.vsplit()
vim.api.nvim_win_set_buf(0, expected_buf) vim.api.nvim_win_set_buf(0, expected_buf)
vim.api.nvim_set_option_value("filetype", "cptest", { buf = expected_buf }) vim.api.nvim_set_option_value('filetype', 'cptest', { buf = expected_buf })
local expected_win = vim.fn.bufwinid(expected_buf) local expected_win = vim.fn.bufwinid(expected_buf)
local actual_win = vim.fn.bufwinid(actual_buf) local actual_win = vim.fn.bufwinid(actual_buf)
@ -224,7 +230,7 @@ local function toggle_test_panel(is_debug)
local max_time_width = 0 local max_time_width = 0
for _, test_case in ipairs(test_state.test_cases) do for _, test_case in ipairs(test_state.test_cases) do
local status_text = test_case.status == "pending" and "" or string.upper(test_case.status) local status_text = test_case.status == 'pending' and '' or string.upper(test_case.status)
max_status_width = math.max(max_status_width, #status_text) max_status_width = math.max(max_status_width, #status_text)
if test_case.code then if test_case.code then
@ -232,30 +238,30 @@ local function toggle_test_panel(is_debug)
end end
if test_case.time_ms then if test_case.time_ms then
local time_text = string.format("%.0fms", test_case.time_ms) local time_text = string.format('%.0fms', test_case.time_ms)
max_time_width = math.max(max_time_width, #time_text) max_time_width = math.max(max_time_width, #time_text)
end end
end end
for i, test_case in ipairs(test_state.test_cases) do for i, test_case in ipairs(test_state.test_cases) do
local prefix = i == test_state.current_index and "> " or " " local prefix = i == test_state.current_index and '> ' or ' '
local tab = string.format("%s%d.", prefix, i) local tab = string.format('%s%d.', prefix, i)
if test_case.ok ~= nil then if test_case.ok ~= nil then
tab = tab .. string.format(" [ok:%-5s]", tostring(test_case.ok)) tab = tab .. string.format(' [ok:%-5s]', tostring(test_case.ok))
end end
if test_case.code then if test_case.code then
tab = tab .. string.format(" [code:%-" .. max_code_width .. "s]", tostring(test_case.code)) tab = tab .. string.format(' [code:%-' .. max_code_width .. 's]', tostring(test_case.code))
end end
if test_case.time_ms then if test_case.time_ms then
local time_text = string.format("%.0fms", test_case.time_ms) local time_text = string.format('%.0fms', test_case.time_ms)
tab = tab .. string.format(" [time:%-" .. max_time_width .. "s]", time_text) tab = tab .. string.format(' [time:%-' .. max_time_width .. 's]', time_text)
end end
if test_case.signal then if test_case.signal then
tab = tab .. string.format(" [%s]", test_case.signal) tab = tab .. string.format(' [%s]', test_case.signal)
end end
table.insert(tab_lines, tab) table.insert(tab_lines, tab)
@ -263,9 +269,9 @@ local function toggle_test_panel(is_debug)
local current_test = test_state.test_cases[test_state.current_index] local current_test = test_state.test_cases[test_state.current_index]
if current_test then if current_test then
table.insert(tab_lines, "") table.insert(tab_lines, '')
table.insert(tab_lines, "Input:") table.insert(tab_lines, 'Input:')
for _, line in ipairs(vim.split(current_test.input, "\n", { plain = true, trimempty = true })) do for _, line in ipairs(vim.split(current_test.input, '\n', { plain = true, trimempty = true })) do
table.insert(tab_lines, line) table.insert(tab_lines, line)
end end
end end
@ -282,12 +288,12 @@ local function toggle_test_panel(is_debug)
end end
local expected_text = current_test.expected local expected_text = current_test.expected
local expected_lines = vim.split(expected_text, "\n", { plain = true, trimempty = true }) local expected_lines = vim.split(expected_text, '\n', { plain = true, trimempty = true })
vim.api.nvim_buf_set_lines(test_buffers.expected_buf, 0, -1, false, expected_lines) vim.api.nvim_buf_set_lines(test_buffers.expected_buf, 0, -1, false, expected_lines)
if vim.fn.has("nvim-0.8.0") == 1 then if vim.fn.has('nvim-0.8.0') == 1 then
vim.api.nvim_set_option_value("winbar", "Expected", { win = test_windows.expected_win }) vim.api.nvim_set_option_value('winbar', 'Expected', { win = test_windows.expected_win })
end end
end end
@ -303,20 +309,20 @@ local function toggle_test_panel(is_debug)
local enable_diff = false local enable_diff = false
if current_test.actual then if current_test.actual then
actual_lines = vim.split(current_test.actual, "\n", { plain = true, trimempty = true }) actual_lines = vim.split(current_test.actual, '\n', { plain = true, trimempty = true })
enable_diff = current_test.status == "fail" enable_diff = current_test.status == 'fail'
else else
actual_lines = { "(not run yet)" } actual_lines = { '(not run yet)' }
end end
vim.api.nvim_buf_set_lines(test_buffers.actual_buf, 0, -1, false, actual_lines) vim.api.nvim_buf_set_lines(test_buffers.actual_buf, 0, -1, false, actual_lines)
if vim.fn.has("nvim-0.8.0") == 1 then if vim.fn.has('nvim-0.8.0') == 1 then
vim.api.nvim_set_option_value("winbar", "Actual", { win = test_windows.actual_win }) vim.api.nvim_set_option_value('winbar', 'Actual', { win = test_windows.actual_win })
end end
vim.api.nvim_set_option_value("diff", enable_diff, { win = test_windows.expected_win }) vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.expected_win })
vim.api.nvim_set_option_value("diff", enable_diff, { win = test_windows.actual_win }) vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.actual_win })
if enable_diff then if enable_diff then
vim.api.nvim_win_call(test_windows.expected_win, function() vim.api.nvim_win_call(test_windows.expected_win, function()
@ -356,15 +362,15 @@ local function toggle_test_panel(is_debug)
refresh_test_panel() refresh_test_panel()
end end
vim.keymap.set("n", "<c-n>", function() vim.keymap.set('n', '<c-n>', function()
navigate_test_case(1) navigate_test_case(1)
end, { buffer = test_buffers.tab_buf, silent = true }) end, { buffer = test_buffers.tab_buf, silent = true })
vim.keymap.set("n", "<c-p>", function() vim.keymap.set('n', '<c-p>', function()
navigate_test_case(-1) navigate_test_case(-1)
end, { buffer = test_buffers.tab_buf, silent = true }) end, { buffer = test_buffers.tab_buf, silent = true })
for _, buf in pairs(test_buffers) do for _, buf in pairs(test_buffers) do
vim.keymap.set("n", "q", function() vim.keymap.set('n', 'q', function()
toggle_test_panel() toggle_test_panel()
end, { buffer = buf, silent = true }) end, { buffer = buf, silent = true })
end end
@ -373,7 +379,7 @@ local function toggle_test_panel(is_debug)
config.hooks.before_debug(ctx) config.hooks.before_debug(ctx)
end end
local execute_module = require("cp.execute") local execute_module = require('cp.execute')
local contest_config = config.contests[state.platform] local contest_config = config.contests[state.platform]
if execute_module.compile_problem(ctx, contest_config, is_debug) then if execute_module.compile_problem(ctx, contest_config, is_debug) then
test_module.run_all_test_cases(ctx, contest_config) test_module.run_all_test_cases(ctx, contest_config)
@ -387,35 +393,38 @@ local function toggle_test_panel(is_debug)
state.test_buffers = test_buffers state.test_buffers = test_buffers
state.test_windows = test_windows state.test_windows = test_windows
local test_state = test_module.get_test_panel_state() local test_state = test_module.get_test_panel_state()
logger.log(string.format("test panel opened (%d test cases)", #test_state.test_cases)) logger.log(string.format('test panel opened (%d test cases)', #test_state.test_cases))
end end
---@param delta number 1 for next, -1 for prev ---@param delta number 1 for next, -1 for prev
---@param language? string ---@param language? string
local function navigate_problem(delta, language) local function navigate_problem(delta, language)
if not state.platform or not state.contest_id then if not state.platform or not state.contest_id then
logger.log("no contest set. run :CP <platform> <contest> first", vim.log.levels.ERROR) logger.log('no contest set. run :CP <platform> <contest> first', vim.log.levels.ERROR)
return return
end end
cache.load() cache.load()
local contest_data = cache.get_contest_data(state.platform, state.contest_id) local contest_data = cache.get_contest_data(state.platform, state.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 metadata found. set up a problem first to cache contest data", vim.log.levels.ERROR) logger.log(
'no contest metadata found. set up a problem first to cache contest data',
vim.log.levels.ERROR
)
return return
end end
local problems = contest_data.problems local problems = contest_data.problems
local current_problem_id local current_problem_id
if state.platform == "cses" then if state.platform == 'cses' then
current_problem_id = state.contest_id current_problem_id = state.contest_id
else else
current_problem_id = state.problem_id current_problem_id = state.problem_id
end end
if not current_problem_id then if not current_problem_id then
logger.log("no current problem set", vim.log.levels.ERROR) logger.log('no current problem set', vim.log.levels.ERROR)
return return
end end
@ -428,21 +437,21 @@ local function navigate_problem(delta, language)
end end
if not current_index then if not current_index then
logger.log("current problem not found in contest", vim.log.levels.ERROR) logger.log('current problem not found in contest', vim.log.levels.ERROR)
return return
end end
local new_index = current_index + delta local new_index = current_index + delta
if new_index < 1 or new_index > #problems then if new_index < 1 or new_index > #problems then
local direction = delta > 0 and "next" or "previous" local direction = delta > 0 and 'next' or 'previous'
logger.log(("no %s problem available"):format(direction), vim.log.levels.INFO) logger.log(('no %s problem available'):format(direction), vim.log.levels.INFO)
return return
end end
local new_problem = problems[new_index] local new_problem = problems[new_index]
if state.platform == "cses" then if state.platform == 'cses' then
setup_problem(new_problem.id, nil, language) setup_problem(new_problem.id, nil, language)
else else
setup_problem(state.contest_id, new_problem.id, language) setup_problem(state.contest_id, new_problem.id, language)
@ -452,8 +461,8 @@ end
local function parse_command(args) local function parse_command(args)
if #args == 0 then if #args == 0 then
return { return {
type = "error", type = 'error',
message = "Usage: :CP <platform> <contest> [problem] [--lang=<language>] | :CP <action> | :CP <problem>", message = 'Usage: :CP <platform> <contest> [problem] [--lang=<language>] | :CP <action> | :CP <problem>',
} }
end end
@ -461,48 +470,48 @@ local function parse_command(args)
local debug = false local debug = false
for i, arg in ipairs(args) do for i, arg in ipairs(args) do
local lang_match = arg:match("^--lang=(.+)$") local lang_match = arg:match('^--lang=(.+)$')
if lang_match then if lang_match then
language = lang_match language = lang_match
elseif arg == "--lang" then elseif arg == '--lang' then
if i + 1 <= #args then if i + 1 <= #args then
language = args[i + 1] language = args[i + 1]
else else
return { type = "error", message = "--lang requires a value" } return { type = 'error', message = '--lang requires a value' }
end end
elseif arg == "--debug" then elseif arg == '--debug' then
debug = true debug = true
end end
end end
local filtered_args = vim.tbl_filter(function(arg) local filtered_args = vim.tbl_filter(function(arg)
return not (arg:match("^--lang") or arg == language or arg == "--debug") return not (arg:match('^--lang') or arg == language or arg == '--debug')
end, args) end, args)
local first = filtered_args[1] local first = filtered_args[1]
if vim.tbl_contains(actions, first) then if vim.tbl_contains(actions, first) then
return { type = "action", action = first, language = language, debug = debug } return { type = 'action', action = first, language = language, debug = debug }
end end
if vim.tbl_contains(platforms, first) then if vim.tbl_contains(platforms, first) then
if #filtered_args == 1 then if #filtered_args == 1 then
return { return {
type = "platform_only", type = 'platform_only',
platform = first, platform = first,
language = language, language = language,
} }
elseif #filtered_args == 2 then elseif #filtered_args == 2 then
if first == "cses" then if first == 'cses' then
return { return {
type = "cses_problem", type = 'cses_problem',
platform = first, platform = first,
problem = filtered_args[2], problem = filtered_args[2],
language = language, language = language,
} }
else else
return { return {
type = "contest_setup", type = 'contest_setup',
platform = first, platform = first,
contest = filtered_args[2], contest = filtered_args[2],
language = language, language = language,
@ -510,14 +519,14 @@ local function parse_command(args)
end end
elseif #filtered_args == 3 then elseif #filtered_args == 3 then
return { return {
type = "full_setup", type = 'full_setup',
platform = first, platform = first,
contest = filtered_args[2], contest = filtered_args[2],
problem = filtered_args[3], problem = filtered_args[3],
language = language, language = language,
} }
else else
return { type = "error", message = "Too many arguments" } return { type = 'error', message = 'Too many arguments' }
end end
end end
@ -529,55 +538,59 @@ local function parse_command(args)
return prob.id return prob.id
end, contest_data.problems) end, contest_data.problems)
if vim.tbl_contains(problem_ids, first) then if vim.tbl_contains(problem_ids, first) then
return { type = "problem_switch", problem = first, language = language } return { type = 'problem_switch', problem = first, language = language }
end end
end end
return { return {
type = "error", type = 'error',
message = ("invalid subcommand '%s'"):format(first), message = ("invalid subcommand '%s'"):format(first),
} }
end end
return { type = "error", message = "Unknown command or no contest context" } return { type = 'error', message = 'Unknown command or no contest context' }
end end
function M.handle_command(opts) function M.handle_command(opts)
local cmd = parse_command(opts.fargs) local cmd = parse_command(opts.fargs)
if cmd.type == "error" then if cmd.type == 'error' then
logger.log(cmd.message, vim.log.levels.ERROR) logger.log(cmd.message, vim.log.levels.ERROR)
return return
end end
if cmd.type == "action" then if cmd.type == 'action' then
if cmd.action == "test" then if cmd.action == 'test' then
toggle_test_panel(cmd.debug) toggle_test_panel(cmd.debug)
elseif cmd.action == "next" then elseif cmd.action == 'next' then
navigate_problem(1, cmd.language) navigate_problem(1, cmd.language)
elseif cmd.action == "prev" then elseif cmd.action == 'prev' then
navigate_problem(-1, cmd.language) navigate_problem(-1, cmd.language)
end end
return return
end end
if cmd.type == "platform_only" then if cmd.type == 'platform_only' then
set_platform(cmd.platform) set_platform(cmd.platform)
return return
end end
if cmd.type == "contest_setup" then if cmd.type == 'contest_setup' then
if set_platform(cmd.platform) then if set_platform(cmd.platform) then
state.contest_id = cmd.contest state.contest_id = cmd.contest
if vim.tbl_contains(config.scrapers, cmd.platform) then if vim.tbl_contains(config.scrapers, cmd.platform) then
local metadata_result = scrape.scrape_contest_metadata(cmd.platform, cmd.contest) local metadata_result = scrape.scrape_contest_metadata(cmd.platform, cmd.contest)
if not metadata_result.success then if not metadata_result.success then
logger.log( logger.log(
"failed to load contest metadata: " .. (metadata_result.error or "unknown error"), 'failed to load contest metadata: ' .. (metadata_result.error or 'unknown error'),
vim.log.levels.WARN vim.log.levels.WARN
) )
else else
logger.log( logger.log(
("loaded %d problems for %s %s"):format(#metadata_result.problems, cmd.platform, cmd.contest) ('loaded %d problems for %s %s'):format(
#metadata_result.problems,
cmd.platform,
cmd.contest
)
) )
end end
end end
@ -585,7 +598,7 @@ function M.handle_command(opts)
return return
end end
if cmd.type == "full_setup" then if cmd.type == 'full_setup' then
if set_platform(cmd.platform) then if set_platform(cmd.platform) then
state.contest_id = cmd.contest state.contest_id = cmd.contest
local problem_ids = {} local problem_ids = {}
@ -595,14 +608,18 @@ function M.handle_command(opts)
local metadata_result = scrape.scrape_contest_metadata(cmd.platform, cmd.contest) local metadata_result = scrape.scrape_contest_metadata(cmd.platform, cmd.contest)
if not metadata_result.success then if not metadata_result.success then
logger.log( logger.log(
"failed to load contest metadata: " .. (metadata_result.error or "unknown error"), 'failed to load contest metadata: ' .. (metadata_result.error or 'unknown error'),
vim.log.levels.ERROR vim.log.levels.ERROR
) )
return return
end end
logger.log( logger.log(
("loaded %d problems for %s %s"):format(#metadata_result.problems, cmd.platform, cmd.contest) ('loaded %d problems for %s %s'):format(
#metadata_result.problems,
cmd.platform,
cmd.contest
)
) )
problem_ids = vim.tbl_map(function(prob) problem_ids = vim.tbl_map(function(prob)
return prob.id return prob.id
@ -632,13 +649,13 @@ function M.handle_command(opts)
return return
end end
if cmd.type == "cses_problem" then if cmd.type == 'cses_problem' then
if set_platform(cmd.platform) then if set_platform(cmd.platform) then
if vim.tbl_contains(config.scrapers, cmd.platform) then if vim.tbl_contains(config.scrapers, cmd.platform) then
local metadata_result = scrape.scrape_contest_metadata(cmd.platform, "") local metadata_result = scrape.scrape_contest_metadata(cmd.platform, '')
if not metadata_result.success then if not metadata_result.success then
logger.log( logger.log(
"failed to load contest metadata: " .. (metadata_result.error or "unknown error"), 'failed to load contest metadata: ' .. (metadata_result.error or 'unknown error'),
vim.log.levels.WARN vim.log.levels.WARN
) )
end end
@ -648,8 +665,8 @@ function M.handle_command(opts)
return return
end end
if cmd.type == "problem_switch" then if cmd.type == 'problem_switch' then
if state.platform == "cses" then if state.platform == 'cses' then
setup_problem(cmd.problem, nil, cmd.language) setup_problem(cmd.problem, nil, cmd.language)
else else
setup_problem(state.contest_id, cmd.problem, cmd.language) setup_problem(state.contest_id, cmd.problem, cmd.language)

View file

@ -9,7 +9,7 @@ end
function M.log(msg, level) function M.log(msg, level)
level = level or vim.log.levels.INFO level = level or vim.log.levels.INFO
if not config or config.debug or level >= vim.log.levels.WARN then if not config or config.debug or level >= vim.log.levels.WARN then
vim.notify(("[cp.nvim]: %s"):format(msg), level) vim.notify(('[cp.nvim]: %s'):format(msg), level)
end end
end end

View file

@ -19,11 +19,11 @@ local M = {}
---@return ProblemContext ---@return ProblemContext
function M.create_context(contest, contest_id, problem_id, config, language) function M.create_context(contest, contest_id, problem_id, config, language)
vim.validate({ vim.validate({
contest = { contest, "string" }, contest = { contest, 'string' },
contest_id = { contest_id, "string" }, contest_id = { contest_id, 'string' },
problem_id = { problem_id, { "string", "nil" }, true }, problem_id = { problem_id, { 'string', 'nil' }, true },
config = { config, "table" }, config = { config, 'table' },
language = { language, { "string", "nil" }, true }, language = { language, { 'string', 'nil' }, true },
}) })
local contest_config = config.contests[contest] local contest_config = config.contests[contest]
@ -37,29 +37,31 @@ function M.create_context(contest, contest_id, problem_id, config, language)
error(("No language config found for '%s' in contest '%s'"):format(target_language, contest)) error(("No language config found for '%s' in contest '%s'"):format(target_language, contest))
end end
if not language_config.extension then if not language_config.extension then
error(("No extension configured for language '%s' in contest '%s'"):format(target_language, contest)) error(
("No extension configured for language '%s' in contest '%s'"):format(target_language, contest)
)
end end
local base_name local base_name
if config.filename then if config.filename then
local source_file = config.filename(contest, contest_id, problem_id, config, language) local source_file = config.filename(contest, contest_id, problem_id, config, language)
base_name = vim.fn.fnamemodify(source_file, ":t:r") base_name = vim.fn.fnamemodify(source_file, ':t:r')
else else
local default_filename = require("cp.config").default_filename local default_filename = require('cp.config').default_filename
base_name = default_filename(contest_id, problem_id) base_name = default_filename(contest_id, problem_id)
end end
local source_file = base_name .. "." .. language_config.extension local source_file = base_name .. '.' .. language_config.extension
return { return {
contest = contest, contest = contest,
contest_id = contest_id, contest_id = contest_id,
problem_id = problem_id, problem_id = problem_id,
source_file = source_file, source_file = source_file,
binary_file = ("build/%s.run"):format(base_name), binary_file = ('build/%s.run'):format(base_name),
input_file = ("io/%s.cpin"):format(base_name), input_file = ('io/%s.cpin'):format(base_name),
output_file = ("io/%s.cpout"):format(base_name), output_file = ('io/%s.cpout'):format(base_name),
expected_file = ("io/%s.expected"):format(base_name), expected_file = ('io/%s.expected'):format(base_name),
problem_name = base_name, problem_name = base_name,
} }
end end

View file

@ -10,43 +10,43 @@
---@field error? string ---@field error? string
local M = {} local M = {}
local logger = require("cp.log") local cache = require('cp.cache')
local cache = require("cp.cache") local logger = require('cp.log')
local function get_plugin_path() local function get_plugin_path()
local plugin_path = debug.getinfo(1, "S").source:sub(2) local plugin_path = debug.getinfo(1, 'S').source:sub(2)
return vim.fn.fnamemodify(plugin_path, ":h:h:h") return vim.fn.fnamemodify(plugin_path, ':h:h:h')
end end
local function ensure_io_directory() local function ensure_io_directory()
vim.fn.mkdir("io", "p") vim.fn.mkdir('io', 'p')
end end
local function check_internet_connectivity() local function check_internet_connectivity()
local result = vim.system({ "ping", "-c", "1", "-W", "3", "8.8.8.8" }, { text = true }):wait() local result = vim.system({ 'ping', '-c', '1', '-W', '3', '8.8.8.8' }, { text = true }):wait()
return result.code == 0 return result.code == 0
end end
local function setup_python_env() local function setup_python_env()
local plugin_path = get_plugin_path() local plugin_path = get_plugin_path()
local venv_dir = plugin_path .. "/.venv" local venv_dir = plugin_path .. '/.venv'
if vim.fn.executable("uv") == 0 then if vim.fn.executable('uv') == 0 then
logger.log( logger.log(
"uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/", 'uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/',
vim.log.levels.WARN vim.log.levels.WARN
) )
return false return false
end end
if vim.fn.isdirectory(venv_dir) == 0 then if vim.fn.isdirectory(venv_dir) == 0 then
logger.log("setting up Python environment for scrapers...") logger.log('setting up Python environment for scrapers...')
local result = vim.system({ "uv", "sync" }, { cwd = plugin_path, text = true }):wait() local result = vim.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true }):wait()
if result.code ~= 0 then if result.code ~= 0 then
logger.log("failed to setup Python environment: " .. result.stderr, vim.log.levels.ERROR) logger.log('failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR)
return false return false
end end
logger.log("python environment setup complete") logger.log('python environment setup complete')
end end
return true return true
@ -57,8 +57,8 @@ end
---@return {success: boolean, problems?: table[], error?: string} ---@return {success: boolean, problems?: table[], error?: string}
function M.scrape_contest_metadata(platform, contest_id) function M.scrape_contest_metadata(platform, contest_id)
vim.validate({ vim.validate({
platform = { platform, "string" }, platform = { platform, 'string' },
contest_id = { contest_id, "string" }, contest_id = { contest_id, 'string' },
}) })
cache.load() cache.load()
@ -74,52 +74,54 @@ function M.scrape_contest_metadata(platform, contest_id)
if not check_internet_connectivity() then if not check_internet_connectivity() then
return { return {
success = false, success = false,
error = "No internet connection available", error = 'No internet connection available',
} }
end end
if not setup_python_env() then if not setup_python_env() then
return { return {
success = false, success = false,
error = "Python environment setup failed", error = 'Python environment setup failed',
} }
end end
local plugin_path = get_plugin_path() local plugin_path = get_plugin_path()
local scraper_path = plugin_path .. "/scrapers/" .. platform .. ".py" local scraper_path = plugin_path .. '/scrapers/' .. platform .. '.py'
local args local args
if platform == "cses" then if platform == 'cses' then
args = { args = {
"uv", 'uv',
"run", 'run',
"--directory", '--directory',
plugin_path, plugin_path,
scraper_path, scraper_path,
"metadata", 'metadata',
} }
else else
args = { args = {
"uv", 'uv',
"run", 'run',
"--directory", '--directory',
plugin_path, plugin_path,
scraper_path, scraper_path,
"metadata", 'metadata',
contest_id, contest_id,
} }
end end
local result = vim.system(args, { local result = vim
.system(args, {
cwd = plugin_path, cwd = plugin_path,
text = true, text = true,
timeout = 30000, timeout = 30000,
}):wait() })
:wait()
if result.code ~= 0 then if result.code ~= 0 then
return { return {
success = false, success = false,
error = "Failed to run metadata scraper: " .. (result.stderr or "Unknown error"), error = 'Failed to run metadata scraper: ' .. (result.stderr or 'Unknown error'),
} }
end end
@ -127,7 +129,7 @@ function M.scrape_contest_metadata(platform, contest_id)
if not ok then if not ok then
return { return {
success = false, success = false,
error = "Failed to parse metadata scraper output: " .. tostring(data), error = 'Failed to parse metadata scraper output: ' .. tostring(data),
} }
end end
@ -136,8 +138,8 @@ function M.scrape_contest_metadata(platform, contest_id)
end end
local problems_list local problems_list
if platform == "cses" then if platform == 'cses' then
problems_list = data.categories and data.categories["CSES Problem Set"] or {} problems_list = data.categories and data.categories['CSES Problem Set'] or {}
else else
problems_list = data.problems or {} problems_list = data.problems or {}
end end
@ -153,23 +155,23 @@ end
---@return {success: boolean, problem_id: string, test_count?: number, test_cases?: ScraperTestCase[], url?: string, error?: string} ---@return {success: boolean, problem_id: string, test_count?: number, test_cases?: ScraperTestCase[], url?: string, error?: string}
function M.scrape_problem(ctx) function M.scrape_problem(ctx)
vim.validate({ vim.validate({
ctx = { ctx, "table" }, ctx = { ctx, 'table' },
}) })
ensure_io_directory() ensure_io_directory()
if vim.fn.filereadable(ctx.input_file) == 1 and vim.fn.filereadable(ctx.expected_file) == 1 then if vim.fn.filereadable(ctx.input_file) == 1 and vim.fn.filereadable(ctx.expected_file) == 1 then
local base_name = vim.fn.fnamemodify(ctx.input_file, ":r") local base_name = vim.fn.fnamemodify(ctx.input_file, ':r')
local test_cases = {} local test_cases = {}
local i = 1 local i = 1
while true do while true do
local input_file = base_name .. "." .. i .. ".cpin" local input_file = base_name .. '.' .. i .. '.cpin'
local expected_file = base_name .. "." .. i .. ".cpout" local expected_file = base_name .. '.' .. i .. '.cpout'
if vim.fn.filereadable(input_file) == 1 and vim.fn.filereadable(expected_file) == 1 then if vim.fn.filereadable(input_file) == 1 and vim.fn.filereadable(expected_file) == 1 then
local input_content = table.concat(vim.fn.readfile(input_file), "\n") local input_content = table.concat(vim.fn.readfile(input_file), '\n')
local expected_content = table.concat(vim.fn.readfile(expected_file), "\n") local expected_content = table.concat(vim.fn.readfile(expected_file), '\n')
table.insert(test_cases, { table.insert(test_cases, {
index = i, index = i,
@ -194,7 +196,7 @@ function M.scrape_problem(ctx)
return { return {
success = false, success = false,
problem_id = ctx.problem_name, problem_id = ctx.problem_name,
error = "No internet connection available", error = 'No internet connection available',
} }
end end
@ -202,48 +204,50 @@ function M.scrape_problem(ctx)
return { return {
success = false, success = false,
problem_id = ctx.problem_name, problem_id = ctx.problem_name,
error = "Python environment setup failed", error = 'Python environment setup failed',
} }
end end
local plugin_path = get_plugin_path() local plugin_path = get_plugin_path()
local scraper_path = plugin_path .. "/scrapers/" .. ctx.contest .. ".py" local scraper_path = plugin_path .. '/scrapers/' .. ctx.contest .. '.py'
local args local args
if ctx.contest == "cses" then if ctx.contest == 'cses' then
args = { args = {
"uv", 'uv',
"run", 'run',
"--directory", '--directory',
plugin_path, plugin_path,
scraper_path, scraper_path,
"tests", 'tests',
ctx.contest_id, ctx.contest_id,
} }
else else
args = { args = {
"uv", 'uv',
"run", 'run',
"--directory", '--directory',
plugin_path, plugin_path,
scraper_path, scraper_path,
"tests", 'tests',
ctx.contest_id, ctx.contest_id,
ctx.problem_id, ctx.problem_id,
} }
end end
local result = vim.system(args, { local result = vim
.system(args, {
cwd = plugin_path, cwd = plugin_path,
text = true, text = true,
timeout = 30000, timeout = 30000,
}):wait() })
:wait()
if result.code ~= 0 then if result.code ~= 0 then
return { return {
success = false, success = false,
problem_id = ctx.problem_name, problem_id = ctx.problem_name,
error = "Failed to run tests scraper: " .. (result.stderr or "Unknown error"), error = 'Failed to run tests scraper: ' .. (result.stderr or 'Unknown error'),
} }
end end
@ -252,7 +256,7 @@ function M.scrape_problem(ctx)
return { return {
success = false, success = false,
problem_id = ctx.problem_name, problem_id = ctx.problem_name,
error = "Failed to parse tests scraper output: " .. tostring(data), error = 'Failed to parse tests scraper output: ' .. tostring(data),
} }
end end
@ -261,17 +265,17 @@ function M.scrape_problem(ctx)
end end
if data.tests and #data.tests > 0 then if data.tests and #data.tests > 0 then
local base_name = vim.fn.fnamemodify(ctx.input_file, ":r") local base_name = vim.fn.fnamemodify(ctx.input_file, ':r')
for i, test_case in ipairs(data.tests) do for i, test_case in ipairs(data.tests) do
local input_file = base_name .. "." .. i .. ".cpin" local input_file = base_name .. '.' .. i .. '.cpin'
local expected_file = base_name .. "." .. i .. ".cpout" local expected_file = base_name .. '.' .. i .. '.cpout'
local input_content = test_case.input:gsub("\r", "") local input_content = test_case.input:gsub('\r', '')
local expected_content = test_case.expected:gsub("\r", "") local expected_content = test_case.expected:gsub('\r', '')
vim.fn.writefile(vim.split(input_content, "\n", true), input_file) vim.fn.writefile(vim.split(input_content, '\n', true), input_file)
vim.fn.writefile(vim.split(expected_content, "\n", true), expected_file) vim.fn.writefile(vim.split(expected_content, '\n', true), expected_file)
end end
end end

View file

@ -1,16 +1,16 @@
local M = {} local M = {}
local logger = require("cp.log") 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 disabled', vim.log.levels.INFO)
return return
end end
local s, i, fmt = ls.snippet, ls.insert_node, require("luasnip.extras.fmt").fmt local s, i, fmt = ls.snippet, ls.insert_node, require('luasnip.extras.fmt').fmt
local constants = require("cp.constants") local constants = require('cp.constants')
local filetype_to_language = constants.filetype_to_language local filetype_to_language = constants.filetype_to_language
local language_to_filetype = {} local language_to_filetype = {}
@ -110,14 +110,14 @@ if __name__ == "__main__":
local filetype = constants.canonical_filetypes[language] local filetype = constants.canonical_filetypes[language]
for contest, template in pairs(template_set) do for contest, template in pairs(template_set) do
local prefixed_trigger = ("cp.nvim/%s.%s"):format(contest, language) local prefixed_trigger = ('cp.nvim/%s.%s'):format(contest, language)
if not user_overrides[prefixed_trigger] then if not user_overrides[prefixed_trigger] then
table.insert(snippets, s(prefixed_trigger, fmt(template, { i(1) }))) table.insert(snippets, s(prefixed_trigger, fmt(template, { i(1) })))
end end
end end
for trigger, snippet in pairs(user_overrides) do for trigger, snippet in pairs(user_overrides) do
local prefix_match = trigger:match("^cp%.nvim/[^.]+%.(.+)$") local prefix_match = trigger:match('^cp%.nvim/[^.]+%.(.+)$')
if prefix_match == language then if prefix_match == language then
table.insert(snippets, snippet) table.insert(snippets, snippet)
end end

View file

@ -21,8 +21,8 @@
---@field saved_layout table? ---@field saved_layout table?
local M = {} local M = {}
local logger = require("cp.log") local constants = require('cp.constants')
local constants = require("cp.constants") local logger = require('cp.log')
---@type TestPanelState ---@type TestPanelState
local test_panel_state = { local test_panel_state = {
@ -43,7 +43,7 @@ local function create_test_case(index, input, expected)
index = index, index = index,
input = input, input = input,
expected = expected, expected = expected,
status = "pending", status = 'pending',
actual = nil, actual = nil,
time_ms = nil, time_ms = nil,
error = nil, error = nil,
@ -56,7 +56,7 @@ end
---@param problem_id string? ---@param problem_id string?
---@return TestCase[] ---@return TestCase[]
local function parse_test_cases_from_cache(platform, contest_id, problem_id) local function parse_test_cases_from_cache(platform, contest_id, problem_id)
local cache = require("cp.cache") local cache = require('cp.cache')
cache.load() cache.load()
local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id) local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id)
@ -68,7 +68,7 @@ local function parse_test_cases_from_cache(platform, contest_id, problem_id)
for i, test_case in ipairs(cached_test_cases) do for i, test_case in ipairs(cached_test_cases) do
local index = test_case.index or i local index = test_case.index or i
local expected = test_case.expected or test_case.output or "" local expected = test_case.expected or test_case.output or ''
table.insert(test_cases, create_test_case(index, test_case.input, expected)) table.insert(test_cases, create_test_case(index, test_case.input, expected))
end end
@ -83,17 +83,20 @@ local function parse_test_cases_from_files(input_file, expected_file)
return {} return {}
end end
local base_name = vim.fn.fnamemodify(input_file, ":r") local base_name = vim.fn.fnamemodify(input_file, ':r')
local test_cases = {} local test_cases = {}
local i = 1 local i = 1
while true do while true do
local individual_input_file = base_name .. "." .. i .. ".cpin" local individual_input_file = base_name .. '.' .. i .. '.cpin'
local individual_expected_file = base_name .. "." .. i .. ".cpout" local individual_expected_file = base_name .. '.' .. i .. '.cpout'
if vim.fn.filereadable(individual_input_file) == 1 and vim.fn.filereadable(individual_expected_file) == 1 then if
local input_content = table.concat(vim.fn.readfile(individual_input_file), "\n") vim.fn.filereadable(individual_input_file) == 1
local expected_content = table.concat(vim.fn.readfile(individual_expected_file), "\n") and vim.fn.filereadable(individual_expected_file) == 1
then
local input_content = table.concat(vim.fn.readfile(individual_input_file), '\n')
local expected_content = table.concat(vim.fn.readfile(individual_expected_file), '\n')
table.insert(test_cases, create_test_case(i, input_content, expected_content)) table.insert(test_cases, create_test_case(i, input_content, expected_content))
i = i + 1 i = i + 1
@ -103,8 +106,8 @@ local function parse_test_cases_from_files(input_file, expected_file)
end end
if #test_cases == 0 then if #test_cases == 0 then
local input_content = table.concat(vim.fn.readfile(input_file), "\n") local input_content = table.concat(vim.fn.readfile(input_file), '\n')
local expected_content = table.concat(vim.fn.readfile(expected_file), "\n") local expected_content = table.concat(vim.fn.readfile(expected_file), '\n')
return { create_test_case(1, input_content, expected_content) } return { create_test_case(1, input_content, expected_content) }
end end
@ -116,15 +119,15 @@ end
---@param test_case TestCase ---@param test_case TestCase
---@return table ---@return table
local function run_single_test_case(ctx, contest_config, test_case) local function run_single_test_case(ctx, contest_config, test_case)
local language = vim.fn.fnamemodify(ctx.source_file, ":e") local language = vim.fn.fnamemodify(ctx.source_file, ':e')
local language_name = constants.filetype_to_language[language] or contest_config.default_language local language_name = constants.filetype_to_language[language] or contest_config.default_language
local language_config = contest_config[language_name] local language_config = contest_config[language_name]
if not language_config then if not language_config then
return { return {
status = "fail", status = 'fail',
actual = "", actual = '',
error = "No language configuration", error = 'No language configuration',
time_ms = 0, time_ms = 0,
} }
end end
@ -134,7 +137,7 @@ local function run_single_test_case(ctx, contest_config, test_case)
for _, arg in ipairs(cmd_template) do for _, arg in ipairs(cmd_template) do
local substituted = arg local substituted = arg
for key, value in pairs(substitutions) do for key, value in pairs(substitutions) do
substituted = substituted:gsub("{" .. key .. "}", value) substituted = substituted:gsub('{' .. key .. '}', value)
end end
table.insert(result, substituted) table.insert(result, substituted)
end end
@ -152,18 +155,18 @@ local function run_single_test_case(ctx, contest_config, test_case)
local substitutions = { local substitutions = {
source = ctx.source_file, source = ctx.source_file,
binary = ctx.binary_file, binary = ctx.binary_file,
version = tostring(language_config.version or ""), version = tostring(language_config.version or ''),
} }
if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then
logger.log("binary not found, compiling first...") logger.log('binary not found, compiling first...')
local compile_cmd = substitute_template(language_config.compile, substitutions) local compile_cmd = substitute_template(language_config.compile, substitutions)
local compile_result = vim.system(compile_cmd, { text = true }):wait() local compile_result = vim.system(compile_cmd, { text = true }):wait()
if compile_result.code ~= 0 then if compile_result.code ~= 0 then
return { return {
status = "fail", status = 'fail',
actual = "", actual = '',
error = "Compilation failed: " .. (compile_result.stderr or "Unknown error"), error = 'Compilation failed: ' .. (compile_result.stderr or 'Unknown error'),
time_ms = 0, time_ms = 0,
} }
end end
@ -171,28 +174,30 @@ local function run_single_test_case(ctx, contest_config, test_case)
local run_cmd = build_command(language_config.run, language_config.executable, substitutions) local run_cmd = build_command(language_config.run, language_config.executable, substitutions)
local stdin_content = test_case.input .. "\n" local stdin_content = test_case.input .. '\n'
local start_time = vim.uv.hrtime() local start_time = vim.uv.hrtime()
local result = vim.system(run_cmd, { local result = vim
.system(run_cmd, {
stdin = stdin_content, stdin = stdin_content,
timeout = contest_config.timeout_ms or 2000, timeout = contest_config.timeout_ms or 2000,
text = true, text = true,
}):wait() })
:wait()
local execution_time = (vim.uv.hrtime() - start_time) / 1000000 local execution_time = (vim.uv.hrtime() - start_time) / 1000000
local actual_output = (result.stdout or ""):gsub("\n$", "") local actual_output = (result.stdout or ''):gsub('\n$', '')
local expected_output = test_case.expected:gsub("\n$", "") local expected_output = test_case.expected:gsub('\n$', '')
local ok = actual_output == expected_output local ok = actual_output == expected_output
local status local status
local timed_out = result.code == 143 or result.code == 124 local timed_out = result.code == 143 or result.code == 124
if timed_out then if timed_out then
status = "timeout" status = 'timeout'
elseif result.code == 0 and ok then elseif result.code == 0 and ok then
status = "pass" status = 'pass'
else else
status = "fail" status = 'fail'
end end
local signal = nil local signal = nil
@ -225,7 +230,7 @@ function M.load_test_cases(ctx, state)
test_panel_state.test_cases = test_cases test_panel_state.test_cases = test_cases
test_panel_state.current_index = 1 test_panel_state.current_index = 1
logger.log(("loaded %d test case(s)"):format(#test_cases)) logger.log(('loaded %d test case(s)'):format(#test_cases))
return #test_cases > 0 return #test_cases > 0
end end
@ -239,8 +244,8 @@ function M.run_test_case(ctx, contest_config, index)
return false return false
end end
logger.log(("running test case %d"):format(index)) logger.log(('running test case %d'):format(index))
test_case.status = "running" test_case.status = 'running'
local result = run_single_test_case(ctx, contest_config, test_case) local result = run_single_test_case(ctx, contest_config, test_case)

View file

@ -1,25 +1,27 @@
local M = {} local M = {}
local function get_git_version() local function get_git_version()
local plugin_path = debug.getinfo(1, "S").source:sub(2) local plugin_path = debug.getinfo(1, 'S').source:sub(2)
local plugin_root = vim.fn.fnamemodify(plugin_path, ":h:h:h") local plugin_root = vim.fn.fnamemodify(plugin_path, ':h:h:h')
local result = vim.system({ "git", "describe", "--tags", "--always", "--dirty" }, { local result = vim
.system({ 'git', 'describe', '--tags', '--always', '--dirty' }, {
cwd = plugin_root, cwd = plugin_root,
text = true, text = true,
}):wait() })
:wait()
if result.code == 0 then if result.code == 0 then
return result.stdout:gsub("\n", "") return result.stdout:gsub('\n', '')
else else
return "unknown" return 'unknown'
end end
end end
local function parse_semver(version_string) local function parse_semver(version_string)
local semver = version_string:match("^v?(%d+%.%d+%.%d+)") local semver = version_string:match('^v?(%d+%.%d+%.%d+)')
if semver then if semver then
local major, minor, patch = semver:match("(%d+)%.(%d+)%.(%d+)") local major, minor, patch = semver:match('(%d+)%.(%d+)%.(%d+)')
return { return {
full = semver, full = semver,
major = tonumber(major), major = tonumber(major),

View file

@ -10,7 +10,7 @@
---@field height integer ---@field height integer
local M = {} local M = {}
local constants = require("cp.constants") local constants = require('cp.constants')
---@return WindowState ---@return WindowState
function M.save_layout() function M.save_layout()
@ -38,8 +38,8 @@ end
---@param tile_fn? fun(source_buf: integer, input_buf: integer, output_buf: integer) ---@param tile_fn? fun(source_buf: integer, input_buf: integer, output_buf: integer)
function M.restore_layout(state, tile_fn) function M.restore_layout(state, tile_fn)
vim.validate({ vim.validate({
state = { state, { "table", "nil" }, true }, state = { state, { 'table', 'nil' }, true },
tile_fn = { tile_fn, { "function", "nil" }, true }, tile_fn = { tile_fn, { 'function', 'nil' }, true },
}) })
if not state then if not state then
@ -48,32 +48,36 @@ function M.restore_layout(state, tile_fn)
vim.cmd.diffoff() vim.cmd.diffoff()
local problem_id = vim.fn.expand("%:t:r") local problem_id = vim.fn.expand('%:t:r')
if problem_id == "" then if problem_id == '' then
for win, win_state in pairs(state.windows) do for win, win_state in pairs(state.windows) do
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_buf_is_valid(win_state.bufnr) then if vim.api.nvim_win_is_valid(win) and vim.api.nvim_buf_is_valid(win_state.bufnr) then
local bufname = vim.api.nvim_buf_get_name(win_state.bufnr) local bufname = vim.api.nvim_buf_get_name(win_state.bufnr)
if not bufname:match("%.in$") and not bufname:match("%.out$") and not bufname:match("%.expected$") then if
problem_id = vim.fn.fnamemodify(bufname, ":t:r") not bufname:match('%.in$')
and not bufname:match('%.out$')
and not bufname:match('%.expected$')
then
problem_id = vim.fn.fnamemodify(bufname, ':t:r')
break break
end end
end end
end end
end end
if problem_id ~= "" then if problem_id ~= '' then
vim.cmd("silent only") vim.cmd('silent only')
local base_fp = vim.fn.getcwd() local base_fp = vim.fn.getcwd()
local input_file = ("%s/io/%s.in"):format(base_fp, problem_id) local input_file = ('%s/io/%s.in'):format(base_fp, problem_id)
local output_file = ("%s/io/%s.out"):format(base_fp, problem_id) local output_file = ('%s/io/%s.out'):format(base_fp, problem_id)
local source_files = vim.fn.glob(problem_id .. ".*") local source_files = vim.fn.glob(problem_id .. '.*')
local source_file local source_file
if source_files ~= "" then if source_files ~= '' then
local files = vim.split(source_files, "\n") local files = vim.split(source_files, '\n')
local valid_extensions = vim.tbl_keys(constants.filetype_to_language) local valid_extensions = vim.tbl_keys(constants.filetype_to_language)
for _, file in ipairs(files) do for _, file in ipairs(files) do
local ext = vim.fn.fnamemodify(file, ":e") local ext = vim.fn.fnamemodify(file, ':e')
if vim.tbl_contains(valid_extensions, ext) then if vim.tbl_contains(valid_extensions, ext) then
source_file = file source_file = file
break break
@ -119,20 +123,20 @@ end
---@param output_buf integer ---@param output_buf integer
local function default_tile(source_buf, input_buf, output_buf) local function default_tile(source_buf, input_buf, output_buf)
vim.validate({ vim.validate({
source_buf = { source_buf, "number" }, source_buf = { source_buf, 'number' },
input_buf = { input_buf, "number" }, input_buf = { input_buf, 'number' },
output_buf = { output_buf, "number" }, output_buf = { output_buf, 'number' },
}) })
vim.api.nvim_set_current_buf(source_buf) vim.api.nvim_set_current_buf(source_buf)
vim.cmd.vsplit() vim.cmd.vsplit()
vim.api.nvim_set_current_buf(output_buf) vim.api.nvim_set_current_buf(output_buf)
vim.bo.filetype = "cp" vim.bo.filetype = 'cp'
vim.cmd(("vertical resize %d"):format(math.floor(vim.o.columns * 0.3))) vim.cmd(('vertical resize %d'):format(math.floor(vim.o.columns * 0.3)))
vim.cmd.split() vim.cmd.split()
vim.api.nvim_set_current_buf(input_buf) vim.api.nvim_set_current_buf(input_buf)
vim.bo.filetype = "cp" vim.bo.filetype = 'cp'
vim.cmd.wincmd("h") vim.cmd.wincmd('h')
end end
M.default_tile = default_tile M.default_tile = default_tile

View file

@ -3,30 +3,30 @@ if vim.g.loaded_cp then
end end
vim.g.loaded_cp = 1 vim.g.loaded_cp = 1
local constants = require("cp.constants") local constants = require('cp.constants')
local platforms = constants.PLATFORMS local platforms = constants.PLATFORMS
local actions = constants.ACTIONS local actions = constants.ACTIONS
vim.api.nvim_create_user_command("CP", function(opts) vim.api.nvim_create_user_command('CP', function(opts)
local cp = require("cp") local cp = require('cp')
cp.handle_command(opts) cp.handle_command(opts)
end, { end, {
nargs = "*", nargs = '*',
desc = "Competitive programming helper", desc = 'Competitive programming helper',
complete = function(ArgLead, CmdLine, _) complete = function(ArgLead, CmdLine, _)
local args = vim.split(vim.trim(CmdLine), "%s+") local args = vim.split(vim.trim(CmdLine), '%s+')
local num_args = #args local num_args = #args
if CmdLine:sub(-1) == " " then if CmdLine:sub(-1) == ' ' then
num_args = num_args + 1 num_args = num_args + 1
end end
if num_args == 2 then if num_args == 2 then
local candidates = {} local candidates = {}
local cp = require("cp") local cp = require('cp')
local context = cp.get_current_context() local context = cp.get_current_context()
if context.platform and context.contest_id then if context.platform and context.contest_id then
vim.list_extend(candidates, actions) vim.list_extend(candidates, actions)
local cache = require("cp.cache") local cache = require('cp.cache')
cache.load() cache.load()
local contest_data = cache.get_contest_data(context.platform, context.contest_id) local contest_data = cache.get_contest_data(context.platform, context.contest_id)
if contest_data and contest_data.problems then if contest_data and contest_data.problems then
@ -42,7 +42,7 @@ end, {
end, candidates) end, candidates)
elseif num_args == 4 then elseif num_args == 4 then
if vim.tbl_contains(platforms, args[2]) then if vim.tbl_contains(platforms, args[2]) then
local cache = require("cp.cache") local cache = require('cp.cache')
cache.load() cache.load()
local contest_data = cache.get_contest_data(args[2], args[3]) local contest_data = cache.get_contest_data(args[2], args[3])
if contest_data and contest_data.problems then if contest_data and contest_data.problems then

View file

@ -1 +1 @@
std = "lua51+vim" std = "vim"

7
spec/plugin_spec.lua Normal file
View file

@ -0,0 +1,7 @@
local cp = require('cp')
describe('neovim plugin', function()
it('work as expect', function()
cp.setup()
end)
end)

8
stylua.toml Normal file
View file

@ -0,0 +1,8 @@
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferSingle"
call_parentheses = "Always"
[sort_requires]
enabled = true

View file

@ -1,6 +1,18 @@
[selene] [selene]
base = "lua52" base = "lua51"
name = "vim" name = "vim"
[vim] [vim]
any = true any = true
[jit]
any = true
[assert]
any = true
[describe]
any = true
[it]
any = true