From add022af8cda4292e3cee308aad48c6c964b4bb1 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Tue, 3 Mar 2026 00:31:42 -0500 Subject: [PATCH] refactor(hooks): replace flat hooks API with setup/on namespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: the hooks API conflated distinct lifecycle scopes under a flat table with inconsistent naming (setup_code, before_run, setup_io_input), making it hard to reason about when each hook fires. Solution: introduce two namespaces — hooks.setup.{contest,code,io} for one-time initialization and hooks.on.{enter,run,debug} for recurring events. hooks.setup.contest fires once when a contest dir is newly created; hooks.on.enter is registered as a buffer-scoped BufEnter autocmd and fires immediately after setup.code. The provisional buffer setup_code callsite is removed as it ran on an unresolved temp buffer. --- lua/cp/config.lua | 66 ++++++++++++++++++++++++++++++++++----------- lua/cp/setup.lua | 52 ++++++++++++++++++++++------------- lua/cp/ui/views.lua | 23 +++++++--------- 3 files changed, 95 insertions(+), 46 deletions(-) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 280ecd5..7ca81eb 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -33,12 +33,23 @@ ---@class DiffConfig ---@field git DiffGitConfig +---@class CpSetupIOHooks +---@field input? fun(bufnr: integer, state: cp.State) +---@field output? fun(bufnr: integer, state: cp.State) + +---@class CpSetupHooks +---@field contest? fun(state: cp.State) +---@field code? fun(state: cp.State) +---@field io? CpSetupIOHooks + +---@class CpOnHooks +---@field enter? fun(state: cp.State) +---@field run? fun(state: cp.State) +---@field debug? fun(state: cp.State) + ---@class Hooks ----@field before_run? fun(state: cp.State) ----@field before_debug? fun(state: cp.State) ----@field setup_code? fun(state: cp.State) ----@field setup_io_input? fun(bufnr: integer, state: cp.State) ----@field setup_io_output? fun(bufnr: integer, state: cp.State) +---@field setup? CpSetupHooks +---@field on? CpOnHooks ---@class VerdictFormatData ---@field index integer @@ -156,11 +167,19 @@ M.defaults = { }, }, hooks = { - before_run = nil, - before_debug = nil, - setup_code = nil, - setup_io_input = helpers.clearcol, - setup_io_output = helpers.clearcol, + setup = { + contest = nil, + code = nil, + io = { + input = helpers.clearcol, + output = helpers.clearcol, + }, + }, + on = { + enter = nil, + run = nil, + debug = nil, + }, }, debug = false, scrapers = constants.PLATFORMS, @@ -352,12 +371,29 @@ function M.setup(user_config) end, ('one of {%s}'):format(table.concat(constants.PLATFORMS, ',')), }, - before_run = { cfg.hooks.before_run, { 'function', 'nil' }, true }, - before_debug = { cfg.hooks.before_debug, { 'function', 'nil' }, true }, - setup_code = { cfg.hooks.setup_code, { 'function', 'nil' }, true }, - setup_io_input = { cfg.hooks.setup_io_input, { 'function', 'nil' }, true }, - setup_io_output = { cfg.hooks.setup_io_output, { 'function', 'nil' }, true }, }) + if cfg.hooks.setup ~= nil then + vim.validate({ setup = { cfg.hooks.setup, 'table' } }) + vim.validate({ + contest = { cfg.hooks.setup.contest, { 'function', 'nil' }, true }, + code = { cfg.hooks.setup.code, { 'function', 'nil' }, true }, + }) + if cfg.hooks.setup.io ~= nil then + vim.validate({ io = { cfg.hooks.setup.io, 'table' } }) + vim.validate({ + input = { cfg.hooks.setup.io.input, { 'function', 'nil' }, true }, + output = { cfg.hooks.setup.io.output, { 'function', 'nil' }, true }, + }) + end + end + if cfg.hooks.on ~= nil then + vim.validate({ on = { cfg.hooks.on, 'table' } }) + vim.validate({ + enter = { cfg.hooks.on.enter, { 'function', 'nil' }, true }, + run = { cfg.hooks.on.run, { 'function', 'nil' }, true }, + debug = { cfg.hooks.on.debug, { 'function', 'nil' }, true }, + }) + end local layouts = require('cp.ui.layouts') vim.validate({ diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 43a50c0..46c3dc0 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -196,13 +196,6 @@ function M.setup_contest(platform, contest_id, problem_id, language) state.set_language(lang) - if cfg.hooks and cfg.hooks.setup_code and not vim.b[bufnr].cp_setup_done then - local ok = pcall(cfg.hooks.setup_code, state) - if ok then - vim.b[bufnr].cp_setup_done = true - end - end - state.set_provisional({ bufnr = bufnr, platform = platform, @@ -281,7 +274,15 @@ function M.setup_problem(problem_id, language) return end - vim.fn.mkdir(vim.fn.fnamemodify(source_file, ':h'), 'p') + local contest_dir = vim.fn.fnamemodify(source_file, ':h') + local is_new_dir = vim.fn.isdirectory(contest_dir) == 0 + vim.fn.mkdir(contest_dir, 'p') + if is_new_dir then + local s = config.hooks and config.hooks.setup + if s and s.contest then + pcall(s.contest, state) + end + end local prov = state.get_provisional() if prov and prov.platform == platform and prov.contest_id == (state.get_contest_id() or '') then @@ -302,15 +303,23 @@ function M.setup_problem(problem_id, language) state.set_solution_win(vim.api.nvim_get_current_win()) if not vim.b[prov.bufnr].cp_setup_done then apply_template(prov.bufnr, lang, platform) - if config.hooks and config.hooks.setup_code then - local ok = pcall(config.hooks.setup_code, state) - if ok then - vim.b[prov.bufnr].cp_setup_done = true - end + local s = config.hooks and config.hooks.setup + if s and s.code then + local ok = pcall(s.code, state) + if ok then vim.b[prov.bufnr].cp_setup_done = true end else helpers.clearcol(prov.bufnr) vim.b[prov.bufnr].cp_setup_done = true end + local o = config.hooks and config.hooks.on + if o and o.enter then + local bufnr = prov.bufnr + vim.api.nvim_create_autocmd('BufEnter', { + buffer = bufnr, + callback = function() pcall(o.enter, state) end, + }) + pcall(o.enter, state) + end end cache.set_file_state( vim.fn.fnamemodify(source_file, ':p'), @@ -339,15 +348,22 @@ function M.setup_problem(problem_id, language) if is_new then apply_template(bufnr, lang, platform) end - if config.hooks and config.hooks.setup_code then - local ok = pcall(config.hooks.setup_code, state) - if ok then - vim.b[bufnr].cp_setup_done = true - end + local s = config.hooks and config.hooks.setup + if s and s.code then + local ok = pcall(s.code, state) + if ok then vim.b[bufnr].cp_setup_done = true end else helpers.clearcol(bufnr) vim.b[bufnr].cp_setup_done = true end + local o = config.hooks and config.hooks.on + if o and o.enter then + vim.api.nvim_create_autocmd('BufEnter', { + buffer = bufnr, + callback = function() pcall(o.enter, state) end, + }) + pcall(o.enter, state) + end end cache.set_file_state( vim.fn.expand('%:p'), diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index 2d67569..c6d1281 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -444,12 +444,12 @@ function M.ensure_io_view() local cfg = config_module.get_config() - if cfg.hooks and cfg.hooks.setup_io_output then - pcall(cfg.hooks.setup_io_output, output_buf, state) + local io = cfg.hooks and cfg.hooks.setup and cfg.hooks.setup.io + if io and io.output then + pcall(io.output, output_buf, state) end - - if cfg.hooks and cfg.hooks.setup_io_input then - pcall(cfg.hooks.setup_io_input, input_buf, state) + if io and io.input then + pcall(io.input, input_buf, state) end local test_cases = cache.get_test_cases(platform, contest_id, problem_id) @@ -958,15 +958,12 @@ function M.toggle_panel(panel_opts) setup_keybindings_for_buffer(test_buffers.tab_buf) - if config.hooks and config.hooks.before_run then - vim.schedule_wrap(function() - config.hooks.before_run(state) - end) + local o = config.hooks and config.hooks.on + if o and o.run then + vim.schedule(function() o.run(state) end) end - if panel_opts and panel_opts.debug and config.hooks and config.hooks.before_debug then - vim.schedule_wrap(function() - config.hooks.before_debug(state) - end) + if panel_opts and panel_opts.debug and o and o.debug then + vim.schedule(function() o.debug(state) end) end vim.api.nvim_set_current_win(test_windows.tab_win)