diff --git a/doc/cp.txt b/doc/cp.txt index 3e816a4..10e69c3 100644 --- a/doc/cp.txt +++ b/doc/cp.txt @@ -152,7 +152,7 @@ Here's an example configuration with lazy.nvim: >lua diff_mode = 'vim', next_test_key = '', prev_test_key = '', - toggle_diff_key = 't', + toggle_diff_key = '', max_output_lines = 50, }, diff = { @@ -203,18 +203,19 @@ Here's an example configuration with lazy.nvim: >lua *cp.RunPanelConfig* Fields: ~ - {ansi} (boolean, default: true) Enable ANSI color parsing and - highlighting. When true, compiler output and test results - display with colored syntax highlighting. When false, - ANSI escape codes are stripped for plain text display. - Requires vim.g.terminal_color_* to be configured for - proper color display. - {diff_mode} (string, default: "none") Diff backend: "none", "vim", or "git". - "none" displays plain buffers without highlighting, - "vim" uses built-in diff, "git" provides character-level precision. - {next_test_key} (string, default: "") Key to navigate to next test case. - {prev_test_key} (string, default: "") Key to navigate to previous test case. - {toggle_diff_key} (string, default: "t") Key to cycle through diff modes. + {ansi} (boolean, default: true) Enable ANSI color parsing and + highlighting. When true, compiler output and test results + display with colored syntax highlighting. When false, + ANSI escape codes are stripped for plain text display. + Requires vim.g.terminal_color_* to be configured for + proper color display. + {diff_mode} (string, default: "none") Diff backend: "none", "vim", or "git". + "none" displays plain buffers without highlighting, + "vim" uses built-in diff, "git" provides character-level precision. + {next_test_key} (string, default: "") Key to navigate to next test case. + {prev_test_key} (string, default: "") Key to navigate to previous test case. + {toggle_diff_key} (string, default: "") Key to cycle through diff modes. + {close_key} (string, default: "") Close the run panel/interactive terminal {max_output_lines} (number, default: 50) Maximum lines of test output. *cp.DiffConfig* @@ -531,9 +532,9 @@ RUN PANEL KEYMAPS *cp-test-keys* run_panel.next_test_key) Navigate to previous test case (configurable via run_panel.prev_test_key) -t Cycle through diff modes: none → git → vim (configurable + Cycle through diff modes: none → git → vim (configurable via run_panel.toggle_diff_key) -q Exit test panel and restore layout + Exit run panel/interactive terminal and restore layout Diff Modes ~ diff --git a/lua/cp/commands/init.lua b/lua/cp/commands/init.lua index 632411e..88c1f24 100644 --- a/lua/cp/commands/init.lua +++ b/lua/cp/commands/init.lua @@ -126,7 +126,9 @@ function M.handle_command(opts) local setup = require('cp.setup') local ui = require('cp.ui.panel') - if cmd.action == 'run' then + if cmd.action == 'interact' then + ui.toggle_interactive() + elseif cmd.action == 'run' then ui.toggle_run_panel(cmd.debug) elseif cmd.action == 'next' then setup.navigate_problem(1, cmd.language) diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 8efddb8..786d570 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -34,6 +34,7 @@ ---@field next_test_key string Key to navigate to next test case ---@field prev_test_key string Key to navigate to previous test case ---@field toggle_diff_key string Key to cycle through diff modes +---@field close_key string Key to close panel/interactive terminal ---@field max_output_lines number Maximum lines of test output to display ---@class DiffGitConfig @@ -103,7 +104,8 @@ M.defaults = { diff_mode = 'none', next_test_key = '', prev_test_key = '', - toggle_diff_key = 't', + toggle_diff_key = '', + close_key = '', max_output_lines = 50, }, diff = { @@ -229,6 +231,13 @@ function M.setup(user_config) end, 'toggle_diff_key must be a non-empty string', }, + close_key = { + config.run_panel.close_key, + function(value) + return type(value) == 'string' and value ~= '' + end, + 'close_key must be a non-empty string', + }, max_output_lines = { config.run_panel.max_output_lines, function(value) diff --git a/lua/cp/constants.lua b/lua/cp/constants.lua index 7544435..7d81242 100644 --- a/lua/cp/constants.lua +++ b/lua/cp/constants.lua @@ -1,7 +1,7 @@ local M = {} M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' } -M.ACTIONS = { 'run', 'next', 'prev', 'pick', 'cache' } +M.ACTIONS = { 'run', 'next', 'prev', 'pick', 'cache', 'interact' } M.PLATFORM_DISPLAY_NAMES = { atcoder = 'AtCoder', diff --git a/lua/cp/state.lua b/lua/cp/state.lua index 0cd74eb..ca38189 100644 --- a/lua/cp/state.lua +++ b/lua/cp/state.lua @@ -7,8 +7,6 @@ ---@field set_problem_id fun(problem_id: string) ---@field get_test_cases fun(): table[]? ---@field set_test_cases fun(test_cases: table[]) ----@field is_run_panel_active fun(): boolean ----@field set_run_panel_active fun(active: boolean) ---@field get_saved_session fun(): table? ---@field set_saved_session fun(session: table) ---@field get_context fun(): {platform: string?, contest_id: string?, problem_id: string?} @@ -28,8 +26,8 @@ local state = { contest_id = nil, problem_id = nil, test_cases = nil, - run_panel_active = false, saved_session = nil, + active_panel = nil, } function M.get_platform() @@ -64,14 +62,6 @@ function M.set_test_cases(test_cases) state.test_cases = test_cases end -function M.is_run_panel_active() - return state.run_panel_active -end - -function M.set_run_panel_active(active) - state.run_panel_active = active -end - function M.get_saved_session() return state.saved_session end @@ -149,6 +139,14 @@ function M.has_context() return state.platform and state.contest_id end +function M.get_active_panel() + return state.active_panel +end + +function M.set_active_panel(panel) + state.active_panel = panel +end + function M.reset() state.platform = nil state.contest_id = nil diff --git a/lua/cp/ui/panel.lua b/lua/cp/ui/panel.lua index 70ae2fa..8cb1bae 100644 --- a/lua/cp/ui/panel.lua +++ b/lua/cp/ui/panel.lua @@ -9,8 +9,66 @@ local state = require('cp.state') local current_diff_layout = nil local current_mode = nil +function M.toggle_interactive() + if state.get_active_panel() == 'interactive' then + if state.interactive_buf and vim.api.nvim_buf_is_valid(state.interactive_buf) then + local job = vim.b[state.interactive_buf].terminal_job_id + if job then + vim.fn.jobstop(job) + end + end + if state.saved_interactive_session then + vim.cmd(('source %s'):format(state.saved_interactive_session)) + vim.fn.delete(state.saved_interactive_session) + state.saved_interactive_session = nil + end + state.set_active_panel(nil) + logger.log('interactive closed') + return + end + + if state.get_active_panel() then + logger.log('another panel is already active', vim.log.levels.ERROR) + return + end + + state.saved_interactive_session = vim.fn.tempname() + vim.cmd(('mksession! %s'):format(state.saved_interactive_session)) + vim.cmd('silent only') + + local config = config_module.get_config() + local contest_config = config.contests[state.get_platform() or ''] + local execute = require('cp.runner.execute') + local compile_result = execute.compile_problem(contest_config, false) + if not compile_result.success then + require('cp.runner.run').handle_compilation_failure(compile_result.output) + return + end + + local binary = state.get_binary_file() + if not binary then + logger.log('no binary path found', vim.log.levels.ERROR) + return + end + + vim.cmd('terminal') + local term_buf = vim.api.nvim_get_current_buf() + local term_win = vim.api.nvim_get_current_win() + + vim.fn.chansend(vim.b.terminal_job_id, binary .. '\n') + + vim.keymap.set('t', config.run_panel.close_key, function() + M.toggle_interactive() + end, { buffer = term_buf, silent = true }) + + state.interactive_buf = term_buf + state.interactive_win = term_win + state.set_active_panel('interactive') + logger.log(('interactive opened, running %s'):format(binary)) +end + function M.toggle_run_panel(is_debug) - if state.is_run_panel_active() then + if state.get_active_panel() == 'run' then if current_diff_layout then current_diff_layout.cleanup() current_diff_layout = nil @@ -21,12 +79,16 @@ function M.toggle_run_panel(is_debug) vim.fn.delete(state.saved_session) state.saved_session = nil end - - state.set_run_panel_active(false) + state.set_active_panel(nil) logger.log('test panel closed') return end + if state.get_active_panel() then + logger.log('another panel is already active', vim.log.levels.ERROR) + return + end + if not state.get_platform() then logger.log( 'No contest configured. Use :CP to set up first.', @@ -55,13 +117,11 @@ function M.toggle_run_panel(is_debug) if config.hooks and config.hooks.before_run then config.hooks.before_run(state) end - if is_debug and config.hooks and config.hooks.before_debug then config.hooks.before_debug(state) end local run = require('cp.runner.run') - local input_file = state.get_input_file() logger.log(('run panel: checking test cases for %s'):format(input_file or 'none')) @@ -72,7 +132,6 @@ function M.toggle_run_panel(is_debug) state.saved_session = vim.fn.tempname() vim.cmd(('mksession! %s'):format(state.saved_session)) - vim.cmd('silent only') local tab_buf = buffer_utils.create_buffer_with_options() @@ -80,13 +139,8 @@ function M.toggle_run_panel(is_debug) vim.api.nvim_win_set_buf(main_win, tab_buf) vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf }) - local test_windows = { - tab_win = main_win, - } - local test_buffers = { - tab_buf = tab_buf, - } - + local test_windows = { tab_win = main_win } + local test_buffers = { tab_buf = tab_buf } local test_list_namespace = vim.api.nvim_create_namespace('cp_test_list') local setup_keybindings_for_buffer @@ -106,10 +160,8 @@ function M.toggle_run_panel(is_debug) if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then return end - local run_render = require('cp.runner.run_render') run_render.setup_highlights() - local test_state = run.get_run_panel_state() local tab_lines, tab_highlights = run_render.render_test_list(test_state) buffer_utils.update_buffer_content( @@ -118,7 +170,6 @@ function M.toggle_run_panel(is_debug) tab_highlights, test_list_namespace ) - update_diff_panes() end @@ -127,14 +178,12 @@ function M.toggle_run_panel(is_debug) if #test_state.test_cases == 0 then return end - test_state.current_index = (test_state.current_index + delta) % #test_state.test_cases - refresh_run_panel() end setup_keybindings_for_buffer = function(buf) - vim.keymap.set('n', 'q', function() + vim.keymap.set('n', config.run_panel.close_key, function() M.toggle_run_panel() end, { buffer = buf, silent = true }) vim.keymap.set('n', config.run_panel.toggle_diff_key, function() @@ -189,10 +238,9 @@ function M.toggle_run_panel(is_debug) end) vim.api.nvim_set_current_win(test_windows.tab_win) - - state.set_run_panel_active(true) state.test_buffers = test_buffers state.test_windows = test_windows + state.set_active_panel('run') local test_state = run.get_run_panel_state() logger.log( string.format('test panel opened (%d test cases)', #test_state.test_cases), diff --git a/spec/command_parsing_spec.lua b/spec/command_parsing_spec.lua index 775b5dc..c1a8d97 100644 --- a/spec/command_parsing_spec.lua +++ b/spec/command_parsing_spec.lua @@ -33,18 +33,15 @@ describe('cp command parsing', function() get_problem_id = function() return 'a' end, - is_run_panel_active = function() - return false - end, set_platform = function() end, set_contest_id = function() end, set_problem_id = function() end, - set_run_panel_active = function() end, } package.loaded['cp.state'] = mock_state local mock_ui_panel = { toggle_run_panel = function() end, + toggle_interactive = function() end, } package.loaded['cp.ui.panel'] = mock_ui_panel