From db85bacd4cb1c7ab38d8c4a9a3f46da30ef12466 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 19 Sep 2025 22:23:01 -0400 Subject: [PATCH] feat(hl): better hl --- README.md | 2 +- doc/cp.txt | 155 +++++++++++++++++--------------------- lua/cp/init.lua | 2 +- lua/cp/test_render.lua | 31 ++++---- spec/test_render_spec.lua | 43 +++++++++-- 5 files changed, 123 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 14b5fe9..87deda1 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ https://github.com/user-attachments/assets/cb142535-fba0-4280-8f11-66ad1ca50ca9 ## Features - 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 - Integrated running and debugging - Enhanced test viewer diff --git a/doc/cp.txt b/doc/cp.txt index 6625e0f..de21cfb 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -72,61 +72,29 @@ Here's an example configuration with lazy.nvim: > 'barrett-ruth/cp.nvim', cmd = 'CP', opts = { - debug = false, - scrapers = { - 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", - }, - }, + contests = {}, + snippets = {}, hooks = { - before_run = function(ctx) vim.cmd.w() end, - before_debug = function(ctx) ... end, - setup_code = function(ctx) - vim.wo.foldmethod = "marker" - vim.wo.foldmarker = "{{{,}}}" - vim.diagnostic.enable(false) - end, + before_run = nil, + before_debug = nil, + setup_code = nil, }, + debug = false, + scrapers = { atcoder = true, codeforces = true, cses = true }, + filename = nil, run_panel = { - diff_mode = "vim", - next_test_key = "", - prev_test_key = "", - toggle_diff_key = "t", + diff_mode = 'vim', + next_test_key = '', + prev_test_key = '', + toggle_diff_key = '', max_output_lines = 50, }, diff = { git = { - command = "git", - args = {"diff", "--no-index", "--word-diff=plain", - "--word-diff-regex=.", "--no-prefix"}, + command = 'git', + args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' }, }, }, - snippets = { ... }, -- LuaSnip snippets - filename = function(contest, contest_id, problem_id, config, language) ... end, } } < @@ -134,16 +102,16 @@ Here's an example configuration with lazy.nvim: > *cp.Config* Fields: ~ - • {contests} (`table`) Contest configurations. - • {hooks} (`cp.Hooks`) Hook functions called at various stages. - • {snippets} (`table[]`) LuaSnip snippet definitions. - • {debug} (`boolean`, default: `false`) Show info messages + - {contests} (`table`) Contest configurations. + - {hooks} (`cp.Hooks`) Hook functions called at various stages. + - {snippets} (`table[]`) LuaSnip snippet definitions. + - {debug} (`boolean`, default: `false`) Show info messages during operation. - • {scrapers} (`table`) Per-platform scraper control. + - {scrapers} (`table`) Per-platform scraper control. Default enables all platforms. - • {run_panel} (`RunPanelConfig`) Test panel behavior configuration. - • {diff} (`DiffConfig`) Diff backend configuration. - • {filename}? (`function`) Custom filename generation function. + - {run_panel} (`RunPanelConfig`) Test panel behavior configuration. + - {diff} (`DiffConfig`) Diff backend configuration. + - {filename}? (`function`) Custom filename generation function. `function(contest, contest_id, problem_id, config, language)` Should return full filename with extension. (default: concats contest_id and problem id) @@ -151,60 +119,60 @@ Here's an example configuration with lazy.nvim: > *cp.ContestConfig* Fields: ~ - • {cpp} (`LanguageConfig`) C++ language configuration. - • {python} (`LanguageConfig`) Python language configuration. - • {default_language} (`string`, default: `"cpp"`) Default language when + - {cpp} (`LanguageConfig`) C++ language configuration. + - {python} (`LanguageConfig`) Python language configuration. + - {default_language} (`string`, default: `"cpp"`) Default language when `--lang` not specified. *cp.LanguageConfig* Fields: ~ - • {compile}? (`string[]`) Compile command template with + - {compile}? (`string[]`) Compile command template with `{version}`, `{source}`, `{binary}` placeholders. - • {test} (`string[]`) Test execution command template. - • {debug}? (`string[]`) Debug compile command template. - • {version}? (`number`) Language version (e.g. 20, 23 for C++). - • {extension} (`string`) File extension (e.g. "cc", "py"). - • {executable}? (`string`) Executable name for interpreted languages. + - {test} (`string[]`) Test execution command template. + - {debug}? (`string[]`) Debug compile command template. + - {version}? (`number`) Language version (e.g. 20, 23 for C++). + - {extension} (`string`) File extension (e.g. "cc", "py"). + - {executable}? (`string`) Executable name for interpreted languages. *cp.RunPanelConfig* Fields: ~ - • {diff_mode} (`string`, default: `"vim"`) Diff backend: "vim" or "git". + - {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: `""`) Key to navigate to next test case. - • {prev_test_key} (`string`, default: `""`) Key to navigate to previous test case. - • {toggle_diff_key} (`string`, default: `"t"`) Key to toggle diff mode between vim and git. - • {max_output_lines} (`number`, default: `50`) Maximum lines of test output to display. + - {next_test_key} (`string`, default: `""`) Key to navigate to next test case. + - {prev_test_key} (`string`, default: `""`) Key to navigate to previous test case. + - {toggle_diff_key} (`string`, default: `""`) Key to toggle diff mode between vim and git. + - {max_output_lines} (`number`, default: `50`) Maximum lines of test output to display. *cp.DiffConfig* Fields: ~ - • {git} (`DiffGitConfig`) Git diff backend configuration. + - {git} (`DiffGitConfig`) Git diff backend configuration. *cp.Hooks* Fields: ~ - • {before_run}? (`function`) Called before test panel opens. + - {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)` - • {setup_code}? (`function`) Called after source file is opened. + - {setup_code}? (`function`) Called after source file is opened. Good for configuring buffer settings. `function(ctx: ProblemContext)` *ProblemContext* Fields: ~ - • {contest} (`string`) Platform name (e.g. "atcoder", "codeforces") - • {contest_id} (`string`) Contest ID (e.g. "abc123", "1933") - • {problem_id}? (`string`) Problem ID (e.g. "a", "b") - nil for CSES - • {source_file} (`string`) Source filename (e.g. "abc123a.cpp") - • {binary_file} (`string`) Binary output path (e.g. "build/abc123a.run") - • {input_file} (`string`) Test input path (e.g. "io/abc123a.cpin") - • {output_file} (`string`) Program output path (e.g. "io/abc123a.cpout") - • {expected_file} (`string`) Expected output path (e.g. "io/abc123a.expected") - • {problem_name} (`string`) Display name (e.g. "abc123a") + - {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* @@ -337,13 +305,28 @@ characters below) > Status Indicators ~ -Test cases use competitive programming terminology: +Test cases use competitive programming terminology with color highlighting: - AC Accepted (passed) - WA Wrong Answer (output mismatch) - TLE Time Limit Exceeded (timeout) - RTE Runtime Error (non-zero exit) + AC Accepted (passed) - Green + WA Wrong Answer (output mismatch) - Red + TLE Time Limit Exceeded (timeout) - Orange + 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 ~ *cp-test-keys* Navigate to next test case (configurable via run_panel.next_test_key) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index cdf5aba..4ffa793 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -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, { end_col = hl.col_end, hl_group = hl.highlight_group, - priority = 100, + priority = 200, }) end end diff --git a/lua/cp/test_render.lua b/lua/cp/test_render.lua index 29a435a..f8ad0a5 100644 --- a/lua/cp/test_render.lua +++ b/lua/cp/test_render.lua @@ -30,14 +30,14 @@ function M.get_status_info(test_case) return { text = 'AC', highlight_group = 'CpTestAC' } elseif test_case.status == 'fail' 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 - return { text = 'RTE', highlight_group = 'CpTestError' } + return { text = 'RTE', highlight_group = 'CpTestRTE' } else - return { text = 'WA', highlight_group = 'CpTestError' } + return { text = 'WA', highlight_group = 'CpTestWA' } end elseif test_case.status == 'timeout' then - return { text = 'TLE', highlight_group = 'CpTestError' } + return { text = 'TLE', highlight_group = 'CpTestTLE' } elseif test_case.status == 'running' then return { text = '...', highlight_group = 'CpTestPending' } else @@ -264,15 +264,14 @@ local function data_row(c, idx, tc, is_current, test_state) local hi if status.text ~= '' then - local content = ' ' .. status.text .. ' ' - local pad = w.status - #content - local status_start_col = 1 + w.num + 1 + pad + 1 - 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, - } + local status_pos = line:find(status.text) + if status_pos then + hi = { + col_start = status_pos - 1, + col_end = status_pos - 1 + #status.text, + highlight_group = status.highlight_group, + } + end end return line, hi @@ -352,8 +351,10 @@ end ---@return table function M.get_highlight_groups() return { - CpTestAC = { fg = '#10b981', bold = true }, - CpTestError = { fg = '#ef4444', bold = true }, + CpTestAC = { fg = '#10b981' }, + CpTestWA = { fg = '#ef4444' }, + CpTestTLE = { fg = '#f59e0b' }, + CpTestRTE = { fg = '#8b5cf6' }, CpTestPending = { fg = '#6b7280' }, CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' }, CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' }, diff --git a/spec/test_render_spec.lua b/spec/test_render_spec.lua index aff3c3b..96719c0 100644 --- a/spec/test_render_spec.lua +++ b/spec/test_render_spec.lua @@ -13,28 +13,28 @@ describe('cp.test_render', 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) + assert.equals('CpTestWA', 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) + assert.equals('CpTestTLE', 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) + assert.equals('CpTestTLE', 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) + assert.equals('CpTestRTE', result.highlight_group) end) 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') 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 }) + assert.spy(mock_set_hl).was_called(7) + assert.spy(mock_set_hl).was_called_with(0, 'CpTestAC', { fg = '#10b981' }) + assert.spy(mock_set_hl).was_called_with(0, '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() 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)