feat(hl): better hl

This commit is contained in:
Barrett Ruth 2025-09-19 22:23:01 -04:00
parent 93be3b0dc9
commit db85bacd4c
5 changed files with 123 additions and 110 deletions

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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,15 +264,14 @@ 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 hi = {
local status_end_col = status_start_col + #status.text col_start = status_pos - 1,
hi = { col_end = status_pos - 1 + #status.text,
col_start = status_start_col, highlight_group = status.highlight_group,
col_end = status_end_col, }
highlight_group = status.highlight_group, end
}
end end
return line, hi return line, hi
@ -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' },

View file

@ -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)