feat(test): test panel
This commit is contained in:
parent
8ae7fff12b
commit
289e6efe62
5 changed files with 504 additions and 65 deletions
|
|
@ -31,6 +31,22 @@
|
||||||
---@field before_debug? fun(ctx: ProblemContext)
|
---@field before_debug? fun(ctx: ProblemContext)
|
||||||
---@field setup_code? fun(ctx: ProblemContext)
|
---@field setup_code? fun(ctx: ProblemContext)
|
||||||
|
|
||||||
|
---@class TestPanelConfig
|
||||||
|
---@field diff_mode "vim"|"git" Diff backend to use
|
||||||
|
---@field toggle_key string Key to toggle test panel
|
||||||
|
---@field status_format "compact"|"verbose" Status display format
|
||||||
|
|
||||||
|
---@class DiffGitConfig
|
||||||
|
---@field command string Git executable name
|
||||||
|
---@field args string[] Additional git diff arguments
|
||||||
|
|
||||||
|
---@class DiffVimConfig
|
||||||
|
---@field enable_diffthis boolean Enable vim's diffthis
|
||||||
|
|
||||||
|
---@class DiffConfig
|
||||||
|
---@field git DiffGitConfig
|
||||||
|
---@field vim DiffVimConfig
|
||||||
|
|
||||||
---@class cp.Config
|
---@class cp.Config
|
||||||
---@field contests table<string, ContestConfig>
|
---@field contests table<string, ContestConfig>
|
||||||
---@field snippets table[]
|
---@field snippets table[]
|
||||||
|
|
@ -38,6 +54,8 @@
|
||||||
---@field debug boolean
|
---@field debug boolean
|
||||||
---@field scrapers table<string, boolean>
|
---@field scrapers table<string, boolean>
|
||||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||||
|
---@field test_panel TestPanelConfig
|
||||||
|
---@field diff DiffConfig
|
||||||
|
|
||||||
---@class cp.UserConfig
|
---@class cp.UserConfig
|
||||||
---@field contests? table<string, PartialContestConfig>
|
---@field contests? table<string, PartialContestConfig>
|
||||||
|
|
@ -46,6 +64,8 @@
|
||||||
---@field debug? boolean
|
---@field debug? boolean
|
||||||
---@field scrapers? table<string, boolean>
|
---@field scrapers? table<string, boolean>
|
||||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||||
|
---@field test_panel? TestPanelConfig
|
||||||
|
---@field diff? DiffConfig
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
local constants = require('cp.constants')
|
local constants = require('cp.constants')
|
||||||
|
|
@ -62,6 +82,20 @@ M.defaults = {
|
||||||
debug = false,
|
debug = false,
|
||||||
scrapers = constants.PLATFORMS,
|
scrapers = constants.PLATFORMS,
|
||||||
filename = nil,
|
filename = nil,
|
||||||
|
test_panel = {
|
||||||
|
diff_mode = "vim",
|
||||||
|
toggle_key = "t",
|
||||||
|
status_format = "compact",
|
||||||
|
},
|
||||||
|
diff = {
|
||||||
|
git = {
|
||||||
|
command = "git",
|
||||||
|
args = {"diff", "--no-index", "--word-diff=plain", "--word-diff-regex=.", "--no-prefix"},
|
||||||
|
},
|
||||||
|
vim = {
|
||||||
|
enable_diffthis = true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
---@param user_config cp.UserConfig|nil
|
---@param user_config cp.UserConfig|nil
|
||||||
|
|
@ -79,6 +113,8 @@ function M.setup(user_config)
|
||||||
debug = { user_config.debug, { 'boolean', 'nil' }, true },
|
debug = { user_config.debug, { 'boolean', 'nil' }, true },
|
||||||
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
|
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
|
||||||
filename = { user_config.filename, { 'function', 'nil' }, true },
|
filename = { user_config.filename, { 'function', 'nil' }, true },
|
||||||
|
test_panel = { user_config.test_panel, { 'table', 'nil' }, true },
|
||||||
|
diff = { user_config.diff, { 'table', 'nil' }, true },
|
||||||
})
|
})
|
||||||
|
|
||||||
if user_config.hooks then
|
if user_config.hooks then
|
||||||
|
|
@ -101,6 +137,33 @@ function M.setup(user_config)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if user_config.test_panel then
|
||||||
|
vim.validate({
|
||||||
|
diff_mode = {
|
||||||
|
user_config.test_panel.diff_mode,
|
||||||
|
function(value)
|
||||||
|
return vim.tbl_contains({"vim", "git"}, value)
|
||||||
|
end,
|
||||||
|
"diff_mode must be 'vim' or 'git'",
|
||||||
|
},
|
||||||
|
toggle_key = { user_config.test_panel.toggle_key, 'string', true },
|
||||||
|
status_format = {
|
||||||
|
user_config.test_panel.status_format,
|
||||||
|
function(value)
|
||||||
|
return vim.tbl_contains({"compact", "verbose"}, value)
|
||||||
|
end,
|
||||||
|
"status_format must be 'compact' or 'verbose'",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
if user_config.diff then
|
||||||
|
vim.validate({
|
||||||
|
git = { user_config.diff.git, { 'table', 'nil' }, true },
|
||||||
|
vim = { user_config.diff.vim, { 'table', 'nil' }, true },
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
if user_config.contests then
|
if user_config.contests then
|
||||||
for contest_name, contest_config in pairs(user_config.contests) do
|
for contest_name, contest_config in pairs(user_config.contests) do
|
||||||
for lang_name, lang_config in pairs(contest_config) do
|
for lang_name, lang_config in pairs(contest_config) do
|
||||||
|
|
|
||||||
150
lua/cp/diff.lua
Normal file
150
lua/cp/diff.lua
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
---@class DiffResult
|
||||||
|
---@field content string[]
|
||||||
|
---@field highlights table[]?
|
||||||
|
|
||||||
|
---@class DiffBackend
|
||||||
|
---@field name string
|
||||||
|
---@field render fun(expected: string, actual: string, mode: string?): DiffResult
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---Vim's built-in diff backend using diffthis
|
||||||
|
---@type DiffBackend
|
||||||
|
local vim_backend = {
|
||||||
|
name = 'vim',
|
||||||
|
render = function(expected, actual, mode)
|
||||||
|
-- For vim backend, we return the content as-is since diffthis handles highlighting
|
||||||
|
local expected_lines = vim.split(expected, '\n', { plain = true, trimempty = true })
|
||||||
|
local actual_lines = vim.split(actual, '\n', { plain = true, trimempty = true })
|
||||||
|
|
||||||
|
return {
|
||||||
|
content = actual_lines,
|
||||||
|
highlights = nil -- diffthis handles highlighting
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
---Git word-diff backend for character-level precision
|
||||||
|
---@type DiffBackend
|
||||||
|
local git_backend = {
|
||||||
|
name = 'git',
|
||||||
|
render = function(expected, actual, mode)
|
||||||
|
-- Create temporary files for git diff
|
||||||
|
local tmp_expected = vim.fn.tempname()
|
||||||
|
local tmp_actual = vim.fn.tempname()
|
||||||
|
|
||||||
|
vim.fn.writefile(vim.split(expected, '\n', { plain = true }), tmp_expected)
|
||||||
|
vim.fn.writefile(vim.split(actual, '\n', { plain = true }), tmp_actual)
|
||||||
|
|
||||||
|
local cmd = {
|
||||||
|
'git', 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.',
|
||||||
|
'--no-prefix', tmp_expected, tmp_actual
|
||||||
|
}
|
||||||
|
|
||||||
|
local result = vim.system(cmd, { text = true }):wait()
|
||||||
|
|
||||||
|
-- Clean up temp files
|
||||||
|
vim.fn.delete(tmp_expected)
|
||||||
|
vim.fn.delete(tmp_actual)
|
||||||
|
|
||||||
|
if result.code == 0 then
|
||||||
|
-- No differences, return actual content as-is
|
||||||
|
return {
|
||||||
|
content = vim.split(actual, '\n', { plain = true, trimempty = true }),
|
||||||
|
highlights = {}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
-- Parse git diff output
|
||||||
|
local lines = vim.split(result.stdout or '', '\n', { plain = true })
|
||||||
|
local content_lines = {}
|
||||||
|
local highlights = {}
|
||||||
|
|
||||||
|
-- Skip git diff header lines (start with @@, +++, ---, etc.)
|
||||||
|
local content_started = false
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
if content_started or (not line:match('^@@') and not line:match('^%+%+%+') and not line:match('^%-%-%-') and not line:match('^index')) then
|
||||||
|
content_started = true
|
||||||
|
if line:match('^[^%-+]') or line:match('^%+') then
|
||||||
|
-- Skip lines starting with - (removed lines) for the actual pane
|
||||||
|
-- Only show lines that are unchanged or added
|
||||||
|
if not line:match('^%-') then
|
||||||
|
local clean_line = line:gsub('^%+', '') -- Remove + prefix
|
||||||
|
table.insert(content_lines, clean_line)
|
||||||
|
|
||||||
|
-- Parse highlights will be handled in highlight.lua
|
||||||
|
table.insert(highlights, {
|
||||||
|
line = #content_lines,
|
||||||
|
content = clean_line
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
content = content_lines,
|
||||||
|
highlights = highlights
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
---Available diff backends
|
||||||
|
---@type table<string, DiffBackend>
|
||||||
|
local backends = {
|
||||||
|
vim = vim_backend,
|
||||||
|
git = git_backend,
|
||||||
|
}
|
||||||
|
|
||||||
|
---Get available backend names
|
||||||
|
---@return string[]
|
||||||
|
function M.get_available_backends()
|
||||||
|
return vim.tbl_keys(backends)
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get a diff backend by name
|
||||||
|
---@param name string
|
||||||
|
---@return DiffBackend?
|
||||||
|
function M.get_backend(name)
|
||||||
|
return backends[name]
|
||||||
|
end
|
||||||
|
|
||||||
|
---Check if git backend is available
|
||||||
|
---@return boolean
|
||||||
|
function M.is_git_available()
|
||||||
|
local result = vim.system({'git', '--version'}, { text = true }):wait()
|
||||||
|
return result.code == 0
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get the best available backend based on config and system availability
|
||||||
|
---@param preferred_backend? string
|
||||||
|
---@return DiffBackend
|
||||||
|
function M.get_best_backend(preferred_backend)
|
||||||
|
if preferred_backend and backends[preferred_backend] then
|
||||||
|
if preferred_backend == 'git' and not M.is_git_available() then
|
||||||
|
-- Fall back to vim if git is not available
|
||||||
|
return backends.vim
|
||||||
|
end
|
||||||
|
return backends[preferred_backend]
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Default to git if available, otherwise vim
|
||||||
|
if M.is_git_available() then
|
||||||
|
return backends.git
|
||||||
|
else
|
||||||
|
return backends.vim
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Render diff using specified backend
|
||||||
|
---@param expected string
|
||||||
|
---@param actual string
|
||||||
|
---@param backend_name? string
|
||||||
|
---@param mode? string
|
||||||
|
---@return DiffResult
|
||||||
|
function M.render_diff(expected, actual, backend_name, mode)
|
||||||
|
local backend = M.get_best_backend(backend_name)
|
||||||
|
return backend.render(expected, actual, mode)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
164
lua/cp/highlight.lua
Normal file
164
lua/cp/highlight.lua
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
---@class DiffHighlight
|
||||||
|
---@field line number
|
||||||
|
---@field col_start number
|
||||||
|
---@field col_end number
|
||||||
|
---@field highlight_group string
|
||||||
|
|
||||||
|
---@class ParsedDiff
|
||||||
|
---@field content string[]
|
||||||
|
---@field highlights DiffHighlight[]
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---Parse git diff markers and extract highlight information
|
||||||
|
---@param text string Raw git diff output line
|
||||||
|
---@return string cleaned_text, DiffHighlight[]
|
||||||
|
local function parse_diff_line(text)
|
||||||
|
local highlights = {}
|
||||||
|
local cleaned_text = text
|
||||||
|
local offset = 0
|
||||||
|
|
||||||
|
-- Pattern for removed text: [-removed text-]
|
||||||
|
for removed_text in text:gmatch('%[%-(.-)%-%]') do
|
||||||
|
local start_pos = text:find('%[%-' .. vim.pesc(removed_text) .. '%-%]', 1, false)
|
||||||
|
if start_pos then
|
||||||
|
-- Remove the marker and adjust positions
|
||||||
|
local marker_len = #('[-%-%]') + #removed_text
|
||||||
|
cleaned_text = cleaned_text:gsub('%[%-' .. vim.pesc(removed_text) .. '%-%]', '', 1)
|
||||||
|
|
||||||
|
-- Since we're removing text, we don't add highlights for removed content in the actual pane
|
||||||
|
-- This is handled by showing removed content in the expected pane
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Reset for added text parsing on the cleaned text
|
||||||
|
local final_text = cleaned_text
|
||||||
|
local final_highlights = {}
|
||||||
|
offset = 0
|
||||||
|
|
||||||
|
-- Pattern for added text: {+added text+}
|
||||||
|
for added_text in cleaned_text:gmatch('{%+(.-)%+}') do
|
||||||
|
local start_pos = final_text:find('{%+' .. vim.pesc(added_text) .. '%+}', 1, false)
|
||||||
|
if start_pos then
|
||||||
|
-- Calculate position after previous highlights
|
||||||
|
local highlight_start = start_pos - offset - 1 -- 0-based for extmarks
|
||||||
|
local highlight_end = highlight_start + #added_text
|
||||||
|
|
||||||
|
table.insert(final_highlights, {
|
||||||
|
line = 0, -- Will be set by caller
|
||||||
|
col_start = highlight_start,
|
||||||
|
col_end = highlight_end,
|
||||||
|
highlight_group = 'CpDiffAdded'
|
||||||
|
})
|
||||||
|
|
||||||
|
-- Remove the marker
|
||||||
|
local marker_len = #{'{+'} + #{'+}'} + #added_text
|
||||||
|
final_text = final_text:gsub('{%+' .. vim.pesc(added_text) .. '%+}', added_text, 1)
|
||||||
|
offset = offset + #{'{+'} + #{'+}'}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return final_text, final_highlights
|
||||||
|
end
|
||||||
|
|
||||||
|
---Parse complete git diff output
|
||||||
|
---@param diff_output string
|
||||||
|
---@return ParsedDiff
|
||||||
|
function M.parse_git_diff(diff_output)
|
||||||
|
local lines = vim.split(diff_output, '\n', { plain = true })
|
||||||
|
local content_lines = {}
|
||||||
|
local all_highlights = {}
|
||||||
|
|
||||||
|
-- Skip git diff header lines
|
||||||
|
local content_started = false
|
||||||
|
for _, line in ipairs(lines) do
|
||||||
|
-- Skip header lines (@@, +++, ---, index, etc.)
|
||||||
|
if content_started or (
|
||||||
|
not line:match('^@@') and
|
||||||
|
not line:match('^%+%+%+') and
|
||||||
|
not line:match('^%-%-%-') and
|
||||||
|
not line:match('^index') and
|
||||||
|
not line:match('^diff %-%-git')
|
||||||
|
) then
|
||||||
|
content_started = true
|
||||||
|
|
||||||
|
-- Process content lines
|
||||||
|
if line:match('^%+') then
|
||||||
|
-- Added line - remove + prefix and parse highlights
|
||||||
|
local clean_line = line:sub(2) -- Remove + prefix
|
||||||
|
local parsed_line, line_highlights = parse_diff_line(clean_line)
|
||||||
|
|
||||||
|
table.insert(content_lines, parsed_line)
|
||||||
|
|
||||||
|
-- Set line numbers for highlights
|
||||||
|
local line_num = #content_lines
|
||||||
|
for _, highlight in ipairs(line_highlights) do
|
||||||
|
highlight.line = line_num - 1 -- 0-based for extmarks
|
||||||
|
table.insert(all_highlights, highlight)
|
||||||
|
end
|
||||||
|
elseif line:match('^%-') then
|
||||||
|
-- Removed line - we handle this in the expected pane, skip for actual
|
||||||
|
elseif not line:match('^\\') then -- Skip "\ No newline" messages
|
||||||
|
-- Unchanged line
|
||||||
|
local parsed_line, line_highlights = parse_diff_line(line)
|
||||||
|
table.insert(content_lines, parsed_line)
|
||||||
|
|
||||||
|
-- Set line numbers for any highlights (shouldn't be any for unchanged lines)
|
||||||
|
local line_num = #content_lines
|
||||||
|
for _, highlight in ipairs(line_highlights) do
|
||||||
|
highlight.line = line_num - 1 -- 0-based for extmarks
|
||||||
|
table.insert(all_highlights, highlight)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return {
|
||||||
|
content = content_lines,
|
||||||
|
highlights = all_highlights
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Apply highlights to a buffer using extmarks
|
||||||
|
---@param bufnr number
|
||||||
|
---@param highlights DiffHighlight[]
|
||||||
|
---@param namespace number
|
||||||
|
function M.apply_highlights(bufnr, highlights, namespace)
|
||||||
|
-- Clear existing highlights in this namespace
|
||||||
|
vim.api.nvim_buf_clear_namespace(bufnr, namespace, 0, -1)
|
||||||
|
|
||||||
|
for _, highlight in ipairs(highlights) do
|
||||||
|
if highlight.col_start < highlight.col_end then
|
||||||
|
vim.api.nvim_buf_set_extmark(bufnr, namespace, highlight.line, highlight.col_start, {
|
||||||
|
end_col = highlight.col_end,
|
||||||
|
hl_group = highlight.highlight_group,
|
||||||
|
priority = 100,
|
||||||
|
})
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create namespace for diff highlights
|
||||||
|
---@return number
|
||||||
|
function M.create_namespace()
|
||||||
|
return vim.api.nvim_create_namespace('cp_diff_highlights')
|
||||||
|
end
|
||||||
|
|
||||||
|
---Parse and apply git diff to buffer
|
||||||
|
---@param bufnr number
|
||||||
|
---@param diff_output string
|
||||||
|
---@param namespace number
|
||||||
|
---@return string[] content_lines
|
||||||
|
function M.parse_and_apply_diff(bufnr, diff_output, namespace)
|
||||||
|
local parsed = M.parse_git_diff(diff_output)
|
||||||
|
|
||||||
|
-- Set buffer content
|
||||||
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, parsed.content)
|
||||||
|
|
||||||
|
-- Apply highlights
|
||||||
|
M.apply_highlights(bufnr, parsed.highlights, namespace)
|
||||||
|
|
||||||
|
return parsed.content
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
|
|
@ -191,9 +191,13 @@ local function toggle_test_panel(is_debug)
|
||||||
local expected_buf = vim.api.nvim_create_buf(false, true)
|
local expected_buf = vim.api.nvim_create_buf(false, true)
|
||||||
local actual_buf = vim.api.nvim_create_buf(false, true)
|
local actual_buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = tab_buf })
|
-- Set buffer options
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = expected_buf })
|
local buffer_opts = { 'bufhidden', 'wipe' }
|
||||||
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = actual_buf })
|
for _, buf in ipairs({tab_buf, expected_buf, actual_buf}) do
|
||||||
|
vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf })
|
||||||
|
vim.api.nvim_set_option_value('readonly', true, { buf = buf })
|
||||||
|
vim.api.nvim_set_option_value('modifiable', false, { buf = buf })
|
||||||
|
end
|
||||||
|
|
||||||
local main_win = vim.api.nvim_get_current_win()
|
local main_win = vim.api.nvim_get_current_win()
|
||||||
vim.api.nvim_win_set_buf(main_win, tab_buf)
|
vim.api.nvim_win_set_buf(main_win, tab_buf)
|
||||||
|
|
@ -222,61 +226,16 @@ local function toggle_test_panel(is_debug)
|
||||||
}
|
}
|
||||||
|
|
||||||
local function render_test_tabs()
|
local function render_test_tabs()
|
||||||
|
local test_render = require('cp.test_render')
|
||||||
|
test_render.setup_highlights()
|
||||||
local test_state = test_module.get_test_panel_state()
|
local test_state = test_module.get_test_panel_state()
|
||||||
local tab_lines = {}
|
return test_render.render_test_list(test_state, config.test_panel)
|
||||||
|
end
|
||||||
|
|
||||||
local max_status_width = 0
|
local function update_buffer_content(bufnr, lines)
|
||||||
local max_code_width = 0
|
vim.api.nvim_set_option_value('modifiable', true, { buf = bufnr })
|
||||||
local max_time_width = 0
|
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
|
||||||
|
vim.api.nvim_set_option_value('modifiable', false, { buf = bufnr })
|
||||||
for _, test_case in ipairs(test_state.test_cases) do
|
|
||||||
local status_text = test_case.status == 'pending' and '' or string.upper(test_case.status)
|
|
||||||
max_status_width = math.max(max_status_width, #status_text)
|
|
||||||
|
|
||||||
if test_case.code then
|
|
||||||
max_code_width = math.max(max_code_width, #tostring(test_case.code))
|
|
||||||
end
|
|
||||||
|
|
||||||
if test_case.time_ms then
|
|
||||||
local time_text = string.format('%.0fms', test_case.time_ms)
|
|
||||||
max_time_width = math.max(max_time_width, #time_text)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
for i, test_case in ipairs(test_state.test_cases) do
|
|
||||||
local prefix = i == test_state.current_index and '> ' or ' '
|
|
||||||
local tab = string.format('%s%d.', prefix, i)
|
|
||||||
|
|
||||||
if test_case.ok ~= nil then
|
|
||||||
tab = tab .. string.format(' [ok:%-5s]', tostring(test_case.ok))
|
|
||||||
end
|
|
||||||
|
|
||||||
if test_case.code then
|
|
||||||
tab = tab .. string.format(' [code:%-' .. max_code_width .. 's]', tostring(test_case.code))
|
|
||||||
end
|
|
||||||
|
|
||||||
if test_case.time_ms then
|
|
||||||
local time_text = string.format('%.0fms', test_case.time_ms)
|
|
||||||
tab = tab .. string.format(' [time:%-' .. max_time_width .. 's]', time_text)
|
|
||||||
end
|
|
||||||
|
|
||||||
if test_case.signal then
|
|
||||||
tab = tab .. string.format(' [%s]', test_case.signal)
|
|
||||||
end
|
|
||||||
|
|
||||||
table.insert(tab_lines, tab)
|
|
||||||
end
|
|
||||||
|
|
||||||
local current_test = test_state.test_cases[test_state.current_index]
|
|
||||||
if current_test then
|
|
||||||
table.insert(tab_lines, '')
|
|
||||||
table.insert(tab_lines, 'Input:')
|
|
||||||
for _, line in ipairs(vim.split(current_test.input, '\n', { plain = true, trimempty = true })) do
|
|
||||||
table.insert(tab_lines, line)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return tab_lines
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function update_expected_pane()
|
local function update_expected_pane()
|
||||||
|
|
@ -290,11 +249,7 @@ local function toggle_test_panel(is_debug)
|
||||||
local expected_text = current_test.expected
|
local expected_text = current_test.expected
|
||||||
local expected_lines = vim.split(expected_text, '\n', { plain = true, trimempty = true })
|
local expected_lines = vim.split(expected_text, '\n', { plain = true, trimempty = true })
|
||||||
|
|
||||||
vim.api.nvim_buf_set_lines(test_buffers.expected_buf, 0, -1, false, expected_lines)
|
update_buffer_content(test_buffers.expected_buf, expected_lines)
|
||||||
|
|
||||||
if vim.fn.has('nvim-0.8.0') == 1 then
|
|
||||||
vim.api.nvim_set_option_value('winbar', 'Expected', { win = test_windows.expected_win })
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function update_actual_pane()
|
local function update_actual_pane()
|
||||||
|
|
@ -315,10 +270,12 @@ local function toggle_test_panel(is_debug)
|
||||||
actual_lines = { '(not run yet)' }
|
actual_lines = { '(not run yet)' }
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.api.nvim_buf_set_lines(test_buffers.actual_buf, 0, -1, false, actual_lines)
|
update_buffer_content(test_buffers.actual_buf, actual_lines)
|
||||||
|
|
||||||
if vim.fn.has('nvim-0.8.0') == 1 then
|
local test_render = require('cp.test_render')
|
||||||
vim.api.nvim_set_option_value('winbar', 'Actual', { win = test_windows.actual_win })
|
local status_bar_text = test_render.render_status_bar(current_test)
|
||||||
|
if status_bar_text ~= '' then
|
||||||
|
vim.api.nvim_set_option_value('winbar', status_bar_text, { win = test_windows.actual_win })
|
||||||
end
|
end
|
||||||
|
|
||||||
vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.expected_win })
|
vim.api.nvim_set_option_value('diff', enable_diff, { win = test_windows.expected_win })
|
||||||
|
|
@ -340,7 +297,7 @@ local function toggle_test_panel(is_debug)
|
||||||
end
|
end
|
||||||
|
|
||||||
local tab_lines = render_test_tabs()
|
local tab_lines = render_test_tabs()
|
||||||
vim.api.nvim_buf_set_lines(test_buffers.tab_buf, 0, -1, false, tab_lines)
|
update_buffer_content(test_buffers.tab_buf, tab_lines)
|
||||||
|
|
||||||
update_expected_pane()
|
update_expected_pane()
|
||||||
update_actual_pane()
|
update_actual_pane()
|
||||||
|
|
@ -373,6 +330,9 @@ local function toggle_test_panel(is_debug)
|
||||||
vim.keymap.set('n', 'q', function()
|
vim.keymap.set('n', 'q', function()
|
||||||
toggle_test_panel()
|
toggle_test_panel()
|
||||||
end, { buffer = buf, silent = true })
|
end, { buffer = buf, silent = true })
|
||||||
|
vim.keymap.set('n', config.test_panel.toggle_key, function()
|
||||||
|
toggle_test_panel()
|
||||||
|
end, { buffer = buf, silent = true })
|
||||||
end
|
end
|
||||||
|
|
||||||
if is_debug and config.hooks and config.hooks.before_debug then
|
if is_debug and config.hooks and config.hooks.before_debug then
|
||||||
|
|
|
||||||
102
lua/cp/test_render.lua
Normal file
102
lua/cp/test_render.lua
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
---@class TestRenderConfig
|
||||||
|
---@field status_format "compact"|"verbose"
|
||||||
|
|
||||||
|
---@class StatusInfo
|
||||||
|
---@field text string
|
||||||
|
---@field highlight_group string
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
---Convert test status to CP terminology with colors
|
||||||
|
---@param test_case TestCase
|
||||||
|
---@return StatusInfo
|
||||||
|
local function get_status_info(test_case)
|
||||||
|
if test_case.status == 'pass' then
|
||||||
|
return { text = 'AC', highlight_group = 'CpTestAC' }
|
||||||
|
elseif test_case.status == 'fail' then
|
||||||
|
if test_case.timed_out then
|
||||||
|
return { text = 'TLE', highlight_group = 'CpTestError' }
|
||||||
|
elseif test_case.code and test_case.code ~= 0 then
|
||||||
|
return { text = 'RTE', highlight_group = 'CpTestError' }
|
||||||
|
else
|
||||||
|
return { text = 'WA', highlight_group = 'CpTestError' }
|
||||||
|
end
|
||||||
|
elseif test_case.status == 'timeout' then
|
||||||
|
return { text = 'TLE', highlight_group = 'CpTestError' }
|
||||||
|
elseif test_case.status == 'running' then
|
||||||
|
return { text = '...', highlight_group = 'CpTestPending' }
|
||||||
|
else
|
||||||
|
return { text = '', highlight_group = 'CpTestPending' }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---Render test cases list with improved layout
|
||||||
|
---@param test_state TestPanelState
|
||||||
|
---@param config? TestRenderConfig
|
||||||
|
---@return string[]
|
||||||
|
function M.render_test_list(test_state, config)
|
||||||
|
config = config or { status_format = 'compact' }
|
||||||
|
local lines = {}
|
||||||
|
|
||||||
|
for i, test_case in ipairs(test_state.test_cases) do
|
||||||
|
local is_current = i == test_state.current_index
|
||||||
|
local prefix = is_current and '> ' or ' '
|
||||||
|
local status_info = get_status_info(test_case)
|
||||||
|
|
||||||
|
local status_text = status_info.text ~= '' and status_info.text or ''
|
||||||
|
local line = string.format('%s%d. %s', prefix, i, status_text)
|
||||||
|
|
||||||
|
table.insert(lines, line)
|
||||||
|
|
||||||
|
if is_current and test_case.input and test_case.input ~= '' then
|
||||||
|
for _, input_line in ipairs(vim.split(test_case.input, '\n', { plain = true, trimempty = false })) do
|
||||||
|
table.insert(lines, ' ' .. input_line)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return lines
|
||||||
|
end
|
||||||
|
|
||||||
|
---Create status bar content for diff pane
|
||||||
|
---@param test_case TestCase?
|
||||||
|
---@return string
|
||||||
|
function M.render_status_bar(test_case)
|
||||||
|
if not test_case then
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
|
||||||
|
local parts = {}
|
||||||
|
|
||||||
|
if test_case.time_ms then
|
||||||
|
table.insert(parts, string.format('%.0fms', test_case.time_ms))
|
||||||
|
end
|
||||||
|
|
||||||
|
if test_case.code then
|
||||||
|
table.insert(parts, string.format('Exit: %d', test_case.code))
|
||||||
|
end
|
||||||
|
|
||||||
|
return table.concat(parts, ' │ ')
|
||||||
|
end
|
||||||
|
|
||||||
|
---Get highlight groups needed for test rendering
|
||||||
|
---@return table<string, table>
|
||||||
|
function M.get_highlight_groups()
|
||||||
|
return {
|
||||||
|
CpTestAC = { fg = '#10b981', bold = true },
|
||||||
|
CpTestError = { fg = '#ef4444', bold = true },
|
||||||
|
CpTestPending = { fg = '#6b7280' },
|
||||||
|
CpDiffRemoved = { fg = '#ef4444', bg = '#1f1f1f' },
|
||||||
|
CpDiffAdded = { fg = '#10b981', bg = '#1f1f1f' },
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
---Setup highlight groups
|
||||||
|
function M.setup_highlights()
|
||||||
|
local groups = M.get_highlight_groups()
|
||||||
|
for group_name, opts in pairs(groups) do
|
||||||
|
vim.api.nvim_set_hl(0, group_name, opts)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
Loading…
Add table
Add a link
Reference in a new issue