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
|
- uses: JohnnyMorganz/stylua-action@v4
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
version: latest
|
version: 2.1.0
|
||||||
args: --check .
|
args: --check .
|
||||||
|
|
||||||
lua-lint:
|
lua-lint:
|
||||||
|
|
@ -114,4 +114,4 @@ jobs:
|
||||||
- name: Install dependencies with mypy
|
- name: Install dependencies with mypy
|
||||||
run: uv sync --dev
|
run: uv sync --dev
|
||||||
- name: Type check Python files with mypy
|
- name: Type check Python files with mypy
|
||||||
run: uv run mypy scrapers/ tests/scrapers/
|
run: uv run mypy scrapers/ tests/scrapers/
|
||||||
|
|
|
||||||
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)
|
- [competitest.nvim](https://github.com/xeluxee/competitest.nvim)
|
||||||
- [assistant.nvim](https://github.com/A7Lavinraj/assistant.nvim)
|
- [assistant.nvim](https://github.com/A7Lavinraj/assistant.nvim)
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
- 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 ~
|
Action Commands ~
|
||||||
|
|
||||||
:CP test [--debug] Toggle test panel for individual test case
|
:CP run [--debug] Toggle run panel for individual test case
|
||||||
debugging. Shows per-test results with three-pane
|
debugging. Shows per-test results with redesigned
|
||||||
layout for easy Expected/Actual comparison.
|
layout for efficient comparison.
|
||||||
Use --debug flag to compile with debug flags
|
Use --debug flag to compile with debug flags
|
||||||
Requires contest setup first.
|
Requires contest setup first.
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ CONFIGURATION *cp-config*
|
||||||
|
|
||||||
cp.nvim works out of the box. No setup required.
|
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',
|
'barrett-ruth/cp.nvim',
|
||||||
cmd = 'CP',
|
cmd = 'CP',
|
||||||
|
|
@ -75,7 +75,7 @@ Optional configuration with lazy.nvim: >
|
||||||
debug = false,
|
debug = false,
|
||||||
scrapers = {
|
scrapers = {
|
||||||
atcoder = true,
|
atcoder = true,
|
||||||
codeforces = false, -- disable codeforces scraping
|
codeforces = false,
|
||||||
cses = true,
|
cses = true,
|
||||||
},
|
},
|
||||||
contests = {
|
contests = {
|
||||||
|
|
@ -85,7 +85,7 @@ Optional configuration with lazy.nvim: >
|
||||||
'g++', '-std=c++{version}', '-O2', '-Wall', '-Wextra',
|
'g++', '-std=c++{version}', '-O2', '-Wall', '-Wextra',
|
||||||
'-DLOCAL', '{source}', '-o', '{binary}',
|
'-DLOCAL', '{source}', '-o', '{binary}',
|
||||||
},
|
},
|
||||||
run = { '{binary}' },
|
test = { '{binary}' },
|
||||||
debug = {
|
debug = {
|
||||||
'g++', '-std=c++{version}', '-g3',
|
'g++', '-std=c++{version}', '-g3',
|
||||||
'-fsanitize=address,undefined', '-DLOCAL',
|
'-fsanitize=address,undefined', '-DLOCAL',
|
||||||
|
|
@ -95,7 +95,7 @@ Optional configuration with lazy.nvim: >
|
||||||
extension = "cc",
|
extension = "cc",
|
||||||
},
|
},
|
||||||
python = {
|
python = {
|
||||||
run = { 'python3', '{source}' },
|
test = { 'python3', '{source}' },
|
||||||
debug = { 'python3', '{source}' },
|
debug = { 'python3', '{source}' },
|
||||||
extension = "py",
|
extension = "py",
|
||||||
},
|
},
|
||||||
|
|
@ -105,16 +105,26 @@ Optional configuration with lazy.nvim: >
|
||||||
},
|
},
|
||||||
hooks = {
|
hooks = {
|
||||||
before_run = function(ctx) vim.cmd.w() end,
|
before_run = function(ctx) vim.cmd.w() end,
|
||||||
before_debug = function(ctx)
|
before_debug = function(ctx) ... end,
|
||||||
-- ctx.problem_id, ctx.platform, ctx.source_file, etc.
|
|
||||||
vim.cmd.w()
|
|
||||||
end,
|
|
||||||
setup_code = function(ctx)
|
setup_code = function(ctx)
|
||||||
vim.wo.foldmethod = "marker"
|
vim.wo.foldmethod = "marker"
|
||||||
vim.wo.foldmarker = "{{{,}}}"
|
vim.wo.foldmarker = "{{{,}}}"
|
||||||
vim.diagnostic.enable(false)
|
vim.diagnostic.enable(false)
|
||||||
end,
|
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
|
snippets = { ... }, -- LuaSnip snippets
|
||||||
filename = function(contest, contest_id, problem_id, config, language) ... end,
|
filename = function(contest, contest_id, problem_id, config, language) ... end,
|
||||||
}
|
}
|
||||||
|
|
@ -131,6 +141,8 @@ Optional configuration with lazy.nvim: >
|
||||||
during operation.
|
during operation.
|
||||||
• {scrapers} (`table<string,boolean>`) Per-platform scraper control.
|
• {scrapers} (`table<string,boolean>`) Per-platform scraper control.
|
||||||
Default enables all platforms.
|
Default enables all platforms.
|
||||||
|
• {run_panel} (`RunPanelConfig`) Test panel behavior configuration.
|
||||||
|
• {diff} (`DiffConfig`) Diff backend configuration.
|
||||||
• {filename}? (`function`) Custom filename generation function.
|
• {filename}? (`function`) Custom filename generation function.
|
||||||
`function(contest, contest_id, problem_id, config, language)`
|
`function(contest, contest_id, problem_id, config, language)`
|
||||||
Should return full filename with extension.
|
Should return full filename with extension.
|
||||||
|
|
@ -151,21 +163,50 @@ Optional configuration with lazy.nvim: >
|
||||||
Fields: ~
|
Fields: ~
|
||||||
• {compile}? (`string[]`) Compile command template with
|
• {compile}? (`string[]`) Compile command template with
|
||||||
`{version}`, `{source}`, `{binary}` placeholders.
|
`{version}`, `{source}`, `{binary}` placeholders.
|
||||||
• {run} (`string[]`) Run command template.
|
• {test} (`string[]`) Test execution command template.
|
||||||
• {debug}? (`string[]`) Debug compile command template.
|
• {debug}? (`string[]`) Debug compile command template.
|
||||||
• {version}? (`number`) Language version (e.g. 20, 23 for C++).
|
• {version}? (`number`) Language version (e.g. 20, 23 for C++).
|
||||||
• {extension} (`string`) File extension (e.g. "cc", "py").
|
• {extension} (`string`) File extension (e.g. "cc", "py").
|
||||||
• {executable}? (`string`) Executable name for interpreted languages.
|
• {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*
|
*cp.Hooks*
|
||||||
|
|
||||||
Fields: ~
|
Fields: ~
|
||||||
|
• {before_run}? (`function`) Called before test panel opens.
|
||||||
|
`function(ctx: ProblemContext)`
|
||||||
• {before_debug}? (`function`) Called before debug compilation.
|
• {before_debug}? (`function`) Called before debug compilation.
|
||||||
`function(ctx: ProblemContext)`
|
`function(ctx: ProblemContext)`
|
||||||
• {setup_code}? (`function`) Called after source file is opened.
|
• {setup_code}? (`function`) Called after source file is opened.
|
||||||
Used to configure buffer settings.
|
Good for configuring buffer settings.
|
||||||
`function(ctx: ProblemContext)`
|
`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*
|
WORKFLOW *cp-workflow*
|
||||||
|
|
||||||
For the sake of consistency and simplicity, cp.nvim extracts contest/problem identifiers from
|
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: >
|
3. Start with problem A: >
|
||||||
:CP a
|
:CP a
|
||||||
|
|
||||||
|
Or do both at once with:
|
||||||
|
:CP atcoder abc324 a
|
||||||
|
|
||||||
< This creates a.cc and scrapes test cases
|
< This creates a.cc and scrapes test cases
|
||||||
|
|
||||||
4. Code your solution, then test: >
|
4. Code your solution, then test: >
|
||||||
:CP test
|
:CP run
|
||||||
< Navigate with j/k, run specific tests with <enter>
|
< 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: >
|
5. If needed, debug with sanitizers: >
|
||||||
:CP test --debug
|
:CP run --debug
|
||||||
<
|
<
|
||||||
6. Move to next problem: >
|
6. Move to next problem: >
|
||||||
:CP next
|
:CP next
|
||||||
|
|
@ -248,20 +293,17 @@ Example: Setting up and solving AtCoder contest ABC324
|
||||||
|
|
||||||
6. Continue solving problems with :CP next/:CP prev navigation
|
6. Continue solving problems with :CP next/:CP prev navigation
|
||||||
7. Submit solutions on AtCoder website
|
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
|
The run panel provides individual test case debugging with a streamlined
|
||||||
layout showing test list, expected output, and actual output side-by-side.
|
layout optimized for modern screens. Shows test status with competitive
|
||||||
|
programming terminology and efficient space usage.
|
||||||
|
|
||||||
Activation ~
|
Activation ~
|
||||||
*:CP-test*
|
*:CP-run*
|
||||||
:CP test [--debug] Toggle test panel on/off. When activated,
|
:CP run [--debug] Toggle run panel on/off. When activated,
|
||||||
replaces current layout with test interface.
|
replaces current layout with test interface.
|
||||||
Automatically compiles and runs all tests.
|
Automatically compiles and runs all tests.
|
||||||
Use --debug flag to compile with debug symbols
|
Use --debug flag to compile with debug symbols
|
||||||
|
|
@ -270,29 +312,48 @@ Activation ~
|
||||||
|
|
||||||
Interface ~
|
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) >
|
||||||
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌──────┬────────┬────────┬───────────┐ ┌─ Expected vs Actual ──────────────────┐
|
||||||
│ 1. [ok:true ] [code:0] [time:12ms] │
|
│ # │ Status │ Time │ Exit Code │ │ 45.70ms │ Exit: 0 │
|
||||||
│> 2. [ok:false] [code:0] [time:45ms] │
|
├──────┼────────┼────────┼───────────┤ ├────────────────────────────────────────┤
|
||||||
│ │
|
│ 1 │ AC │12.00ms │ 0 │ │ │
|
||||||
│ Input: │
|
│ >2 │ WA │45.70ms │ 1 │ │ 4[-2-]{+3+} │
|
||||||
│ 5 3 │
|
├──────┴────────┴────────┴───────────┤ │ 100 │
|
||||||
│ │
|
│5 3 │ │ hello w[-o-]r{+o+}ld │
|
||||||
└─────────────────────────────────────────────────────────────┘
|
├──────┬────────┬────────┬───────────┤ │ │
|
||||||
┌─ Expected ──────────────────┐ ┌───── Actual ────────────────┐
|
│ 3 │ AC │ 9.00ms │ 0 │ └────────────────────────────────────────┘
|
||||||
│ 8 │ │ 7 │
|
│ 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 ~
|
Keymaps ~
|
||||||
*cp-test-keys*
|
*cp-test-keys*
|
||||||
<c-n> Navigate to next test case
|
<c-n> Navigate to next test case (configurable via run_panel.next_test_key)
|
||||||
<c-p> Navigate to previous test case
|
<c-p> Navigate to previous test case (configurable via run_panel.prev_test_key)
|
||||||
q Exit test panel (restore layout)
|
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 ~
|
Execution Details ~
|
||||||
|
|
||||||
|
|
@ -309,8 +370,8 @@ cp.nvim creates the following file structure upon problem setup:
|
||||||
build/
|
build/
|
||||||
{problem_id}.run " Compiled binary
|
{problem_id}.run " Compiled binary
|
||||||
io/
|
io/
|
||||||
{problem_id}.cpin " Test input
|
{problem_id}.n.cpin " nth test input
|
||||||
{problem_id}.cpout " Program output
|
{problem_id}.n.cpout " nth program output
|
||||||
{problem_id}.expected " Expected output
|
{problem_id}.expected " Expected output
|
||||||
|
|
||||||
The plugin automatically manages this structure and navigation between problems
|
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
|
cp.nvim integrates with LuaSnip for automatic template expansion. Built-in
|
||||||
snippets include basic C++ and Python templates for each contest type.
|
snippets include basic C++ and Python templates for each contest type.
|
||||||
|
|
||||||
Snippet trigger names must EXACTLY match platform names ("codeforces" for
|
Snippet trigger names must match the following format exactly:
|
||||||
CodeForces, "cses" for CSES, etc.).
|
|
||||||
|
cp.nvim/{platform}
|
||||||
|
|
||||||
Custom snippets can be added via the `snippets` configuration field.
|
Custom snippets can be added via the `snippets` configuration field.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
---@class LanguageConfig
|
---@class LanguageConfig
|
||||||
---@field compile? string[] Compile command template
|
---@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 debug? string[] Debug command template
|
||||||
---@field executable? string Executable name
|
---@field executable? string Executable name
|
||||||
---@field version? number Language version
|
---@field version? number Language version
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
|
|
||||||
---@class PartialLanguageConfig
|
---@class PartialLanguageConfig
|
||||||
---@field compile? string[] Compile command template
|
---@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 debug? string[] Debug command template
|
||||||
---@field executable? string Executable name
|
---@field executable? string Executable name
|
||||||
---@field version? number Language version
|
---@field version? number Language version
|
||||||
|
|
@ -31,6 +31,19 @@
|
||||||
---@field before_debug? fun(ctx: ProblemContext)
|
---@field before_debug? fun(ctx: ProblemContext)
|
||||||
---@field setup_code? 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
|
---@class cp.Config
|
||||||
---@field contests table<string, ContestConfig>
|
---@field contests table<string, ContestConfig>
|
||||||
---@field snippets table[]
|
---@field snippets table[]
|
||||||
|
|
@ -38,6 +51,8 @@
|
||||||
---@field debug boolean
|
---@field debug boolean
|
||||||
---@field scrapers table<string, boolean>
|
---@field scrapers table<string, boolean>
|
||||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
---@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
|
---@class cp.UserConfig
|
||||||
---@field contests? table<string, PartialContestConfig>
|
---@field contests? table<string, PartialContestConfig>
|
||||||
|
|
@ -46,6 +61,8 @@
|
||||||
---@field debug? boolean
|
---@field debug? boolean
|
||||||
---@field scrapers? table<string, boolean>
|
---@field scrapers? table<string, boolean>
|
||||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
---@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 M = {}
|
||||||
local constants = require('cp.constants')
|
local constants = require('cp.constants')
|
||||||
|
|
@ -62,6 +79,18 @@ M.defaults = {
|
||||||
debug = false,
|
debug = false,
|
||||||
scrapers = constants.PLATFORMS,
|
scrapers = constants.PLATFORMS,
|
||||||
filename = nil,
|
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
|
---@param user_config cp.UserConfig|nil
|
||||||
|
|
@ -79,28 +108,10 @@ function M.setup(user_config)
|
||||||
debug = { user_config.debug, { 'boolean', 'nil' }, true },
|
debug = { user_config.debug, { 'boolean', 'nil' }, true },
|
||||||
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
|
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
|
||||||
filename = { user_config.filename, { 'function', '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
|
if user_config.contests then
|
||||||
for contest_name, contest_config in pairs(user_config.contests) do
|
for contest_name, contest_config in pairs(user_config.contests) do
|
||||||
for lang_name, lang_config in pairs(contest_config) 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 {})
|
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 _, contest_config in pairs(config.contests) do
|
||||||
for lang_name, lang_config in pairs(contest_config) do
|
for lang_name, lang_config in pairs(contest_config) do
|
||||||
if type(lang_config) == 'table' and not lang_config.extension then
|
if type(lang_config) == 'table' and not lang_config.extension then
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
|
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
|
||||||
M.ACTIONS = { 'test', 'next', 'prev' }
|
M.ACTIONS = { 'run', 'next', 'prev' }
|
||||||
|
|
||||||
M.CPP = 'cpp'
|
M.CPP = 'cpp'
|
||||||
M.PYTHON = 'python'
|
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'
|
input_data = table.concat(vim.fn.readfile(ctx.input_file), '\n') .. '\n'
|
||||||
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 exec_result = execute_command(run_cmd, input_data, contest_config.timeout_ms)
|
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 formatted_output = format_output(exec_result, ctx.expected_file, is_debug)
|
||||||
|
|
||||||
local output_buf = vim.fn.bufnr(ctx.output_file)
|
local output_buf = vim.fn.bufnr(ctx.output_file)
|
||||||
if output_buf ~= -1 then
|
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_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.api.nvim_buf_call(output_buf, function()
|
||||||
vim.cmd.write()
|
vim.cmd.write()
|
||||||
end)
|
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,
|
saved_session = nil,
|
||||||
test_cases = nil,
|
test_cases = nil,
|
||||||
test_states = {},
|
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 constants = require('cp.constants')
|
||||||
local platforms = constants.PLATFORMS
|
local platforms = constants.PLATFORMS
|
||||||
local actions = constants.ACTIONS
|
local actions = constants.ACTIONS
|
||||||
|
|
@ -149,14 +152,32 @@ local function get_current_problem()
|
||||||
return filename
|
return filename
|
||||||
end
|
end
|
||||||
|
|
||||||
local function toggle_test_panel(is_debug)
|
local function create_buffer_with_options(filetype)
|
||||||
if state.test_panel_active then
|
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
|
if state.saved_session then
|
||||||
vim.cmd(('source %s'):format(state.saved_session))
|
vim.cmd(('source %s'):format(state.saved_session))
|
||||||
vim.fn.delete(state.saved_session)
|
vim.fn.delete(state.saved_session)
|
||||||
state.saved_session = nil
|
state.saved_session = nil
|
||||||
end
|
end
|
||||||
state.test_panel_active = false
|
state.run_panel_active = false
|
||||||
logger.log('test panel closed')
|
logger.log('test panel closed')
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -187,167 +208,224 @@ local function toggle_test_panel(is_debug)
|
||||||
|
|
||||||
vim.cmd('silent only')
|
vim.cmd('silent only')
|
||||||
|
|
||||||
local tab_buf = vim.api.nvim_create_buf(false, true)
|
local tab_buf = create_buffer_with_options()
|
||||||
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 main_win = vim.api.nvim_get_current_win()
|
local main_win = vim.api.nvim_get_current_win()
|
||||||
vim.api.nvim_win_set_buf(main_win, tab_buf)
|
vim.api.nvim_win_set_buf(main_win, tab_buf)
|
||||||
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = 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 = {
|
local test_windows = {
|
||||||
tab_win = main_win,
|
tab_win = main_win,
|
||||||
actual_win = actual_win,
|
|
||||||
expected_win = expected_win,
|
|
||||||
}
|
}
|
||||||
local test_buffers = {
|
local test_buffers = {
|
||||||
tab_buf = tab_buf,
|
tab_buf = tab_buf,
|
||||||
expected_buf = expected_buf,
|
|
||||||
actual_buf = actual_buf,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
local function render_test_tabs()
|
local highlight = require('cp.highlight')
|
||||||
local test_state = test_module.get_test_panel_state()
|
local diff_namespace = highlight.create_namespace()
|
||||||
local tab_lines = {}
|
|
||||||
|
|
||||||
local max_status_width = 0
|
local test_list_namespace = vim.api.nvim_create_namespace('cp_test_list')
|
||||||
local max_code_width = 0
|
|
||||||
local max_time_width = 0
|
|
||||||
|
|
||||||
for _, test_case in ipairs(test_state.test_cases) do
|
local function update_buffer_content(bufnr, lines, highlights)
|
||||||
local status_text = test_case.status == 'pending' and '' or string.upper(test_case.status)
|
local was_readonly = vim.api.nvim_get_option_value('readonly', { buf = bufnr })
|
||||||
max_status_width = math.max(max_status_width, #status_text)
|
|
||||||
|
|
||||||
if test_case.code then
|
vim.api.nvim_set_option_value('readonly', false, { buf = bufnr })
|
||||||
max_code_width = math.max(max_code_width, #tostring(test_case.code))
|
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||||
end
|
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
|
vim.api.nvim_buf_clear_namespace(bufnr, test_list_namespace, 0, -1)
|
||||||
local time_text = string.format('%.0fms', test_case.time_ms)
|
for _, hl in ipairs(highlights) do
|
||||||
max_time_width = math.max(max_time_width, #time_text)
|
vim.api.nvim_buf_set_extmark(bufnr, test_list_namespace, hl.line, hl.col_start, {
|
||||||
end
|
end_col = hl.col_end,
|
||||||
end
|
hl_group = hl.highlight_group,
|
||||||
|
priority = 100,
|
||||||
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 })
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function update_actual_pane()
|
local function create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||||
local test_state = test_module.get_test_panel_state()
|
local expected_buf = create_buffer_with_options()
|
||||||
local current_test = test_state.test_cases[test_state.current_index]
|
local actual_buf = create_buffer_with_options()
|
||||||
|
|
||||||
if not current_test then
|
vim.api.nvim_set_current_win(parent_win)
|
||||||
return
|
vim.cmd.split()
|
||||||
end
|
local actual_win = vim.api.nvim_get_current_win()
|
||||||
|
vim.api.nvim_win_set_buf(actual_win, actual_buf)
|
||||||
|
|
||||||
local actual_lines = {}
|
vim.cmd.vsplit()
|
||||||
local enable_diff = false
|
local expected_win = vim.api.nvim_get_current_win()
|
||||||
|
vim.api.nvim_win_set_buf(expected_win, expected_buf)
|
||||||
|
|
||||||
if current_test.actual then
|
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = expected_buf })
|
||||||
actual_lines = vim.split(current_test.actual, '\n', { plain = true, trimempty = true })
|
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = actual_buf })
|
||||||
enable_diff = current_test.status == 'fail'
|
|
||||||
|
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
|
else
|
||||||
actual_lines = { '(not run yet)' }
|
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||||
|
update_buffer_content(diff_buf, lines, {})
|
||||||
end
|
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
|
local function create_diff_layout(mode, parent_win, expected_content, actual_content)
|
||||||
vim.api.nvim_set_option_value('winbar', 'Actual', { win = test_windows.actual_win })
|
if mode == 'git' then
|
||||||
end
|
return create_git_diff_layout(parent_win, expected_content, actual_content)
|
||||||
|
else
|
||||||
vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.expected_win })
|
return create_vim_diff_layout(parent_win, expected_content, actual_content)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
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
|
if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
local tab_lines = render_test_tabs()
|
local test_render = require('cp.test_render')
|
||||||
vim.api.nvim_buf_set_lines(test_buffers.tab_buf, 0, -1, false, tab_lines)
|
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_diff_panes()
|
||||||
update_actual_pane()
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function navigate_test_case(delta)
|
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
|
if #test_state.test_cases == 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -359,20 +437,30 @@ local function toggle_test_panel(is_debug)
|
||||||
test_state.current_index = 1
|
test_state.current_index = 1
|
||||||
end
|
end
|
||||||
|
|
||||||
refresh_test_panel()
|
refresh_run_panel()
|
||||||
end
|
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)
|
navigate_test_case(1)
|
||||||
end, { buffer = test_buffers.tab_buf, silent = true })
|
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)
|
navigate_test_case(-1)
|
||||||
end, { buffer = test_buffers.tab_buf, silent = true })
|
end, { buffer = test_buffers.tab_buf, silent = true })
|
||||||
|
|
||||||
for _, buf in pairs(test_buffers) do
|
setup_keybindings_for_buffer(test_buffers.tab_buf)
|
||||||
vim.keymap.set('n', 'q', function()
|
|
||||||
toggle_test_panel()
|
if config.hooks and config.hooks.before_run then
|
||||||
end, { buffer = buf, silent = true })
|
config.hooks.before_run(ctx)
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_debug and config.hooks and config.hooks.before_debug then
|
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)
|
test_module.run_all_test_cases(ctx, contest_config)
|
||||||
end
|
end
|
||||||
|
|
||||||
refresh_test_panel()
|
refresh_run_panel()
|
||||||
|
|
||||||
vim.api.nvim_set_current_win(test_windows.tab_win)
|
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_buffers = test_buffers
|
||||||
state.test_windows = test_windows
|
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))
|
logger.log(string.format('test panel opened (%d test cases)', #test_state.test_cases))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -559,8 +647,8 @@ function M.handle_command(opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
if cmd.type == 'action' then
|
if cmd.type == 'action' then
|
||||||
if cmd.action == 'test' then
|
if cmd.action == 'run' then
|
||||||
toggle_test_panel(cmd.debug)
|
toggle_run_panel(cmd.debug)
|
||||||
elseif cmd.action == 'next' then
|
elseif cmd.action == 'next' then
|
||||||
navigate_problem(1, cmd.language)
|
navigate_problem(1, cmd.language)
|
||||||
elseif cmd.action == 'prev' then
|
elseif cmd.action == 'prev' then
|
||||||
|
|
|
||||||
|
|
@ -102,7 +102,7 @@ if __name__ == "__main__":
|
||||||
|
|
||||||
local user_overrides = {}
|
local user_overrides = {}
|
||||||
for _, snippet in ipairs(config.snippets or {}) do
|
for _, snippet in ipairs(config.snippets or {}) do
|
||||||
user_overrides[snippet.trigger] = snippet
|
user_overrides[snippet.trigger:lower()] = snippet
|
||||||
end
|
end
|
||||||
|
|
||||||
for language, template_set in pairs(template_definitions) do
|
for language, template_set in pairs(template_definitions) do
|
||||||
|
|
@ -110,14 +110,14 @@ if __name__ == "__main__":
|
||||||
local filetype = constants.canonical_filetypes[language]
|
local filetype = constants.canonical_filetypes[language]
|
||||||
|
|
||||||
for contest, template in pairs(template_set) do
|
for contest, template in pairs(template_set) do
|
||||||
local prefixed_trigger = ('cp.nvim/%s.%s'):format(contest, language)
|
local prefixed_trigger = ('cp.nvim/%s.%s'):format(contest:lower(), language)
|
||||||
if not user_overrides[prefixed_trigger] then
|
if not user_overrides[prefixed_trigger:lower()] then
|
||||||
table.insert(snippets, s(prefixed_trigger, fmt(template, { i(1) })))
|
table.insert(snippets, s(prefixed_trigger, fmt(template, { i(1) })))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
for trigger, snippet in pairs(user_overrides) do
|
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
|
if prefix_match == language then
|
||||||
table.insert(snippets, snippet)
|
table.insert(snippets, snippet)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
---@field signal string?
|
---@field signal string?
|
||||||
---@field timed_out boolean?
|
---@field timed_out boolean?
|
||||||
|
|
||||||
---@class TestPanelState
|
---@class RunPanelState
|
||||||
---@field test_cases TestCase[]
|
---@field test_cases TestCase[]
|
||||||
---@field current_index number
|
---@field current_index number
|
||||||
---@field buffer number?
|
---@field buffer number?
|
||||||
|
|
@ -24,8 +24,8 @@ local M = {}
|
||||||
local constants = require('cp.constants')
|
local constants = require('cp.constants')
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
|
|
||||||
---@type TestPanelState
|
---@type RunPanelState
|
||||||
local test_panel_state = {
|
local run_panel_state = {
|
||||||
test_cases = {},
|
test_cases = {},
|
||||||
current_index = 1,
|
current_index = 1,
|
||||||
buffer = nil,
|
buffer = nil,
|
||||||
|
|
@ -172,7 +172,7 @@ local function run_single_test_case(ctx, contest_config, test_case)
|
||||||
end
|
end
|
||||||
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'
|
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)
|
test_cases = parse_test_cases_from_files(ctx.input_file, ctx.expected_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
test_panel_state.test_cases = test_cases
|
run_panel_state.test_cases = test_cases
|
||||||
test_panel_state.current_index = 1
|
run_panel_state.current_index = 1
|
||||||
|
|
||||||
logger.log(('loaded %d test case(s)'):format(#test_cases))
|
logger.log(('loaded %d test case(s)'):format(#test_cases))
|
||||||
return #test_cases > 0
|
return #test_cases > 0
|
||||||
|
|
@ -239,7 +239,7 @@ end
|
||||||
---@param index number
|
---@param index number
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function M.run_test_case(ctx, contest_config, index)
|
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
|
if not test_case then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
@ -266,16 +266,16 @@ end
|
||||||
---@return TestCase[]
|
---@return TestCase[]
|
||||||
function M.run_all_test_cases(ctx, contest_config)
|
function M.run_all_test_cases(ctx, contest_config)
|
||||||
local results = {}
|
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)
|
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
|
end
|
||||||
return results
|
return results
|
||||||
end
|
end
|
||||||
|
|
||||||
---@return TestPanelState
|
---@return RunPanelState
|
||||||
function M.get_test_panel_state()
|
function M.get_run_panel_state()
|
||||||
return test_panel_state
|
return run_panel_state
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
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",
|
"types-requests>=2.32.4.20250913",
|
||||||
"pytest>=8.0.0",
|
"pytest>=8.0.0",
|
||||||
"pytest-mock>=3.12.0",
|
"pytest-mock>=3.12.0",
|
||||||
|
"pre-commit>=4.3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
std = "vim"
|
std = 'vim'
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ describe('cp command parsing', function()
|
||||||
|
|
||||||
describe('action commands', function()
|
describe('action commands', function()
|
||||||
it('handles test action without error', function()
|
it('handles test action without error', function()
|
||||||
local opts = { fargs = { 'test' } }
|
local opts = { fargs = { 'run' } }
|
||||||
|
|
||||||
assert.has_no_errors(function()
|
assert.has_no_errors(function()
|
||||||
cp.handle_command(opts)
|
cp.handle_command(opts)
|
||||||
|
|
@ -126,7 +126,7 @@ describe('cp command parsing', function()
|
||||||
|
|
||||||
describe('language flag parsing', function()
|
describe('language flag parsing', function()
|
||||||
it('logs error for --lang flag missing value', function()
|
it('logs error for --lang flag missing value', function()
|
||||||
local opts = { fargs = { 'test', '--lang' } }
|
local opts = { fargs = { 'run', '--lang' } }
|
||||||
|
|
||||||
cp.handle_command(opts)
|
cp.handle_command(opts)
|
||||||
|
|
||||||
|
|
@ -169,7 +169,7 @@ describe('cp command parsing', function()
|
||||||
|
|
||||||
describe('debug flag parsing', function()
|
describe('debug flag parsing', function()
|
||||||
it('handles debug flag without error', function()
|
it('handles debug flag without error', function()
|
||||||
local opts = { fargs = { 'test', '--debug' } }
|
local opts = { fargs = { 'run', '--debug' } }
|
||||||
|
|
||||||
assert.has_no_errors(function()
|
assert.has_no_errors(function()
|
||||||
cp.handle_command(opts)
|
cp.handle_command(opts)
|
||||||
|
|
@ -177,7 +177,7 @@ describe('cp command parsing', function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles combined language and debug flags', function()
|
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()
|
assert.has_no_errors(function()
|
||||||
cp.handle_command(opts)
|
cp.handle_command(opts)
|
||||||
|
|
@ -234,7 +234,7 @@ describe('cp command parsing', function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles flag order variations', function()
|
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()
|
assert.has_no_errors(function()
|
||||||
cp.handle_command(opts)
|
cp.handle_command(opts)
|
||||||
|
|
@ -242,7 +242,7 @@ describe('cp command parsing', function()
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('handles multiple language flags', function()
|
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()
|
assert.has_no_errors(function()
|
||||||
cp.handle_command(opts)
|
cp.handle_command(opts)
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,52 @@ describe('cp.config', function()
|
||||||
config.setup(invalid_config)
|
config.setup(invalid_config)
|
||||||
end)
|
end)
|
||||||
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)
|
end)
|
||||||
|
|
||||||
describe('default_filename', function()
|
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)
|
assert.equals(1, codeforces_count)
|
||||||
end)
|
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)
|
||||||
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "charset-normalizer"
|
name = "charset-normalizer"
|
||||||
version = "3.4.3"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.10"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "25.0"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.6.0"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "pygments"
|
name = "pygments"
|
||||||
version = "2.19.2"
|
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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
|
|
@ -278,6 +383,7 @@ dependencies = [
|
||||||
[package.dev-dependencies]
|
[package.dev-dependencies]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "mypy" },
|
{ name = "mypy" },
|
||||||
|
{ name = "pre-commit" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-mock" },
|
{ name = "pytest-mock" },
|
||||||
{ name = "types-beautifulsoup4" },
|
{ name = "types-beautifulsoup4" },
|
||||||
|
|
@ -294,6 +400,7 @@ requires-dist = [
|
||||||
[package.metadata.requires-dev]
|
[package.metadata.requires-dev]
|
||||||
dev = [
|
dev = [
|
||||||
{ name = "mypy", specifier = ">=1.18.2" },
|
{ name = "mypy", specifier = ">=1.18.2" },
|
||||||
|
{ name = "pre-commit", specifier = ">=4.3.0" },
|
||||||
{ name = "pytest", specifier = ">=8.0.0" },
|
{ name = "pytest", specifier = ">=8.0.0" },
|
||||||
{ name = "pytest-mock", specifier = ">=3.12.0" },
|
{ name = "pytest-mock", specifier = ">=3.12.0" },
|
||||||
{ name = "types-beautifulsoup4", specifier = ">=4.12.0.20250516" },
|
{ name = "types-beautifulsoup4", specifier = ">=4.12.0.20250516" },
|
||||||
|
|
@ -359,3 +466,17 @@ sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599
|
||||||
wheels = [
|
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" },
|
{ 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]
|
[after_each]
|
||||||
any = true
|
any = true
|
||||||
|
|
||||||
|
[spy]
|
||||||
|
any = true
|
||||||
|
|
||||||
|
[stub]
|
||||||
|
any = true
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue