diff --git a/README.md b/README.md index c2da671..58efb20 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,13 @@ https://github.com/user-attachments/assets/50b19481-8e6d-47b4-bebc-15e16c61a9c9 - **Multi-platform support**: AtCoder, Codeforces, CSES with consistent interface - **Automatic problem setup**: Scrape test cases and metadata in seconds -- **Rich test output**: ANSI color support for compiler errors and program output -- **Language agnostic**: Works with any compiled language -- **Template integration**: Contest-specific snippets via LuaSnip -- **Diff viewer**: Compare expected vs actual output with precision +- **Rich test output**: 256 color ANSI support for compiler errors and program output +- **Language agnostic**: Works with any language +- **Diff viewer**: Compare expected vs actual output with 3 diff modes ## Optional Dependencies - [uv](https://docs.astral.sh/uv/) for problem scraping -- [LuaSnip](https://github.com/L3MON4D3/LuaSnip) for templates - GNU [time](https://www.gnu.org/software/time/) and [timeout](https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html) ## Quick Start diff --git a/after/ftplugin/cp.lua b/after/ftplugin/cp.lua index 622ad6a..eab89a5 100644 --- a/after/ftplugin/cp.lua +++ b/after/ftplugin/cp.lua @@ -4,3 +4,4 @@ vim.opt_local.statuscolumn = '' vim.opt_local.signcolumn = 'no' vim.opt_local.wrap = true vim.opt_local.linebreak = true +vim.opt_local.foldcolumn = '0' diff --git a/after/ftplugin/cpin.lua b/after/ftplugin/cpin.lua deleted file mode 100644 index 622ad6a..0000000 --- a/after/ftplugin/cpin.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/ftplugin/cpout.lua b/after/ftplugin/cpout.lua deleted file mode 100644 index 1f4855f..0000000 --- a/after/ftplugin/cpout.lua +++ /dev/null @@ -1,7 +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 -vim.opt_local.modifiable = true diff --git a/after/ftplugin/cptest.lua b/after/ftplugin/cptest.lua deleted file mode 100644 index eab89a5..0000000 --- a/after/ftplugin/cptest.lua +++ /dev/null @@ -1,7 +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 -vim.opt_local.foldcolumn = '0' diff --git a/after/syntax/cp.vim b/after/syntax/cp.vim deleted file mode 100644 index fe9eef7..0000000 --- a/after/syntax/cp.vim +++ /dev/null @@ -1,17 +0,0 @@ -if exists("b:current_syntax") - finish -endif - -syntax match cpOutputCode /^\[code\]:/ -syntax match cpOutputTime /^\[time\]:/ -syntax match cpOutputDebug /^\[debug\]:/ -syntax match cpOutputOkTrue /^\[ok\]:\ze true$/ -syntax match cpOutputOkFalse /^\[ok\]:\ze false$/ - -highlight default link cpOutputCode DiagnosticInfo -highlight default link cpOutputTime Comment -highlight default link cpOutputDebug Comment -highlight default link cpOutputOkTrue DiffAdd -highlight default link cpOutputOkFalse DiffDelete - -let b:current_syntax = "cp" diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 0dfc82d..e3afa98 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -16,10 +16,7 @@ REQUIREMENTS *cp-requirements* - Neovim 0.10.0+ - Unix-like operating system - -Optional: - uv package manager (https://docs.astral.sh/uv/) -- LuaSnip for template expansion (https://github.com/L3MON4D3/LuaSnip) ============================================================================== COMMANDS *cp-commands* @@ -51,11 +48,13 @@ COMMANDS *cp-commands* :CP codeforces 1951 < Action Commands ~ - :CP run [--debug] Toggle run panel for individual test case - debugging. Shows per-test results with redesigned + :CP run Toggle run panel for individual test cases. + Shows per-test results with redesigned layout for efficient comparison. - Use --debug flag to compile with debug flags. - Requires contest setup first. + + :CP debug + Same as above but with the debug mode configured + settings. :CP pick Launch configured picker for interactive platform/contest selection. @@ -65,10 +64,12 @@ COMMANDS *cp-commands* Stops at last problem (no wrapping). - Navigation Commands ~ :CP prev Navigate to previous problem in current contest. Stops at first problem (no wrapping). + :CP {problem_id} Jump to problem {problem_id} in a contest. + Requires that a contest has already been set up. + Cache Commands ~ :CP cache clear [contest] Clear the cache data (contest list, problem @@ -79,20 +80,6 @@ COMMANDS *cp-commands* View the cache in a pretty-printed lua buffer. Exit with q. -Command Flags ~ - *cp-flags* - Flags can be used with setup and action commands: - - --debug Use the debug command template. - For compiled languages, this selects - `commands.debug` (a debug *build*) instead of - `commands.build`. For interpreted languages, - this selects `commands.debug` in place of - `commands.run`. - Example: > - :CP run --debug -< - Template Variables ~ *cp-template-vars* Command templates support variable substitution using `{variable}` syntax: @@ -136,7 +123,6 @@ Here's an example configuration with lazy.nvim: >lua }, }, }, - platforms = { cses = { enabled_languages = { 'cpp', 'python' }, @@ -154,10 +140,7 @@ Here's an example configuration with lazy.nvim: >lua default_language = 'cpp', }, }, - - snippets = {}, debug = false, - ui = { run_panel = { ansi = true, @@ -223,7 +206,6 @@ run CSES problems with Rust using the single schema: {platforms} (table) Per-platform enablement, default language, and optional overrides. {hooks} (|cp.Hooks|) Hook functions called at various stages. - {snippets} (table[]) LuaSnip snippet definitions. {debug} (boolean, default: false) Show info messages. {scrapers} (string[]) Supported platform ids. {filename} (function, optional) @@ -359,22 +341,18 @@ Example: Setting up and solving AtCoder contest ABC324 < Navigate with j/k, run specific tests with Exit test panel with q or :CP run when done -5. If needed, debug with sanitizers: > - :CP run --debug -< - -6. Move to next problem: > +5. Move to next problem: > :CP next < This automatically sets up problem B -7. Continue solving problems with :CP next/:CP prev navigation +6. Continue solving problems with :CP next/:CP prev navigation -8. Switch to another file (e.g. previous contest): > +7. Switch to another file (e.g. previous contest): > :e ~/contests/abc323/a.cpp :CP < Automatically restores abc323 contest context -9. Submit solutions on AtCoder website +8. Submit solutions on AtCoder website ============================================================================== PICKER INTEGRATION *cp-picker* @@ -396,18 +374,9 @@ PICKER KEYMAPS *cp-picker-keys* ============================================================================== RUN PANEL *cp-run* -The run panel provides individual test case debugging with a streamlined -layout optimized for modern screens. Shows test status with competitive -programming terminology and efficient space usage. - -Activation ~ - *:CP-run* -:CP run [--debug] Toggle run panel on/off. When activated, - replaces current layout with test interface. - Automatically compiles and runs all tests. - Use --debug flag to compile with debug symbols - and sanitizers. Toggle again to restore original - layout. +The run panel provides individual test case debugging. Problem time/memory +limit constraints are in columns Time/Mem respectively. Used time/memory are +in columns Runtime/RSS respectively. Interface ~ @@ -441,7 +410,7 @@ Test cases use competitive programming terminology with color highlighting: TLE Time Limit Exceeded (timeout) MLE Memory Limit Exceeded Error (heuristic) RTE Runtime Error (other non-zero exit code) - NA Any other state (undecipherable, error, running) + NA Any other state < ============================================================================== @@ -450,13 +419,28 @@ ANSI COLORS AND HIGHLIGHTING *cp-ansi* cp.nvim provides comprehensive ANSI color support and highlighting for compiler output, program stderr, and diff visualization. +If you cannot see color highlighting in your config, it is likely due to an +erroneous config. Most tools (GCC, Python, Clang, Rustc) color stdout based on +whether stdout is connected to a terminal. One can usually get aorund this by +leveraging flags to force colored output. For example, to force colors with GCC, +alter your config as follows: + + { + commands = { + build = { + 'g++', + '-fdiagnostics-color=always', + ... + } + } + } + + ============================================================================== HIGHLIGHT GROUPS *cp-highlights* Test Status Groups ~ -Test cases use competitive programming terminology with color highlighting: - CpTestAC Green foreground for AC status CpTestWA Red foreground for WA status CpTestTLE Orange foreground for TLE status @@ -470,56 +454,21 @@ cp.nvim preserves ANSI colors from compiler output and program stderr using a sophisticated parsing system. Colors are automatically mapped to your terminal colorscheme via vim.g.terminal_color_* variables. -Basic formatting groups: - CpAnsiBold Bold text formatting - CpAnsiItalic Italic text formatting - CpAnsiBoldItalic Combined bold and italic formatting - -Standard terminal colors (each supports Bold, Italic, BoldItalic variants): - CpAnsiRed Standard red (terminal_color_1) - CpAnsiGreen Standard green (terminal_color_2) - CpAnsiYellow Standard yellow (terminal_color_3) - CpAnsiBlue Standard blue (terminal_color_4) - CpAnsiMagenta Standard magenta (terminal_color_5) - CpAnsiCyan Standard cyan (terminal_color_6) - CpAnsiWhite Standard white (terminal_color_7) - CpAnsiBlack Standard black (terminal_color_0) - -Bright color variants: - CpAnsiBrightRed Bright red (terminal_color_9) - CpAnsiBrightGreen Bright green (terminal_color_10) - CpAnsiBrightYellow Bright yellow (terminal_color_11) - CpAnsiBrightBlue Bright blue (terminal_color_12) - CpAnsiBrightMagenta Bright magenta (terminal_color_13) - CpAnsiBrightCyan Bright cyan (terminal_color_14) - CpAnsiBrightWhite Bright white (terminal_color_15) - CpAnsiBrightBlack Bright black (terminal_color_8) - -Example combinations: - CpAnsiBoldRed Bold red combination - CpAnsiItalicGreen Italic green combination - CpAnsiBoldItalicYellow Bold italic yellow combination - Diff Highlighting ~ -Diff visualization uses Neovim's built-in highlight groups that automatically +The git diff backend uses Neovim's built-in highlight groups that automatically adapt to your colorscheme: DiffAdd Highlights added text in git diffs DiffDelete Highlights removed text in git diffs -These groups are automatically used by the git diff backend for character-level -difference visualization with optimal colorscheme integration. - ============================================================================== TERMINAL COLOR INTEGRATION *cp-terminal-colors* -ANSI colors automatically use your terminal's color palette through Neovim's -vim.g.terminal_color_* variables. This ensures compiler colors match your -colorscheme without manual configuration. +ANSI colors automatically use the terminal's color palette through Neovim's +vim.g.terminal_color_* variables. -If your colorscheme doesn't set terminal colors, cp.nvim will warn you and -ANSI colors won't display properly - set them like so: >vim +If your colorscheme doesn't set terminal colors, set them like so: >vim let g:terminal_color_1 = '#ff6b6b' ... @@ -550,6 +499,7 @@ prevent them from being overridden: >lua ============================================================================== RUN PANEL KEYMAPS *cp-test-keys* + Navigate to next test case Navigate to previous test case t Cycle through diff modes: none → git → vim @@ -558,27 +508,27 @@ q Exit run panel and restore layout Diff Modes ~ -Two diff backends are available: +Three diff backends are available: + none Nothing vim Built-in vim diff (default, always available) git Character-level git word-diff (requires git, more precise) The git backend shows character-level changes with [-removed-] and {+added+} -markers for precise difference analysis. +markers. Execution Details ~ Test cases are executed individually using the same compilation and execution pipeline, but with isolated input/output for -precise failure analysis. All tests are automatically run when the -panel opens. +precise failure analysis. ============================================================================== FILE STRUCTURE *cp-files* cp.nvim creates the following file structure upon problem setup: > - {problem_id}.{ext} " Source file (e.g. a.cc, b.py) + {problem_id}.{ext} " Source file build/ {problem_id}.run " Compiled binary io/ @@ -586,27 +536,6 @@ cp.nvim creates the following file structure upon problem setup: > {problem_id}.n.cpout " nth program output {problem_id}.expected " Expected output < -============================================================================== -SNIPPETS *cp-snippets* - -cp.nvim integrates with LuaSnip for automatic template expansion. Built-in -snippets include basic C++ and Python templates for each contest type. - -Snippet trigger names must match the following format exactly: > - - cp.nvim/{platform}.{language} -< -Where {platform} is the contest platform (atcoder, codeforces, cses) and -{language} is the programming language (cpp, python). - -Examples: > - cp.nvim/atcoder.cpp - cp.nvim/codeforces.python - cp.nvim/cses.cpp -< - -Custom snippets can be added via the `snippets` configuration field. - ============================================================================== HEALTH CHECK *cp-health* diff --git a/ftdetect/cp.lua b/ftdetect/cp.lua deleted file mode 100644 index d5c6327..0000000 --- a/ftdetect/cp.lua +++ /dev/null @@ -1,6 +0,0 @@ -vim.filetype.add({ - extension = { - cpin = 'cpin', - cpout = 'cpout', - }, -}) diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 6923a0f..73f3338 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -10,11 +10,11 @@ local actions = constants.ACTIONS ---@class ParsedCommand ---@field type string ---@field error string? ----@field debug? boolean ---@field action? string ---@field message? string ---@field contest? string ---@field platform? string +---@field problem_id? string --- Turn raw args into normalized structure to later dispatch ---@param args string[] The raw command-line mode args @@ -26,22 +26,16 @@ local function parse_command(args) } end - local debug = vim.tbl_contains(args, '--debug') - - local filtered_args = vim.tbl_filter(function(arg) - return arg ~= '--debug' - end, args) - - local first = filtered_args[1] + local first = args[1] if vim.tbl_contains(actions, first) then if first == 'cache' then - local subcommand = filtered_args[2] + local subcommand = args[2] if not subcommand then return { type = 'error', message = 'cache command requires subcommand: clear' } end if vim.tbl_contains({ 'clear', 'read' }, subcommand) then - local platform = filtered_args[3] + local platform = args[3] return { type = 'cache', subcommand = subcommand, @@ -51,42 +45,40 @@ local function parse_command(args) return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand } end else - return { type = 'action', action = first, debug = debug } + return { type = 'action', action = first } end end if vim.tbl_contains(platforms, first) then - if #filtered_args == 1 then + if #args == 1 then return { type = 'error', message = 'Too few arguments - specify a contest.', } - elseif #filtered_args == 2 then + elseif #args == 2 then return { type = 'contest_setup', platform = first, - contest = filtered_args[2], + contest = args[2], } - elseif #filtered_args == 3 then + elseif #args == 3 then return { type = 'error', - message = 'Setup contests with :CP ', + message = 'Setup contests with :CP .', } else return { type = 'error', message = 'Too many arguments' } end end - if state.get_platform() and state.get_contest_id() then - local cache = require('cp.cache') - cache.load() + if #args == 1 then return { - type = 'error', - message = ("invalid subcommand '%s'"):format(first), + type = 'problem_jump', + problem_id = first, } end - return { type = 'error', message = 'Unknown command or no contest context' } + return { type = 'error', message = 'Unknown command or no contest context.' } end --- Core logic for handling `:CP ...` commands @@ -109,7 +101,9 @@ function M.handle_command(opts) if cmd.action == 'interact' then ui.toggle_interactive() elseif cmd.action == 'run' then - ui.toggle_run_panel(cmd.debug) + ui.toggle_run_panel() + elseif cmd.action == 'debug' then + ui.toggle_run_panel({ debug = true }) elseif cmd.action == 'next' then setup.navigate_problem(1) elseif cmd.action == 'prev' then @@ -118,6 +112,30 @@ function M.handle_command(opts) local picker = require('cp.commands.picker') picker.handle_pick_action() end + elseif cmd.type == 'problem_jump' then + local platform = state.get_platform() + local contest_id = state.get_contest_id() + local problem_id = cmd.problem_id + + if not (platform and contest_id) then + logger.log('No contest is currently active.', vim.log.levels.ERROR) + return + end + + local cache = require('cp.cache') + cache.load() + local contest_data = cache.get_contest_data(platform, contest_id) + + if not (contest_data and contest_data.index_map and contest_data.index_map[problem_id]) then + logger.log( + ("%s contest '%s' has no problem '%s'."):format(platform, contest_id, problem_id), + vim.log.levels.ERROR + ) + return + end + + local setup = require('cp.setup') + setup.setup_contest(platform, contest_id, problem_id) elseif cmd.type == 'cache' then local cache_commands = require('cp.commands.cache') cache_commands.handle_cache_command(cmd) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index f58c369..31540cf 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -42,7 +42,6 @@ ---@field languages table ---@field platforms table ---@field hooks Hooks ----@field snippets any[] ---@field debug boolean ---@field scrapers string[] ---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string @@ -100,7 +99,6 @@ M.defaults = { default_language = 'cpp', }, }, - snippets = {}, hooks = { before_run = nil, before_debug = nil, setup_code = nil }, debug = false, scrapers = constants.PLATFORMS, diff --git a/lua/cp/health.lua b/lua/cp/health.lua index c5e5113..ba3879a 100644 --- a/lua/cp/health.lua +++ b/lua/cp/health.lua @@ -2,7 +2,7 @@ local M = {} local utils = require('cp.utils') -local function check_required() +local function check() vim.health.start('cp.nvim [required] ~') if vim.fn.has('nvim-0.10.0') == 1 then @@ -49,24 +49,12 @@ local function check_required() end end -local function check_optional() - vim.health.start('cp.nvim [optional] ~') - - local has_luasnip = pcall(require, 'luasnip') - if has_luasnip then - vim.health.ok('LuaSnip integration available') - else - vim.health.info('LuaSnip not available (templates optional)') - end -end - function M.check() local version = require('cp.version') vim.health.start('cp.nvim health check ~') vim.health.info('Version: ' .. version.version) - check_required() - check_optional() + check() end return M diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 88467ee..4e2be8a 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -2,7 +2,6 @@ local M = {} local config_module = require('cp.config') local logger = require('cp.log') -local snippets = require('cp.snippets') if vim.fn.has('nvim-0.10.0') == 0 then logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR) @@ -11,7 +10,6 @@ end local user_config = {} local config = nil -local snippets_initialized = false local initialized = false --- Root handler for all `:CP ...` commands @@ -27,10 +25,6 @@ function M.setup(opts) config = config_module.setup(user_config) config_module.set_current_config(config) - if not snippets_initialized then - snippets.setup(config) - snippets_initialized = true - end initialized = true end diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 0e7c8f4..4127c93 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -107,26 +107,6 @@ function M.setup_problem(problem_id, language) local lang = language or config.platforms[platform].default_language local source_file = state.get_source_file(lang) vim.cmd.e(source_file) - local source_buf = vim.api.nvim_get_current_buf() - - if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == '' then - local ok, luasnip = pcall(require, 'luasnip') - if ok then - local trigger = ('cp.nvim/%s.%s'):format(platform, lang) - vim.api.nvim_buf_set_lines(0, 0, -1, false, { trigger }) - vim.api.nvim_win_set_cursor(0, { 1, #trigger }) - vim.cmd.startinsert({ bang = true }) - vim.schedule(function() - if luasnip.expandable() then - luasnip.expand() - else - vim.api.nvim_buf_set_lines(0, 0, 1, false, { '' }) - vim.api.nvim_win_set_cursor(0, { 1, 0 }) - end - vim.cmd.stopinsert() - end) - end - end if config.hooks and config.hooks.setup_code then config.hooks.setup_code(state) diff --git a/lua/cp/snippets.lua b/lua/cp/snippets.lua deleted file mode 100644 index 9108286..0000000 --- a/lua/cp/snippets.lua +++ /dev/null @@ -1,134 +0,0 @@ -local M = {} -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 are disabled.', vim.log.levels.INFO, true) - return - end - - 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 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 - -using namespace std; - -void solve() {{ - {} -}} - -int main() {{ - std::cin.tie(nullptr)->sync_with_stdio(false); - - int tc = 1; - std::cin >> tc; - - for (int t = 0; t < tc; ++t) {{ - solve(); - }} - - return 0; -}}]], - - atcoder = [[#include - -using namespace std; - -void solve() {{ - {} -}} - -int main() {{ - std::cin.tie(nullptr)->sync_with_stdio(false); - -#ifdef LOCAL - int tc; - std::cin >> tc; - - for (int t = 0; t < tc; ++t) {{ - solve(); - }} -#else - solve(); -#endif - - return 0; -}}]], - - cses = [[#include - -using namespace std; - -int main() {{ - std::cin.tie(nullptr)->sync_with_stdio(false); - - {} - - return 0; -}}]], - }, - - python = { - codeforces = [[def solve(): - {} - -if __name__ == "__main__": - tc = int(input()) - for _ in range(tc): - solve()]], - - atcoder = [[def solve(): - {} - -if __name__ == "__main__": - solve()]], - - cses = [[def solve(): - {} - -if __name__ == "__main__": - solve()]], - }, - } - - local user_overrides = {} - for _, snippet in ipairs(config.snippets or {}) do - user_overrides[snippet.trigger:lower()] = snippet - end - - 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:lower(), language) - if not user_overrides[prefixed_trigger:lower()] 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:lower():match('^cp%.nvim/[^.]+%.(.+)$') - if prefix_match == language then - table.insert(snippets, snippet) - end - end - - ls.add_snippets(filetype, snippets) - end -end - -return M diff --git a/lua/cp/state.lua b/lua/cp/state.lua index c90396c..e228212 100644 --- a/lua/cp/state.lua +++ b/lua/cp/state.lua @@ -65,6 +65,10 @@ function M.get_base_name() end end +function M.get_language() + return +end + function M.get_source_file(language) local base_name = M.get_base_name() if not base_name or not M.get_platform() then diff --git a/lua/cp/ui/ansi.lua b/lua/cp/ui/ansi.lua index 8bc8581..a72bcad 100644 --- a/lua/cp/ui/ansi.lua +++ b/lua/cp/ui/ansi.lua @@ -10,10 +10,46 @@ local M = {} -local logger = require('cp.log') - local dyn_hl_cache = {} +local ANSI_TERMINAL_COLOR_CODE_FALLBACK = { + [0] = '#000000', + [1] = '#800000', + [2] = '#008000', + [3] = '#808000', + [4] = '#000080', + [5] = '#800080', + [6] = '#008080', + [7] = '#c0c0c0', + [8] = '#808080', + [9] = '#ff0000', + [10] = '#00ff00', + [11] = '#ffff00', + [12] = '#0000ff', + [13] = '#ff00ff', + [14] = '#00ffff', + [15] = '#ffffff', +} + +local function xterm_to_hex(n) + if n >= 0 and n <= 15 then + local key = 'terminal_color_' .. n + return vim.g[key] or ANSI_TERMINAL_COLOR_CODE_FALLBACK[n] + end + if n >= 16 and n <= 231 then + local c = n - 16 + local r = math.floor(c / 36) % 6 + local g = math.floor(c / 6) % 6 + local b = c % 6 + local function level(x) + return x == 0 and 0 or 55 + 40 * x + end + return ('#%02x%02x%02x'):format(level(r), level(g), level(b)) + end + local l = 8 + 10 * (n - 232) + return ('#%02x%02x%02x'):format(l, l, l) +end + ---@param s string|table ---@return string function M.bytes_to_string(s) @@ -40,24 +76,7 @@ local function ensure_hl_for(fg, bold, italic) suffix = fg.name elseif fg and fg.kind == 'xterm' then suffix = ('X%03d'):format(fg.idx) - local function xterm_to_hex(n) - if n >= 0 and n <= 15 then - local key = 'terminal_color_' .. n - return vim.g[key] - end - if n >= 16 and n <= 231 then - local c = n - 16 - local r = math.floor(c / 36) % 6 - local g = math.floor(c / 6) % 6 - local b = c % 6 - local function level(x) - return x == 0 and 0 or 55 + 40 * x - end - return ('#%02x%02x%02x'):format(level(r), level(g), level(b)) - end - local l = 8 + 10 * (n - 232) - return ('#%02x%02x%02x'):format(l, l, l) - end + opts.fg = xterm_to_hex(fg.idx) or 'NONE' elseif fg and fg.kind == 'rgb' then suffix = ('Rgb%02x%02x%02x'):format(fg.r, fg.g, fg.b) @@ -256,31 +275,24 @@ end ---@return nil function M.setup_highlight_groups() local color_map = { - Black = vim.g.terminal_color_0, - Red = vim.g.terminal_color_1, - Green = vim.g.terminal_color_2, - Yellow = vim.g.terminal_color_3, - Blue = vim.g.terminal_color_4, - Magenta = vim.g.terminal_color_5, - Cyan = vim.g.terminal_color_6, - White = vim.g.terminal_color_7, - BrightBlack = vim.g.terminal_color_8, - BrightRed = vim.g.terminal_color_9, - BrightGreen = vim.g.terminal_color_10, - BrightYellow = vim.g.terminal_color_11, - BrightBlue = vim.g.terminal_color_12, - BrightMagenta = vim.g.terminal_color_13, - BrightCyan = vim.g.terminal_color_14, - BrightWhite = vim.g.terminal_color_15, + Black = vim.g.terminal_color_0 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[0], + Red = vim.g.terminal_color_1 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[1], + Green = vim.g.terminal_color_2 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[2], + Yellow = vim.g.terminal_color_3 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[3], + Blue = vim.g.terminal_color_4 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[4], + Magenta = vim.g.terminal_color_5 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[5], + Cyan = vim.g.terminal_color_6 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[6], + White = vim.g.terminal_color_7 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[7], + BrightBlack = vim.g.terminal_color_8 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[8], + BrightRed = vim.g.terminal_color_9 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[9], + BrightGreen = vim.g.terminal_color_10 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[10], + BrightYellow = vim.g.terminal_color_11 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[11], + BrightBlue = vim.g.terminal_color_12 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[12], + BrightMagenta = vim.g.terminal_color_13 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[13], + BrightCyan = vim.g.terminal_color_14 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[14], + BrightWhite = vim.g.terminal_color_15 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[15], } - if vim.tbl_count(color_map) < 16 then - logger.log( - 'ansi terminal colors (vim.g.terminal_color_*) not configured. ANSI colors will not display properly.', - vim.log.levels.WARN - ) - end - local combinations = { { bold = false, italic = false }, { bold = true, italic = false }, diff --git a/lua/cp/ui/layouts.lua b/lua/cp/ui/layouts.lua index c2613a5..ea73cd7 100644 --- a/lua/cp/ui/layouts.lua +++ b/lua/cp/ui/layouts.lua @@ -16,8 +16,8 @@ local function create_none_diff_layout(parent_win, expected_content, actual_cont local expected_win = vim.api.nvim_get_current_win() vim.api.nvim_win_set_buf(expected_win, expected_buf) - vim.api.nvim_set_option_value('filetype', 'cptest', { buf = expected_buf }) - vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_buf }) + vim.api.nvim_set_option_value('filetype', 'cp', { buf = expected_buf }) + vim.api.nvim_set_option_value('filetype', 'cp', { buf = actual_buf }) vim.api.nvim_set_option_value('winbar', 'Expected', { win = expected_win }) vim.api.nvim_set_option_value('winbar', 'Actual', { win = actual_win }) @@ -53,8 +53,8 @@ local function create_vim_diff_layout(parent_win, expected_content, actual_conte local expected_win = vim.api.nvim_get_current_win() vim.api.nvim_win_set_buf(expected_win, expected_buf) - vim.api.nvim_set_option_value('filetype', 'cptest', { buf = expected_buf }) - vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_buf }) + vim.api.nvim_set_option_value('filetype', 'cp', { buf = expected_buf }) + vim.api.nvim_set_option_value('filetype', 'cp', { buf = actual_buf }) vim.api.nvim_set_option_value('winbar', 'Expected', { win = expected_win }) vim.api.nvim_set_option_value('winbar', 'Actual', { win = actual_win }) @@ -96,7 +96,7 @@ local function create_git_diff_layout(parent_win, expected_content, actual_conte local diff_win = vim.api.nvim_get_current_win() vim.api.nvim_win_set_buf(diff_win, diff_buf) - vim.api.nvim_set_option_value('filetype', 'cptest', { buf = diff_buf }) + vim.api.nvim_set_option_value('filetype', 'cp', { buf = diff_buf }) vim.api.nvim_set_option_value('winbar', 'Expected vs Actual', { win = diff_win }) local diff_backend = require('cp.ui.diff') @@ -132,7 +132,7 @@ local function create_single_layout(parent_win, content) vim.cmd('resize ' .. math.floor(vim.o.lines * 0.35)) local win = vim.api.nvim_get_current_win() vim.api.nvim_win_set_buf(win, buf) - vim.api.nvim_set_option_value('filetype', 'cptest', { buf = buf }) + vim.api.nvim_set_option_value('filetype', 'cp', { buf = buf }) return { buffers = { buf }, diff --git a/lua/cp/ui/panel.lua b/lua/cp/ui/panel.lua index 4467f0e..949d503 100644 --- a/lua/cp/ui/panel.lua +++ b/lua/cp/ui/panel.lua @@ -1,5 +1,8 @@ local M = {} +---@class RunOpts +---@field debug? boolean + local config_module = require('cp.config') local layouts = require('cp.ui.layouts') local logger = require('cp.log') @@ -51,19 +54,13 @@ function M.toggle_interactive() local platform, contest_id = state.get_platform(), state.get_contest_id() if not platform then - logger.log( - 'No platform configured. Use :CP [--{lang=,debug}] first.', - vim.log.levels.ERROR - ) + logger.log('No platform configured.', vim.log.levels.ERROR) return end if not contest_id then logger.log( - ('No contest %s configured for platform %s. Use :CP [--{lang=,debug}] to set up first.'):format( - contest_id, - platform - ), + ('No contest %s configured for platform %s.'):format(contest_id, platform), vim.log.levels.ERROR ) return @@ -118,8 +115,8 @@ function M.toggle_interactive() state.set_active_panel('interactive') end ----@param debug? boolean -function M.toggle_run_panel(debug) +---@param run_opts? RunOpts +function M.toggle_run_panel(run_opts) if state.get_active_panel() == 'run' then if current_diff_layout then current_diff_layout.cleanup() @@ -152,10 +149,7 @@ function M.toggle_run_panel(debug) if not contest_id then logger.log( - ('No contest %s configured for platform %s. Use :CP [--{lang=,debug}] to set up first.'):format( - contest_id, - platform - ), + ('No contest %s configured for platform %s.'):format(contest_id, platform), vim.log.levels.ERROR ) return @@ -187,13 +181,6 @@ function M.toggle_run_panel(debug) ) local config = config_module.get_config() - if config.hooks and config.hooks.before_run then - config.hooks.before_run(state) - end - if debug and config.hooks and config.hooks.before_debug then - config.hooks.before_debug(state) - end - local run = require('cp.runner.run') local input_file = state.get_input_file() logger.log(('run panel: checking test cases for %s'):format(input_file or 'none')) @@ -210,7 +197,7 @@ function M.toggle_run_panel(debug) local tab_buf = utils.create_buffer_with_options() local main_win = vim.api.nvim_get_current_win() vim.api.nvim_win_set_buf(main_win, tab_buf) - vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf }) + vim.api.nvim_set_option_value('filetype', 'cp', { buf = tab_buf }) local test_windows = { tab_win = main_win } local test_buffers = { tab_buf = tab_buf } @@ -282,6 +269,17 @@ function M.toggle_run_panel(debug) setup_keybindings_for_buffer(test_buffers.tab_buf) + if config.hooks and config.hooks.before_run then + vim.schedule_wrap(function() + config.hooks.before_run(state) + end) + end + if run_opts and run_opts.debug and config.hooks and config.hooks.before_debug then + vim.schedule_wrap(function() + config.hooks.before_debug(state) + end) + end + local execute = require('cp.runner.execute') local compile_result = execute.compile_problem() if compile_result.success then diff --git a/plugin/cp.lua b/plugin/cp.lua index 193beeb..7081f15 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -23,22 +23,26 @@ end, { if num_args == 2 then local candidates = {} local state = require('cp.state') - local platform, contest_id = state.get_platform(), state.get_contest_id() + local platform = state.get_platform() + local contest_id = state.get_contest_id() + if platform and contest_id then vim.list_extend(candidates, actions) local cache = require('cp.cache') cache.load() local contest_data = cache.get_contest_data(platform, contest_id) - if contest_data and contest_data.problems then - for _, problem in ipairs(contest_data.problems) do - table.insert(candidates, problem.id) - end + + if contest_data and contest_data.index_map then + local ids = vim.tbl_keys(contest_data.index_map) + table.sort(ids) + vim.list_extend(candidates, ids) end else vim.list_extend(candidates, platforms) table.insert(candidates, 'cache') table.insert(candidates, 'pick') end + return vim.tbl_filter(function(cmd) return cmd:find(ArgLead, 1, true) == 1 end, candidates)