Merge pull request #146 from barrett-ruth/feat/cli-enhancements

fixups
This commit is contained in:
Barrett Ruth 2025-10-05 19:01:35 +02:00 committed by GitHub
commit 5e9c00014d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 185 additions and 438 deletions

View file

@ -10,15 +10,13 @@ https://github.com/user-attachments/assets/50b19481-8e6d-47b4-bebc-15e16c61a9c9
- **Multi-platform support**: AtCoder, Codeforces, CSES with consistent interface - **Multi-platform support**: AtCoder, Codeforces, CSES with consistent interface
- **Automatic problem setup**: Scrape test cases and metadata in seconds - **Automatic problem setup**: Scrape test cases and metadata in seconds
- **Rich test output**: ANSI color support for compiler errors and program output - **Rich test output**: 256 color ANSI support for compiler errors and program output
- **Language agnostic**: Works with any compiled language - **Language agnostic**: Works with any language
- **Template integration**: Contest-specific snippets via LuaSnip - **Diff viewer**: Compare expected vs actual output with 3 diff modes
- **Diff viewer**: Compare expected vs actual output with precision
## Optional Dependencies ## Optional Dependencies
- [uv](https://docs.astral.sh/uv/) for problem scraping - [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) - GNU [time](https://www.gnu.org/software/time/) and [timeout](https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html)
## Quick Start ## Quick Start

View file

@ -4,3 +4,4 @@ vim.opt_local.statuscolumn = ''
vim.opt_local.signcolumn = 'no' vim.opt_local.signcolumn = 'no'
vim.opt_local.wrap = true vim.opt_local.wrap = true
vim.opt_local.linebreak = true vim.opt_local.linebreak = true
vim.opt_local.foldcolumn = '0'

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

@ -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

View file

@ -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'

View file

@ -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"

View file

@ -16,10 +16,7 @@ REQUIREMENTS *cp-requirements*
- Neovim 0.10.0+ - Neovim 0.10.0+
- Unix-like operating system - Unix-like operating system
Optional:
- uv package manager (https://docs.astral.sh/uv/) - uv package manager (https://docs.astral.sh/uv/)
- LuaSnip for template expansion (https://github.com/L3MON4D3/LuaSnip)
============================================================================== ==============================================================================
COMMANDS *cp-commands* COMMANDS *cp-commands*
@ -51,11 +48,13 @@ COMMANDS *cp-commands*
:CP codeforces 1951 :CP codeforces 1951
< <
Action Commands ~ Action Commands ~
:CP run [--debug] Toggle run panel for individual test case :CP run Toggle run panel for individual test cases.
debugging. Shows per-test results with redesigned Shows per-test results with redesigned
layout for efficient comparison. 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 :CP pick Launch configured picker for interactive
platform/contest selection. platform/contest selection.
@ -65,10 +64,12 @@ COMMANDS *cp-commands*
Stops at last problem (no wrapping). Stops at last problem (no wrapping).
Navigation Commands ~
:CP prev Navigate to previous problem in current contest. :CP prev Navigate to previous problem in current contest.
Stops at first problem (no wrapping). 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 ~ Cache Commands ~
:CP cache clear [contest] :CP cache clear [contest]
Clear the cache data (contest list, problem Clear the cache data (contest list, problem
@ -79,20 +80,6 @@ COMMANDS *cp-commands*
View the cache in a pretty-printed lua buffer. View the cache in a pretty-printed lua buffer.
Exit with q. 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 ~ Template Variables ~
*cp-template-vars* *cp-template-vars*
Command templates support variable substitution using `{variable}` syntax: Command templates support variable substitution using `{variable}` syntax:
@ -136,7 +123,6 @@ Here's an example configuration with lazy.nvim: >lua
}, },
}, },
}, },
platforms = { platforms = {
cses = { cses = {
enabled_languages = { 'cpp', 'python' }, enabled_languages = { 'cpp', 'python' },
@ -154,10 +140,7 @@ Here's an example configuration with lazy.nvim: >lua
default_language = 'cpp', default_language = 'cpp',
}, },
}, },
snippets = {},
debug = false, debug = false,
ui = { ui = {
run_panel = { run_panel = {
ansi = true, ansi = true,
@ -223,7 +206,6 @@ run CSES problems with Rust using the single schema:
{platforms} (table<string,|CpPlatform|>) Per-platform enablement, {platforms} (table<string,|CpPlatform|>) Per-platform enablement,
default language, and optional overrides. default language, and optional overrides.
{hooks} (|cp.Hooks|) Hook functions called at various stages. {hooks} (|cp.Hooks|) Hook functions called at various stages.
{snippets} (table[]) LuaSnip snippet definitions.
{debug} (boolean, default: false) Show info messages. {debug} (boolean, default: false) Show info messages.
{scrapers} (string[]) Supported platform ids. {scrapers} (string[]) Supported platform ids.
{filename} (function, optional) {filename} (function, optional)
@ -359,22 +341,18 @@ Example: Setting up and solving AtCoder contest ABC324
< Navigate with j/k, run specific tests with <enter> < Navigate with j/k, run specific tests with <enter>
Exit test panel with q or :CP run when done Exit test panel with q or :CP run when done
5. If needed, debug with sanitizers: > 5. Move to next problem: >
:CP run --debug
<
6. Move to next problem: >
:CP next :CP next
< This automatically sets up problem B < 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 :e ~/contests/abc323/a.cpp
:CP :CP
< Automatically restores abc323 contest context < Automatically restores abc323 contest context
9. Submit solutions on AtCoder website 8. Submit solutions on AtCoder website
============================================================================== ==============================================================================
PICKER INTEGRATION *cp-picker* PICKER INTEGRATION *cp-picker*
@ -396,18 +374,9 @@ PICKER KEYMAPS *cp-picker-keys*
============================================================================== ==============================================================================
RUN PANEL *cp-run* RUN PANEL *cp-run*
The run panel provides individual test case debugging with a streamlined The run panel provides individual test case debugging. Problem time/memory
layout optimized for modern screens. Shows test status with competitive limit constraints are in columns Time/Mem respectively. Used time/memory are
programming terminology and efficient space usage. in columns Runtime/RSS respectively.
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.
Interface ~ Interface ~
@ -441,7 +410,7 @@ Test cases use competitive programming terminology with color highlighting:
TLE Time Limit Exceeded (timeout) TLE Time Limit Exceeded (timeout)
MLE Memory Limit Exceeded Error (heuristic) MLE Memory Limit Exceeded Error (heuristic)
RTE Runtime Error (other non-zero exit code) 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 cp.nvim provides comprehensive ANSI color support and highlighting for
compiler output, program stderr, and diff visualization. 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* HIGHLIGHT GROUPS *cp-highlights*
Test Status Groups ~ Test Status Groups ~
Test cases use competitive programming terminology with color highlighting:
CpTestAC Green foreground for AC status CpTestAC Green foreground for AC status
CpTestWA Red foreground for WA status CpTestWA Red foreground for WA status
CpTestTLE Orange foreground for TLE 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 a sophisticated parsing system. Colors are automatically mapped to your
terminal colorscheme via vim.g.terminal_color_* variables. 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 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: adapt to your colorscheme:
DiffAdd Highlights added text in git diffs DiffAdd Highlights added text in git diffs
DiffDelete Highlights removed 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* TERMINAL COLOR INTEGRATION *cp-terminal-colors*
ANSI colors automatically use your terminal's color palette through Neovim's ANSI colors automatically use the terminal's color palette through Neovim's
vim.g.terminal_color_* variables. This ensures compiler colors match your vim.g.terminal_color_* variables.
colorscheme without manual configuration.
If your colorscheme doesn't set terminal colors, cp.nvim will warn you and If your colorscheme doesn't set terminal colors, set them like so: >vim
ANSI colors won't display properly - set them like so: >vim
let g:terminal_color_1 = '#ff6b6b' let g:terminal_color_1 = '#ff6b6b'
... ...
@ -550,6 +499,7 @@ prevent them from being overridden: >lua
============================================================================== ==============================================================================
RUN PANEL KEYMAPS *cp-test-keys* RUN PANEL KEYMAPS *cp-test-keys*
<c-n> Navigate to next test case <c-n> Navigate to next test case
<c-p> Navigate to previous test case <c-p> Navigate to previous test case
t Cycle through diff modes: none → git → vim t Cycle through diff modes: none → git → vim
@ -558,27 +508,27 @@ q Exit run panel and restore layout
Diff Modes ~ Diff Modes ~
Two diff backends are available: Three diff backends are available:
none Nothing
vim Built-in vim diff (default, always available) vim Built-in vim diff (default, always available)
git Character-level git word-diff (requires git, more precise) git Character-level git word-diff (requires git, more precise)
The git backend shows character-level changes with [-removed-] and {+added+} The git backend shows character-level changes with [-removed-] and {+added+}
markers for precise difference analysis. markers.
Execution Details ~ Execution Details ~
Test cases are executed individually using the same compilation and Test cases are executed individually using the same compilation and
execution pipeline, but with isolated input/output for execution pipeline, but with isolated input/output for
precise failure analysis. All tests are automatically run when the precise failure analysis.
panel opens.
============================================================================== ==============================================================================
FILE STRUCTURE *cp-files* FILE STRUCTURE *cp-files*
cp.nvim creates the following file structure upon problem setup: > 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/ build/
{problem_id}.run " Compiled binary {problem_id}.run " Compiled binary
io/ io/
@ -586,27 +536,6 @@ cp.nvim creates the following file structure upon problem setup: >
{problem_id}.n.cpout " nth program output {problem_id}.n.cpout " nth program output
{problem_id}.expected " Expected 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* HEALTH CHECK *cp-health*

View file

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

View file

@ -10,11 +10,11 @@ local actions = constants.ACTIONS
---@class ParsedCommand ---@class ParsedCommand
---@field type string ---@field type string
---@field error string? ---@field error string?
---@field debug? boolean
---@field action? string ---@field action? string
---@field message? string ---@field message? string
---@field contest? string ---@field contest? string
---@field platform? string ---@field platform? string
---@field problem_id? string
--- Turn raw args into normalized structure to later dispatch --- Turn raw args into normalized structure to later dispatch
---@param args string[] The raw command-line mode args ---@param args string[] The raw command-line mode args
@ -26,22 +26,16 @@ local function parse_command(args)
} }
end end
local debug = vim.tbl_contains(args, '--debug') local first = args[1]
local filtered_args = vim.tbl_filter(function(arg)
return arg ~= '--debug'
end, args)
local first = filtered_args[1]
if vim.tbl_contains(actions, first) then if vim.tbl_contains(actions, first) then
if first == 'cache' then if first == 'cache' then
local subcommand = filtered_args[2] local subcommand = args[2]
if not subcommand then if not subcommand then
return { type = 'error', message = 'cache command requires subcommand: clear' } return { type = 'error', message = 'cache command requires subcommand: clear' }
end end
if vim.tbl_contains({ 'clear', 'read' }, subcommand) then if vim.tbl_contains({ 'clear', 'read' }, subcommand) then
local platform = filtered_args[3] local platform = args[3]
return { return {
type = 'cache', type = 'cache',
subcommand = subcommand, subcommand = subcommand,
@ -51,42 +45,40 @@ local function parse_command(args)
return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand } return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand }
end end
else else
return { type = 'action', action = first, debug = debug } return { type = 'action', action = first }
end end
end end
if vim.tbl_contains(platforms, first) then if vim.tbl_contains(platforms, first) then
if #filtered_args == 1 then if #args == 1 then
return { return {
type = 'error', type = 'error',
message = 'Too few arguments - specify a contest.', message = 'Too few arguments - specify a contest.',
} }
elseif #filtered_args == 2 then elseif #args == 2 then
return { return {
type = 'contest_setup', type = 'contest_setup',
platform = first, platform = first,
contest = filtered_args[2], contest = args[2],
} }
elseif #filtered_args == 3 then elseif #args == 3 then
return { return {
type = 'error', type = 'error',
message = 'Setup contests with :CP <platform> <contest_id>', message = 'Setup contests with :CP <platform> <contest_id>.',
} }
else else
return { type = 'error', message = 'Too many arguments' } return { type = 'error', message = 'Too many arguments' }
end end
end end
if state.get_platform() and state.get_contest_id() then if #args == 1 then
local cache = require('cp.cache')
cache.load()
return { return {
type = 'error', type = 'problem_jump',
message = ("invalid subcommand '%s'"):format(first), problem_id = first,
} }
end end
return { type = 'error', message = 'Unknown command or no contest context' } return { type = 'error', message = 'Unknown command or no contest context.' }
end end
--- Core logic for handling `:CP ...` commands --- Core logic for handling `:CP ...` commands
@ -109,7 +101,9 @@ function M.handle_command(opts)
if cmd.action == 'interact' then if cmd.action == 'interact' then
ui.toggle_interactive() ui.toggle_interactive()
elseif cmd.action == 'run' then 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 elseif cmd.action == 'next' then
setup.navigate_problem(1) setup.navigate_problem(1)
elseif cmd.action == 'prev' then elseif cmd.action == 'prev' then
@ -118,6 +112,30 @@ function M.handle_command(opts)
local picker = require('cp.commands.picker') local picker = require('cp.commands.picker')
picker.handle_pick_action() picker.handle_pick_action()
end 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 elseif cmd.type == 'cache' then
local cache_commands = require('cp.commands.cache') local cache_commands = require('cp.commands.cache')
cache_commands.handle_cache_command(cmd) cache_commands.handle_cache_command(cmd)

View file

@ -42,7 +42,6 @@
---@field languages table<string, CpLanguage> ---@field languages table<string, CpLanguage>
---@field platforms table<string, CpPlatform> ---@field platforms table<string, CpPlatform>
---@field hooks Hooks ---@field hooks Hooks
---@field snippets any[]
---@field debug boolean ---@field debug boolean
---@field scrapers string[] ---@field scrapers string[]
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string ---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
@ -100,7 +99,6 @@ M.defaults = {
default_language = 'cpp', default_language = 'cpp',
}, },
}, },
snippets = {},
hooks = { before_run = nil, before_debug = nil, setup_code = nil }, hooks = { before_run = nil, before_debug = nil, setup_code = nil },
debug = false, debug = false,
scrapers = constants.PLATFORMS, scrapers = constants.PLATFORMS,

View file

@ -2,7 +2,7 @@ local M = {}
local utils = require('cp.utils') local utils = require('cp.utils')
local function check_required() local function check()
vim.health.start('cp.nvim [required] ~') vim.health.start('cp.nvim [required] ~')
if vim.fn.has('nvim-0.10.0') == 1 then if vim.fn.has('nvim-0.10.0') == 1 then
@ -49,24 +49,12 @@ local function check_required()
end end
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() function M.check()
local version = require('cp.version') local version = require('cp.version')
vim.health.start('cp.nvim health check ~') vim.health.start('cp.nvim health check ~')
vim.health.info('Version: ' .. version.version) vim.health.info('Version: ' .. version.version)
check_required() check()
check_optional()
end end
return M return M

View file

@ -2,7 +2,6 @@ local M = {}
local config_module = require('cp.config') local config_module = require('cp.config')
local logger = require('cp.log') local logger = require('cp.log')
local snippets = require('cp.snippets')
if vim.fn.has('nvim-0.10.0') == 0 then if vim.fn.has('nvim-0.10.0') == 0 then
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR) logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR)
@ -11,7 +10,6 @@ end
local user_config = {} local user_config = {}
local config = nil local config = nil
local snippets_initialized = false
local initialized = false local initialized = false
--- Root handler for all `:CP ...` commands --- Root handler for all `:CP ...` commands
@ -27,10 +25,6 @@ function M.setup(opts)
config = config_module.setup(user_config) config = config_module.setup(user_config)
config_module.set_current_config(config) config_module.set_current_config(config)
if not snippets_initialized then
snippets.setup(config)
snippets_initialized = true
end
initialized = true initialized = true
end end

View file

@ -107,26 +107,6 @@ function M.setup_problem(problem_id, language)
local lang = language or config.platforms[platform].default_language local lang = language or config.platforms[platform].default_language
local source_file = state.get_source_file(lang) local source_file = state.get_source_file(lang)
vim.cmd.e(source_file) 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 if config.hooks and config.hooks.setup_code then
config.hooks.setup_code(state) config.hooks.setup_code(state)

View file

@ -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 <bits/stdc++.h>
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 <bits/stdc++.h>
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 <bits/stdc++.h>
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

View file

@ -65,6 +65,10 @@ function M.get_base_name()
end end
end end
function M.get_language()
return
end
function M.get_source_file(language) function M.get_source_file(language)
local base_name = M.get_base_name() local base_name = M.get_base_name()
if not base_name or not M.get_platform() then if not base_name or not M.get_platform() then

View file

@ -10,10 +10,46 @@
local M = {} local M = {}
local logger = require('cp.log')
local dyn_hl_cache = {} 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 ---@param s string|table
---@return string ---@return string
function M.bytes_to_string(s) function M.bytes_to_string(s)
@ -40,24 +76,7 @@ local function ensure_hl_for(fg, bold, italic)
suffix = fg.name suffix = fg.name
elseif fg and fg.kind == 'xterm' then elseif fg and fg.kind == 'xterm' then
suffix = ('X%03d'):format(fg.idx) 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' opts.fg = xterm_to_hex(fg.idx) or 'NONE'
elseif fg and fg.kind == 'rgb' then elseif fg and fg.kind == 'rgb' then
suffix = ('Rgb%02x%02x%02x'):format(fg.r, fg.g, fg.b) suffix = ('Rgb%02x%02x%02x'):format(fg.r, fg.g, fg.b)
@ -256,31 +275,24 @@ end
---@return nil ---@return nil
function M.setup_highlight_groups() function M.setup_highlight_groups()
local color_map = { local color_map = {
Black = vim.g.terminal_color_0, Black = vim.g.terminal_color_0 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[0],
Red = vim.g.terminal_color_1, Red = vim.g.terminal_color_1 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[1],
Green = vim.g.terminal_color_2, Green = vim.g.terminal_color_2 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[2],
Yellow = vim.g.terminal_color_3, Yellow = vim.g.terminal_color_3 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[3],
Blue = vim.g.terminal_color_4, Blue = vim.g.terminal_color_4 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[4],
Magenta = vim.g.terminal_color_5, Magenta = vim.g.terminal_color_5 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[5],
Cyan = vim.g.terminal_color_6, Cyan = vim.g.terminal_color_6 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[6],
White = vim.g.terminal_color_7, White = vim.g.terminal_color_7 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[7],
BrightBlack = vim.g.terminal_color_8, BrightBlack = vim.g.terminal_color_8 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[8],
BrightRed = vim.g.terminal_color_9, BrightRed = vim.g.terminal_color_9 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[9],
BrightGreen = vim.g.terminal_color_10, BrightGreen = vim.g.terminal_color_10 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[10],
BrightYellow = vim.g.terminal_color_11, BrightYellow = vim.g.terminal_color_11 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[11],
BrightBlue = vim.g.terminal_color_12, BrightBlue = vim.g.terminal_color_12 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[12],
BrightMagenta = vim.g.terminal_color_13, BrightMagenta = vim.g.terminal_color_13 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[13],
BrightCyan = vim.g.terminal_color_14, BrightCyan = vim.g.terminal_color_14 or ANSI_TERMINAL_COLOR_CODE_FALLBACK[14],
BrightWhite = vim.g.terminal_color_15, 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 = { local combinations = {
{ bold = false, italic = false }, { bold = false, italic = false },
{ bold = true, italic = false }, { bold = true, italic = false },

View file

@ -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() local expected_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(expected_win, expected_buf) 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', 'cp', { buf = expected_buf })
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_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', 'Expected', { win = expected_win })
vim.api.nvim_set_option_value('winbar', 'Actual', { win = actual_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() local expected_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(expected_win, expected_buf) 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', 'cp', { buf = expected_buf })
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_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', 'Expected', { win = expected_win })
vim.api.nvim_set_option_value('winbar', 'Actual', { win = actual_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() local diff_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(diff_win, diff_buf) 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 }) vim.api.nvim_set_option_value('winbar', 'Expected vs Actual', { win = diff_win })
local diff_backend = require('cp.ui.diff') 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)) vim.cmd('resize ' .. math.floor(vim.o.lines * 0.35))
local win = vim.api.nvim_get_current_win() local win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(win, buf) 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 { return {
buffers = { buf }, buffers = { buf },

View file

@ -1,5 +1,8 @@
local M = {} local M = {}
---@class RunOpts
---@field debug? boolean
local config_module = require('cp.config') local config_module = require('cp.config')
local layouts = require('cp.ui.layouts') local layouts = require('cp.ui.layouts')
local logger = require('cp.log') local logger = require('cp.log')
@ -51,19 +54,13 @@ function M.toggle_interactive()
local platform, contest_id = state.get_platform(), state.get_contest_id() local platform, contest_id = state.get_platform(), state.get_contest_id()
if not platform then if not platform then
logger.log( logger.log('No platform configured.', vim.log.levels.ERROR)
'No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug}] first.',
vim.log.levels.ERROR
)
return return
end end
if not contest_id then if not contest_id then
logger.log( logger.log(
('No contest %s configured for platform %s. Use :CP <platform> <contest> [--{lang=<lang>,debug}] to set up first.'):format( ('No contest %s configured for platform %s.'):format(contest_id, platform),
contest_id,
platform
),
vim.log.levels.ERROR vim.log.levels.ERROR
) )
return return
@ -118,8 +115,8 @@ function M.toggle_interactive()
state.set_active_panel('interactive') state.set_active_panel('interactive')
end end
---@param debug? boolean ---@param run_opts? RunOpts
function M.toggle_run_panel(debug) function M.toggle_run_panel(run_opts)
if state.get_active_panel() == 'run' then if state.get_active_panel() == 'run' then
if current_diff_layout then if current_diff_layout then
current_diff_layout.cleanup() current_diff_layout.cleanup()
@ -152,10 +149,7 @@ function M.toggle_run_panel(debug)
if not contest_id then if not contest_id then
logger.log( logger.log(
('No contest %s configured for platform %s. Use :CP <platform> <contest> [--{lang=<lang>,debug}] to set up first.'):format( ('No contest %s configured for platform %s.'):format(contest_id, platform),
contest_id,
platform
),
vim.log.levels.ERROR vim.log.levels.ERROR
) )
return return
@ -187,13 +181,6 @@ function M.toggle_run_panel(debug)
) )
local config = config_module.get_config() 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 run = require('cp.runner.run')
local input_file = state.get_input_file() local input_file = state.get_input_file()
logger.log(('run panel: checking test cases for %s'):format(input_file or 'none')) 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 tab_buf = utils.create_buffer_with_options()
local main_win = vim.api.nvim_get_current_win() local main_win = vim.api.nvim_get_current_win()
vim.api.nvim_win_set_buf(main_win, tab_buf) 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_windows = { tab_win = main_win }
local test_buffers = { tab_buf = tab_buf } 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) 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 execute = require('cp.runner.execute')
local compile_result = execute.compile_problem() local compile_result = execute.compile_problem()
if compile_result.success then if compile_result.success then

View file

@ -23,22 +23,26 @@ end, {
if num_args == 2 then if num_args == 2 then
local candidates = {} local candidates = {}
local state = require('cp.state') 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 if platform and contest_id then
vim.list_extend(candidates, actions) vim.list_extend(candidates, actions)
local cache = require('cp.cache') local cache = require('cp.cache')
cache.load() cache.load()
local contest_data = cache.get_contest_data(platform, contest_id) 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 if contest_data and contest_data.index_map then
table.insert(candidates, problem.id) local ids = vim.tbl_keys(contest_data.index_map)
end table.sort(ids)
vim.list_extend(candidates, ids)
end end
else else
vim.list_extend(candidates, platforms) vim.list_extend(candidates, platforms)
table.insert(candidates, 'cache') table.insert(candidates, 'cache')
table.insert(candidates, 'pick') table.insert(candidates, 'pick')
end end
return vim.tbl_filter(function(cmd) return vim.tbl_filter(function(cmd)
return cmd:find(ArgLead, 1, true) == 1 return cmd:find(ArgLead, 1, true) == 1
end, candidates) end, candidates)