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:
|
outputs:
|
||||||
lua: ${{ steps.changes.outputs.lua }}
|
lua: ${{ steps.changes.outputs.lua }}
|
||||||
python: ${{ steps.changes.outputs.python }}
|
python: ${{ steps.changes.outputs.python }}
|
||||||
|
markdown: ${{ steps.changes.outputs.markdown }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dorny/paths-filter@v3
|
- uses: dorny/paths-filter@v3
|
||||||
|
|
@ -33,6 +34,9 @@ jobs:
|
||||||
- 'tests/scrapers/**'
|
- 'tests/scrapers/**'
|
||||||
- 'pyproject.toml'
|
- 'pyproject.toml'
|
||||||
- 'uv.lock'
|
- 'uv.lock'
|
||||||
|
markdown:
|
||||||
|
- '*.md'
|
||||||
|
- 'docs/**/*.md'
|
||||||
|
|
||||||
lua-format:
|
lua-format:
|
||||||
name: Lua Format Check
|
name: Lua Format Check
|
||||||
|
|
@ -115,3 +119,23 @@ jobs:
|
||||||
run: uv sync --dev
|
run: uv sync --dev
|
||||||
- name: Type check Python files with mypy
|
- name: Type check Python files with mypy
|
||||||
run: uv run mypy scrapers/ tests/scrapers/
|
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:
|
hooks:
|
||||||
- id: stylua-github
|
- id: stylua-github
|
||||||
name: stylua (Lua formatter)
|
name: stylua (Lua formatter)
|
||||||
args: ["--check", "."]
|
args: ["."]
|
||||||
files: ^(lua/|spec/|plugin/|after/|ftdetect/|.*\.lua$)
|
files: ^(lua/|spec/|plugin/|after/|ftdetect/|.*\.lua$)
|
||||||
additional_dependencies: []
|
additional_dependencies: []
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
|
|
@ -16,7 +16,7 @@ repos:
|
||||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||||
- id: ruff
|
- id: ruff
|
||||||
name: ruff (lint)
|
name: ruff (lint)
|
||||||
args: ["--no-fix"]
|
args: ["--fix", "--select=I"]
|
||||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||||
- repo: local
|
- repo: local
|
||||||
hooks:
|
hooks:
|
||||||
|
|
@ -27,3 +27,9 @@ repos:
|
||||||
args: ["scrapers/", "tests/scrapers/"]
|
args: ["scrapers/", "tests/scrapers/"]
|
||||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||||
pass_filenames: false
|
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
|
# 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
|
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
|
## Features
|
||||||
|
|
||||||
- Support for multiple online judges ([AtCoder](https://atcoder.jp/), [Codeforces](https://codeforces.com/), [CSES](https://cses.fi))
|
- **Multi-platform support**: AtCoder, Codeforces, CSES with consistent interface
|
||||||
- Language-agnostic features
|
- **Automatic problem setup**: Scrape test cases and metadata in seconds
|
||||||
- Automatic problem scraping and test case management
|
- **Rich test output**: ANSI color support for compiler errors and program output
|
||||||
- Integrated running and debugging
|
- **Language agnostic**: Works with any compiled language
|
||||||
- Enhanced test viewer
|
- **Template integration**: Contest-specific snippets via LuaSnip
|
||||||
- Templates via LuaSnip
|
- **Diff viewer**: Compare expected vs actual output with precision
|
||||||
|
|
||||||
## Requirements
|
## Optional Dependencies
|
||||||
|
|
||||||
- Neovim 0.10.0+
|
- [uv](https://docs.astral.sh/uv/) for problem scraping
|
||||||
- [uv](https://docs.astral.sh/uv/): problem scraping (optional)
|
- [LuaSnip](https://github.com/L3MON4D3/LuaSnip) for templates
|
||||||
- [LuaSnip](https://github.com/L3MON4D3/LuaSnip): contest-specific snippets (optional)
|
|
||||||
|
## 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
|
## Documentation
|
||||||
|
|
||||||
|
|
@ -29,37 +56,13 @@ https://github.com/user-attachments/assets/cb142535-fba0-4280-8f11-66ad1ca50ca9
|
||||||
:help cp.nvim
|
:help cp.nvim
|
||||||
```
|
```
|
||||||
|
|
||||||
## Philosophy
|
See [my config](https://github.com/barrett-ruth/dots/blob/main/nvim/lua/plugins/cp.lua) for a relatively advanced setup.
|
||||||
|
|
||||||
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!)
|
|
||||||
|
|
||||||
|
|
||||||
## Similar Projects
|
## Similar Projects
|
||||||
|
|
||||||
- [competitest.nvim](https://github.com/xeluxee/competitest.nvim)
|
- [competitest.nvim](https://github.com/xeluxee/competitest.nvim)
|
||||||
- [assistant.nvim](https://github.com/A7Lavinraj/assistant.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*
|
REQUIREMENTS *cp-requirements*
|
||||||
|
|
||||||
- Neovim 0.10.0+
|
- Neovim 0.10.0+
|
||||||
- uv package manager (https://docs.astral.sh/uv/)
|
- Unix-like operating system
|
||||||
- Language runtime/compiler (g++, python3)
|
|
||||||
|
|
||||||
Optional:
|
Optional:
|
||||||
|
- uv package manager (https://docs.astral.sh/uv/)
|
||||||
- LuaSnip for template expansion (https://github.com/L3MON4D3/LuaSnip)
|
- 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
|
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
|
:e ~/contests/abc323/a.cpp
|
||||||
:CP
|
:CP
|
||||||
< Automatically restores abc323 contest context
|
< Automatically restores abc323 contest context
|
||||||
|
|
@ -421,9 +421,9 @@ The run panel uses the following table layout: >
|
||||||
└─────┴────────┴──────────────┴───────────┴──────────┴─────────────┘
|
└─────┴────────┴──────────────┴───────────┴──────────┴─────────────┘
|
||||||
┌──────────────────────────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
│Expected vs Actual │
|
│Expected vs Actual │
|
||||||
│4[-2-]{+3+} │
|
│423 │
|
||||||
│100 │
|
│100 │
|
||||||
│hello w[-o-]r{+o+}ld │
|
│hello world │
|
||||||
└──────────────────────────────────────────────────────────────────┘
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
|
||||||
Status Indicators ~
|
Status Indicators ~
|
||||||
|
|
@ -434,10 +434,20 @@ Test cases use competitive programming terminology with color highlighting:
|
||||||
WA Wrong Answer (output mismatch) - Red
|
WA Wrong Answer (output mismatch) - Red
|
||||||
TLE Time Limit Exceeded (timeout) - Orange
|
TLE Time Limit Exceeded (timeout) - Orange
|
||||||
RTE Runtime Error (non-zero exit) - Purple
|
RTE Runtime Error (non-zero exit) - Purple
|
||||||
|
<
|
||||||
|
|
||||||
Highlight Groups ~
|
==============================================================================
|
||||||
*cp-highlights*
|
ANSI COLORS AND HIGHLIGHTING *cp-ansi*
|
||||||
cp.nvim defines the following highlight groups for status indicators:
|
|
||||||
|
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
|
CpTestAC Green foreground for AC status
|
||||||
CpTestWA Red foreground for WA 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
|
CpTestRTE Purple foreground for RTE status
|
||||||
CpTestPending Gray foreground for pending tests
|
CpTestPending Gray foreground for pending tests
|
||||||
|
|
||||||
You can customize these colors by linking to other highlight groups in your
|
ANSI Color Groups ~
|
||||||
colorscheme or by redefining them: >lua
|
|
||||||
vim.api.nvim_set_hl(0, 'CpTestAC', { link = 'DiffAdd' })
|
cp.nvim preserves ANSI colors from compiler output and program stderr using
|
||||||
vim.api.nvim_set_hl(0, 'CpTestWA', { fg = '#ff0000' })
|
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
|
<c-n> Navigate to next test case (configurable via
|
||||||
run_panel.next_test_key)
|
run_panel.next_test_key)
|
||||||
<c-p> Navigate to previous test case (configurable via
|
<c-p> Navigate to previous test case (configurable via
|
||||||
|
|
@ -507,10 +596,6 @@ Cache Location ~
|
||||||
Cache is stored at: >
|
Cache is stored at: >
|
||||||
vim.fn.stdpath('data') .. '/cp-nvim.json'
|
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 ~
|
Cache Structure ~
|
||||||
*cp-cache-structure*
|
*cp-cache-structure*
|
||||||
The cache contains four main sections:
|
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 = {},
|
highlights = {},
|
||||||
}
|
}
|
||||||
else
|
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 {
|
return {
|
||||||
content = {},
|
content = lines,
|
||||||
highlights = {},
|
highlights = highlights,
|
||||||
raw_diff = result.stdout or '',
|
raw_diff = diff_content,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
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 extension = vim.fn.fnamemodify(source_file, ':e')
|
||||||
local language = filetype_to_language[extension] or contest_config.default_language
|
local language = filetype_to_language[extension] or contest_config.default_language
|
||||||
logger.log(('detected language: %s (extension: %s)'):format(language, extension))
|
|
||||||
return language
|
return language
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -70,7 +69,7 @@ end
|
||||||
|
|
||||||
---@param language_config table
|
---@param language_config table
|
||||||
---@param substitutions table<string, string>
|
---@param substitutions table<string, string>
|
||||||
---@return {code: integer, stderr: string}
|
---@return {code: integer, stdout: string, stderr: string}
|
||||||
function M.compile_generic(language_config, substitutions)
|
function M.compile_generic(language_config, substitutions)
|
||||||
vim.validate({
|
vim.validate({
|
||||||
language_config = { language_config, 'table' },
|
language_config = { language_config, 'table' },
|
||||||
|
|
@ -83,19 +82,25 @@ function M.compile_generic(language_config, substitutions)
|
||||||
end
|
end
|
||||||
|
|
||||||
local compile_cmd = substitute_template(language_config.compile, substitutions)
|
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 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 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
|
if result.code == 0 then
|
||||||
logger.log(('compilation successful (%.1fms)'):format(compile_time))
|
logger.log(('compilation successful (%.1fms)'):format(compile_time))
|
||||||
else
|
else
|
||||||
logger.log(
|
logger.log(('compilation failed (%.1fms)'):format(compile_time))
|
||||||
('compilation failed (%.1fms): %s'):format(compile_time, result.stderr),
|
|
||||||
vim.log.levels.WARN
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
@ -112,12 +117,15 @@ local function execute_command(cmd, input_data, timeout_ms)
|
||||||
timeout_ms = { timeout_ms, 'number' },
|
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 start_time = vim.uv.hrtime()
|
||||||
|
|
||||||
local result = vim
|
local result = vim
|
||||||
.system(cmd, {
|
.system({ 'sh', '-c', table.concat(redirected_cmd, ' ') }, {
|
||||||
stdin = input_data,
|
stdin = input_data,
|
||||||
timeout = timeout_ms,
|
timeout = timeout_ms,
|
||||||
text = true,
|
text = true,
|
||||||
|
|
@ -202,7 +210,7 @@ end
|
||||||
---@param ctx ProblemContext
|
---@param ctx ProblemContext
|
||||||
---@param contest_config ContestConfig
|
---@param contest_config ContestConfig
|
||||||
---@param is_debug? boolean
|
---@param is_debug? boolean
|
||||||
---@return boolean success
|
---@return {success: boolean, output: string?}
|
||||||
function M.compile_problem(ctx, contest_config, is_debug)
|
function M.compile_problem(ctx, contest_config, is_debug)
|
||||||
vim.validate({
|
vim.validate({
|
||||||
ctx = { ctx, 'table' },
|
ctx = { ctx, 'table' },
|
||||||
|
|
@ -214,7 +222,7 @@ function M.compile_problem(ctx, contest_config, is_debug)
|
||||||
|
|
||||||
if not language_config then
|
if not language_config then
|
||||||
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
|
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
|
||||||
return false
|
return { success = false, output = 'No configuration for language: ' .. language }
|
||||||
end
|
end
|
||||||
|
|
||||||
local substitutions = {
|
local substitutions = {
|
||||||
|
|
@ -229,16 +237,12 @@ function M.compile_problem(ctx, contest_config, is_debug)
|
||||||
language_config.compile = compile_cmd
|
language_config.compile = compile_cmd
|
||||||
local compile_result = M.compile_generic(language_config, substitutions)
|
local compile_result = M.compile_generic(language_config, substitutions)
|
||||||
if compile_result.code ~= 0 then
|
if compile_result.code ~= 0 then
|
||||||
logger.log(
|
return { success = false, output = compile_result.stdout or 'unknown error' }
|
||||||
'compilation failed: ' .. (compile_result.stderr or 'unknown error'),
|
|
||||||
vim.log.levels.ERROR
|
|
||||||
)
|
|
||||||
return false
|
|
||||||
end
|
end
|
||||||
logger.log(('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'))
|
logger.log(('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'))
|
||||||
end
|
end
|
||||||
|
|
||||||
return true
|
return { success = true, output = nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.run_problem(ctx, contest_config, is_debug)
|
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)
|
vim.fn.delete(state.saved_session)
|
||||||
state.saved_session = nil
|
state.saved_session = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
state.run_panel_active = false
|
state.run_panel_active = false
|
||||||
logger.log('test panel closed')
|
logger.log('test panel closed')
|
||||||
return
|
return
|
||||||
|
|
@ -198,9 +199,9 @@ local function toggle_run_panel(is_debug)
|
||||||
end
|
end
|
||||||
|
|
||||||
local ctx = problem.create_context(state.platform, state.contest_id, state.problem_id, config)
|
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)
|
logger.log('no test cases found', vim.log.levels.WARN)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -226,8 +227,9 @@ local function toggle_run_panel(is_debug)
|
||||||
local diff_namespace = highlight.create_namespace()
|
local diff_namespace = highlight.create_namespace()
|
||||||
|
|
||||||
local test_list_namespace = vim.api.nvim_create_namespace('cp_test_list')
|
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 })
|
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
|
||||||
|
|
||||||
vim.api.nvim_set_option_value('readonly', false, { 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('modifiable', false, { buf = bufnr })
|
||||||
vim.api.nvim_set_option_value('readonly', was_readonly, { 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)
|
highlight.apply_highlights(bufnr, highlights, namespace or test_list_namespace)
|
||||||
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
|
|
||||||
end
|
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)
|
||||||
|
|
@ -326,8 +321,31 @@ local function toggle_run_panel(is_debug)
|
||||||
}
|
}
|
||||||
end
|
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)
|
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)
|
return create_git_diff_layout(parent_win, expected_content, actual_content)
|
||||||
else
|
else
|
||||||
return create_vim_diff_layout(parent_win, expected_content, actual_content)
|
return create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||||
|
|
@ -335,7 +353,7 @@ local function toggle_run_panel(is_debug)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function update_diff_panes()
|
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]
|
local current_test = test_state.test_cases[test_state.current_index]
|
||||||
|
|
||||||
if not current_test then
|
if not current_test then
|
||||||
|
|
@ -344,14 +362,19 @@ local function toggle_run_panel(is_debug)
|
||||||
|
|
||||||
local expected_content = current_test.expected or ''
|
local expected_content = current_test.expected or ''
|
||||||
local actual_content = current_test.actual or '(not run yet)'
|
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
|
if not should_show_diff then
|
||||||
expected_content = expected_content
|
expected_content = expected_content
|
||||||
actual_content = actual_content
|
actual_content = actual_content
|
||||||
end
|
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
|
if current_diff_layout and current_mode ~= desired_mode then
|
||||||
local saved_pos = vim.api.nvim_win_get_cursor(0)
|
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)
|
setup_keybindings_for_buffer(buf)
|
||||||
end
|
end
|
||||||
else
|
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 diff_backend = require('cp.diff')
|
||||||
local backend = diff_backend.get_best_backend('git')
|
local backend = diff_backend.get_best_backend('git')
|
||||||
local diff_result = backend.render(expected_content, actual_content)
|
local diff_result = backend.render(expected_content, actual_content)
|
||||||
|
|
@ -393,13 +424,23 @@ local function toggle_run_panel(is_debug)
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
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
|
end
|
||||||
else
|
else
|
||||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||||
local actual_lines = vim.split(actual_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[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
|
if should_show_diff then
|
||||||
vim.api.nvim_set_option_value('diff', true, { win = current_diff_layout.windows[1] })
|
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.api.nvim_win_call(current_diff_layout.windows[2], function()
|
||||||
vim.cmd.diffthis()
|
vim.cmd.diffthis()
|
||||||
end)
|
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
|
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[1] })
|
||||||
vim.api.nvim_set_option_value('diff', false, { win = current_diff_layout.windows[2] })
|
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
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local test_render = require('cp.test_render')
|
local run_render = require('cp.run_render')
|
||||||
test_render.setup_highlights()
|
run_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 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_buffer_content(test_buffers.tab_buf, tab_lines, tab_highlights)
|
||||||
|
|
||||||
update_diff_panes()
|
update_diff_panes()
|
||||||
end
|
end
|
||||||
|
|
||||||
local function navigate_test_case(delta)
|
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
|
if #test_state.test_cases == 0 then
|
||||||
return
|
return
|
||||||
end
|
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'
|
config.run_panel.diff_mode = config.run_panel.diff_mode == 'vim' and 'git' or 'vim'
|
||||||
refresh_run_panel()
|
refresh_run_panel()
|
||||||
end, { buffer = buf, silent = true })
|
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
|
end
|
||||||
|
|
||||||
vim.keymap.set('n', config.run_panel.next_test_key, function()
|
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 execute_module = require('cp.execute')
|
||||||
local contest_config = config.contests[state.platform]
|
local contest_config = config.contests[state.platform]
|
||||||
if execute_module.compile_problem(ctx, contest_config, is_debug) then
|
local compile_result = execute_module.compile_problem(ctx, contest_config, is_debug)
|
||||||
test_module.run_all_test_cases(ctx, contest_config, config)
|
if compile_result.success then
|
||||||
|
run.run_all_test_cases(ctx, contest_config, config)
|
||||||
|
else
|
||||||
|
run.handle_compilation_failure(compile_result.output)
|
||||||
end
|
end
|
||||||
|
|
||||||
refresh_run_panel()
|
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)
|
vim.api.nvim_set_current_win(test_windows.tab_win)
|
||||||
|
|
||||||
state.run_panel_active = true
|
state.run_panel_active = true
|
||||||
state.test_buffers = test_buffers
|
state.test_buffers = test_buffers
|
||||||
state.test_windows = test_windows
|
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))
|
logger.log(string.format('test panel opened (%d test cases)', #test_state.test_cases))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -540,8 +601,8 @@ local function navigate_problem(delta, language)
|
||||||
local new_index = current_index + delta
|
local new_index = current_index + delta
|
||||||
|
|
||||||
if new_index < 1 or new_index > #problems then
|
if new_index < 1 or new_index > #problems then
|
||||||
local direction = delta > 0 and 'next' or 'previous'
|
local msg = delta > 0 and 'at last problem' or 'at first problem'
|
||||||
logger.log(('no %s problem available'):format(direction), vim.log.levels.INFO)
|
logger.log(msg, vim.log.levels.WARN)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,10 @@
|
||||||
---@field expected string
|
---@field expected string
|
||||||
---@field status "pending"|"pass"|"fail"|"running"|"timeout"
|
---@field status "pending"|"pass"|"fail"|"running"|"timeout"
|
||||||
---@field actual string?
|
---@field actual string?
|
||||||
|
---@field actual_highlights table[]?
|
||||||
---@field time_ms number?
|
---@field time_ms number?
|
||||||
---@field error string?
|
---@field error string?
|
||||||
|
---@field stderr string?
|
||||||
---@field selected boolean
|
---@field selected boolean
|
||||||
---@field code number?
|
---@field code number?
|
||||||
---@field ok boolean?
|
---@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
|
if language_config.compile and vim.fn.filereadable(ctx.binary_file) == 0 then
|
||||||
logger.log('binary not found, compiling first...')
|
logger.log('binary not found, compiling first...')
|
||||||
local compile_cmd = substitute_template(language_config.compile, substitutions)
|
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
|
if compile_result.code ~= 0 then
|
||||||
return {
|
return {
|
||||||
status = 'fail',
|
status = 'fail',
|
||||||
actual = '',
|
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,
|
time_ms = 0,
|
||||||
|
code = compile_result.code,
|
||||||
|
ok = false,
|
||||||
|
signal = nil,
|
||||||
|
timed_out = false,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
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
|
if not run_panel_state.constraints then
|
||||||
logger.log('no problem constraints available, using default 2000ms timeout')
|
logger.log('no problem constraints available, using default 2000ms timeout')
|
||||||
end
|
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
|
local result = vim
|
||||||
.system(run_cmd, {
|
.system({ 'sh', '-c', table.concat(redirected_run_cmd, ' ') }, {
|
||||||
stdin = stdin_content,
|
stdin = stdin_content,
|
||||||
timeout = timeout_ms,
|
timeout = timeout_ms,
|
||||||
text = true,
|
text = false,
|
||||||
})
|
})
|
||||||
:wait()
|
:wait()
|
||||||
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
|
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 max_lines = cp_config.run_panel.max_output_lines
|
||||||
local output_lines = vim.split(actual_output, '\n')
|
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 {
|
return {
|
||||||
status = status,
|
status = status,
|
||||||
actual = actual_output,
|
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,
|
time_ms = execution_time,
|
||||||
code = result.code,
|
code = result.code,
|
||||||
ok = ok,
|
ok = ok,
|
||||||
|
|
@ -295,14 +325,15 @@ function M.run_test_case(ctx, contest_config, cp_config, index)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
logger.log(('running test case %d'):format(index))
|
|
||||||
test_case.status = 'running'
|
test_case.status = 'running'
|
||||||
|
|
||||||
local result = run_single_test_case(ctx, contest_config, cp_config, test_case)
|
local result = run_single_test_case(ctx, contest_config, cp_config, test_case)
|
||||||
|
|
||||||
test_case.status = result.status
|
test_case.status = result.status
|
||||||
test_case.actual = result.actual
|
test_case.actual = result.actual
|
||||||
|
test_case.actual_highlights = result.actual_highlights
|
||||||
test_case.error = result.error
|
test_case.error = result.error
|
||||||
|
test_case.stderr = result.stderr
|
||||||
test_case.time_ms = result.time_ms
|
test_case.time_ms = result.time_ms
|
||||||
test_case.code = result.code
|
test_case.code = result.code
|
||||||
test_case.ok = result.ok
|
test_case.ok = result.ok
|
||||||
|
|
@ -329,4 +360,26 @@ function M.get_run_panel_state()
|
||||||
return run_panel_state
|
return run_panel_state
|
||||||
end
|
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
|
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)
|
assert.is_true(#mock_system_calls > 0)
|
||||||
|
|
||||||
local compile_call = mock_system_calls[1]
|
local compile_call = mock_system_calls[1]
|
||||||
assert.equals('g++', compile_call.cmd[1])
|
assert.equals('sh', compile_call.cmd[1])
|
||||||
assert.equals('test.cpp', compile_call.cmd[2])
|
assert.equals('-c', compile_call.cmd[2])
|
||||||
assert.equals('-o', compile_call.cmd[3])
|
assert.is_not_nil(string.find(compile_call.cmd[3], 'g%+%+ test%.cpp %-o test%.run'))
|
||||||
assert.equals('test.run', compile_call.cmd[4])
|
assert.is_not_nil(string.find(compile_call.cmd[3], '2>&1'))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles multiple substitutions in single argument', function()
|
it('handles multiple substitutions in single argument', function()
|
||||||
|
|
@ -100,7 +100,7 @@ describe('cp.execute', function()
|
||||||
execute.compile_generic(language_config, substitutions)
|
execute.compile_generic(language_config, substitutions)
|
||||||
|
|
||||||
local compile_call = mock_system_calls[1]
|
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)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -131,8 +131,8 @@ describe('cp.execute', function()
|
||||||
assert.is_true(#mock_system_calls > 0)
|
assert.is_true(#mock_system_calls > 0)
|
||||||
|
|
||||||
local compile_call = mock_system_calls[1]
|
local compile_call = mock_system_calls[1]
|
||||||
assert.equals('g++', compile_call.cmd[1])
|
assert.equals('sh', compile_call.cmd[1])
|
||||||
assert.is_true(vim.tbl_contains(compile_call.cmd, '-std=c++17'))
|
assert.is_not_nil(string.find(compile_call.cmd[3], '%-std=c%+%+17'))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles compilation errors gracefully', function()
|
it('handles compilation errors gracefully', function()
|
||||||
|
|
@ -199,7 +199,7 @@ describe('cp.execute', function()
|
||||||
it('handles command execution', function()
|
it('handles command execution', function()
|
||||||
vim.system = function(_, opts)
|
vim.system = function(_, opts)
|
||||||
if opts then
|
if opts then
|
||||||
assert.equals(true, opts.text)
|
assert.equals(false, opts.text)
|
||||||
end
|
end
|
||||||
return {
|
return {
|
||||||
wait = function()
|
wait = function()
|
||||||
|
|
@ -266,9 +266,10 @@ describe('cp.execute', function()
|
||||||
execute.compile_generic(language_config, {})
|
execute.compile_generic(language_config, {})
|
||||||
|
|
||||||
local mkdir_call = mock_system_calls[1]
|
local mkdir_call = mock_system_calls[1]
|
||||||
assert.equals('mkdir', mkdir_call.cmd[1])
|
assert.equals('sh', mkdir_call.cmd[1])
|
||||||
assert.is_true(vim.tbl_contains(mkdir_call.cmd, 'build'))
|
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'mkdir'))
|
||||||
assert.is_true(vim.tbl_contains(mkdir_call.cmd, 'io'))
|
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'build'))
|
||||||
|
assert.is_not_nil(string.find(mkdir_call.cmd[3], 'io'))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -316,8 +317,8 @@ describe('cp.execute', function()
|
||||||
assert.equals(0, result.code)
|
assert.equals(0, result.code)
|
||||||
|
|
||||||
local echo_call = mock_system_calls[1]
|
local echo_call = mock_system_calls[1]
|
||||||
assert.equals('echo', echo_call.cmd[1])
|
assert.equals('sh', echo_call.cmd[1])
|
||||||
assert.equals('hello', echo_call.cmd[2])
|
assert.is_not_nil(string.find(echo_call.cmd[3], 'echo hello'))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles multiple consecutive substitutions', function()
|
it('handles multiple consecutive substitutions', function()
|
||||||
|
|
@ -332,8 +333,119 @@ describe('cp.execute', function()
|
||||||
execute.compile_generic(language_config, substitutions)
|
execute.compile_generic(language_config, substitutions)
|
||||||
|
|
||||||
local call = mock_system_calls[1]
|
local call = mock_system_calls[1]
|
||||||
assert.equals('g++g++', call.cmd[1])
|
assert.equals('sh', call.cmd[1])
|
||||||
assert.equals('test.cpptest.cpp', call.cmd[2])
|
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)
|
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 spec_helper = require('spec.spec_helper')
|
||||||
local test_render
|
|
||||||
|
|
||||||
before_each(function()
|
before_each(function()
|
||||||
spec_helper.setup()
|
spec_helper.setup()
|
||||||
test_render = require('cp.test_render')
|
|
||||||
end)
|
end)
|
||||||
|
|
||||||
after_each(function()
|
after_each(function()
|
||||||
|
|
@ -14,49 +13,49 @@ describe('cp.test_render', function()
|
||||||
describe('get_status_info', function()
|
describe('get_status_info', function()
|
||||||
it('returns AC for pass status', function()
|
it('returns AC for pass status', function()
|
||||||
local test_case = { status = 'pass' }
|
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('AC', result.text)
|
||||||
assert.equals('CpTestAC', result.highlight_group)
|
assert.equals('CpTestAC', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns WA for fail status with normal exit codes', function()
|
it('returns WA for fail status with normal exit codes', function()
|
||||||
local test_case = { status = 'fail', code = 1 }
|
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('WA', result.text)
|
||||||
assert.equals('CpTestWA', result.highlight_group)
|
assert.equals('CpTestWA', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns TLE for timeout status', function()
|
it('returns TLE for timeout status', function()
|
||||||
local test_case = { status = 'timeout' }
|
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('TLE', result.text)
|
||||||
assert.equals('CpTestTLE', result.highlight_group)
|
assert.equals('CpTestTLE', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns TLE for timed out fail status', function()
|
it('returns TLE for timed out fail status', function()
|
||||||
local test_case = { status = 'fail', timed_out = true }
|
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('TLE', result.text)
|
||||||
assert.equals('CpTestTLE', result.highlight_group)
|
assert.equals('CpTestTLE', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns RTE for fail with signal codes (>= 128)', function()
|
it('returns RTE for fail with signal codes (>= 128)', function()
|
||||||
local test_case = { status = 'fail', code = 139 }
|
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('RTE', result.text)
|
||||||
assert.equals('CpTestRTE', result.highlight_group)
|
assert.equals('CpTestRTE', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns empty for pending status', function()
|
it('returns empty for pending status', function()
|
||||||
local test_case = { status = 'pending' }
|
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('', result.text)
|
||||||
assert.equals('CpTestPending', result.highlight_group)
|
assert.equals('CpTestPending', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns running indicator for running status', function()
|
it('returns running indicator for running status', function()
|
||||||
local test_case = { status = 'running' }
|
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('...', result.text)
|
||||||
assert.equals('CpTestPending', result.highlight_group)
|
assert.equals('CpTestPending', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
@ -71,7 +70,7 @@ describe('cp.test_render', function()
|
||||||
},
|
},
|
||||||
current_index = 1,
|
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[1]:find('^┌') ~= nil)
|
||||||
assert.is_true(result[2]:find('│.*#.*│.*Status.*│.*Time.*│.*Exit Code.*│') ~= nil)
|
assert.is_true(result[2]:find('│.*#.*│.*Status.*│.*Time.*│.*Exit Code.*│') ~= nil)
|
||||||
assert.is_true(result[3]:find('^├') ~= nil)
|
assert.is_true(result[3]:find('^├') ~= nil)
|
||||||
|
|
@ -85,7 +84,7 @@ describe('cp.test_render', function()
|
||||||
},
|
},
|
||||||
current_index = 2,
|
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
|
local found_current = false
|
||||||
for _, line in ipairs(result) do
|
for _, line in ipairs(result) do
|
||||||
if line:match('│.*> 2.*│') then
|
if line:match('│.*> 2.*│') then
|
||||||
|
|
@ -104,7 +103,7 @@ describe('cp.test_render', function()
|
||||||
},
|
},
|
||||||
current_index = 1,
|
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
|
local found_input = false
|
||||||
for _, line in ipairs(result) do
|
for _, line in ipairs(result) do
|
||||||
if line:match('│5 3') then
|
if line:match('│5 3') then
|
||||||
|
|
@ -117,7 +116,7 @@ describe('cp.test_render', function()
|
||||||
|
|
||||||
it('handles empty test cases', function()
|
it('handles empty test cases', function()
|
||||||
local test_state = { test_cases = {}, current_index = 1 }
|
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)
|
assert.equals(3, #result)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
|
@ -128,7 +127,7 @@ describe('cp.test_render', function()
|
||||||
},
|
},
|
||||||
current_index = 1,
|
current_index = 1,
|
||||||
}
|
}
|
||||||
local result = test_render.render_test_list(test_state)
|
local result = run_render.render_test_list(test_state)
|
||||||
local input_lines = {}
|
local input_lines = {}
|
||||||
for _, line in ipairs(result) do
|
for _, line in ipairs(result) do
|
||||||
if line:match('^│[531]') then
|
if line:match('^│[531]') then
|
||||||
|
|
@ -142,24 +141,24 @@ describe('cp.test_render', function()
|
||||||
describe('render_status_bar', function()
|
describe('render_status_bar', function()
|
||||||
it('formats time and exit code', function()
|
it('formats time and exit code', function()
|
||||||
local test_case = { time_ms = 45.7, code = 0 }
|
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)
|
assert.equals('45.70ms │ Exit: 0', result)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles missing time', function()
|
it('handles missing time', function()
|
||||||
local test_case = { code = 0 }
|
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)
|
assert.equals('Exit: 0', result)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles missing exit code', function()
|
it('handles missing exit code', function()
|
||||||
local test_case = { time_ms = 123 }
|
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)
|
assert.equals('123.00ms', result)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns empty for nil test case', function()
|
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)
|
assert.equals('', result)
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
@ -167,7 +166,7 @@ describe('cp.test_render', function()
|
||||||
describe('setup_highlights', function()
|
describe('setup_highlights', function()
|
||||||
it('sets up all highlight groups', function()
|
it('sets up all highlight groups', function()
|
||||||
local mock_set_hl = spy.on(vim.api, 'nvim_set_hl')
|
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(7)
|
||||||
assert.spy(mock_set_hl).was_called_with(0, 'CpTestAC', { fg = '#10b981' })
|
assert.spy(mock_set_hl).was_called_with(0, 'CpTestAC', { fg = '#10b981' })
|
||||||
|
|
@ -188,7 +187,7 @@ describe('cp.test_render', function()
|
||||||
},
|
},
|
||||||
current_index = 1,
|
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)
|
assert.equals(2, #highlights)
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue