diff --git a/doc/cp.txt b/doc/cp.txt index bb0f0f4..f730423 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -148,6 +148,7 @@ Here's an example configuration with lazy.nvim: >lua scrapers = { 'atcoder', 'codeforces', 'cses' }, filename = default_filename, -- + run_panel = { + ansi = true, diff_mode = 'vim', next_test_key = '', prev_test_key = '', @@ -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. diff --git a/lua/cp/ansi.lua b/lua/cp/ansi.lua index d8116f0..51067fe 100644 --- a/lua/cp/ansi.lua +++ b/lua/cp/ansi.lua @@ -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') diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 1a52a5c..a1564fd 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -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 = '', prev_test_key = '', @@ -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) diff --git a/lua/cp/init.lua b/lua/cp/init.lua index 2e3a742..b8c2397 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -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 diff --git a/lua/cp/run.lua b/lua/cp/run.lua index 3303cbd..ddd2346 100644 --- a/lua/cp/run.lua +++ b/lua/cp/run.lua @@ -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' diff --git a/spec/ansi_spec.lua b/spec/ansi_spec.lua index 0b7faf8..6d129b4 100644 --- a/spec/ansi_spec.lua +++ b/spec/ansi_spec.lua @@ -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) diff --git a/spec/config_spec.lua b/spec/config_spec.lua index 7fc049b..1688410 100644 --- a/spec/config_spec.lua +++ b/spec/config_spec.lua @@ -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', diff --git a/spec/run_spec.lua b/spec/run_spec.lua new file mode 100644 index 0000000..8e568be --- /dev/null +++ b/spec/run_spec.lua @@ -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)