Merge pull request #161 from barrett-ruth/feat/ui/io-view

io view
This commit is contained in:
Barrett Ruth 2025-10-23 22:35:11 -04:00 committed by GitHub
commit 1becd25cc0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 16906 additions and 3484 deletions

View file

@ -3,7 +3,7 @@ name: Release
on:
push:
tags:
- "*"
- '*'
workflow_dispatch:
jobs:

View file

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

View file

@ -1,4 +1,4 @@
minimum_pre_commit_version: "3.5.0"
minimum_pre_commit_version: '3.5.0'
repos:
- repo: https://github.com/JohnnyMorganz/StyLua
@ -17,7 +17,7 @@ repos:
files: \.py$
- id: ruff
name: ruff (lint imports)
args: ["--fix", "--select=I"]
args: ['--fix', '--select=I']
files: \.py$
- repo: local
@ -26,7 +26,7 @@ repos:
name: mypy (type check)
entry: uv run mypy
language: system
args: ["."]
args: ['.']
pass_filenames: false
- repo: https://github.com/pre-commit/mirrors-prettier
@ -35,4 +35,3 @@ repos:
- id: prettier
name: prettier (format markdown)
files: \.md$

8
.prettierignore Normal file
View file

@ -0,0 +1,8 @@
.pytest_cache/
node_modules/
.venv/
build/
dist/
*.pyc
__pycache__/
tests/fixtures/

View file

@ -2,22 +2,28 @@
**The definitive competitive programming environment for Neovim**
Scrape problems, run tests, and debug solutions across multiple platforms with zero configuration.
Scrape problems, run tests, and debug solutions across multiple platforms with
zero configuration.
https://github.com/user-attachments/assets/50b19481-8e6d-47b4-bebc-15e16c61a9c9
## Features
- **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
- **Rich test output**: 256 color ANSI support for compiler errors and program output
- **Dual view modes**: Lightweight I/O view for quick feedback, full panel for
detailed analysis
- **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
- 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
@ -32,10 +38,11 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
:CP codeforces 1848
```
3. **Code and test** with instant feedback and rich diffs
3. **Code and test** with instant feedback
```
:CP run
:CP run " Quick verdict summary in splits
:CP panel " Detailed analysis with diffs
```
4. **Navigate between problems**
@ -54,7 +61,9 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
:help cp.nvim
```
See [my config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua) for a relatively advanced setup.
See
[my config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua)
for a relatively advanced setup.
## Similar Projects

View file

@ -4,7 +4,7 @@ Author: Barrett Ruth <br.barrettruth@gmail.com>
License: Same terms as Vim itself (see |license|)
==============================================================================
INTRODUCTION *cp.nvim*
INTRODUCTION *cp.nvim*
cp.nvim is a competitive programming plugin that automates problem setup,
compilation, and testing workflow for online judges.
@ -12,16 +12,16 @@ compilation, and testing workflow for online judges.
Supported platforms (for now!): AtCoder, Codeforces, CSES
==============================================================================
REQUIREMENTS *cp-requirements*
REQUIREMENTS *cp-requirements*
- Neovim 0.10.0+
- Unix-like operating system
- uv package manager (https://docs.astral.sh/uv/)
==============================================================================
COMMANDS *cp-commands*
COMMANDS *cp-commands*
:CP *:CP*
:CP *:CP*
cp.nvim uses a single :CP command with intelligent argument parsing:
Setup Commands ~
@ -38,14 +38,23 @@ COMMANDS *cp-commands*
Example: >
:CP atcoder abc324
<
Action Commands ~
:CP run Toggle run panel for individual test cases.
Shows per-test results with redesigned
layout for efficient comparison.
:CP debug
Same as above but with the debug mode configured
settings.
View Commands ~
:CP run [n] Run tests in I/O view (see |cp-io-view|).
Lightweight split showing test verdicts.
Without [n]: runs all tests, shows verdict summary
With [n]: runs test n, shows detailed output
Examples: >
:CP run " All tests, verdict list
:CP run 3 " Test 3 detail
<
:CP panel [n] Open full-screen test panel (see |cp-panel|).
Aggregate table with diff modes for detailed analysis.
Optional [n] focuses on specific test.
Example: >
:CP panel " All tests with diffs
:CP panel 2 " Focus on test 2
<
:CP debug [n] Same as :CP panel but uses debug build configuration.
:CP pick Launch configured picker for interactive
platform/contest selection.
@ -84,7 +93,7 @@ COMMANDS *cp-commands*
Exit with q.
Template Variables ~
*cp-template-vars*
*cp-template-vars*
Command templates support variable substitution using {variable} syntax:
• {source} Source file path (e.g. "abc324a.cpp")
@ -97,7 +106,7 @@ Template Variables ~
<
==============================================================================
CONFIGURATION *cp-config*
CONFIGURATION *cp-config*
Here's an example configuration with lazy.nvim:
>lua
@ -145,8 +154,8 @@ Here's an example configuration with lazy.nvim:
open_url = true,
debug = false,
ui = {
ansi = true,
panel = {
ansi = true,
diff_mode = 'vim',
max_output_lines = 50,
},
@ -197,7 +206,7 @@ run CSES problems with Rust using the single schema:
},
}
<
*cp.Config*
*cp.Config*
Fields: ~
{languages} (table<string,|CpLanguage|>) Global language registry.
Each language provides an {extension} and {commands}.
@ -214,19 +223,19 @@ run CSES problems with Rust using the single schema:
{open_url} (boolean) Open the contest & problem url in the browser
when the contest is first opened.
*CpPlatform*
*CpPlatform*
Fields: ~
{enabled_languages} (string[]) Language ids enabled on this platform.
{default_language} (string) One of {enabled_languages}.
{overrides} (table<string,|CpPlatformOverrides|>, optional)
Per-language overrides of {extension} and/or {commands}.
*CpLanguage*
*CpLanguage*
Fields: ~
{extension} (string) File extension without leading dot.
{commands} (|CpLangCommands|) Command templates.
*CpLangCommands*
*CpLangCommands*
Fields: ~
{build} (string[], optional) For compiled languages.
Must include {source} and {binary}.
@ -236,25 +245,25 @@ run CSES problems with Rust using the single schema:
{debug} (string[], optional) Debug variant; same token rules
as {build} (compiled) or {run} (interpreted).
*CpUI*
*CpUI*
Fields: ~
{ansi} (boolean, default: true) Enable ANSI color parsing
and highlighting in both I/O view and panel.
{panel} (|PanelConfig|) Test panel behavior configuration.
{diff} (|DiffConfig|) Diff backend configuration.
{picker} (string|nil) 'telescope', 'fzf-lua', or nil.
*cp.PanelConfig*
*cp.PanelConfig*
Fields: ~
{ansi} (boolean, default: true) Enable ANSI color parsing
and highlighting.
{diff_mode} (string, default: "none") Diff backend: "none",
"vim", or "git".
{max_output_lines} (number, default: 50) Maximum lines of test output.
*cp.DiffConfig*
*cp.DiffConfig*
Fields: ~
{git} (|cp.DiffGitConfig|) Git diff backend configuration.
*cp.DiffGitConfig*
*cp.DiffGitConfig*
Fields: ~
{args} (string[]) Command-line arguments for git diff.
Default: { 'diff', '--no-index', '--word-diff=plain',
@ -264,40 +273,56 @@ run CSES problems with Rust using the single schema:
• --word-diff-regex=.: Split on every character
• --no-prefix: Remove a/ b/ prefixes from output
*cp.Hooks*
*cp.Hooks*
Fields: ~
{before_run} (function, optional) Called before test panel opens.
function(state: cp.State)
{before_debug} (function, optional) Called before debug build/run.
function(state: cp.State)
{setup_code} (function, optional) Called after source file is opened.
function(state: cp.State)
{before_run} (function, optional) Called before test panel opens.
function(state: cp.State)
{before_debug} (function, optional) Called before debug build/run.
function(state: cp.State)
{setup_code} (function, optional) Called after source file is opened.
function(state: cp.State)
{setup_io_input} (function, optional) Called when I/O input buffer created.
function(bufnr: integer, state: cp.State)
Default: helpers.clearcol (removes line numbers/columns)
{setup_io_output} (function, optional) Called when I/O output buffer created.
function(bufnr: integer, state: cp.State)
Default: helpers.clearcol (removes line numbers/columns)
Hook functions receive the cp.nvim state object (cp.State). See the state
module documentation (lua/cp/state.lua) for available methods and fields.
Hook functions receive the cp.nvim state object (|cp.State|). See
|lua/cp/state.lua| for available methods and fields.
Example usage in hook:
The I/O buffer hooks are called once when the buffers are first created
during problem setup. Use these to customize buffer appearance (e.g.,
remove line numbers, set custom options). Access helpers via:
>lua
local helpers = require('cp').helpers
<
Example usage:
>lua
hooks = {
setup_code = function(state)
print("Setting up " .. state.get_base_name())
print("Source file: " .. state.get_source_file())
end,
setup_io_input = function(bufnr, state)
-- Custom setup for input buffer
vim.api.nvim_set_option_value('number', false, { buf = bufnr })
end
}
<
==============================================================================
WORKFLOW *cp-workflow*
WORKFLOW *cp-workflow*
For the sake of consistency and simplicity, cp.nvim extracts contest/problem
identifiers from URLs. This means that, for example, CodeForces/AtCoder
contests are configured by their round id rather than round number. See below.
==============================================================================
PLATFORM-SPECIFIC USAGE *cp-platforms*
PLATFORM-SPECIFIC USAGE *cp-platforms*
AtCoder ~
*cp-atcoder*
*cp-atcoder*
URL format:
https://atcoder.jp/contests/{contest_id}/tasks/{contest_id}_{problem_id}
@ -305,14 +330,14 @@ Usage examples: >
:CP atcoder abc324 " Set up atcoder.jp/contests/abc324
Codeforces ~
*cp-codeforces*
*cp-codeforces*
URL format: https://codeforces.com/contest/{contest_id}/problem/{problem_id}
Usage examples: >
:CP codeforces 1934 " Set up codeforces.com/contest/1934
CSES ~
*cp-cses*
*cp-cses*
URL format: https://cses.fi/problemset/task/{problem_id}
Usage examples: >
@ -320,7 +345,7 @@ Usage examples: >
==============================================================================
COMPLETE WORKFLOW EXAMPLE *cp-example*
COMPLETE WORKFLOW EXAMPLE *cp-example*
Example: Setting up and solving AtCoder contest ABC324
@ -333,8 +358,9 @@ Example: Setting up and solving AtCoder contest ABC324
3. Code your solution, then test: >
:CP run
< Navigate with j/k, run specific tests with <enter>
Exit test panel with q or :CP run when done
< View test verdicts in I/O splits. For detailed analysis:
:CP panel
< Navigate tests with <c-n>/<c-p>, exit with q
4. Move to next problem: >
:CP next
@ -350,28 +376,68 @@ Example: Setting up and solving AtCoder contest ABC324
7. Submit solutions on AtCoder website
==============================================================================
PICKER INTEGRATION *cp-picker*
I/O VIEW *cp-io-view*
The I/O view provides the main view aggregate view into test input and
program output. Used time/memory per test case are appended to the output.
The |cp-panel| offers more fine-grained analysis into each test case.
Access the I/O view with :CP run [n]
Layout ~
The I/O view appears as 30% width splits on the right side: >
┌──────────────────────────┬──────────────────────────┐
│ │ Output │
│ │ Test 1: AC (42ms, 8MB) │
│ │ Test 2: AC (38ms, 8MB) │
│ Solution Code │ Test 3: WA (45ms, 8MB) │
│ │ Test 4: AC (51ms, 9MB) │
│ ├──────────────────────────┤
│ │ Input │
│ │ 5 3 │
│ │ 1 2 3 4 5 │
│ │ 2 1 │
│ │ 10 20 │
└──────────────────────────┴──────────────────────────┘
<
Usage ~
:CP run Run all tests
:CP run 3 Run test 3
Buffer Customization ~
Use the setup_io_input and setup_io_output hooks (see |cp.Hooks|) to customize
buffer appearance. By default, line numbers and columns are removed via
helpers.clearcol (see |cp-helpers|).
==============================================================================
PICKER INTEGRATION *cp-picker*
When picker integration is enabled in configuration, cp.nvim provides interactive
platform and contest selection using telescope.nvim or fzf-lua.
:CP pick *:CP-pick*
:CP pick *:CP-pick*
Launch configured picker for interactive problem selection.
Control Flow: Select Platform → Contest → Code!
Requires picker = 'telescope' or picker = 'fzf-lua' in configuration.
Requires corresponding plugin (telescope.nvim or fzf-lua) to be installed.
PICKER KEYMAPS *cp-picker-keys*
PICKER KEYMAPS *cp-picker-keys*
<c-r> Force refresh/update contest list.
Useful when contest lists are outdated or incomplete
==============================================================================
PANEL *cp-run*
PANEL *cp-panel*
The 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.
The panel provides full-screen test analysis with diff modes for detailed
debugging. Problem time/memory limit constraints are in columns Time/Mem
respectively. Used time/memory are in columns Runtime/RSS respectively.
Access with :CP panel or :CP debug (uses debug build configuration).
Interface ~
@ -409,7 +475,7 @@ Test cases use competitive programming terminology with color highlighting:
<
==============================================================================
INTERACTIVE MODE *cp-interact*
INTERACTIVE MODE *cp-interact*
Run interactive problems manually or with an orchestrator. :CP interact is
available for interactive problems. Test cases are ignored in interactive mode
@ -436,7 +502,7 @@ Keymaps ~
<c-q> Close the terminal and restore the previous layout.
==============================================================================
ANSI COLORS AND HIGHLIGHTING *cp-ansi*
ANSI COLORS AND HIGHLIGHTING *cp-ansi*
cp.nvim provides comprehensive ANSI color support and highlighting for
compiler output, program stderr, and diff visualization.
@ -459,7 +525,7 @@ alter your config as follows:
<
==============================================================================
HIGHLIGHT GROUPS *cp-highlights*
HIGHLIGHT GROUPS *cp-highlights*
Test Status Groups ~
@ -485,13 +551,13 @@ adapt to your colorscheme:
DiffDelete Highlights removed text in git diffs
==============================================================================
TERMINAL COLOR INTEGRATION *cp-terminal-colors*
TERMINAL COLOR INTEGRATION *cp-terminal-colors*
ANSI colors automatically use the terminal's color palette through Neovim's
vim.g.terminal_color_* variables.
==============================================================================
HIGHLIGHT CUSTOMIZATION *cp-highlight-custom*
HIGHLIGHT CUSTOMIZATION *cp-highlight-custom*
Customize highlight groups after your colorscheme loads:
>lua
@ -502,12 +568,33 @@ Customize highlight groups after your colorscheme loads:
})
==============================================================================
PANEL KEYMAPS *cp-test-keys*
HELPERS *cp-helpers*
The helpers module provides utility functions for buffer customization.
Access via:
>lua
local helpers = require('cp').helpers
<
Functions ~
helpers.clearcol({bufnr}) *helpers.clearcol*
Remove line numbers, columns, and signs from buffer.
Sets:
• number = false
• relativenumber = false
• signcolumn = 'no'
• statuscolumn = ''
Parameters: ~
{bufnr} (integer) Buffer handle
==============================================================================
PANEL KEYMAPS *cp-panel-keys*
<c-n> Navigate to next test case
<c-p> Navigate to previous test case
t Cycle through diff modes: none → git → vim
q Exit run panel and restore layout
q Exit panel and restore layout
<c-q> Exit interactive terminal and restore layout
Diff Modes ~
@ -528,7 +615,7 @@ execution pipeline, but with isolated input/output for
precise failure analysis.
==============================================================================
FILE STRUCTURE *cp-files*
FILE STRUCTURE *cp-files*
cp.nvim creates the following file structure upon problem setup: >
@ -537,11 +624,10 @@ cp.nvim creates the following file structure upon problem setup: >
{problem_id}.run " Compiled binary
io/
{problem_id}.n.cpin " nth test input
{problem_id}.n.cpout " nth program output
{problem_id}.expected " Expected output
{problem_id}.n.cpout " nth test expected output
<
==============================================================================
HEALTH CHECK *cp-health*
HEALTH CHECK *cp-health*
Run |:checkhealth| cp to verify your setup.

View file

@ -16,6 +16,7 @@ local actions = constants.ACTIONS
---@field platform? string
---@field problem_id? string
---@field interactor_cmd? string
---@field test_index? integer
--- Turn raw args into normalized structure to later dispatch
---@param args string[] The raw command-line mode args
@ -52,6 +53,23 @@ local function parse_command(args)
else
return { type = 'action', action = 'interact' }
end
elseif first == 'run' then
local test_arg = args[2]
if test_arg then
local test_index = tonumber(test_arg)
if not test_index then
return {
type = 'error',
message = ("Test index '%s' is not a number"):format(test_index),
}
end
if test_index < 1 or test_index ~= math.floor(test_index) then
return { type = 'error', message = ("'%s' is not a valid test index"):format(test_index) }
end
return { type = 'action', action = 'run', test_index = test_index }
else
return { type = 'action', action = 'run' }
end
else
return { type = 'action', action = first }
end
@ -109,6 +127,8 @@ function M.handle_command(opts)
if cmd.action == 'interact' then
ui.toggle_interactive(cmd.interactor_cmd)
elseif cmd.action == 'run' then
ui.run_io_view(cmd.test_index)
elseif cmd.action == 'panel' then
ui.toggle_panel()
elseif cmd.action == 'debug' then
ui.toggle_panel({ debug = true })

View file

@ -18,7 +18,6 @@
---@field overrides? table<string, CpPlatformOverrides>
---@class PanelConfig
---@field ansi boolean
---@field diff_mode "none"|"vim"|"git"
---@field max_output_lines integer
@ -32,8 +31,11 @@
---@field before_run? fun(state: cp.State)
---@field before_debug? fun(state: cp.State)
---@field setup_code? fun(state: cp.State)
---@field setup_io_input? fun(bufnr: integer, state: cp.State)
---@field setup_io_output? fun(bufnr: integer, state: cp.State)
---@class CpUI
---@field ansi boolean
---@field panel PanelConfig
---@field diff DiffConfig
---@field picker string|nil
@ -54,6 +56,7 @@
local M = {}
local constants = require('cp.constants')
local helpers = require('cp.helpers')
local utils = require('cp.utils')
-- defaults per the new single schema
@ -101,12 +104,19 @@ M.defaults = {
default_language = 'cpp',
},
},
hooks = { before_run = nil, before_debug = nil, setup_code = nil },
hooks = {
before_run = nil,
before_debug = nil,
setup_code = nil,
setup_io_input = helpers.clearcol,
setup_io_output = helpers.clearcol,
},
debug = false,
scrapers = constants.PLATFORMS,
filename = nil,
ui = {
panel = { ansi = true, diff_mode = 'none', max_output_lines = 50 },
ansi = true,
panel = { diff_mode = 'none', max_output_lines = 50 },
diff = {
git = {
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
@ -229,10 +239,12 @@ function M.setup(user_config)
before_run = { cfg.hooks.before_run, { 'function', 'nil' }, true },
before_debug = { cfg.hooks.before_debug, { 'function', 'nil' }, true },
setup_code = { cfg.hooks.setup_code, { 'function', 'nil' }, true },
setup_io_input = { cfg.hooks.setup_io_input, { 'function', 'nil' }, true },
setup_io_output = { cfg.hooks.setup_io_output, { 'function', 'nil' }, true },
})
vim.validate({
ansi = { cfg.ui.panel.ansi, 'boolean' },
ansi = { cfg.ui.ansi, 'boolean' },
diff_mode = {
cfg.ui.panel.diff_mode,
function(v)

View file

@ -1,7 +1,7 @@
local M = {}
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
M.ACTIONS = { 'run', 'next', 'prev', 'pick', 'cache', 'interact' }
M.ACTIONS = { 'run', 'panel', 'debug', 'next', 'prev', 'pick', 'cache', 'interact' }
M.PLATFORM_DISPLAY_NAMES = {
atcoder = 'AtCoder',

13
lua/cp/helpers.lua Normal file
View file

@ -0,0 +1,13 @@
local M = {}
---@param bufnr integer
function M.clearcol(bufnr)
for _, win in ipairs(vim.fn.win_findbuf(bufnr)) do
vim.wo[win].signcolumn = 'no'
vim.wo[win].statuscolumn = ''
vim.wo[win].number = false
vim.wo[win].relativenumber = false
end
end
return M

View file

@ -1,8 +1,11 @@
local M = {}
local config_module = require('cp.config')
local helpers = require('cp.helpers')
local logger = require('cp.log')
M.helpers = helpers
if vim.fn.has('nvim-0.10.0') == 0 then
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR)
return {}

View file

@ -122,7 +122,7 @@ local function run_single_test_case(test_case)
local out = r.stdout or ''
local highlights = {}
if out ~= '' then
if config.ui.panel.ansi then
if config.ui.ansi then
local parsed = ansi.parse_ansi_text(out)
out = table.concat(parsed.lines, '\n')
highlights = parsed.highlights
@ -224,14 +224,22 @@ function M.run_test_case(index)
return true
end
---@param indices? integer[]
---@return RanTestCase[]
function M.run_all_test_cases()
local results = {}
for i = 1, #panel_state.test_cases do
M.run_test_case(i)
results[i] = panel_state.test_cases[i]
function M.run_all_test_cases(indices)
local to_run = indices
if not to_run then
to_run = {}
for i = 1, #panel_state.test_cases do
to_run[i] = i
end
end
return results
for _, i in ipairs(to_run) do
M.run_test_case(i)
end
return panel_state.test_cases
end
---@return PanelState
@ -247,7 +255,7 @@ function M.handle_compilation_failure(output)
local txt
local hl = {}
if config.ui.panel.ansi then
if config.ui.ansi then
local p = ansi.parse_ansi_text(output or '')
txt = table.concat(p.lines, '\n')
hl = p.highlights

View file

@ -3,6 +3,7 @@ local M = {}
local cache = require('cp.cache')
local config_module = require('cp.config')
local constants = require('cp.constants')
local helpers = require('cp.helpers')
local logger = require('cp.log')
local scraper = require('cp.scraper')
local state = require('cp.state')
@ -59,6 +60,18 @@ local function start_tests(platform, contest_id, problems)
ev.memory_mb or 0,
ev.interactive
)
local io_state = state.get_io_view_state()
if io_state then
local test_cases = cache.get_test_cases(platform, contest_id, state.get_problem_id())
local input_lines = {}
for _, tc in ipairs(test_cases) do
for _, line in ipairs(vim.split(tc.input, '\n')) do
table.insert(input_lines, line)
end
end
require('cp.utils').update_buffer_content(io_state.input_buf, input_lines, nil, nil)
end
end)
end
end
@ -152,6 +165,7 @@ end
function M.setup_problem(problem_id, language)
local platform = state.get_platform()
if not platform then
logger.log('No platform/contest/problem configured.', vim.log.levels.ERROR)
return
end
@ -178,6 +192,9 @@ function M.setup_problem(problem_id, language)
if ok then
vim.b[prov.bufnr].cp_setup_done = true
end
elseif not vim.b[prov.bufnr].cp_setup_done then
helpers.clearcol(prov.bufnr)
vim.b[prov.bufnr].cp_setup_done = true
end
cache.set_file_state(
vim.fn.fnamemodify(source_file, ':p'),
@ -186,6 +203,7 @@ function M.setup_problem(problem_id, language)
state.get_problem_id() or '',
lang
)
require('cp.ui.panel').ensure_io_view()
end
state.set_provisional(nil)
return
@ -201,6 +219,9 @@ function M.setup_problem(problem_id, language)
if ok then
vim.b[bufnr].cp_setup_done = true
end
elseif not vim.b[bufnr].cp_setup_done then
helpers.clearcol(bufnr)
vim.b[bufnr].cp_setup_done = true
end
cache.set_file_state(
vim.fn.expand('%:p'),
@ -209,6 +230,7 @@ function M.setup_problem(problem_id, language)
state.get_problem_id() or '',
lang
)
require('cp.ui.panel').ensure_io_view()
end)
end
@ -247,7 +269,11 @@ function M.navigate_problem(direction)
return
end
require('cp.ui.panel').disable()
local active_panel = state.get_active_panel()
if active_panel == 'run' then
require('cp.ui.panel').disable()
end
M.setup_contest(platform, contest_id, problems[new_index].id)
end

View file

@ -3,9 +3,15 @@
---@field platform string
---@field contest_id string
---@field language string
---@field requested_problem_id string|nil
---@field requested_problem_id string?
---@field token integer
---@class cp.IoViewState
---@field output_buf integer
---@field input_buf integer
---@field output_win integer
---@field input_win integer
---@class cp.State
---@field get_platform fun(): string?
---@field set_platform fun(platform: string)
@ -21,8 +27,12 @@
---@field get_input_file fun(): string?
---@field get_output_file fun(): string?
---@field get_expected_file fun(): string?
---@field get_provisional fun(): cp.ProvisionalState|nil
---@field set_provisional fun(p: cp.ProvisionalState|nil)
---@field get_provisional fun(): cp.ProvisionalState?
---@field set_provisional fun(p: cp.ProvisionalState?)
---@field get_saved_session fun(): string?
---@field set_saved_session fun(path: string?)
---@field get_io_view_state fun(): cp.IoViewState?
---@field set_io_view_state fun(s: cp.IoViewState?)
local M = {}
@ -36,9 +46,10 @@ local state = {
active_panel = nil,
provisional = nil,
solution_win = nil,
io_view_state = nil,
}
---@return string|nil
---@return string?
function M.get_platform()
return state.platform
end
@ -48,7 +59,7 @@ function M.set_platform(platform)
state.platform = platform
end
---@return string|nil
---@return string?
function M.get_contest_id()
return state.contest_id
end
@ -58,7 +69,7 @@ function M.set_contest_id(contest_id)
state.contest_id = contest_id
end
---@return string|nil
---@return string?
function M.get_problem_id()
return state.problem_id
end
@ -68,7 +79,7 @@ function M.set_problem_id(problem_id)
state.problem_id = problem_id
end
---@return string|nil
---@return string?
function M.get_base_name()
local platform, contest_id, problem_id = M.get_platform(), M.get_contest_id(), M.get_problem_id()
if not platform or not contest_id or not problem_id then
@ -86,7 +97,7 @@ function M.get_base_name()
end
---@param language? string
---@return string|nil
---@return string?
function M.get_source_file(language)
local base_name = M.get_base_name()
if not base_name or not M.get_platform() then
@ -110,46 +121,46 @@ function M.get_source_file(language)
return base_name .. '.' .. eff.extension
end
---@return string|nil
---@return string?
function M.get_binary_file()
local base_name = M.get_base_name()
return base_name and ('build/%s.run'):format(base_name) or nil
end
---@return string|nil
---@return string?
function M.get_input_file()
local base_name = M.get_base_name()
return base_name and ('io/%s.cpin'):format(base_name) or nil
end
---@return string|nil
---@return string?
function M.get_output_file()
local base_name = M.get_base_name()
return base_name and ('io/%s.cpout'):format(base_name) or nil
end
---@return string|nil
---@return string?
function M.get_expected_file()
local base_name = M.get_base_name()
return base_name and ('io/%s.expected'):format(base_name) or nil
end
---@return string|nil
---@return string?
function M.get_active_panel()
return state.active_panel
end
---@param panel string|nil
---@param panel string?
function M.set_active_panel(panel)
state.active_panel = panel
end
---@return cp.ProvisionalState|nil
---@return cp.ProvisionalState?
function M.get_provisional()
return state.provisional
end
---@param p cp.ProvisionalState|nil
---@param p cp.ProvisionalState?
function M.set_provisional(p)
state.provisional = p
end
@ -167,6 +178,38 @@ function M.set_solution_win(win)
state.solution_win = win
end
---@return cp.IoViewState?
function M.get_io_view_state()
if not state.io_view_state then
return nil
end
local s = state.io_view_state
if
vim.api.nvim_buf_is_valid(s.output_buf)
and vim.api.nvim_buf_is_valid(s.input_buf)
and vim.api.nvim_win_is_valid(s.output_win)
and vim.api.nvim_win_is_valid(s.input_win)
then
return s
end
return nil
end
---@param s cp.IoViewState?
function M.set_io_view_state(s)
state.io_view_state = s
end
---@return string?
function M.get_saved_session()
return state.saved_session
end
---@param path string?
function M.set_saved_session(path)
state.saved_session = path
end
M._state = state
return M

View file

@ -321,6 +321,25 @@ function M.setup_highlight_groups()
vim.api.nvim_set_hl(0, 'CpAnsiBold', { bold = true })
vim.api.nvim_set_hl(0, 'CpAnsiItalic', { italic = true })
vim.api.nvim_set_hl(0, 'CpAnsiBoldItalic', { bold = true, italic = true })
for _, combo in ipairs(combinations) do
for color_name, _ in pairs(color_map) do
local parts = { 'CpAnsi' }
if combo.bold then
table.insert(parts, 'Bold')
end
if combo.italic then
table.insert(parts, 'Italic')
end
table.insert(parts, color_name)
local hl_name = table.concat(parts)
dyn_hl_cache[hl_name] = true
end
end
dyn_hl_cache['CpAnsiBold'] = true
dyn_hl_cache['CpAnsiItalic'] = true
dyn_hl_cache['CpAnsiBoldItalic'] = true
end
---@param text string

View file

@ -1,10 +1,13 @@
local M = {}
local helpers = require('cp.helpers')
local utils = require('cp.utils')
local function create_none_diff_layout(parent_win, expected_content, actual_content)
local expected_buf = utils.create_buffer_with_options()
local actual_buf = utils.create_buffer_with_options()
helpers.clearcol(expected_buf)
helpers.clearcol(actual_buf)
vim.api.nvim_set_current_win(parent_win)
vim.cmd.split()
@ -42,6 +45,8 @@ end
local function create_vim_diff_layout(parent_win, expected_content, actual_content)
local expected_buf = utils.create_buffer_with_options()
local actual_buf = utils.create_buffer_with_options()
helpers.clearcol(expected_buf)
helpers.clearcol(actual_buf)
vim.api.nvim_set_current_win(parent_win)
vim.cmd.split()
@ -89,6 +94,7 @@ end
local function create_git_diff_layout(parent_win, expected_content, actual_content)
local diff_buf = utils.create_buffer_with_options()
helpers.clearcol(diff_buf)
vim.api.nvim_set_current_win(parent_win)
vim.cmd.split()

View file

@ -3,8 +3,9 @@ local M = {}
---@class PanelOpts
---@field debug? boolean
local cache = require('cp.cache')
local config_module = require('cp.config')
local constants = require('cp.constants')
local helpers = require('cp.helpers')
local layouts = require('cp.ui.layouts')
local logger = require('cp.log')
local state = require('cp.state')
@ -52,38 +53,24 @@ function M.toggle_interactive(interactor_cmd)
return
end
local platform, contest_id = state.get_platform(), state.get_contest_id()
if not platform then
logger.log('No platform configured.', vim.log.levels.ERROR)
return
end
if not contest_id then
local platform, contest_id, problem_id =
state.get_platform(), state.get_contest_id(), state.get_problem_id()
if not platform or not contest_id or not problem_id then
logger.log(
("No contest %s configured for platform '%s'."):format(
contest_id,
constants.PLATFORM_DISPLAY_NAMES[platform]
),
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR
)
return
end
local problem_id = state.get_problem_id()
if not problem_id then
logger.log('No problem is 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
or not contest_data.index_map
or not contest_data.problems[contest_data.index_map[problem_id]]
or not contest_data.problems[contest_data.index_map[problem_id]].interactive
or contest_data.problems[contest_data.index_map[problem_id]].interactive
then
logger.log('This problem is not interactive. Use :CP run.', vim.log.levels.ERROR)
logger.log('This problem is interactive. Use :CP {run,panel}.', vim.log.levels.ERROR)
return
end
@ -92,9 +79,10 @@ function M.toggle_interactive(interactor_cmd)
vim.cmd('silent only')
local execute = require('cp.runner.execute')
local run = require('cp.runner.run')
local compile_result = execute.compile_problem()
if not compile_result.success then
require('cp.runner.run').handle_compilation_failure(compile_result.output)
run.handle_compilation_failure(compile_result.output)
return
end
@ -204,6 +192,230 @@ function M.toggle_interactive(interactor_cmd)
state.set_active_panel('interactive')
end
function M.ensure_io_view()
local platform, contest_id, problem_id =
state.get_platform(), state.get_contest_id(), state.get_problem_id()
if not platform or not contest_id or not problem_id then
logger.log(
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR
)
return
end
cache.load()
local contest_data = cache.get_contest_data(platform, contest_id)
if
contest_data
and contest_data.index_map
and contest_data.problems[contest_data.index_map[problem_id]].interactive
then
logger.log('No platform configured.', vim.log.levels.ERROR)
return
end
local solution_win = state.get_solution_win()
local io_state = state.get_io_view_state()
local output_buf, input_buf, output_win, input_win
if io_state then
output_buf = io_state.output_buf
input_buf = io_state.input_buf
output_win = io_state.output_win
input_win = io_state.input_win
else
vim.api.nvim_set_current_win(solution_win)
vim.cmd.vsplit()
output_win = vim.api.nvim_get_current_win()
local width = math.floor(vim.o.columns * 0.3)
vim.api.nvim_win_set_width(output_win, width)
output_buf = utils.create_buffer_with_options()
vim.api.nvim_win_set_buf(output_win, output_buf)
vim.cmd.split()
input_win = vim.api.nvim_get_current_win()
input_buf = utils.create_buffer_with_options()
vim.api.nvim_win_set_buf(input_win, input_buf)
state.set_io_view_state({
output_buf = output_buf,
input_buf = input_buf,
output_win = output_win,
input_win = input_win,
})
local config = config_module.get_config()
if config.hooks and config.hooks.setup_io_output then
pcall(config.hooks.setup_io_output, output_buf, state)
end
if config.hooks and config.hooks.setup_io_input then
pcall(config.hooks.setup_io_input, input_buf, state)
end
end
utils.update_buffer_content(input_buf, {})
utils.update_buffer_content(output_buf, {})
local test_cases = cache.get_test_cases(platform, contest_id, problem_id)
if test_cases and #test_cases > 0 then
local input_lines = {}
for _, tc in ipairs(test_cases) do
for _, line in ipairs(vim.split(tc.input, '\n')) do
table.insert(input_lines, line)
end
end
utils.update_buffer_content(input_buf, input_lines, nil, nil)
end
vim.api.nvim_set_current_win(solution_win)
end
function M.run_io_view(test_index)
local platform, contest_id, problem_id =
state.get_platform(), state.get_contest_id(), state.get_problem_id()
if not platform or not contest_id or not problem_id then
logger.log(
'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR
)
return
end
cache.load()
local contest_data = cache.get_contest_data(platform, contest_id)
if
not contest_data
or not contest_data.index_map
or contest_data.problems[contest_data.index_map[problem_id]].interactive
then
logger.log('This problem is interactive. Use :CP {run,panel}.', vim.log.levels.ERROR)
return
end
M.ensure_io_view()
local run = require('cp.runner.run')
if not run.load_test_cases() then
logger.log('No test cases available', vim.log.levels.ERROR)
return
end
local test_state = run.get_panel_state()
local test_indices = {}
if test_index then
if test_index < 1 or test_index > #test_state.test_cases then
logger.log(
string.format(
'Test %d does not exist (only %d tests available)',
test_index,
#test_state.test_cases
),
vim.log.levels.WARN
)
return
end
test_indices = { test_index }
else
for i = 1, #test_state.test_cases do
test_indices[i] = i
end
end
local io_state = state.get_io_view_state()
if not io_state then
return
end
local config = config_module.get_config()
if config.ui.ansi then
require('cp.ui.ansi').setup_highlight_groups()
end
local execute = require('cp.runner.execute')
local compile_result = execute.compile_problem()
if not compile_result.success then
local ansi = require('cp.ui.ansi')
local output = compile_result.output or ''
local lines, highlights
if config.ui.ansi then
local parsed = ansi.parse_ansi_text(output)
lines = parsed.lines
highlights = parsed.highlights
else
lines = vim.split(output:gsub('\027%[[%d;]*[a-zA-Z]', ''), '\n')
highlights = {}
end
local ns = vim.api.nvim_create_namespace('cp_io_view_compile_error')
utils.update_buffer_content(io_state.output_buf, lines, highlights, ns)
return
end
run.run_all_test_cases(test_indices)
local input_lines = {}
for _, idx in ipairs(test_indices) do
local tc = test_state.test_cases[idx]
for _, line in ipairs(vim.split(tc.input, '\n')) do
table.insert(input_lines, line)
end
end
utils.update_buffer_content(io_state.input_buf, input_lines, nil, nil)
local run_render = require('cp.runner.run_render')
run_render.setup_highlights()
if #test_indices == 1 then
local idx = test_indices[1]
local tc = test_state.test_cases[idx]
local status = run_render.get_status_info(tc)
local output_lines = {}
if tc.actual then
for _, line in ipairs(vim.split(tc.actual, '\n', { plain = true, trimempty = false })) do
table.insert(output_lines, line)
end
end
table.insert(output_lines, '')
local time = tc.time_ms and string.format('%.2fms', tc.time_ms) or ''
local code = tc.code and tostring(tc.code) or ''
table.insert(output_lines, string.format('--- %s: %s | Exit: %s ---', status.text, time, code))
local highlights = tc.actual_highlights or {}
local ns = vim.api.nvim_create_namespace('cp_io_view_output')
utils.update_buffer_content(io_state.output_buf, output_lines, highlights, ns)
else
local verdict_lines = {}
local verdict_highlights = {}
for _, idx in ipairs(test_indices) do
local tc = test_state.test_cases[idx]
local status = run_render.get_status_info(tc)
local time = tc.time_ms and string.format('%.2f', tc.time_ms) or ''
local mem = tc.rss_mb and string.format('%.0f', tc.rss_mb) or ''
local line = string.format('Test %d: %s (%sms, %sMB)', idx, status.text, time, mem)
table.insert(verdict_lines, line)
local status_pos = line:find(status.text, 1, true)
if status_pos then
table.insert(verdict_highlights, {
line = #verdict_lines - 1,
col_start = status_pos - 1,
col_end = status_pos - 1 + #status.text,
highlight_group = status.highlight_group,
})
end
end
local verdict_ns = vim.api.nvim_create_namespace('cp_io_view_verdict')
utils.update_buffer_content(io_state.output_buf, verdict_lines, verdict_highlights, verdict_ns)
end
end
---@param panel_opts? PanelOpts
function M.toggle_panel(panel_opts)
if state.get_active_panel() == 'run' then
@ -212,12 +424,14 @@ function M.toggle_panel(panel_opts)
current_diff_layout = nil
current_mode = nil
end
if state.saved_session then
vim.cmd(('source %s'):format(state.saved_session))
vim.fn.delete(state.saved_session)
state.saved_session = nil
local saved = state.get_saved_session()
if saved then
vim.cmd(('source %s'):format(saved))
vim.fn.delete(saved)
state.set_saved_session(nil)
end
state.set_active_panel(nil)
M.ensure_io_view()
return
end
@ -228,26 +442,14 @@ function M.toggle_panel(panel_opts)
local platform, contest_id = state.get_platform(), state.get_contest_id()
if not platform then
if not platform or not contest_id then
logger.log(
'No platform configured. Use :CP <platform> <contest> [...] first.',
'No platform/contest configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR
)
return
end
if not contest_id then
logger.log(
("No contest '%s' configured for platform '%s'."):format(
contest_id,
constants.PLATFORM_DISPLAY_NAMES[platform]
),
vim.log.levels.ERROR
)
return
end
local cache = require('cp.cache')
cache.load()
local contest_data = cache.get_contest_data(platform, contest_id)
if
@ -260,6 +462,7 @@ function M.toggle_panel(panel_opts)
local config = config_module.get_config()
local run = require('cp.runner.run')
local run_render = require('cp.runner.run_render')
local input_file = state.get_input_file()
logger.log(('run panel: checking test cases for %s'):format(input_file or 'none'))
@ -268,11 +471,24 @@ function M.toggle_panel(panel_opts)
return
end
state.saved_session = vim.fn.tempname()
vim.cmd(('mksession! %s'):format(state.saved_session))
local io_state = state.get_io_view_state()
if io_state then
if vim.api.nvim_win_is_valid(io_state.output_win) then
vim.api.nvim_win_close(io_state.output_win, true)
end
if vim.api.nvim_win_is_valid(io_state.input_win) then
vim.api.nvim_win_close(io_state.input_win, true)
end
state.set_io_view_state(nil)
end
local session_file = vim.fn.tempname()
state.set_saved_session(session_file)
vim.cmd(('mksession! %s'):format(session_file))
vim.cmd('silent only')
local tab_buf = utils.create_buffer_with_options()
helpers.clearcol(tab_buf)
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', 'cp', { buf = tab_buf })
@ -298,7 +514,6 @@ function M.toggle_panel(panel_opts)
if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
return
end
local run_render = require('cp.runner.run_render')
run_render.setup_highlights()
local test_state = run.get_panel_state()
local tab_lines, tab_highlights = run_render.render_test_list(test_state)
@ -368,9 +583,8 @@ function M.toggle_panel(panel_opts)
refresh_panel()
vim.schedule(function()
if config.ui.panel.ansi then
local ansi = require('cp.ui.ansi')
ansi.setup_highlight_groups()
if config.ui.ansi then
require('cp.ui.ansi').setup_highlight_groups()
end
if current_diff_layout then
update_diff_panes()

View file

@ -125,6 +125,10 @@ function M.create_buffer_with_options(filetype)
return buf
end
---@param bufnr integer
---@param lines string[]
---@param highlights? Highlight[]
---@param namespace? integer
function M.update_buffer_content(bufnr, lines, highlights, namespace)
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })

View file

@ -72,24 +72,24 @@ async def fetch_text(client: httpx.AsyncClient, path: str) -> str:
CATEGORY_BLOCK_RE = re.compile(
r'<h2>(?P<cat>[^<]+)</h2>\s*<ul class="task-list">(?P<body>.*?)</ul>',
r'<h2>(?P<cat>[^<]+)</h2>\s*<ul\s+class="task-list">(?P<body>.*?)</ul>',
re.DOTALL,
)
TASK_LINK_RE = re.compile(
r'<li class="task"><a href="/problemset/task/(?P<id>\d+)/?">(?P<title>[^<]+)</a>',
r'<li\s+class="task">\s*<a\s+href="/problemset/task/(?P<id>\d+)/?">(?P<title>[^<]+)</a\s*>',
re.DOTALL,
)
TITLE_RE = re.compile(
r'<div class="title-block">.*?<h1>(?P<title>[^<]+)</h1>', re.DOTALL
r'<div\s+class="title-block">.*?<h1>(?P<title>[^<]+)</h1>', re.DOTALL
)
TIME_RE = re.compile(r"<li><b>Time limit:</b>\s*([0-9.]+)\s*s</li>")
MEM_RE = re.compile(r"<li><b>Memory limit:</b>\s*(\d+)\s*MB</li>")
TIME_RE = re.compile(r"<li>\s*<b>Time limit:</b>\s*([0-9.]+)\s*s\s*</li>")
MEM_RE = re.compile(r"<li>\s*<b>Memory limit:</b>\s*(\d+)\s*MB\s*</li>")
SIDEBAR_CAT_RE = re.compile(
r'<div class="nav sidebar">.*?<h4>(?P<cat>[^<]+)</h4>', re.DOTALL
r'<div\s+class="nav sidebar">.*?<h4>(?P<cat>[^<]+)</h4>', re.DOTALL
)
MD_BLOCK_RE = re.compile(r'<div class="md">(.*?)</div>', re.DOTALL | re.IGNORECASE)
MD_BLOCK_RE = re.compile(r'<div\s+class="md">(.*?)</div>', re.DOTALL | re.IGNORECASE)
EXAMPLE_SECTION_RE = re.compile(
r"<h[1-6][^>]*>\s*example[s]?:?\s*</h[1-6]>\s*(?P<section>.*?)(?=<h[1-6][^>]*>|$)",
re.DOTALL | re.IGNORECASE,

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long