feat(ansi): better logging and option to disab;e

This commit is contained in:
Barrett Ruth 2025-09-20 14:37:51 -04:00
parent bd81743274
commit f60f6dd5bb
8 changed files with 159 additions and 10 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

@ -192,6 +192,24 @@ function M.setup_highlight_groups()
BrightWhite = vim.g.terminal_color_15,
}
local missing_colors = {}
for color_name, terminal_color in pairs(color_map) do
if terminal_color == nil then
table.insert(missing_colors, color_name)
end
end
if #missing_colors > 0 then
vim.notify(
string.format(
'[cp.nvim] Terminal colors not configured: %s. ANSI colors will not display properly. '
.. 'Set vim.g.terminal_color_* variables or use a colorscheme that provides them.',
table.concat(missing_colors, ', ')
),
vim.log.levels.WARN
)
end
local combinations = {
{ bold = false, italic = false },
{ bold = true, italic = false },
@ -202,7 +220,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

@ -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,32 @@ 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' })
assert.equals('NONE', 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('#ff0000', 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 color parsing must be enabled xor disabled')
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',

65
spec/run_spec.lua Normal file
View file

@ -0,0 +1,65 @@
describe('run module ANSI processing', function()
local run = require('cp.run')
local config = require('cp.config')
describe('ANSI processing modes', function()
it('parses ANSI when ansi config is true', function()
local test_config = config.setup({ run_panel = { ansi = true } })
local result = {
stdout = 'Hello \027[31mworld\027[0m!',
stderr = '',
code = 0,
ok = true,
signal = nil,
timed_out = false,
}
local processed = run.process_test_result(result, 'expected_output', test_config)
assert.equals('Hello world!', processed.actual)
assert.equals(1, #processed.actual_highlights)
assert.equals('CpAnsiRed', processed.actual_highlights[1].highlight_group)
end)
it('strips ANSI when ansi config is false', function()
local test_config = config.setup({ run_panel = { ansi = false } })
local result = {
stdout = 'Hello \027[31mworld\027[0m!',
stderr = '',
code = 0,
ok = true,
signal = nil,
timed_out = false,
}
local processed = run.process_test_result(result, 'expected_output', test_config)
assert.equals('Hello world!', processed.actual)
assert.equals(0, #processed.actual_highlights)
end)
it('handles compilation failure with ansi disabled', function()
local test_config = config.setup({ run_panel = { ansi = false } })
local compilation_output = 'error.cpp:1:1: \027[1m\027[31merror:\027[0m undefined variable'
-- Create mock run panel state
run._test_set_panel_state({
test_cases = {
{ index = 1, input = 'test', expected = 'expected' },
},
})
run.handle_compilation_failure(compilation_output)
local panel_state = run.get_run_panel_state()
local test_case = panel_state.test_cases[1]
assert.equals('error.cpp:1:1: error: undefined variable', test_case.actual)
assert.equals(0, #test_case.actual_highlights)
assert.equals('Compilation failed', test_case.error)
end)
end)
end)