diff --git a/doc/cp.nvim.txt b/doc/cp.nvim.txt index 570beb5..866346f 100644 --- a/doc/cp.nvim.txt +++ b/doc/cp.nvim.txt @@ -258,7 +258,7 @@ Here's an example configuration with lazy.nvim: prev_test_key = '', -- or nil to disable }, panel = { - diff_mode = 'vim', + diff_modes = { 'side-by-side', 'git', 'vim' }, max_output_lines = 50, }, diff = { @@ -378,8 +378,10 @@ run CSES problems with Rust using the single schema: *cp.PanelConfig* Fields: ~ - {diff_mode} (string, default: "none") Diff backend: "none", - "vim", or "git". + {diff_modes} (string[], default: {'side-by-side', 'git', 'vim'}) + List of diff modes to cycle through with 't' key. + First element is the default mode. + Valid modes: 'side-by-side', 'git', 'vim'. {max_output_lines} (number, default: 50) Maximum lines of test output. *cp.DiffConfig* @@ -851,17 +853,20 @@ PANEL KEYMAPS *cp-panel-keys* Navigate to next test case Navigate to previous test case -t Cycle through diff modes: none → git → vim +t Cycle through configured diff modes (see |cp.PanelConfig|) q Exit panel and restore layout Exit interactive terminal and restore layout Diff Modes ~ -Three diff backends are available: +Three diff modes are available: - none Nothing - vim Built-in vim diff (default, always available) - git Character-level git word-diff (requires git, more precise) + side-by-side Expected and actual output shown side-by-side (default) + vim Built-in vim diff (always available) + git Character-level git word-diff (requires git, more precise) + +Configure which modes to cycle through via |cp.PanelConfig|.diff_modes. +The first element is used as the default mode. The git backend shows character-level changes with [-removed-] and {+added+} markers. diff --git a/lua/cp/config.lua b/lua/cp/config.lua index 78f321f..ca5b027 100644 --- a/lua/cp/config.lua +++ b/lua/cp/config.lua @@ -18,7 +18,7 @@ ---@field overrides? table ---@class PanelConfig ----@field diff_mode "none"|"vim"|"git" +---@field diff_modes string[] ---@field max_output_lines integer ---@class DiffGitConfig @@ -173,7 +173,7 @@ M.defaults = { add_test_key = 'ga', save_and_exit_key = 'q', }, - panel = { diff_mode = 'none', max_output_lines = 50 }, + panel = { diff_modes = { 'side-by-side', 'git', 'vim' }, max_output_lines = 50 }, diff = { git = { args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' }, @@ -313,15 +313,26 @@ function M.setup(user_config) setup_io_output = { cfg.hooks.setup_io_output, { 'function', 'nil' }, true }, }) + local layouts = require('cp.ui.layouts') + local valid_modes_str = table.concat(vim.tbl_keys(layouts.DIFF_MODES), ',') + if type(cfg.ui.panel.diff_modes) == 'table' then + local invalid = {} + for _, mode in ipairs(cfg.ui.panel.diff_modes) do + if not layouts.DIFF_MODES[mode] then + table.insert(invalid, mode) + end + end + if #invalid > 0 then + error( + ('invalid diff modes [%s] - must be one of: {%s}'):format( + table.concat(invalid, ','), + valid_modes_str + ) + ) + end + end vim.validate({ ansi = { cfg.ui.ansi, 'boolean' }, - diff_mode = { - cfg.ui.panel.diff_mode, - function(v) - return vim.tbl_contains({ 'none', 'vim', 'git' }, v) - end, - "diff_mode must be 'none', 'vim', or 'git'", - }, max_output_lines = { cfg.ui.panel.max_output_lines, function(v) @@ -383,6 +394,13 @@ function M.setup(user_config) end, 'nil or non-empty string', }, + picker = { + cfg.ui.picker, + function(v) + return v == nil or v == 'telescope' or v == 'fzf-lua' + end, + "nil, 'telescope', or 'fzf-lua'", + }, }) for id, lang in pairs(cfg.languages) do @@ -443,7 +461,18 @@ function M.get_language_for_platform(platform_id, language_id) } end - local effective = cfg.runtime.effective[platform_id][language_id] + local platform_effective = cfg.runtime.effective[platform_id] + if not platform_effective then + return { + valid = false, + error = string.format( + 'No runtime config for platform %s (plugin not initialized)', + platform_id + ), + } + end + + local effective = platform_effective[language_id] if not effective then return { valid = false, diff --git a/lua/cp/runner/execute.lua b/lua/cp/runner/execute.lua index a60ffa3..5c89c8a 100644 --- a/lua/cp/runner/execute.lua +++ b/lua/cp/runner/execute.lua @@ -177,6 +177,16 @@ function M.compile_problem(debug, on_complete) local language = state.get_language() or config.platforms[platform].default_language local eff = config.runtime.effective[platform][language] + local source_file = state.get_source_file() + if source_file then + local buf = vim.fn.bufnr(source_file) + if buf ~= -1 and vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].modified then + vim.api.nvim_buf_call(buf, function() + vim.cmd.write({ mods = { silent = true, noautocmd = true } }) + end) + end + end + local compile_config = (debug and eff.commands.debug) or eff.commands.build if not compile_config then diff --git a/lua/cp/setup.lua b/lua/cp/setup.lua index 56519e4..fce1a0c 100644 --- a/lua/cp/setup.lua +++ b/lua/cp/setup.lua @@ -82,7 +82,7 @@ local function start_tests(platform, contest_id, problems) return not vim.tbl_isempty(cache.get_test_cases(platform, contest_id, p.id)) end, problems) if cached_len ~= #problems then - logger.log(('Fetching problem test data... (%d/%d)'):format(cached_len, #problems)) + logger.log(('Fetching %s/%s problem tests...'):format(cached_len, #problems)) scraper.scrape_all_tests(platform, contest_id, function(ev) local cached_tests = {} if not ev.interactive and vim.tbl_isempty(ev.tests) then diff --git a/lua/cp/ui/layouts.lua b/lua/cp/ui/layouts.lua index 4e737d3..9b40f49 100644 --- a/lua/cp/ui/layouts.lua +++ b/lua/cp/ui/layouts.lua @@ -3,7 +3,13 @@ local M = {} local helpers = require('cp.helpers') local utils = require('cp.utils') -local function create_none_diff_layout(parent_win, expected_content, actual_content) +M.DIFF_MODES = { + ['side-by-side'] = 'side-by-side', + vim = 'vim', + git = 'git', +} + +local function create_side_by_side_layout(parent_win, expected_content, actual_content) local expected_buf = utils.create_buffer_with_options() local actual_buf = utils.create_buffer_with_options() helpers.clearcol(expected_buf) @@ -21,8 +27,13 @@ local function create_none_diff_layout(parent_win, expected_content, actual_cont vim.api.nvim_set_option_value('filetype', 'cp', { buf = expected_buf }) vim.api.nvim_set_option_value('filetype', 'cp', { buf = actual_buf }) - vim.api.nvim_set_option_value('winbar', 'Expected', { win = expected_win }) - vim.api.nvim_set_option_value('winbar', 'Actual', { win = actual_win }) + local label = M.DIFF_MODES['side-by-side'] + vim.api.nvim_set_option_value( + 'winbar', + ('expected (diff: %s)'):format(label), + { win = expected_win } + ) + vim.api.nvim_set_option_value('winbar', ('actual (diff: %s)'):format(label), { win = actual_win }) local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true }) local actual_lines = vim.split(actual_content, '\n', { plain = true }) @@ -33,6 +44,7 @@ local function create_none_diff_layout(parent_win, expected_content, actual_cont return { buffers = { expected_buf, actual_buf }, windows = { expected_win, actual_win }, + mode = 'side-by-side', cleanup = function() pcall(vim.api.nvim_win_close, expected_win, true) pcall(vim.api.nvim_win_close, actual_win, true) @@ -60,8 +72,13 @@ local function create_vim_diff_layout(parent_win, expected_content, actual_conte vim.api.nvim_set_option_value('filetype', 'cp', { buf = expected_buf }) vim.api.nvim_set_option_value('filetype', 'cp', { buf = actual_buf }) - vim.api.nvim_set_option_value('winbar', 'Expected', { win = expected_win }) - vim.api.nvim_set_option_value('winbar', 'Actual', { win = actual_win }) + local label = M.DIFF_MODES.vim + vim.api.nvim_set_option_value( + 'winbar', + ('expected (diff: %s)'):format(label), + { win = expected_win } + ) + vim.api.nvim_set_option_value('winbar', ('actual (diff: %s)'):format(label), { win = actual_win }) local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true }) local actual_lines = vim.split(actual_content, '\n', { plain = true }) @@ -83,6 +100,7 @@ local function create_vim_diff_layout(parent_win, expected_content, actual_conte return { buffers = { expected_buf, actual_buf }, windows = { expected_win, actual_win }, + mode = 'vim', cleanup = function() pcall(vim.api.nvim_win_close, expected_win, true) pcall(vim.api.nvim_win_close, actual_win, true) @@ -103,7 +121,8 @@ local function create_git_diff_layout(parent_win, expected_content, actual_conte vim.api.nvim_win_set_buf(diff_win, diff_buf) vim.api.nvim_set_option_value('filetype', 'cp', { buf = diff_buf }) - vim.api.nvim_set_option_value('winbar', 'Expected vs Actual', { win = diff_win }) + local label = M.DIFF_MODES.git + vim.api.nvim_set_option_value('winbar', ('diff: %s'):format(label), { win = diff_win }) local diff_backend = require('cp.ui.diff') local backend = diff_backend.get_best_backend('git') @@ -121,6 +140,7 @@ local function create_git_diff_layout(parent_win, expected_content, actual_conte return { buffers = { diff_buf }, windows = { diff_win }, + mode = 'git', cleanup = function() pcall(vim.api.nvim_win_close, diff_win, true) pcall(vim.api.nvim_buf_delete, diff_buf, { force = true }) @@ -143,6 +163,7 @@ local function create_single_layout(parent_win, content) return { buffers = { buf }, windows = { win }, + mode = 'single', cleanup = function() pcall(vim.api.nvim_win_close, win, true) pcall(vim.api.nvim_buf_delete, buf, { force = true }) @@ -153,12 +174,14 @@ end function M.create_diff_layout(mode, parent_win, expected_content, actual_content) if mode == 'single' then return create_single_layout(parent_win, actual_content) - elseif mode == 'none' then - return create_none_diff_layout(parent_win, expected_content, actual_content) + elseif mode == 'side-by-side' then + return create_side_by_side_layout(parent_win, expected_content, actual_content) elseif mode == 'git' then return create_git_diff_layout(parent_win, expected_content, actual_content) - else + elseif mode == 'vim' then return create_vim_diff_layout(parent_win, expected_content, actual_content) + else + return create_side_by_side_layout(parent_win, expected_content, actual_content) end end @@ -191,12 +214,13 @@ function M.update_diff_panes( actual_content = actual_content end - local desired_mode = is_compilation_failure and 'single' or config.ui.panel.diff_mode + local default_mode = config.ui.panel.diff_modes[1] + local desired_mode = is_compilation_failure and 'single' or (current_mode or default_mode) local highlight = require('cp.ui.highlight') local diff_namespace = highlight.create_namespace() local ansi_namespace = vim.api.nvim_create_namespace('cp_ansi_highlights') - if current_diff_layout and current_mode ~= desired_mode then + if current_diff_layout and current_diff_layout.mode ~= desired_mode then local saved_pos = vim.api.nvim_win_get_cursor(0) current_diff_layout.cleanup() current_diff_layout = nil @@ -251,7 +275,7 @@ function M.update_diff_panes( ansi_namespace ) end - elseif desired_mode == 'none' then + elseif desired_mode == 'side-by-side' then local expected_lines = vim.split(expected_content, '\n', { plain = true, trimempty = true }) local actual_lines = vim.split(actual_content, '\n', { plain = true }) utils.update_buffer_content(current_diff_layout.buffers[1], expected_lines, {}) diff --git a/lua/cp/ui/views.lua b/lua/cp/ui/views.lua index 4e06bd3..4b03b68 100644 --- a/lua/cp/ui/views.lua +++ b/lua/cp/ui/views.lua @@ -900,15 +900,15 @@ function M.toggle_panel(panel_opts) M.toggle_panel() end, { buffer = buf, silent = true }) vim.keymap.set('n', 't', function() - local modes = { 'none', 'git', 'vim' } + local modes = config.ui.panel.diff_modes local current_idx = 1 for i, mode in ipairs(modes) do - if config.ui.panel.diff_mode == mode then + if current_mode == mode then current_idx = i break end end - config.ui.panel.diff_mode = modes[(current_idx % #modes) + 1] + current_mode = modes[(current_idx % #modes) + 1] refresh_panel() end, { buffer = buf, silent = true }) vim.keymap.set('n', '', function()