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', diff_mode = 'vim',
next_test_key = '<c-n>', next_test_key = '<c-n>',
prev_test_key = '<c-p>', prev_test_key = '<c-p>',
toggle_diff_key = 't', toggle_diff_key = '<c-q>',
max_output_lines = 50, max_output_lines = 50,
}, },
diff = { diff = {
@ -203,18 +203,19 @@ Here's an example configuration with lazy.nvim: >lua
*cp.RunPanelConfig* *cp.RunPanelConfig*
Fields: ~ Fields: ~
{ansi} (boolean, default: true) Enable ANSI color parsing and {ansi} (boolean, default: true) Enable ANSI color parsing and
highlighting. When true, compiler output and test results highlighting. When true, compiler output and test results
display with colored syntax highlighting. When false, display with colored syntax highlighting. When false,
ANSI escape codes are stripped for plain text display. ANSI escape codes are stripped for plain text display.
Requires vim.g.terminal_color_* to be configured for Requires vim.g.terminal_color_* to be configured for
proper color display. proper color display.
{diff_mode} (string, default: "none") Diff backend: "none", "vim", or "git". {diff_mode} (string, default: "none") Diff backend: "none", "vim", or "git".
"none" displays plain buffers without highlighting, "none" displays plain buffers without highlighting,
"vim" uses built-in diff, "git" provides character-level precision. "vim" uses built-in diff, "git" provides character-level precision.
{next_test_key} (string, default: "<c-n>") Key to navigate to next test case. {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. {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. {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. {max_output_lines} (number, default: 50) Maximum lines of test output.
*cp.DiffConfig* *cp.DiffConfig*
@ -531,9 +532,9 @@ RUN PANEL KEYMAPS *cp-test-keys*
run_panel.next_test_key) run_panel.next_test_key)
<c-p> Navigate to previous test case (configurable via <c-p> Navigate to previous test case (configurable via
run_panel.prev_test_key) 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) 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 ~ Diff Modes ~

View file

@ -126,7 +126,9 @@ function M.handle_command(opts)
local setup = require('cp.setup') local setup = require('cp.setup')
local ui = require('cp.ui.panel') 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) ui.toggle_run_panel(cmd.debug)
elseif cmd.action == 'next' then elseif cmd.action == 'next' then
setup.navigate_problem(1, cmd.language) 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 next_test_key string Key to navigate to next test case
---@field prev_test_key string Key to navigate to previous 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 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 ---@field max_output_lines number Maximum lines of test output to display
---@class DiffGitConfig ---@class DiffGitConfig
@ -103,7 +104,8 @@ M.defaults = {
diff_mode = 'none', diff_mode = 'none',
next_test_key = '<c-n>', next_test_key = '<c-n>',
prev_test_key = '<c-p>', prev_test_key = '<c-p>',
toggle_diff_key = 't', toggle_diff_key = '<c-t>',
close_key = '<c-q>',
max_output_lines = 50, max_output_lines = 50,
}, },
diff = { diff = {
@ -229,6 +231,13 @@ function M.setup(user_config)
end, end,
'toggle_diff_key must be a non-empty string', '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 = { max_output_lines = {
config.run_panel.max_output_lines, config.run_panel.max_output_lines,
function(value) function(value)

View file

@ -1,7 +1,7 @@
local M = {} local M = {}
M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' } M.PLATFORMS = { 'atcoder', 'codeforces', 'cses' }
M.ACTIONS = { 'run', 'next', 'prev', 'pick', 'cache' } M.ACTIONS = { 'run', 'next', 'prev', 'pick', 'cache', 'interact' }
M.PLATFORM_DISPLAY_NAMES = { M.PLATFORM_DISPLAY_NAMES = {
atcoder = 'AtCoder', atcoder = 'AtCoder',

View file

@ -7,8 +7,6 @@
---@field set_problem_id fun(problem_id: string) ---@field set_problem_id fun(problem_id: string)
---@field get_test_cases fun(): table[]? ---@field get_test_cases fun(): table[]?
---@field set_test_cases fun(test_cases: 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 get_saved_session fun(): table?
---@field set_saved_session fun(session: table) ---@field set_saved_session fun(session: table)
---@field get_context fun(): {platform: string?, contest_id: string?, problem_id: string?} ---@field get_context fun(): {platform: string?, contest_id: string?, problem_id: string?}
@ -28,8 +26,8 @@ local state = {
contest_id = nil, contest_id = nil,
problem_id = nil, problem_id = nil,
test_cases = nil, test_cases = nil,
run_panel_active = false,
saved_session = nil, saved_session = nil,
active_panel = nil,
} }
function M.get_platform() function M.get_platform()
@ -64,14 +62,6 @@ function M.set_test_cases(test_cases)
state.test_cases = test_cases state.test_cases = test_cases
end 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() function M.get_saved_session()
return state.saved_session return state.saved_session
end end
@ -149,6 +139,14 @@ function M.has_context()
return state.platform and state.contest_id return state.platform and state.contest_id
end 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() function M.reset()
state.platform = nil state.platform = nil
state.contest_id = nil state.contest_id = nil

View file

@ -9,8 +9,66 @@ local state = require('cp.state')
local current_diff_layout = nil local current_diff_layout = nil
local current_mode = 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) 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 if current_diff_layout then
current_diff_layout.cleanup() current_diff_layout.cleanup()
current_diff_layout = nil current_diff_layout = nil
@ -21,12 +79,16 @@ function M.toggle_run_panel(is_debug)
vim.fn.delete(state.saved_session) vim.fn.delete(state.saved_session)
state.saved_session = nil state.saved_session = nil
end end
state.set_active_panel(nil)
state.set_run_panel_active(false)
logger.log('test panel closed') logger.log('test panel closed')
return return
end 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 if not state.get_platform() then
logger.log( logger.log(
'No contest configured. Use :CP <platform> <contest> <problem> to set up first.', '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 if config.hooks and config.hooks.before_run then
config.hooks.before_run(state) config.hooks.before_run(state)
end end
if is_debug and config.hooks and config.hooks.before_debug then if is_debug and config.hooks and config.hooks.before_debug then
config.hooks.before_debug(state) config.hooks.before_debug(state)
end end
local run = require('cp.runner.run') local run = require('cp.runner.run')
local input_file = state.get_input_file() local input_file = state.get_input_file()
logger.log(('run panel: checking test cases for %s'):format(input_file or 'none')) 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() state.saved_session = vim.fn.tempname()
vim.cmd(('mksession! %s'):format(state.saved_session)) vim.cmd(('mksession! %s'):format(state.saved_session))
vim.cmd('silent only') vim.cmd('silent only')
local tab_buf = buffer_utils.create_buffer_with_options() 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_win_set_buf(main_win, tab_buf)
vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf }) vim.api.nvim_set_option_value('filetype', 'cptest', { buf = tab_buf })
local test_windows = { local test_windows = { tab_win = main_win }
tab_win = main_win, local test_buffers = { tab_buf = tab_buf }
}
local test_buffers = {
tab_buf = tab_buf,
}
local test_list_namespace = vim.api.nvim_create_namespace('cp_test_list') local test_list_namespace = vim.api.nvim_create_namespace('cp_test_list')
local setup_keybindings_for_buffer 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 if not test_buffers.tab_buf or not vim.api.nvim_buf_is_valid(test_buffers.tab_buf) then
return return
end end
local run_render = require('cp.runner.run_render') local run_render = require('cp.runner.run_render')
run_render.setup_highlights() run_render.setup_highlights()
local test_state = run.get_run_panel_state() local test_state = run.get_run_panel_state()
local tab_lines, tab_highlights = run_render.render_test_list(test_state) local tab_lines, tab_highlights = run_render.render_test_list(test_state)
buffer_utils.update_buffer_content( buffer_utils.update_buffer_content(
@ -118,7 +170,6 @@ function M.toggle_run_panel(is_debug)
tab_highlights, tab_highlights,
test_list_namespace test_list_namespace
) )
update_diff_panes() update_diff_panes()
end end
@ -127,14 +178,12 @@ function M.toggle_run_panel(is_debug)
if #test_state.test_cases == 0 then if #test_state.test_cases == 0 then
return return
end end
test_state.current_index = (test_state.current_index + delta) % #test_state.test_cases test_state.current_index = (test_state.current_index + delta) % #test_state.test_cases
refresh_run_panel() refresh_run_panel()
end end
setup_keybindings_for_buffer = function(buf) 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() M.toggle_run_panel()
end, { buffer = buf, silent = true }) end, { buffer = buf, silent = true })
vim.keymap.set('n', config.run_panel.toggle_diff_key, function() vim.keymap.set('n', config.run_panel.toggle_diff_key, function()
@ -189,10 +238,9 @@ function M.toggle_run_panel(is_debug)
end) end)
vim.api.nvim_set_current_win(test_windows.tab_win) vim.api.nvim_set_current_win(test_windows.tab_win)
state.set_run_panel_active(true)
state.test_buffers = test_buffers state.test_buffers = test_buffers
state.test_windows = test_windows state.test_windows = test_windows
state.set_active_panel('run')
local test_state = run.get_run_panel_state() local test_state = run.get_run_panel_state()
logger.log( logger.log(
string.format('test panel opened (%d test cases)', #test_state.test_cases), 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() get_problem_id = function()
return 'a' return 'a'
end, end,
is_run_panel_active = function()
return false
end,
set_platform = function() end, set_platform = function() end,
set_contest_id = function() end, set_contest_id = function() end,
set_problem_id = function() end, set_problem_id = function() end,
set_run_panel_active = function() end,
} }
package.loaded['cp.state'] = mock_state package.loaded['cp.state'] = mock_state
local mock_ui_panel = { local mock_ui_panel = {
toggle_run_panel = function() end, toggle_run_panel = function() end,
toggle_interactive = function() end,
} }
package.loaded['cp.ui.panel'] = mock_ui_panel package.loaded['cp.ui.panel'] = mock_ui_panel