Merge pull request #79 from barrett-ruth/fix/qol-logs

Ansi: Option to Disable the parser
This commit is contained in:
Barrett Ruth 2025-09-20 22:57:29 +02:00 committed by GitHub
commit 0c9ae37d74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 337 additions and 11 deletions

View file

@ -148,6 +148,7 @@ Here's an example configuration with lazy.nvim: >lua
scrapers = { 'atcoder', 'codeforces', 'cses' },
filename = default_filename, -- <contest id> + <problem id>
run_panel = {
ansi = true,
diff_mode = 'vim',
next_test_key = '<c-n>',
prev_test_key = '<c-p>',
@ -199,6 +200,12 @@ Here's an example configuration with lazy.nvim: >lua
*cp.RunPanelConfig*
Fields: ~
{ansi} (boolean, default: true) Enable ANSI color parsing and
highlighting. When true, compiler output and test results
display with colored syntax highlighting. When false,
ANSI escape codes are stripped for plain text display.
Requires vim.g.terminal_color_* to be configured for
proper color display.
{diff_mode} (string, default: "vim") Diff backend: "vim" or "git".
Git provides character-level precision, vim uses
built-in diff.

View file

@ -4,6 +4,8 @@
local M = {}
local logger = require('cp.log')
---@param raw_output string|table
---@return string
function M.bytes_to_string(raw_output)
@ -192,6 +194,21 @@ function M.setup_highlight_groups()
BrightWhite = vim.g.terminal_color_15,
}
local missing_color = false
for _, terminal_color in pairs(color_map) do
if terminal_color == nil then
missing_color = true
break
end
end
if missing_color or #color_map == 0 then
logger.log(
'ansi terminal colors (vim.g.terminal_color_*) not configured. . ANSI colors will not display properly. ',
vim.log.levels.WARN
)
end
local combinations = {
{ bold = false, italic = false },
{ bold = true, italic = false },
@ -202,7 +219,7 @@ function M.setup_highlight_groups()
for _, combo in ipairs(combinations) do
for color_name, terminal_color in pairs(color_map) do
local parts = { 'CpAnsi' }
local opts = { fg = terminal_color }
local opts = { fg = terminal_color or 'NONE' }
if combo.bold then
table.insert(parts, 'Bold')

View file

@ -30,6 +30,7 @@
---@field setup_code? fun(ctx: ProblemContext)
---@class RunPanelConfig
---@field ansi boolean Enable ANSI color parsing and highlighting
---@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
@ -97,6 +98,7 @@ M.defaults = {
scrapers = constants.PLATFORMS,
filename = nil,
run_panel = {
ansi = true,
diff_mode = 'vim',
next_test_key = '<c-n>',
prev_test_key = '<c-p>',
@ -186,6 +188,11 @@ function M.setup(user_config)
})
vim.validate({
ansi = {
config.run_panel.ansi,
'boolean',
'ansi color parsing must be enabled xor disabled',
},
diff_mode = {
config.run_panel.diff_mode,
function(value)

View file

@ -8,7 +8,7 @@ local scrape = require('cp.scrape')
local snippets = require('cp.snippets')
if not vim.fn.has('nvim-0.10.0') then
vim.notify('[cp.nvim]: requires nvim-0.10.0+', vim.log.levels.ERROR)
logger.log('[cp.nvim]: requires nvim-0.10.0+', vim.log.levels.ERROR)
return {}
end
@ -537,8 +537,10 @@ local function toggle_run_panel(is_debug)
refresh_run_panel()
vim.schedule(function()
local ansi = require('cp.ansi')
ansi.setup_highlight_groups()
if config.run_panel.ansi then
local ansi = require('cp.ansi')
ansi.setup_highlight_groups()
end
if current_diff_layout then
update_diff_panes()
end

View file

@ -241,9 +241,13 @@ local function run_single_test_case(ctx, contest_config, cp_config, test_case)
local actual_highlights = {}
if actual_output ~= '' then
local parsed = ansi.parse_ansi_text(actual_output)
actual_output = table.concat(parsed.lines, '\n')
actual_highlights = parsed.highlights
if cp_config.run_panel.ansi then
local parsed = ansi.parse_ansi_text(actual_output)
actual_output = table.concat(parsed.lines, '\n')
actual_highlights = parsed.highlights
else
actual_output = actual_output:gsub('\027%[[%d;]*[a-zA-Z]', '')
end
end
local max_lines = cp_config.run_panel.max_output_lines
@ -362,11 +366,18 @@ end
function M.handle_compilation_failure(compilation_output)
local ansi = require('cp.ansi')
local config = require('cp.config').setup()
-- Always parse the compilation output - it contains everything now
local parsed = ansi.parse_ansi_text(compilation_output or '')
local clean_text = table.concat(parsed.lines, '\n')
local highlights = parsed.highlights
local clean_text
local highlights = {}
if config.run_panel.ansi then
local parsed = ansi.parse_ansi_text(compilation_output or '')
clean_text = table.concat(parsed.lines, '\n')
highlights = parsed.highlights
else
clean_text = (compilation_output or ''):gsub('\027%[[%d;]*[a-zA-Z]', '')
end
for _, test_case in ipairs(run_panel_state.test_cases) do
test_case.status = 'fail'

View file

@ -212,4 +212,33 @@ describe('ansi parser', function()
assert.is_nil(state.foreground)
end)
end)
describe('setup_highlight_groups', function()
it('creates highlight groups with fallback colors when terminal colors are nil', function()
local original_colors = {}
for i = 0, 15 do
original_colors[i] = vim.g['terminal_color_' .. i]
vim.g['terminal_color_' .. i] = nil
end
ansi.setup_highlight_groups()
local highlight = vim.api.nvim_get_hl(0, { name = 'CpAnsiRed' })
-- When 'NONE' is set, nvim_get_hl returns nil for that field
assert.is_nil(highlight.fg)
for i = 0, 15 do
vim.g['terminal_color_' .. i] = original_colors[i]
end
end)
it('creates highlight groups with proper colors when terminal colors are set', function()
vim.g.terminal_color_1 = '#ff0000'
ansi.setup_highlight_groups()
local highlight = vim.api.nvim_get_hl(0, { name = 'CpAnsiRed' })
assert.equals(0xff0000, highlight.fg)
end)
end)
end)

View file

@ -81,6 +81,16 @@ describe('cp.config', function()
end)
describe('run_panel config validation', function()
it('validates ansi is boolean', function()
local invalid_config = {
run_panel = { ansi = 'invalid' },
}
assert.has_error(function()
config.setup(invalid_config)
end, 'ansi: expected ansi color parsing must be enabled xor disabled, got string')
end)
it('validates diff_mode values', function()
local invalid_config = {
run_panel = { diff_mode = 'invalid' },
@ -114,6 +124,7 @@ describe('cp.config', function()
it('accepts valid run_panel config', function()
local valid_config = {
run_panel = {
ansi = false,
diff_mode = 'git',
next_test_key = 'j',
prev_test_key = 'k',

215
spec/extmark_spec.lua Normal file
View file

@ -0,0 +1,215 @@
describe('extmarks', function()
local spec_helper = require('spec.spec_helper')
local highlight
before_each(function()
spec_helper.setup()
highlight = require('cp.highlight')
end)
after_each(function()
spec_helper.teardown()
end)
describe('buffer deletion', function()
it('clears namespace on buffer delete', function()
local bufnr = 1
local namespace = 100
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
highlight.apply_highlights(bufnr, {
{
line = 0,
col_start = 0,
col_end = 5,
highlight_group = 'CpDiffAdded',
},
}, namespace)
assert.stub(mock_clear).was_called_with(bufnr, namespace, 0, -1)
mock_clear:revert()
mock_extmark:revert()
end)
it('handles invalid buffer gracefully', function()
local bufnr = 999
local namespace = 100
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
mock_clear.on_call_with(bufnr, namespace, 0, -1).invokes(function()
error('Invalid buffer')
end)
local success = pcall(highlight.apply_highlights, bufnr, {
{
line = 0,
col_start = 0,
col_end = 5,
highlight_group = 'CpDiffAdded',
},
}, namespace)
assert.is_false(success)
mock_clear:revert()
mock_extmark:revert()
end)
end)
describe('namespace isolation', function()
it('creates unique namespaces', function()
local mock_create = stub(vim.api, 'nvim_create_namespace')
mock_create.on_call_with('cp_diff_highlights').returns(100)
mock_create.on_call_with('cp_test_list').returns(200)
mock_create.on_call_with('cp_ansi_highlights').returns(300)
local diff_ns = highlight.create_namespace()
local test_ns = vim.api.nvim_create_namespace('cp_test_list')
local ansi_ns = vim.api.nvim_create_namespace('cp_ansi_highlights')
assert.equals(100, diff_ns)
assert.equals(200, test_ns)
assert.equals(300, ansi_ns)
mock_create:revert()
end)
it('clears specific namespace independently', function()
local bufnr = 1
local ns1 = 100
local ns2 = 200
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
highlight.apply_highlights(bufnr, {
{ line = 0, col_start = 0, col_end = 5, highlight_group = 'CpDiffAdded' },
}, ns1)
highlight.apply_highlights(bufnr, {
{ line = 1, col_start = 0, col_end = 3, highlight_group = 'CpDiffRemoved' },
}, ns2)
assert.stub(mock_clear).was_called_with(bufnr, ns1, 0, -1)
assert.stub(mock_clear).was_called_with(bufnr, ns2, 0, -1)
assert.stub(mock_clear).was_called(2)
mock_clear:revert()
mock_extmark:revert()
end)
end)
describe('multiple updates', function()
it('clears previous extmarks on each update', function()
local bufnr = 1
local namespace = 100
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
highlight.apply_highlights(bufnr, {
{ line = 0, col_start = 0, col_end = 5, highlight_group = 'CpDiffAdded' },
}, namespace)
highlight.apply_highlights(bufnr, {
{ line = 1, col_start = 0, col_end = 3, highlight_group = 'CpDiffRemoved' },
}, namespace)
assert.stub(mock_clear).was_called(2)
assert.stub(mock_clear).was_called_with(bufnr, namespace, 0, -1)
assert.stub(mock_extmark).was_called(2)
mock_clear:revert()
mock_extmark:revert()
end)
it('handles empty highlights', function()
local bufnr = 1
local namespace = 100
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
highlight.apply_highlights(bufnr, {
{ line = 0, col_start = 0, col_end = 5, highlight_group = 'CpDiffAdded' },
}, namespace)
highlight.apply_highlights(bufnr, {}, namespace)
assert.stub(mock_clear).was_called(2)
assert.stub(mock_extmark).was_called(1)
mock_clear:revert()
mock_extmark:revert()
end)
it('skips invalid highlights', function()
local bufnr = 1
local namespace = 100
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
highlight.apply_highlights(bufnr, {
{ line = 0, col_start = 5, col_end = 5, highlight_group = 'CpDiffAdded' },
{ line = 1, col_start = 7, col_end = 3, highlight_group = 'CpDiffAdded' },
{ line = 2, col_start = 0, col_end = 5, highlight_group = 'CpDiffAdded' },
}, namespace)
assert.stub(mock_clear).was_called_with(bufnr, namespace, 0, -1)
assert.stub(mock_extmark).was_called(1)
assert.stub(mock_extmark).was_called_with(bufnr, namespace, 2, 0, {
end_col = 5,
hl_group = 'CpDiffAdded',
priority = 100,
})
mock_clear:revert()
mock_extmark:revert()
end)
end)
describe('error handling', function()
it('fails when clear_namespace fails', function()
local bufnr = 1
local namespace = 100
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
mock_clear.on_call_with(bufnr, namespace, 0, -1).invokes(function()
error('Namespace clear failed')
end)
local success = pcall(highlight.apply_highlights, bufnr, {
{ line = 0, col_start = 0, col_end = 5, highlight_group = 'CpDiffAdded' },
}, namespace)
assert.is_false(success)
assert.stub(mock_extmark).was_not_called()
mock_clear:revert()
mock_extmark:revert()
end)
end)
describe('parse_and_apply_diff cleanup', function()
it('clears namespace before applying parsed diff', function()
local bufnr = 1
local namespace = 100
local mock_clear = stub(vim.api, 'nvim_buf_clear_namespace')
local mock_extmark = stub(vim.api, 'nvim_buf_set_extmark')
local mock_set_lines = stub(vim.api, 'nvim_buf_set_lines')
local mock_get_option = stub(vim.api, 'nvim_get_option_value')
local mock_set_option = stub(vim.api, 'nvim_set_option_value')
mock_get_option.returns(false)
highlight.parse_and_apply_diff(bufnr, '+hello {+world+}', namespace)
assert.stub(mock_clear).was_called_with(bufnr, namespace, 0, -1)
mock_clear:revert()
mock_extmark:revert()
mock_set_lines:revert()
mock_get_option:revert()
mock_set_option:revert()
end)
end)
end)

27
spec/run_spec.lua Normal file
View file

@ -0,0 +1,27 @@
describe('run module', function()
local run = require('cp.run')
describe('basic functionality', function()
it('has required functions', function()
assert.is_function(run.load_test_cases)
assert.is_function(run.run_test_case)
assert.is_function(run.run_all_test_cases)
assert.is_function(run.get_run_panel_state)
assert.is_function(run.handle_compilation_failure)
end)
it('can get panel state', function()
local state = run.get_run_panel_state()
assert.is_table(state)
assert.is_table(state.test_cases)
end)
it('handles compilation failure', function()
local compilation_output = 'error.cpp:1:1: error: undefined variable'
assert.does_not_error(function()
run.handle_compilation_failure(compilation_output)
end)
end)
end)
end)