commit
07be94d7aa
13 changed files with 1030 additions and 156 deletions
24
.github/workflows/quality.yml
vendored
24
.github/workflows/quality.yml
vendored
|
|
@ -12,6 +12,7 @@ jobs:
|
|||
outputs:
|
||||
lua: ${{ steps.changes.outputs.lua }}
|
||||
python: ${{ steps.changes.outputs.python }}
|
||||
markdown: ${{ steps.changes.outputs.markdown }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
|
|
@ -33,6 +34,9 @@ jobs:
|
|||
- 'tests/scrapers/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
markdown:
|
||||
- '*.md'
|
||||
- 'docs/**/*.md'
|
||||
|
||||
lua-format:
|
||||
name: Lua Format Check
|
||||
|
|
@ -115,3 +119,23 @@ jobs:
|
|||
run: uv sync --dev
|
||||
- name: Type check Python files with mypy
|
||||
run: uv run mypy scrapers/ tests/scrapers/
|
||||
|
||||
markdown-format:
|
||||
name: Markdown Format Check
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.markdown == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Install prettier
|
||||
run: pnpm add -g prettier@3.1.0
|
||||
- name: Check markdown formatting with prettier
|
||||
run: prettier --check "*.md" "docs/**/*.md" || true
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ repos:
|
|||
hooks:
|
||||
- id: stylua-github
|
||||
name: stylua (Lua formatter)
|
||||
args: ["--check", "."]
|
||||
args: ["."]
|
||||
files: ^(lua/|spec/|plugin/|after/|ftdetect/|.*\.lua$)
|
||||
additional_dependencies: []
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
|
|
@ -16,7 +16,7 @@ repos:
|
|||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
- id: ruff
|
||||
name: ruff (lint)
|
||||
args: ["--no-fix"]
|
||||
args: ["--fix", "--select=I"]
|
||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
- repo: local
|
||||
hooks:
|
||||
|
|
@ -27,3 +27,9 @@ repos:
|
|||
args: ["scrapers/", "tests/scrapers/"]
|
||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
pass_filenames: false
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: prettier
|
||||
name: prettier (format markdown)
|
||||
files: \.(md)$
|
||||
|
|
|
|||
91
README.md
91
README.md
|
|
@ -1,27 +1,54 @@
|
|||
# cp.nvim
|
||||
|
||||
neovim plugin for competitive programming.
|
||||
**Fast, minimal competitive programming environment for Neovim**
|
||||
|
||||
Scrape problems, run tests, and debug solutions across multiple platforms with zero configuration.
|
||||
|
||||
> **Disclaimer**: cp.nvim webs crapes data from competitive programming platforms - use at your own risk.
|
||||
|
||||
https://github.com/user-attachments/assets/cb142535-fba0-4280-8f11-66ad1ca50ca9
|
||||
|
||||
[video config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua)
|
||||
|
||||
> Sample test data from [codeforces](https://codeforces.com) is scraped via [cloudscraper](https://github.com/VeNoMouS/cloudscraper). Use at your own risk.
|
||||
|
||||
## Features
|
||||
|
||||
- Support for multiple online judges ([AtCoder](https://atcoder.jp/), [Codeforces](https://codeforces.com/), [CSES](https://cses.fi))
|
||||
- Language-agnostic features
|
||||
- Automatic problem scraping and test case management
|
||||
- Integrated running and debugging
|
||||
- Enhanced test viewer
|
||||
- Templates via LuaSnip
|
||||
- **Multi-platform support**: AtCoder, Codeforces, CSES with consistent interface
|
||||
- **Automatic problem setup**: Scrape test cases and metadata in seconds
|
||||
- **Rich test output**: ANSI color support for compiler errors and program output
|
||||
- **Language agnostic**: Works with any compiled language
|
||||
- **Template integration**: Contest-specific snippets via LuaSnip
|
||||
- **Diff viewer**: Compare expected vs actual output with precision
|
||||
|
||||
## Requirements
|
||||
## Optional Dependencies
|
||||
|
||||
- Neovim 0.10.0+
|
||||
- [uv](https://docs.astral.sh/uv/): problem scraping (optional)
|
||||
- [LuaSnip](https://github.com/L3MON4D3/LuaSnip): contest-specific snippets (optional)
|
||||
- [uv](https://docs.astral.sh/uv/) for problem scraping
|
||||
- [LuaSnip](https://github.com/L3MON4D3/LuaSnip) for templates
|
||||
|
||||
## Quick Start
|
||||
|
||||
cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
1. **Find a problem** on the judge website
|
||||
2. **Set up locally** with `:CP <platform> <contest> <problem>`
|
||||
|
||||
```
|
||||
:CP codeforces 1848 A
|
||||
```
|
||||
|
||||
3. **Code and test** with instant feedback and rich diffs
|
||||
|
||||
```
|
||||
:CP run
|
||||
```
|
||||
|
||||
4. **Navigate between problems**
|
||||
|
||||
```
|
||||
:CP next
|
||||
:CP prev
|
||||
```
|
||||
|
||||
5. **Submit** on the original website
|
||||
|
||||
## Documentation
|
||||
|
||||
|
|
@ -29,37 +56,13 @@ https://github.com/user-attachments/assets/cb142535-fba0-4280-8f11-66ad1ca50ca9
|
|||
:help cp.nvim
|
||||
```
|
||||
|
||||
## Philosophy
|
||||
|
||||
This plugin is highly tuned to my workflow and may not fit for you. Personally,
|
||||
I believe there are two aspects of a cp workflow:
|
||||
|
||||
- local work (i.e. coding, running test cases)
|
||||
- site work (i.e. problem reading, submitting)
|
||||
|
||||
Namely, I do not like the idea of submitting problems locally - the experience
|
||||
will never quite offer what the remote does. Therefore, cp.nvim works as
|
||||
follows:
|
||||
|
||||
1. Find a problem
|
||||
|
||||
- Browse the remote and find it
|
||||
- Read it on the remote
|
||||
|
||||
2. Set up your local environment with `:CP ...`
|
||||
|
||||
- test cases and expected output automatically scraped
|
||||
- templates automatically configured
|
||||
|
||||
3. Solve the problem locally
|
||||
|
||||
- easy to run/debug
|
||||
- easy to diff actual vs. expected output
|
||||
|
||||
4. Submit the problem (on the remote!)
|
||||
|
||||
See [my config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua) for a relatively advanced setup.
|
||||
|
||||
## Similar Projects
|
||||
|
||||
- [competitest.nvim](https://github.com/xeluxee/competitest.nvim)
|
||||
- [assistant.nvim](https://github.com/A7Lavinraj/assistant.nvim)
|
||||
|
||||
## TODO
|
||||
|
||||
- Windows support
|
||||
|
|
|
|||
121
doc/cp.txt
121
doc/cp.txt
|
|
@ -16,10 +16,10 @@ Supported languages: C++, Python
|
|||
REQUIREMENTS *cp-requirements*
|
||||
|
||||
- Neovim 0.10.0+
|
||||
- uv package manager (https://docs.astral.sh/uv/)
|
||||
- Language runtime/compiler (g++, python3)
|
||||
- Unix-like operating system
|
||||
|
||||
Optional:
|
||||
- uv package manager (https://docs.astral.sh/uv/)
|
||||
- LuaSnip for template expansion (https://github.com/L3MON4D3/LuaSnip)
|
||||
|
||||
==============================================================================
|
||||
|
|
@ -380,7 +380,7 @@ Example: Setting up and solving AtCoder contest ABC324
|
|||
|
||||
7. Continue solving problems with :CP next/:CP prev navigation
|
||||
|
||||
8. Switch to another file (e.g., previous contest): >
|
||||
8. Switch to another file (e.g. previous contest): >
|
||||
:e ~/contests/abc323/a.cpp
|
||||
:CP
|
||||
< Automatically restores abc323 contest context
|
||||
|
|
@ -421,9 +421,9 @@ The run panel uses the following table layout: >
|
|||
└─────┴────────┴──────────────┴───────────┴──────────┴─────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│Expected vs Actual │
|
||||
│4[-2-]{+3+} │
|
||||
│423 │
|
||||
│100 │
|
||||
│hello w[-o-]r{+o+}ld │
|
||||
│hello world │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Status Indicators ~
|
||||
|
|
@ -434,10 +434,20 @@ Test cases use competitive programming terminology with color highlighting:
|
|||
WA Wrong Answer (output mismatch) - Red
|
||||
TLE Time Limit Exceeded (timeout) - Orange
|
||||
RTE Runtime Error (non-zero exit) - Purple
|
||||
<
|
||||
|
||||
Highlight Groups ~
|
||||
*cp-highlights*
|
||||
cp.nvim defines the following highlight groups for status indicators:
|
||||
==============================================================================
|
||||
ANSI COLORS AND HIGHLIGHTING *cp-ansi*
|
||||
|
||||
cp.nvim provides comprehensive ANSI color support and highlighting for
|
||||
compiler output, program stderr, and diff visualization.
|
||||
|
||||
==============================================================================
|
||||
HIGHLIGHT GROUPS *cp-highlights*
|
||||
|
||||
Test Status Groups ~
|
||||
|
||||
Test cases use competitive programming terminology with color highlighting:
|
||||
|
||||
CpTestAC Green foreground for AC status
|
||||
CpTestWA Red foreground for WA status
|
||||
|
|
@ -445,14 +455,93 @@ cp.nvim defines the following highlight groups for status indicators:
|
|||
CpTestRTE Purple foreground for RTE status
|
||||
CpTestPending Gray foreground for pending tests
|
||||
|
||||
You can customize these colors by linking to other highlight groups in your
|
||||
colorscheme or by redefining them: >lua
|
||||
vim.api.nvim_set_hl(0, 'CpTestAC', { link = 'DiffAdd' })
|
||||
vim.api.nvim_set_hl(0, 'CpTestWA', { fg = '#ff0000' })
|
||||
ANSI Color Groups ~
|
||||
|
||||
cp.nvim preserves ANSI colors from compiler output and program stderr using
|
||||
a sophisticated parsing system. Colors are automatically mapped to your
|
||||
terminal colorscheme via vim.g.terminal_color_* variables.
|
||||
|
||||
Basic formatting groups:
|
||||
CpAnsiBold Bold text formatting
|
||||
CpAnsiItalic Italic text formatting
|
||||
CpAnsiBoldItalic Combined bold and italic formatting
|
||||
|
||||
Standard terminal colors (each supports Bold, Italic, BoldItalic variants):
|
||||
CpAnsiRed Standard red (terminal_color_1)
|
||||
CpAnsiGreen Standard green (terminal_color_2)
|
||||
CpAnsiYellow Standard yellow (terminal_color_3)
|
||||
CpAnsiBlue Standard blue (terminal_color_4)
|
||||
CpAnsiMagenta Standard magenta (terminal_color_5)
|
||||
CpAnsiCyan Standard cyan (terminal_color_6)
|
||||
CpAnsiWhite Standard white (terminal_color_7)
|
||||
CpAnsiBlack Standard black (terminal_color_0)
|
||||
|
||||
Bright color variants:
|
||||
CpAnsiBrightRed Bright red (terminal_color_9)
|
||||
CpAnsiBrightGreen Bright green (terminal_color_10)
|
||||
CpAnsiBrightYellow Bright yellow (terminal_color_11)
|
||||
CpAnsiBrightBlue Bright blue (terminal_color_12)
|
||||
CpAnsiBrightMagenta Bright magenta (terminal_color_13)
|
||||
CpAnsiBrightCyan Bright cyan (terminal_color_14)
|
||||
CpAnsiBrightWhite Bright white (terminal_color_15)
|
||||
CpAnsiBrightBlack Bright black (terminal_color_8)
|
||||
|
||||
Example combinations:
|
||||
CpAnsiBoldRed Bold red combination
|
||||
CpAnsiItalicGreen Italic green combination
|
||||
CpAnsiBoldItalicYellow Bold italic yellow combination
|
||||
|
||||
Diff Highlighting ~
|
||||
|
||||
Diff visualization uses Neovim's built-in highlight groups that automatically
|
||||
adapt to your colorscheme:
|
||||
|
||||
DiffAdd Highlights added text in git diffs
|
||||
DiffDelete Highlights removed text in git diffs
|
||||
|
||||
These groups are automatically used by the git diff backend for character-level
|
||||
difference visualization with optimal colorscheme integration.
|
||||
|
||||
==============================================================================
|
||||
TERMINAL COLOR INTEGRATION *cp-terminal-colors*
|
||||
|
||||
ANSI colors automatically use your terminal's color palette through Neovim's
|
||||
vim.g.terminal_color_* variables. This ensures compiler colors match your
|
||||
colorscheme without manual configuration.
|
||||
|
||||
If your colorscheme doesn't set terminal colors, cp.nvim falls back to
|
||||
sensible defaults. You can override terminal colors in your configuration: >vim
|
||||
let g:terminal_color_1 = '#ff6b6b' " Custom red
|
||||
let g:terminal_color_2 = '#51cf66' " Custom green
|
||||
<
|
||||
|
||||
Keymaps ~
|
||||
*cp-test-keys*
|
||||
==============================================================================
|
||||
HIGHLIGHT CUSTOMIZATION *cp-highlight-custom*
|
||||
|
||||
You can customize any highlight group by linking to existing groups or
|
||||
defining custom colors: >lua
|
||||
|
||||
-- Customize the color of "TLE" text in run panel:
|
||||
vim.api.nvim_set_hl(0, 'CpTestTLE', { fg = '#ffa500', bold = true })
|
||||
|
||||
-- ... or the ANSI colors used to display stderr
|
||||
vim.api.nvim_set_hl(0, 'CpAnsiRed', {
|
||||
fg = vim.g.terminal_color_1 or '#ef4444'
|
||||
})
|
||||
<
|
||||
|
||||
Place customizations in your init.lua or after the colorscheme loads to
|
||||
prevent them from being overridden: >lua
|
||||
vim.api.nvim_create_autocmd('ColorScheme', {
|
||||
callback = function()
|
||||
-- Your cp.nvim highlight customizations here
|
||||
vim.api.nvim_set_hl(0, 'CpTestAC', { link = 'String' })
|
||||
end
|
||||
})
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
RUN PANEL KEYMAPS *cp-test-keys*
|
||||
<c-n> Navigate to next test case (configurable via
|
||||
run_panel.next_test_key)
|
||||
<c-p> Navigate to previous test case (configurable via
|
||||
|
|
@ -507,10 +596,6 @@ Cache Location ~
|
|||
Cache is stored at: >
|
||||
vim.fn.stdpath('data') .. '/cp-nvim.json'
|
||||
<
|
||||
Typically resolves to:
|
||||
• Linux/macOS: `~/.local/share/nvim/cp-nvim.json`
|
||||
• Windows: `%LOCALAPPDATA%\nvim-data\cp-nvim.json`
|
||||
|
||||
Cache Structure ~
|
||||
*cp-cache-structure*
|
||||
The cache contains four main sections:
|
||||
|
|
|
|||
227
lua/cp/ansi.lua
Normal file
227
lua/cp/ansi.lua
Normal file
|
|
@ -0,0 +1,227 @@
|
|||
---@class AnsiParseResult
|
||||
---@field lines string[]
|
||||
---@field highlights table[]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param raw_output string|table
|
||||
---@return string
|
||||
function M.bytes_to_string(raw_output)
|
||||
if type(raw_output) == 'string' then
|
||||
return raw_output
|
||||
end
|
||||
return table.concat(vim.tbl_map(string.char, raw_output))
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@return AnsiParseResult
|
||||
function M.parse_ansi_text(text)
|
||||
local clean_text = text:gsub('\027%[[%d;]*[a-zA-Z]', '')
|
||||
local lines = vim.split(clean_text, '\n', { plain = true, trimempty = false })
|
||||
|
||||
local highlights = {}
|
||||
local line_num = 0
|
||||
local col_pos = 0
|
||||
|
||||
local ansi_state = {
|
||||
bold = false,
|
||||
italic = false,
|
||||
foreground = nil,
|
||||
}
|
||||
|
||||
local function get_highlight_group()
|
||||
if not ansi_state.bold and not ansi_state.italic and not ansi_state.foreground then
|
||||
return nil
|
||||
end
|
||||
|
||||
local parts = { 'CpAnsi' }
|
||||
if ansi_state.bold then
|
||||
table.insert(parts, 'Bold')
|
||||
end
|
||||
if ansi_state.italic then
|
||||
table.insert(parts, 'Italic')
|
||||
end
|
||||
if ansi_state.foreground then
|
||||
table.insert(parts, ansi_state.foreground)
|
||||
end
|
||||
|
||||
return table.concat(parts)
|
||||
end
|
||||
|
||||
local function apply_highlight(start_line, start_col, end_col)
|
||||
local hl_group = get_highlight_group()
|
||||
if hl_group then
|
||||
table.insert(highlights, {
|
||||
line = start_line,
|
||||
col_start = start_col,
|
||||
col_end = end_col,
|
||||
highlight_group = hl_group,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local i = 1
|
||||
while i <= #text do
|
||||
local ansi_start, ansi_end, code, cmd = text:find('\027%[([%d;]*)([a-zA-Z])', i)
|
||||
|
||||
if ansi_start then
|
||||
if ansi_start > i then
|
||||
local segment = text:sub(i, ansi_start - 1)
|
||||
local start_line = line_num
|
||||
local start_col = col_pos
|
||||
|
||||
for char in segment:gmatch('.') do
|
||||
if char == '\n' then
|
||||
if col_pos > start_col then
|
||||
apply_highlight(start_line, start_col, col_pos)
|
||||
end
|
||||
line_num = line_num + 1
|
||||
start_line = line_num
|
||||
col_pos = 0
|
||||
start_col = 0
|
||||
else
|
||||
col_pos = col_pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if col_pos > start_col then
|
||||
apply_highlight(start_line, start_col, col_pos)
|
||||
end
|
||||
end
|
||||
|
||||
if cmd == 'm' then
|
||||
M.update_ansi_state(ansi_state, code)
|
||||
end
|
||||
i = ansi_end + 1
|
||||
else
|
||||
local segment = text:sub(i)
|
||||
if segment ~= '' then
|
||||
local start_line = line_num
|
||||
local start_col = col_pos
|
||||
|
||||
for char in segment:gmatch('.') do
|
||||
if char == '\n' then
|
||||
if col_pos > start_col then
|
||||
apply_highlight(start_line, start_col, col_pos)
|
||||
end
|
||||
line_num = line_num + 1
|
||||
start_line = line_num
|
||||
col_pos = 0
|
||||
start_col = 0
|
||||
else
|
||||
col_pos = col_pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if col_pos > start_col then
|
||||
apply_highlight(start_line, start_col, col_pos)
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
lines = lines,
|
||||
highlights = highlights,
|
||||
}
|
||||
end
|
||||
|
||||
---@param ansi_state table
|
||||
---@param code_string string
|
||||
function M.update_ansi_state(ansi_state, code_string)
|
||||
if code_string == '' or code_string == '0' then
|
||||
ansi_state.bold = false
|
||||
ansi_state.italic = false
|
||||
ansi_state.foreground = nil
|
||||
return
|
||||
end
|
||||
|
||||
local codes = vim.split(code_string, ';', { plain = true })
|
||||
|
||||
for _, code in ipairs(codes) do
|
||||
local num = tonumber(code)
|
||||
if num then
|
||||
if num == 1 then
|
||||
ansi_state.bold = true
|
||||
elseif num == 3 then
|
||||
ansi_state.italic = true
|
||||
elseif num == 22 then
|
||||
ansi_state.bold = false
|
||||
elseif num == 23 then
|
||||
ansi_state.italic = false
|
||||
elseif num >= 30 and num <= 37 then
|
||||
local colors = { 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' }
|
||||
ansi_state.foreground = colors[num - 29]
|
||||
elseif num >= 90 and num <= 97 then
|
||||
local colors = {
|
||||
'BrightBlack',
|
||||
'BrightRed',
|
||||
'BrightGreen',
|
||||
'BrightYellow',
|
||||
'BrightBlue',
|
||||
'BrightMagenta',
|
||||
'BrightCyan',
|
||||
'BrightWhite',
|
||||
}
|
||||
ansi_state.foreground = colors[num - 89]
|
||||
elseif num == 39 then
|
||||
ansi_state.foreground = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function M.setup_highlight_groups()
|
||||
local color_map = {
|
||||
Black = vim.g.terminal_color_0,
|
||||
Red = vim.g.terminal_color_1,
|
||||
Green = vim.g.terminal_color_2,
|
||||
Yellow = vim.g.terminal_color_3,
|
||||
Blue = vim.g.terminal_color_4,
|
||||
Magenta = vim.g.terminal_color_5,
|
||||
Cyan = vim.g.terminal_color_6,
|
||||
White = vim.g.terminal_color_7,
|
||||
BrightBlack = vim.g.terminal_color_8,
|
||||
BrightRed = vim.g.terminal_color_9,
|
||||
BrightGreen = vim.g.terminal_color_10,
|
||||
BrightYellow = vim.g.terminal_color_11,
|
||||
BrightBlue = vim.g.terminal_color_12,
|
||||
BrightMagenta = vim.g.terminal_color_13,
|
||||
BrightCyan = vim.g.terminal_color_14,
|
||||
BrightWhite = vim.g.terminal_color_15,
|
||||
}
|
||||
|
||||
local combinations = {
|
||||
{ bold = false, italic = false },
|
||||
{ bold = true, italic = false },
|
||||
{ bold = false, italic = true },
|
||||
{ bold = true, italic = true },
|
||||
}
|
||||
|
||||
for _, combo in ipairs(combinations) do
|
||||
for color_name, terminal_color in pairs(color_map) do
|
||||
local parts = { 'CpAnsi' }
|
||||
local opts = { fg = terminal_color }
|
||||
|
||||
if combo.bold then
|
||||
table.insert(parts, 'Bold')
|
||||
opts.bold = true
|
||||
end
|
||||
if combo.italic then
|
||||
table.insert(parts, 'Italic')
|
||||
opts.italic = true
|
||||
end
|
||||
table.insert(parts, color_name)
|
||||
|
||||
local hl_name = table.concat(parts)
|
||||
vim.api.nvim_set_hl(0, hl_name, opts)
|
||||
end
|
||||
end
|
||||
|
||||
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 })
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -58,10 +58,95 @@ local git_backend = {
|
|||
highlights = {},
|
||||
}
|
||||
else
|
||||
-- Parse git diff output to extract content and highlights
|
||||
local diff_content = result.stdout or ''
|
||||
local lines = {}
|
||||
local highlights = {}
|
||||
local line_num = 0
|
||||
|
||||
-- Extract content lines that start with space, +, or -
|
||||
for line in diff_content:gmatch('[^\n]*') do
|
||||
if
|
||||
line:match('^[%s%+%-]')
|
||||
or (not line:match('^[@%-+]') and not line:match('^index') and not line:match('^diff'))
|
||||
then
|
||||
-- This is content, not metadata
|
||||
local clean_line = line
|
||||
if line:match('^[%+%-]') then
|
||||
clean_line = line:sub(2) -- Remove +/- prefix
|
||||
end
|
||||
|
||||
-- Parse diff markers in the line
|
||||
local col_pos = 0
|
||||
local processed_line = ''
|
||||
local i = 1
|
||||
|
||||
while i <= #clean_line do
|
||||
local removed_start, removed_end = clean_line:find('%[%-[^%-]*%-]', i)
|
||||
local added_start, added_end = clean_line:find('{%+[^%+]*%+}', i)
|
||||
|
||||
local next_marker_start = nil
|
||||
local marker_type = nil
|
||||
|
||||
if removed_start and (not added_start or removed_start < added_start) then
|
||||
next_marker_start = removed_start
|
||||
marker_type = 'removed'
|
||||
elseif added_start then
|
||||
next_marker_start = added_start
|
||||
marker_type = 'added'
|
||||
end
|
||||
|
||||
if next_marker_start then
|
||||
-- Add text before marker
|
||||
if next_marker_start > i then
|
||||
local before_text = clean_line:sub(i, next_marker_start - 1)
|
||||
processed_line = processed_line .. before_text
|
||||
col_pos = col_pos + #before_text
|
||||
end
|
||||
|
||||
-- Extract and add marker content with highlighting
|
||||
local marker_end = (marker_type == 'removed') and removed_end or added_end
|
||||
local marker_text = clean_line:sub(next_marker_start, marker_end)
|
||||
local content_text
|
||||
|
||||
if marker_type == 'removed' then
|
||||
content_text = marker_text:sub(3, -3) -- Remove [- and -]
|
||||
table.insert(highlights, {
|
||||
line = line_num,
|
||||
col_start = col_pos,
|
||||
col_end = col_pos + #content_text,
|
||||
highlight_group = 'DiffDelete',
|
||||
})
|
||||
else -- added
|
||||
content_text = marker_text:sub(3, -3) -- Remove {+ and +}
|
||||
table.insert(highlights, {
|
||||
line = line_num,
|
||||
col_start = col_pos,
|
||||
col_end = col_pos + #content_text,
|
||||
highlight_group = 'DiffAdd',
|
||||
})
|
||||
end
|
||||
|
||||
processed_line = processed_line .. content_text
|
||||
col_pos = col_pos + #content_text
|
||||
i = marker_end + 1
|
||||
else
|
||||
-- No more markers, add rest of line
|
||||
local rest = clean_line:sub(i)
|
||||
processed_line = processed_line .. rest
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(lines, processed_line)
|
||||
line_num = line_num + 1
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
content = {},
|
||||
highlights = {},
|
||||
raw_diff = result.stdout or '',
|
||||
content = lines,
|
||||
highlights = highlights,
|
||||
raw_diff = diff_content,
|
||||
}
|
||||
end
|
||||
end,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ local function get_language_from_file(source_file, contest_config)
|
|||
|
||||
local extension = vim.fn.fnamemodify(source_file, ':e')
|
||||
local language = filetype_to_language[extension] or contest_config.default_language
|
||||
logger.log(('detected language: %s (extension: %s)'):format(language, extension))
|
||||
return language
|
||||
end
|
||||
|
||||
|
|
@ -70,7 +69,7 @@ end
|
|||
|
||||
---@param language_config table
|
||||
---@param substitutions table<string, string>
|
||||
---@return {code: integer, stderr: string}
|
||||
---@return {code: integer, stdout: string, stderr: string}
|
||||
function M.compile_generic(language_config, substitutions)
|
||||
vim.validate({
|
||||
language_config = { language_config, 'table' },
|
||||
|
|
@ -83,19 +82,25 @@ function M.compile_generic(language_config, substitutions)
|
|||
end
|
||||
|
||||
local compile_cmd = substitute_template(language_config.compile, substitutions)
|
||||
logger.log(('compiling: %s'):format(table.concat(compile_cmd, ' ')))
|
||||
local redirected_cmd = vim.deepcopy(compile_cmd)
|
||||
if #redirected_cmd > 0 then
|
||||
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
|
||||
end
|
||||
|
||||
local start_time = vim.uv.hrtime()
|
||||
local result = vim.system(compile_cmd, { text = true }):wait()
|
||||
local result = vim
|
||||
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, { text = false })
|
||||
:wait()
|
||||
local compile_time = (vim.uv.hrtime() - start_time) / 1000000
|
||||
|
||||
local ansi = require('cp.ansi')
|
||||
result.stdout = ansi.bytes_to_string(result.stdout or '')
|
||||
result.stderr = ansi.bytes_to_string(result.stderr or '')
|
||||
|
||||
if result.code == 0 then
|
||||
logger.log(('compilation successful (%.1fms)'):format(compile_time))
|
||||
else
|
||||
logger.log(
|
||||
('compilation failed (%.1fms): %s'):format(compile_time, result.stderr),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
logger.log(('compilation failed (%.1fms)'):format(compile_time))
|
||||
end
|
||||
|
||||
return result
|
||||
|
|
@ -112,12 +117,15 @@ local function execute_command(cmd, input_data, timeout_ms)
|
|||
timeout_ms = { timeout_ms, 'number' },
|
||||
})
|
||||
|
||||
logger.log(('executing: %s'):format(table.concat(cmd, ' ')))
|
||||
local redirected_cmd = vim.deepcopy(cmd)
|
||||
if #redirected_cmd > 0 then
|
||||
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
|
||||
end
|
||||
|
||||
local start_time = vim.uv.hrtime()
|
||||
|
||||
local result = vim
|
||||
.system(cmd, {
|
||||
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, {
|
||||
stdin = input_data,
|
||||
timeout = timeout_ms,
|
||||
text = true,
|
||||
|
|
@ -202,7 +210,7 @@ end
|
|||
---@param ctx ProblemContext
|
||||
---@param contest_config ContestConfig
|
||||
---@param is_debug? boolean
|
||||
---@return boolean success
|
||||
---@return {success: boolean, output: string?}
|
||||
function M.compile_problem(ctx, contest_config, is_debug)
|
||||
vim.validate({
|
||||
ctx = { ctx, 'table' },
|
||||
|
|
@ -214,7 +222,7 @@ function M.compile_problem(ctx, contest_config, is_debug)
|
|||
|
||||
if not language_config then
|
||||
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
|
||||
return false
|
||||
return { success = false, output = 'No configuration for language: ' .. language }
|
||||
end
|
||||
|
||||
local substitutions = {
|
||||
|
|
@ -229,16 +237,12 @@ function M.compile_problem(ctx, contest_config, is_debug)
|
|||
language_config.compile = compile_cmd
|
||||
local compile_result = M.compile_generic(language_config, substitutions)
|
||||
if compile_result.code ~= 0 then
|
||||
logger.log(
|
||||
'compilation failed: ' .. (compile_result.stderr or 'unknown error'),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return false
|
||||
return { success = false, output = compile_result.stdout or 'unknown error' }
|
||||
end
|
||||
logger.log(('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'))
|
||||
end
|
||||
|
||||
return true
|
||||
return { success = true, output = nil }
|
||||
end
|
||||
|
||||
function M.run_problem(ctx, contest_config, is_debug)
|
||||
|
|
|
|||
117
lua/cp/init.lua
117
lua/cp/init.lua
|
|
@ -179,6 +179,7 @@ local function toggle_run_panel(is_debug)
|
|||
vim.fn.delete(state.saved_session)
|
||||
state.saved_session = nil
|
||||
end
|
||||
|
||||
state.run_panel_active = false
|
||||
logger.log('test panel closed')
|
||||
return
|
||||
|
|
@ -198,9 +199,9 @@ local function toggle_run_panel(is_debug)
|
|||
end
|
||||
|
||||
local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config)
|
||||
local test_module = require('cp.test')
|
||||
local run = require('cp.run')
|
||||
|
||||
if not test_module.load_test_cases(ctx, state) then
|
||||
if not run.load_test_cases(ctx, state) then
|
||||
logger.log('no test cases found', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
|
@ -226,8 +227,9 @@ local function toggle_run_panel(is_debug)
|
|||
local diff_namespace = highlight.create_namespace()
|
||||
|
||||
local test_list_namespace = vim.api.nvim_create_namespace('cp_test_list')
|
||||
local ansi_namespace = vim.api.nvim_create_namespace('cp_ansi_highlights')
|
||||
|
||||
local function update_buffer_content(bufnr, lines, highlights)
|
||||
local function update_buffer_content(bufnr, lines, highlights, namespace)
|
||||
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
|
||||
|
||||
vim.api.nvim_set_option_value('readonly', false, { buf = bufnr })
|
||||
|
|
@ -236,14 +238,7 @@ local function toggle_run_panel(is_debug)
|
|||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
|
||||
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, test_list_namespace, 0, -1)
|
||||
for _, hl in ipairs(highlights) do
|
||||
vim.api.nvim_buf_set_extmark(bufnr, test_list_namespace, hl.line, hl.col_start, {
|
||||
end_col = hl.col_end,
|
||||
hl_group = hl.highlight_group,
|
||||
priority = 200,
|
||||
})
|
||||
end
|
||||
highlight.apply_highlights(bufnr, highlights, namespace or test_list_namespace)
|
||||
end
|
||||
|
||||
local function create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||
|
|
@ -326,8 +321,31 @@ local function toggle_run_panel(is_debug)
|
|||
}
|
||||
end
|
||||
|
||||
local function create_single_layout(parent_win, content)
|
||||
local buf = create_buffer_with_options()
|
||||
local lines = vim.split(content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(buf, lines, {})
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
local win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(win, buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = buf })
|
||||
|
||||
return {
|
||||
buffers = { buf },
|
||||
windows = { win },
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, win, true)
|
||||
pcall(vim.api.nvim_buf_delete, buf, { force = true })
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local function create_diff_layout(mode, parent_win, expected_content, actual_content)
|
||||
if mode == 'git' then
|
||||
if mode == 'single' then
|
||||
return create_single_layout(parent_win, actual_content)
|
||||
elseif mode == 'git' then
|
||||
return create_git_diff_layout(parent_win, expected_content, actual_content)
|
||||
else
|
||||
return create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||
|
|
@ -335,7 +353,7 @@ local function toggle_run_panel(is_debug)
|
|||
end
|
||||
|
||||
local function update_diff_panes()
|
||||
local test_state = test_module.get_run_panel_state()
|
||||
local test_state = run.get_run_panel_state()
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
|
||||
if not current_test then
|
||||
|
|
@ -344,14 +362,19 @@ local function toggle_run_panel(is_debug)
|
|||
|
||||
local expected_content = current_test.expected or ''
|
||||
local actual_content = current_test.actual or '(not run yet)'
|
||||
local should_show_diff = current_test.status == 'fail' and current_test.actual
|
||||
local actual_highlights = current_test.actual_highlights or {}
|
||||
local is_compilation_failure = current_test.error
|
||||
and current_test.error:match('Compilation failed')
|
||||
local should_show_diff = current_test.status == 'fail'
|
||||
and current_test.actual
|
||||
and not is_compilation_failure
|
||||
|
||||
if not should_show_diff then
|
||||
expected_content = expected_content
|
||||
actual_content = actual_content
|
||||
end
|
||||
|
||||
local desired_mode = config.run_panel.diff_mode
|
||||
local desired_mode = is_compilation_failure and 'single' or config.run_panel.diff_mode
|
||||
|
||||
if current_diff_layout and current_mode ~= desired_mode then
|
||||
local saved_pos = vim.api.nvim_win_get_cursor(0)
|
||||
|
|
@ -380,7 +403,15 @@ local function toggle_run_panel(is_debug)
|
|||
setup_keybindings_for_buffer(buf)
|
||||
end
|
||||
else
|
||||
if desired_mode == 'git' then
|
||||
if desired_mode == 'single' then
|
||||
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(
|
||||
current_diff_layout.buffers[1],
|
||||
lines,
|
||||
actual_highlights,
|
||||
ansi_namespace
|
||||
)
|
||||
elseif desired_mode == 'git' then
|
||||
local diff_backend = require('cp.diff')
|
||||
local backend = diff_backend.get_best_backend('git')
|
||||
local diff_result = backend.render(expected_content, actual_content)
|
||||
|
|
@ -393,13 +424,23 @@ local function toggle_run_panel(is_debug)
|
|||
)
|
||||
else
|
||||
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(current_diff_layout.buffers[1], lines, {})
|
||||
update_buffer_content(
|
||||
current_diff_layout.buffers[1],
|
||||
lines,
|
||||
actual_highlights,
|
||||
ansi_namespace
|
||||
)
|
||||
end
|
||||
else
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(current_diff_layout.buffers[1], expected_lines, {})
|
||||
update_buffer_content(current_diff_layout.buffers[2], actual_lines, {})
|
||||
update_buffer_content(
|
||||
current_diff_layout.buffers[2],
|
||||
actual_lines,
|
||||
actual_highlights,
|
||||
ansi_namespace
|
||||
)
|
||||
|
||||
if should_show_diff then
|
||||
vim.api.nvim_set_option_value('diff', true, { win = current_diff_layout.windows[1] })
|
||||
|
|
@ -410,6 +451,8 @@ local function toggle_run_panel(is_debug)
|
|||
vim.api.nvim_win_call(current_diff_layout.windows[2], function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = current_diff_layout.windows[1] })
|
||||
vim.api.nvim_set_option_value('foldcolumn', '0', { win = current_diff_layout.windows[2] })
|
||||
else
|
||||
vim.api.nvim_set_option_value('diff', false, { win = current_diff_layout.windows[1] })
|
||||
vim.api.nvim_set_option_value('diff', false, { win = current_diff_layout.windows[2] })
|
||||
|
|
@ -423,17 +466,18 @@ local function toggle_run_panel(is_debug)
|
|||
return
|
||||
end
|
||||
|
||||
local test_render = require('cp.test_render')
|
||||
test_render.setup_highlights()
|
||||
local test_state = test_module.get_run_panel_state()
|
||||
local tab_lines, tab_highlights = test_render.render_test_list(test_state)
|
||||
local run_render = require('cp.run_render')
|
||||
run_render.setup_highlights()
|
||||
|
||||
local test_state = run.get_run_panel_state()
|
||||
local tab_lines, tab_highlights = run_render.render_test_list(test_state)
|
||||
update_buffer_content(test_buffers.tab_buf, tab_lines, tab_highlights)
|
||||
|
||||
update_diff_panes()
|
||||
end
|
||||
|
||||
local function navigate_test_case(delta)
|
||||
local test_state = test_module.get_run_panel_state()
|
||||
local test_state = run.get_run_panel_state()
|
||||
if #test_state.test_cases == 0 then
|
||||
return
|
||||
end
|
||||
|
|
@ -456,6 +500,12 @@ local function toggle_run_panel(is_debug)
|
|||
config.run_panel.diff_mode = config.run_panel.diff_mode == 'vim' and 'git' or 'vim'
|
||||
refresh_run_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', config.run_panel.next_test_key, function()
|
||||
navigate_test_case(1)
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', config.run_panel.prev_test_key, function()
|
||||
navigate_test_case(-1)
|
||||
end, { buffer = buf, silent = true })
|
||||
end
|
||||
|
||||
vim.keymap.set('n', config.run_panel.next_test_key, function()
|
||||
|
|
@ -477,18 +527,29 @@ local function toggle_run_panel(is_debug)
|
|||
|
||||
local execute_module = require('cp.execute')
|
||||
local contest_config = config.contests[state.platform]
|
||||
if execute_module.compile_problem(ctx, contest_config, is_debug) then
|
||||
test_module.run_all_test_cases(ctx, contest_config, config)
|
||||
local compile_result = execute_module.compile_problem(ctx, contest_config, is_debug)
|
||||
if compile_result.success then
|
||||
run.run_all_test_cases(ctx, contest_config, config)
|
||||
else
|
||||
run.handle_compilation_failure(compile_result.output)
|
||||
end
|
||||
|
||||
refresh_run_panel()
|
||||
|
||||
vim.schedule(function()
|
||||
local ansi = require('cp.ansi')
|
||||
ansi.setup_highlight_groups()
|
||||
if current_diff_layout then
|
||||
update_diff_panes()
|
||||
end
|
||||
end)
|
||||
|
||||
vim.api.nvim_set_current_win(test_windows.tab_win)
|
||||
|
||||
state.run_panel_active = true
|
||||
state.test_buffers = test_buffers
|
||||
state.test_windows = test_windows
|
||||
local test_state = test_module.get_run_panel_state()
|
||||
local test_state = run.get_run_panel_state()
|
||||
logger.log(string.format('test panel opened (%d test cases)', #test_state.test_cases))
|
||||
end
|
||||
|
||||
|
|
@ -540,8 +601,8 @@ local function navigate_problem(delta, language)
|
|||
local new_index = current_index + delta
|
||||
|
||||
if new_index < 1 or new_index > #problems then
|
||||
local direction = delta > 0 and 'next' or 'previous'
|
||||
logger.log(('no %s problem available'):format(direction), vim.log.levels.INFO)
|
||||
local msg = delta > 0 and 'at last problem' or 'at first problem'
|
||||
logger.log(msg, vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,8 +4,10 @@
|
|||
---@field expected string
|
||||
---@field status "pending"|"pass"|"fail"|"running"|"timeout"
|
||||
---@field actual string?
|
||||
---@field actual_highlights table[]?
|
||||
---@field time_ms number?
|
||||
---@field error string?
|
||||
---@field stderr string?
|
||||
---@field selected boolean
|
||||
---@field code number?
|
||||
---@field ok boolean?
|
||||
|
|
@ -186,13 +188,27 @@ local function run_single_test_case(ctx, contest_config, cp_config, test_case)
|
|||
if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then
|
||||
logger.log('binary not found, compiling first...')
|
||||
local compile_cmd = substitute_template(language_config.compile, substitutions)
|
||||
local compile_result = vim.system(compile_cmd, { text = true }):wait()
|
||||
local redirected_cmd = vim.deepcopy(compile_cmd)
|
||||
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
|
||||
local compile_result = vim
|
||||
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, { text = false })
|
||||
:wait()
|
||||
|
||||
local ansi = require('cp.ansi')
|
||||
compile_result.stdout = ansi.bytes_to_string(compile_result.stdout or '')
|
||||
compile_result.stderr = ansi.bytes_to_string(compile_result.stderr or '')
|
||||
|
||||
if compile_result.code ~= 0 then
|
||||
return {
|
||||
status = 'fail',
|
||||
actual = '',
|
||||
error = 'Compilation failed: ' .. (compile_result.stderr or 'Unknown error'),
|
||||
error = 'Compilation failed: ' .. (compile_result.stdout or 'Unknown error'),
|
||||
stderr = compile_result.stdout or '',
|
||||
time_ms = 0,
|
||||
code = compile_result.code,
|
||||
ok = false,
|
||||
signal = nil,
|
||||
timed_out = false,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -207,16 +223,28 @@ local function run_single_test_case(ctx, contest_config, cp_config, test_case)
|
|||
if not run_panel_state.constraints then
|
||||
logger.log('no problem constraints available, using default 2000ms timeout')
|
||||
end
|
||||
local redirected_run_cmd = vim.deepcopy(run_cmd)
|
||||
redirected_run_cmd[#redirected_run_cmd] = redirected_run_cmd[#redirected_run_cmd] .. ' 2>&1'
|
||||
local result = vim
|
||||
.system(run_cmd, {
|
||||
.system({ 'sh', '-c', table.concat(redirected_run_cmd, ' ') }, {
|
||||
stdin = stdin_content,
|
||||
timeout = timeout_ms,
|
||||
text = true,
|
||||
text = false,
|
||||
})
|
||||
:wait()
|
||||
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
|
||||
|
||||
local actual_output = (result.stdout or ''):gsub('\n$', '')
|
||||
local ansi = require('cp.ansi')
|
||||
local stdout_str = ansi.bytes_to_string(result.stdout or '')
|
||||
local actual_output = stdout_str:gsub('\n$', '')
|
||||
|
||||
local actual_highlights = {}
|
||||
|
||||
if actual_output ~= '' then
|
||||
local parsed = ansi.parse_ansi_text(actual_output)
|
||||
actual_output = table.concat(parsed.lines, '\n')
|
||||
actual_highlights = parsed.highlights
|
||||
end
|
||||
|
||||
local max_lines = cp_config.run_panel.max_output_lines
|
||||
local output_lines = vim.split(actual_output, '\n')
|
||||
|
|
@ -250,7 +278,9 @@ local function run_single_test_case(ctx, contest_config, cp_config, test_case)
|
|||
return {
|
||||
status = status,
|
||||
actual = actual_output,
|
||||
error = result.code ~= 0 and result.stderr or nil,
|
||||
actual_highlights = actual_highlights,
|
||||
error = result.code ~= 0 and actual_output or nil,
|
||||
stderr = '',
|
||||
time_ms = execution_time,
|
||||
code = result.code,
|
||||
ok = ok,
|
||||
|
|
@ -295,14 +325,15 @@ function M.run_test_case(ctx, contest_config, cp_config, index)
|
|||
return false
|
||||
end
|
||||
|
||||
logger.log(('running test case %d'):format(index))
|
||||
test_case.status = 'running'
|
||||
|
||||
local result = run_single_test_case(ctx, contest_config, cp_config, test_case)
|
||||
|
||||
test_case.status = result.status
|
||||
test_case.actual = result.actual
|
||||
test_case.actual_highlights = result.actual_highlights
|
||||
test_case.error = result.error
|
||||
test_case.stderr = result.stderr
|
||||
test_case.time_ms = result.time_ms
|
||||
test_case.code = result.code
|
||||
test_case.ok = result.ok
|
||||
|
|
@ -329,4 +360,26 @@ function M.get_run_panel_state()
|
|||
return run_panel_state
|
||||
end
|
||||
|
||||
function M.handle_compilation_failure(compilation_output)
|
||||
local ansi = require('cp.ansi')
|
||||
|
||||
-- Always parse the compilation output - it contains everything now
|
||||
local parsed = ansi.parse_ansi_text(compilation_output or '')
|
||||
local clean_text = table.concat(parsed.lines, '\n')
|
||||
local highlights = parsed.highlights
|
||||
|
||||
for _, test_case in ipairs(run_panel_state.test_cases) do
|
||||
test_case.status = 'fail'
|
||||
test_case.actual = clean_text
|
||||
test_case.actual_highlights = highlights
|
||||
test_case.error = 'Compilation failed'
|
||||
test_case.stderr = ''
|
||||
test_case.time_ms = 0
|
||||
test_case.code = 1
|
||||
test_case.ok = false
|
||||
test_case.signal = nil
|
||||
test_case.timed_out = false
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
215
spec/ansi_spec.lua
Normal file
215
spec/ansi_spec.lua
Normal file
|
|
@ -0,0 +1,215 @@
|
|||
describe('ansi parser', function()
|
||||
local ansi = require('cp.ansi')
|
||||
|
||||
describe('bytes_to_string', function()
|
||||
it('returns string as-is', function()
|
||||
local input = 'hello world'
|
||||
assert.equals('hello world', ansi.bytes_to_string(input))
|
||||
end)
|
||||
|
||||
it('converts byte array to string', function()
|
||||
local input = { 104, 101, 108, 108, 111 }
|
||||
assert.equals('hello', ansi.bytes_to_string(input))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('parse_ansi_text', function()
|
||||
it('strips ansi codes from simple text', function()
|
||||
local input = 'Hello \027[31mworld\027[0m!'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello world!', table.concat(result.lines, '\n'))
|
||||
end)
|
||||
|
||||
it('handles text without ansi codes', function()
|
||||
local input = 'Plain text'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Plain text', table.concat(result.lines, '\n'))
|
||||
assert.equals(0, #result.highlights)
|
||||
end)
|
||||
|
||||
it('creates correct highlight for simple colored text', function()
|
||||
local input = 'Hello \027[31mworld\027[0m!'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals(0, highlight.line)
|
||||
assert.equals(6, highlight.col_start)
|
||||
assert.equals(11, highlight.col_end)
|
||||
assert.equals('CpAnsiRed', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles bold text', function()
|
||||
local input = 'Hello \027[1mbold\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBold', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles italic text', function()
|
||||
local input = 'Hello \027[3mitalic\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello italic world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiItalic', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles bold + color combination', function()
|
||||
local input = 'Hello \027[1;31mbold red\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold red world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldRed', highlight.highlight_group)
|
||||
assert.equals(6, highlight.col_start)
|
||||
assert.equals(14, highlight.col_end)
|
||||
end)
|
||||
|
||||
it('handles italic + color combination', function()
|
||||
local input = 'Hello \027[3;32mitalic green\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello italic green world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiItalicGreen', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles bold + italic + color combination', function()
|
||||
local input = 'Hello \027[1;3;33mbold italic yellow\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold italic yellow world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldItalicYellow', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles sequential attribute setting', function()
|
||||
local input = 'Hello \027[1m\027[31mbold red\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold red world', table.concat(result.lines, '\n'))
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldRed', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles selective attribute reset', function()
|
||||
local input = 'Hello \027[1;31mbold red\027[22mno longer bold\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello bold redno longer bold world', table.concat(result.lines, '\n'))
|
||||
assert.equals(2, #result.highlights)
|
||||
|
||||
local bold_red = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldRed', bold_red.highlight_group)
|
||||
assert.equals(6, bold_red.col_start)
|
||||
assert.equals(14, bold_red.col_end)
|
||||
|
||||
local just_red = result.highlights[2]
|
||||
assert.equals('CpAnsiRed', just_red.highlight_group)
|
||||
assert.equals(14, just_red.col_start)
|
||||
assert.equals(28, just_red.col_end)
|
||||
end)
|
||||
|
||||
it('handles bright colors', function()
|
||||
local input = 'Hello \027[91mbright red\027[0m world'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBrightRed', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles compiler-like output with complex formatting', function()
|
||||
local input =
|
||||
"error.cpp:10:5: \027[1m\027[31merror:\027[0m\027[1m 'undefined' was not declared\027[0m"
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
local clean_text = table.concat(result.lines, '\n')
|
||||
assert.equals("error.cpp:10:5: error: 'undefined' was not declared", clean_text)
|
||||
assert.equals(2, #result.highlights)
|
||||
|
||||
local error_highlight = result.highlights[1]
|
||||
assert.equals('CpAnsiBoldRed', error_highlight.highlight_group)
|
||||
assert.equals(16, error_highlight.col_start)
|
||||
assert.equals(22, error_highlight.col_end)
|
||||
|
||||
local message_highlight = result.highlights[2]
|
||||
assert.equals('CpAnsiBold', message_highlight.highlight_group)
|
||||
assert.equals(22, message_highlight.col_start)
|
||||
assert.equals(51, message_highlight.col_end)
|
||||
end)
|
||||
|
||||
it('handles multiline with persistent state', function()
|
||||
local input = '\027[1;31mline1\nline2\nline3\027[0m'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('line1\nline2\nline3', table.concat(result.lines, '\n'))
|
||||
assert.equals(3, #result.highlights)
|
||||
|
||||
for i, highlight in ipairs(result.highlights) do
|
||||
assert.equals('CpAnsiBoldRed', highlight.highlight_group)
|
||||
assert.equals(i - 1, highlight.line)
|
||||
assert.equals(0, highlight.col_start)
|
||||
assert.equals(5, highlight.col_end)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('update_ansi_state', function()
|
||||
it('resets all state on reset code', function()
|
||||
local state = { bold = true, italic = true, foreground = 'Red' }
|
||||
ansi.update_ansi_state(state, '0')
|
||||
|
||||
assert.is_false(state.bold)
|
||||
assert.is_false(state.italic)
|
||||
assert.is_nil(state.foreground)
|
||||
end)
|
||||
|
||||
it('sets individual attributes', function()
|
||||
local state = { bold = false, italic = false, foreground = nil }
|
||||
|
||||
ansi.update_ansi_state(state, '1')
|
||||
assert.is_true(state.bold)
|
||||
|
||||
ansi.update_ansi_state(state, '3')
|
||||
assert.is_true(state.italic)
|
||||
|
||||
ansi.update_ansi_state(state, '31')
|
||||
assert.equals('Red', state.foreground)
|
||||
end)
|
||||
|
||||
it('handles compound codes', function()
|
||||
local state = { bold = false, italic = false, foreground = nil }
|
||||
ansi.update_ansi_state(state, '1;3;31')
|
||||
|
||||
assert.is_true(state.bold)
|
||||
assert.is_true(state.italic)
|
||||
assert.equals('Red', state.foreground)
|
||||
end)
|
||||
|
||||
it('handles selective resets', function()
|
||||
local state = { bold = true, italic = true, foreground = 'Red' }
|
||||
|
||||
ansi.update_ansi_state(state, '22')
|
||||
assert.is_false(state.bold)
|
||||
assert.is_true(state.italic)
|
||||
assert.equals('Red', state.foreground)
|
||||
|
||||
ansi.update_ansi_state(state, '39')
|
||||
assert.is_false(state.bold)
|
||||
assert.is_true(state.italic)
|
||||
assert.is_nil(state.foreground)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -82,10 +82,10 @@ describe('cp.execute', function()
|
|||
assert.is_true(#mock_system_calls > 0)
|
||||
|
||||
local compile_call = mock_system_calls[1]
|
||||
assert.equals('g++', compile_call.cmd[1])
|
||||
assert.equals('test.cpp', compile_call.cmd[2])
|
||||
assert.equals('-o', compile_call.cmd[3])
|
||||
assert.equals('test.run', compile_call.cmd[4])
|
||||
assert.equals('sh', compile_call.cmd[1])
|
||||
assert.equals('-c', compile_call.cmd[2])
|
||||
assert.is_not_nil(string.find(compile_call.cmd[3], 'g%+%+ test%.cpp %-o test%.run'))
|
||||
assert.is_not_nil(string.find(compile_call.cmd[3], '2>&1'))
|
||||
end)
|
||||
|
||||
it('handles multiple substitutions in single argument', function()
|
||||
|
|
@ -100,7 +100,7 @@ describe('cp.execute', function()
|
|||
execute.compile_generic(language_config, substitutions)
|
||||
|
||||
local compile_call = mock_system_calls[1]
|
||||
assert.equals('-omain.out', compile_call.cmd[3])
|
||||
assert.is_not_nil(string.find(compile_call.cmd[3], '%-omain%.out'))
|
||||
end)
|
||||
end)
|
||||
|
||||
|
|
@ -131,8 +131,8 @@ describe('cp.execute', function()
|
|||
assert.is_true(#mock_system_calls > 0)
|
||||
|
||||
local compile_call = mock_system_calls[1]
|
||||
assert.equals('g++', compile_call.cmd[1])
|
||||
assert.is_true(vim.tbl_contains(compile_call.cmd, '-std=c++17'))
|
||||
assert.equals('sh', compile_call.cmd[1])
|
||||
assert.is_not_nil(string.find(compile_call.cmd[3], '%-std=c%+%+17'))
|
||||
end)
|
||||
|
||||
it('handles compilation errors gracefully', function()
|
||||
|
|
@ -199,7 +199,7 @@ describe('cp.execute', function()
|
|||
it('handles command execution', function()
|
||||
vim.system = function(_, opts)
|
||||
if opts then
|
||||
assert.equals(true, opts.text)
|
||||
assert.equals(false, opts.text)
|
||||
end
|
||||
return {
|
||||
wait = function()
|
||||
|
|
@ -266,9 +266,10 @@ describe('cp.execute', function()
|
|||
execute.compile_generic(language_config, {})
|
||||
|
||||
local mkdir_call = mock_system_calls[1]
|
||||
assert.equals('mkdir', mkdir_call.cmd[1])
|
||||
assert.is_true(vim.tbl_contains(mkdir_call.cmd, 'build'))
|
||||
assert.is_true(vim.tbl_contains(mkdir_call.cmd, 'io'))
|
||||
assert.equals('sh', mkdir_call.cmd[1])
|
||||
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'mkdir'))
|
||||
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'build'))
|
||||
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'io'))
|
||||
end)
|
||||
end)
|
||||
|
||||
|
|
@ -316,8 +317,8 @@ describe('cp.execute', function()
|
|||
assert.equals(0, result.code)
|
||||
|
||||
local echo_call = mock_system_calls[1]
|
||||
assert.equals('echo', echo_call.cmd[1])
|
||||
assert.equals('hello', echo_call.cmd[2])
|
||||
assert.equals('sh', echo_call.cmd[1])
|
||||
assert.is_not_nil(string.find(echo_call.cmd[3], 'echo hello'))
|
||||
end)
|
||||
|
||||
it('handles multiple consecutive substitutions', function()
|
||||
|
|
@ -332,8 +333,119 @@ describe('cp.execute', function()
|
|||
execute.compile_generic(language_config, substitutions)
|
||||
|
||||
local call = mock_system_calls[1]
|
||||
assert.equals('g++g++', call.cmd[1])
|
||||
assert.equals('test.cpptest.cpp', call.cmd[2])
|
||||
assert.equals('sh', call.cmd[1])
|
||||
assert.is_not_nil(string.find(call.cmd[3], 'g%+%+g%+%+ test%.cpptest%.cpp'))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('stderr/stdout redirection', function()
|
||||
it('should use stderr redirection (2>&1)', function()
|
||||
local original_system = vim.system
|
||||
local captured_command = nil
|
||||
|
||||
vim.system = function(cmd, _)
|
||||
captured_command = cmd
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0, stdout = '', stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
compile = { 'g++', '-std=c++17', '-o', '{binary}', '{source}' },
|
||||
}
|
||||
local substitutions = { source = 'test.cpp', binary = 'build/test', version = '17' }
|
||||
execute.compile_generic(language_config, substitutions)
|
||||
|
||||
assert.is_not_nil(captured_command)
|
||||
assert.equals('sh', captured_command[1])
|
||||
assert.equals('-c', captured_command[2])
|
||||
assert.is_not_nil(
|
||||
string.find(captured_command[3], '2>&1'),
|
||||
'Command should contain 2>&1 redirection'
|
||||
)
|
||||
|
||||
vim.system = original_system
|
||||
end)
|
||||
|
||||
it('should return combined stdout+stderr in result', function()
|
||||
local original_system = vim.system
|
||||
local test_output = 'STDOUT: Hello\nSTDERR: Error message\n'
|
||||
|
||||
vim.system = function(_, _)
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 1, stdout = test_output, stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local language_config = {
|
||||
compile = { 'g++', '-std=c++17', '-o', '{binary}', '{source}' },
|
||||
}
|
||||
local substitutions = { source = 'test.cpp', binary = 'build/test', version = '17' }
|
||||
local result = execute.compile_generic(language_config, substitutions)
|
||||
|
||||
assert.equals(1, result.code)
|
||||
assert.equals(test_output, result.stdout)
|
||||
|
||||
vim.system = original_system
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('integration with execute_command function', function()
|
||||
it('tests the full execute_command flow with stderr/stdout combination', function()
|
||||
local cmd = { 'echo', 'test output' }
|
||||
local input_data = 'test input'
|
||||
local timeout_ms = 1000
|
||||
|
||||
local original_system = vim.system
|
||||
vim.system = function(shell_cmd, opts)
|
||||
assert.equals('sh', shell_cmd[1])
|
||||
assert.equals('-c', shell_cmd[2])
|
||||
assert.is_not_nil(string.find(shell_cmd[3], '2>&1'))
|
||||
assert.equals(input_data, opts.stdin)
|
||||
assert.equals(timeout_ms, opts.timeout)
|
||||
assert.is_true(opts.text)
|
||||
|
||||
return {
|
||||
wait = function()
|
||||
return { code = 0, stdout = 'combined output from stdout and stderr', stderr = '' }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local execute_command = require('cp.execute').execute_command
|
||||
or function(command, stdin_data, timeout)
|
||||
local redirected_cmd = vim.deepcopy(command)
|
||||
if #redirected_cmd > 0 then
|
||||
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
|
||||
end
|
||||
|
||||
local result = vim
|
||||
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, {
|
||||
stdin = stdin_data,
|
||||
timeout = timeout,
|
||||
text = true,
|
||||
})
|
||||
:wait()
|
||||
|
||||
return {
|
||||
stdout = result.stdout or '',
|
||||
stderr = result.stderr or '',
|
||||
code = result.code or 0,
|
||||
time_ms = 0,
|
||||
timed_out = result.code == 124,
|
||||
}
|
||||
end
|
||||
|
||||
local result = execute_command(cmd, input_data, timeout_ms)
|
||||
|
||||
assert.equals(0, result.code)
|
||||
assert.equals('combined output from stdout and stderr', result.stdout)
|
||||
|
||||
vim.system = original_system
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
describe('cp.test_render', function()
|
||||
describe('cp.run_render', function()
|
||||
local run_render = require('cp.run_render')
|
||||
local spec_helper = require('spec.spec_helper')
|
||||
local test_render
|
||||
|
||||
before_each(function()
|
||||
spec_helper.setup()
|
||||
test_render = require('cp.test_render')
|
||||
end)
|
||||
|
||||
after_each(function()
|
||||
|
|
@ -14,49 +13,49 @@ describe('cp.test_render', function()
|
|||
describe('get_status_info', function()
|
||||
it('returns AC for pass status', function()
|
||||
local test_case = { status = 'pass' }
|
||||
local result = test_render.get_status_info(test_case)
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('AC', result.text)
|
||||
assert.equals('CpTestAC', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns WA for fail status with normal exit codes', function()
|
||||
local test_case = { status = 'fail', code = 1 }
|
||||
local result = test_render.get_status_info(test_case)
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('WA', result.text)
|
||||
assert.equals('CpTestWA', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns TLE for timeout status', function()
|
||||
local test_case = { status = 'timeout' }
|
||||
local result = test_render.get_status_info(test_case)
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('TLE', result.text)
|
||||
assert.equals('CpTestTLE', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns TLE for timed out fail status', function()
|
||||
local test_case = { status = 'fail', timed_out = true }
|
||||
local result = test_render.get_status_info(test_case)
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('TLE', result.text)
|
||||
assert.equals('CpTestTLE', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns RTE for fail with signal codes (>= 128)', function()
|
||||
local test_case = { status = 'fail', code = 139 }
|
||||
local result = test_render.get_status_info(test_case)
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('RTE', result.text)
|
||||
assert.equals('CpTestRTE', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns empty for pending status', function()
|
||||
local test_case = { status = 'pending' }
|
||||
local result = test_render.get_status_info(test_case)
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('', result.text)
|
||||
assert.equals('CpTestPending', result.highlight_group)
|
||||
end)
|
||||
|
||||
it('returns running indicator for running status', function()
|
||||
local test_case = { status = 'running' }
|
||||
local result = test_render.get_status_info(test_case)
|
||||
local result = run_render.get_status_info(test_case)
|
||||
assert.equals('...', result.text)
|
||||
assert.equals('CpTestPending', result.highlight_group)
|
||||
end)
|
||||
|
|
@ -71,7 +70,7 @@ describe('cp.test_render', function()
|
|||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = test_render.render_test_list(test_state)
|
||||
local result = run_render.render_test_list(test_state)
|
||||
assert.is_true(result[1]:find('^┌') ~= nil)
|
||||
assert.is_true(result[2]:find('│.*#.*│.*Status.*│.*Time.*│.*Exit Code.*│') ~= nil)
|
||||
assert.is_true(result[3]:find('^├') ~= nil)
|
||||
|
|
@ -85,7 +84,7 @@ describe('cp.test_render', function()
|
|||
},
|
||||
current_index = 2,
|
||||
}
|
||||
local result = test_render.render_test_list(test_state)
|
||||
local result = run_render.render_test_list(test_state)
|
||||
local found_current = false
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('│.*> 2.*│') then
|
||||
|
|
@ -104,7 +103,7 @@ describe('cp.test_render', function()
|
|||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = test_render.render_test_list(test_state)
|
||||
local result = run_render.render_test_list(test_state)
|
||||
local found_input = false
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('│5 3') then
|
||||
|
|
@ -117,7 +116,7 @@ describe('cp.test_render', function()
|
|||
|
||||
it('handles empty test cases', function()
|
||||
local test_state = { test_cases = {}, current_index = 1 }
|
||||
local result = test_render.render_test_list(test_state)
|
||||
local result = run_render.render_test_list(test_state)
|
||||
assert.equals(3, #result)
|
||||
end)
|
||||
|
||||
|
|
@ -128,7 +127,7 @@ describe('cp.test_render', function()
|
|||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = test_render.render_test_list(test_state)
|
||||
local result = run_render.render_test_list(test_state)
|
||||
local input_lines = {}
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('^│[531]') then
|
||||
|
|
@ -142,24 +141,24 @@ describe('cp.test_render', function()
|
|||
describe('render_status_bar', function()
|
||||
it('formats time and exit code', function()
|
||||
local test_case = { time_ms = 45.7, code = 0 }
|
||||
local result = test_render.render_status_bar(test_case)
|
||||
local result = run_render.render_status_bar(test_case)
|
||||
assert.equals('45.70ms │ Exit: 0', result)
|
||||
end)
|
||||
|
||||
it('handles missing time', function()
|
||||
local test_case = { code = 0 }
|
||||
local result = test_render.render_status_bar(test_case)
|
||||
local result = run_render.render_status_bar(test_case)
|
||||
assert.equals('Exit: 0', result)
|
||||
end)
|
||||
|
||||
it('handles missing exit code', function()
|
||||
local test_case = { time_ms = 123 }
|
||||
local result = test_render.render_status_bar(test_case)
|
||||
local result = run_render.render_status_bar(test_case)
|
||||
assert.equals('123.00ms', result)
|
||||
end)
|
||||
|
||||
it('returns empty for nil test case', function()
|
||||
local result = test_render.render_status_bar(nil)
|
||||
local result = run_render.render_status_bar(nil)
|
||||
assert.equals('', result)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -167,7 +166,7 @@ describe('cp.test_render', function()
|
|||
describe('setup_highlights', function()
|
||||
it('sets up all highlight groups', function()
|
||||
local mock_set_hl = spy.on(vim.api, 'nvim_set_hl')
|
||||
test_render.setup_highlights()
|
||||
run_render.setup_highlights()
|
||||
|
||||
assert.spy(mock_set_hl).was_called(7)
|
||||
assert.spy(mock_set_hl).was_called_with(0, 'CpTestAC', { fg = '#10b981' })
|
||||
|
|
@ -188,7 +187,7 @@ describe('cp.test_render', function()
|
|||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local lines, highlights = test_render.render_test_list(test_state)
|
||||
local lines, highlights = run_render.render_test_list(test_state)
|
||||
|
||||
assert.equals(2, #highlights)
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue