diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 27cf18e..c8b17ce 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -25,18 +25,13 @@ COMMANDS *cp-commands* cp.nvim uses a single :CP command with intelligent argument parsing: Setup Commands ~ - :CP {platform} {contest_id} + :CP {platform} {contest_id} [--lang {language}] Full setup: set platform and load contest metadata. Scrapes test cases and creates source file. - Example: > + --lang: Use specific language (default: platform default) + Examples: > :CP codeforces 1933 -< - :CP {platform} {contest_id} - Contest setup: set platform, load contest metadata, - and scrape all test cases in the contest. - Opens the first problem after completion. - Example: > - :CP atcoder abc324 + :CP codeforces 1933 --lang python < View Commands ~ :CP run [--debug] [n] @@ -59,8 +54,14 @@ COMMANDS *cp-commands* :CP panel --debug 3 " Test 3, debug build < - :CP pick Launch configured picker for interactive + :CP pick [--lang {language}] + Launch configured picker for interactive platform/contest selection. + --lang: Pre-select language for chosen contest. + Example: > + :CP pick + :CP pick --lang python +< :CP interact [script] Open an interactive terminal for the current problem. @@ -70,15 +71,36 @@ COMMANDS *cp-commands* file. Only valid for interactive problems. Navigation Commands ~ - :CP next Navigate to next problem in current contest. + :CP next [--lang {language}] + Navigate to next problem in current contest. Stops at last problem (no wrapping). - - - :CP prev Navigate to previous problem in current contest. + --lang: Use specific language for next problem. + By default, preserves current file's language if + enabled for the new problem, otherwise uses platform + default. + Examples: > + :CP next + :CP next --lang python +< + :CP prev [--lang {language}] + Navigate to previous problem in current contest. Stops at first problem (no wrapping). - - :CP {problem_id} Jump to problem {problem_id} in a contest. + --lang: Use specific language for previous problem. + By default, preserves current file's language if + enabled for the new problem, otherwise uses platform + default. + Examples: > + :CP prev + :CP prev --lang cpp +< + :CP {problem_id} [--lang {language}] + Jump to problem {problem_id} in a contest. Requires that a contest has already been set up. + --lang: Use specific language for this problem. + Examples: > + :CP B + :CP C --lang python +< State Restoration ~ :CP Restore state from current file. @@ -357,6 +379,49 @@ run CSES problems with Rust using the single schema: } < +============================================================================== +LANGUAGE SELECTION *cp-lang-selection* + +cp.nvim supports multiple languages per problem. Each platform enables specific +languages and has a default. You can override the language for any setup or +navigation command using the --lang flag. + +Language Selection Behavior ~ + +When setting up or navigating to a problem: + +1. Explicit --lang flag takes highest priority +2. If no --lang flag, tries to preserve current file's language + (only if that language is enabled for the new problem) +3. Falls back to platform's default language + +Multiple Solution Files ~ + +Different languages create different solution files. For example: + 1848a.cc (C++ solution) + 1848a.py (Python solution) + +Both files can exist simultaneously with their own state. Switching between +languages means switching between different files. + +Examples ~ +> + :CP codeforces 1848 " Use platform default (likely C++) + :CP codeforces 1848 --lang python " Use Python explicitly + + " In 1848a.cc (C++ file): + :CP next " Next problem tries to use C++ + :CP next --lang python " Next problem uses Python + + " In 1848a.py (Python file): + :CP next " Next problem tries to use Python + :CP next --lang cpp " Next problem switches to C++ +< +Language Validation ~ + +If you request a language that isn't enabled for a platform, cp.nvim will show +a helpful error message listing available languages for that platform. + ============================================================================== WORKFLOW *cp-workflow* @@ -374,6 +439,7 @@ https://atcoder.jp/contests/{contest_id}/tasks/{contest_id}_{problem_id} Usage examples: > :CP atcoder abc324 " Set up atcoder.jp/contests/abc324 + :CP atcoder abc324 --lang python " Set up with Python instead of default Codeforces ~ *cp-codeforces* @@ -381,6 +447,7 @@ URL format: https://codeforces.com/contest/{contest_id}/problem/{problem_id} Usage examples: > :CP codeforces 1934 " Set up codeforces.com/contest/1934 + :CP codeforces 1934 --lang cpp " Set up with C++ CSES ~ *cp-cses* @@ -404,7 +471,7 @@ Example: Setting up and solving AtCoder contest ABC324 3. Code your solution, then test: > :CP run -< View test verdicts in I/O splits. For detailed analysis: +< View test verdicts in I/O splits. For detailed analysis: > :CP panel < Navigate tests with /, exit with q @@ -414,12 +481,16 @@ Example: Setting up and solving AtCoder contest ABC324 5. Continue solving problems with :CP next/:CP prev navigation -6. Switch to another file (e.g. previous contest): > +6. Try a different language for a problem: > + :CP C --lang python +< Opens problem C with Python instead of C++ + +7. Switch to another file (e.g. previous contest): > :e ~/contests/abc323/a.cpp :CP < Automatically restores abc323 contest context -7. Submit solutions on AtCoder website +8. Submit solutions on AtCoder website ============================================================================== I/O VIEW *cp-io-view* diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 70c07b0..fa4f658 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -18,6 +18,7 @@ local actions = constants.ACTIONS ---@field interactor_cmd? string ---@field test_index? integer ---@field debug? boolean +---@field language? string --- Turn raw args into normalized structure to later dispatch ---@param args string[] The raw command-line mode args @@ -79,7 +80,16 @@ local function parse_command(args) return { type = 'action', action = first, test_index = test_index, debug = debug } else - return { type = 'action', action = first } + local language = nil + if #args >= 3 and args[2] == '--lang' then + language = args[3] + elseif #args >= 2 and args[2] ~= nil and args[2]:sub(1, 2) ~= '--' then + return { + type = 'error', + message = ("Unknown argument '%s' for action '%s'"):format(args[2], first), + } + end + return { type = 'action', action = first, language = language } end end @@ -95,13 +105,18 @@ local function parse_command(args) platform = first, contest = args[2], } - elseif #args == 3 then + elseif #args == 4 and args[3] == '--lang' then return { - type = 'error', - message = 'Setup contests with :CP .', + type = 'contest_setup', + platform = first, + contest = args[2], + language = args[4], } else - return { type = 'error', message = 'Too many arguments' } + return { + type = 'error', + message = 'Invalid arguments. Usage: :CP [--lang ]', + } end end @@ -110,6 +125,12 @@ local function parse_command(args) type = 'problem_jump', problem_id = first, } + elseif #args == 3 and args[2] == '--lang' then + return { + type = 'problem_jump', + problem_id = first, + language = args[3], + } end return { type = 'error', message = 'Unknown command or no contest context.' } @@ -139,12 +160,12 @@ function M.handle_command(opts) elseif cmd.action == 'panel' then ui.toggle_panel({ debug = cmd.debug, test_index = cmd.test_index }) elseif cmd.action == 'next' then - setup.navigate_problem(1) + setup.navigate_problem(1, cmd.language) elseif cmd.action == 'prev' then - setup.navigate_problem(-1) + setup.navigate_problem(-1, cmd.language) elseif cmd.action == 'pick' then local picker = require('cp.commands.picker') - picker.handle_pick_action() + picker.handle_pick_action(cmd.language) end elseif cmd.type == 'problem_jump' then local platform = state.get_platform() @@ -173,13 +194,13 @@ function M.handle_command(opts) end local setup = require('cp.setup') - setup.setup_contest(platform, contest_id, problem_id) + setup.setup_contest(platform, contest_id, problem_id, cmd.language) elseif cmd.type == 'cache' then local cache_commands = require('cp.commands.cache') cache_commands.handle_cache_command(cmd) elseif cmd.type == 'contest_setup' then local setup = require('cp.setup') - setup.setup_contest(cmd.platform, cmd.contest, nil) + setup.setup_contest(cmd.platform, cmd.contest, nil, cmd.language) return end end diff --git a/lua/cp/commands/picker.lua b/lua/cp/commands/picker.lua index a733b58..08cbcd9 100644 --- a/lua/cp/commands/picker.lua +++ b/lua/cp/commands/picker.lua @@ -4,8 +4,9 @@ local config_module = require('cp.config') local logger = require('cp.log') --- Dispatch `:CP pick` to appropriate picker +---@param language? string ---@return nil -function M.handle_pick_action() +function M.handle_pick_action(language) local config = config_module.get_config() if not (config.ui and config.ui.picker) then @@ -53,7 +54,7 @@ function M.handle_pick_action() picker = fzf_picker end - picker.pick() + picker.pick(language) end return M diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 1f926b5..4ae06d1 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -351,6 +351,50 @@ function M.get_config() return current_config or M.defaults end +---Validate and get effective language config for a platform +---@param platform_id string +---@param language_id string +---@return { valid: boolean, effective?: CpLanguage, extension?: string, error?: string } +function M.get_language_for_platform(platform_id, language_id) + local cfg = M.get_config() + + if not cfg.platforms[platform_id] then + return { valid = false, error = string.format("Unknown platform '%s'", platform_id) } + end + + if not cfg.languages[language_id] then + return { valid = false, error = string.format("Unknown language '%s'", language_id) } + end + + local platform = cfg.platforms[platform_id] + if not vim.tbl_contains(platform.enabled_languages, language_id) then + local available = table.concat(platform.enabled_languages, ', ') + return { + valid = false, + error = string.format( + "Language '%s' not enabled for %s. Available: %s", + language_id, + platform_id, + available + ), + } + end + + local effective = cfg.runtime.effective[platform_id][language_id] + if not effective then + return { + valid = false, + error = string.format('No effective config for %s/%s', platform_id, language_id), + } + end + + return { + valid = true, + effective = effective, + extension = effective.extension, + } +end + ---@param contest_id string ---@param problem_id? string ---@return string diff --git a/lua/cp/pickers/fzf_lua.lua b/lua/cp/pickers/fzf_lua.lua index 6735f54..d1af5d2 100644 --- a/lua/cp/pickers/fzf_lua.lua +++ b/lua/cp/pickers/fzf_lua.lua @@ -2,7 +2,7 @@ local picker_utils = require('cp.pickers') local M = {} -local function contest_picker(platform, refresh) +local function contest_picker(platform, refresh, language) local constants = require('cp.constants') local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform] local fzf = require('fzf-lua') @@ -42,19 +42,24 @@ local function contest_picker(platform, refresh) if contest then local cp = require('cp') - cp.handle_command({ fargs = { platform, contest.id } }) + local fargs = { platform, contest.id } + if language then + table.insert(fargs, '--lang') + table.insert(fargs, language) + end + cp.handle_command({ fargs = fargs }) end end, ['ctrl-r'] = function() local cache = require('cp.cache') cache.clear_contest_list(platform) - contest_picker(platform, true) + contest_picker(platform, true, language) end, }, }) end -function M.pick() +function M.pick(language) local fzf = require('fzf-lua') local platforms = picker_utils.get_platforms() local entries = vim.tbl_map(function(platform) @@ -79,7 +84,7 @@ function M.pick() end if platform then - contest_picker(platform.id) + contest_picker(platform.id, false, language) end end, }, diff --git a/lua/cp/pickers/telescope.lua b/lua/cp/pickers/telescope.lua index 9b3c0db..0a7a676 100644 --- a/lua/cp/pickers/telescope.lua +++ b/lua/cp/pickers/telescope.lua @@ -8,7 +8,7 @@ local picker_utils = require('cp.pickers') local M = {} -local function contest_picker(opts, platform, refresh) +local function contest_picker(opts, platform, refresh, language) local constants = require('cp.constants') local platform_display_name = constants.PLATFORM_DISPLAY_NAMES[platform] local contests = picker_utils.get_platform_contests(platform, refresh) @@ -43,13 +43,18 @@ local function contest_picker(opts, platform, refresh) if selection then local cp = require('cp') - cp.handle_command({ fargs = { platform, selection.value.id } }) + local fargs = { platform, selection.value.id } + if language then + table.insert(fargs, '--lang') + table.insert(fargs, language) + end + cp.handle_command({ fargs = fargs }) end end) map('i', '', function() actions.close(prompt_bufnr) - contest_picker(opts, platform, true) + contest_picker(opts, platform, true, language) end) return true @@ -58,9 +63,8 @@ local function contest_picker(opts, platform, refresh) :find() end -function M.pick(opts) - opts = opts or {} - +function M.pick(language) + local opts = {} local platforms = picker_utils.get_platforms() pickers @@ -83,7 +87,7 @@ function M.pick(opts) actions.close(prompt_bufnr) if selection then - contest_picker(opts, selection.value.id) + contest_picker(opts, selection.value.id, false, language) end end) return true diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 50d603d..6129614 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -8,6 +8,18 @@ local logger = require('cp.log') local scraper = require('cp.scraper') local state = require('cp.state') +---Get the language of the current file from cache +---@return string|nil +local function get_current_file_language() + local current_file = vim.fn.expand('%:p') + if current_file == '' then + return nil + end + cache.load() + local file_state = cache.get_file_state(current_file) + return file_state and file_state.language or nil +end + ---@class TestCaseLite ---@field input string ---@field expected string @@ -86,6 +98,14 @@ function M.setup_contest(platform, contest_id, problem_id, language) state.set_platform(platform) state.set_contest_id(contest_id) + if language then + local lang_result = config_module.get_language_for_platform(platform, language) + if not lang_result.valid then + logger.log(lang_result.error, vim.log.levels.ERROR) + return + end + end + local is_new_contest = old_platform ~= platform and old_contest_id ~= contest_id cache.load() @@ -173,6 +193,15 @@ function M.setup_problem(problem_id, language) local config = config_module.get_config() local lang = language or (config.platforms[platform] and config.platforms[platform].default_language) + + if language then + local lang_result = config_module.get_language_for_platform(platform, language) + if not lang_result.valid then + logger.log(lang_result.error, vim.log.levels.ERROR) + return + end + end + local source_file = state.get_source_file(lang) if not source_file then return @@ -235,7 +264,8 @@ function M.setup_problem(problem_id, language) end ---@param direction integer -function M.navigate_problem(direction) +---@param language? string +function M.navigate_problem(direction, language) if direction == 0 then return end @@ -274,7 +304,15 @@ function M.navigate_problem(direction) require('cp.ui.views').disable() end - M.setup_contest(platform, contest_id, problems[new_index].id) + local lang = language or get_current_file_language() + if lang then + local lang_result = config_module.get_language_for_platform(platform, lang) + if not lang_result.valid then + lang = nil + end + end + + M.setup_contest(platform, contest_id, problems[new_index].id, lang) end return M