Merge pull request #56 from barrett-ruth/fix/doc-keybindings
Test Panel Updates
This commit is contained in:
commit
653a139395
23 changed files with 1824 additions and 246 deletions
4
.github/workflows/quality.yml
vendored
4
.github/workflows/quality.yml
vendored
|
|
@ -44,7 +44,7 @@ jobs:
|
|||
- uses: JohnnyMorganz/stylua-action@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: latest
|
||||
version: 2.1.0
|
||||
args: --check .
|
||||
|
||||
lua-lint:
|
||||
|
|
@ -114,4 +114,4 @@ jobs:
|
|||
- name: Install dependencies with mypy
|
||||
run: uv sync --dev
|
||||
- name: Type check Python files with mypy
|
||||
run: uv run mypy scrapers/ tests/scrapers/
|
||||
run: uv run mypy scrapers/ tests/scrapers/
|
||||
|
|
|
|||
29
.pre-commit-config.yaml
Normal file
29
.pre-commit-config.yaml
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
minimum_pre_commit_version: "3.5.0"
|
||||
repos:
|
||||
- repo: https://github.com/JohnnyMorganz/StyLua
|
||||
rev: v2.1.0
|
||||
hooks:
|
||||
- id: stylua-github
|
||||
name: stylua (Lua formatter)
|
||||
args: ["--check", "."]
|
||||
files: ^(lua/|spec/|plugin/|after/|ftdetect/|.*\.lua$)
|
||||
additional_dependencies: []
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.6.9
|
||||
hooks:
|
||||
- id: ruff-format
|
||||
name: ruff (format)
|
||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
- id: ruff
|
||||
name: ruff (lint)
|
||||
args: ["--no-fix"]
|
||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: mypy
|
||||
name: mypy (type check)
|
||||
entry: uv run mypy
|
||||
language: system
|
||||
args: ["scrapers/", "tests/scrapers/"]
|
||||
files: ^(scrapers/|tests/scrapers/|.*\.py$)
|
||||
pass_filenames: false
|
||||
|
|
@ -63,12 +63,3 @@ follows:
|
|||
|
||||
- [competitest.nvim](https://github.com/xeluxee/competitest.nvim)
|
||||
- [assistant.nvim](https://github.com/A7Lavinraj/assistant.nvim)
|
||||
|
||||
## TODO
|
||||
|
||||
- general `:CP test` window improvements
|
||||
- fzf/telescope integration (whichever available)
|
||||
- finer-tuned problem limits (i.e. per-problem codeforces time, memory)
|
||||
- notify discord members
|
||||
- handle infinite output/trimming file to 500 lines (customizable)
|
||||
- update barrettruth.com to post
|
||||
|
|
|
|||
156
doc/cp.txt
156
doc/cp.txt
|
|
@ -49,9 +49,9 @@ Setup Commands ~
|
|||
|
||||
Action Commands ~
|
||||
|
||||
:CP test [--debug] Toggle test panel for individual test case
|
||||
debugging. Shows per-test results with three-pane
|
||||
layout for easy Expected/Actual comparison.
|
||||
:CP run [--debug] Toggle run panel for individual test case
|
||||
debugging. Shows per-test results with redesigned
|
||||
layout for efficient comparison.
|
||||
Use --debug flag to compile with debug flags
|
||||
Requires contest setup first.
|
||||
|
||||
|
|
@ -67,7 +67,7 @@ CONFIGURATION *cp-config*
|
|||
|
||||
cp.nvim works out of the box. No setup required.
|
||||
|
||||
Optional configuration with lazy.nvim: >
|
||||
Here's an example configuration with lazy.nvim: >
|
||||
{
|
||||
'barrett-ruth/cp.nvim',
|
||||
cmd = 'CP',
|
||||
|
|
@ -75,7 +75,7 @@ Optional configuration with lazy.nvim: >
|
|||
debug = false,
|
||||
scrapers = {
|
||||
atcoder = true,
|
||||
codeforces = false, -- disable codeforces scraping
|
||||
codeforces = false,
|
||||
cses = true,
|
||||
},
|
||||
contests = {
|
||||
|
|
@ -85,7 +85,7 @@ Optional configuration with lazy.nvim: >
|
|||
'g++', '-std=c++{version}', '-O2', '-Wall', '-Wextra',
|
||||
'-DLOCAL', '{source}', '-o', '{binary}',
|
||||
},
|
||||
run = { '{binary}' },
|
||||
test = { '{binary}' },
|
||||
debug = {
|
||||
'g++', '-std=c++{version}', '-g3',
|
||||
'-fsanitize=address,undefined', '-DLOCAL',
|
||||
|
|
@ -95,7 +95,7 @@ Optional configuration with lazy.nvim: >
|
|||
extension = "cc",
|
||||
},
|
||||
python = {
|
||||
run = { 'python3', '{source}' },
|
||||
test = { 'python3', '{source}' },
|
||||
debug = { 'python3', '{source}' },
|
||||
extension = "py",
|
||||
},
|
||||
|
|
@ -105,16 +105,26 @@ Optional configuration with lazy.nvim: >
|
|||
},
|
||||
hooks = {
|
||||
before_run = function(ctx) vim.cmd.w() end,
|
||||
before_debug = function(ctx)
|
||||
-- ctx.problem_id, ctx.platform, ctx.source_file, etc.
|
||||
vim.cmd.w()
|
||||
end,
|
||||
before_debug = function(ctx) ... end,
|
||||
setup_code = function(ctx)
|
||||
vim.wo.foldmethod = "marker"
|
||||
vim.wo.foldmarker = "{{{,}}}"
|
||||
vim.diagnostic.enable(false)
|
||||
end,
|
||||
},
|
||||
run_panel = {
|
||||
diff_mode = "vim",
|
||||
next_test_key = "<c-n>",
|
||||
prev_test_key = "<c-p>",
|
||||
toggle_diff_key = "t",
|
||||
},
|
||||
diff = {
|
||||
git = {
|
||||
command = "git",
|
||||
args = {"diff", "--no-index", "--word-diff=plain",
|
||||
"--word-diff-regex=.", "--no-prefix"},
|
||||
},
|
||||
},
|
||||
snippets = { ... }, -- LuaSnip snippets
|
||||
filename = function(contest, contest_id, problem_id, config, language) ... end,
|
||||
}
|
||||
|
|
@ -131,6 +141,8 @@ Optional configuration with lazy.nvim: >
|
|||
during operation.
|
||||
• {scrapers} (`table<string,boolean>`) Per-platform scraper control.
|
||||
Default enables all platforms.
|
||||
• {run_panel} (`RunPanelConfig`) Test panel behavior configuration.
|
||||
• {diff} (`DiffConfig`) Diff backend configuration.
|
||||
• {filename}? (`function`) Custom filename generation function.
|
||||
`function(contest, contest_id, problem_id, config, language)`
|
||||
Should return full filename with extension.
|
||||
|
|
@ -151,21 +163,50 @@ Optional configuration with lazy.nvim: >
|
|||
Fields: ~
|
||||
• {compile}? (`string[]`) Compile command template with
|
||||
`{version}`, `{source}`, `{binary}` placeholders.
|
||||
• {run} (`string[]`) Run command template.
|
||||
• {test} (`string[]`) Test execution command template.
|
||||
• {debug}? (`string[]`) Debug compile command template.
|
||||
• {version}? (`number`) Language version (e.g. 20, 23 for C++).
|
||||
• {extension} (`string`) File extension (e.g. "cc", "py").
|
||||
• {executable}? (`string`) Executable name for interpreted languages.
|
||||
|
||||
*cp.RunPanelConfig*
|
||||
|
||||
Fields: ~
|
||||
• {diff_mode} (`string`, default: `"vim"`) Diff backend: "vim" or "git".
|
||||
Git provides character-level precision, vim uses built-in diff.
|
||||
• {next_test_key} (`string`, default: `"<c-n>"`) Key to navigate to next test case.
|
||||
• {prev_test_key} (`string`, default: `"<c-p>"`) Key to navigate to previous test case.
|
||||
• {toggle_diff_key} (`string`, default: `"t"`) Key to toggle diff mode between vim and git.
|
||||
|
||||
*cp.DiffConfig*
|
||||
|
||||
Fields: ~
|
||||
• {git} (`DiffGitConfig`) Git diff backend configuration.
|
||||
|
||||
*cp.Hooks*
|
||||
|
||||
Fields: ~
|
||||
• {before_run}? (`function`) Called before test panel opens.
|
||||
`function(ctx: ProblemContext)`
|
||||
• {before_debug}? (`function`) Called before debug compilation.
|
||||
`function(ctx: ProblemContext)`
|
||||
• {setup_code}? (`function`) Called after source file is opened.
|
||||
Used to configure buffer settings.
|
||||
Good for configuring buffer settings.
|
||||
`function(ctx: ProblemContext)`
|
||||
|
||||
*ProblemContext*
|
||||
|
||||
Fields: ~
|
||||
• {contest} (`string`) Platform name (e.g. "atcoder", "codeforces")
|
||||
• {contest_id} (`string`) Contest ID (e.g. "abc123", "1933")
|
||||
• {problem_id}? (`string`) Problem ID (e.g. "a", "b") - nil for CSES
|
||||
• {source_file} (`string`) Source filename (e.g. "abc123a.cpp")
|
||||
• {binary_file} (`string`) Binary output path (e.g. "build/abc123a.run")
|
||||
• {input_file} (`string`) Test input path (e.g. "io/abc123a.cpin")
|
||||
• {output_file} (`string`) Program output path (e.g. "io/abc123a.cpout")
|
||||
• {expected_file} (`string`) Expected output path (e.g. "io/abc123a.expected")
|
||||
• {problem_name} (`string`) Display name (e.g. "abc123a")
|
||||
|
||||
WORKFLOW *cp-workflow*
|
||||
|
||||
For the sake of consistency and simplicity, cp.nvim extracts contest/problem identifiers from
|
||||
|
|
@ -232,15 +273,19 @@ Example: Setting up and solving AtCoder contest ABC324
|
|||
|
||||
3. Start with problem A: >
|
||||
:CP a
|
||||
|
||||
Or do both at once with:
|
||||
:CP atcoder abc324 a
|
||||
|
||||
< This creates a.cc and scrapes test cases
|
||||
|
||||
4. Code your solution, then test: >
|
||||
:CP test
|
||||
:CP run
|
||||
< Navigate with j/k, run specific tests with <enter>
|
||||
Exit test panel with q or :CP test when done
|
||||
Exit test panel with q or :CP run when done
|
||||
|
||||
5. If needed, debug with sanitizers: >
|
||||
:CP test --debug
|
||||
:CP run --debug
|
||||
<
|
||||
6. Move to next problem: >
|
||||
:CP next
|
||||
|
|
@ -248,20 +293,17 @@ Example: Setting up and solving AtCoder contest ABC324
|
|||
|
||||
6. Continue solving problems with :CP next/:CP prev navigation
|
||||
7. Submit solutions on AtCoder website
|
||||
|
||||
Example: Quick setup for single Codeforces problem >
|
||||
:CP codeforces 1933 a " One command setup
|
||||
:CP test " Test immediately
|
||||
<
|
||||
|
||||
TEST PANEL *cp-test*
|
||||
RUN PANEL *cp-run*
|
||||
|
||||
The test panel provides individual test case debugging with a three-pane
|
||||
layout showing test list, expected output, and actual output side-by-side.
|
||||
The run panel provides individual test case debugging with a streamlined
|
||||
layout optimized for modern screens. Shows test status with competitive
|
||||
programming terminology and efficient space usage.
|
||||
|
||||
Activation ~
|
||||
*:CP-test*
|
||||
:CP test [--debug] Toggle test panel on/off. When activated,
|
||||
*:CP-run*
|
||||
:CP run [--debug] Toggle run panel on/off. When activated,
|
||||
replaces current layout with test interface.
|
||||
Automatically compiles and runs all tests.
|
||||
Use --debug flag to compile with debug symbols
|
||||
|
|
@ -270,29 +312,48 @@ Activation ~
|
|||
|
||||
Interface ~
|
||||
|
||||
The test panel uses a three-pane layout for easy comparison: >
|
||||
The run panel uses a professional table layout with precise column alignment:
|
||||
(note that the diff is indeed highlighted, not the weird amalgamation of
|
||||
characters below) >
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 1. [ok:true ] [code:0] [time:12ms] │
|
||||
│> 2. [ok:false] [code:0] [time:45ms] │
|
||||
│ │
|
||||
│ Input: │
|
||||
│ 5 3 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
┌─ Expected ──────────────────┐ ┌───── Actual ────────────────┐
|
||||
│ 8 │ │ 7 │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
│ │ │ │
|
||||
└─────────────────────────────┘ └─────────────────────────────┘
|
||||
┌──────┬────────┬────────┬───────────┐ ┌─ Expected vs Actual ──────────────────┐
|
||||
│ # │ Status │ Time │ Exit Code │ │ 45.70ms │ Exit: 0 │
|
||||
├──────┼────────┼────────┼───────────┤ ├────────────────────────────────────────┤
|
||||
│ 1 │ AC │12.00ms │ 0 │ │ │
|
||||
│ >2 │ WA │45.70ms │ 1 │ │ 4[-2-]{+3+} │
|
||||
├──────┴────────┴────────┴───────────┤ │ 100 │
|
||||
│5 3 │ │ hello w[-o-]r{+o+}ld │
|
||||
├──────┬────────┬────────┬───────────┤ │ │
|
||||
│ 3 │ AC │ 9.00ms │ 0 │ └────────────────────────────────────────┘
|
||||
│ 4 │ RTE │ 0.00ms │139 (SIGUSR2)│
|
||||
└──────┴────────┴────────┴───────────┘
|
||||
<
|
||||
|
||||
Status Indicators ~
|
||||
|
||||
Test cases use competitive programming terminology:
|
||||
|
||||
AC Accepted (passed)
|
||||
WA Wrong Answer (output mismatch)
|
||||
TLE Time Limit Exceeded (timeout)
|
||||
RTE Runtime Error (non-zero exit)
|
||||
|
||||
Keymaps ~
|
||||
*cp-test-keys*
|
||||
<c-n> Navigate to next test case
|
||||
<c-p> Navigate to previous test case
|
||||
q Exit test panel (restore layout)
|
||||
<c-n> Navigate to next test case (configurable via run_panel.next_test_key)
|
||||
<c-p> Navigate to previous test case (configurable via run_panel.prev_test_key)
|
||||
t Toggle diff mode between vim and git (configurable via run_panel.toggle_diff_key)
|
||||
q Exit test panel and restore layout
|
||||
|
||||
Diff Modes ~
|
||||
|
||||
Two diff backends are available:
|
||||
|
||||
vim Built-in vim diff (default, always available)
|
||||
git Character-level git word-diff (requires git, more precise)
|
||||
|
||||
The git backend shows character-level changes with [-removed-] and {+added+}
|
||||
markers for precise difference analysis.
|
||||
|
||||
Execution Details ~
|
||||
|
||||
|
|
@ -309,8 +370,8 @@ cp.nvim creates the following file structure upon problem setup:
|
|||
build/
|
||||
{problem_id}.run " Compiled binary
|
||||
io/
|
||||
{problem_id}.cpin " Test input
|
||||
{problem_id}.cpout " Program output
|
||||
{problem_id}.n.cpin " nth test input
|
||||
{problem_id}.n.cpout " nth program output
|
||||
{problem_id}.expected " Expected output
|
||||
|
||||
The plugin automatically manages this structure and navigation between problems
|
||||
|
|
@ -321,8 +382,9 @@ SNIPPETS *cp-snippets*
|
|||
cp.nvim integrates with LuaSnip for automatic template expansion. Built-in
|
||||
snippets include basic C++ and Python templates for each contest type.
|
||||
|
||||
Snippet trigger names must EXACTLY match platform names ("codeforces" for
|
||||
CodeForces, "cses" for CSES, etc.).
|
||||
Snippet trigger names must match the following format exactly:
|
||||
|
||||
cp.nvim/{platform}
|
||||
|
||||
Custom snippets can be added via the `snippets` configuration field.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
---@class LanguageConfig
|
||||
---@field compile? string[] Compile command template
|
||||
---@field run string[] Run command template
|
||||
---@field test string[] Test execution command template
|
||||
---@field debug? string[] Debug command template
|
||||
---@field executable? string Executable name
|
||||
---@field version? number Language version
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
---@class PartialLanguageConfig
|
||||
---@field compile? string[] Compile command template
|
||||
---@field run? string[] Run command template
|
||||
---@field test? string[] Test execution command template
|
||||
---@field debug? string[] Debug command template
|
||||
---@field executable? string Executable name
|
||||
---@field version? number Language version
|
||||
|
|
@ -31,6 +31,19 @@
|
|||
---@field before_debug? fun(ctx: ProblemContext)
|
||||
---@field setup_code? fun(ctx: ProblemContext)
|
||||
|
||||
---@class RunPanelConfig
|
||||
---@field diff_mode "vim"|"git" Diff backend to use
|
||||
---@field next_test_key string Key to navigate to next test case
|
||||
---@field prev_test_key string Key to navigate to previous test case
|
||||
---@field toggle_diff_key string Key to toggle diff mode
|
||||
|
||||
---@class DiffGitConfig
|
||||
---@field command string Git executable name
|
||||
---@field args string[] Additional git diff arguments
|
||||
|
||||
---@class DiffConfig
|
||||
---@field git DiffGitConfig
|
||||
|
||||
---@class cp.Config
|
||||
---@field contests table<string, ContestConfig>
|
||||
---@field snippets table[]
|
||||
|
|
@ -38,6 +51,8 @@
|
|||
---@field debug boolean
|
||||
---@field scrapers table<string, boolean>
|
||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||
---@field run_panel RunPanelConfig
|
||||
---@field diff DiffConfig
|
||||
|
||||
---@class cp.UserConfig
|
||||
---@field contests? table<string, PartialContestConfig>
|
||||
|
|
@ -46,6 +61,8 @@
|
|||
---@field debug? boolean
|
||||
---@field scrapers? table<string, boolean>
|
||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||
---@field run_panel? RunPanelConfig
|
||||
---@field diff? DiffConfig
|
||||
|
||||
local M = {}
|
||||
local constants = require('cp.constants')
|
||||
|
|
@ -62,6 +79,18 @@ M.defaults = {
|
|||
debug = false,
|
||||
scrapers = constants.PLATFORMS,
|
||||
filename = nil,
|
||||
run_panel = {
|
||||
diff_mode = 'vim',
|
||||
next_test_key = '<c-n>',
|
||||
prev_test_key = '<c-p>',
|
||||
toggle_diff_key = 't',
|
||||
},
|
||||
diff = {
|
||||
git = {
|
||||
command = 'git',
|
||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
---@param user_config cp.UserConfig|nil
|
||||
|
|
@ -79,28 +108,10 @@ function M.setup(user_config)
|
|||
debug = { user_config.debug, { 'boolean', 'nil' }, true },
|
||||
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
|
||||
filename = { user_config.filename, { 'function', 'nil' }, true },
|
||||
run_panel = { user_config.run_panel, { 'table', 'nil' }, true },
|
||||
diff = { user_config.diff, { 'table', 'nil' }, true },
|
||||
})
|
||||
|
||||
if user_config.hooks then
|
||||
vim.validate({
|
||||
before_run = {
|
||||
user_config.hooks.before_run,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
before_debug = {
|
||||
user_config.hooks.before_debug,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
setup_code = {
|
||||
user_config.hooks.setup_code,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
if user_config.contests then
|
||||
for contest_name, contest_config in pairs(user_config.contests) do
|
||||
for lang_name, lang_config in pairs(contest_config) do
|
||||
|
|
@ -144,6 +155,60 @@ function M.setup(user_config)
|
|||
|
||||
local config = vim.tbl_deep_extend('force', M.defaults, user_config or {})
|
||||
|
||||
-- Validate merged config values
|
||||
vim.validate({
|
||||
before_run = {
|
||||
config.hooks.before_run,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
before_debug = {
|
||||
config.hooks.before_debug,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
setup_code = {
|
||||
config.hooks.setup_code,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
})
|
||||
|
||||
vim.validate({
|
||||
diff_mode = {
|
||||
config.run_panel.diff_mode,
|
||||
function(value)
|
||||
return vim.tbl_contains({ 'vim', 'git' }, value)
|
||||
end,
|
||||
"diff_mode must be 'vim' or 'git'",
|
||||
},
|
||||
next_test_key = {
|
||||
config.run_panel.next_test_key,
|
||||
function(value)
|
||||
return type(value) == 'string' and value ~= ''
|
||||
end,
|
||||
'next_test_key must be a non-empty string',
|
||||
},
|
||||
prev_test_key = {
|
||||
config.run_panel.prev_test_key,
|
||||
function(value)
|
||||
return type(value) == 'string' and value ~= ''
|
||||
end,
|
||||
'prev_test_key must be a non-empty string',
|
||||
},
|
||||
toggle_diff_key = {
|
||||
config.run_panel.toggle_diff_key,
|
||||
function(value)
|
||||
return type(value) == 'string' and value ~= ''
|
||||
end,
|
||||
'toggle_diff_key must be a non-empty string',
|
||||
},
|
||||
})
|
||||
|
||||
vim.validate({
|
||||
git = { config.diff.git, { 'table', 'nil' }, true },
|
||||
})
|
||||
|
||||
for _, contest_config in pairs(config.contests) do
|
||||
for lang_name, lang_config in pairs(contest_config) do
|
||||
if type(lang_config) == 'table' and not lang_config.extension then
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
local M = {}
|
||||
|
||||
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
|
||||
M.ACTIONS = { 'test', 'next', 'prev' }
|
||||
M.ACTIONS = { 'run', 'next', 'prev' }
|
||||
|
||||
M.CPP = 'cpp'
|
||||
M.PYTHON = 'python'
|
||||
|
|
|
|||
121
lua/cp/diff.lua
Normal file
121
lua/cp/diff.lua
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
---@class DiffResult
|
||||
---@field content string[]
|
||||
---@field highlights table[]?
|
||||
---@field raw_diff string?
|
||||
|
||||
---@class DiffBackend
|
||||
---@field name string
|
||||
---@field render fun(expected: string, actual: string): DiffResult
|
||||
|
||||
local M = {}
|
||||
|
||||
---Vim's built-in diff backend using diffthis
|
||||
---@type DiffBackend
|
||||
local vim_backend = {
|
||||
name = 'vim',
|
||||
render = function(_, actual)
|
||||
local actual_lines = vim.split(actual, '\n', { plain = true, trimempty = true })
|
||||
|
||||
return {
|
||||
content = actual_lines,
|
||||
highlights = nil, -- diffthis handles highlighting
|
||||
}
|
||||
end,
|
||||
}
|
||||
|
||||
---Git word-diff backend for character-level precision
|
||||
---@type DiffBackend
|
||||
local git_backend = {
|
||||
name = 'git',
|
||||
render = function(expected, actual)
|
||||
-- Create temporary files for git diff
|
||||
local tmp_expected = vim.fn.tempname()
|
||||
local tmp_actual = vim.fn.tempname()
|
||||
|
||||
vim.fn.writefile(vim.split(expected, '\n', { plain = true }), tmp_expected)
|
||||
vim.fn.writefile(vim.split(actual, '\n', { plain = true }), tmp_actual)
|
||||
|
||||
local cmd = {
|
||||
'git',
|
||||
'diff',
|
||||
'--no-index',
|
||||
'--word-diff=plain',
|
||||
'--word-diff-regex=.',
|
||||
'--no-prefix',
|
||||
tmp_expected,
|
||||
tmp_actual,
|
||||
}
|
||||
|
||||
local result = vim.system(cmd, { text = true }):wait()
|
||||
|
||||
-- Clean up temp files
|
||||
vim.fn.delete(tmp_expected)
|
||||
vim.fn.delete(tmp_actual)
|
||||
|
||||
if result.code == 0 then
|
||||
return {
|
||||
content = vim.split(actual, '\n', { plain = true, trimempty = true }),
|
||||
highlights = {},
|
||||
}
|
||||
else
|
||||
return {
|
||||
content = {},
|
||||
highlights = {},
|
||||
raw_diff = result.stdout or '',
|
||||
}
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
---Available diff backends
|
||||
---@type table<string, DiffBackend>
|
||||
local backends = {
|
||||
vim = vim_backend,
|
||||
git = git_backend,
|
||||
}
|
||||
|
||||
---Get available backend names
|
||||
---@return string[]
|
||||
function M.get_available_backends()
|
||||
return vim.tbl_keys(backends)
|
||||
end
|
||||
|
||||
---Get a diff backend by name
|
||||
---@param name string
|
||||
---@return DiffBackend?
|
||||
function M.get_backend(name)
|
||||
return backends[name]
|
||||
end
|
||||
|
||||
---Check if git backend is available
|
||||
---@return boolean
|
||||
function M.is_git_available()
|
||||
local result = vim.system({ 'git', '--version' }, { text = true }):wait()
|
||||
return result.code == 0
|
||||
end
|
||||
|
||||
---Get the best available backend based on config and system availability
|
||||
---@param preferred_backend? string
|
||||
---@return DiffBackend
|
||||
function M.get_best_backend(preferred_backend)
|
||||
if preferred_backend and backends[preferred_backend] then
|
||||
if preferred_backend == 'git' and not M.is_git_available() then
|
||||
return backends.vim
|
||||
end
|
||||
return backends[preferred_backend]
|
||||
end
|
||||
|
||||
return backends.vim
|
||||
end
|
||||
|
||||
---Render diff using specified backend
|
||||
---@param expected string
|
||||
---@param actual string
|
||||
---@param backend_name? string
|
||||
---@return DiffResult
|
||||
function M.render_diff(expected, actual, backend_name)
|
||||
local backend = M.get_best_backend(backend_name)
|
||||
return backend.render(expected, actual)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -278,13 +278,19 @@ function M.run_problem(ctx, contest_config, is_debug)
|
|||
input_data = table.concat(vim.fn.readfile(ctx.input_file), '\n') .. '\n'
|
||||
end
|
||||
|
||||
local run_cmd = build_command(language_config.run, language_config.executable, substitutions)
|
||||
local run_cmd = build_command(language_config.test, language_config.executable, substitutions)
|
||||
local exec_result = execute_command(run_cmd, input_data, contest_config.timeout_ms)
|
||||
local formatted_output = format_output(exec_result, ctx.expected_file, is_debug)
|
||||
|
||||
local output_buf = vim.fn.bufnr(ctx.output_file)
|
||||
if output_buf ~= -1 then
|
||||
local was_modifiable = vim.api.nvim_get_option_value('modifiable', { buf = output_buf })
|
||||
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = output_buf })
|
||||
vim.api.nvim_set_option_value('readonly', false, { buf = output_buf })
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = output_buf })
|
||||
vim.api.nvim_buf_set_lines(output_buf, 0, -1, false, vim.split(formatted_output, '\n'))
|
||||
vim.api.nvim_set_option_value('modifiable', was_modifiable, { buf = output_buf })
|
||||
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = output_buf })
|
||||
vim.api.nvim_buf_call(output_buf, function()
|
||||
vim.cmd.write()
|
||||
end)
|
||||
|
|
|
|||
170
lua/cp/highlight.lua
Normal file
170
lua/cp/highlight.lua
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
---@class DiffHighlight
|
||||
---@field line number
|
||||
---@field col_start number
|
||||
---@field col_end number
|
||||
---@field highlight_group string
|
||||
|
||||
---@class ParsedDiff
|
||||
---@field content string[]
|
||||
---@field highlights DiffHighlight[]
|
||||
|
||||
local M = {}
|
||||
|
||||
---Parse git diff markers and extract highlight information
|
||||
---@param text string Raw git diff output line
|
||||
---@return string cleaned_text, DiffHighlight[]
|
||||
local function parse_diff_line(text)
|
||||
local result_text = ''
|
||||
local highlights = {}
|
||||
local pos = 1
|
||||
|
||||
while pos <= #text do
|
||||
local removed_start, removed_end, removed_content = text:find('%[%-(.-)%-%]', pos)
|
||||
if removed_start and removed_start == pos then
|
||||
local highlight_start = #result_text
|
||||
result_text = result_text .. removed_content
|
||||
table.insert(highlights, {
|
||||
line = 0,
|
||||
col_start = highlight_start,
|
||||
col_end = #result_text,
|
||||
highlight_group = 'CpDiffRemoved',
|
||||
})
|
||||
pos = removed_end + 1
|
||||
else
|
||||
local added_start, added_end, added_content = text:find('{%+(.-)%+}', pos)
|
||||
if added_start and added_start == pos then
|
||||
local highlight_start = #result_text
|
||||
result_text = result_text .. added_content
|
||||
table.insert(highlights, {
|
||||
line = 0,
|
||||
col_start = highlight_start,
|
||||
col_end = #result_text,
|
||||
highlight_group = 'CpDiffAdded',
|
||||
})
|
||||
pos = added_end + 1
|
||||
else
|
||||
result_text = result_text .. text:sub(pos, pos)
|
||||
pos = pos + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return result_text, highlights
|
||||
end
|
||||
|
||||
---Parse complete git diff output
|
||||
---@param diff_output string
|
||||
---@return ParsedDiff
|
||||
function M.parse_git_diff(diff_output)
|
||||
if diff_output == '' then
|
||||
return { content = {}, highlights = {} }
|
||||
end
|
||||
|
||||
local lines = vim.split(diff_output, '\n', { plain = true })
|
||||
local content_lines = {}
|
||||
local all_highlights = {}
|
||||
|
||||
-- Skip git diff header lines
|
||||
local content_started = false
|
||||
for _, line in ipairs(lines) do
|
||||
-- Skip header lines (@@, +++, ---, index, etc.)
|
||||
if
|
||||
content_started
|
||||
or (
|
||||
not line:match('^@@')
|
||||
and not line:match('^%+%+%+')
|
||||
and not line:match('^%-%-%-')
|
||||
and not line:match('^index')
|
||||
and not line:match('^diff %-%-git')
|
||||
)
|
||||
then
|
||||
content_started = true
|
||||
|
||||
-- Process content lines
|
||||
if line:match('^%+') then
|
||||
-- Added line - remove + prefix and parse highlights
|
||||
local clean_line = line:sub(2) -- Remove + prefix
|
||||
local parsed_line, line_highlights = parse_diff_line(clean_line)
|
||||
|
||||
table.insert(content_lines, parsed_line)
|
||||
|
||||
-- Set line numbers for highlights
|
||||
local line_num = #content_lines
|
||||
for _, highlight in ipairs(line_highlights) do
|
||||
highlight.line = line_num - 1 -- 0-based for extmarks
|
||||
table.insert(all_highlights, highlight)
|
||||
end
|
||||
elseif not line:match('^%-') and not line:match('^\\') then -- Skip removed lines and "\ No newline" messages
|
||||
-- Word-diff content line or unchanged line
|
||||
local clean_line = line:match('^%s') and line:sub(2) or line
|
||||
local parsed_line, line_highlights = parse_diff_line(clean_line)
|
||||
|
||||
-- Only add non-empty lines
|
||||
if parsed_line ~= '' then
|
||||
table.insert(content_lines, parsed_line)
|
||||
|
||||
-- Set line numbers for highlights
|
||||
local line_num = #content_lines
|
||||
for _, highlight in ipairs(line_highlights) do
|
||||
highlight.line = line_num - 1 -- 0-based for extmarks
|
||||
table.insert(all_highlights, highlight)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
content = content_lines,
|
||||
highlights = all_highlights,
|
||||
}
|
||||
end
|
||||
|
||||
---Apply highlights to a buffer using extmarks
|
||||
---@param bufnr number
|
||||
---@param highlights DiffHighlight[]
|
||||
---@param namespace number
|
||||
function M.apply_highlights(bufnr, highlights, namespace)
|
||||
-- Clear existing highlights in this namespace
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
|
||||
|
||||
for _, highlight in ipairs(highlights) do
|
||||
if highlight.col_start < highlight.col_end then
|
||||
vim.api.nvim_buf_set_extmark(bufnr, namespace, highlight.line, highlight.col_start, {
|
||||
end_col = highlight.col_end,
|
||||
hl_group = highlight.highlight_group,
|
||||
priority = 100,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---Create namespace for diff highlights
|
||||
---@return number
|
||||
function M.create_namespace()
|
||||
return vim.api.nvim_create_namespace('cp_diff_highlights')
|
||||
end
|
||||
|
||||
---Parse and apply git diff to buffer
|
||||
---@param bufnr number
|
||||
---@param diff_output string
|
||||
---@param namespace number
|
||||
---@return string[] content_lines
|
||||
function M.parse_and_apply_diff(bufnr, diff_output, namespace)
|
||||
local parsed = M.parse_git_diff(diff_output)
|
||||
|
||||
local was_modifiable = vim.api.nvim_get_option_value('modifiable', { 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('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, parsed.content)
|
||||
vim.api.nvim_set_option_value('modifiable', was_modifiable, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
|
||||
|
||||
M.apply_highlights(bufnr, parsed.highlights, namespace)
|
||||
|
||||
return parsed.content
|
||||
end
|
||||
|
||||
return M
|
||||
370
lua/cp/init.lua
370
lua/cp/init.lua
|
|
@ -25,9 +25,12 @@ local state = {
|
|||
saved_session = nil,
|
||||
test_cases = nil,
|
||||
test_states = {},
|
||||
test_panel_active = false,
|
||||
run_panel_active = false,
|
||||
}
|
||||
|
||||
local current_diff_layout = nil
|
||||
local current_mode = nil
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local platforms = constants.PLATFORMS
|
||||
local actions = constants.ACTIONS
|
||||
|
|
@ -149,14 +152,32 @@ local function get_current_problem()
|
|||
return filename
|
||||
end
|
||||
|
||||
local function toggle_test_panel(is_debug)
|
||||
if state.test_panel_active then
|
||||
local function create_buffer_with_options(filetype)
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf })
|
||||
vim.api.nvim_set_option_value('readonly', true, { buf = buf })
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
|
||||
if filetype then
|
||||
vim.api.nvim_set_option_value('filetype', filetype, { buf = buf })
|
||||
end
|
||||
return buf
|
||||
end
|
||||
|
||||
local setup_keybindings_for_buffer
|
||||
|
||||
local function toggle_run_panel(is_debug)
|
||||
if state.run_panel_active then
|
||||
if current_diff_layout then
|
||||
current_diff_layout.cleanup()
|
||||
current_diff_layout = nil
|
||||
current_mode = nil
|
||||
end
|
||||
if state.saved_session then
|
||||
vim.cmd(('source %s'):format(state.saved_session))
|
||||
vim.fn.delete(state.saved_session)
|
||||
state.saved_session = nil
|
||||
end
|
||||
state.test_panel_active = false
|
||||
state.run_panel_active = false
|
||||
logger.log('test panel closed')
|
||||
return
|
||||
end
|
||||
|
|
@ -187,167 +208,224 @@ local function toggle_test_panel(is_debug)
|
|||
|
||||
vim.cmd('silent only')
|
||||
|
||||
local tab_buf = vim.api.nvim_create_buf(false, true)
|
||||
local expected_buf = vim.api.nvim_create_buf(false, true)
|
||||
local actual_buf = vim.api.nvim_create_buf(false, true)
|
||||
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = tab_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = expected_buf })
|
||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = actual_buf })
|
||||
|
||||
local tab_buf = create_buffer_with_options()
|
||||
local main_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(main_win, tab_buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf })
|
||||
|
||||
vim.cmd.split()
|
||||
vim.api.nvim_win_set_buf(0, actual_buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_buf })
|
||||
|
||||
vim.cmd.vsplit()
|
||||
vim.api.nvim_win_set_buf(0, expected_buf)
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = expected_buf })
|
||||
|
||||
local expected_win = vim.fn.bufwinid(expected_buf)
|
||||
local actual_win = vim.fn.bufwinid(actual_buf)
|
||||
|
||||
local test_windows = {
|
||||
tab_win = main_win,
|
||||
actual_win = actual_win,
|
||||
expected_win = expected_win,
|
||||
}
|
||||
local test_buffers = {
|
||||
tab_buf = tab_buf,
|
||||
expected_buf = expected_buf,
|
||||
actual_buf = actual_buf,
|
||||
}
|
||||
|
||||
local function render_test_tabs()
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
local tab_lines = {}
|
||||
local highlight = require('cp.highlight')
|
||||
local diff_namespace = highlight.create_namespace()
|
||||
|
||||
local max_status_width = 0
|
||||
local max_code_width = 0
|
||||
local max_time_width = 0
|
||||
local test_list_namespace = vim.api.nvim_create_namespace('cp_test_list')
|
||||
|
||||
for _, test_case in ipairs(test_state.test_cases) do
|
||||
local status_text = test_case.status == 'pending' and '' or string.upper(test_case.status)
|
||||
max_status_width = math.max(max_status_width, #status_text)
|
||||
local function update_buffer_content(bufnr, lines, highlights)
|
||||
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
|
||||
|
||||
if test_case.code then
|
||||
max_code_width = math.max(max_code_width, #tostring(test_case.code))
|
||||
end
|
||||
vim.api.nvim_set_option_value('readonly', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
|
||||
|
||||
if test_case.time_ms then
|
||||
local time_text = string.format('%.0fms', test_case.time_ms)
|
||||
max_time_width = math.max(max_time_width, #time_text)
|
||||
end
|
||||
end
|
||||
|
||||
for i, test_case in ipairs(test_state.test_cases) do
|
||||
local prefix = i == test_state.current_index and '> ' or ' '
|
||||
local tab = string.format('%s%d.', prefix, i)
|
||||
|
||||
if test_case.ok ~= nil then
|
||||
tab = tab .. string.format(' [ok:%-5s]', tostring(test_case.ok))
|
||||
end
|
||||
|
||||
if test_case.code then
|
||||
tab = tab .. string.format(' [code:%-' .. max_code_width .. 's]', tostring(test_case.code))
|
||||
end
|
||||
|
||||
if test_case.time_ms then
|
||||
local time_text = string.format('%.0fms', test_case.time_ms)
|
||||
tab = tab .. string.format(' [time:%-' .. max_time_width .. 's]', time_text)
|
||||
end
|
||||
|
||||
if test_case.signal then
|
||||
tab = tab .. string.format(' [%s]', test_case.signal)
|
||||
end
|
||||
|
||||
table.insert(tab_lines, tab)
|
||||
end
|
||||
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
if current_test then
|
||||
table.insert(tab_lines, '')
|
||||
table.insert(tab_lines, 'Input:')
|
||||
for _, line in ipairs(vim.split(current_test.input, '\n', { plain = true, trimempty = true })) do
|
||||
table.insert(tab_lines, line)
|
||||
end
|
||||
end
|
||||
|
||||
return tab_lines
|
||||
end
|
||||
|
||||
local function update_expected_pane()
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
|
||||
if not current_test then
|
||||
return
|
||||
end
|
||||
|
||||
local expected_text = current_test.expected
|
||||
local expected_lines = vim.split(expected_text, '\n', { plain = true, trimempty = true })
|
||||
|
||||
vim.api.nvim_buf_set_lines(test_buffers.expected_buf, 0, -1, false, expected_lines)
|
||||
|
||||
if vim.fn.has('nvim-0.8.0') == 1 then
|
||||
vim.api.nvim_set_option_value('winbar', 'Expected', { win = test_windows.expected_win })
|
||||
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 = 100,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
local function update_actual_pane()
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
local function create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||
local expected_buf = create_buffer_with_options()
|
||||
local actual_buf = create_buffer_with_options()
|
||||
|
||||
if not current_test then
|
||||
return
|
||||
end
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
local actual_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(actual_win, actual_buf)
|
||||
|
||||
local actual_lines = {}
|
||||
local enable_diff = false
|
||||
vim.cmd.vsplit()
|
||||
local expected_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(expected_win, expected_buf)
|
||||
|
||||
if current_test.actual then
|
||||
actual_lines = vim.split(current_test.actual, '\n', { plain = true, trimempty = true })
|
||||
enable_diff = current_test.status == 'fail'
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = expected_buf })
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_buf })
|
||||
|
||||
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(expected_buf, expected_lines, {})
|
||||
update_buffer_content(actual_buf, actual_lines, {})
|
||||
|
||||
vim.api.nvim_set_option_value('diff', true, { win = expected_win })
|
||||
vim.api.nvim_set_option_value('diff', true, { win = actual_win })
|
||||
vim.api.nvim_win_call(expected_win, function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_win_call(actual_win, function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
|
||||
return {
|
||||
buffers = { expected_buf, actual_buf },
|
||||
windows = { expected_win, actual_win },
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, expected_win, true)
|
||||
pcall(vim.api.nvim_win_close, actual_win, true)
|
||||
pcall(vim.api.nvim_buf_delete, expected_buf, { force = true })
|
||||
pcall(vim.api.nvim_buf_delete, actual_buf, { force = true })
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local function create_git_diff_layout(parent_win, expected_content, actual_content)
|
||||
local diff_buf = create_buffer_with_options()
|
||||
|
||||
vim.api.nvim_set_current_win(parent_win)
|
||||
vim.cmd.split()
|
||||
local diff_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(diff_win, diff_buf)
|
||||
|
||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = diff_buf })
|
||||
|
||||
local diff_backend = require('cp.diff')
|
||||
local backend = diff_backend.get_best_backend('git')
|
||||
local diff_result = backend.render(expected_content, actual_content)
|
||||
|
||||
if diff_result.raw_diff and diff_result.raw_diff ~= '' then
|
||||
highlight.parse_and_apply_diff(diff_buf, diff_result.raw_diff, diff_namespace)
|
||||
else
|
||||
actual_lines = { '(not run yet)' }
|
||||
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(diff_buf, lines, {})
|
||||
end
|
||||
|
||||
vim.api.nvim_buf_set_lines(test_buffers.actual_buf, 0, -1, false, actual_lines)
|
||||
return {
|
||||
buffers = { diff_buf },
|
||||
windows = { diff_win },
|
||||
cleanup = function()
|
||||
pcall(vim.api.nvim_win_close, diff_win, true)
|
||||
pcall(vim.api.nvim_buf_delete, diff_buf, { force = true })
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
if vim.fn.has('nvim-0.8.0') == 1 then
|
||||
vim.api.nvim_set_option_value('winbar', 'Actual', { win = test_windows.actual_win })
|
||||
end
|
||||
|
||||
vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.expected_win })
|
||||
vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.actual_win })
|
||||
|
||||
if enable_diff then
|
||||
vim.api.nvim_win_call(test_windows.expected_win, function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_win_call(test_windows.actual_win, function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
local function create_diff_layout(mode, parent_win, expected_content, actual_content)
|
||||
if 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)
|
||||
end
|
||||
end
|
||||
|
||||
local function refresh_test_panel()
|
||||
local function update_diff_panes()
|
||||
local test_state = test_module.get_run_panel_state()
|
||||
local current_test = test_state.test_cases[test_state.current_index]
|
||||
|
||||
if not current_test then
|
||||
return
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
if not should_show_diff then
|
||||
expected_content = expected_content
|
||||
actual_content = actual_content
|
||||
end
|
||||
|
||||
local desired_mode = should_show_diff and config.run_panel.diff_mode or 'vim'
|
||||
|
||||
if current_diff_layout and current_mode ~= desired_mode then
|
||||
local saved_pos = vim.api.nvim_win_get_cursor(0)
|
||||
current_diff_layout.cleanup()
|
||||
current_diff_layout = nil
|
||||
current_mode = nil
|
||||
|
||||
current_diff_layout =
|
||||
create_diff_layout(desired_mode, main_win, expected_content, actual_content)
|
||||
current_mode = desired_mode
|
||||
|
||||
for _, buf in ipairs(current_diff_layout.buffers) do
|
||||
setup_keybindings_for_buffer(buf)
|
||||
end
|
||||
|
||||
pcall(vim.api.nvim_win_set_cursor, 0, saved_pos)
|
||||
return
|
||||
end
|
||||
|
||||
if not current_diff_layout then
|
||||
current_diff_layout =
|
||||
create_diff_layout(desired_mode, main_win, expected_content, actual_content)
|
||||
current_mode = desired_mode
|
||||
|
||||
for _, buf in ipairs(current_diff_layout.buffers) do
|
||||
setup_keybindings_for_buffer(buf)
|
||||
end
|
||||
else
|
||||
if 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)
|
||||
|
||||
if diff_result.raw_diff and diff_result.raw_diff ~= '' then
|
||||
highlight.parse_and_apply_diff(
|
||||
current_diff_layout.buffers[1],
|
||||
diff_result.raw_diff,
|
||||
diff_namespace
|
||||
)
|
||||
else
|
||||
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(current_diff_layout.buffers[1], lines, {})
|
||||
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, {})
|
||||
|
||||
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[2] })
|
||||
vim.api.nvim_win_call(current_diff_layout.windows[1], function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
vim.api.nvim_win_call(current_diff_layout.windows[2], function()
|
||||
vim.cmd.diffthis()
|
||||
end)
|
||||
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] })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function refresh_run_panel()
|
||||
if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local tab_lines = render_test_tabs()
|
||||
vim.api.nvim_buf_set_lines(test_buffers.tab_buf, 0, -1, false, tab_lines)
|
||||
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)
|
||||
update_buffer_content(test_buffers.tab_buf, tab_lines, tab_highlights)
|
||||
|
||||
update_expected_pane()
|
||||
update_actual_pane()
|
||||
update_diff_panes()
|
||||
end
|
||||
|
||||
local function navigate_test_case(delta)
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
local test_state = test_module.get_run_panel_state()
|
||||
if #test_state.test_cases == 0 then
|
||||
return
|
||||
end
|
||||
|
|
@ -359,20 +437,30 @@ local function toggle_test_panel(is_debug)
|
|||
test_state.current_index = 1
|
||||
end
|
||||
|
||||
refresh_test_panel()
|
||||
refresh_run_panel()
|
||||
end
|
||||
|
||||
vim.keymap.set('n', '<c-n>', function()
|
||||
setup_keybindings_for_buffer = function(buf)
|
||||
vim.keymap.set('n', 'q', function()
|
||||
toggle_run_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', config.run_panel.toggle_diff_key, function()
|
||||
config.run_panel.diff_mode = config.run_panel.diff_mode == 'vim' and 'git' or 'vim'
|
||||
refresh_run_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
end
|
||||
|
||||
vim.keymap.set('n', config.run_panel.next_test_key, function()
|
||||
navigate_test_case(1)
|
||||
end, { buffer = test_buffers.tab_buf, silent = true })
|
||||
vim.keymap.set('n', '<c-p>', function()
|
||||
vim.keymap.set('n', config.run_panel.prev_test_key, function()
|
||||
navigate_test_case(-1)
|
||||
end, { buffer = test_buffers.tab_buf, silent = true })
|
||||
|
||||
for _, buf in pairs(test_buffers) do
|
||||
vim.keymap.set('n', 'q', function()
|
||||
toggle_test_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
setup_keybindings_for_buffer(test_buffers.tab_buf)
|
||||
|
||||
if config.hooks and config.hooks.before_run then
|
||||
config.hooks.before_run(ctx)
|
||||
end
|
||||
|
||||
if is_debug and config.hooks and config.hooks.before_debug then
|
||||
|
|
@ -385,14 +473,14 @@ local function toggle_test_panel(is_debug)
|
|||
test_module.run_all_test_cases(ctx, contest_config)
|
||||
end
|
||||
|
||||
refresh_test_panel()
|
||||
refresh_run_panel()
|
||||
|
||||
vim.api.nvim_set_current_win(test_windows.tab_win)
|
||||
|
||||
state.test_panel_active = true
|
||||
state.run_panel_active = true
|
||||
state.test_buffers = test_buffers
|
||||
state.test_windows = test_windows
|
||||
local test_state = test_module.get_test_panel_state()
|
||||
local test_state = test_module.get_run_panel_state()
|
||||
logger.log(string.format('test panel opened (%d test cases)', #test_state.test_cases))
|
||||
end
|
||||
|
||||
|
|
@ -559,8 +647,8 @@ function M.handle_command(opts)
|
|||
end
|
||||
|
||||
if cmd.type == 'action' then
|
||||
if cmd.action == 'test' then
|
||||
toggle_test_panel(cmd.debug)
|
||||
if cmd.action == 'run' then
|
||||
toggle_run_panel(cmd.debug)
|
||||
elseif cmd.action == 'next' then
|
||||
navigate_problem(1, cmd.language)
|
||||
elseif cmd.action == 'prev' then
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ if __name__ == "__main__":
|
|||
|
||||
local user_overrides = {}
|
||||
for _, snippet in ipairs(config.snippets or {}) do
|
||||
user_overrides[snippet.trigger] = snippet
|
||||
user_overrides[snippet.trigger:lower()] = snippet
|
||||
end
|
||||
|
||||
for language, template_set in pairs(template_definitions) do
|
||||
|
|
@ -110,14 +110,14 @@ if __name__ == "__main__":
|
|||
local filetype = constants.canonical_filetypes[language]
|
||||
|
||||
for contest, template in pairs(template_set) do
|
||||
local prefixed_trigger = ('cp.nvim/%s.%s'):format(contest, language)
|
||||
if not user_overrides[prefixed_trigger] then
|
||||
local prefixed_trigger = ('cp.nvim/%s.%s'):format(contest:lower(), language)
|
||||
if not user_overrides[prefixed_trigger:lower()] then
|
||||
table.insert(snippets, s(prefixed_trigger, fmt(template, { i(1) })))
|
||||
end
|
||||
end
|
||||
|
||||
for trigger, snippet in pairs(user_overrides) do
|
||||
local prefix_match = trigger:match('^cp%.nvim/[^.]+%.(.+)$')
|
||||
local prefix_match = trigger:lower():match('^cp%.nvim/[^.]+%.(.+)$')
|
||||
if prefix_match == language then
|
||||
table.insert(snippets, snippet)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
---@field signal string?
|
||||
---@field timed_out boolean?
|
||||
|
||||
---@class TestPanelState
|
||||
---@class RunPanelState
|
||||
---@field test_cases TestCase[]
|
||||
---@field current_index number
|
||||
---@field buffer number?
|
||||
|
|
@ -24,8 +24,8 @@ local M = {}
|
|||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
|
||||
---@type TestPanelState
|
||||
local test_panel_state = {
|
||||
---@type RunPanelState
|
||||
local run_panel_state = {
|
||||
test_cases = {},
|
||||
current_index = 1,
|
||||
buffer = nil,
|
||||
|
|
@ -172,7 +172,7 @@ local function run_single_test_case(ctx, contest_config, test_case)
|
|||
end
|
||||
end
|
||||
|
||||
local run_cmd = build_command(language_config.run, language_config.executable, substitutions)
|
||||
local run_cmd = build_command(language_config.test, language_config.executable, substitutions)
|
||||
|
||||
local stdin_content = test_case.input .. '\n'
|
||||
|
||||
|
|
@ -227,8 +227,8 @@ function M.load_test_cases(ctx, state)
|
|||
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
|
||||
end
|
||||
|
||||
test_panel_state.test_cases = test_cases
|
||||
test_panel_state.current_index = 1
|
||||
run_panel_state.test_cases = test_cases
|
||||
run_panel_state.current_index = 1
|
||||
|
||||
logger.log(('loaded %d test case(s)'):format(#test_cases))
|
||||
return #test_cases > 0
|
||||
|
|
@ -239,7 +239,7 @@ end
|
|||
---@param index number
|
||||
---@return boolean
|
||||
function M.run_test_case(ctx, contest_config, index)
|
||||
local test_case = test_panel_state.test_cases[index]
|
||||
local test_case = run_panel_state.test_cases[index]
|
||||
if not test_case then
|
||||
return false
|
||||
end
|
||||
|
|
@ -266,16 +266,16 @@ end
|
|||
---@return TestCase[]
|
||||
function M.run_all_test_cases(ctx, contest_config)
|
||||
local results = {}
|
||||
for i, _ in ipairs(test_panel_state.test_cases) do
|
||||
for i, _ in ipairs(run_panel_state.test_cases) do
|
||||
M.run_test_case(ctx, contest_config, i)
|
||||
table.insert(results, test_panel_state.test_cases[i])
|
||||
table.insert(results, run_panel_state.test_cases[i])
|
||||
end
|
||||
return results
|
||||
end
|
||||
|
||||
---@return TestPanelState
|
||||
function M.get_test_panel_state()
|
||||
return test_panel_state
|
||||
---@return RunPanelState
|
||||
function M.get_run_panel_state()
|
||||
return run_panel_state
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
289
lua/cp/test_render.lua
Normal file
289
lua/cp/test_render.lua
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
---@class StatusInfo
|
||||
---@field text string
|
||||
---@field highlight_group string
|
||||
|
||||
local M = {}
|
||||
|
||||
local exit_code_names = {
|
||||
[128] = 'SIGHUP',
|
||||
[129] = 'SIGINT',
|
||||
[130] = 'SIGQUIT',
|
||||
[131] = 'SIGILL',
|
||||
[132] = 'SIGTRAP',
|
||||
[133] = 'SIGABRT',
|
||||
[134] = 'SIGBUS',
|
||||
[135] = 'SIGFPE',
|
||||
[136] = 'SIGKILL',
|
||||
[137] = 'SIGUSR1',
|
||||
[138] = 'SIGSEGV',
|
||||
[139] = 'SIGUSR2',
|
||||
[140] = 'SIGPIPE',
|
||||
[141] = 'SIGALRM',
|
||||
[142] = 'SIGTERM',
|
||||
[143] = 'SIGCHLD',
|
||||
}
|
||||
|
||||
---@param test_case TestCase
|
||||
---@return StatusInfo
|
||||
function M.get_status_info(test_case)
|
||||
if test_case.status == 'pass' then
|
||||
return { text = 'AC', highlight_group = 'CpTestAC' }
|
||||
elseif test_case.status == 'fail' then
|
||||
if test_case.timed_out then
|
||||
return { text = 'TLE', highlight_group = 'CpTestError' }
|
||||
elseif test_case.code and test_case.code >= 128 then
|
||||
return { text = 'RTE', highlight_group = 'CpTestError' }
|
||||
else
|
||||
return { text = 'WA', highlight_group = 'CpTestError' }
|
||||
end
|
||||
elseif test_case.status == 'timeout' then
|
||||
return { text = 'TLE', highlight_group = 'CpTestError' }
|
||||
elseif test_case.status == 'running' then
|
||||
return { text = '...', highlight_group = 'CpTestPending' }
|
||||
else
|
||||
return { text = '', highlight_group = 'CpTestPending' }
|
||||
end
|
||||
end
|
||||
|
||||
local function format_exit_code(code)
|
||||
if not code then
|
||||
return '—'
|
||||
end
|
||||
local signal_name = exit_code_names[code]
|
||||
return signal_name and string.format('%d (%s)', code, signal_name) or tostring(code)
|
||||
end
|
||||
|
||||
-- Compute column widths + aggregates
|
||||
local function compute_cols(test_state)
|
||||
local w = { num = 3, status = 8, time = 6, exit = 11 }
|
||||
|
||||
for i, tc in ipairs(test_state.test_cases) do
|
||||
local prefix = (i == test_state.current_index) and '>' or ' '
|
||||
w.num = math.max(w.num, #(prefix .. i))
|
||||
w.status = math.max(w.status, #(' ' .. M.get_status_info(tc).text))
|
||||
local time_str = tc.time_ms and (string.format('%.2f', tc.time_ms) .. 'ms') or '—'
|
||||
w.time = math.max(w.time, #time_str)
|
||||
w.exit = math.max(w.exit, #(' ' .. format_exit_code(tc.code)))
|
||||
end
|
||||
|
||||
w.num = math.max(w.num, #' #')
|
||||
w.status = math.max(w.status, #' Status')
|
||||
w.time = math.max(w.time, #' Time')
|
||||
w.exit = math.max(w.exit, #' Exit Code')
|
||||
|
||||
local sum = w.num + w.status + w.time + w.exit
|
||||
local inner = sum + 3 -- three inner vertical dividers
|
||||
local total = inner + 2 -- two outer borders
|
||||
return { w = w, sum = sum, inner = inner, total = total }
|
||||
end
|
||||
|
||||
local function center(text, width)
|
||||
local pad = width - #text
|
||||
if pad <= 0 then
|
||||
return text
|
||||
end
|
||||
local left = math.floor(pad / 2)
|
||||
return string.rep(' ', left) .. text .. string.rep(' ', pad - left)
|
||||
end
|
||||
|
||||
local function top_border(c)
|
||||
local w = c.w
|
||||
return '┌'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┐'
|
||||
end
|
||||
|
||||
local function row_sep(c)
|
||||
local w = c.w
|
||||
return '├'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┼'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┤'
|
||||
end
|
||||
|
||||
local function bottom_border(c)
|
||||
local w = c.w
|
||||
return '└'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┘'
|
||||
end
|
||||
|
||||
local function flat_fence_above(c)
|
||||
local w = c.w
|
||||
return '├'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┴'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┤'
|
||||
end
|
||||
|
||||
local function flat_fence_below(c)
|
||||
local w = c.w
|
||||
return '├'
|
||||
.. string.rep('─', w.num)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.status)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.time)
|
||||
.. '┬'
|
||||
.. string.rep('─', w.exit)
|
||||
.. '┤'
|
||||
end
|
||||
|
||||
local function flat_bottom_border(c)
|
||||
return '└' .. string.rep('─', c.inner) .. '┘'
|
||||
end
|
||||
|
||||
local function header_line(c)
|
||||
local w = c.w
|
||||
return '│'
|
||||
.. center('#', w.num)
|
||||
.. '│'
|
||||
.. center('Status', w.status)
|
||||
.. '│'
|
||||
.. center('Time', w.time)
|
||||
.. '│'
|
||||
.. center('Exit Code', w.exit)
|
||||
.. '│'
|
||||
end
|
||||
|
||||
local function data_row(c, idx, tc, is_current)
|
||||
local w = c.w
|
||||
local prefix = is_current and '>' or ' '
|
||||
local status = M.get_status_info(tc)
|
||||
local time = tc.time_ms and (string.format('%.2f', tc.time_ms) .. 'ms') or '—'
|
||||
local exit = format_exit_code(tc.code)
|
||||
|
||||
local line = '│'
|
||||
.. center(prefix .. idx, w.num)
|
||||
.. '│'
|
||||
.. center(status.text, w.status)
|
||||
.. '│'
|
||||
.. center(time, w.time)
|
||||
.. '│'
|
||||
.. center(exit, w.exit)
|
||||
.. '│'
|
||||
|
||||
local hi
|
||||
if status.text ~= '' then
|
||||
local pad = w.status - #status.text
|
||||
local left = math.floor(pad / 2)
|
||||
local status_start_col = 1 + w.num + 1 + left
|
||||
local status_end_col = status_start_col + #status.text
|
||||
hi = {
|
||||
col_start = status_start_col,
|
||||
col_end = status_end_col,
|
||||
highlight_group = status.highlight_group,
|
||||
}
|
||||
end
|
||||
|
||||
return line, hi
|
||||
end
|
||||
|
||||
---@param test_state RunPanelState
|
||||
---@return string[], table[] lines and highlight positions
|
||||
function M.render_test_list(test_state)
|
||||
local lines, highlights = {}, {}
|
||||
local c = compute_cols(test_state)
|
||||
|
||||
table.insert(lines, top_border(c))
|
||||
table.insert(lines, header_line(c))
|
||||
table.insert(lines, row_sep(c))
|
||||
|
||||
for i, tc in ipairs(test_state.test_cases) do
|
||||
local is_current = (i == test_state.current_index)
|
||||
local row, hi = data_row(c, i, tc, is_current)
|
||||
table.insert(lines, row)
|
||||
if hi then
|
||||
hi.line = #lines - 1
|
||||
table.insert(highlights, hi)
|
||||
end
|
||||
|
||||
local has_next = (i < #test_state.test_cases)
|
||||
local has_input = is_current and tc.input and tc.input ~= ''
|
||||
|
||||
if has_input then
|
||||
table.insert(lines, flat_fence_above(c))
|
||||
|
||||
for _, input_line in ipairs(vim.split(tc.input, '\n', { plain = true, trimempty = false })) do
|
||||
local s = input_line or ''
|
||||
if #s > c.inner then
|
||||
s = string.sub(s, 1, c.inner)
|
||||
end
|
||||
local pad = c.inner - #s
|
||||
table.insert(lines, '│' .. s .. string.rep(' ', pad) .. '│')
|
||||
end
|
||||
|
||||
if has_next then
|
||||
table.insert(lines, flat_fence_below(c))
|
||||
else
|
||||
table.insert(lines, flat_bottom_border(c))
|
||||
end
|
||||
else
|
||||
if has_next then
|
||||
table.insert(lines, row_sep(c))
|
||||
else
|
||||
table.insert(lines, bottom_border(c))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lines, highlights
|
||||
end
|
||||
|
||||
---@param test_case TestCase?
|
||||
---@return string
|
||||
function M.render_status_bar(test_case)
|
||||
if not test_case then
|
||||
return ''
|
||||
end
|
||||
local parts = {}
|
||||
if test_case.time_ms then
|
||||
table.insert(parts, string.format('%.2fms', test_case.time_ms))
|
||||
end
|
||||
if test_case.code then
|
||||
table.insert(parts, string.format('Exit: %d', test_case.code))
|
||||
end
|
||||
return table.concat(parts, ' │ ')
|
||||
end
|
||||
|
||||
---@return table<string, table>
|
||||
function M.get_highlight_groups()
|
||||
return {
|
||||
CpTestAC = { fg = '#10b981', bold = true },
|
||||
CpTestError = { fg = '#ef4444', bold = true },
|
||||
CpTestPending = { fg = '#6b7280' },
|
||||
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },
|
||||
CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' },
|
||||
}
|
||||
end
|
||||
|
||||
function M.setup_highlights()
|
||||
local groups = M.get_highlight_groups()
|
||||
for name, opts in pairs(groups) do
|
||||
vim.api.nvim_set_hl(0, name, opts)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -17,6 +17,7 @@ dev = [
|
|||
"types-requests>=2.32.4.20250913",
|
||||
"pytest>=8.0.0",
|
||||
"pytest-mock>=3.12.0",
|
||||
"pre-commit>=4.3.0",
|
||||
]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
std = "vim"
|
||||
std = 'vim'
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ describe('cp command parsing', function()
|
|||
|
||||
describe('action commands', function()
|
||||
it('handles test action without error', function()
|
||||
local opts = { fargs = { 'test' } }
|
||||
local opts = { fargs = { 'run' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
|
|
@ -126,7 +126,7 @@ describe('cp command parsing', function()
|
|||
|
||||
describe('language flag parsing', function()
|
||||
it('logs error for --lang flag missing value', function()
|
||||
local opts = { fargs = { 'test', '--lang' } }
|
||||
local opts = { fargs = { 'run', '--lang' } }
|
||||
|
||||
cp.handle_command(opts)
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ describe('cp command parsing', function()
|
|||
|
||||
describe('debug flag parsing', function()
|
||||
it('handles debug flag without error', function()
|
||||
local opts = { fargs = { 'test', '--debug' } }
|
||||
local opts = { fargs = { 'run', '--debug' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
|
|
@ -177,7 +177,7 @@ describe('cp command parsing', function()
|
|||
end)
|
||||
|
||||
it('handles combined language and debug flags', function()
|
||||
local opts = { fargs = { 'test', '--lang=cpp', '--debug' } }
|
||||
local opts = { fargs = { 'run', '--lang=cpp', '--debug' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
|
|
@ -234,7 +234,7 @@ describe('cp command parsing', function()
|
|||
end)
|
||||
|
||||
it('handles flag order variations', function()
|
||||
local opts = { fargs = { '--debug', 'test', '--lang=python' } }
|
||||
local opts = { fargs = { '--debug', 'run', '--lang=python' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
|
|
@ -242,7 +242,7 @@ describe('cp command parsing', function()
|
|||
end)
|
||||
|
||||
it('handles multiple language flags', function()
|
||||
local opts = { fargs = { 'test', '--lang=cpp', '--lang=python' } }
|
||||
local opts = { fargs = { 'run', '--lang=cpp', '--lang=python' } }
|
||||
|
||||
assert.has_no_errors(function()
|
||||
cp.handle_command(opts)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,52 @@ describe('cp.config', function()
|
|||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('run_panel config validation', function()
|
||||
it('validates diff_mode values', function()
|
||||
local invalid_config = {
|
||||
run_panel = { diff_mode = 'invalid' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates next_test_key is non-empty string', function()
|
||||
local invalid_config = {
|
||||
run_panel = { next_test_key = '' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('validates prev_test_key is non-empty string', function()
|
||||
local invalid_config = {
|
||||
run_panel = { prev_test_key = '' },
|
||||
}
|
||||
|
||||
assert.has_error(function()
|
||||
config.setup(invalid_config)
|
||||
end)
|
||||
end)
|
||||
|
||||
it('accepts valid run_panel config', function()
|
||||
local valid_config = {
|
||||
run_panel = {
|
||||
diff_mode = 'git',
|
||||
next_test_key = 'j',
|
||||
prev_test_key = 'k',
|
||||
},
|
||||
}
|
||||
|
||||
assert.has_no.errors(function()
|
||||
config.setup(valid_config)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('default_filename', function()
|
||||
|
|
|
|||
190
spec/diff_spec.lua
Normal file
190
spec/diff_spec.lua
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
describe('cp.diff', function()
|
||||
local diff = require('cp.diff')
|
||||
|
||||
describe('get_available_backends', function()
|
||||
it('returns vim and git backends', function()
|
||||
local backends = diff.get_available_backends()
|
||||
table.sort(backends)
|
||||
assert.same({ 'git', 'vim' }, backends)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('get_backend', function()
|
||||
it('returns vim backend by name', function()
|
||||
local backend = diff.get_backend('vim')
|
||||
assert.is_not_nil(backend)
|
||||
assert.equals('vim', backend.name)
|
||||
end)
|
||||
|
||||
it('returns git backend by name', function()
|
||||
local backend = diff.get_backend('git')
|
||||
assert.is_not_nil(backend)
|
||||
assert.equals('git', backend.name)
|
||||
end)
|
||||
|
||||
it('returns nil for invalid name', function()
|
||||
local backend = diff.get_backend('invalid')
|
||||
assert.is_nil(backend)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('is_git_available', function()
|
||||
it('returns true when git command succeeds', function()
|
||||
local mock_system = stub(vim, 'system')
|
||||
mock_system.returns({
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
})
|
||||
|
||||
local result = diff.is_git_available()
|
||||
assert.is_true(result)
|
||||
|
||||
mock_system:revert()
|
||||
end)
|
||||
|
||||
it('returns false when git command fails', function()
|
||||
local mock_system = stub(vim, 'system')
|
||||
mock_system.returns({
|
||||
wait = function()
|
||||
return { code = 1 }
|
||||
end,
|
||||
})
|
||||
|
||||
local result = diff.is_git_available()
|
||||
assert.is_false(result)
|
||||
|
||||
mock_system:revert()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('get_best_backend', function()
|
||||
it('returns preferred backend when available', function()
|
||||
local mock_is_available = stub(diff, 'is_git_available')
|
||||
mock_is_available.returns(true)
|
||||
|
||||
local backend = diff.get_best_backend('git')
|
||||
assert.equals('git', backend.name)
|
||||
|
||||
mock_is_available:revert()
|
||||
end)
|
||||
|
||||
it('falls back to vim when git unavailable', function()
|
||||
local mock_is_available = stub(diff, 'is_git_available')
|
||||
mock_is_available.returns(false)
|
||||
|
||||
local backend = diff.get_best_backend('git')
|
||||
assert.equals('vim', backend.name)
|
||||
|
||||
mock_is_available:revert()
|
||||
end)
|
||||
|
||||
it('defaults to vim backend', function()
|
||||
local backend = diff.get_best_backend()
|
||||
assert.equals('vim', backend.name)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('vim backend', function()
|
||||
it('returns content as-is', function()
|
||||
local backend = diff.get_backend('vim')
|
||||
local result = backend.render('expected', 'actual')
|
||||
|
||||
assert.same({ 'actual' }, result.content)
|
||||
assert.is_nil(result.highlights)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('git backend', function()
|
||||
it('creates temp files for diff', function()
|
||||
local mock_system = stub(vim, 'system')
|
||||
local mock_tempname = stub(vim.fn, 'tempname')
|
||||
local mock_writefile = stub(vim.fn, 'writefile')
|
||||
local mock_delete = stub(vim.fn, 'delete')
|
||||
|
||||
mock_tempname.returns('/tmp/expected', '/tmp/actual')
|
||||
mock_system.returns({
|
||||
wait = function()
|
||||
return { code = 1, stdout = 'diff output' }
|
||||
end,
|
||||
})
|
||||
|
||||
local backend = diff.get_backend('git')
|
||||
backend.render('expected text', 'actual text')
|
||||
|
||||
assert.stub(mock_writefile).was_called(2)
|
||||
assert.stub(mock_delete).was_called(2)
|
||||
|
||||
mock_system:revert()
|
||||
mock_tempname:revert()
|
||||
mock_writefile:revert()
|
||||
mock_delete:revert()
|
||||
end)
|
||||
|
||||
it('returns raw diff output', function()
|
||||
local mock_system = stub(vim, 'system')
|
||||
local mock_tempname = stub(vim.fn, 'tempname')
|
||||
local mock_writefile = stub(vim.fn, 'writefile')
|
||||
local mock_delete = stub(vim.fn, 'delete')
|
||||
|
||||
mock_tempname.returns('/tmp/expected', '/tmp/actual')
|
||||
mock_system.returns({
|
||||
wait = function()
|
||||
return { code = 1, stdout = 'git diff output' }
|
||||
end,
|
||||
})
|
||||
|
||||
local backend = diff.get_backend('git')
|
||||
local result = backend.render('expected', 'actual')
|
||||
|
||||
assert.equals('git diff output', result.raw_diff)
|
||||
|
||||
mock_system:revert()
|
||||
mock_tempname:revert()
|
||||
mock_writefile:revert()
|
||||
mock_delete:revert()
|
||||
end)
|
||||
|
||||
it('handles no differences', function()
|
||||
local mock_system = stub(vim, 'system')
|
||||
local mock_tempname = stub(vim.fn, 'tempname')
|
||||
local mock_writefile = stub(vim.fn, 'writefile')
|
||||
local mock_delete = stub(vim.fn, 'delete')
|
||||
|
||||
mock_tempname.returns('/tmp/expected', '/tmp/actual')
|
||||
mock_system.returns({
|
||||
wait = function()
|
||||
return { code = 0 }
|
||||
end,
|
||||
})
|
||||
|
||||
local backend = diff.get_backend('git')
|
||||
local result = backend.render('same', 'same')
|
||||
|
||||
assert.same({ 'same' }, result.content)
|
||||
assert.same({}, result.highlights)
|
||||
|
||||
mock_system:revert()
|
||||
mock_tempname:revert()
|
||||
mock_writefile:revert()
|
||||
mock_delete:revert()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('render_diff', function()
|
||||
it('uses best available backend', function()
|
||||
local mock_backend = {
|
||||
render = function()
|
||||
return {}
|
||||
end,
|
||||
}
|
||||
local mock_get_best = stub(diff, 'get_best_backend')
|
||||
mock_get_best.returns(mock_backend)
|
||||
|
||||
diff.render_diff('expected', 'actual', 'vim')
|
||||
|
||||
assert.stub(mock_get_best).was_called_with('vim')
|
||||
mock_get_best:revert()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
182
spec/highlight_spec.lua
Normal file
182
spec/highlight_spec.lua
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
describe('cp.highlight', function()
|
||||
local highlight = require('cp.highlight')
|
||||
|
||||
describe('parse_git_diff', function()
|
||||
it('skips git diff headers', function()
|
||||
local diff_output = [[diff --git a/test b/test
|
||||
index 1234567..abcdefg 100644
|
||||
--- a/test
|
||||
+++ b/test
|
||||
@@ -1,3 +1,3 @@
|
||||
hello
|
||||
+world
|
||||
-goodbye]]
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.same({ 'hello', 'world' }, result.content)
|
||||
end)
|
||||
|
||||
it('processes added lines', function()
|
||||
local diff_output = '+hello w{+o+}rld'
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.same({ 'hello world' }, result.content)
|
||||
assert.equals(1, #result.highlights)
|
||||
assert.equals('CpDiffAdded', result.highlights[1].highlight_group)
|
||||
end)
|
||||
|
||||
it('ignores removed lines', function()
|
||||
local diff_output = 'hello\n-removed line\n+kept line'
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.same({ 'hello', 'kept line' }, result.content)
|
||||
end)
|
||||
|
||||
it('handles unchanged lines', function()
|
||||
local diff_output = 'unchanged line\n+added line'
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.same({ 'unchanged line', 'added line' }, result.content)
|
||||
end)
|
||||
|
||||
it('sets correct line numbers', function()
|
||||
local diff_output = '+first {+added+}\n+second {+text+}'
|
||||
local result = highlight.parse_git_diff(diff_output)
|
||||
assert.equals(0, result.highlights[1].line)
|
||||
assert.equals(1, result.highlights[2].line)
|
||||
end)
|
||||
|
||||
it('handles empty diff output', function()
|
||||
local result = highlight.parse_git_diff('')
|
||||
assert.same({}, result.content)
|
||||
assert.same({}, result.highlights)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('apply_highlights', function()
|
||||
it('clears existing highlights', function()
|
||||
local mock_clear = spy.on(vim.api, 'nvim_buf_clear_namespace')
|
||||
local bufnr = 1
|
||||
local namespace = 100
|
||||
|
||||
highlight.apply_highlights(bufnr, {}, namespace)
|
||||
|
||||
assert.spy(mock_clear).was_called_with(bufnr, namespace, 0, -1)
|
||||
mock_clear:revert()
|
||||
end)
|
||||
|
||||
it('applies extmarks with correct positions', function()
|
||||
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
|
||||
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
|
||||
local bufnr = 1
|
||||
local namespace = 100
|
||||
local highlights = {
|
||||
{
|
||||
line = 0,
|
||||
col_start = 5,
|
||||
col_end = 10,
|
||||
highlight_group = 'CpDiffAdded',
|
||||
},
|
||||
}
|
||||
|
||||
highlight.apply_highlights(bufnr, highlights, namespace)
|
||||
|
||||
assert.stub(mock_extmark).was_called_with(bufnr, namespace, 0, 5, {
|
||||
end_col = 10,
|
||||
hl_group = 'CpDiffAdded',
|
||||
priority = 100,
|
||||
})
|
||||
mock_extmark:revert()
|
||||
mock_clear:revert()
|
||||
end)
|
||||
|
||||
it('uses correct highlight groups', function()
|
||||
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
|
||||
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
|
||||
local highlights = {
|
||||
{
|
||||
line = 0,
|
||||
col_start = 0,
|
||||
col_end = 5,
|
||||
highlight_group = 'CpDiffAdded',
|
||||
},
|
||||
}
|
||||
|
||||
highlight.apply_highlights(1, highlights, 100)
|
||||
|
||||
assert.stub(mock_extmark).was_called_with(1, 100, 0, 0, {
|
||||
end_col = 5,
|
||||
hl_group = 'CpDiffAdded',
|
||||
priority = 100,
|
||||
})
|
||||
mock_extmark:revert()
|
||||
mock_clear:revert()
|
||||
end)
|
||||
|
||||
it('handles empty highlights', function()
|
||||
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
|
||||
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
|
||||
|
||||
highlight.apply_highlights(1, {}, 100)
|
||||
|
||||
assert.stub(mock_extmark).was_not_called()
|
||||
mock_extmark:revert()
|
||||
mock_clear:revert()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('create_namespace', function()
|
||||
it('creates unique namespace', function()
|
||||
local mock_create = stub(vim.api, 'nvim_create_namespace')
|
||||
mock_create.returns(42)
|
||||
|
||||
local result = highlight.create_namespace()
|
||||
|
||||
assert.equals(42, result)
|
||||
assert.stub(mock_create).was_called_with('cp_diff_highlights')
|
||||
mock_create:revert()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('parse_and_apply_diff', function()
|
||||
it('parses diff and applies to buffer', function()
|
||||
local mock_set_lines = stub(vim.api, 'nvim_buf_set_lines')
|
||||
local mock_apply = stub(highlight, 'apply_highlights')
|
||||
local bufnr = 1
|
||||
local namespace = 100
|
||||
local diff_output = '+hello {+world+}'
|
||||
|
||||
local result = highlight.parse_and_apply_diff(bufnr, diff_output, namespace)
|
||||
|
||||
assert.same({ 'hello world' }, result)
|
||||
assert.stub(mock_set_lines).was_called_with(bufnr, 0, -1, false, { 'hello world' })
|
||||
assert.stub(mock_apply).was_called()
|
||||
|
||||
mock_set_lines:revert()
|
||||
mock_apply:revert()
|
||||
end)
|
||||
|
||||
it('sets buffer content', function()
|
||||
local mock_set_lines = stub(vim.api, 'nvim_buf_set_lines')
|
||||
local mock_apply = stub(highlight, 'apply_highlights')
|
||||
|
||||
highlight.parse_and_apply_diff(1, '+test line', 100)
|
||||
|
||||
assert.stub(mock_set_lines).was_called_with(1, 0, -1, false, { 'test line' })
|
||||
mock_set_lines:revert()
|
||||
mock_apply:revert()
|
||||
end)
|
||||
|
||||
it('applies highlights', function()
|
||||
local mock_set_lines = stub(vim.api, 'nvim_buf_set_lines')
|
||||
local mock_apply = stub(highlight, 'apply_highlights')
|
||||
|
||||
highlight.parse_and_apply_diff(1, '+hello {+world+}', 100)
|
||||
|
||||
assert.stub(mock_apply).was_called()
|
||||
mock_set_lines:revert()
|
||||
mock_apply:revert()
|
||||
end)
|
||||
|
||||
it('returns content lines', function()
|
||||
local result = highlight.parse_and_apply_diff(1, '+first\n+second', 100)
|
||||
assert.same({ 'first', 'second' }, result)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
@ -211,5 +211,47 @@ describe('cp.snippets', function()
|
|||
|
||||
assert.equals(1, codeforces_count)
|
||||
end)
|
||||
|
||||
it('handles case-insensitive snippet triggers', function()
|
||||
local mixed_case_snippet = {
|
||||
trigger = 'cp.nvim/CodeForces.cpp',
|
||||
body = 'mixed case template',
|
||||
}
|
||||
local upper_case_snippet = {
|
||||
trigger = 'cp.nvim/ATCODER.cpp',
|
||||
body = 'upper case template',
|
||||
}
|
||||
local config = {
|
||||
snippets = { mixed_case_snippet, upper_case_snippet },
|
||||
}
|
||||
|
||||
snippets.setup(config)
|
||||
|
||||
local cpp_snippets = mock_luasnip.added.cpp or {}
|
||||
|
||||
local has_mixed_case = false
|
||||
local has_upper_case = false
|
||||
local default_codeforces_count = 0
|
||||
local default_atcoder_count = 0
|
||||
|
||||
for _, snippet in ipairs(cpp_snippets) do
|
||||
if snippet.trigger == 'cp.nvim/CodeForces.cpp' then
|
||||
has_mixed_case = true
|
||||
assert.equals('mixed case template', snippet.body)
|
||||
elseif snippet.trigger == 'cp.nvim/ATCODER.cpp' then
|
||||
has_upper_case = true
|
||||
assert.equals('upper case template', snippet.body)
|
||||
elseif snippet.trigger == 'cp.nvim/codeforces.cpp' then
|
||||
default_codeforces_count = default_codeforces_count + 1
|
||||
elseif snippet.trigger == 'cp.nvim/atcoder.cpp' then
|
||||
default_atcoder_count = default_atcoder_count + 1
|
||||
end
|
||||
end
|
||||
|
||||
assert.is_true(has_mixed_case)
|
||||
assert.is_true(has_upper_case)
|
||||
assert.equals(0, default_codeforces_count, 'Default codeforces snippet should be overridden')
|
||||
assert.equals(0, default_atcoder_count, 'Default atcoder snippet should be overridden')
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
|
|
|||
169
spec/test_render_spec.lua
Normal file
169
spec/test_render_spec.lua
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
describe('cp.test_render', function()
|
||||
local test_render = require('cp.test_render')
|
||||
|
||||
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)
|
||||
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)
|
||||
assert.equals('WA', result.text)
|
||||
assert.equals('CpTestError', 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)
|
||||
assert.equals('TLE', result.text)
|
||||
assert.equals('CpTestError', 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)
|
||||
assert.equals('TLE', result.text)
|
||||
assert.equals('CpTestError', 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)
|
||||
assert.equals('RTE', result.text)
|
||||
assert.equals('CpTestError', 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)
|
||||
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)
|
||||
assert.equals('...', result.text)
|
||||
assert.equals('CpTestPending', result.highlight_group)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('render_test_list', function()
|
||||
it('renders table with headers and borders', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '5' },
|
||||
{ status = 'fail', code = 1, input = '3' },
|
||||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = test_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)
|
||||
end)
|
||||
|
||||
it('shows current test with > prefix in table', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '' },
|
||||
{ status = 'pass', input = '' },
|
||||
},
|
||||
current_index = 2,
|
||||
}
|
||||
local result = test_render.render_test_list(test_state)
|
||||
local found_current = false
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('│.*>2.*│') then
|
||||
found_current = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(found_current)
|
||||
end)
|
||||
|
||||
it('displays input only for current test', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '5 3' },
|
||||
{ status = 'pass', input = '2 4' },
|
||||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = test_render.render_test_list(test_state)
|
||||
local found_input = false
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('│5 3') then
|
||||
found_input = true
|
||||
break
|
||||
end
|
||||
end
|
||||
assert.is_true(found_input)
|
||||
end)
|
||||
|
||||
it('handles empty test cases', function()
|
||||
local test_state = { test_cases = {}, current_index = 1 }
|
||||
local result = test_render.render_test_list(test_state)
|
||||
assert.equals(3, #result)
|
||||
end)
|
||||
|
||||
it('preserves input line breaks', function()
|
||||
local test_state = {
|
||||
test_cases = {
|
||||
{ status = 'pass', input = '5\n3\n1' },
|
||||
},
|
||||
current_index = 1,
|
||||
}
|
||||
local result = test_render.render_test_list(test_state)
|
||||
local input_lines = {}
|
||||
for _, line in ipairs(result) do
|
||||
if line:match('^│[531]') then
|
||||
table.insert(input_lines, line:match('│([531])'))
|
||||
end
|
||||
end
|
||||
assert.same({ '5', '3', '1' }, input_lines)
|
||||
end)
|
||||
end)
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
assert.equals('123.00ms', result)
|
||||
end)
|
||||
|
||||
it('returns empty for nil test case', function()
|
||||
local result = test_render.render_status_bar(nil)
|
||||
assert.equals('', result)
|
||||
end)
|
||||
end)
|
||||
|
||||
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()
|
||||
|
||||
assert.spy(mock_set_hl).was_called(5)
|
||||
assert.spy(mock_set_hl).was_called_with(0, 'CpTestAC', { fg = '#10b981', bold = true })
|
||||
assert.spy(mock_set_hl).was_called_with(0, 'CpTestError', { fg = '#ef4444', bold = true })
|
||||
|
||||
mock_set_hl:revert()
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
121
uv.lock
generated
121
uv.lock
generated
|
|
@ -24,6 +24,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.3"
|
||||
|
|
@ -100,6 +109,33 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.14"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
|
|
@ -165,6 +201,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.9.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
|
|
@ -183,6 +228,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
|
|
@ -192,6 +246,22 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
{ name = "identify" },
|
||||
{ name = "nodeenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ff/29/7cf5bbc236333876e4b41f56e06857a87937ce4bf91e117a6991a2dbb02a/pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16", size = 193792, upload-time = "2025-08-09T18:56:14.651Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/a5/987a405322d78a73b66e39e4a90e4ef156fd7141bf71df987e50717c321b/pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8", size = 220965, upload-time = "2025-08-09T18:56:13.192Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
|
|
@ -238,6 +308,41 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.5"
|
||||
|
|
@ -278,6 +383,7 @@ dependencies = [
|
|||
[package.dev-dependencies]
|
||||
dev = [
|
||||
{ name = "mypy" },
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-mock" },
|
||||
{ name = "types-beautifulsoup4" },
|
||||
|
|
@ -294,6 +400,7 @@ requires-dist = [
|
|||
[package.metadata.requires-dev]
|
||||
dev = [
|
||||
{ name = "mypy", specifier = ">=1.18.2" },
|
||||
{ name = "pre-commit", specifier = ">=4.3.0" },
|
||||
{ name = "pytest", specifier = ">=8.0.0" },
|
||||
{ name = "pytest-mock", specifier = ">=3.12.0" },
|
||||
{ name = "types-beautifulsoup4", specifier = ">=4.12.0.20250516" },
|
||||
|
|
@ -359,3 +466,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599
|
|||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.34.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" },
|
||||
]
|
||||
|
|
|
|||
6
vim.toml
6
vim.toml
|
|
@ -22,3 +22,9 @@ any = true
|
|||
|
||||
[after_each]
|
||||
any = true
|
||||
|
||||
[spy]
|
||||
any = true
|
||||
|
||||
[stub]
|
||||
any = true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue