From 94f5828a0a583015ab0aa6eb059d6be2187f7121 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 12 Sep 2025 17:29:55 -0500 Subject: [PATCH] feat: modernize the plugin --- lua/cp/config.lua | 71 ++++++++++++++++++++++++++++-- lua/cp/health.lua | 107 ++++++++++++++++++++++++++++++++++++++++++++++ lua/cp/init.lua | 79 +++++++++++++++------------------- plugin/cp.lua | 18 +++++++- 4 files changed, 227 insertions(+), 48 deletions(-) create mode 100644 lua/cp/health.lua diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 0c59be5..1ed0214 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -1,5 +1,21 @@ +---@class cp.ContestConfig +---@field cpp_version number +---@field compile_flags string[] +---@field debug_flags string[] +---@field timeout_ms number + +---@class cp.HooksConfig +---@field before_run? function +---@field before_debug? function + +---@class cp.Config +---@field contests table +---@field snippets table +---@field hooks cp.HooksConfig + local M = {} +---@type cp.Config M.defaults = { contests = { default = { @@ -25,6 +41,9 @@ M.defaults = { }, } +---@param base_config cp.ContestConfig +---@param contest_config cp.ContestConfig +---@return cp.ContestConfig local function extend_contest_config(base_config, contest_config) local result = vim.tbl_deep_extend("force", base_config, contest_config) @@ -35,23 +54,69 @@ local function extend_contest_config(base_config, contest_config) return result end +---@param path string +---@param tbl table +---@return boolean is_valid +---@return string|nil error_message +local function validate_path(path, tbl) + local ok, err = pcall(vim.validate, tbl) + return ok, err and path .. "." .. err +end + +---@param path string +---@param contest_config table|nil +---@return boolean is_valid +---@return string|nil error_message +local function validate_contest_config(path, contest_config) + if not contest_config then + return true, nil + end + + return validate_path(path, { + cpp_version = { contest_config.cpp_version, "number", true }, + compile_flags = { contest_config.compile_flags, "table", true }, + debug_flags = { contest_config.debug_flags, "table", true }, + timeout_ms = { contest_config.timeout_ms, "number", true }, + }) +end + +---@param user_config cp.Config|nil +---@return cp.Config function M.setup(user_config) - vim.validate({ + local ok, err = validate_path("config", { user_config = { user_config, { "table", "nil" }, true }, }) + if not ok then + error(err) + end if user_config then - vim.validate({ + ok, err = validate_path("config", { contests = { user_config.contests, { "table", "nil" }, true }, snippets = { user_config.snippets, { "table", "nil" }, true }, hooks = { user_config.hooks, { "table", "nil" }, true }, }) + if not ok then + error(err) + end if user_config.hooks then - vim.validate({ + ok, err = validate_path("config.hooks", { before_run = { user_config.hooks.before_run, { "function", "nil" }, true }, before_debug = { user_config.hooks.before_debug, { "function", "nil" }, true }, }) + if not ok then + error(err) + end + end + + if user_config.contests then + for contest_name, contest_config in pairs(user_config.contests) do + ok, err = validate_contest_config("config.contests." .. contest_name, contest_config) + if not ok then + error(err) + end + end end end diff --git a/lua/cp/health.lua b/lua/cp/health.lua new file mode 100644 index 0000000..150700f --- /dev/null +++ b/lua/cp/health.lua @@ -0,0 +1,107 @@ +local M = {} + +local function check_nvim_version() + if vim.fn.has("nvim-0.10.0") == 1 then + vim.health.ok("Neovim 0.10.0+ detected") + else + vim.health.error("cp.nvim requires Neovim 0.10.0+") + end +end + +local function check_uv() + if vim.fn.executable("uv") == 1 then + vim.health.ok("uv executable found") + + local result = vim.system({ "uv", "--version" }, { text = true }):wait() + if result.code == 0 then + vim.health.info("uv version: " .. result.stdout:gsub("\n", "")) + end + else + vim.health.warn("uv not found - install from https://docs.astral.sh/uv/ for problem scraping") + end +end + +local function check_python_env() + local plugin_path = debug.getinfo(1, "S").source:sub(2) + plugin_path = vim.fn.fnamemodify(plugin_path, ":h:h:h:h") + local venv_dir = plugin_path .. "/.venv" + + if vim.fn.isdirectory(venv_dir) == 1 then + vim.health.ok("Python virtual environment found at " .. venv_dir) + else + vim.health.warn("Python virtual environment not set up - run :CP command to initialize") + end +end + +local function check_scrapers() + local plugin_path = debug.getinfo(1, "S").source:sub(2) + plugin_path = vim.fn.fnamemodify(plugin_path, ":h:h:h:h") + + local scrapers = { "atcoder.py", "codeforces.py", "cses.py" } + for _, scraper in ipairs(scrapers) do + local scraper_path = plugin_path .. "/scrapers/" .. scraper + if vim.fn.filereadable(scraper_path) == 1 then + vim.health.ok("Scraper found: " .. scraper) + else + vim.health.error("Missing scraper: " .. scraper) + end + end +end + +local function check_luasnip() + local has_luasnip, luasnip = pcall(require, "luasnip") + if has_luasnip then + vim.health.ok("LuaSnip integration available") + local snippet_count = #luasnip.get_snippets("all") + vim.health.info("LuaSnip snippets loaded: " .. snippet_count) + else + vim.health.info("LuaSnip not available - template expansion will be limited") + end +end + +local function check_directories() + local cwd = vim.fn.getcwd() + local build_dir = cwd .. "/build" + local io_dir = cwd .. "/io" + + if vim.fn.isdirectory(build_dir) == 1 then + vim.health.ok("Build directory exists: " .. build_dir) + else + vim.health.info("Build directory will be created when needed") + end + + if vim.fn.isdirectory(io_dir) == 1 then + vim.health.ok("IO directory exists: " .. io_dir) + else + vim.health.info("IO directory will be created when needed") + end +end + +local function check_config() + local cp = require("cp") + if cp.is_initialized() then + vim.health.ok("Plugin initialized") + + if vim.g.cp_contest then + vim.health.info("Current contest: " .. vim.g.cp_contest) + else + vim.health.info("No contest mode set") + end + else + vim.health.warn("Plugin not initialized - configuration may be incomplete") + end +end + +function M.check() + vim.health.start("cp.nvim health check") + + check_nvim_version() + check_uv() + check_python_env() + check_scrapers() + check_luasnip() + check_directories() + check_config() +end + +return M diff --git a/lua/cp/init.lua b/lua/cp/init.lua index e8ac6a9..fa124b9 100644 --- a/lua/cp/init.lua +++ b/lua/cp/init.lua @@ -214,66 +214,57 @@ end local initialized = false +function M.is_initialized() + return initialized +end + function M.setup(user_config) if initialized and not user_config then return end config = config_module.setup(user_config) - snippets.setup(config) + initialized = true +end - if initialized then +function M.handle_command(opts) + local args = opts.fargs + if #args == 0 then + log("Usage: :CP ", vim.log.levels.ERROR) return end - initialized = true - vim.api.nvim_create_user_command("CP", function(opts) - local args = opts.fargs - if #args == 0 then - log("Usage: :CP ", vim.log.levels.ERROR) - return - end + local cmd = args[1] - local cmd = args[1] - - if vim.tbl_contains(competition_types, cmd) then - if args[2] then - setup_contest(cmd) - if (cmd == "atcoder" or cmd == "codeforces") and args[3] then - setup_problem(args[2], args[3]) - else - setup_problem(args[2]) - end + if vim.tbl_contains(competition_types, cmd) then + if args[2] then + setup_contest(cmd) + if (cmd == "atcoder" or cmd == "codeforces") and args[3] then + setup_problem(args[2], args[3]) else - setup_contest(cmd) + setup_problem(args[2]) end - elseif cmd == "run" then - run_problem() - elseif cmd == "debug" then - debug_problem() - elseif cmd == "diff" then - diff_problem() else - if vim.g.cp_contest then - if (vim.g.cp_contest == "atcoder" or vim.g.cp_contest == "codeforces") and args[2] then - setup_problem(cmd, args[2]) - else - setup_problem(cmd) - end - else - log("no contest mode set. run :CP first or use full command", vim.log.levels.ERROR) - end + setup_contest(cmd) end - end, { - nargs = "*", - complete = function(ArgLead, _, _) - local commands = vim.list_extend(vim.deepcopy(competition_types), { "run", "debug", "diff" }) - return vim.tbl_filter(function(cmd) - return cmd:find(ArgLead, 1, true) == 1 - end, commands) - end, - }) + elseif cmd == "run" then + run_problem() + elseif cmd == "debug" then + debug_problem() + elseif cmd == "diff" then + diff_problem() + else + if vim.g.cp_contest then + if (vim.g.cp_contest == "atcoder" or vim.g.cp_contest == "codeforces") and args[2] then + setup_problem(cmd, args[2]) + else + setup_problem(cmd) + end + else + log("no contest mode set. run :CP first or use full command", vim.log.levels.ERROR) + end + end end return M diff --git a/plugin/cp.lua b/plugin/cp.lua index 7ba14b7..9fc7e28 100644 --- a/plugin/cp.lua +++ b/plugin/cp.lua @@ -3,4 +3,20 @@ if vim.g.loaded_cp then end vim.g.loaded_cp = 1 -require("cp").setup() +local competition_types = { "atcoder", "codeforces", "cses" } + +vim.api.nvim_create_user_command("CP", function(opts) + local cp = require("cp") + if not cp.is_initialized() then + cp.setup() + end + cp.handle_command(opts) +end, { + nargs = "*", + complete = function(ArgLead, _, _) + local commands = vim.list_extend(vim.deepcopy(competition_types), { "run", "debug", "diff" }) + return vim.tbl_filter(function(cmd) + return cmd:find(ArgLead, 1, true) == 1 + end, commands) + end, +})