diff --git a/lua/cp/commands/cache.lua b/lua/cp/commands/cache.lua index 08f50de..892ddf3 100644 --- a/lua/cp/commands/cache.lua +++ b/lua/cp/commands/cache.lua @@ -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 diff --git a/lua/cp/commands/picker.lua b/lua/cp/commands/picker.lua index 1c3ef6a..80d79be 100644 --- a/lua/cp/commands/picker.lua +++ b/lua/cp/commands/picker.lua @@ -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 diff --git a/lua/cp/init.lua b/lua/cp/init.lua index da82f9c..b2881a9 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -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 diff --git a/lua/cp/pickers/init.lua b/lua/cp/pickers/init.lua index e6317bc..f97e73b 100644 --- a/lua/cp/pickers/init.lua +++ b/lua/cp/pickers/init.lua @@ -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 diff --git a/lua/cp/restore.lua b/lua/cp/restore.lua index 60236b4..0df57d4 100644 --- a/lua/cp/restore.lua +++ b/lua/cp/restore.lua @@ -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 first.', + 'No cached state found for current file. Use :CP [...] 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 ) ) diff --git a/lua/cp/runner/execute.lua b/lua/cp/runner/execute.lua index 8bffa33..4fce9e2 100644 --- a/lua/cp/runner/execute.lua +++ b/lua/cp/runner/execute.lua @@ -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) diff --git a/lua/cp/runner/run.lua b/lua/cp/runner/run.lua index 300ed1c..84e71af 100644 --- a/lua/cp/runner/run.lua +++ b/lua/cp/runner/run.lua @@ -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 diff --git a/lua/cp/scraper.lua b/lua/cp/scraper.lua index d06968c..38b1aff 100644 --- a/lua/cp/scraper.lua +++ b/lua/cp/scraper.lua @@ -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 diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index b1537a4..faab15e 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -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 [...] 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 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'):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 [...] 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 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 diff --git a/spec/command_parsing_spec.lua b/spec/command_parsing_spec.lua index c6114e3..d5a07d3 100644 --- a/spec/command_parsing_spec.lua +++ b/spec/command_parsing_spec.lua @@ -19,7 +19,6 @@ describe('cp command parsing', function() setup_contest = function() end, navigate_problem = function() end, setup_problem = function() end, - scrape_remaining_problems = function() end, } package.loaded['cp.setup'] = mock_setup