fix(log): improve logging

This commit is contained in:
Barrett Ruth 2025-10-01 16:41:24 -04:00
parent 62af1965f8
commit a925686a17
10 changed files with 144 additions and 225 deletions

View file

@ -7,15 +7,20 @@ local logger = require('cp.log')
local platforms = constants.PLATFORMS
function M.handle_cache_command(cmd)
cmd.platform = cmd.platform:lower()
if cmd.subcommand == 'clear' then
cache.load()
if cmd.platform then
if vim.tbl_contains(platforms, cmd.platform) then
cache.clear_platform(cmd.platform)
logger.log(('cleared cache for %s'):format(cmd.platform), vim.log.levels.INFO, true)
logger.log(
('Cache cleared for platform %s'):format(cmd.platform),
vim.log.levels.INFO,
true
)
else
logger.log(
('unknown platform: %s. Available: %s'):format(
("Unknown platform: '%s'. Available: %s"):format(
cmd.platform,
table.concat(platforms, ', ')
),
@ -24,7 +29,7 @@ function M.handle_cache_command(cmd)
end
else
cache.clear_all()
logger.log('cleared all cache', vim.log.levels.INFO, true)
logger.log('Cache cleared', vim.log.levels.INFO, true)
end
end
end

View file

@ -8,7 +8,7 @@ function M.handle_pick_action()
if not config.picker then
logger.log(
'No picker configured. Set picker = "telescope" or picker = "fzf-lua" in config',
'No picker configured. Set picker = "{telescope,fzf-lua}" in your config.',
vim.log.levels.ERROR
)
return
@ -20,14 +20,14 @@ function M.handle_pick_action()
local ok = pcall(require, 'telescope')
if not ok then
logger.log(
'Telescope not available. Install telescope.nvim or change picker config',
'telescope.nvim is not available. Install telescope.nvim xor change your picker config.',
vim.log.levels.ERROR
)
return
end
local ok_cp, telescope_picker = pcall(require, 'cp.pickers.telescope')
if not ok_cp then
logger.log('Failed to load telescope integration', vim.log.levels.ERROR)
logger.log('Failed to load telescope integration.', vim.log.levels.ERROR)
return
end
@ -36,14 +36,14 @@ function M.handle_pick_action()
local ok, _ = pcall(require, 'fzf-lua')
if not ok then
logger.log(
'fzf-lua not available. Install fzf-lua or change picker config',
'fzf-lua is not available. Install fzf-lua xor change your picker config',
vim.log.levels.ERROR
)
return
end
local ok_cp, fzf_picker = pcall(require, 'cp.pickers.fzf_lua')
if not ok_cp then
logger.log('Failed to load fzf-lua integration', vim.log.levels.ERROR)
logger.log('Failed to load fzf-lua integration.', vim.log.levels.ERROR)
return
end

View file

@ -5,7 +5,7 @@ local logger = require('cp.log')
local snippets = require('cp.snippets')
if not vim.fn.has('nvim-0.10.0') then
logger.log('[cp.nvim]: requires nvim-0.10.0+', vim.log.levels.ERROR)
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR)
return {}
end

View file

@ -2,6 +2,7 @@ local M = {}
local cache = require('cp.cache')
local config = require('cp.config').get_config()
local constants = require('cp.constants')
local logger = require('cp.log')
local scraper = require('cp.scraper')
@ -21,7 +22,6 @@ local scraper = require('cp.scraper')
---@return cp.PlatformItem[]
function M.get_platforms()
local constants = require('cp.constants')
local result = {}
for _, platform in ipairs(constants.PLATFORMS) do
@ -40,7 +40,11 @@ end
---@param platform string Platform identifier (e.g. "codeforces", "atcoder")
---@return cp.ContestItem[]
function M.get_contests_for_platform(platform)
logger.log(('Loading %s contests..'):format(platform), vim.log.levels.INFO, true)
logger.log(
('Loading %s contests...'):format(constants.PLATFORM_DISPLAY_NAMES[platform]),
vim.log.levels.INFO,
true
)
cache.load()
@ -62,21 +66,11 @@ function M.get_contests_for_platform(platform)
end
logger.log(
('Loaded %d %s contests.'):format(#picker_contests, platform),
('Loaded %s %s contests.'):format(#picker_contests, constants.PLATFORM_DISPLAY_NAMES[platform]),
vim.log.levels.INFO,
true
)
return picker_contests
end
---@param platform string Platform identifier
---@param contest_id string Contest identifier
---@param problem_id string Problem identifier
function M.setup_problem(platform, contest_id, problem_id)
vim.schedule(function()
local cp = require('cp')
cp.handle_command({ fargs = { platform, contest_id, problem_id } })
end)
end
return M

View file

@ -7,7 +7,7 @@ local state = require('cp.state')
function M.restore_from_current_file()
local current_file = vim.fn.expand('%:p')
if current_file == '' then
logger.log('No file is currently open', vim.log.levels.ERROR)
logger.log('No file is currently open.', vim.log.levels.ERROR)
return false
end
@ -15,7 +15,7 @@ function M.restore_from_current_file()
local file_state = cache.get_file_state(current_file)
if not file_state then
logger.log(
'No cached state found for current file. Use :CP <platform> <contest> <problem> first.',
'No cached state found for current file. Use :CP <platform> <contest> <problem> [...] first.',
vim.log.levels.ERROR
)
return false
@ -25,7 +25,7 @@ function M.restore_from_current_file()
('Restoring from cached state: %s %s %s'):format(
file_state.platform,
file_state.contest_id,
file_state.problem_id or 'N/A'
file_state.problem_id
)
)

View file

@ -52,7 +52,7 @@ end
---@return {code: integer, stdout: string, stderr: string}
function M.compile_generic(language_config, substitutions)
if not language_config.compile then
logger.log('no compilation step required')
logger.log('No compilation step required for language - skipping.')
return { code = 0, stderr = '' }
end
@ -73,9 +73,9 @@ function M.compile_generic(language_config, substitutions)
result.stderr = ansi.bytes_to_string(result.stderr or '')
if result.code == 0 then
logger.log(('compilation successful (%.1fms)'):format(compile_time), vim.log.levels.INFO)
logger.log(('Compilation successful in %.1fms.'):format(compile_time), vim.log.levels.INFO)
else
logger.log(('compilation failed (%.1fms)'):format(compile_time))
logger.log(('Compilation failed in %.1fms.'):format(compile_time))
end
return result
@ -107,14 +107,14 @@ local function execute_command(cmd, input_data, timeout_ms)
local actual_code = result.code or 0
if result.code == 124 then
logger.log(('execution timed out after %.1fms'):format(execution_time), vim.log.levels.WARN)
logger.log(('Execution timed out in %.1fms.'):format(execution_time), vim.log.levels.WARN)
elseif actual_code ~= 0 then
logger.log(
('execution failed (exit code %d, %.1fms)'):format(actual_code, execution_time),
('Execution failed in %.1fms (exit code %d).'):format(execution_time, actual_code),
vim.log.levels.WARN
)
else
logger.log(('execution successful (%.1fms)'):format(execution_time))
logger.log(('Execution successful in %.1fms.'):format(execution_time))
end
return {
@ -177,8 +177,8 @@ function M.compile_problem(contest_config, is_debug)
local state = require('cp.state')
local source_file = state.get_source_file()
if not source_file then
logger.log('No source file found', vim.log.levels.ERROR)
return { success = false, output = 'No source file found' }
logger.log('No source file found.', vim.log.levels.ERROR)
return { success = false, output = 'No source file found.' }
end
local language = get_language_from_file(source_file, contest_config)
@ -186,7 +186,7 @@ function M.compile_problem(contest_config, is_debug)
if not language_config then
logger.log('No configuration for language: ' .. language, vim.log.levels.ERROR)
return { success = false, output = 'No configuration for language: ' .. language }
return { success = false, output = ('No configuration for language %s.'):format(language) }
end
local binary_file = state.get_binary_file()
@ -203,10 +203,6 @@ function M.compile_problem(contest_config, is_debug)
if compile_result.code ~= 0 then
return { success = false, output = compile_result.stdout or 'unknown error' }
end
logger.log(
('compilation successful (%s)'):format(is_debug and 'debug mode' or 'test mode'),
vim.log.levels.INFO
)
end
return { success = true, output = nil }
@ -220,7 +216,10 @@ function M.run_problem(contest_config, is_debug)
local output_file = state.get_output_file()
if not source_file or not output_file then
logger.log('Missing required file paths', vim.log.levels.ERROR)
logger.log(
('Missing required file paths %s and %s'):format(source_file, output_file),
vim.log.levels.ERROR
)
return
end
@ -257,13 +256,14 @@ function M.run_problem(contest_config, is_debug)
local cache = require('cp.cache')
cache.load()
local platform = state.get_platform()
local contest_id = state.get_contest_id()
local problem_id = state.get_problem_id()
local expected_file = state.get_expected_file()
if not platform or not contest_id or not expected_file then
logger.log('configure a contest before running a problem', vim.log.levels.ERROR)
logger.log('Configure a contest before running a problem', vim.log.levels.ERROR)
return
end
local timeout_ms, _ = cache.get_constraints(platform, contest_id, problem_id)

View file

@ -185,7 +185,7 @@ local function run_single_test_case(contest_config, cp_config, test_case)
}
if language_config.compile and binary_file and vim.fn.filereadable(binary_file) == 0 then
logger.log('binary not found, compiling first...')
logger.log('Binary not found - compiling first.')
local compile_cmd = substitute_template(language_config.compile, substitutions)
local redirected_cmd = vim.deepcopy(compile_cmd)
redirected_cmd[#redirected_cmd] = redirected_cmd[#redirected_cmd] .. ' 2>&1'
@ -219,9 +219,6 @@ local function run_single_test_case(contest_config, cp_config, test_case)
local start_time = vim.uv.hrtime()
local timeout_ms = run_panel_state.constraints and run_panel_state.constraints.timeout_ms or 2000
if not run_panel_state.constraints then
logger.log('no problem constraints available, using default 2000ms timeout')
end
local redirected_run_cmd = vim.deepcopy(run_cmd)
redirected_run_cmd[#redirected_run_cmd] = redirected_run_cmd[#redirected_run_cmd] .. ' 2>&1'
local result = vim
@ -315,14 +312,7 @@ function M.load_test_cases(state)
state.get_problem_id()
)
local constraint_info = run_panel_state.constraints
and string.format(
' with %dms/%dMB limits',
run_panel_state.constraints.timeout_ms,
run_panel_state.constraints.memory_mb
)
or ''
logger.log(('loaded %d test case(s)%s'):format(#test_cases, constraint_info), vim.log.levels.INFO)
logger.log(('Loaded %d test case(s)'):format(#test_cases), vim.log.levels.INFO)
return #test_cases > 0
end

View file

@ -5,7 +5,7 @@ local logger = require('cp.log')
local function syshandle(result)
if result.code ~= 0 then
local msg = 'Scraper failed: ' .. (result.error or result.stderr or 'Unknown error')
local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error')
logger.log(msg, vim.log.levels.ERROR)
return {
success = false,
@ -69,13 +69,17 @@ end
function M.scrape_contest_metadata(platform, contest_id, callback)
run_scraper(platform, 'metadata', { contest_id }, {
on_exit = function(result)
if not result.success then
if not result.success or vim.tbl_isempty(result.data.problems) then
logger.log(
('Failed to scrape metadata for %s contest %s - aborting.'):format(platform, contest_id)
('Failed to scrape metadata for %s contest %s - aborting.'):format(platform, contest_id),
vim.log.levels.ERROR
)
return
end
callback(result.data)
if type(callback) == 'function' then
callback(result.data)
end
end,
})
end
@ -83,7 +87,10 @@ end
function M.scrape_contest_list(platform)
local result = run_scraper(platform, 'contests', {}, { sync = true })
if not result.success or not result.data.contests then
logger.log(('Could not scrape contests list for platform %s: %s'):format(platform, result.msg))
logger.log(
('Could not scrape contests list for platform %s: %s'):format(platform, result.msg),
vim.log.levels.ERROR
)
return {}
end
@ -123,7 +130,9 @@ function M.scrape_problem_tests(platform, contest_id, problem_id, callback)
end
end)
callback(result.data)
if type(callback) == 'function' then
callback(result.data)
end
end,
})
end

View file

@ -28,155 +28,7 @@ function M.set_platform(platform)
return true
end
-- NOTE: this is backwards
function M.setup_contest(platform, contest_id, problem_id, language)
if not state.get_platform() then
logger.log('No platform configured. Use :CP <platform> <contest> [...] first.')
return
end
local config = config_module.get_config()
if not vim.tbl_contains(config.scrapers, platform) then
logger.log(('Scraping disabled for %s - aborting'):format(platform), vim.log.levels.WARN)
return
end
state.set_contest_id(contest_id)
logger.log('fetching contests problems...', vim.log.levels.INFO, true)
scraper.scrape_contest_metadata(platform, contest_id, function(result)
local problems = result.problems
if vim.tbl_isempty(problems) then
logger.log('no problems found in contest', vim.log.levels.ERROR)
return
end
logger.log(('found %d problems'):format(#problems))
local target_problem = problem_id or problems[1].id
if problem_id then
local problem_exists = false
for _, prob in ipairs(problems) do
if prob.id == problem_id then
problem_exists = true
break
end
end
if not problem_exists then
logger.log(
('invalid problem %s for contest %s'):format(problem_id, contest_id),
vim.log.levels.ERROR
)
return
end
end
-- NOTE: should setup buffer without a name, then save it with proper name later for immediate editing
M.setup_problem(contest_id, target_problem, language)
M.scrape_remaining_problems(platform, contest_id, problems)
end)
end
function M.setup_problem(contest_id, problem_id, language)
if not state.get_platform() then
logger.log('no platform set. run :CP <platform> <contest> first', vim.log.levels.ERROR)
return
end
local config = config_module.get_config()
local platform = state.get_platform() or ''
logger.log(('setting up problem %s%s...'):format(contest_id, problem_id or ''))
state.set_contest_id(contest_id)
state.set_problem_id(problem_id)
vim.schedule(function()
local ok, err = pcall(function()
vim.cmd.only({ mods = { silent = true } })
local source_file = state.get_source_file(language)
if not source_file then
return
end
vim.cmd.e(source_file)
local source_buf = vim.api.nvim_get_current_buf()
if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == '' then
local has_luasnip, luasnip = pcall(require, 'luasnip')
if has_luasnip then
local filetype = vim.api.nvim_get_option_value('filetype', { buf = source_buf })
local language_name = constants.filetype_to_language[filetype]
local canonical_language = constants.canonical_filetypes[language_name] or language_name
local prefixed_trigger = ('cp.nvim/%s.%s'):format(platform, canonical_language)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { prefixed_trigger })
vim.api.nvim_win_set_cursor(0, { 1, #prefixed_trigger })
vim.cmd.startinsert({ bang = true })
vim.schedule(function()
if luasnip.expandable() then
luasnip.expand()
else
vim.api.nvim_buf_set_lines(0, 0, 1, false, { '' })
vim.api.nvim_win_set_cursor(0, { 1, 0 })
end
vim.cmd.stopinsert()
end)
else
vim.api.nvim_input(('i%s<c-space><esc>'):format(platform))
end
end
if config.hooks and config.hooks.setup_code then
config.hooks.setup_code(state)
end
cache.set_file_state(vim.fn.expand('%:p'), platform, contest_id, problem_id, language)
logger.log(('ready - problem %s'):format(state.get_base_name()))
end)
if not ok then
logger.log(('setup error: %s'):format(err), vim.log.levels.ERROR)
end
end)
local cached_tests = cache.get_test_cases(platform, contest_id, problem_id)
if cached_tests then
state.set_test_cases(cached_tests)
logger.log(('using cached test cases (%d)'):format(#cached_tests))
else
logger.log('loading test cases...')
scraper.scrape_problem_tests(platform, contest_id, problem_id, function(result)
state.set_test_cases(result.tests or {})
cached_tests = {}
for i, test_case in ipairs(result.tests or {}) do
table.insert(cached_tests, {
index = i,
input = test_case.input,
expected = test_case.expected,
})
end
cache.set_test_cases(
platform,
contest_id,
problem_id,
cached_tests,
result.timeout_ms,
result.memory_mb
)
end)
end
end
function M.scrape_remaining_problems(platform, contest_id, problems)
local function scrape_contest_problems(platform, contest_id, problems)
cache.load()
local missing_problems = {}
@ -188,21 +40,98 @@ function M.scrape_remaining_problems(platform, contest_id, problems)
end
if vim.tbl_isempty(missing_problems) then
logger.log('all problems already cached')
logger.log(('All problems already cached for %s contest %s'):format(platform, contest_id))
return
end
logger.log(('caching %d remaining problems...'):format(#missing_problems))
for _, prob in ipairs(missing_problems) do
scraper.scrape_problem_tests(platform, contest_id, prob.id, function(result)
if result.success then
logger.log(('background: scraped problem %s'):format(prob.id))
end
end)
scraper.scrape_problem_tests(platform, contest_id, prob.id)
end
end
function M.setup_contest(platform, contest_id, problem_id, language)
if not state.get_platform() then
logger.log('No platform configured. Use :CP <platform> <contest> [...] first.')
return
end
local config = config_module.get_config()
if not vim.tbl_contains(config.scrapers, platform) then
logger.log(('Scraping disabled for %s - aborting.'):format(platform), vim.log.levels.WARN)
return
end
state.set_contest_id(contest_id)
logger.log('Fetching contests problems...', vim.log.levels.INFO, true)
scraper.scrape_contest_metadata(platform, contest_id, function(result)
local problems = result.problems
logger.log(('found %d problems'):format(#problems))
local target_problem = problem_id or problems[1].id
M.setup_problem(contest_id, target_problem, language)
scrape_contest_problems(platform, contest_id, problems)
end)
end
function M.setup_problem(contest_id, problem_id, language)
if not state.get_platform() then
logger.log('No platform set. run :CP <platform> <contest> first', vim.log.levels.ERROR)
return
end
local config = config_module.get_config()
local platform = state.get_platform() or ''
state.set_contest_id(contest_id)
state.set_problem_id(problem_id)
vim.schedule(function()
vim.cmd.only({ mods = { silent = true } })
local source_file = state.get_source_file(language)
if not source_file then
return
end
vim.cmd.e(source_file)
local source_buf = vim.api.nvim_get_current_buf()
if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == '' then
local has_luasnip, luasnip = pcall(require, 'luasnip')
if has_luasnip then
local filetype = vim.api.nvim_get_option_value('filetype', { buf = source_buf })
local language_name = constants.filetype_to_language[filetype]
local canonical_language = constants.canonical_filetypes[language_name] or language_name
local prefixed_trigger = ('cp.nvim/%s.%s'):format(platform, canonical_language)
vim.api.nvim_buf_set_lines(0, 0, -1, false, { prefixed_trigger })
vim.api.nvim_win_set_cursor(0, { 1, #prefixed_trigger })
vim.cmd.startinsert({ bang = true })
vim.schedule(function()
if luasnip.expandable() then
luasnip.expand()
else
vim.api.nvim_buf_set_lines(0, 0, 1, false, { '' })
vim.api.nvim_win_set_cursor(0, { 1, 0 })
end
vim.cmd.stopinsert()
end)
end
end
if config.hooks and config.hooks.setup_code then
config.hooks.setup_code(state)
end
cache.set_file_state(vim.fn.expand('%:p'), platform, contest_id, problem_id, language)
end)
end
function M.navigate_problem(direction, language)
local platform = state.get_platform()
local contest_id = state.get_contest_id()
@ -219,7 +148,7 @@ function M.navigate_problem(direction, language)
cache.load()
local contest_data = cache.get_contest_data(platform, contest_id)
if not contest_data or not contest_data.problems then
logger.log('no contest data available', vim.log.levels.ERROR)
logger.log('No contest data available', vim.log.levels.ERROR)
return
end
@ -232,19 +161,12 @@ function M.navigate_problem(direction, language)
end
end
if not current_index then
logger.log('current problem not found in contest', vim.log.levels.ERROR)
return
end
local new_index = current_index + direction
if new_index < 1 or new_index > #problems then
logger.log('no more problems in that direction', vim.log.levels.WARN)
return
end
local new_problem = problems[new_index]
M.setup_problem(contest_id, new_problem.id, language)
M.setup_problem(contest_id, problems[new_index].id, language)
end
return M