diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2e897f..ecde609 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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/ diff --git a/.gitignore b/.gitignore index 297e1e1..510792a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .venv/ venv/ +CLAUDE.md diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..e83a348 --- /dev/null +++ b/.luarc.json @@ -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" +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index d231b36..0000000 --- a/CLAUDE.md +++ /dev/null @@ -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. \ No newline at end of file diff --git a/after/ftdetect/cp.lua b/after/ftdetect/cp.lua deleted file mode 100644 index 16634eb..0000000 --- a/after/ftdetect/cp.lua +++ /dev/null @@ -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, -}) diff --git a/after/ftplugin/cpinput.lua b/after/ftplugin/cp.lua similarity index 100% rename from after/ftplugin/cpinput.lua rename to after/ftplugin/cp.lua diff --git a/after/ftplugin/cpoutput.lua b/after/ftplugin/cpoutput.lua deleted file mode 100644 index 76a9f86..0000000 --- a/after/ftplugin/cpoutput.lua +++ /dev/null @@ -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 diff --git a/after/syntax/cpoutput.vim b/after/syntax/cp.vim similarity index 94% rename from after/syntax/cpoutput.vim rename to after/syntax/cp.vim index 28092a7..331d80f 100644 --- a/after/syntax/cpoutput.vim +++ b/after/syntax/cp.vim @@ -14,4 +14,4 @@ highlight default link cpOutputDebug Comment highlight default link cpOutputMatchesTrue DiffAdd highlight default link cpOutputMatchesFalse DiffDelete -let b:current_syntax = "cpoutput" \ No newline at end of file +let b:current_syntax = "cp" \ No newline at end of file diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 1bbda23..ea78176 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -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 diff --git a/lua/cp/health.lua b/lua/cp/health.lua new file mode 100644 index 0000000..150700f --- /dev/null +++ b/lua/cp/health.lua @@ -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 diff --git a/lua/cp/init.lua b/lua/cp/init.lua index d3d1aa3..12920ce 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -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 ", 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 ", 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 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 first or use full command", vim.log.levels.ERROR) + end + end end return M diff --git a/lua/cp/scrape.lua b/lua/cp/scrape.lua index dd0bb79..600c351 100644 --- a/lua/cp/scrape.lua +++ b/lua/cp/scrape.lua @@ -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 { diff --git a/lua/cp/window.lua b/lua/cp/window.lua index 8c7ba11..e4d7b6d 100644 --- a/lua/cp/window.lua +++ b/lua/cp/window.lua @@ -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 diff --git a/plugin/cp.lua b/plugin/cp.lua index 7ba14b7..9fc7e28 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -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, +}) diff --git a/readme.md b/readme.md index 002f713..6985571 100644 --- a/readme.md +++ b/readme.md @@ -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 diff --git a/templates/scrapers/atcoder.py b/scrapers/atcoder.py similarity index 89% rename from templates/scrapers/atcoder.py rename to scrapers/atcoder.py index 788a573..1f84f2e 100644 --- a/templates/scrapers/atcoder.py +++ b/scrapers/atcoder.py @@ -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, diff --git a/templates/scrapers/codeforces.py b/scrapers/codeforces.py similarity index 81% rename from templates/scrapers/codeforces.py rename to scrapers/codeforces.py index d1c24fa..35589a0 100644 --- a/templates/scrapers/codeforces.py +++ b/scrapers/codeforces.py @@ -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) diff --git a/templates/scrapers/cses.py b/scrapers/cses.py similarity index 100% rename from templates/scrapers/cses.py rename to scrapers/cses.py diff --git a/templates/.clang-format b/templates/.clang-format deleted file mode 100644 index e7350c4..0000000 --- a/templates/.clang-format +++ /dev/null @@ -1,9 +0,0 @@ -BasedOnStyle: Google -AllowShortBlocksOnASingleLine: false -AllowShortCaseLabelsOnASingleLine: false -AllowShortCompoundRequirementOnASingleLine: false -AllowShortEnumsOnASingleLine: false -AllowShortFunctionsOnASingleLine: false -AllowShortIfStatementsOnASingleLine: false -AllowShortLambdasOnASingleLine: false -AllowShortLoopsOnASingleLine: false diff --git a/templates/makefile b/templates/makefile deleted file mode 100644 index 0ae8c41..0000000 --- a/templates/makefile +++ /dev/null @@ -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)) - -%: - @: diff --git a/templates/scripts/debug.sh b/templates/scripts/debug.sh deleted file mode 100644 index 1e63f37..0000000 --- a/templates/scripts/debug.sh +++ /dev/null @@ -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 $? diff --git a/templates/scripts/run.sh b/templates/scripts/run.sh deleted file mode 100644 index ab9aa7d..0000000 --- a/templates/scripts/run.sh +++ /dev/null @@ -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 $? diff --git a/templates/scripts/scrape.sh b/templates/scripts/scrape.sh deleted file mode 100755 index 973ef7b..0000000 --- a/templates/scripts/scrape.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/sh - -CONTEST="$1" -PROBLEM="$2" -PROBLEM_LETTER="$3" - -if [ -z "$CONTEST" ] || [ -z "$PROBLEM" ]; then - echo "Usage: make scrape [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" diff --git a/templates/scripts/utils.sh b/templates/scripts/utils.sh deleted file mode 100644 index b804089..0000000 --- a/templates/scripts/utils.sh +++ /dev/null @@ -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 -}