commit
8cf32d5877
5 changed files with 123 additions and 110 deletions
|
|
@ -11,7 +11,7 @@ https://github.com/user-attachments/assets/cb142535-fba0-4280-8f11-66ad1ca50ca9
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Support for multiple online judges ([AtCoder](https://atcoder.jp/), [Codeforces](https://codeforces.com/), [CSES](https://cses.fi))
|
- Support for multiple online judges ([AtCoder](https://atcoder.jp/), [Codeforces](https://codeforces.com/), [CSES](https://cses.fi))
|
||||||
- Multi-language support (C++, Python)
|
- Language-agnostic features
|
||||||
- Automatic problem scraping and test case management
|
- Automatic problem scraping and test case management
|
||||||
- Integrated running and debugging
|
- Integrated running and debugging
|
||||||
- Enhanced test viewer
|
- Enhanced test viewer
|
||||||
|
|
|
||||||
155
doc/cp.txt
155
doc/cp.txt
|
|
@ -72,61 +72,29 @@ Here's an example configuration with lazy.nvim: >
|
||||||
'barrett-ruth/cp.nvim',
|
'barrett-ruth/cp.nvim',
|
||||||
cmd = 'CP',
|
cmd = 'CP',
|
||||||
opts = {
|
opts = {
|
||||||
debug = false,
|
contests = {},
|
||||||
scrapers = {
|
snippets = {},
|
||||||
atcoder = true,
|
|
||||||
codeforces = false,
|
|
||||||
cses = true,
|
|
||||||
},
|
|
||||||
contests = {
|
|
||||||
codeforces = {
|
|
||||||
cpp = {
|
|
||||||
compile = {
|
|
||||||
'g++', '-std=c++{version}', '-O2', '-Wall', '-Wextra',
|
|
||||||
'-DLOCAL', '{source}', '-o', '{binary}',
|
|
||||||
},
|
|
||||||
test = { '{binary}' },
|
|
||||||
debug = {
|
|
||||||
'g++', '-std=c++{version}', '-g3',
|
|
||||||
'-fsanitize=address,undefined', '-DLOCAL',
|
|
||||||
'{source}', '-o', '{binary}',
|
|
||||||
},
|
|
||||||
version = 23,
|
|
||||||
extension = "cc",
|
|
||||||
},
|
|
||||||
python = {
|
|
||||||
test = { 'python3', '{source}' },
|
|
||||||
debug = { 'python3', '{source}' },
|
|
||||||
extension = "py",
|
|
||||||
},
|
|
||||||
default_language = "cpp",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hooks = {
|
hooks = {
|
||||||
before_run = function(ctx) vim.cmd.w() end,
|
before_run = nil,
|
||||||
before_debug = function(ctx) ... end,
|
before_debug = nil,
|
||||||
setup_code = function(ctx)
|
setup_code = nil,
|
||||||
vim.wo.foldmethod = "marker"
|
|
||||||
vim.wo.foldmarker = "{{{,}}}"
|
|
||||||
vim.diagnostic.enable(false)
|
|
||||||
end,
|
|
||||||
},
|
},
|
||||||
|
debug = false,
|
||||||
|
scrapers = { atcoder = true, codeforces = true, cses = true },
|
||||||
|
filename = nil,
|
||||||
run_panel = {
|
run_panel = {
|
||||||
diff_mode = "vim",
|
diff_mode = 'vim',
|
||||||
next_test_key = "<c-n>",
|
next_test_key = '<c-n>',
|
||||||
prev_test_key = "<c-p>",
|
prev_test_key = '<c-p>',
|
||||||
toggle_diff_key = "t",
|
toggle_diff_key = '<c-t>',
|
||||||
max_output_lines = 50,
|
max_output_lines = 50,
|
||||||
},
|
},
|
||||||
diff = {
|
diff = {
|
||||||
git = {
|
git = {
|
||||||
command = "git",
|
command = 'git',
|
||||||
args = {"diff", "--no-index", "--word-diff=plain",
|
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||||
"--word-diff-regex=.", "--no-prefix"},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
snippets = { ... }, -- LuaSnip snippets
|
|
||||||
filename = function(contest, contest_id, problem_id, config, language) ... end,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
|
@ -134,16 +102,16 @@ Here's an example configuration with lazy.nvim: >
|
||||||
*cp.Config*
|
*cp.Config*
|
||||||
|
|
||||||
Fields: ~
|
Fields: ~
|
||||||
• {contests} (`table<string,ContestConfig>`) Contest configurations.
|
- {contests} (`table<string,ContestConfig>`) Contest configurations.
|
||||||
• {hooks} (`cp.Hooks`) Hook functions called at various stages.
|
- {hooks} (`cp.Hooks`) Hook functions called at various stages.
|
||||||
• {snippets} (`table[]`) LuaSnip snippet definitions.
|
- {snippets} (`table[]`) LuaSnip snippet definitions.
|
||||||
• {debug} (`boolean`, default: `false`) Show info messages
|
- {debug} (`boolean`, default: `false`) Show info messages
|
||||||
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.
|
- {run_panel} (`RunPanelConfig`) Test panel behavior configuration.
|
||||||
• {diff} (`DiffConfig`) Diff backend 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.
|
||||||
(default: concats contest_id and problem id)
|
(default: concats contest_id and problem id)
|
||||||
|
|
@ -151,60 +119,60 @@ Here's an example configuration with lazy.nvim: >
|
||||||
*cp.ContestConfig*
|
*cp.ContestConfig*
|
||||||
|
|
||||||
Fields: ~
|
Fields: ~
|
||||||
• {cpp} (`LanguageConfig`) C++ language configuration.
|
- {cpp} (`LanguageConfig`) C++ language configuration.
|
||||||
• {python} (`LanguageConfig`) Python language configuration.
|
- {python} (`LanguageConfig`) Python language configuration.
|
||||||
• {default_language} (`string`, default: `"cpp"`) Default language when
|
- {default_language} (`string`, default: `"cpp"`) Default language when
|
||||||
`--lang` not specified.
|
`--lang` not specified.
|
||||||
|
|
||||||
*cp.LanguageConfig*
|
*cp.LanguageConfig*
|
||||||
|
|
||||||
Fields: ~
|
Fields: ~
|
||||||
• {compile}? (`string[]`) Compile command template with
|
- {compile}? (`string[]`) Compile command template with
|
||||||
`{version}`, `{source}`, `{binary}` placeholders.
|
`{version}`, `{source}`, `{binary}` placeholders.
|
||||||
• {test} (`string[]`) Test execution 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*
|
*cp.RunPanelConfig*
|
||||||
|
|
||||||
Fields: ~
|
Fields: ~
|
||||||
• {diff_mode} (`string`, default: `"vim"`) Diff backend: "vim" or "git".
|
- {diff_mode} (`string`, default: `"vim"`) Diff backend: "vim" or "git".
|
||||||
Git provides character-level precision, vim uses built-in diff.
|
Git provides character-level precision, vim uses built-in diff.
|
||||||
• {next_test_key} (`string`, default: `"<c-n>"`) Key to navigate to next test case.
|
- {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.
|
- {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.
|
- {toggle_diff_key} (`string`, default: `"<c-t>"`) Key to toggle diff mode between vim and git.
|
||||||
• {max_output_lines} (`number`, default: `50`) Maximum lines of test output to display.
|
- {max_output_lines} (`number`, default: `50`) Maximum lines of test output to display.
|
||||||
|
|
||||||
*cp.DiffConfig*
|
*cp.DiffConfig*
|
||||||
|
|
||||||
Fields: ~
|
Fields: ~
|
||||||
• {git} (`DiffGitConfig`) Git diff backend configuration.
|
- {git} (`DiffGitConfig`) Git diff backend configuration.
|
||||||
|
|
||||||
*cp.Hooks*
|
*cp.Hooks*
|
||||||
|
|
||||||
Fields: ~
|
Fields: ~
|
||||||
• {before_run}? (`function`) Called before test panel opens.
|
- {before_run}? (`function`) Called before test panel opens.
|
||||||
`function(ctx: ProblemContext)`
|
`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.
|
||||||
Good for configuring buffer settings.
|
Good for configuring buffer settings.
|
||||||
`function(ctx: ProblemContext)`
|
`function(ctx: ProblemContext)`
|
||||||
|
|
||||||
*ProblemContext*
|
*ProblemContext*
|
||||||
|
|
||||||
Fields: ~
|
Fields: ~
|
||||||
• {contest} (`string`) Platform name (e.g. "atcoder", "codeforces")
|
- {contest} (`string`) Platform name (e.g. "atcoder", "codeforces")
|
||||||
• {contest_id} (`string`) Contest ID (e.g. "abc123", "1933")
|
- {contest_id} (`string`) Contest ID (e.g. "abc123", "1933")
|
||||||
• {problem_id}? (`string`) Problem ID (e.g. "a", "b") - nil for CSES
|
- {problem_id}? (`string`) Problem ID (e.g. "a", "b") - nil for CSES
|
||||||
• {source_file} (`string`) Source filename (e.g. "abc123a.cpp")
|
- {source_file} (`string`) Source filename (e.g. "abc123a.cpp")
|
||||||
• {binary_file} (`string`) Binary output path (e.g. "build/abc123a.run")
|
- {binary_file} (`string`) Binary output path (e.g. "build/abc123a.run")
|
||||||
• {input_file} (`string`) Test input path (e.g. "io/abc123a.cpin")
|
- {input_file} (`string`) Test input path (e.g. "io/abc123a.cpin")
|
||||||
• {output_file} (`string`) Program output path (e.g. "io/abc123a.cpout")
|
- {output_file} (`string`) Program output path (e.g. "io/abc123a.cpout")
|
||||||
• {expected_file} (`string`) Expected output path (e.g. "io/abc123a.expected")
|
- {expected_file} (`string`) Expected output path (e.g. "io/abc123a.expected")
|
||||||
• {problem_name} (`string`) Display name (e.g. "abc123a")
|
- {problem_name} (`string`) Display name (e.g. "abc123a")
|
||||||
|
|
||||||
WORKFLOW *cp-workflow*
|
WORKFLOW *cp-workflow*
|
||||||
|
|
||||||
|
|
@ -337,13 +305,28 @@ characters below) >
|
||||||
|
|
||||||
Status Indicators ~
|
Status Indicators ~
|
||||||
|
|
||||||
Test cases use competitive programming terminology:
|
Test cases use competitive programming terminology with color highlighting:
|
||||||
|
|
||||||
AC Accepted (passed)
|
AC Accepted (passed) - Green
|
||||||
WA Wrong Answer (output mismatch)
|
WA Wrong Answer (output mismatch) - Red
|
||||||
TLE Time Limit Exceeded (timeout)
|
TLE Time Limit Exceeded (timeout) - Orange
|
||||||
RTE Runtime Error (non-zero exit)
|
RTE Runtime Error (non-zero exit) - Purple
|
||||||
|
|
||||||
|
Highlight Groups ~
|
||||||
|
*cp-highlights*
|
||||||
|
cp.nvim defines the following highlight groups for status indicators:
|
||||||
|
|
||||||
|
CpTestAC Green foreground for AC status
|
||||||
|
CpTestWA Red foreground for WA status
|
||||||
|
CpTestTLE Orange foreground for TLE status
|
||||||
|
CpTestRTE Purple foreground for RTE status
|
||||||
|
CpTestPending Gray foreground for pending tests
|
||||||
|
|
||||||
|
You can customize these colors by linking to other highlight groups in your
|
||||||
|
colorscheme or by redefining them: >
|
||||||
|
vim.api.nvim_set_hl(0, 'CpTestAC', { link = 'DiffAdd' })
|
||||||
|
vim.api.nvim_set_hl(0, 'CpTestWA', { fg = '#ff0000' })
|
||||||
|
<
|
||||||
Keymaps ~
|
Keymaps ~
|
||||||
*cp-test-keys*
|
*cp-test-keys*
|
||||||
<c-n> Navigate to next test case (configurable via run_panel.next_test_key)
|
<c-n> Navigate to next test case (configurable via run_panel.next_test_key)
|
||||||
|
|
|
||||||
|
|
@ -239,7 +239,7 @@ local function toggle_run_panel(is_debug)
|
||||||
vim.api.nvim_buf_set_extmark(bufnr, test_list_namespace, hl.line, hl.col_start, {
|
vim.api.nvim_buf_set_extmark(bufnr, test_list_namespace, hl.line, hl.col_start, {
|
||||||
end_col = hl.col_end,
|
end_col = hl.col_end,
|
||||||
hl_group = hl.highlight_group,
|
hl_group = hl.highlight_group,
|
||||||
priority = 100,
|
priority = 200,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,14 @@ function M.get_status_info(test_case)
|
||||||
return { text = 'AC', highlight_group = 'CpTestAC' }
|
return { text = 'AC', highlight_group = 'CpTestAC' }
|
||||||
elseif test_case.status == 'fail' then
|
elseif test_case.status == 'fail' then
|
||||||
if test_case.timed_out then
|
if test_case.timed_out then
|
||||||
return { text = 'TLE', highlight_group = 'CpTestError' }
|
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
||||||
elseif test_case.code and test_case.code >= 128 then
|
elseif test_case.code and test_case.code >= 128 then
|
||||||
return { text = 'RTE', highlight_group = 'CpTestError' }
|
return { text = 'RTE', highlight_group = 'CpTestRTE' }
|
||||||
else
|
else
|
||||||
return { text = 'WA', highlight_group = 'CpTestError' }
|
return { text = 'WA', highlight_group = 'CpTestWA' }
|
||||||
end
|
end
|
||||||
elseif test_case.status == 'timeout' then
|
elseif test_case.status == 'timeout' then
|
||||||
return { text = 'TLE', highlight_group = 'CpTestError' }
|
return { text = 'TLE', highlight_group = 'CpTestTLE' }
|
||||||
elseif test_case.status == 'running' then
|
elseif test_case.status == 'running' then
|
||||||
return { text = '...', highlight_group = 'CpTestPending' }
|
return { text = '...', highlight_group = 'CpTestPending' }
|
||||||
else
|
else
|
||||||
|
|
@ -264,16 +264,15 @@ local function data_row(c, idx, tc, is_current, test_state)
|
||||||
|
|
||||||
local hi
|
local hi
|
||||||
if status.text ~= '' then
|
if status.text ~= '' then
|
||||||
local content = ' ' .. status.text .. ' '
|
local status_pos = line:find(status.text)
|
||||||
local pad = w.status - #content
|
if status_pos then
|
||||||
local status_start_col = 1 + w.num + 1 + pad + 1
|
|
||||||
local status_end_col = status_start_col + #status.text
|
|
||||||
hi = {
|
hi = {
|
||||||
col_start = status_start_col,
|
col_start = status_pos - 1,
|
||||||
col_end = status_end_col,
|
col_end = status_pos - 1 + #status.text,
|
||||||
highlight_group = status.highlight_group,
|
highlight_group = status.highlight_group,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
return line, hi
|
return line, hi
|
||||||
end
|
end
|
||||||
|
|
@ -352,8 +351,10 @@ end
|
||||||
---@return table<string, table>
|
---@return table<string, table>
|
||||||
function M.get_highlight_groups()
|
function M.get_highlight_groups()
|
||||||
return {
|
return {
|
||||||
CpTestAC = { fg = '#10b981', bold = true },
|
CpTestAC = { fg = '#10b981' },
|
||||||
CpTestError = { fg = '#ef4444', bold = true },
|
CpTestWA = { fg = '#ef4444' },
|
||||||
|
CpTestTLE = { fg = '#f59e0b' },
|
||||||
|
CpTestRTE = { fg = '#8b5cf6' },
|
||||||
CpTestPending = { fg = '#6b7280' },
|
CpTestPending = { fg = '#6b7280' },
|
||||||
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },
|
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },
|
||||||
CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' },
|
CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' },
|
||||||
|
|
|
||||||
|
|
@ -13,28 +13,28 @@ describe('cp.test_render', function()
|
||||||
local test_case = { status = 'fail', code = 1 }
|
local test_case = { status = 'fail', code = 1 }
|
||||||
local result = test_render.get_status_info(test_case)
|
local result = test_render.get_status_info(test_case)
|
||||||
assert.equals('WA', result.text)
|
assert.equals('WA', result.text)
|
||||||
assert.equals('CpTestError', result.highlight_group)
|
assert.equals('CpTestWA', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns TLE for timeout status', function()
|
it('returns TLE for timeout status', function()
|
||||||
local test_case = { status = 'timeout' }
|
local test_case = { status = 'timeout' }
|
||||||
local result = test_render.get_status_info(test_case)
|
local result = test_render.get_status_info(test_case)
|
||||||
assert.equals('TLE', result.text)
|
assert.equals('TLE', result.text)
|
||||||
assert.equals('CpTestError', result.highlight_group)
|
assert.equals('CpTestTLE', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns TLE for timed out fail status', function()
|
it('returns TLE for timed out fail status', function()
|
||||||
local test_case = { status = 'fail', timed_out = true }
|
local test_case = { status = 'fail', timed_out = true }
|
||||||
local result = test_render.get_status_info(test_case)
|
local result = test_render.get_status_info(test_case)
|
||||||
assert.equals('TLE', result.text)
|
assert.equals('TLE', result.text)
|
||||||
assert.equals('CpTestError', result.highlight_group)
|
assert.equals('CpTestTLE', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns RTE for fail with signal codes (>= 128)', function()
|
it('returns RTE for fail with signal codes (>= 128)', function()
|
||||||
local test_case = { status = 'fail', code = 139 }
|
local test_case = { status = 'fail', code = 139 }
|
||||||
local result = test_render.get_status_info(test_case)
|
local result = test_render.get_status_info(test_case)
|
||||||
assert.equals('RTE', result.text)
|
assert.equals('RTE', result.text)
|
||||||
assert.equals('CpTestError', result.highlight_group)
|
assert.equals('CpTestRTE', result.highlight_group)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
it('returns empty for pending status', function()
|
it('returns empty for pending status', function()
|
||||||
|
|
@ -159,11 +159,40 @@ describe('cp.test_render', function()
|
||||||
local mock_set_hl = spy.on(vim.api, 'nvim_set_hl')
|
local mock_set_hl = spy.on(vim.api, 'nvim_set_hl')
|
||||||
test_render.setup_highlights()
|
test_render.setup_highlights()
|
||||||
|
|
||||||
assert.spy(mock_set_hl).was_called(5)
|
assert.spy(mock_set_hl).was_called(7)
|
||||||
assert.spy(mock_set_hl).was_called_with(0, 'CpTestAC', { fg = '#10b981', bold = true })
|
assert.spy(mock_set_hl).was_called_with(0, 'CpTestAC', { fg = '#10b981' })
|
||||||
assert.spy(mock_set_hl).was_called_with(0, 'CpTestError', { fg = '#ef4444', bold = true })
|
assert.spy(mock_set_hl).was_called_with(0, 'CpTestWA', { fg = '#ef4444' })
|
||||||
|
assert.spy(mock_set_hl).was_called_with(0, 'CpTestTLE', { fg = '#f59e0b' })
|
||||||
|
assert.spy(mock_set_hl).was_called_with(0, 'CpTestRTE', { fg = '#8b5cf6' })
|
||||||
|
|
||||||
mock_set_hl:revert()
|
mock_set_hl:revert()
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
describe('highlight positioning', function()
|
||||||
|
it('generates correct highlight positions for status text', function()
|
||||||
|
local test_state = {
|
||||||
|
test_cases = {
|
||||||
|
{ status = 'pass', input = '' },
|
||||||
|
{ status = 'fail', code = 1, input = '' },
|
||||||
|
},
|
||||||
|
current_index = 1,
|
||||||
|
}
|
||||||
|
local lines, highlights = test_render.render_test_list(test_state)
|
||||||
|
|
||||||
|
assert.equals(2, #highlights)
|
||||||
|
|
||||||
|
for _, hl in ipairs(highlights) do
|
||||||
|
assert.is_not_nil(hl.line)
|
||||||
|
assert.is_not_nil(hl.col_start)
|
||||||
|
assert.is_not_nil(hl.col_end)
|
||||||
|
assert.is_not_nil(hl.highlight_group)
|
||||||
|
assert.is_true(hl.col_end > hl.col_start)
|
||||||
|
|
||||||
|
local line_content = lines[hl.line + 1]
|
||||||
|
local highlighted_text = line_content:sub(hl.col_start + 1, hl.col_end)
|
||||||
|
assert.is_true(highlighted_text == 'AC' or highlighted_text == 'WA')
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue