fix(ci): format

This commit is contained in:
Barrett Ruth 2025-09-18 20:04:36 -04:00
parent 228b088bf7
commit bc315818e4
25 changed files with 1759 additions and 1660 deletions

3
.editorconfig Normal file
View file

@ -0,0 +1,3 @@
[*.lua]
indent_style = space
indent_size = 2

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

@ -0,0 +1,18 @@
name: Push to Luarocks
on:
push:
tags:
- "*"
pull_request:
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 }}

6
.stylua.toml Normal file
View file

@ -0,0 +1,6 @@
column_width = 100
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferSingle"
call_parentheses = "Always"

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'

View file

@ -9,4 +9,4 @@ test_dependencies = {
'lua >= 5.1', 'lua >= 5.1',
'nlua', 'nlua',
'busted >= 2.1.1', '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,129 +20,129 @@
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
end end
---@param contest_data ContestData ---@param contest_data ContestData
---@param platform string ---@param platform string
---@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
local expires_at = contest_data.expires_at local expires_at = contest_data.expires_at
if not expires_at then if not expires_at then
return false return false
end end
return os.time() < expires_at return os.time() < expires_at
end end
function M.load() function M.load()
if vim.fn.filereadable(cache_file) == 0 then if vim.fn.filereadable(cache_file) == 0 then
cache_data = {} cache_data = {}
return return
end end
local content = vim.fn.readfile(cache_file) local content = vim.fn.readfile(cache_file)
if #content == 0 then if #content == 0 then
cache_data = {} cache_data = {}
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
cache_data = {} cache_data = {}
end end
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
---@param contest_id string ---@param contest_id string
---@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
return nil return nil
end end
local contest_data = cache_data[platform][contest_id] local contest_data = cache_data[platform][contest_id]
if not contest_data then if not contest_data then
return nil return nil
end end
if not is_cache_valid(contest_data, platform) then if not is_cache_valid(contest_data, platform) then
return nil return nil
end end
return contest_data return contest_data
end end
---@param platform string ---@param platform string
---@param contest_id string ---@param contest_id string
---@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
cache_data[platform] = {} cache_data[platform] = {}
end end
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),
} }
M.save() M.save()
end end
---@param platform string ---@param platform string
---@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
cache_data[platform][contest_id] = nil cache_data[platform][contest_id] = nil
M.save() M.save()
end end
end end
---@param platform string ---@param platform string
@ -150,17 +150,17 @@ end
---@param problem_id? string ---@param problem_id? string
---@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
return cache_data[platform][problem_key].test_cases return cache_data[platform][problem_key].test_cases
end end
---@param platform string ---@param platform string
@ -168,24 +168,24 @@ end
---@param problem_id? string ---@param problem_id? string
---@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
if not cache_data[platform][problem_key] then if not cache_data[platform][problem_key] then
cache_data[platform][problem_key] = {} cache_data[platform][problem_key] = {}
end end
cache_data[platform][problem_key].test_cases = test_cases cache_data[platform][problem_key].test_cases = test_cases
cache_data[platform][problem_key].test_cases_cached_at = os.time() cache_data[platform][problem_key].test_cases_cached_at = os.time()
M.save() M.save()
end end
return M return M

View file

@ -48,115 +48,120 @@
---@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 = {
contests = {}, contests = {},
snippets = {}, snippets = {},
hooks = { hooks = {
before_run = nil, before_run = nil,
before_debug = nil, before_debug = nil,
setup_code = nil, setup_code = nil,
}, },
debug = false, debug = false,
scrapers = constants.PLATFORMS, scrapers = constants.PLATFORMS,
filename = nil, filename = nil,
} }
---@param user_config cp.UserConfig|nil ---@param user_config cp.UserConfig|nil
---@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,
}, },
}) })
end end
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(
then vim.tbl_keys(constants.filetype_to_language),
error( lang_config.extension
("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format( )
lang_config.extension, then
lang_name, error(
contest_name, ("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format(
table.concat(vim.tbl_keys(constants.filetype_to_language), ", ") lang_config.extension,
) lang_name,
) contest_name,
end table.concat(vim.tbl_keys(constants.filetype_to_language), ', ')
end )
end )
end end
end end
end
end
end
if user_config.scrapers then if user_config.scrapers then
for contest_name, enabled in pairs(user_config.scrapers) do for contest_name, enabled in pairs(user_config.scrapers) do
if not vim.tbl_contains(constants.PLATFORMS, contest_name) then if not vim.tbl_contains(constants.PLATFORMS, contest_name) then
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(
end ("Scraper setting for '%s' must be boolean, got %s"):format(contest_name, type(enabled))
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
---@param contest_id string ---@param contest_id string
---@param problem_id? string ---@param problem_id? string
---@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
return (contest_id .. problem_id):lower() return (contest_id .. problem_id):lower()
else else
return contest_id:lower() return contest_id:lower()
end end
end end
M.default_filename = default_filename M.default_filename = default_filename

View file

@ -1,43 +1,43 @@
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 = {
cc = M.CPP, cc = M.CPP,
cxx = M.CPP, cxx = M.CPP,
cpp = M.CPP, cpp = M.CPP,
py = M.PYTHON, py = M.PYTHON,
py3 = M.PYTHON, py3 = M.PYTHON,
} }
---@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,44 +6,44 @@
---@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
---@param contest_config table ---@param contest_config table
---@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
---@param cmd_template string[] ---@param cmd_template string[]
---@param substitutions table<string, string> ---@param substitutions table<string, string>
---@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
return result return result
end end
---@param cmd_template string[] ---@param cmd_template string[]
@ -51,51 +51,54 @@ end
---@param substitutions table<string, string> ---@param substitutions table<string, string>
---@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)
if executable then if executable then
table.insert(cmd, 1, executable) table.insert(cmd, 1, executable)
end end
return cmd return cmd
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
---@param substitutions table<string, string> ---@param substitutions table<string, string>
---@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(
end ('compilation failed (%.1fms): %s'):format(compile_time, result.stderr),
vim.log.levels.WARN
)
end
return result return result
end end
---@param cmd string[] ---@param cmd string[]
@ -103,42 +106,47 @@ end
---@param timeout_ms integer ---@param timeout_ms integer
---@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
stdin = input_data, .system(cmd, {
timeout = timeout_ms, stdin = input_data,
text = true, timeout = timeout_ms,
}):wait() text = true,
})
: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
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(
else ('execution failed (exit code %d, %.1fms)'):format(actual_code, execution_time),
logger.log(("execution successful (%.1fms)"):format(execution_time)) vim.log.levels.WARN
end )
else
logger.log(('execution successful (%.1fms)'):format(execution_time))
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,
} }
end end
---@param exec_result ExecuteResult ---@param exec_result ExecuteResult
@ -146,49 +154,49 @@ end
---@param is_debug boolean ---@param is_debug boolean
---@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
local ok = #actual_lines == #expected_content local ok = #actual_lines == #expected_content
if ok then if ok then
for i, line in ipairs(actual_lines) do for i, line in ipairs(actual_lines) do
if line ~= expected_content[i] then if line ~= expected_content[i] then
ok = false ok = false
break break
end end
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
@ -196,89 +204,93 @@ end
---@param is_debug? boolean ---@param is_debug? boolean
---@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
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), 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
if compile_cmd then or language_config.compile
language_config.compile = compile_cmd if compile_cmd then
local compile_result = M.compile_generic(language_config, substitutions) language_config.compile = compile_cmd
if compile_result.code ~= 0 then local compile_result = M.compile_generic(language_config, substitutions)
logger.log("compilation failed: " .. (compile_result.stderr or "unknown error"), vim.log.levels.ERROR) if compile_result.code ~= 0 then
return false logger.log(
end 'compilation failed: ' .. (compile_result.stderr or 'unknown error'),
logger.log(("compilation successful (%s)"):format(is_debug and "debug mode" or "test mode")) vim.log.levels.ERROR
end )
return false
end
logger.log(('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'))
end
return true return true
end 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()
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
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
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), version = tostring(language_config.version),
} }
local compile_cmd = is_debug and language_config.debug or language_config.compile local compile_cmd = is_debug and language_config.debug or language_config.compile
if compile_cmd then if compile_cmd then
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
vim.fn.writefile({ compile_result.stderr }, ctx.output_file) vim.fn.writefile({ compile_result.stderr }, ctx.output_file)
return return
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)
local exec_result = execute_command(run_cmd, input_data, contest_config.timeout_ms) local exec_result = execute_command(run_cmd, input_data, contest_config.timeout_ms)
local formatted_output = format_output(exec_result, ctx.expected_file, is_debug) local formatted_output = format_output(exec_result, ctx.expected_file, 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
return M return M

View file

@ -1,95 +1,95 @@
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()
check_python_env() check_python_env()
check_scrapers() check_scrapers()
check_luasnip() check_luasnip()
check_config() check_config()
end end
return M return M

File diff suppressed because it is too large Load diff

View file

@ -3,14 +3,14 @@ local M = {}
local config = nil local config = nil
function M.set_config(user_config) function M.set_config(user_config)
config = user_config config = user_config
end 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
return M return M

View file

@ -18,50 +18,52 @@ local M = {}
---@param language? string ---@param language? string
---@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]
if not contest_config then if not contest_config then
error(("No contest config found for '%s'"):format(contest)) error(("No contest config found for '%s'"):format(contest))
end end
local target_language = language or contest_config.default_language local target_language = language or contest_config.default_language
local language_config = contest_config[target_language] local language_config = contest_config[target_language]
if not language_config then if not language_config then
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(
end ("No extension configured for language '%s' in contest '%s'"):format(target_language, contest)
)
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
return M return M

View file

@ -10,278 +10,282 @@
---@field error? string ---@field error? string
local M = {} local M = {}
local logger = require("cp.log") local logger = require('cp.log')
local cache = require("cp.cache") local cache = require('cp.cache')
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
end end
---@param platform string ---@param platform string
---@param contest_id string ---@param contest_id string
---@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()
local cached_data = cache.get_contest_data(platform, contest_id) local cached_data = cache.get_contest_data(platform, contest_id)
if cached_data then if cached_data then
return { return {
success = true, success = true,
problems = cached_data.problems, problems = cached_data.problems,
} }
end end
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
cwd = plugin_path, .system(args, {
text = true, cwd = plugin_path,
timeout = 30000, text = true,
}):wait() timeout = 30000,
})
: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
local ok, data = pcall(vim.json.decode, result.stdout) local ok, data = pcall(vim.json.decode, result.stdout)
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
if not data.success then if not data.success then
return data return data
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
cache.set_contest_data(platform, contest_id, problems_list) cache.set_contest_data(platform, contest_id, problems_list)
return { return {
success = true, success = true,
problems = problems_list, problems = problems_list,
} }
end end
---@param ctx ProblemContext ---@param ctx ProblemContext
---@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,
input = input_content, input = input_content,
output = expected_content, output = expected_content,
}) })
i = i + 1 i = i + 1
else else
break break
end end
end end
return { return {
success = true, success = true,
problem_id = ctx.problem_name, problem_id = ctx.problem_name,
test_count = #test_cases, test_count = #test_cases,
test_cases = test_cases, test_cases = test_cases,
} }
end end
if not check_internet_connectivity() then if not check_internet_connectivity() then
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
if not setup_python_env() then if not setup_python_env() then
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
cwd = plugin_path, .system(args, {
text = true, cwd = plugin_path,
timeout = 30000, text = true,
}):wait() timeout = 30000,
})
: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
local ok, data = pcall(vim.json.decode, result.stdout) local ok, data = pcall(vim.json.decode, result.stdout)
if not ok then if not ok then
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
if not data.success then if not data.success then
return data return data
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
return { return {
success = true, success = true,
problem_id = ctx.problem_name, problem_id = ctx.problem_name,
test_count = data.tests and #data.tests or 0, test_count = data.tests and #data.tests or 0,
test_cases = data.tests, test_cases = data.tests,
url = data.url, url = data.url,
} }
end end
return M return M

View file

@ -1,28 +1,28 @@
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 = {}
for ext, lang in pairs(filetype_to_language) do for ext, lang in pairs(filetype_to_language) do
if not language_to_filetype[lang] then if not language_to_filetype[lang] then
language_to_filetype[lang] = ext language_to_filetype[lang] = ext
end end
end end
local template_definitions = { local template_definitions = {
cpp = { cpp = {
codeforces = [[#include <bits/stdc++.h> codeforces = [[#include <bits/stdc++.h>
using namespace std; using namespace std;
@ -43,7 +43,7 @@ int main() {{
return 0; return 0;
}}]], }}]],
atcoder = [[#include <bits/stdc++.h> atcoder = [[#include <bits/stdc++.h>
using namespace std; using namespace std;
@ -68,7 +68,7 @@ int main() {{
return 0; return 0;
}}]], }}]],
cses = [[#include <bits/stdc++.h> cses = [[#include <bits/stdc++.h>
using namespace std; using namespace std;
@ -79,10 +79,10 @@ int main() {{
return 0; return 0;
}}]], }}]],
}, },
python = { python = {
codeforces = [[def solve(): codeforces = [[def solve():
{} {}
if __name__ == "__main__": if __name__ == "__main__":
@ -90,41 +90,41 @@ if __name__ == "__main__":
for _ in range(tc): for _ in range(tc):
solve()]], solve()]],
atcoder = [[def solve(): atcoder = [[def solve():
{} {}
if __name__ == "__main__": if __name__ == "__main__":
solve()]], solve()]],
cses = [[{}]], cses = [[{}]],
}, },
} }
local user_overrides = {} local user_overrides = {}
for _, snippet in ipairs(config.snippets or {}) do for _, snippet in ipairs(config.snippets or {}) do
user_overrides[snippet.trigger] = snippet user_overrides[snippet.trigger] = snippet
end end
for language, template_set in pairs(template_definitions) do for language, template_set in pairs(template_definitions) do
local snippets = {} local snippets = {}
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
end end
ls.add_snippets(filetype, snippets) ls.add_snippets(filetype, snippets)
end end
end end
return M return M

View file

@ -21,17 +21,17 @@
---@field saved_layout table? ---@field saved_layout table?
local M = {} local M = {}
local logger = require("cp.log") local logger = require('cp.log')
local constants = require("cp.constants") local constants = require('cp.constants')
---@type TestPanelState ---@type TestPanelState
local test_panel_state = { local test_panel_state = {
test_cases = {}, test_cases = {},
current_index = 1, current_index = 1,
buffer = nil, buffer = nil,
namespace = nil, namespace = nil,
is_active = false, is_active = false,
saved_layout = nil, saved_layout = nil,
} }
---@param index number ---@param index number
@ -39,16 +39,16 @@ local test_panel_state = {
---@param expected string ---@param expected string
---@return TestCase ---@return TestCase
local function create_test_case(index, input, expected) local function create_test_case(index, input, expected)
return { return {
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,
selected = true, selected = true,
} }
end end
---@param platform string ---@param platform string
@ -56,59 +56,62 @@ 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)
if not cached_test_cases or #cached_test_cases == 0 then if not cached_test_cases or #cached_test_cases == 0 then
return {} return {}
end end
local test_cases = {} local test_cases = {}
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
return test_cases return test_cases
end end
---@param input_file string ---@param input_file string
---@param expected_file string ---@param expected_file string
---@return TestCase[] ---@return TestCase[]
local function parse_test_cases_from_files(input_file, expected_file) local function parse_test_cases_from_files(input_file, expected_file)
if vim.fn.filereadable(input_file) == 0 or vim.fn.filereadable(expected_file) == 0 then if vim.fn.filereadable(input_file) == 0 or vim.fn.filereadable(expected_file) == 0 then
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
else else
break break
end end
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
return test_cases return test_cases
end end
---@param ctx ProblemContext ---@param ctx ProblemContext
@ -116,117 +119,119 @@ 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
local function substitute_template(cmd_template, substitutions) local function substitute_template(cmd_template, substitutions)
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
return result return result
end end
local function build_command(cmd_template, executable, substitutions) local function build_command(cmd_template, executable, substitutions)
local cmd = substitute_template(cmd_template, substitutions) local cmd = substitute_template(cmd_template, substitutions)
if executable then if executable then
table.insert(cmd, 1, executable) table.insert(cmd, 1, executable)
end end
return cmd return cmd
end end
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
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)
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
stdin = stdin_content, .system(run_cmd, {
timeout = contest_config.timeout_ms or 2000, stdin = stdin_content,
text = true, timeout = contest_config.timeout_ms or 2000,
}):wait() text = true,
local execution_time = (vim.uv.hrtime() - start_time) / 1000000 })
:wait()
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
if result.code >= 128 then if result.code >= 128 then
signal = constants.signal_codes[result.code] signal = constants.signal_codes[result.code]
end end
return { return {
status = status, status = status,
actual = actual_output, actual = actual_output,
error = result.code ~= 0 and result.stderr or nil, error = result.code ~= 0 and result.stderr or nil,
time_ms = execution_time, time_ms = execution_time,
code = result.code, code = result.code,
ok = ok, ok = ok,
signal = signal, signal = signal,
timed_out = timed_out, timed_out = timed_out,
} }
end end
---@param ctx ProblemContext ---@param ctx ProblemContext
---@param state table ---@param state table
---@return boolean ---@return boolean
function M.load_test_cases(ctx, state) function M.load_test_cases(ctx, state)
local test_cases = parse_test_cases_from_cache(state.platform, state.contest_id, state.problem_id) local test_cases = parse_test_cases_from_cache(state.platform, state.contest_id, state.problem_id)
if #test_cases == 0 then if #test_cases == 0 then
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file) test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
end end
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
---@param ctx ProblemContext ---@param ctx ProblemContext
@ -234,43 +239,43 @@ end
---@param index number ---@param index number
---@return boolean ---@return boolean
function M.run_test_case(ctx, contest_config, index) function M.run_test_case(ctx, contest_config, index)
local test_case = test_panel_state.test_cases[index] local test_case = test_panel_state.test_cases[index]
if not test_case then if not test_case then
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)
test_case.status = result.status test_case.status = result.status
test_case.actual = result.actual test_case.actual = result.actual
test_case.error = result.error test_case.error = result.error
test_case.time_ms = result.time_ms test_case.time_ms = result.time_ms
test_case.code = result.code test_case.code = result.code
test_case.ok = result.ok test_case.ok = result.ok
test_case.signal = result.signal test_case.signal = result.signal
test_case.timed_out = result.timed_out test_case.timed_out = result.timed_out
return true return true
end end
---@param ctx ProblemContext ---@param ctx ProblemContext
---@param contest_config ContestConfig ---@param contest_config ContestConfig
---@return TestCase[] ---@return TestCase[]
function M.run_all_test_cases(ctx, contest_config) function M.run_all_test_cases(ctx, contest_config)
local results = {} local results = {}
for i, _ in ipairs(test_panel_state.test_cases) do for i, _ in ipairs(test_panel_state.test_cases) do
M.run_test_case(ctx, contest_config, i) M.run_test_case(ctx, contest_config, i)
table.insert(results, test_panel_state.test_cases[i]) table.insert(results, test_panel_state.test_cases[i])
end end
return results return results
end end
---@return TestPanelState ---@return TestPanelState
function M.get_test_panel_state() function M.get_test_panel_state()
return test_panel_state return test_panel_state
end end
return M return M

View file

@ -1,33 +1,35 @@
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
cwd = plugin_root, .system({ 'git', 'describe', '--tags', '--always', '--dirty' }, {
text = true, cwd = plugin_root,
}):wait() text = true,
})
: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),
minor = tonumber(minor), minor = tonumber(minor),
patch = tonumber(patch), patch = tonumber(patch),
} }
end end
return nil return nil
end end
M.version = get_git_version() M.version = get_git_version()

View file

@ -10,129 +10,133 @@
---@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()
local windows = {} local windows = {}
for _, win in ipairs(vim.api.nvim_list_wins()) do for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_is_valid(win) then if vim.api.nvim_win_is_valid(win) then
local bufnr = vim.api.nvim_win_get_buf(win) local bufnr = vim.api.nvim_win_get_buf(win)
windows[win] = { windows[win] = {
bufnr = bufnr, bufnr = bufnr,
view = vim.fn.winsaveview(), view = vim.fn.winsaveview(),
width = vim.api.nvim_win_get_width(win), width = vim.api.nvim_win_get_width(win),
height = vim.api.nvim_win_get_height(win), height = vim.api.nvim_win_get_height(win),
} }
end end
end end
return { return {
windows = windows, windows = windows,
current_win = vim.api.nvim_get_current_win(), current_win = vim.api.nvim_get_current_win(),
layout = vim.fn.winrestcmd(), layout = vim.fn.winrestcmd(),
} }
end end
---@param state? WindowState ---@param state? WindowState
---@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
return return
end end
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$')
break and not bufname:match('%.out$')
end and not bufname:match('%.expected$')
end then
end problem_id = vim.fn.fnamemodify(bufname, ':t:r')
end break
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
end end
end end
source_file = source_file or files[1] source_file = source_file or files[1]
end end
if not source_file or vim.fn.filereadable(source_file) == 0 then if not source_file or vim.fn.filereadable(source_file) == 0 then
return return
end end
vim.cmd.edit(source_file) vim.cmd.edit(source_file)
local source_buf = vim.api.nvim_get_current_buf() local source_buf = vim.api.nvim_get_current_buf()
local input_buf = vim.fn.bufnr(input_file, true) local input_buf = vim.fn.bufnr(input_file, true)
local output_buf = vim.fn.bufnr(output_file, true) local output_buf = vim.fn.bufnr(output_file, true)
if tile_fn then if tile_fn then
tile_fn(source_buf, input_buf, output_buf) tile_fn(source_buf, input_buf, output_buf)
else else
M.default_tile(source_buf, input_buf, output_buf) M.default_tile(source_buf, input_buf, output_buf)
end end
else else
vim.cmd(state.layout) vim.cmd(state.layout)
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) then if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_set_current_win(win) vim.api.nvim_set_current_win(win)
if vim.api.nvim_get_current_buf() == win_state.bufnr then if vim.api.nvim_get_current_buf() == win_state.bufnr then
vim.fn.winrestview(win_state.view) vim.fn.winrestview(win_state.view)
end end
end end
end end
if vim.api.nvim_win_is_valid(state.current_win) then if vim.api.nvim_win_is_valid(state.current_win) then
vim.api.nvim_set_current_win(state.current_win) vim.api.nvim_set_current_win(state.current_win)
end end
end end
end end
---@param source_buf integer ---@param source_buf integer
---@param input_buf integer ---@param input_buf integer
---@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

@ -1,61 +1,61 @@
if vim.g.loaded_cp then if vim.g.loaded_cp then
return return
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
for _, problem in ipairs(contest_data.problems) do for _, problem in ipairs(contest_data.problems) do
table.insert(candidates, problem.id) table.insert(candidates, problem.id)
end end
end end
else else
vim.list_extend(candidates, platforms) vim.list_extend(candidates, platforms)
end end
return vim.tbl_filter(function(cmd) return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1 return cmd:find(ArgLead, 1, true) == 1
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
local candidates = {} local candidates = {}
for _, problem in ipairs(contest_data.problems) do for _, problem in ipairs(contest_data.problems) do
table.insert(candidates, problem.id) table.insert(candidates, problem.id)
end end
return vim.tbl_filter(function(cmd) return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1 return cmd:find(ArgLead, 1, true) == 1
end, candidates) end, candidates)
end end
end end
end end
return {} return {}
end, end,
}) })

View file

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