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: on:
push: push:
tags: tags:
- "*" - '*'
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View file

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

View file

@ -1,4 +1,4 @@
minimum_pre_commit_version: "3.5.0" minimum_pre_commit_version: '3.5.0'
repos: repos:
- repo: https://github.com/JohnnyMorganz/StyLua - repo: https://github.com/JohnnyMorganz/StyLua
@ -17,7 +17,7 @@ repos:
files: \.py$ files: \.py$
- id: ruff - id: ruff
name: ruff (lint imports) name: ruff (lint imports)
args: ["--fix", "--select=I"] args: ['--fix', '--select=I']
files: \.py$ files: \.py$
- repo: local - repo: local
@ -26,7 +26,7 @@ repos:
name: mypy (type check) name: mypy (type check)
entry: uv run mypy entry: uv run mypy
language: system language: system
args: ["."] args: ['.']
pass_filenames: false pass_filenames: false
- repo: https://github.com/pre-commit/mirrors-prettier - repo: https://github.com/pre-commit/mirrors-prettier
@ -35,4 +35,3 @@ repos:
- id: prettier - id: prettier
name: prettier (format markdown) name: prettier (format markdown)
files: \.md$ 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** **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 https://github.com/user-attachments/assets/50b19481-8e6d-47b4-bebc-15e16c61a9c9
## Features ## 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 - **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 - **Language agnostic**: Works with any language
- **Diff viewer**: Compare expected vs actual output with 3 diff modes - **Diff viewer**: Compare expected vs actual output with 3 diff modes
## Optional Dependencies ## Optional Dependencies
- [uv](https://docs.astral.sh/uv/) for problem scraping - [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 ## Quick Start
@ -32,10 +38,11 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
:CP codeforces 1848 :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** 4. **Navigate between problems**
@ -54,7 +61,9 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
:help cp.nvim :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 ## Similar Projects

View file

@ -38,14 +38,23 @@ COMMANDS *cp-commands*
Example: > Example: >
:CP atcoder abc324 :CP atcoder abc324
< <
Action Commands ~ View Commands ~
:CP run Toggle run panel for individual test cases. :CP run [n] Run tests in I/O view (see |cp-io-view|).
Shows per-test results with redesigned Lightweight split showing test verdicts.
layout for efficient comparison. Without [n]: runs all tests, shows verdict summary
With [n]: runs test n, shows detailed output
:CP debug Examples: >
Same as above but with the debug mode configured :CP run " All tests, verdict list
settings. :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 :CP pick Launch configured picker for interactive
platform/contest selection. platform/contest selection.
@ -145,8 +154,8 @@ Here's an example configuration with lazy.nvim:
open_url = true, open_url = true,
debug = false, debug = false,
ui = { ui = {
panel = {
ansi = true, ansi = true,
panel = {
diff_mode = 'vim', diff_mode = 'vim',
max_output_lines = 50, max_output_lines = 50,
}, },
@ -238,14 +247,14 @@ run CSES problems with Rust using the single schema:
*CpUI* *CpUI*
Fields: ~ Fields: ~
{ansi} (boolean, default: true) Enable ANSI color parsing
and highlighting in both I/O view and panel.
{panel} (|PanelConfig|) Test panel behavior configuration. {panel} (|PanelConfig|) Test panel behavior configuration.
{diff} (|DiffConfig|) Diff backend configuration. {diff} (|DiffConfig|) Diff backend configuration.
{picker} (string|nil) 'telescope', 'fzf-lua', or nil. {picker} (string|nil) 'telescope', 'fzf-lua', or nil.
*cp.PanelConfig* *cp.PanelConfig*
Fields: ~ Fields: ~
{ansi} (boolean, default: true) Enable ANSI color parsing
and highlighting.
{diff_mode} (string, default: "none") Diff backend: "none", {diff_mode} (string, default: "none") Diff backend: "none",
"vim", or "git". "vim", or "git".
{max_output_lines} (number, default: 50) Maximum lines of test output. {max_output_lines} (number, default: 50) Maximum lines of test output.
@ -272,16 +281,32 @@ run CSES problems with Rust using the single schema:
function(state: cp.State) function(state: cp.State)
{setup_code} (function, optional) Called after source file is opened. {setup_code} (function, optional) Called after source file is opened.
function(state: cp.State) 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 Hook functions receive the cp.nvim state object (|cp.State|). See
module documentation (lua/cp/state.lua) for available methods and fields. |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 >lua
hooks = { hooks = {
setup_code = function(state) setup_code = function(state)
print("Setting up " .. state.get_base_name()) print("Setting up " .. state.get_base_name())
print("Source file: " .. state.get_source_file()) 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 end
} }
< <
@ -333,8 +358,9 @@ Example: Setting up and solving AtCoder contest ABC324
3. Code your solution, then test: > 3. Code your solution, then test: >
:CP run :CP run
< Navigate with j/k, run specific tests with <enter> < View test verdicts in I/O splits. For detailed analysis:
Exit test panel with q or :CP run when done :CP panel
< Navigate tests with <c-n>/<c-p>, exit with q
4. Move to next problem: > 4. Move to next problem: >
:CP next :CP next
@ -349,6 +375,44 @@ Example: Setting up and solving AtCoder contest ABC324
7. Submit solutions on AtCoder website 7. Submit solutions on AtCoder website
==============================================================================
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* PICKER INTEGRATION *cp-picker*
@ -367,11 +431,13 @@ PICKER KEYMAPS *cp-picker-keys*
Useful when contest lists are outdated or incomplete Useful when contest lists are outdated or incomplete
============================================================================== ==============================================================================
PANEL *cp-run* PANEL *cp-panel*
The panel provides individual test case debugging. Problem time/memory The panel provides full-screen test analysis with diff modes for detailed
limit constraints are in columns Time/Mem respectively. Used time/memory are debugging. Problem time/memory limit constraints are in columns Time/Mem
in columns Runtime/RSS respectively. respectively. Used time/memory are in columns Runtime/RSS respectively.
Access with :CP panel or :CP debug (uses debug build configuration).
Interface ~ Interface ~
@ -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-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
q Exit run panel and restore layout q Exit panel and restore layout
<c-q> Exit interactive terminal and restore layout <c-q> Exit interactive terminal and restore layout
Diff Modes ~ Diff Modes ~
@ -537,8 +624,7 @@ cp.nvim creates the following file structure upon problem setup: >
{problem_id}.run " Compiled binary {problem_id}.run " Compiled binary
io/ io/
{problem_id}.n.cpin " nth test input {problem_id}.n.cpin " nth test input
{problem_id}.n.cpout " nth program output {problem_id}.n.cpout " nth test expected output
{problem_id}.expected " Expected output
< <
============================================================================== ==============================================================================
HEALTH CHECK *cp-health* HEALTH CHECK *cp-health*

View file

@ -16,6 +16,7 @@ local actions = constants.ACTIONS
---@field platform? string ---@field platform? string
---@field problem_id? string ---@field problem_id? string
---@field interactor_cmd? string ---@field interactor_cmd? string
---@field test_index? integer
--- 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
@ -52,6 +53,23 @@ local function parse_command(args)
else else
return { type = 'action', action = 'interact' } return { type = 'action', action = 'interact' }
end 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 else
return { type = 'action', action = first } return { type = 'action', action = first }
end end
@ -109,6 +127,8 @@ function M.handle_command(opts)
if cmd.action == 'interact' then if cmd.action == 'interact' then
ui.toggle_interactive(cmd.interactor_cmd) ui.toggle_interactive(cmd.interactor_cmd)
elseif cmd.action == 'run' then elseif cmd.action == 'run' then
ui.run_io_view(cmd.test_index)
elseif cmd.action == 'panel' then
ui.toggle_panel() ui.toggle_panel()
elseif cmd.action == 'debug' then elseif cmd.action == 'debug' then
ui.toggle_panel({ debug = true }) ui.toggle_panel({ debug = true })

View file

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

View file

@ -1,7 +1,7 @@
local M = {} local M = {}
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' } 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 = { M.PLATFORM_DISPLAY_NAMES = {
atcoder = 'AtCoder', 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 M = {}
local config_module = require('cp.config') local config_module = require('cp.config')
local helpers = require('cp.helpers')
local logger = require('cp.log') local logger = require('cp.log')
M.helpers = helpers
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)
return {} return {}

View file

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

View file

@ -3,6 +3,7 @@ local M = {}
local cache = require('cp.cache') local cache = require('cp.cache')
local config_module = require('cp.config') local config_module = require('cp.config')
local constants = require('cp.constants') local constants = require('cp.constants')
local helpers = require('cp.helpers')
local logger = require('cp.log') local logger = require('cp.log')
local scraper = require('cp.scraper') local scraper = require('cp.scraper')
local state = require('cp.state') local state = require('cp.state')
@ -59,6 +60,18 @@ local function start_tests(platform, contest_id, problems)
ev.memory_mb or 0, ev.memory_mb or 0,
ev.interactive 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 end
end end
@ -152,6 +165,7 @@ end
function M.setup_problem(problem_id, language) function M.setup_problem(problem_id, language)
local platform = state.get_platform() local platform = state.get_platform()
if not platform then if not platform then
logger.log('No platform/contest/problem configured.', vim.log.levels.ERROR)
return return
end end
@ -178,6 +192,9 @@ function M.setup_problem(problem_id, language)
if ok then if ok then
vim.b[prov.bufnr].cp_setup_done = true vim.b[prov.bufnr].cp_setup_done = true
end end
elseif not vim.b[prov.bufnr].cp_setup_done then
helpers.clearcol(prov.bufnr)
vim.b[prov.bufnr].cp_setup_done = true
end end
cache.set_file_state( cache.set_file_state(
vim.fn.fnamemodify(source_file, ':p'), vim.fn.fnamemodify(source_file, ':p'),
@ -186,6 +203,7 @@ function M.setup_problem(problem_id, language)
state.get_problem_id() or '', state.get_problem_id() or '',
lang lang
) )
require('cp.ui.panel').ensure_io_view()
end end
state.set_provisional(nil) state.set_provisional(nil)
return return
@ -201,6 +219,9 @@ function M.setup_problem(problem_id, language)
if ok then if ok then
vim.b[bufnr].cp_setup_done = true vim.b[bufnr].cp_setup_done = true
end end
elseif not vim.b[bufnr].cp_setup_done then
helpers.clearcol(bufnr)
vim.b[bufnr].cp_setup_done = true
end end
cache.set_file_state( cache.set_file_state(
vim.fn.expand('%:p'), vim.fn.expand('%:p'),
@ -209,6 +230,7 @@ function M.setup_problem(problem_id, language)
state.get_problem_id() or '', state.get_problem_id() or '',
lang lang
) )
require('cp.ui.panel').ensure_io_view()
end) end)
end end
@ -247,7 +269,11 @@ function M.navigate_problem(direction)
return return
end end
local active_panel = state.get_active_panel()
if active_panel == 'run' then
require('cp.ui.panel').disable() require('cp.ui.panel').disable()
end
M.setup_contest(platform, contest_id, problems[new_index].id) M.setup_contest(platform, contest_id, problems[new_index].id)
end end

View file

@ -3,9 +3,15 @@
---@field platform string ---@field platform string
---@field contest_id string ---@field contest_id string
---@field language string ---@field language string
---@field requested_problem_id string|nil ---@field requested_problem_id string?
---@field token integer ---@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 ---@class cp.State
---@field get_platform fun(): string? ---@field get_platform fun(): string?
---@field set_platform fun(platform: string) ---@field set_platform fun(platform: string)
@ -21,8 +27,12 @@
---@field get_input_file fun(): string? ---@field get_input_file fun(): string?
---@field get_output_file fun(): string? ---@field get_output_file fun(): string?
---@field get_expected_file fun(): string? ---@field get_expected_file fun(): string?
---@field get_provisional fun(): cp.ProvisionalState|nil ---@field get_provisional fun(): cp.ProvisionalState?
---@field set_provisional fun(p: cp.ProvisionalState|nil) ---@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 = {} local M = {}
@ -36,9 +46,10 @@ local state = {
active_panel = nil, active_panel = nil,
provisional = nil, provisional = nil,
solution_win = nil, solution_win = nil,
io_view_state = nil,
} }
---@return string|nil ---@return string?
function M.get_platform() function M.get_platform()
return state.platform return state.platform
end end
@ -48,7 +59,7 @@ function M.set_platform(platform)
state.platform = platform state.platform = platform
end end
---@return string|nil ---@return string?
function M.get_contest_id() function M.get_contest_id()
return state.contest_id return state.contest_id
end end
@ -58,7 +69,7 @@ function M.set_contest_id(contest_id)
state.contest_id = contest_id state.contest_id = contest_id
end end
---@return string|nil ---@return string?
function M.get_problem_id() function M.get_problem_id()
return state.problem_id return state.problem_id
end end
@ -68,7 +79,7 @@ function M.set_problem_id(problem_id)
state.problem_id = problem_id state.problem_id = problem_id
end end
---@return string|nil ---@return string?
function M.get_base_name() function M.get_base_name()
local platform, contest_id, problem_id = M.get_platform(), M.get_contest_id(), M.get_problem_id() 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 if not platform or not contest_id or not problem_id then
@ -86,7 +97,7 @@ function M.get_base_name()
end end
---@param language? string ---@param language? string
---@return string|nil ---@return string?
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
@ -110,46 +121,46 @@ function M.get_source_file(language)
return base_name .. '.' .. eff.extension return base_name .. '.' .. eff.extension
end end
---@return string|nil ---@return string?
function M.get_binary_file() function M.get_binary_file()
local base_name = M.get_base_name() local base_name = M.get_base_name()
return base_name and ('build/%s.run'):format(base_name) or nil return base_name and ('build/%s.run'):format(base_name) or nil
end end
---@return string|nil ---@return string?
function M.get_input_file() function M.get_input_file()
local base_name = M.get_base_name() local base_name = M.get_base_name()
return base_name and ('io/%s.cpin'):format(base_name) or nil return base_name and ('io/%s.cpin'):format(base_name) or nil
end end
---@return string|nil ---@return string?
function M.get_output_file() function M.get_output_file()
local base_name = M.get_base_name() local base_name = M.get_base_name()
return base_name and ('io/%s.cpout'):format(base_name) or nil return base_name and ('io/%s.cpout'):format(base_name) or nil
end end
---@return string|nil ---@return string?
function M.get_expected_file() function M.get_expected_file()
local base_name = M.get_base_name() local base_name = M.get_base_name()
return base_name and ('io/%s.expected'):format(base_name) or nil return base_name and ('io/%s.expected'):format(base_name) or nil
end end
---@return string|nil ---@return string?
function M.get_active_panel() function M.get_active_panel()
return state.active_panel return state.active_panel
end end
---@param panel string|nil ---@param panel string?
function M.set_active_panel(panel) function M.set_active_panel(panel)
state.active_panel = panel state.active_panel = panel
end end
---@return cp.ProvisionalState|nil ---@return cp.ProvisionalState?
function M.get_provisional() function M.get_provisional()
return state.provisional return state.provisional
end end
---@param p cp.ProvisionalState|nil ---@param p cp.ProvisionalState?
function M.set_provisional(p) function M.set_provisional(p)
state.provisional = p state.provisional = p
end end
@ -167,6 +178,38 @@ function M.set_solution_win(win)
state.solution_win = win state.solution_win = win
end 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 M._state = state
return M 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, 'CpAnsiBold', { bold = true })
vim.api.nvim_set_hl(0, 'CpAnsiItalic', { italic = true }) vim.api.nvim_set_hl(0, 'CpAnsiItalic', { italic = true })
vim.api.nvim_set_hl(0, 'CpAnsiBoldItalic', { bold = true, 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 end
---@param text string ---@param text string

View file

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

View file

@ -3,8 +3,9 @@ local M = {}
---@class PanelOpts ---@class PanelOpts
---@field debug? boolean ---@field debug? boolean
local cache = require('cp.cache')
local config_module = require('cp.config') local config_module = require('cp.config')
local constants = require('cp.constants') local helpers = require('cp.helpers')
local layouts = require('cp.ui.layouts') local layouts = require('cp.ui.layouts')
local logger = require('cp.log') local logger = require('cp.log')
local state = require('cp.state') local state = require('cp.state')
@ -52,38 +53,24 @@ function M.toggle_interactive(interactor_cmd)
return return
end end
local platform, contest_id = state.get_platform(), state.get_contest_id() local platform, contest_id, problem_id =
if not platform then state.get_platform(), state.get_contest_id(), state.get_problem_id()
logger.log('No platform configured.', vim.log.levels.ERROR) if not platform or not contest_id or not problem_id then
return
end
if not contest_id then
logger.log( logger.log(
("No contest %s configured for platform '%s'."):format( 'No platform/contest/problem configured. Use :CP <platform> <contest> [...] first.',
contest_id,
constants.PLATFORM_DISPLAY_NAMES[platform]
),
vim.log.levels.ERROR vim.log.levels.ERROR
) )
return return
end 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() cache.load()
local contest_data = cache.get_contest_data(platform, contest_id) local contest_data = cache.get_contest_data(platform, contest_id)
if if
not contest_data not contest_data
or not contest_data.index_map or not contest_data.index_map
or not contest_data.problems[contest_data.index_map[problem_id]] or contest_data.problems[contest_data.index_map[problem_id]].interactive
or not contest_data.problems[contest_data.index_map[problem_id]].interactive
then 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 return
end end
@ -92,9 +79,10 @@ function M.toggle_interactive(interactor_cmd)
vim.cmd('silent only') vim.cmd('silent only')
local execute = require('cp.runner.execute') local execute = require('cp.runner.execute')
local run = require('cp.runner.run')
local compile_result = execute.compile_problem() local compile_result = execute.compile_problem()
if not compile_result.success then if not compile_result.success then
require('cp.runner.run').handle_compilation_failure(compile_result.output) run.handle_compilation_failure(compile_result.output)
return return
end end
@ -204,6 +192,230 @@ function M.toggle_interactive(interactor_cmd)
state.set_active_panel('interactive') state.set_active_panel('interactive')
end 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 ---@param panel_opts? PanelOpts
function M.toggle_panel(panel_opts) function M.toggle_panel(panel_opts)
if state.get_active_panel() == 'run' then if state.get_active_panel() == 'run' then
@ -212,12 +424,14 @@ function M.toggle_panel(panel_opts)
current_diff_layout = nil current_diff_layout = nil
current_mode = nil current_mode = nil
end end
if state.saved_session then local saved = state.get_saved_session()
vim.cmd(('source %s'):format(state.saved_session)) if saved then
vim.fn.delete(state.saved_session) vim.cmd(('source %s'):format(saved))
state.saved_session = nil vim.fn.delete(saved)
state.set_saved_session(nil)
end end
state.set_active_panel(nil) state.set_active_panel(nil)
M.ensure_io_view()
return return
end end
@ -228,26 +442,14 @@ function M.toggle_panel(panel_opts)
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 or not contest_id then
logger.log( logger.log(
'No platform configured. Use :CP <platform> <contest> [...] first.', 'No platform/contest configured. Use :CP <platform> <contest> [...] first.',
vim.log.levels.ERROR vim.log.levels.ERROR
) )
return return
end 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() cache.load()
local contest_data = cache.get_contest_data(platform, contest_id) local contest_data = cache.get_contest_data(platform, contest_id)
if if
@ -260,6 +462,7 @@ function M.toggle_panel(panel_opts)
local config = config_module.get_config() local config = config_module.get_config()
local run = require('cp.runner.run') local run = require('cp.runner.run')
local run_render = require('cp.runner.run_render')
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'))
@ -268,11 +471,24 @@ function M.toggle_panel(panel_opts)
return return
end end
state.saved_session = vim.fn.tempname() local io_state = state.get_io_view_state()
vim.cmd(('mksession! %s'):format(state.saved_session)) 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') vim.cmd('silent only')
local tab_buf = utils.create_buffer_with_options() local tab_buf = utils.create_buffer_with_options()
helpers.clearcol(tab_buf)
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', 'cp', { buf = 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 if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
return return
end end
local run_render = require('cp.runner.run_render')
run_render.setup_highlights() run_render.setup_highlights()
local test_state = run.get_panel_state() local test_state = run.get_panel_state()
local tab_lines, tab_highlights = run_render.render_test_list(test_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() refresh_panel()
vim.schedule(function() vim.schedule(function()
if config.ui.panel.ansi then if config.ui.ansi then
local ansi = require('cp.ui.ansi') require('cp.ui.ansi').setup_highlight_groups()
ansi.setup_highlight_groups()
end end
if current_diff_layout then if current_diff_layout then
update_diff_panes() update_diff_panes()

View file

@ -125,6 +125,10 @@ function M.create_buffer_with_options(filetype)
return buf return buf
end end
---@param bufnr integer
---@param lines string[]
---@param highlights? Highlight[]
---@param namespace? integer
function M.update_buffer_content(bufnr, lines, highlights, namespace) function M.update_buffer_content(bufnr, lines, highlights, namespace)
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr }) 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( 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, re.DOTALL,
) )
TASK_LINK_RE = re.compile( 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, re.DOTALL,
) )
TITLE_RE = re.compile( 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>") TIME_RE = re.compile(r"<li>\s*<b>Time limit:</b>\s*([0-9.]+)\s*s\s*</li>")
MEM_RE = re.compile(r"<li><b>Memory limit:</b>\s*(\d+)\s*MB</li>") MEM_RE = re.compile(r"<li>\s*<b>Memory limit:</b>\s*(\d+)\s*MB\s*</li>")
SIDEBAR_CAT_RE = re.compile( 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( EXAMPLE_SECTION_RE = re.compile(
r"<h[1-6][^>]*>\s*example[s]?:?\s*</h[1-6]>\s*(?P<section>.*?)(?=<h[1-6][^>]*>|$)", r"<h[1-6][^>]*>\s*example[s]?:?\s*</h[1-6]>\s*(?P<section>.*?)(?=<h[1-6][^>]*>|$)",
re.DOTALL | re.IGNORECASE, 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