Merge pull request #7 from barrett-ruth/feat/validation

config specs and window layout fixes
This commit is contained in:
Barrett Ruth 2025-09-13 00:45:47 +02:00 committed by GitHub
commit 42a3fa370f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 352 additions and 586 deletions

View file

@ -29,6 +29,18 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
args: --display-style quiet .
lua-typecheck:
name: Lua Type Checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Lua LS Type Check
uses: mrcjkb/lua-typecheck-action@v0
with:
checklevel: Warning
directories: lua
configpath: .luarc.json
python-format:
name: Python Formatting
runs-on: ubuntu-latest
@ -39,7 +51,7 @@ jobs:
- name: Install ruff
run: uv tool install ruff
- name: Check Python formatting with ruff
run: ruff format --check templates/scrapers/
run: ruff format --check scrapers/
python-lint:
name: Python Linting
@ -51,4 +63,4 @@ jobs:
- name: Install ruff
run: uv tool install ruff
- name: Lint Python files with ruff
run: ruff check templates/scrapers/
run: ruff check scrapers/

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
.venv/
venv/
CLAUDE.md

16
.luarc.json Normal file
View file

@ -0,0 +1,16 @@
{
"runtime.version": "LuaJIT",
"runtime.path": [
"lua/?.lua",
"lua/?/init.lua"
],
"diagnostics.globals": [
"vim"
],
"workspace.library": [
"$VIMRUNTIME/lua",
"${3rd}/luv/library"
],
"workspace.checkThirdParty": false,
"completion.callSnippet": "Replace"
}

188
CLAUDE.md
View file

@ -1,188 +0,0 @@
# CP.nvim Modernization Status & Upcoming Changes
## ✅ Completed Modernization
### Core System Overhaul
- **Eliminated makefile dependency** - Pure Lua implementation using `vim.system()`
- **Native Neovim 0.10+ APIs** throughout (no more shell scripts or `vim.fn.system`)
- **JSON-based problem scraping** replacing `---INPUT---` format with structured data
- **Modular architecture** with dedicated modules:
- `execute.lua` - Compilation and execution
- `scrape.lua` - Problem scraping with JSON parsing
- `window.lua` - Native window management
- `config.lua` - Base + extension configuration system
### Enhanced Features
- **Configurable timeouts** per contest type
- **Contest-specific compiler flags** with inheritance
- **Direct Python scraper integration** without shell script middleware
- **Native diff mode** with proper window state management (eliminated vim-zoom dependency)
- **LuaSnip integration** with automatic snippet expansion
- **Debug flag accuracy** in output formatting
### Configuration System
- **Base + extension pattern** for contest configuration
- **Dynamic C++ standard flag generation** (`-std=c++XX`)
- **Default snippet templates** built into the plugin
## 🔧 Upcoming Development Tasks
### 1. Multi-Test Case Support
**Current**: Only writes first scraped test case to `.in`/`.expected`
**Needed**:
- Write all test cases or let users cycle through them
- Support for test case selection/management
### 2. Template System Enhancement
**Current**: Basic snippet expansion with contest types
**Needed**:
- Template customization beyond snippets
- Problem-specific template variables
- Integration with scraped problem metadata
### 3. Enhanced Output Processing
**Current**: Basic output comparison with expected results
**Needed**:
- Better diff visualization in output window
- Partial match scoring
- Test case result breakdown for multi-case problems
### 4. Error Handling & User Experience
**Current**: Basic error messages and logging
**Needed**:
- Better error recovery from failed scraping
- Progress indicators for long-running operations
- More descriptive compilation error formatting
### 5. Contest Platform Extensions
**Current**: AtCoder, Codeforces, CSES support
**Potential**:
- USACO integration (mentioned in TODO)
- Additional platform support as needed
### 6. Documentation & Examples
**Current**: Basic README
**Needed**:
- Video demonstration (TODO item)
- Comprehensive vim docs (`:help cp.nvim`)
- Configuration examples and best practices
## 🏗️ Architecture Notes for Future Development
### Plugin Structure
```
lua/cp/
├── init.lua # Main plugin logic & command setup
├── config.lua # Configuration with inheritance
├── execute.lua # Compilation & execution (vim.system)
├── scrape.lua # JSON-based problem scraping
├── snippets.lua # Default LuaSnip templates
└── window.lua # Native window management
```
### Key Design Principles
1. **Pure Lua** - No external shell dependencies
2. **Modular** - Each concern in separate module
3. **Configurable** - Base + extension configuration pattern
4. **Modern APIs** - Neovim 0.10+ native functions
5. **Graceful Fallbacks** - Degrade gracefully when optional deps unavailable
## 📊 Current Feature Completeness
| Feature | Status | Notes |
|---------|--------|-------|
| Problem Setup | ✅ Complete | Native scraping + file generation |
| Code Execution | ✅ Complete | Direct compilation with timeouts |
| Debug Mode | ✅ Complete | Separate flags + debug output |
| Diff Comparison | ✅ Complete | Native window management |
| LuaSnip Integration | ✅ Complete | Auto-expanding templates |
| Multi-platform | ✅ Complete | AtCoder, Codeforces, CSES |
| Configuration | ✅ Complete | Flexible inheritance system |
## 📋 PLAN - Remaining Tasks
### Phase 1: Core System Testing (High Priority)
1. **End-to-end functionality testing**
- Test all contest types: `CP atcoder`, `CP codeforces`, `CP cses`
- Verify problem setup with scraping: `CP atcoder abc123 a`
- Test run/debug/diff workflow for each platform
- Validate compilation with different C++ standards
2. **Python environment validation**
- Verify `uv sync` creates proper virtual environment
- Test Python scrapers output valid JSON
- Handle scraping failures gracefully
- Check scraper dependencies (requests, beautifulsoup, etc.)
3. **Configuration system testing**
- Test default vs custom contest configurations
- Verify timeout settings work correctly
- Test compiler flag inheritance and overrides
- Validate C++ version flag generation
### Phase 2: Integration Testing (Medium Priority)
4. **LuaSnip integration validation**
- Test snippet expansion for contest types
- Verify fallback when LuaSnip unavailable
- Test custom user snippets alongside defaults
- Validate snippet triggering in empty files
5. **Window management testing**
- Test diff mode enter/exit functionality
- Verify layout save/restore works correctly
- Test window state persistence across operations
- Validate split ratios and window positioning
6. **File I/O and path handling**
- Test with various problem ID formats
- Verify input/output/expected file handling
- Test directory creation (build/, io/)
- Validate file path resolution across platforms
### Phase 3: Error Handling & Edge Cases (Medium Priority)
7. **Error scenario testing**
- Test with invalid contest types
- Test with missing Python dependencies
- Test compilation failures
- Test scraping failures (network issues, rate limits)
- Test with malformed JSON from scrapers
8. **Performance validation**
- Test timeout handling for slow compilations
- Verify non-blocking execution with vim.system
- Test with large output files
- Validate memory usage with multiple test cases
### Phase 4: User Experience Testing (Lower Priority)
9. **Command completion and validation**
- Test command-line completion for contest types
- Test argument validation and error messages
- Verify help text accuracy
10. **Documentation verification**
- Test all examples in README work correctly
- Verify installation instructions are accurate
- Test user configuration examples
### Phase 5: Regression Testing (Ongoing)
11. **Compare with original functionality**
- Ensure no features were lost in migration
- Verify output format matches expectations
- Test backward compatibility with existing workflows
### Testing Priority Order:
1. **Critical Path**: Problem setup → Run → Output verification
2. **Core Features**: Debug mode, diff mode, LuaSnip integration
3. **Error Handling**: Graceful failures, helpful error messages
4. **Edge Cases**: Unusual problem formats, network issues
5. **Performance**: Large problems, slow networks, timeout handling
### Success Criteria:
- All basic workflows complete without errors
- Python scrapers produce valid problem files
- Compilation and execution work for all contest types
- LuaSnip integration functions or degrades gracefully
- Error messages are clear and actionable
- No vim.fn.system calls remain (all migrated to vim.system)
The plugin is now fully modernized and ready for comprehensive testing to ensure production readiness.

View file

@ -1,13 +0,0 @@
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
pattern = "*/io/*.in",
callback = function()
vim.bo.filetype = "cpinput"
end,
})
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
pattern = "*/io/*.out",
callback = function()
vim.bo.filetype = "cpoutput"
end,
})

View file

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

View file

@ -14,4 +14,4 @@ highlight default link cpOutputDebug Comment
highlight default link cpOutputMatchesTrue DiffAdd
highlight default link cpOutputMatchesFalse DiffDelete
let b:current_syntax = "cpoutput"
let b:current_syntax = "cp"

View file

@ -1,5 +1,11 @@
---@class cp.Config
---@field contests table
---@field snippets table
---@field hooks table
local M = {}
---@type cp.Config
M.defaults = {
contests = {
default = {
@ -14,11 +20,20 @@ M.defaults = {
codeforces = {
cpp_version = 23,
},
cses = {},
cses = {
cpp_version = 20,
},
},
snippets = {},
hooks = {
before_run = nil,
before_debug = nil,
},
}
---@param base_config table
---@param contest_config table
---@return table
local function extend_contest_config(base_config, contest_config)
local result = vim.tbl_deep_extend("force", base_config, contest_config)
@ -29,7 +44,28 @@ local function extend_contest_config(base_config, contest_config)
return result
end
---@param user_config table|nil
---@return table
function M.setup(user_config)
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 },
})
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 },
})
end
end
local config = vim.tbl_deep_extend("force", M.defaults, user_config or {})
local default_contest = config.contests.default

107
lua/cp/health.lua Normal file
View file

@ -0,0 +1,107 @@
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
end
local function check_uv()
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
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: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
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: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
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
end
local function check_directories()
local cwd = vim.fn.getcwd()
local build_dir = cwd .. "/build"
local io_dir = cwd .. "/io"
if vim.fn.isdirectory(build_dir) == 1 then
vim.health.ok("Build directory exists: " .. build_dir)
else
vim.health.info("Build directory will be created when needed")
end
if vim.fn.isdirectory(io_dir) == 1 then
vim.health.ok("IO directory exists: " .. io_dir)
else
vim.health.info("IO directory will be created when needed")
end
end
local function check_config()
local cp = require("cp")
if cp.is_initialized() then
vim.health.ok("Plugin initialized")
if vim.g.cp_contest then
vim.health.info("Current contest: " .. vim.g.cp_contest)
else
vim.health.info("No contest mode set")
end
else
vim.health.warn("Plugin not initialized - configuration may be incomplete")
end
end
function M.check()
vim.health.start("cp.nvim health check")
check_nvim_version()
check_uv()
check_python_env()
check_scrapers()
check_luasnip()
check_directories()
check_config()
end
return M

View file

@ -16,36 +16,6 @@ if not vim.fn.has("nvim-0.10.0") then
return M
end
local function get_plugin_path()
local plugin_path = debug.getinfo(1, "S").source:sub(2)
return vim.fn.fnamemodify(plugin_path, ":h:h:h")
end
local function setup_python_env()
local plugin_path = get_plugin_path()
local venv_dir = plugin_path .. "/.venv"
if vim.fn.executable("uv") == 0 then
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
log("setting up Python environment for scrapers...")
local result = vim.system({ "uv", "sync" }, { cwd = plugin_path, text = true }):wait()
if result.code ~= 0 then
log("failed to setup Python environment: " .. result.stderr, vim.log.levels.ERROR)
return false
end
log("python environment setup complete")
end
return true
end
local competition_types = { "atcoder", "codeforces", "cses" }
local function setup_contest(contest_type)
@ -82,7 +52,7 @@ local function setup_problem(problem_id, problem_letter)
vim.g.cp_diff_mode = false
end
vim.cmd.only()
vim.cmd("silent only")
local scrape_result = scrape.scrape_problem(vim.g.cp_contest, problem_id, problem_letter)
@ -136,10 +106,12 @@ local function setup_problem(problem_id, problem_letter)
vim.cmd.vsplit(output)
vim.cmd.w()
vim.bo.filetype = "cp"
window.clearcol()
vim.cmd(("vertical resize %d"):format(math.floor(vim.o.columns * 0.3)))
vim.cmd.split(input)
vim.cmd.w()
vim.bo.filetype = "cp"
window.clearcol()
vim.cmd.wincmd("h")
@ -161,9 +133,8 @@ local function run_problem()
return
end
local has_lsp, lsp = pcall(require, "lsp")
if has_lsp and lsp.lsp_format then
lsp.lsp_format({ async = true })
if config.hooks and config.hooks.before_run then
config.hooks.before_run(problem_id)
end
if not vim.g.cp_contest then
@ -185,9 +156,8 @@ local function debug_problem()
return
end
local has_lsp, lsp = pcall(require, "lsp")
if has_lsp and lsp.lsp_format then
lsp.lsp_format({ async = true })
if config.hooks and config.hooks.before_debug then
config.hooks.before_debug(problem_id)
end
if not vim.g.cp_contest then
@ -239,71 +209,57 @@ end
local initialized = false
function M.is_initialized()
return initialized
end
function M.setup(user_config)
if initialized and not user_config then
return
end
config = config_module.setup(user_config)
local plugin_path = get_plugin_path()
config.snippets.path = plugin_path .. "/templates/snippets"
snippets.setup(config)
initialized = true
end
if initialized then
function M.handle_command(opts)
local args = opts.fargs
if #args == 0 then
log("Usage: :CP <contest|problem_id|run|debug|diff>", vim.log.levels.ERROR)
return
end
initialized = true
setup_python_env()
local cmd = args[1]
vim.api.nvim_create_user_command("CP", function(opts)
local args = opts.fargs
if #args == 0 then
log("Usage: :CP <contest|problem_id|run|debug|diff>", vim.log.levels.ERROR)
return
end
local cmd = args[1]
if vim.tbl_contains(competition_types, cmd) then
if args[2] then
setup_contest(cmd)
if (cmd == "atcoder" or cmd == "codeforces") and args[3] then
setup_problem(args[2], args[3])
else
setup_problem(args[2])
end
if vim.tbl_contains(competition_types, cmd) then
if args[2] then
setup_contest(cmd)
if (cmd == "atcoder" or cmd == "codeforces") and args[3] then
setup_problem(args[2], args[3])
else
setup_contest(cmd)
setup_problem(args[2])
end
elseif cmd == "run" then
run_problem()
elseif cmd == "debug" then
debug_problem()
elseif cmd == "diff" then
diff_problem()
else
if vim.g.cp_contest then
if (vim.g.cp_contest == "atcoder" or vim.g.cp_contest == "codeforces") and args[2] then
setup_problem(cmd, args[2])
else
setup_problem(cmd)
end
else
log("no contest mode set. run :CP <contest> first or use full command", vim.log.levels.ERROR)
end
setup_contest(cmd)
end
end, {
nargs = "*",
complete = function(ArgLead, _, _)
local commands = vim.list_extend(vim.deepcopy(competition_types), { "run", "debug", "diff" })
return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1
end, commands)
end,
})
elseif cmd == "run" then
run_problem()
elseif cmd == "debug" then
debug_problem()
elseif cmd == "diff" then
diff_problem()
else
if vim.g.cp_contest then
if (vim.g.cp_contest == "atcoder" or vim.g.cp_contest == "codeforces") and args[2] then
setup_problem(cmd, args[2])
else
setup_problem(cmd)
end
else
log("no contest mode set. run :CP <contest> first or use full command", vim.log.levels.ERROR)
end
end
end
return M

View file

@ -9,11 +9,47 @@ local function ensure_io_directory()
vim.fn.mkdir("io", "p")
end
local function log(msg, level)
vim.notify(("[cp.nvim]: %s"):format(msg), level or vim.log.levels.INFO)
end
local function setup_python_env()
local plugin_path = get_plugin_path()
local venv_dir = plugin_path .. "/.venv"
if vim.fn.executable("uv") == 0 then
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
log("setting up Python environment for scrapers...")
local result = vim.system({ "uv", "sync" }, { cwd = plugin_path, text = true }):wait()
if result.code ~= 0 then
log("failed to setup Python environment: " .. result.stderr, vim.log.levels.ERROR)
return false
end
log("python environment setup complete")
end
return true
end
function M.scrape_problem(contest, problem_id, problem_letter)
ensure_io_directory()
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 .. "/templates/scrapers/" .. contest .. ".py"
local scraper_path = plugin_path .. "/scrapers/" .. contest .. ".py"
local args
if contest == "cses" then
@ -47,14 +83,29 @@ function M.scrape_problem(contest, problem_id, problem_letter)
return data
end
local full_problem_id = data.problem_id
local full_problem_id = data.problem_id:lower()
local input_file = "io/" .. full_problem_id .. ".in"
local expected_file = "io/" .. full_problem_id .. ".expected"
if #data.test_cases > 0 then
local first_test = data.test_cases[1]
vim.fn.writefile(vim.split(first_test.input, "\n"), input_file)
vim.fn.writefile(vim.split(first_test.output, "\n"), expected_file)
local all_inputs = {}
local all_outputs = {}
for _, test_case in ipairs(data.test_cases) do
local input_lines = vim.split(test_case.input:gsub("\r", ""):gsub("\n+$", ""), "\n")
local output_lines = vim.split(test_case.output:gsub("\r", ""):gsub("\n+$", ""), "\n")
for _, line in ipairs(input_lines) do
table.insert(all_inputs, line)
end
for _, line in ipairs(output_lines) do
table.insert(all_outputs, line)
end
end
vim.fn.writefile(all_inputs, input_file)
vim.fn.writefile(all_outputs, expected_file)
end
return {

View file

@ -35,41 +35,77 @@ function M.restore_layout(state)
end
vim.cmd.diffoff()
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)
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 bufname:match("%.cc$") then
problem_id = vim.fn.fnamemodify(bufname, ":t:r")
break
end
end
end
end
if vim.api.nvim_win_is_valid(state.current_win) then
vim.api.nvim_set_current_win(state.current_win)
if problem_id ~= "" then
vim.cmd("silent only")
local base_fp = vim.fn.getcwd()
local input = ("%s/io/%s.in"):format(base_fp, problem_id)
local output = ("%s/io/%s.out"):format(base_fp, problem_id)
local source = problem_id .. ".cc"
vim.cmd.edit(source)
vim.cmd.vsplit(output)
vim.bo.filetype = "cp"
M.clearcol()
vim.cmd(("vertical resize %d"):format(math.floor(vim.o.columns * 0.3)))
vim.cmd.split(input)
vim.bo.filetype = "cp"
M.clearcol()
vim.cmd.wincmd("h")
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
if vim.api.nvim_win_is_valid(state.current_win) then
vim.api.nvim_set_current_win(state.current_win)
end
end
end
function M.setup_diff_layout(actual_output, expected_output, input_file)
vim.cmd.diffoff()
vim.cmd.only()
vim.cmd("silent only")
local output_lines = vim.split(actual_output, "\n")
local output_buf = vim.api.nvim_create_buf(false, true)
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, output_lines)
vim.bo[output_buf].filetype = "cpoutput"
vim.bo[output_buf].filetype = "cp"
vim.cmd.edit()
vim.api.nvim_set_current_buf(output_buf)
vim.cmd.diffthis()
M.clearcol()
vim.cmd.diffthis()
vim.cmd.vsplit(expected_output)
vim.cmd.diffthis()
vim.bo.filetype = "cp"
M.clearcol()
vim.cmd.diffthis()
vim.cmd.wincmd("h")
vim.cmd(("botright split %s"):format(input_file))
vim.bo.filetype = "cp"
M.clearcol()
vim.cmd.wincmd("k")
end

View file

@ -3,4 +3,20 @@ if vim.g.loaded_cp then
end
vim.g.loaded_cp = 1
require("cp").setup()
local competition_types = { "atcoder", "codeforces", "cses" }
vim.api.nvim_create_user_command("CP", function(opts)
local cp = require("cp")
if not cp.is_initialized() then
cp.setup()
end
cp.handle_command(opts)
end, {
nargs = "*",
complete = function(ArgLead, _, _)
local commands = vim.list_extend(vim.deepcopy(competition_types), { "run", "debug", "diff" })
return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1
end, commands)
end,
})

View file

@ -41,9 +41,6 @@ Using [lazy.nvim](https://github.com/folke/lazy.nvim):
## TODO
- update templates to minimum vrsion that's comatible with scrapers (i.e.
aggregated testcases, ifdef local, etc.)
- vimdocs
- vimdocs
- example video
- more flexible setup (more of a question of philosophy)
- USACO support

View file

@ -68,7 +68,7 @@ def main():
contest_id = sys.argv[1]
problem_letter = sys.argv[2]
problem_id = contest_id + problem_letter
problem_id = contest_id + problem_letter.lower()
url = parse_problem_url(contest_id, problem_letter)
print(f"Scraping: {url}", file=sys.stderr)
@ -89,6 +89,13 @@ def main():
for input_data, output_data in tests:
test_cases.append({"input": input_data, "output": output_data})
if test_cases:
combined_input = (
str(len(test_cases)) + "\n" + "\n".join(tc["input"] for tc in test_cases)
)
combined_output = "\n".join(tc["output"] for tc in test_cases)
test_cases = [{"input": combined_input, "output": combined_output}]
result = {
"success": True,
"problem_id": problem_id,

View file

@ -43,16 +43,9 @@ def scrape(url: str):
output_lines.append(line_div.get_text().strip())
if input_lines and output_lines:
if len(input_lines) > 1 and input_lines[0].isdigit():
test_count = int(input_lines[0])
remaining_input = input_lines[1:]
for i in range(min(test_count, len(output_lines))):
if i < len(remaining_input):
tests.append((remaining_input[i], output_lines[i]))
else:
input_text = "\n".join(input_lines)
output_text = "\n".join(output_lines)
tests.append((input_text, output_text))
input_text = "\n".join(input_lines)
output_text = "\n".join(output_lines)
tests.append((input_text, output_text))
return tests
@ -84,7 +77,7 @@ def main():
contest_id = sys.argv[1]
problem_letter = sys.argv[2]
problem_id = contest_id + problem_letter.upper()
problem_id = contest_id + problem_letter.lower()
url = parse_problem_url(contest_id, problem_letter)
tests = scrape_sample_tests(url)

View file

@ -1,9 +0,0 @@
BasedOnStyle: Google
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortCompoundRequirementOnASingleLine: false
AllowShortEnumsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
AllowShortIfStatementsOnASingleLine: false
AllowShortLambdasOnASingleLine: false
AllowShortLoopsOnASingleLine: false

View file

@ -1,30 +0,0 @@
.PHONY: run debug clean setup init scrape
VERSION ?= 20
SRC = $(word 2,$(MAKECMDGOALS))
.SILENT:
run:
sh scripts/run.sh $(SRC)
debug:
sh scripts/debug.sh $(SRC)
clean:
rm -rf build/*
setup:
test -d build || mkdir -p build
test -d io || mkdir -p io
test -f compile_flags.txt && echo -std=c++$(VERSION) >>compile_flags.txt
init:
make setup
scrape:
sh scripts/scrape.sh $(word 2,$(MAKECMDGOALS)) $(word 3,$(MAKECMDGOALS)) $(word 4,$(MAKECMDGOALS))
%:
@:

View file

@ -1,29 +0,0 @@
#!/bin/sh
. ./scripts/utils.sh
SRC="$1"
BASE=$(basename "$SRC" .cc)
INPUT="${BASE}.in"
OUTPUT="${BASE}.out"
DBG_BIN="${BASE}.debug"
test -d build || mkdir -p build
test -d io || mkdir -p io
test -f "$INPUT" && test ! -f "io/$INPUT" && mv "$INPUT" "io/"
test -f "$OUTPUT" && test ! -f "io/$OUTPUT" && mv "$OUTPUT" "io/"
test -f "io/$INPUT" || touch "io/$INPUT"
test -f "io/$OUTPUT" || touch "io/$OUTPUT"
INPUT="io/$INPUT"
OUTPUT="io/$OUTPUT"
DBG_BIN="build/$DBG_BIN"
compile_source "$SRC" "$DBG_BIN" "$OUTPUT" @debug_flags.txt
CODE=$?
test $CODE -gt 0 && exit $CODE
execute_binary "$DBG_BIN" "$INPUT" "$OUTPUT" true
exit $?

View file

@ -1,29 +0,0 @@
#!/bin/sh
. ./scripts/utils.sh
SRC="$1"
BASE=$(basename "$SRC" .cc)
INPUT="${BASE}.in"
OUTPUT="${BASE}.out"
RUN_BIN="${BASE}.run"
test -d build || mkdir -p build
test -d io || mkdir -p io
test -f "$INPUT" && test ! -f "io/$INPUT" && mv "$INPUT" "io/"
test -f "$OUTPUT" && test ! -f "io/$OUTPUT" && mv "$OUTPUT" "io/"
test -f "io/$INPUT" || touch "io/$INPUT"
test -f "io/$OUTPUT" || touch "io/$OUTPUT"
INPUT="io/$INPUT"
OUTPUT="io/$OUTPUT"
RUN_BIN="build/$RUN_BIN"
compile_source "$SRC" "$RUN_BIN" "$OUTPUT" ""
CODE=$?
test $CODE -gt 0 && exit $CODE
execute_binary "$RUN_BIN" "$INPUT" "$OUTPUT"
exit $?

View file

@ -1,85 +0,0 @@
#!/bin/sh
CONTEST="$1"
PROBLEM="$2"
PROBLEM_LETTER="$3"
if [ -z "$CONTEST" ] || [ -z "$PROBLEM" ]; then
echo "Usage: make scrape <contest> <problem_id> [problem_letter]"
echo "Available contests: cses, atcoder, codeforces"
echo "Examples:"
echo " make scrape cses 1068"
echo " make scrape atcoder abc042 a"
echo " make scrape codeforces 1234 A"
exit
fi
test -d io && true || mkdir -p io
TMPFILE=$(mktemp)
ORIGDIR=$(pwd)
case "$CONTEST" in
cses)
cd "$(dirname "$0")/../.." && uv run scrapers/cses.py "$PROBLEM" > "$TMPFILE"
if [ $? -eq 0 ]; then
cd "$ORIGDIR"
awk '/^---INPUT---$/ {getline; while ($0 != "---OUTPUT---") {print; getline}} END {}' "$TMPFILE" > "io/$PROBLEM.in"
awk '/^---OUTPUT---$/ {getline; while ($0 != "---END---") {print; getline}} END {}' "$TMPFILE" > "io/$PROBLEM.expected"
echo "Scraped problem $PROBLEM to io/$PROBLEM.in and io/$PROBLEM.expected"
else
echo "Failed to scrape problem $PROBLEM"
cat "$TMPFILE"
rm "$TMPFILE"
exit
fi
;;
atcoder)
if [ -z "$PROBLEM_LETTER" ]; then
echo "AtCoder requires problem letter (e.g., make scrape atcoder abc042 a)"
rm "$TMPFILE"
exit
fi
FULL_PROBLEM_ID="${PROBLEM}${PROBLEM_LETTER}"
cd "$(dirname "$0")/../.." && uv run scrapers/atcoder.py "$PROBLEM" "$PROBLEM_LETTER" > "$TMPFILE"
if [ $? -eq 0 ]; then
cd "$ORIGDIR"
awk '/^---INPUT---$/ {getline; while ($0 != "---OUTPUT---") {print; getline}} END {}' "$TMPFILE" > "io/$FULL_PROBLEM_ID.in"
awk '/^---OUTPUT---$/ {getline; while ($0 != "---END---") {print; getline}} END {}' "$TMPFILE" > "io/$FULL_PROBLEM_ID.expected"
echo "Scraped problem $FULL_PROBLEM_ID to io/$FULL_PROBLEM_ID.in and io/$FULL_PROBLEM_ID.expected"
else
echo "Failed to scrape problem $FULL_PROBLEM_ID"
cat "$TMPFILE"
rm "$TMPFILE"
exit
fi
;;
codeforces)
if [ -z "$PROBLEM_LETTER" ]; then
echo "Codeforces requires problem letter (e.g., make scrape codeforces 1234 A)"
rm "$TMPFILE"
exit
fi
FULL_PROBLEM_ID="${PROBLEM}${PROBLEM_LETTER}"
cd "$(dirname "$0")/../.." && uv run scrapers/codeforces.py "$PROBLEM" "$PROBLEM_LETTER" > "$TMPFILE"
if [ $? -eq 0 ]; then
cd "$ORIGDIR"
awk '/^---INPUT---$/ {getline; while ($0 != "---OUTPUT---") {print; getline}} END {}' "$TMPFILE" > "io/$FULL_PROBLEM_ID.in"
awk '/^---OUTPUT---$/ {getline; while ($0 != "---END---") {print; getline}} END {}' "$TMPFILE" > "io/$FULL_PROBLEM_ID.expected"
echo "Scraped problem $FULL_PROBLEM_ID to io/$FULL_PROBLEM_ID.in and io/$FULL_PROBLEM_ID.expected"
else
echo "Failed to scrape problem $FULL_PROBLEM_ID"
echo "You can manually add test cases to io/$FULL_PROBLEM_ID.in and io/$FULL_PROBLEM_ID.expected"
cat "$TMPFILE"
rm "$TMPFILE"
exit
fi
;;
*)
echo "Unknown contest type: $CONTEST"
echo "Available contests: cses, atcoder, codeforces"
rm "$TMPFILE"
exit
;;
esac
rm "$TMPFILE"

View file

@ -1,73 +0,0 @@
#!/bin/sh
execute_binary() {
binary="$1"
input="$2"
output="$3"
is_debug="$4"
start=$(date '+%s.%N')
if [ -n "$is_debug" ]; then
asan="$(ldconfig -p | grep libasan.so | head -n1 | awk '{print $4}')"
LD_PRELOAD="$asan" timeout 2s ./"$binary" <"$input" >"$output" 2>&1
else
timeout 2s ./"$binary" <"$input" >"$output" 2>&1
fi
CODE=$?
end=$(date '+%s.%N')
truncate -s "$(head -n 1000 "$output" | wc -c)" "$output"
if [ $CODE -ge 124 ]; then
MSG=''
case $CODE in
124) MSG='TIMEOUT' ;;
128) MSG='SIGILL' ;;
130) MSG='SIGABRT' ;;
131) MSG='SIGBUS' ;;
136) MSG='SIGFPE' ;;
135) MSG='SIGSEGV' ;;
137) MSG='SIGPIPE' ;;
139) MSG='SIGTERM' ;;
esac
[ $CODE -ne 124 ] && sed -i '$d' "$output"
test -n "$MSG" && printf '\n[code]: %s (%s)' "$CODE" "$MSG" >>"$output"
else
printf '\n[code]: %s' "$CODE" >>"$output"
fi
printf '\n[time]: %s ms' "$(awk "BEGIN {print ($end - $start) * 1000}")" >>$output
test -n "$is_debug" && is_debug_string=true || is_debug_string=false
printf '\n[debug]: %s' "$is_debug_string" >>$output
expected_file="${output%.out}.expected"
if [ -f "$expected_file" ] && [ $CODE -eq 0 ]; then
awk '/^\[[^]]*\]:/ {exit} {print}' "$output" > /tmp/program_output
if cmp -s /tmp/program_output "$expected_file"; then
printf '\n[matches]: true' >>"$output"
else
printf '\n[matches]: false' >>"$output"
fi
rm -f /tmp/program_output
fi
return $CODE
}
compile_source() {
src="$1"
bin="$2"
output="$3"
flags="$4"
test -f "$bin" && rm "$bin" || true
g++ @compile_flags.txt $flags "$src" -o "$bin" 2>"$output"
CODE=$?
if [ $CODE -gt 0 ]; then
printf '\n[code]: %s' "$CODE" >>"$output"
return $CODE
else
echo '' >"$output"
return 0
fi
}