feat(panel): color stder
This commit is contained in:
parent
4e880a2d84
commit
21b7765105
5 changed files with 387 additions and 12 deletions
183
lua/cp/ansi.lua
Normal file
183
lua/cp/ansi.lua
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
---@class AnsiParseResult
|
||||
---@field lines string[]
|
||||
---@field highlights table[]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@param raw_output string|table
|
||||
---@return string
|
||||
function M.bytes_to_string(raw_output)
|
||||
if type(raw_output) == 'string' then
|
||||
return raw_output
|
||||
end
|
||||
return table.concat(vim.tbl_map(string.char, raw_output))
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@return AnsiParseResult
|
||||
function M.parse_ansi_text(text)
|
||||
local clean_text = text:gsub('\027%[[%d;]*[a-zA-Z]', '')
|
||||
local lines = vim.split(clean_text, '\n', { plain = true, trimempty = false })
|
||||
|
||||
local highlights = {}
|
||||
local line_num = 0
|
||||
local col_pos = 0
|
||||
local current_style = nil
|
||||
|
||||
local i = 1
|
||||
while i <= #text do
|
||||
local ansi_start, ansi_end, code, cmd = text:find('\027%[([%d;]*)([a-zA-Z])', i)
|
||||
|
||||
if ansi_start then
|
||||
if ansi_start > i then
|
||||
local segment = text:sub(i, ansi_start - 1)
|
||||
if current_style then
|
||||
local start_line = line_num
|
||||
local start_col = col_pos
|
||||
|
||||
for char in segment:gmatch('.') do
|
||||
if char == '\n' then
|
||||
if col_pos > start_col then
|
||||
table.insert(highlights, {
|
||||
line = start_line,
|
||||
col_start = start_col,
|
||||
col_end = col_pos,
|
||||
highlight_group = current_style,
|
||||
})
|
||||
end
|
||||
line_num = line_num + 1
|
||||
start_line = line_num
|
||||
col_pos = 0
|
||||
start_col = 0
|
||||
else
|
||||
col_pos = col_pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if col_pos > start_col then
|
||||
table.insert(highlights, {
|
||||
line = start_line,
|
||||
col_start = start_col,
|
||||
col_end = col_pos,
|
||||
highlight_group = current_style,
|
||||
})
|
||||
end
|
||||
else
|
||||
for char in segment:gmatch('.') do
|
||||
if char == '\n' then
|
||||
line_num = line_num + 1
|
||||
col_pos = 0
|
||||
else
|
||||
col_pos = col_pos + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if cmd == 'm' then
|
||||
local logger = require('cp.log')
|
||||
logger.log('Found color code: "' .. code .. '"')
|
||||
current_style = M.ansi_code_to_highlight(code)
|
||||
logger.log('Mapped to highlight: ' .. (current_style or 'nil'))
|
||||
end
|
||||
i = ansi_end + 1
|
||||
else
|
||||
local segment = text:sub(i)
|
||||
if current_style and segment ~= '' then
|
||||
local start_line = line_num
|
||||
local start_col = col_pos
|
||||
|
||||
for char in segment:gmatch('.') do
|
||||
if char == '\n' then
|
||||
if col_pos > start_col then
|
||||
table.insert(highlights, {
|
||||
line = start_line,
|
||||
col_start = start_col,
|
||||
col_end = col_pos,
|
||||
highlight_group = current_style,
|
||||
})
|
||||
end
|
||||
line_num = line_num + 1
|
||||
start_line = line_num
|
||||
col_pos = 0
|
||||
start_col = 0
|
||||
else
|
||||
col_pos = col_pos + 1
|
||||
end
|
||||
end
|
||||
|
||||
if col_pos > start_col then
|
||||
table.insert(highlights, {
|
||||
line = start_line,
|
||||
col_start = start_col,
|
||||
col_end = col_pos,
|
||||
highlight_group = current_style,
|
||||
})
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
lines = lines,
|
||||
highlights = highlights,
|
||||
}
|
||||
end
|
||||
|
||||
---@param code string
|
||||
---@return string?
|
||||
function M.ansi_code_to_highlight(code)
|
||||
local color_map = {
|
||||
-- Simple colors
|
||||
['31'] = 'CpAnsiRed',
|
||||
['32'] = 'CpAnsiGreen',
|
||||
['33'] = 'CpAnsiYellow',
|
||||
['34'] = 'CpAnsiBlue',
|
||||
['35'] = 'CpAnsiMagenta',
|
||||
['36'] = 'CpAnsiCyan',
|
||||
['37'] = 'CpAnsiWhite',
|
||||
['91'] = 'CpAnsiBrightRed',
|
||||
['92'] = 'CpAnsiBrightGreen',
|
||||
['93'] = 'CpAnsiBrightYellow',
|
||||
-- Bold colors (G++ style)
|
||||
['01;31'] = 'CpAnsiBrightRed', -- bold red
|
||||
['01;32'] = 'CpAnsiBrightGreen', -- bold green
|
||||
['01;33'] = 'CpAnsiBrightYellow', -- bold yellow
|
||||
['01;34'] = 'CpAnsiBrightBlue', -- bold blue
|
||||
['01;35'] = 'CpAnsiBrightMagenta', -- bold magenta
|
||||
['01;36'] = 'CpAnsiBrightCyan', -- bold cyan
|
||||
-- Bold/formatting only
|
||||
['01'] = 'CpAnsiBold', -- bold text
|
||||
['1'] = 'CpAnsiBold', -- bold text (alternate)
|
||||
-- Reset codes
|
||||
['0'] = nil,
|
||||
[''] = nil,
|
||||
}
|
||||
return color_map[code]
|
||||
end
|
||||
|
||||
function M.setup_highlight_groups()
|
||||
local groups = {
|
||||
CpAnsiRed = { fg = vim.g.terminal_color_1 },
|
||||
CpAnsiGreen = { fg = vim.g.terminal_color_2 },
|
||||
CpAnsiYellow = { fg = vim.g.terminal_color_3 },
|
||||
CpAnsiBlue = { fg = vim.g.terminal_color_4 },
|
||||
CpAnsiMagenta = { fg = vim.g.terminal_color_5 },
|
||||
CpAnsiCyan = { fg = vim.g.terminal_color_6 },
|
||||
CpAnsiWhite = { fg = vim.g.terminal_color_7 },
|
||||
CpAnsiBrightRed = { fg = vim.g.terminal_color_9 },
|
||||
CpAnsiBrightGreen = { fg = vim.g.terminal_color_10 },
|
||||
CpAnsiBrightYellow = { fg = vim.g.terminal_color_11 },
|
||||
CpAnsiBrightBlue = { fg = vim.g.terminal_color_12 },
|
||||
CpAnsiBrightMagenta = { fg = vim.g.terminal_color_13 },
|
||||
CpAnsiBrightCyan = { fg = vim.g.terminal_color_14 },
|
||||
CpAnsiBold = { bold = true },
|
||||
}
|
||||
|
||||
for name, opts in pairs(groups) do
|
||||
vim.api.nvim_set_hl(0, name, opts)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
@ -86,9 +86,13 @@ function M.compile_generic(language_config, substitutions)
|
|||
logger.log(('compiling: %s'):format(table.concat(compile_cmd, ' ')))
|
||||
|
||||
local start_time = vim.uv.hrtime()
|
||||
local result = vim.system(compile_cmd, { text = true }):wait()
|
||||
local result = vim.system(compile_cmd, { text = false }):wait()
|
||||
local compile_time = (vim.uv.hrtime() - start_time) / 1000000
|
||||
|
||||
local ansi = require('cp.ansi')
|
||||
result.stdout = ansi.bytes_to_string(result.stdout or '')
|
||||
result.stderr = ansi.bytes_to_string(result.stderr or '')
|
||||
|
||||
if result.code == 0 then
|
||||
logger.log(('compilation successful (%.1fms)'):format(compile_time))
|
||||
else
|
||||
|
|
|
|||
|
|
@ -251,8 +251,25 @@ local function toggle_run_panel(is_debug)
|
|||
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||
vim.api.nvim_set_option_value('readonly', was_readonly, { buf = bufnr })
|
||||
|
||||
local ansi = require('cp.ansi')
|
||||
ansi.setup_highlight_groups()
|
||||
|
||||
vim.api.nvim_buf_clear_namespace(bufnr, test_list_namespace, 0, -1)
|
||||
for _, hl in ipairs(highlights) do
|
||||
local logger = require('cp.log')
|
||||
logger.log(
|
||||
'Applying extmark: buf='
|
||||
.. bufnr
|
||||
.. ' line='
|
||||
.. hl.line
|
||||
.. ' col='
|
||||
.. hl.col_start
|
||||
.. '-'
|
||||
.. hl.col_end
|
||||
.. ' group='
|
||||
.. (hl.highlight_group or 'nil')
|
||||
)
|
||||
|
||||
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,
|
||||
|
|
@ -382,6 +399,7 @@ local function toggle_run_panel(is_debug)
|
|||
|
||||
local expected_content = current_test.expected or ''
|
||||
local actual_content = current_test.actual or '(not run yet)'
|
||||
local actual_highlights = current_test.actual_highlights or {}
|
||||
local is_compilation_failure = current_test.error
|
||||
and current_test.error:match('Compilation failed')
|
||||
local should_show_diff = current_test.status == 'fail'
|
||||
|
|
@ -424,7 +442,7 @@ local function toggle_run_panel(is_debug)
|
|||
else
|
||||
if desired_mode == 'single' then
|
||||
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(current_diff_layout.buffers[1], lines, {})
|
||||
update_buffer_content(current_diff_layout.buffers[1], lines, actual_highlights)
|
||||
elseif desired_mode == 'git' then
|
||||
local diff_backend = require('cp.diff')
|
||||
local backend = diff_backend.get_best_backend('git')
|
||||
|
|
@ -438,13 +456,13 @@ local function toggle_run_panel(is_debug)
|
|||
)
|
||||
else
|
||||
local lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(current_diff_layout.buffers[1], lines, {})
|
||||
update_buffer_content(current_diff_layout.buffers[1], lines, actual_highlights)
|
||||
end
|
||||
else
|
||||
local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true })
|
||||
local actual_lines = vim.split(actual_content, '\n', { plain = true, trimempty = true })
|
||||
update_buffer_content(current_diff_layout.buffers[1], expected_lines, {})
|
||||
update_buffer_content(current_diff_layout.buffers[2], actual_lines, {})
|
||||
update_buffer_content(current_diff_layout.buffers[2], actual_lines, actual_highlights)
|
||||
|
||||
if should_show_diff then
|
||||
vim.api.nvim_set_option_value('diff', true, { win = current_diff_layout.windows[1] })
|
||||
|
|
@ -472,6 +490,9 @@ local function toggle_run_panel(is_debug)
|
|||
|
||||
local test_render = require('cp.test_render')
|
||||
test_render.setup_highlights()
|
||||
|
||||
local ansi = require('cp.ansi')
|
||||
ansi.setup_highlight_groups()
|
||||
local test_state = test_module.get_run_panel_state()
|
||||
local tab_lines, tab_highlights = test_render.render_test_list(test_state)
|
||||
update_buffer_content(test_buffers.tab_buf, tab_lines, tab_highlights)
|
||||
|
|
@ -596,8 +617,8 @@ local function navigate_problem(delta, language)
|
|||
local new_index = current_index + delta
|
||||
|
||||
if new_index < 1 or new_index > #problems then
|
||||
local direction = delta > 0 and 'next' or 'previous'
|
||||
logger.log(('no %s problem available'):format(direction), vim.log.levels.INFO)
|
||||
local msg = delta > 0 and 'at last problem' or 'at first problem'
|
||||
logger.log(msg, vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
---@field expected string
|
||||
---@field status "pending"|"pass"|"fail"|"running"|"timeout"
|
||||
---@field actual string?
|
||||
---@field actual_highlights table[]?
|
||||
---@field time_ms number?
|
||||
---@field error string?
|
||||
---@field selected boolean
|
||||
|
|
@ -216,19 +217,41 @@ local function run_single_test_case(ctx, contest_config, cp_config, test_case)
|
|||
.system(run_cmd, {
|
||||
stdin = stdin_content,
|
||||
timeout = timeout_ms,
|
||||
text = true,
|
||||
text = false,
|
||||
})
|
||||
:wait()
|
||||
local execution_time = (vim.uv.hrtime() - start_time) / 1000000
|
||||
|
||||
local actual_output = (result.stdout or ''):gsub('\n$', '')
|
||||
local stderr_output = (result.stderr or ''):gsub('\n$', '')
|
||||
local ansi = require('cp.ansi')
|
||||
local stdout_str = ansi.bytes_to_string(result.stdout or '')
|
||||
local stderr_str = ansi.bytes_to_string(result.stderr or '')
|
||||
|
||||
local actual_output = stdout_str:gsub('\n$', '')
|
||||
local stderr_output = stderr_str:gsub('\n$', '')
|
||||
|
||||
local actual_highlights = {}
|
||||
|
||||
if stderr_output ~= '' then
|
||||
local stderr_parsed = ansi.parse_ansi_text(stderr_output)
|
||||
local clean_stderr = table.concat(stderr_parsed.lines, '\n')
|
||||
|
||||
local line_offset
|
||||
if actual_output ~= '' then
|
||||
actual_output = actual_output .. '\n\n--- stderr ---\n' .. stderr_output
|
||||
local stdout_lines = vim.split(actual_output, '\n')
|
||||
line_offset = #stdout_lines + 2 -- +1 for empty line, +1 for "--- stderr ---"
|
||||
actual_output = actual_output .. '\n\n--- stderr ---\n' .. clean_stderr
|
||||
else
|
||||
actual_output = '--- stderr ---\n' .. stderr_output
|
||||
line_offset = 1 -- +1 for "--- stderr ---"
|
||||
actual_output = '--- stderr ---\n' .. clean_stderr
|
||||
end
|
||||
|
||||
for _, highlight in ipairs(stderr_parsed.highlights) do
|
||||
table.insert(actual_highlights, {
|
||||
line = highlight.line + line_offset,
|
||||
col_start = highlight.col_start,
|
||||
col_end = highlight.col_end,
|
||||
highlight_group = highlight.highlight_group,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -264,6 +287,7 @@ local function run_single_test_case(ctx, contest_config, cp_config, test_case)
|
|||
return {
|
||||
status = status,
|
||||
actual = actual_output,
|
||||
actual_highlights = actual_highlights,
|
||||
error = result.code ~= 0 and result.stderr or nil,
|
||||
stderr = result.stderr or '',
|
||||
time_ms = execution_time,
|
||||
|
|
@ -317,6 +341,7 @@ function M.run_test_case(ctx, contest_config, cp_config, index)
|
|||
|
||||
test_case.status = result.status
|
||||
test_case.actual = result.actual
|
||||
test_case.actual_highlights = result.actual_highlights
|
||||
test_case.error = result.error
|
||||
test_case.stderr = result.stderr
|
||||
test_case.time_ms = result.time_ms
|
||||
|
|
@ -346,9 +371,44 @@ function M.get_run_panel_state()
|
|||
end
|
||||
|
||||
function M.handle_compilation_failure(compilation_stderr)
|
||||
local ansi = require('cp.ansi')
|
||||
local logger = require('cp.log')
|
||||
local clean_text = 'Compilation failed'
|
||||
local highlights = {}
|
||||
|
||||
if compilation_stderr and compilation_stderr ~= '' then
|
||||
logger.log('Raw compilation stderr length: ' .. #compilation_stderr)
|
||||
logger.log('Has ANSI codes: ' .. tostring(compilation_stderr:find('\027%[[%d;]*m') ~= nil))
|
||||
|
||||
-- Show first 200 chars to see actual ANSI sequences
|
||||
local sample = compilation_stderr:sub(1, 200):gsub('\027', '\\027')
|
||||
logger.log('Stderr sample: ' .. sample)
|
||||
|
||||
local parsed = ansi.parse_ansi_text(compilation_stderr)
|
||||
clean_text = table.concat(parsed.lines, '\n')
|
||||
highlights = parsed.highlights
|
||||
|
||||
logger.log('Parsed highlights count: ' .. #highlights)
|
||||
for i, hl in ipairs(highlights) do
|
||||
logger.log(
|
||||
'Highlight '
|
||||
.. i
|
||||
.. ': line='
|
||||
.. hl.line
|
||||
.. ' col='
|
||||
.. hl.col_start
|
||||
.. '-'
|
||||
.. hl.col_end
|
||||
.. ' group='
|
||||
.. (hl.highlight_group or 'nil')
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
for i, test_case in ipairs(run_panel_state.test_cases) do
|
||||
test_case.status = 'fail'
|
||||
test_case.actual = compilation_stderr or 'Compilation failed'
|
||||
test_case.actual = clean_text
|
||||
test_case.actual_highlights = highlights
|
||||
test_case.error = 'Compilation failed'
|
||||
test_case.stderr = ''
|
||||
test_case.time_ms = 0
|
||||
|
|
|
|||
107
spec/ansi_spec.lua
Normal file
107
spec/ansi_spec.lua
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
describe('ansi parser', function()
|
||||
local ansi = require('cp.ansi')
|
||||
|
||||
describe('bytes_to_string', function()
|
||||
it('returns string as-is', function()
|
||||
local input = 'hello world'
|
||||
assert.equals('hello world', ansi.bytes_to_string(input))
|
||||
end)
|
||||
|
||||
it('converts byte array to string', function()
|
||||
local input = { 104, 101, 108, 108, 111 }
|
||||
assert.equals('hello', ansi.bytes_to_string(input))
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('parse_ansi_text', function()
|
||||
it('strips ansi codes from simple text', function()
|
||||
local input = 'Hello \027[31mworld\027[0m!'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Hello world!', table.concat(result.lines, '\n'))
|
||||
end)
|
||||
|
||||
it('handles text without ansi codes', function()
|
||||
local input = 'Plain text'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('Plain text', table.concat(result.lines, '\n'))
|
||||
assert.equals(0, #result.highlights)
|
||||
end)
|
||||
|
||||
it('creates correct highlight for colored text', function()
|
||||
local input = 'Hello \027[31mworld\027[0m!'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals(1, #result.highlights)
|
||||
local highlight = result.highlights[1]
|
||||
assert.equals(0, highlight.line)
|
||||
assert.equals(6, highlight.col_start)
|
||||
assert.equals(11, highlight.col_end)
|
||||
assert.equals('CpAnsiRed', highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles multiple colors on same line', function()
|
||||
local input = '\027[31mred\027[0m and \027[32mgreen\027[0m'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('red and green', table.concat(result.lines, '\n'))
|
||||
assert.equals(2, #result.highlights)
|
||||
|
||||
local red_highlight = result.highlights[1]
|
||||
assert.equals(0, red_highlight.col_start)
|
||||
assert.equals(3, red_highlight.col_end)
|
||||
assert.equals('CpAnsiRed', red_highlight.highlight_group)
|
||||
|
||||
local green_highlight = result.highlights[2]
|
||||
assert.equals(8, green_highlight.col_start)
|
||||
assert.equals(13, green_highlight.col_end)
|
||||
assert.equals('CpAnsiGreen', green_highlight.highlight_group)
|
||||
end)
|
||||
|
||||
it('handles multiline colored text', function()
|
||||
local input = '\027[31mline1\nline2\027[0m'
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
assert.equals('line1\nline2', table.concat(result.lines, '\n'))
|
||||
assert.equals(2, #result.highlights)
|
||||
|
||||
local line1_highlight = result.highlights[1]
|
||||
assert.equals(0, line1_highlight.line)
|
||||
assert.equals(0, line1_highlight.col_start)
|
||||
assert.equals(5, line1_highlight.col_end)
|
||||
|
||||
local line2_highlight = result.highlights[2]
|
||||
assert.equals(1, line2_highlight.line)
|
||||
assert.equals(0, line2_highlight.col_start)
|
||||
assert.equals(5, line2_highlight.col_end)
|
||||
end)
|
||||
|
||||
it('handles compiler-like output', function()
|
||||
local input =
|
||||
"error.cpp:10:5: \027[1m\027[31merror:\027[0m\027[1m 'undefined' was not declared\027[0m"
|
||||
local result = ansi.parse_ansi_text(input)
|
||||
|
||||
local clean_text = table.concat(result.lines, '\n')
|
||||
assert.is_true(clean_text:find("error.cpp:10:5: error: 'undefined' was not declared") ~= nil)
|
||||
assert.is_false(clean_text:find('\027') ~= nil)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe('ansi_code_to_highlight', function()
|
||||
it('maps standard colors', function()
|
||||
assert.equals('CpAnsiRed', ansi.ansi_code_to_highlight('31'))
|
||||
assert.equals('CpAnsiGreen', ansi.ansi_code_to_highlight('32'))
|
||||
assert.equals('CpAnsiYellow', ansi.ansi_code_to_highlight('33'))
|
||||
end)
|
||||
|
||||
it('handles reset codes', function()
|
||||
assert.is_nil(ansi.ansi_code_to_highlight('0'))
|
||||
assert.is_nil(ansi.ansi_code_to_highlight(''))
|
||||
end)
|
||||
|
||||
it('handles unknown codes', function()
|
||||
assert.is_nil(ansi.ansi_code_to_highlight('99'))
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
Loading…
Add table
Add a link
Reference in a new issue