Merge pull request #111 from barrett-ruth/feat/interact

Feat/interact
This commit is contained in:
Barrett Ruth 2025-09-26 15:07:59 +02:00 committed by GitHub
commit 83645b48be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 109 additions and 54 deletions

View file

@ -152,7 +152,7 @@ Here's an example configuration with lazy.nvim: >lua
diff_mode = 'vim',
next_test_key = '<c-n>',
prev_test_key = '<c-p>',
toggle_diff_key = 't',
toggle_diff_key = '<c-q>',
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: "<c-n>") Key to navigate to next test case.
{prev_test_key} (string, default: "<c-p>") 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: "<c-n>") Key to navigate to next test case.
{prev_test_key} (string, default: "<c-p>") Key to navigate to previous test case.
{toggle_diff_key} (string, default: "<c-t>") Key to cycle through diff modes.
{close_key} (string, default: "<c-q>") 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)
<c-p> Navigate to previous test case (configurable via
run_panel.prev_test_key)
t Cycle through diff modes: none → git → vim (configurable
<c-t> Cycle through diff modes: none → git → vim (configurable
via run_panel.toggle_diff_key)
q Exit test panel and restore layout
<c-q> Exit run panel/interactive terminal and restore layout
Diff Modes ~

View file

@ -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)

View file

@ -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 = '<c-n>',
prev_test_key = '<c-p>',
toggle_diff_key = 't',
toggle_diff_key = '<c-t>',
close_key = '<c-q>',
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)

View file

@ -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',

View file

@ -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

View file

@ -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 <platform> <contest> <problem> 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),

View file

@ -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