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.relativenumber = false
vim.opt_local.statuscolumn = ""
vim.opt_local.signcolumn = "no"
vim.opt_local.statuscolumn = ''
vim.opt_local.signcolumn = 'no'
vim.opt_local.wrap = true
vim.opt_local.linebreak = true

View file

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

View file

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

View file

@ -1,7 +1,7 @@
vim.opt_local.number = false
vim.opt_local.relativenumber = false
vim.opt_local.statuscolumn = ""
vim.opt_local.signcolumn = "no"
vim.opt_local.statuscolumn = ''
vim.opt_local.signcolumn = 'no'
vim.opt_local.wrap = 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',
'nlua',
'busted >= 2.1.1',
}
}

View file

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

View file

@ -20,129 +20,129 @@
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 = {}
---@param platform string
---@return number?
local function get_expiry_date(platform)
vim.validate({
platform = { platform, "string" },
})
vim.validate({
platform = { platform, 'string' },
})
if platform == "cses" then
return os.time() + (30 * 24 * 60 * 60)
end
return nil
if platform == 'cses' then
return os.time() + (30 * 24 * 60 * 60)
end
return nil
end
---@param contest_data ContestData
---@param platform string
---@return boolean
local function is_cache_valid(contest_data, platform)
vim.validate({
contest_data = { contest_data, "table" },
platform = { platform, "string" },
})
vim.validate({
contest_data = { contest_data, 'table' },
platform = { platform, 'string' },
})
if platform ~= "cses" then
return true
end
if platform ~= 'cses' then
return true
end
local expires_at = contest_data.expires_at
if not expires_at then
return false
end
local expires_at = contest_data.expires_at
if not expires_at then
return false
end
return os.time() < expires_at
return os.time() < expires_at
end
function M.load()
if vim.fn.filereadable(cache_file) == 0 then
cache_data = {}
return
end
if vim.fn.filereadable(cache_file) == 0 then
cache_data = {}
return
end
local content = vim.fn.readfile(cache_file)
if #content == 0 then
cache_data = {}
return
end
local content = vim.fn.readfile(cache_file)
if #content == 0 then
cache_data = {}
return
end
local ok, decoded = pcall(vim.json.decode, table.concat(content, "\n"))
if ok then
cache_data = decoded
else
cache_data = {}
end
local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n'))
if ok then
cache_data = decoded
else
cache_data = {}
end
end
function M.save()
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ":h"), "p")
local encoded = vim.json.encode(cache_data)
vim.fn.writefile(vim.split(encoded, "\n"), cache_file)
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
local encoded = vim.json.encode(cache_data)
vim.fn.writefile(vim.split(encoded, '\n'), cache_file)
end
---@param platform string
---@param contest_id string
---@return ContestData?
function M.get_contest_data(platform, contest_id)
vim.validate({
platform = { platform, "string" },
contest_id = { contest_id, "string" },
})
vim.validate({
platform = { platform, 'string' },
contest_id = { contest_id, 'string' },
})
if not cache_data[platform] then
return nil
end
if not cache_data[platform] then
return nil
end
local contest_data = cache_data[platform][contest_id]
if not contest_data then
return nil
end
local contest_data = cache_data[platform][contest_id]
if not contest_data then
return nil
end
if not is_cache_valid(contest_data, platform) then
return nil
end
if not is_cache_valid(contest_data, platform) then
return nil
end
return contest_data
return contest_data
end
---@param platform string
---@param contest_id string
---@param problems Problem[]
function M.set_contest_data(platform, contest_id, problems)
vim.validate({
platform = { platform, "string" },
contest_id = { contest_id, "string" },
problems = { problems, "table" },
})
vim.validate({
platform = { platform, 'string' },
contest_id = { contest_id, 'string' },
problems = { problems, 'table' },
})
if not cache_data[platform] then
cache_data[platform] = {}
end
if not cache_data[platform] then
cache_data[platform] = {}
end
cache_data[platform][contest_id] = {
problems = problems,
scraped_at = os.date("%Y-%m-%d"),
expires_at = get_expiry_date(platform),
}
cache_data[platform][contest_id] = {
problems = problems,
scraped_at = os.date('%Y-%m-%d'),
expires_at = get_expiry_date(platform),
}
M.save()
M.save()
end
---@param platform string
---@param contest_id string
function M.clear_contest_data(platform, contest_id)
vim.validate({
platform = { platform, "string" },
contest_id = { contest_id, "string" },
})
vim.validate({
platform = { platform, 'string' },
contest_id = { contest_id, 'string' },
})
if cache_data[platform] and cache_data[platform][contest_id] then
cache_data[platform][contest_id] = nil
M.save()
end
if cache_data[platform] and cache_data[platform][contest_id] then
cache_data[platform][contest_id] = nil
M.save()
end
end
---@param platform string
@ -150,17 +150,17 @@ end
---@param problem_id? string
---@return CachedTestCase[]?
function M.get_test_cases(platform, contest_id, problem_id)
vim.validate({
platform = { platform, "string" },
contest_id = { contest_id, "string" },
problem_id = { problem_id, { "string", "nil" }, true },
})
vim.validate({
platform = { platform, 'string' },
contest_id = { contest_id, 'string' },
problem_id = { problem_id, { 'string', 'nil' }, true },
})
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
return nil
end
return cache_data[platform][problem_key].test_cases
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
return nil
end
return cache_data[platform][problem_key].test_cases
end
---@param platform string
@ -168,24 +168,24 @@ end
---@param problem_id? string
---@param test_cases CachedTestCase[]
function M.set_test_cases(platform, contest_id, problem_id, test_cases)
vim.validate({
platform = { platform, "string" },
contest_id = { contest_id, "string" },
problem_id = { problem_id, { "string", "nil" }, true },
test_cases = { test_cases, "table" },
})
vim.validate({
platform = { platform, 'string' },
contest_id = { contest_id, 'string' },
problem_id = { problem_id, { 'string', 'nil' }, true },
test_cases = { test_cases, 'table' },
})
local problem_key = problem_id and (contest_id .. "_" .. problem_id) or contest_id
if not cache_data[platform] then
cache_data[platform] = {}
end
if not cache_data[platform][problem_key] then
cache_data[platform][problem_key] = {}
end
local problem_key = problem_id and (contest_id .. '_' .. problem_id) or contest_id
if not cache_data[platform] then
cache_data[platform] = {}
end
if not cache_data[platform][problem_key] then
cache_data[platform][problem_key] = {}
end
cache_data[platform][problem_key].test_cases = test_cases
cache_data[platform][problem_key].test_cases_cached_at = os.time()
M.save()
cache_data[platform][problem_key].test_cases = test_cases
cache_data[platform][problem_key].test_cases_cached_at = os.time()
M.save()
end
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
local M = {}
local constants = require("cp.constants")
local constants = require('cp.constants')
---@type cp.Config
M.defaults = {
contests = {},
snippets = {},
hooks = {
before_run = nil,
before_debug = nil,
setup_code = nil,
},
debug = false,
scrapers = constants.PLATFORMS,
filename = nil,
contests = {},
snippets = {},
hooks = {
before_run = nil,
before_debug = nil,
setup_code = nil,
},
debug = false,
scrapers = constants.PLATFORMS,
filename = nil,
}
---@param user_config cp.UserConfig|nil
---@return cp.Config
function M.setup(user_config)
vim.validate({
user_config = { user_config, { "table", "nil" }, true },
})
vim.validate({
user_config = { user_config, { 'table', 'nil' }, true },
})
if user_config then
vim.validate({
contests = { user_config.contests, { "table", "nil" }, true },
snippets = { user_config.snippets, { "table", "nil" }, true },
hooks = { user_config.hooks, { "table", "nil" }, true },
debug = { user_config.debug, { "boolean", "nil" }, true },
scrapers = { user_config.scrapers, { "table", "nil" }, true },
filename = { user_config.filename, { "function", "nil" }, true },
})
if user_config then
vim.validate({
contests = { user_config.contests, { 'table', 'nil' }, true },
snippets = { user_config.snippets, { 'table', 'nil' }, true },
hooks = { user_config.hooks, { 'table', 'nil' }, true },
debug = { user_config.debug, { 'boolean', 'nil' }, true },
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
filename = { user_config.filename, { 'function', 'nil' }, true },
})
if user_config.hooks then
vim.validate({
before_run = {
user_config.hooks.before_run,
{ "function", "nil" },
true,
},
before_debug = {
user_config.hooks.before_debug,
{ "function", "nil" },
true,
},
setup_code = {
user_config.hooks.setup_code,
{ "function", "nil" },
true,
},
})
end
if user_config.hooks then
vim.validate({
before_run = {
user_config.hooks.before_run,
{ 'function', 'nil' },
true,
},
before_debug = {
user_config.hooks.before_debug,
{ 'function', 'nil' },
true,
},
setup_code = {
user_config.hooks.setup_code,
{ 'function', 'nil' },
true,
},
})
end
if user_config.contests then
for contest_name, contest_config in pairs(user_config.contests) do
for lang_name, lang_config in pairs(contest_config) do
if type(lang_config) == "table" and lang_config.extension then
if
not vim.tbl_contains(vim.tbl_keys(constants.filetype_to_language), lang_config.extension)
then
error(
("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format(
lang_config.extension,
lang_name,
contest_name,
table.concat(vim.tbl_keys(constants.filetype_to_language), ", ")
)
)
end
end
end
end
end
if user_config.contests then
for contest_name, contest_config in pairs(user_config.contests) do
for lang_name, lang_config in pairs(contest_config) do
if type(lang_config) == 'table' and lang_config.extension then
if
not vim.tbl_contains(
vim.tbl_keys(constants.filetype_to_language),
lang_config.extension
)
then
error(
("Invalid extension '%s' for language '%s' in contest '%s'. Valid extensions: %s"):format(
lang_config.extension,
lang_name,
contest_name,
table.concat(vim.tbl_keys(constants.filetype_to_language), ', ')
)
)
end
end
end
end
end
if user_config.scrapers then
for contest_name, enabled in pairs(user_config.scrapers) do
if not vim.tbl_contains(constants.PLATFORMS, contest_name) then
error(
("Invalid contest '%s' in scrapers config. Valid contests: %s"):format(
contest_name,
table.concat(constants.PLATFORMS, ", ")
)
)
end
if type(enabled) ~= "boolean" then
error(("Scraper setting for '%s' must be boolean, got %s"):format(contest_name, type(enabled)))
end
end
end
end
if user_config.scrapers then
for contest_name, enabled in pairs(user_config.scrapers) do
if not vim.tbl_contains(constants.PLATFORMS, contest_name) then
error(
("Invalid contest '%s' in scrapers config. Valid contests: %s"):format(
contest_name,
table.concat(constants.PLATFORMS, ', ')
)
)
end
if type(enabled) ~= 'boolean' then
error(
("Scraper setting for '%s' must be boolean, got %s"):format(contest_name, type(enabled))
)
end
end
end
end
local config = vim.tbl_deep_extend("force", M.defaults, user_config or {})
return config
local config = vim.tbl_deep_extend('force', M.defaults, user_config or {})
return config
end
---@param contest_id string
---@param problem_id? string
---@return string
local function default_filename(contest_id, problem_id)
vim.validate({
contest_id = { contest_id, "string" },
problem_id = { problem_id, { "string", "nil" }, true },
})
vim.validate({
contest_id = { contest_id, 'string' },
problem_id = { problem_id, { 'string', 'nil' }, true },
})
if problem_id then
return (contest_id .. problem_id):lower()
else
return contest_id:lower()
end
if problem_id then
return (contest_id .. problem_id):lower()
else
return contest_id:lower()
end
end
M.default_filename = default_filename

View file

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

View file

@ -6,44 +6,44 @@
---@field timed_out boolean
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
---@param source_file string
---@param contest_config table
---@return string
local function get_language_from_file(source_file, contest_config)
vim.validate({
source_file = { source_file, "string" },
contest_config = { contest_config, "table" },
})
vim.validate({
source_file = { source_file, 'string' },
contest_config = { contest_config, 'table' },
})
local extension = vim.fn.fnamemodify(source_file, ":e")
local language = filetype_to_language[extension] or contest_config.default_language
logger.log(("detected language: %s (extension: %s)"):format(language, extension))
return language
local extension = vim.fn.fnamemodify(source_file, ':e')
local language = filetype_to_language[extension] or contest_config.default_language
logger.log(('detected language: %s (extension: %s)'):format(language, extension))
return language
end
---@param cmd_template string[]
---@param substitutions table<string, string>
---@return string[]
local function substitute_template(cmd_template, substitutions)
vim.validate({
cmd_template = { cmd_template, "table" },
substitutions = { substitutions, "table" },
})
vim.validate({
cmd_template = { cmd_template, 'table' },
substitutions = { substitutions, 'table' },
})
local result = {}
for _, arg in ipairs(cmd_template) do
local substituted = arg
for key, value in pairs(substitutions) do
substituted = substituted:gsub("{" .. key .. "}", value)
end
table.insert(result, substituted)
end
return result
local result = {}
for _, arg in ipairs(cmd_template) do
local substituted = arg
for key, value in pairs(substitutions) do
substituted = substituted:gsub('{' .. key .. '}', value)
end
table.insert(result, substituted)
end
return result
end
---@param cmd_template string[]
@ -51,51 +51,54 @@ end
---@param substitutions table<string, string>
---@return string[]
local function build_command(cmd_template, executable, substitutions)
vim.validate({
cmd_template = { cmd_template, "table" },
executable = { executable, { "string", "nil" }, true },
substitutions = { substitutions, "table" },
})
vim.validate({
cmd_template = { cmd_template, 'table' },
executable = { executable, { 'string', 'nil' }, true },
substitutions = { substitutions, 'table' },
})
local cmd = substitute_template(cmd_template, substitutions)
if executable then
table.insert(cmd, 1, executable)
end
return cmd
local cmd = substitute_template(cmd_template, substitutions)
if executable then
table.insert(cmd, 1, executable)
end
return cmd
end
local function ensure_directories()
vim.system({ "mkdir", "-p", "build", "io" }):wait()
vim.system({ 'mkdir', '-p', 'build', 'io' }):wait()
end
---@param language_config table
---@param substitutions table<string, string>
---@return {code: integer, stderr: string}
function M.compile_generic(language_config, substitutions)
vim.validate({
language_config = { language_config, "table" },
substitutions = { substitutions, "table" },
})
vim.validate({
language_config = { language_config, 'table' },
substitutions = { substitutions, 'table' },
})
if not language_config.compile then
logger.log("no compilation step required")
return { code = 0, stderr = "" }
end
if not language_config.compile then
logger.log('no compilation step required')
return { code = 0, stderr = '' }
end
local compile_cmd = substitute_template(language_config.compile, substitutions)
logger.log(("compiling: %s"):format(table.concat(compile_cmd, " ")))
local compile_cmd = substitute_template(language_config.compile, substitutions)
logger.log(('compiling: %s'):format(table.concat(compile_cmd, ' ')))
local start_time = vim.uv.hrtime()
local result = vim.system(compile_cmd, { text = true }):wait()
local compile_time = (vim.uv.hrtime() - start_time) / 1000000
local start_time = vim.uv.hrtime()
local result = vim.system(compile_cmd, { text = true }):wait()
local compile_time = (vim.uv.hrtime() - start_time) / 1000000
if result.code == 0 then
logger.log(("compilation successful (%.1fms)"):format(compile_time))
else
logger.log(("compilation failed (%.1fms): %s"):format(compile_time, result.stderr), vim.log.levels.WARN)
end
if result.code == 0 then
logger.log(('compilation successful (%.1fms)'):format(compile_time))
else
logger.log(
('compilation failed (%.1fms): %s'):format(compile_time, result.stderr),
vim.log.levels.WARN
)
end
return result
return result
end
---@param cmd string[]
@ -103,42 +106,47 @@ end
---@param timeout_ms integer
---@return ExecuteResult
local function execute_command(cmd, input_data, timeout_ms)
vim.validate({
cmd = { cmd, "table" },
input_data = { input_data, "string" },
timeout_ms = { timeout_ms, "number" },
})
vim.validate({
cmd = { cmd, 'table' },
input_data = { input_data, 'string' },
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, {
stdin = input_data,
timeout = timeout_ms,
text = true,
}):wait()
local result = vim
.system(cmd, {
stdin = input_data,
timeout = timeout_ms,
text = true,
})
:wait()
local end_time = vim.uv.hrtime()
local execution_time = (end_time - start_time) / 1000000
local end_time = vim.uv.hrtime()
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
logger.log(("execution timed out after %.1fms"):format(execution_time), vim.log.levels.WARN)
elseif actual_code ~= 0 then
logger.log(("execution failed (exit code %d, %.1fms)"):format(actual_code, execution_time), vim.log.levels.WARN)
else
logger.log(("execution successful (%.1fms)"):format(execution_time))
end
if result.code == 124 then
logger.log(('execution timed out after %.1fms'):format(execution_time), vim.log.levels.WARN)
elseif actual_code ~= 0 then
logger.log(
('execution failed (exit code %d, %.1fms)'):format(actual_code, execution_time),
vim.log.levels.WARN
)
else
logger.log(('execution successful (%.1fms)'):format(execution_time))
end
return {
stdout = result.stdout or "",
stderr = result.stderr or "",
code = actual_code,
time_ms = execution_time,
timed_out = result.code == 124,
}
return {
stdout = result.stdout or '',
stderr = result.stderr or '',
code = actual_code,
time_ms = execution_time,
timed_out = result.code == 124,
}
end
---@param exec_result ExecuteResult
@ -146,49 +154,49 @@ end
---@param is_debug boolean
---@return string
local function format_output(exec_result, expected_file, is_debug)
vim.validate({
exec_result = { exec_result, "table" },
expected_file = { expected_file, "string" },
is_debug = { is_debug, "boolean" },
})
vim.validate({
exec_result = { exec_result, 'table' },
expected_file = { expected_file, 'string' },
is_debug = { is_debug, 'boolean' },
})
local output_lines = { exec_result.stdout }
local metadata_lines = {}
local output_lines = { exec_result.stdout }
local metadata_lines = {}
if exec_result.timed_out then
table.insert(metadata_lines, "[code]: 124 (TIMEOUT)")
elseif exec_result.code >= 128 then
local signal_name = constants.signal_codes[exec_result.code] or "SIGNAL"
table.insert(metadata_lines, ("[code]: %d (%s)"):format(exec_result.code, signal_name))
else
table.insert(metadata_lines, ("[code]: %d"):format(exec_result.code))
end
if exec_result.timed_out then
table.insert(metadata_lines, '[code]: 124 (TIMEOUT)')
elseif exec_result.code >= 128 then
local signal_name = constants.signal_codes[exec_result.code] or 'SIGNAL'
table.insert(metadata_lines, ('[code]: %d (%s)'):format(exec_result.code, signal_name))
else
table.insert(metadata_lines, ('[code]: %d'):format(exec_result.code))
end
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, ('[time]: %.2f ms'):format(exec_result.time_ms))
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
local expected_content = vim.fn.readfile(expected_file)
local actual_lines = vim.split(exec_result.stdout, "\n")
if vim.fn.filereadable(expected_file) == 1 and exec_result.code == 0 then
local expected_content = vim.fn.readfile(expected_file)
local actual_lines = vim.split(exec_result.stdout, '\n')
while #actual_lines > 0 and actual_lines[#actual_lines] == "" do
table.remove(actual_lines)
end
while #actual_lines > 0 and actual_lines[#actual_lines] == '' do
table.remove(actual_lines)
end
local ok = #actual_lines == #expected_content
if ok then
for i, line in ipairs(actual_lines) do
if line ~= expected_content[i] then
ok = false
break
end
end
end
local ok = #actual_lines == #expected_content
if ok then
for i, line in ipairs(actual_lines) do
if line ~= expected_content[i] then
ok = false
break
end
end
end
table.insert(metadata_lines, ("[ok]: %s"):format(ok and "true" or "false"))
end
table.insert(metadata_lines, ('[ok]: %s'):format(ok and 'true' or 'false'))
end
return table.concat(output_lines, "") .. "\n" .. table.concat(metadata_lines, "\n")
return table.concat(output_lines, '') .. '\n' .. table.concat(metadata_lines, '\n')
end
---@param ctx ProblemContext
@ -196,89 +204,93 @@ end
---@param is_debug? boolean
---@return boolean success
function M.compile_problem(ctx, contest_config, is_debug)
vim.validate({
ctx = { ctx, "table" },
contest_config = { contest_config, "table" },
})
vim.validate({
ctx = { ctx, 'table' },
contest_config = { contest_config, 'table' },
})
local language = get_language_from_file(ctx.source_file, contest_config)
local language_config = contest_config[language]
local language = get_language_from_file(ctx.source_file, contest_config)
local language_config = contest_config[language]
if not language_config then
logger.log("No configuration for language: " .. language, vim.log.levels.ERROR)
return false
end
if not language_config then
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
return false
end
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version),
}
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version),
}
local compile_cmd = (is_debug and language_config.debug) and language_config.debug or language_config.compile
if compile_cmd then
language_config.compile = compile_cmd
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
logger.log("compilation failed: " .. (compile_result.stderr or "unknown error"), vim.log.levels.ERROR)
return false
end
logger.log(("compilation successful (%s)"):format(is_debug and "debug mode" or "test mode"))
end
local compile_cmd = (is_debug and language_config.debug) and language_config.debug
or language_config.compile
if compile_cmd then
language_config.compile = compile_cmd
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
logger.log(
'compilation failed: ' .. (compile_result.stderr or 'unknown error'),
vim.log.levels.ERROR
)
return false
end
logger.log(('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'))
end
return true
return true
end
function M.run_problem(ctx, contest_config, is_debug)
vim.validate({
ctx = { ctx, "table" },
contest_config = { contest_config, "table" },
is_debug = { is_debug, "boolean" },
})
vim.validate({
ctx = { ctx, 'table' },
contest_config = { contest_config, 'table' },
is_debug = { is_debug, 'boolean' },
})
ensure_directories()
ensure_directories()
local language = get_language_from_file(ctx.source_file, contest_config)
local language_config = contest_config[language]
local language = get_language_from_file(ctx.source_file, contest_config)
local language_config = contest_config[language]
if not language_config then
vim.fn.writefile({ "Error: No configuration for language: " .. language }, ctx.output_file)
return
end
if not language_config then
vim.fn.writefile({ 'Error: No configuration for language: ' .. language }, ctx.output_file)
return
end
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version),
}
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version),
}
local compile_cmd = is_debug and language_config.debug or language_config.compile
if compile_cmd then
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
vim.fn.writefile({ compile_result.stderr }, ctx.output_file)
return
end
end
local compile_cmd = is_debug and language_config.debug or language_config.compile
if compile_cmd then
local compile_result = M.compile_generic(language_config, substitutions)
if compile_result.code ~= 0 then
vim.fn.writefile({ compile_result.stderr }, ctx.output_file)
return
end
end
local input_data = ""
if vim.fn.filereadable(ctx.input_file) == 1 then
input_data = table.concat(vim.fn.readfile(ctx.input_file), "\n") .. "\n"
end
local input_data = ''
if vim.fn.filereadable(ctx.input_file) == 1 then
input_data = table.concat(vim.fn.readfile(ctx.input_file), '\n') .. '\n'
end
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 formatted_output = format_output(exec_result, ctx.expected_file, is_debug)
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 formatted_output = format_output(exec_result, ctx.expected_file, is_debug)
local output_buf = vim.fn.bufnr(ctx.output_file)
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_call(output_buf, function()
vim.cmd.write()
end)
else
vim.fn.writefile(vim.split(formatted_output, "\n"), ctx.output_file)
end
local output_buf = vim.fn.bufnr(ctx.output_file)
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_call(output_buf, function()
vim.cmd.write()
end)
else
vim.fn.writefile(vim.split(formatted_output, '\n'), ctx.output_file)
end
end
return M

View file

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

File diff suppressed because it is too large Load diff

View file

@ -3,14 +3,14 @@ local M = {}
local config = nil
function M.set_config(user_config)
config = user_config
config = user_config
end
function M.log(msg, level)
level = level or vim.log.levels.INFO
if not config or config.debug or level >= vim.log.levels.WARN then
vim.notify(("[cp.nvim]: %s"):format(msg), level)
end
level = level or vim.log.levels.INFO
if not config or config.debug or level >= vim.log.levels.WARN then
vim.notify(('[cp.nvim]: %s'):format(msg), level)
end
end
return M

View file

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

View file

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

View file

@ -1,28 +1,28 @@
local M = {}
local logger = require("cp.log")
local logger = require('cp.log')
function M.setup(config)
local ok, ls = pcall(require, "luasnip")
if not ok then
logger.log("LuaSnip not available - snippets disabled", vim.log.levels.INFO)
return
end
local ok, ls = pcall(require, 'luasnip')
if not ok then
logger.log('LuaSnip not available - snippets disabled', vim.log.levels.INFO)
return
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 filetype_to_language = constants.filetype_to_language
local constants = require('cp.constants')
local filetype_to_language = constants.filetype_to_language
local language_to_filetype = {}
for ext, lang in pairs(filetype_to_language) do
if not language_to_filetype[lang] then
language_to_filetype[lang] = ext
end
end
local language_to_filetype = {}
for ext, lang in pairs(filetype_to_language) do
if not language_to_filetype[lang] then
language_to_filetype[lang] = ext
end
end
local template_definitions = {
cpp = {
codeforces = [[#include <bits/stdc++.h>
local template_definitions = {
cpp = {
codeforces = [[#include <bits/stdc++.h>
using namespace std;
@ -43,7 +43,7 @@ int main() {{
return 0;
}}]],
atcoder = [[#include <bits/stdc++.h>
atcoder = [[#include <bits/stdc++.h>
using namespace std;
@ -68,7 +68,7 @@ int main() {{
return 0;
}}]],
cses = [[#include <bits/stdc++.h>
cses = [[#include <bits/stdc++.h>
using namespace std;
@ -79,10 +79,10 @@ int main() {{
return 0;
}}]],
},
},
python = {
codeforces = [[def solve():
python = {
codeforces = [[def solve():
{}
if __name__ == "__main__":
@ -90,41 +90,41 @@ if __name__ == "__main__":
for _ in range(tc):
solve()]],
atcoder = [[def solve():
atcoder = [[def solve():
{}
if __name__ == "__main__":
solve()]],
cses = [[{}]],
},
}
cses = [[{}]],
},
}
local user_overrides = {}
for _, snippet in ipairs(config.snippets or {}) do
user_overrides[snippet.trigger] = snippet
end
local user_overrides = {}
for _, snippet in ipairs(config.snippets or {}) do
user_overrides[snippet.trigger] = snippet
end
for language, template_set in pairs(template_definitions) do
local snippets = {}
local filetype = constants.canonical_filetypes[language]
for language, template_set in pairs(template_definitions) do
local snippets = {}
local filetype = constants.canonical_filetypes[language]
for contest, template in pairs(template_set) do
local prefixed_trigger = ("cp.nvim/%s.%s"):format(contest, language)
if not user_overrides[prefixed_trigger] then
table.insert(snippets, s(prefixed_trigger, fmt(template, { i(1) })))
end
end
for contest, template in pairs(template_set) do
local prefixed_trigger = ('cp.nvim/%s.%s'):format(contest, language)
if not user_overrides[prefixed_trigger] then
table.insert(snippets, s(prefixed_trigger, fmt(template, { i(1) })))
end
end
for trigger, snippet in pairs(user_overrides) do
local prefix_match = trigger:match("^cp%.nvim/[^.]+%.(.+)$")
if prefix_match == language then
table.insert(snippets, snippet)
end
end
for trigger, snippet in pairs(user_overrides) do
local prefix_match = trigger:match('^cp%.nvim/[^.]+%.(.+)$')
if prefix_match == language then
table.insert(snippets, snippet)
end
end
ls.add_snippets(filetype, snippets)
end
ls.add_snippets(filetype, snippets)
end
end
return M

View file

@ -21,17 +21,17 @@
---@field saved_layout table?
local M = {}
local logger = require("cp.log")
local constants = require("cp.constants")
local logger = require('cp.log')
local constants = require('cp.constants')
---@type TestPanelState
local test_panel_state = {
test_cases = {},
current_index = 1,
buffer = nil,
namespace = nil,
is_active = false,
saved_layout = nil,
test_cases = {},
current_index = 1,
buffer = nil,
namespace = nil,
is_active = false,
saved_layout = nil,
}
---@param index number
@ -39,16 +39,16 @@ local test_panel_state = {
---@param expected string
---@return TestCase
local function create_test_case(index, input, expected)
return {
index = index,
input = input,
expected = expected,
status = "pending",
actual = nil,
time_ms = nil,
error = nil,
selected = true,
}
return {
index = index,
input = input,
expected = expected,
status = 'pending',
actual = nil,
time_ms = nil,
error = nil,
selected = true,
}
end
---@param platform string
@ -56,59 +56,62 @@ end
---@param problem_id string?
---@return TestCase[]
local function parse_test_cases_from_cache(platform, contest_id, problem_id)
local cache = require("cp.cache")
cache.load()
local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id)
local cache = require('cp.cache')
cache.load()
local cached_test_cases = cache.get_test_cases(platform, contest_id, problem_id)
if not cached_test_cases or #cached_test_cases == 0 then
return {}
end
if not cached_test_cases or #cached_test_cases == 0 then
return {}
end
local test_cases = {}
local test_cases = {}
for i, test_case in ipairs(cached_test_cases) do
local index = test_case.index or i
local expected = test_case.expected or test_case.output or ""
table.insert(test_cases, create_test_case(index, test_case.input, expected))
end
for i, test_case in ipairs(cached_test_cases) do
local index = test_case.index or i
local expected = test_case.expected or test_case.output or ''
table.insert(test_cases, create_test_case(index, test_case.input, expected))
end
return test_cases
return test_cases
end
---@param input_file string
---@param expected_file string
---@return TestCase[]
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
return {}
end
if vim.fn.filereadable(input_file) == 0 or vim.fn.filereadable(expected_file) == 0 then
return {}
end
local base_name = vim.fn.fnamemodify(input_file, ":r")
local test_cases = {}
local i = 1
local base_name = vim.fn.fnamemodify(input_file, ':r')
local test_cases = {}
local i = 1
while true do
local individual_input_file = base_name .. "." .. i .. ".cpin"
local individual_expected_file = base_name .. "." .. i .. ".cpout"
while true do
local individual_input_file = base_name .. '.' .. i .. '.cpin'
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
local input_content = table.concat(vim.fn.readfile(individual_input_file), "\n")
local expected_content = table.concat(vim.fn.readfile(individual_expected_file), "\n")
if
vim.fn.filereadable(individual_input_file) == 1
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))
i = i + 1
else
break
end
end
table.insert(test_cases, create_test_case(i, input_content, expected_content))
i = i + 1
else
break
end
end
if #test_cases == 0 then
local input_content = table.concat(vim.fn.readfile(input_file), "\n")
local expected_content = table.concat(vim.fn.readfile(expected_file), "\n")
return { create_test_case(1, input_content, expected_content) }
end
if #test_cases == 0 then
local input_content = table.concat(vim.fn.readfile(input_file), '\n')
local expected_content = table.concat(vim.fn.readfile(expected_file), '\n')
return { create_test_case(1, input_content, expected_content) }
end
return test_cases
return test_cases
end
---@param ctx ProblemContext
@ -116,117 +119,119 @@ end
---@param test_case TestCase
---@return table
local function run_single_test_case(ctx, contest_config, test_case)
local language = vim.fn.fnamemodify(ctx.source_file, ":e")
local language_name = constants.filetype_to_language[language] or contest_config.default_language
local language_config = contest_config[language_name]
local language = vim.fn.fnamemodify(ctx.source_file, ':e')
local language_name = constants.filetype_to_language[language] or contest_config.default_language
local language_config = contest_config[language_name]
if not language_config then
return {
status = "fail",
actual = "",
error = "No language configuration",
time_ms = 0,
}
end
if not language_config then
return {
status = 'fail',
actual = '',
error = 'No language configuration',
time_ms = 0,
}
end
local function substitute_template(cmd_template, substitutions)
local result = {}
for _, arg in ipairs(cmd_template) do
local substituted = arg
for key, value in pairs(substitutions) do
substituted = substituted:gsub("{" .. key .. "}", value)
end
table.insert(result, substituted)
end
return result
end
local function substitute_template(cmd_template, substitutions)
local result = {}
for _, arg in ipairs(cmd_template) do
local substituted = arg
for key, value in pairs(substitutions) do
substituted = substituted:gsub('{' .. key .. '}', value)
end
table.insert(result, substituted)
end
return result
end
local function build_command(cmd_template, executable, substitutions)
local cmd = substitute_template(cmd_template, substitutions)
if executable then
table.insert(cmd, 1, executable)
end
return cmd
end
local function build_command(cmd_template, executable, substitutions)
local cmd = substitute_template(cmd_template, substitutions)
if executable then
table.insert(cmd, 1, executable)
end
return cmd
end
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version or ""),
}
local substitutions = {
source = ctx.source_file,
binary = ctx.binary_file,
version = tostring(language_config.version or ''),
}
if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then
logger.log("binary not found, compiling first...")
local compile_cmd = substitute_template(language_config.compile, substitutions)
local compile_result = vim.system(compile_cmd, { text = true }):wait()
if compile_result.code ~= 0 then
return {
status = "fail",
actual = "",
error = "Compilation failed: " .. (compile_result.stderr or "Unknown error"),
time_ms = 0,
}
end
end
if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then
logger.log('binary not found, compiling first...')
local compile_cmd = substitute_template(language_config.compile, substitutions)
local compile_result = vim.system(compile_cmd, { text = true }):wait()
if compile_result.code ~= 0 then
return {
status = 'fail',
actual = '',
error = 'Compilation failed: ' .. (compile_result.stderr or 'Unknown error'),
time_ms = 0,
}
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 result = vim.system(run_cmd, {
stdin = stdin_content,
timeout = contest_config.timeout_ms or 2000,
text = true,
}):wait()
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
local start_time = vim.uv.hrtime()
local result = vim
.system(run_cmd, {
stdin = stdin_content,
timeout = contest_config.timeout_ms or 2000,
text = true,
})
:wait()
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
local actual_output = (result.stdout or ""):gsub("\n$", "")
local expected_output = test_case.expected:gsub("\n$", "")
local ok = actual_output == expected_output
local actual_output = (result.stdout or ''):gsub('\n$', '')
local expected_output = test_case.expected:gsub('\n$', '')
local ok = actual_output == expected_output
local status
local timed_out = result.code == 143 or result.code == 124
if timed_out then
status = "timeout"
elseif result.code == 0 and ok then
status = "pass"
else
status = "fail"
end
local status
local timed_out = result.code == 143 or result.code == 124
if timed_out then
status = 'timeout'
elseif result.code == 0 and ok then
status = 'pass'
else
status = 'fail'
end
local signal = nil
if result.code >= 128 then
signal = constants.signal_codes[result.code]
end
local signal = nil
if result.code >= 128 then
signal = constants.signal_codes[result.code]
end
return {
status = status,
actual = actual_output,
error = result.code ~= 0 and result.stderr or nil,
time_ms = execution_time,
code = result.code,
ok = ok,
signal = signal,
timed_out = timed_out,
}
return {
status = status,
actual = actual_output,
error = result.code ~= 0 and result.stderr or nil,
time_ms = execution_time,
code = result.code,
ok = ok,
signal = signal,
timed_out = timed_out,
}
end
---@param ctx ProblemContext
---@param state table
---@return boolean
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
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
end
if #test_cases == 0 then
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
end
test_panel_state.test_cases = test_cases
test_panel_state.current_index = 1
test_panel_state.test_cases = test_cases
test_panel_state.current_index = 1
logger.log(("loaded %d test case(s)"):format(#test_cases))
return #test_cases > 0
logger.log(('loaded %d test case(s)'):format(#test_cases))
return #test_cases > 0
end
---@param ctx ProblemContext
@ -234,43 +239,43 @@ end
---@param index number
---@return boolean
function M.run_test_case(ctx, contest_config, index)
local test_case = test_panel_state.test_cases[index]
if not test_case then
return false
end
local test_case = test_panel_state.test_cases[index]
if not test_case then
return false
end
logger.log(("running test case %d"):format(index))
test_case.status = "running"
logger.log(('running test case %d'):format(index))
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.actual = result.actual
test_case.error = result.error
test_case.time_ms = result.time_ms
test_case.code = result.code
test_case.ok = result.ok
test_case.signal = result.signal
test_case.timed_out = result.timed_out
test_case.status = result.status
test_case.actual = result.actual
test_case.error = result.error
test_case.time_ms = result.time_ms
test_case.code = result.code
test_case.ok = result.ok
test_case.signal = result.signal
test_case.timed_out = result.timed_out
return true
return true
end
---@param ctx ProblemContext
---@param contest_config ContestConfig
---@return TestCase[]
function M.run_all_test_cases(ctx, contest_config)
local results = {}
for i, _ in ipairs(test_panel_state.test_cases) do
M.run_test_case(ctx, contest_config, i)
table.insert(results, test_panel_state.test_cases[i])
end
return results
local results = {}
for i, _ in ipairs(test_panel_state.test_cases) do
M.run_test_case(ctx, contest_config, i)
table.insert(results, test_panel_state.test_cases[i])
end
return results
end
---@return TestPanelState
function M.get_test_panel_state()
return test_panel_state
return test_panel_state
end
return M

View file

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

View file

@ -10,129 +10,133 @@
---@field height integer
local M = {}
local constants = require("cp.constants")
local constants = require('cp.constants')
---@return WindowState
function M.save_layout()
local windows = {}
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_is_valid(win) then
local bufnr = vim.api.nvim_win_get_buf(win)
windows[win] = {
bufnr = bufnr,
view = vim.fn.winsaveview(),
width = vim.api.nvim_win_get_width(win),
height = vim.api.nvim_win_get_height(win),
}
end
end
local windows = {}
for _, win in ipairs(vim.api.nvim_list_wins()) do
if vim.api.nvim_win_is_valid(win) then
local bufnr = vim.api.nvim_win_get_buf(win)
windows[win] = {
bufnr = bufnr,
view = vim.fn.winsaveview(),
width = vim.api.nvim_win_get_width(win),
height = vim.api.nvim_win_get_height(win),
}
end
end
return {
windows = windows,
current_win = vim.api.nvim_get_current_win(),
layout = vim.fn.winrestcmd(),
}
return {
windows = windows,
current_win = vim.api.nvim_get_current_win(),
layout = vim.fn.winrestcmd(),
}
end
---@param state? WindowState
---@param tile_fn? fun(source_buf: integer, input_buf: integer, output_buf: integer)
function M.restore_layout(state, tile_fn)
vim.validate({
state = { state, { "table", "nil" }, true },
tile_fn = { tile_fn, { "function", "nil" }, true },
})
vim.validate({
state = { state, { 'table', 'nil' }, true },
tile_fn = { tile_fn, { 'function', 'nil' }, true },
})
if not state then
return
end
if not state then
return
end
vim.cmd.diffoff()
vim.cmd.diffoff()
local problem_id = vim.fn.expand("%:t:r")
if problem_id == "" then
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
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
problem_id = vim.fn.fnamemodify(bufname, ":t:r")
break
end
end
end
end
local problem_id = vim.fn.expand('%:t:r')
if problem_id == '' then
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
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
problem_id = vim.fn.fnamemodify(bufname, ':t:r')
break
end
end
end
end
if problem_id ~= "" then
vim.cmd("silent only")
if problem_id ~= '' then
vim.cmd('silent only')
local base_fp = vim.fn.getcwd()
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 source_files = vim.fn.glob(problem_id .. ".*")
local source_file
if source_files ~= "" then
local files = vim.split(source_files, "\n")
local valid_extensions = vim.tbl_keys(constants.filetype_to_language)
for _, file in ipairs(files) do
local ext = vim.fn.fnamemodify(file, ":e")
if vim.tbl_contains(valid_extensions, ext) then
source_file = file
break
end
end
source_file = source_file or files[1]
end
local base_fp = vim.fn.getcwd()
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 source_files = vim.fn.glob(problem_id .. '.*')
local source_file
if source_files ~= '' then
local files = vim.split(source_files, '\n')
local valid_extensions = vim.tbl_keys(constants.filetype_to_language)
for _, file in ipairs(files) do
local ext = vim.fn.fnamemodify(file, ':e')
if vim.tbl_contains(valid_extensions, ext) then
source_file = file
break
end
end
source_file = source_file or files[1]
end
if not source_file or vim.fn.filereadable(source_file) == 0 then
return
end
if not source_file or vim.fn.filereadable(source_file) == 0 then
return
end
vim.cmd.edit(source_file)
local source_buf = vim.api.nvim_get_current_buf()
local input_buf = vim.fn.bufnr(input_file, true)
local output_buf = vim.fn.bufnr(output_file, true)
vim.cmd.edit(source_file)
local source_buf = vim.api.nvim_get_current_buf()
local input_buf = vim.fn.bufnr(input_file, true)
local output_buf = vim.fn.bufnr(output_file, true)
if tile_fn then
tile_fn(source_buf, input_buf, output_buf)
else
M.default_tile(source_buf, input_buf, output_buf)
end
else
vim.cmd(state.layout)
if tile_fn then
tile_fn(source_buf, input_buf, output_buf)
else
M.default_tile(source_buf, input_buf, output_buf)
end
else
vim.cmd(state.layout)
for win, win_state in pairs(state.windows) do
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_set_current_win(win)
if vim.api.nvim_get_current_buf() == win_state.bufnr then
vim.fn.winrestview(win_state.view)
end
end
end
for win, win_state in pairs(state.windows) do
if vim.api.nvim_win_is_valid(win) then
vim.api.nvim_set_current_win(win)
if vim.api.nvim_get_current_buf() == win_state.bufnr then
vim.fn.winrestview(win_state.view)
end
end
end
if vim.api.nvim_win_is_valid(state.current_win) then
vim.api.nvim_set_current_win(state.current_win)
end
end
if vim.api.nvim_win_is_valid(state.current_win) then
vim.api.nvim_set_current_win(state.current_win)
end
end
end
---@param source_buf integer
---@param input_buf integer
---@param output_buf integer
local function default_tile(source_buf, input_buf, output_buf)
vim.validate({
source_buf = { source_buf, "number" },
input_buf = { input_buf, "number" },
output_buf = { output_buf, "number" },
})
vim.validate({
source_buf = { source_buf, 'number' },
input_buf = { input_buf, 'number' },
output_buf = { output_buf, 'number' },
})
vim.api.nvim_set_current_buf(source_buf)
vim.cmd.vsplit()
vim.api.nvim_set_current_buf(output_buf)
vim.bo.filetype = "cp"
vim.cmd(("vertical resize %d"):format(math.floor(vim.o.columns * 0.3)))
vim.cmd.split()
vim.api.nvim_set_current_buf(input_buf)
vim.bo.filetype = "cp"
vim.cmd.wincmd("h")
vim.api.nvim_set_current_buf(source_buf)
vim.cmd.vsplit()
vim.api.nvim_set_current_buf(output_buf)
vim.bo.filetype = 'cp'
vim.cmd(('vertical resize %d'):format(math.floor(vim.o.columns * 0.3)))
vim.cmd.split()
vim.api.nvim_set_current_buf(input_buf)
vim.bo.filetype = 'cp'
vim.cmd.wincmd('h')
end
M.default_tile = default_tile

View file

@ -1,61 +1,61 @@
if vim.g.loaded_cp then
return
return
end
vim.g.loaded_cp = 1
local constants = require("cp.constants")
local constants = require('cp.constants')
local platforms = constants.PLATFORMS
local actions = constants.ACTIONS
vim.api.nvim_create_user_command("CP", function(opts)
local cp = require("cp")
cp.handle_command(opts)
vim.api.nvim_create_user_command('CP', function(opts)
local cp = require('cp')
cp.handle_command(opts)
end, {
nargs = "*",
desc = "Competitive programming helper",
complete = function(ArgLead, CmdLine, _)
local args = vim.split(vim.trim(CmdLine), "%s+")
local num_args = #args
if CmdLine:sub(-1) == " " then
num_args = num_args + 1
end
nargs = '*',
desc = 'Competitive programming helper',
complete = function(ArgLead, CmdLine, _)
local args = vim.split(vim.trim(CmdLine), '%s+')
local num_args = #args
if CmdLine:sub(-1) == ' ' then
num_args = num_args + 1
end
if num_args == 2 then
local candidates = {}
local cp = require("cp")
local context = cp.get_current_context()
if context.platform and context.contest_id then
vim.list_extend(candidates, actions)
local cache = require("cp.cache")
cache.load()
local contest_data = cache.get_contest_data(context.platform, context.contest_id)
if contest_data and contest_data.problems then
for _, problem in ipairs(contest_data.problems) do
table.insert(candidates, problem.id)
end
end
else
vim.list_extend(candidates, platforms)
end
return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1
end, candidates)
elseif num_args == 4 then
if vim.tbl_contains(platforms, args[2]) then
local cache = require("cp.cache")
cache.load()
local contest_data = cache.get_contest_data(args[2], args[3])
if contest_data and contest_data.problems then
local candidates = {}
for _, problem in ipairs(contest_data.problems) do
table.insert(candidates, problem.id)
end
return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1
end, candidates)
end
end
end
return {}
end,
if num_args == 2 then
local candidates = {}
local cp = require('cp')
local context = cp.get_current_context()
if context.platform and context.contest_id then
vim.list_extend(candidates, actions)
local cache = require('cp.cache')
cache.load()
local contest_data = cache.get_contest_data(context.platform, context.contest_id)
if contest_data and contest_data.problems then
for _, problem in ipairs(contest_data.problems) do
table.insert(candidates, problem.id)
end
end
else
vim.list_extend(candidates, platforms)
end
return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1
end, candidates)
elseif num_args == 4 then
if vim.tbl_contains(platforms, args[2]) then
local cache = require('cp.cache')
cache.load()
local contest_data = cache.get_contest_data(args[2], args[3])
if contest_data and contest_data.problems then
local candidates = {}
for _, problem in ipairs(contest_data.problems) do
table.insert(candidates, problem.id)
end
return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1
end, candidates)
end
end
end
return {}
end,
})

View file

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