Merge pull request #145 from barrett-ruth/feat/cli-enhancements
Misc CLI/Config Enhancements
This commit is contained in:
commit
45d21be879
19 changed files with 677 additions and 643 deletions
|
|
@ -28,7 +28,7 @@ cp.nvim follows a simple principle: **solve locally, submit remotely**.
|
|||
### Basic Usage
|
||||
|
||||
1. **Find a contest or problem** on the judge website
|
||||
2. **Set up locally** with `:CP <platform> <contest> [--{lang=<lang>,debug}]`
|
||||
2. **Set up locally** with `:CP <platform> <contest>`
|
||||
|
||||
```
|
||||
:CP codeforces 1848
|
||||
|
|
|
|||
283
doc/cp.nvim.txt
283
doc/cp.nvim.txt
|
|
@ -32,16 +32,13 @@ COMMANDS *cp-commands*
|
|||
Automatically detects platform, contest, problem,
|
||||
and language from cached state. Use this after
|
||||
switching files to restore your CP environment.
|
||||
Requires previous setup with full :CP command.
|
||||
|
||||
Setup Commands ~
|
||||
:CP {platform} {contest_id} {problem_id} [--lang={language}]
|
||||
Full setup: set platform, load contest metadata,
|
||||
and set up specific problem. Scrapes test cases
|
||||
and creates source file.
|
||||
:CP {platform} {contest_id}
|
||||
Full setup: set platform and load contest metadata.
|
||||
Scrapes test cases and creates source file.
|
||||
Example: >
|
||||
:CP codeforces 1933 a
|
||||
:CP codeforces 1933 a --lang=python
|
||||
:CP codeforces 1933
|
||||
<
|
||||
:CP {platform} {contest_id}
|
||||
Contest setup: set platform, load contest metadata,
|
||||
|
|
@ -52,10 +49,6 @@ COMMANDS *cp-commands*
|
|||
Example: >
|
||||
:CP atcoder abc324
|
||||
:CP codeforces 1951
|
||||
<
|
||||
:CP {platform} Platform setup: set platform only.
|
||||
Example: >
|
||||
:CP cses
|
||||
<
|
||||
Action Commands ~
|
||||
:CP run [--debug] Toggle run panel for individual test case
|
||||
|
|
@ -65,12 +58,13 @@ COMMANDS *cp-commands*
|
|||
Requires contest setup first.
|
||||
|
||||
:CP pick Launch configured picker for interactive
|
||||
platform/contest/problem selection.
|
||||
platform/contest selection.
|
||||
|
||||
Navigation Commands ~
|
||||
:CP next Navigate to next problem in current contest.
|
||||
Stops at last problem (no wrapping).
|
||||
|
||||
|
||||
Navigation Commands ~
|
||||
:CP prev Navigate to previous problem in current contest.
|
||||
Stops at first problem (no wrapping).
|
||||
|
|
@ -79,7 +73,7 @@ COMMANDS *cp-commands*
|
|||
:CP cache clear [contest]
|
||||
Clear the cache data (contest list, problem
|
||||
data, file states) for the specified contest,
|
||||
or all contests if none specified
|
||||
or all contests if none specified.
|
||||
|
||||
:CP cache read
|
||||
View the cache in a pretty-printed lua buffer.
|
||||
|
|
@ -89,22 +83,15 @@ Command Flags ~
|
|||
*cp-flags*
|
||||
Flags can be used with setup and action commands:
|
||||
|
||||
--lang={language} Specify language for the problem.
|
||||
--lang {language} Alternative syntax for language specification.
|
||||
Supported languages: cpp, python
|
||||
Example: >
|
||||
:CP atcoder abc324 a --lang=python
|
||||
:CP b --lang cpp
|
||||
<
|
||||
--debug Enable debug compilation with additional flags.
|
||||
Uses the `debug` command template instead of
|
||||
`compile`. Typically includes debug symbols and
|
||||
sanitizers for memory error detection.
|
||||
--debug Use the debug command template.
|
||||
For compiled languages, this selects
|
||||
`commands.debug` (a debug *build*) instead of
|
||||
`commands.build`. For interpreted languages,
|
||||
this selects `commands.debug` in place of
|
||||
`commands.run`.
|
||||
Example: >
|
||||
:CP run --debug
|
||||
<
|
||||
Note: Debug compilation may be slower but provides
|
||||
better error reporting for runtime issues.
|
||||
|
||||
Template Variables ~
|
||||
*cp-template-vars*
|
||||
|
|
@ -116,7 +103,7 @@ Template Variables ~
|
|||
• {problem} Problem identifier (e.g. "a", "b")
|
||||
|
||||
Example template: >
|
||||
compile = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
|
||||
build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
|
||||
< Would expand to: >
|
||||
g++ abc324a.cpp -o build/abc324a.run -std=c++17
|
||||
<
|
||||
|
|
@ -127,110 +114,161 @@ CONFIGURATION *cp-config*
|
|||
Here's an example configuration with lazy.nvim: >lua
|
||||
|
||||
{
|
||||
'barrett-ruth/cp.nvim',
|
||||
cmd = 'CP',
|
||||
opts = {
|
||||
contests = {
|
||||
default = {
|
||||
cpp = {
|
||||
compile = { 'g++', '{source}', '-o', '{binary}',
|
||||
'-std=c++17', '-fdiagnostic-colors=always' },
|
||||
test = { '{binary}' },
|
||||
debug = { 'g++', '{source}', '-o', '{binary}',
|
||||
'-std=c++17', '-g',
|
||||
'-fdiagnostic-colors=always'
|
||||
'-fsanitize=address,undefined' },
|
||||
},
|
||||
python = {
|
||||
test = { 'python3', '{source}' },
|
||||
},
|
||||
},
|
||||
'barrett-ruth/cp.nvim',
|
||||
cmd = 'CP',
|
||||
build = 'uv sync',
|
||||
opts = {
|
||||
languages = {
|
||||
cpp = {
|
||||
extension = 'cc',
|
||||
commands = {
|
||||
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' },
|
||||
run = { '{binary}' },
|
||||
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
|
||||
'{source}', '-o', '{binary}' },
|
||||
},
|
||||
snippets = {},
|
||||
debug = false,
|
||||
scrapers = { 'atcoder', 'codeforces', 'cses' },
|
||||
run_panel = {
|
||||
ansi = true,
|
||||
diff_mode = 'vim',
|
||||
next_test_key = '<c-n>',
|
||||
prev_test_key = '<c-p>',
|
||||
max_output_lines = 50,
|
||||
},
|
||||
python = {
|
||||
extension = 'py',
|
||||
commands = {
|
||||
run = { 'python', '{source}' },
|
||||
debug = { 'python', '{source}' },
|
||||
},
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain',
|
||||
'--word-diff-regex=.', '--no-prefix' },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
platforms = {
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
overrides = {
|
||||
cpp = { extension = 'cpp', commands = { build = { ... } } }
|
||||
},
|
||||
picker = 'telescope', -- 'telescope', 'fzf-lua', or nil (disabled)
|
||||
}
|
||||
},
|
||||
atcoder = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
codeforces = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
},
|
||||
|
||||
snippets = {},
|
||||
debug = false,
|
||||
|
||||
ui = {
|
||||
run_panel = {
|
||||
ansi = true,
|
||||
diff_mode = 'vim',
|
||||
max_output_lines = 50,
|
||||
},
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain',
|
||||
'--word-diff-regex=.', '--no-prefix' },
|
||||
},
|
||||
},
|
||||
picker = 'telescope',
|
||||
},
|
||||
}
|
||||
}
|
||||
<
|
||||
|
||||
By default, all contests are configured to use C++ with the g++ compiler and ISO standard
|
||||
17. Python is also configured with the system executable python as a non-default option. Consult lua/cp/config.lua for
|
||||
more information.
|
||||
By default, C++ (g++ with ISO C++17) and Python are preconfigured under
|
||||
`languages`. Platforms select which languages are enabled and which one is
|
||||
the default; per-platform overrides can tweak `extension` or `commands`.
|
||||
|
||||
For example, to run CodeForces contests with Python, only the following config
|
||||
is required:
|
||||
For example, to run CodeForces contests with Python by default:
|
||||
|
||||
>lua
|
||||
{
|
||||
contests = {
|
||||
codeforces = {
|
||||
default_langauge = 'python'
|
||||
}
|
||||
}
|
||||
platforms = {
|
||||
codeforces = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'python',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
|
||||
Any language is supported provided the proper configuration. For example, to
|
||||
run CSES problems with Rust using the single schema:
|
||||
|
||||
>lua
|
||||
{
|
||||
languages = {
|
||||
rust = {
|
||||
extension = 'rs',
|
||||
commands = {
|
||||
build = { 'rustc', '{source}', '-o', '{binary}' },
|
||||
run = { '{binary}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
platforms = {
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python', 'rust' },
|
||||
default_language = 'rust',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
|
||||
*cp.Config*
|
||||
Fields: ~
|
||||
{contests} (table<string,ContestConfig>) Contest configurations.
|
||||
{languages} (table<string,|CpLanguage|>) Global language registry.
|
||||
Each language provides an {extension} and {commands}.
|
||||
{platforms} (table<string,|CpPlatform|>) Per-platform enablement,
|
||||
default language, and optional overrides.
|
||||
{hooks} (|cp.Hooks|) Hook functions called at various stages.
|
||||
{snippets} (table[]) LuaSnip snippet definitions.
|
||||
{debug} (boolean, default: false) Show info messages
|
||||
during operation.
|
||||
{scrapers} (table<string>) List of enabled scrapers.
|
||||
Default: all scrapers enabled
|
||||
{run_panel} (|RunPanelConfig|) Test panel behavior configuration.
|
||||
{diff} (|DiffConfig|) Diff backend configuration.
|
||||
{picker} (string, optional) Picker integration: "telescope",
|
||||
"fzf-lua", or nil to disable. When enabled, provides
|
||||
:CP pick for interactive platform/contest/problem selection.
|
||||
{filename} (function, optional) Custom filename generation.
|
||||
function(contest, contest_id, problem_id, config, language)
|
||||
{debug} (boolean, default: false) Show info messages.
|
||||
{scrapers} (string[]) Supported platform ids.
|
||||
{filename} (function, optional)
|
||||
function(contest, contest_id, problem_id, config, language): string
|
||||
Should return full filename with extension.
|
||||
(default: concatenates contest_id and problem_id, lowercased)
|
||||
{ui} (|CpUI|) UI settings: run panel, diff backend, picker.
|
||||
|
||||
*cp.ContestConfig*
|
||||
Fields: ~
|
||||
{cpp} (|LanguageConfig|) C++ language configuration.
|
||||
{python} (|LanguageConfig|) Python language configuration.
|
||||
{default_language} (string, default: "cpp") Default language when
|
||||
--lang not specified.
|
||||
*cp.PlatformConfig*
|
||||
Replaced by |CpPlatform|. Platforms no longer inline language tables.
|
||||
|
||||
*cp.LanguageConfig*
|
||||
*CpPlatform*
|
||||
Fields: ~
|
||||
{compile} (string[], optional) Compile command template with
|
||||
{source}, {binary} placeholders.
|
||||
{test} (string[]) Test execution command template.
|
||||
{debug} (string[], optional) Debug compile command template.
|
||||
{extension} (string) File extension (e.g. "cc", "py").
|
||||
{executable} (string, optional) Executable name for interpreted languages.
|
||||
{enabled_languages} (string[]) Language ids enabled on this platform.
|
||||
{default_language} (string) One of {enabled_languages}.
|
||||
{overrides} (table<string,|CpPlatformOverrides|>, optional)
|
||||
Per-language overrides of {extension} and/or {commands}.
|
||||
|
||||
*CpLanguage*
|
||||
Fields: ~
|
||||
{extension} (string) File extension without leading dot.
|
||||
{commands} (|CpLangCommands|) Command templates.
|
||||
|
||||
*CpLangCommands*
|
||||
Fields: ~
|
||||
{build} (string[], optional) For compiled languages.
|
||||
Must include {source} and {binary}.
|
||||
{run} (string[], optional) Runtime command.
|
||||
Compiled: must include {binary}.
|
||||
Interpreted: must include {source}.
|
||||
{debug} (string[], optional) Debug variant; same token rules
|
||||
as {build} (compiled) or {run} (interpreted).
|
||||
|
||||
*CpUI*
|
||||
Fields: ~
|
||||
{run_panel} (|RunPanelConfig|) Test panel behavior configuration.
|
||||
{diff} (|DiffConfig|) Diff backend configuration.
|
||||
{picker} (string|nil) 'telescope', 'fzf-lua', or nil.
|
||||
|
||||
*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: "<c-t>") Key to cycle through diff modes.
|
||||
{ansi} (boolean, default: true) Enable ANSI color parsing
|
||||
and highlighting.
|
||||
{diff_mode} (string, default: "none") Diff backend: "none",
|
||||
"vim", or "git".
|
||||
{max_output_lines} (number, default: 50) Maximum lines of test output.
|
||||
|
||||
*cp.DiffConfig*
|
||||
|
|
@ -251,10 +289,9 @@ is required:
|
|||
Fields: ~
|
||||
{before_run} (function, optional) Called before test panel opens.
|
||||
function(state: cp.State)
|
||||
{before_debug} (function, optional) Called before debug compilation.
|
||||
{before_debug} (function, optional) Called before debug build/run.
|
||||
function(state: cp.State)
|
||||
{setup_code} (function, optional) Called after source file is opened.
|
||||
Good for configuring buffer settings.
|
||||
function(state: cp.State)
|
||||
|
||||
Hook functions receive the cp.nvim state object (cp.State). See the state
|
||||
|
|
@ -284,41 +321,21 @@ AtCoder ~
|
|||
URL format: https://atcoder.jp/contests/abc123/tasks/abc123_a
|
||||
|
||||
Usage examples: >
|
||||
:CP atcoder abc324 a " Full setup: problem A from contest ABC324
|
||||
:CP atcoder abc324 " Contest setup: load contest metadata only
|
||||
:CP next " Navigate to next problem in contest
|
||||
<
|
||||
Note: AtCoder template includes optimizations
|
||||
for multi-test case problems commonly found
|
||||
in contests.
|
||||
|
||||
AtCoder Heuristic Contests (AHC) are excluded
|
||||
from the contest list as they don't have
|
||||
standard sample test cases.
|
||||
|
||||
Codeforces ~
|
||||
*cp-codeforces*
|
||||
URL format: https://codeforces.com/contest/1234/problem/A
|
||||
|
||||
Usage examples: >
|
||||
:CP codeforces 1934 a " Full setup: problem A from contest 1934
|
||||
:CP codeforces 1934 " Contest setup: load contest metadata only
|
||||
:CP prev " Navigate to previous problem in contest
|
||||
<
|
||||
Note: Problem IDs are automatically converted
|
||||
to lowercase for consistency.
|
||||
|
||||
CSES ~
|
||||
*cp-cses*
|
||||
URL format: https://cses.fi/problemset/task/1068
|
||||
|
||||
Usage examples: >
|
||||
:CP cses dynamic_programming 1633 " Set up problem 1633 from DP category
|
||||
:CP cses dynamic_programming " Set up ALL problems from DP category
|
||||
<
|
||||
Note: Category name is always required. For bulk
|
||||
setup, omit the problem ID to scrape all problems
|
||||
in the category.
|
||||
|
||||
==============================================================================
|
||||
|
||||
|
|
@ -533,10 +550,8 @@ prevent them from being overridden: >lua
|
|||
|
||||
==============================================================================
|
||||
RUN PANEL KEYMAPS *cp-test-keys*
|
||||
<c-n> Navigate to next test case (configurable via
|
||||
run_panel.next_test_key)
|
||||
<c-p> Navigate to previous test case (configurable via
|
||||
run_panel.prev_test_key)
|
||||
<c-n> Navigate to next test case
|
||||
<c-p> Navigate to previous test case
|
||||
t Cycle through diff modes: none → git → vim
|
||||
q Exit run panel and restore layout
|
||||
<c-q> Exit interactive terminal and restore layout
|
||||
|
|
|
|||
|
|
@ -86,7 +86,9 @@ function M.get_contest_data(platform, contest_id)
|
|||
contest_id = { contest_id, 'string' },
|
||||
})
|
||||
|
||||
return cache_data[platform][contest_id] or {}
|
||||
cache_data[platform] = cache_data[platform] or {}
|
||||
cache_data[platform][contest_id] = cache_data[platform][contest_id] or {}
|
||||
return cache_data[platform][contest_id]
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
|
|
@ -105,7 +107,7 @@ function M.set_contest_data(platform, contest_id, problems)
|
|||
local out = {
|
||||
name = prev.name,
|
||||
display_name = prev.display_name,
|
||||
problems = vim.deepcopy(problems),
|
||||
problems = problems,
|
||||
index_map = {},
|
||||
}
|
||||
for i, p in ipairs(out.problems) do
|
||||
|
|
@ -207,32 +209,27 @@ function M.get_constraints(platform, contest_id, problem_id)
|
|||
end
|
||||
|
||||
---@param file_path string
|
||||
---@return FileState?
|
||||
---@return FileState|nil
|
||||
function M.get_file_state(file_path)
|
||||
if not cache_data.file_states then
|
||||
return nil
|
||||
end
|
||||
|
||||
M.load()
|
||||
cache_data.file_states = cache_data.file_states or {}
|
||||
return cache_data.file_states[file_path]
|
||||
end
|
||||
|
||||
---@param file_path string
|
||||
---@param path string
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param problem_id? string
|
||||
---@param language? string
|
||||
function M.set_file_state(file_path, platform, contest_id, problem_id, language)
|
||||
if not cache_data.file_states then
|
||||
cache_data.file_states = {}
|
||||
end
|
||||
|
||||
cache_data.file_states[file_path] = {
|
||||
---@param problem_id string
|
||||
---@param language string|nil
|
||||
function M.set_file_state(path, platform, contest_id, problem_id, language)
|
||||
M.load()
|
||||
cache_data.file_states = cache_data.file_states or {}
|
||||
cache_data.file_states[path] = {
|
||||
platform = platform,
|
||||
contest_id = contest_id,
|
||||
problem_id = problem_id,
|
||||
language = language,
|
||||
}
|
||||
|
||||
M.save()
|
||||
end
|
||||
|
||||
|
|
@ -255,7 +252,7 @@ end
|
|||
function M.set_contest_summaries(platform, contests)
|
||||
cache_data[platform] = cache_data[platform] or {}
|
||||
for _, contest in ipairs(contests) do
|
||||
cache_data[platform][contest.id] = cache_data[platform][contest] or {}
|
||||
cache_data[platform][contest.id] = cache_data[platform][contest.id] or {}
|
||||
cache_data[platform][contest.id].display_name = contest.display_name
|
||||
cache_data[platform][contest.id].name = contest.name
|
||||
end
|
||||
|
|
@ -284,4 +281,6 @@ function M.get_data_pretty()
|
|||
return vim.inspect(cache_data)
|
||||
end
|
||||
|
||||
M._cache = cache_data
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ local actions = constants.ACTIONS
|
|||
---@class ParsedCommand
|
||||
---@field type string
|
||||
---@field error string?
|
||||
---@field language? string
|
||||
---@field debug? boolean
|
||||
---@field action? string
|
||||
---@field message? string
|
||||
|
|
@ -27,26 +26,10 @@ local function parse_command(args)
|
|||
}
|
||||
end
|
||||
|
||||
local language = nil
|
||||
local debug = false
|
||||
|
||||
for i, arg in ipairs(args) do
|
||||
local lang_match = arg:match('^--lang=(.+)$')
|
||||
if lang_match then
|
||||
language = lang_match
|
||||
elseif arg == '--lang' then
|
||||
if i + 1 <= #args then
|
||||
language = args[i + 1]
|
||||
else
|
||||
return { type = 'error', message = '--lang requires a value' }
|
||||
end
|
||||
elseif arg == '--debug' then
|
||||
debug = true
|
||||
end
|
||||
end
|
||||
local debug = vim.tbl_contains(args, '--debug')
|
||||
|
||||
local filtered_args = vim.tbl_filter(function(arg)
|
||||
return not (arg:match('^--lang') or arg == language or arg == '--debug')
|
||||
return arg ~= '--debug'
|
||||
end, args)
|
||||
|
||||
local first = filtered_args[1]
|
||||
|
|
@ -68,7 +51,7 @@ local function parse_command(args)
|
|||
return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand }
|
||||
end
|
||||
else
|
||||
return { type = 'action', action = first, language = language, debug = debug }
|
||||
return { type = 'action', action = first, debug = debug }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -83,12 +66,11 @@ local function parse_command(args)
|
|||
type = 'contest_setup',
|
||||
platform = first,
|
||||
contest = filtered_args[2],
|
||||
language = language,
|
||||
}
|
||||
elseif #filtered_args == 3 then
|
||||
return {
|
||||
type = 'error',
|
||||
message = 'Setup contests with :CP <platform> <contest_id> [--{lang=<lang>,debug}]',
|
||||
message = 'Setup contests with :CP <platform> <contest_id>',
|
||||
}
|
||||
else
|
||||
return { type = 'error', message = 'Too many arguments' }
|
||||
|
|
@ -129,9 +111,9 @@ function M.handle_command(opts)
|
|||
elseif cmd.action == 'run' then
|
||||
ui.toggle_run_panel(cmd.debug)
|
||||
elseif cmd.action == 'next' then
|
||||
setup.navigate_problem(1, cmd.language)
|
||||
setup.navigate_problem(1)
|
||||
elseif cmd.action == 'prev' then
|
||||
setup.navigate_problem(-1, cmd.language)
|
||||
setup.navigate_problem(-1)
|
||||
elseif cmd.action == 'pick' then
|
||||
local picker = require('cp.commands.picker')
|
||||
picker.handle_pick_action()
|
||||
|
|
@ -142,7 +124,7 @@ function M.handle_command(opts)
|
|||
elseif cmd.type == 'contest_setup' then
|
||||
local setup = require('cp.setup')
|
||||
if setup.set_platform(cmd.platform) then
|
||||
setup.setup_contest(cmd.platform, cmd.contest, cmd.language, nil)
|
||||
setup.setup_contest(cmd.platform, cmd.contest, nil)
|
||||
end
|
||||
return
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ local logger = require('cp.log')
|
|||
function M.handle_pick_action()
|
||||
local config = config_module.get_config()
|
||||
|
||||
if not config.picker then
|
||||
if not (config.ui and config.ui.picker) then
|
||||
logger.log(
|
||||
'No picker configured. Set picker = "{telescope,fzf-lua}" in your config.',
|
||||
'No picker configured. Set ui.picker = "{telescope,fzf-lua}" in your config.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
|
|
@ -18,7 +18,8 @@ function M.handle_pick_action()
|
|||
|
||||
local picker
|
||||
|
||||
if config.picker == 'telescope' then
|
||||
local picker_name = config.ui.picker
|
||||
if picker_name == 'telescope' then
|
||||
local ok = pcall(require, 'telescope')
|
||||
if not ok then
|
||||
logger.log(
|
||||
|
|
@ -34,7 +35,7 @@ function M.handle_pick_action()
|
|||
end
|
||||
|
||||
picker = telescope_picker
|
||||
elseif config.picker == 'fzf-lua' then
|
||||
elseif picker_name == 'fzf-lua' then
|
||||
local ok, _ = pcall(require, 'fzf-lua')
|
||||
if not ok then
|
||||
logger.log(
|
||||
|
|
|
|||
|
|
@ -1,262 +1,279 @@
|
|||
---@class LanguageConfig
|
||||
---@field compile? string[] Compile command template
|
||||
---@field test string[] Test execution command template
|
||||
---@field debug? string[] Debug command template
|
||||
---@field executable? string Executable name
|
||||
---@field version? number Language version
|
||||
---@field extension? string File extension
|
||||
-- lua/cp/config.lua
|
||||
---@class CpLangCommands
|
||||
---@field build? string[]
|
||||
---@field run? string[]
|
||||
---@field debug? string[]
|
||||
|
||||
---@class ContestConfig
|
||||
---@field cpp LanguageConfig
|
||||
---@field python LanguageConfig
|
||||
---@field default_language? string
|
||||
---@class CpLanguage
|
||||
---@field extension string
|
||||
---@field commands CpLangCommands
|
||||
|
||||
---@class CpPlatformOverrides
|
||||
---@field extension? string
|
||||
---@field commands? CpLangCommands
|
||||
|
||||
---@class CpPlatform
|
||||
---@field enabled_languages string[]
|
||||
---@field default_language string
|
||||
---@field overrides? table<string, CpPlatformOverrides>
|
||||
|
||||
---@class RunPanelConfig
|
||||
---@field ansi boolean
|
||||
---@field diff_mode "none"|"vim"|"git"
|
||||
---@field max_output_lines integer
|
||||
|
||||
---@class DiffGitConfig
|
||||
---@field args string[]
|
||||
|
||||
---@class DiffConfig
|
||||
---@field git DiffGitConfig
|
||||
|
||||
---@class Hooks
|
||||
---@field before_run? fun(state: cp.State)
|
||||
---@field before_debug? fun(state: cp.State)
|
||||
---@field setup_code? fun(state: cp.State)
|
||||
|
||||
---@class RunPanelConfig
|
||||
---@field ansi boolean Enable ANSI color parsing and highlighting
|
||||
---@field diff_mode "none"|"vim"|"git" Diff backend to use
|
||||
---@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 max_output_lines number Maximum lines of test output to display
|
||||
|
||||
---@class DiffGitConfig
|
||||
---@field args string[] Git diff arguments
|
||||
|
||||
---@class DiffConfig
|
||||
---@field git DiffGitConfig
|
||||
|
||||
---@class cp.Config
|
||||
---@field contests table<string, ContestConfig>
|
||||
---@field snippets any[]
|
||||
---@field hooks Hooks
|
||||
---@field debug boolean
|
||||
---@field scrapers string[]
|
||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||
---@class CpUI
|
||||
---@field run_panel RunPanelConfig
|
||||
---@field diff DiffConfig
|
||||
---@field picker string|nil
|
||||
|
||||
---@class cp.PartialConfig
|
||||
---@field contests? table<string, ContestConfig>
|
||||
---@field snippets? any[]
|
||||
---@field hooks? Hooks
|
||||
---@field debug? boolean
|
||||
---@field scrapers? string[]
|
||||
---@class cp.Config
|
||||
---@field languages table<string, CpLanguage>
|
||||
---@field platforms table<string, CpPlatform>
|
||||
---@field hooks Hooks
|
||||
---@field snippets any[]
|
||||
---@field debug boolean
|
||||
---@field scrapers string[]
|
||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||
---@field run_panel? RunPanelConfig
|
||||
---@field diff? DiffConfig
|
||||
---@field picker? string|nil
|
||||
---@field ui CpUI
|
||||
---@field runtime { effective: table<string, table<string, CpLanguage>> } -- computed
|
||||
|
||||
---@class cp.PartialConfig: cp.Config
|
||||
|
||||
local M = {}
|
||||
|
||||
local constants = require('cp.constants')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
local default_contest_config = {
|
||||
cpp = {
|
||||
compile = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' },
|
||||
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined', '{source}', '-o', '{binary}' },
|
||||
test = { '{binary}' },
|
||||
},
|
||||
python = {
|
||||
test = { '{source}' },
|
||||
debug = { '{source}' },
|
||||
executable = 'python',
|
||||
extension = 'py',
|
||||
},
|
||||
default_language = 'cpp',
|
||||
}
|
||||
|
||||
-- defaults per the new single schema
|
||||
---@type cp.Config
|
||||
M.defaults = {
|
||||
contests = {
|
||||
codeforces = default_contest_config,
|
||||
atcoder = default_contest_config,
|
||||
cses = default_contest_config,
|
||||
languages = {
|
||||
cpp = {
|
||||
extension = 'cc',
|
||||
commands = {
|
||||
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' },
|
||||
run = { '{binary}' },
|
||||
debug = {
|
||||
'g++',
|
||||
'-std=c++17',
|
||||
'-fsanitize=address,undefined',
|
||||
'{source}',
|
||||
'-o',
|
||||
'{binary}',
|
||||
},
|
||||
},
|
||||
},
|
||||
python = {
|
||||
extension = 'py',
|
||||
commands = {
|
||||
run = { 'python', '{source}' },
|
||||
debug = { 'python', '{source}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
platforms = {
|
||||
codeforces = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
overrides = {
|
||||
-- example override, safe to keep empty initially
|
||||
},
|
||||
},
|
||||
atcoder = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python' },
|
||||
default_language = 'cpp',
|
||||
},
|
||||
},
|
||||
snippets = {},
|
||||
hooks = {
|
||||
before_run = nil,
|
||||
before_debug = nil,
|
||||
setup_code = nil,
|
||||
},
|
||||
hooks = { before_run = nil, before_debug = nil, setup_code = nil },
|
||||
debug = false,
|
||||
scrapers = constants.PLATFORMS,
|
||||
filename = nil,
|
||||
run_panel = {
|
||||
ansi = true,
|
||||
diff_mode = 'none',
|
||||
next_test_key = '<c-n>',
|
||||
prev_test_key = '<c-p>',
|
||||
max_output_lines = 50,
|
||||
},
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||
ui = {
|
||||
run_panel = { ansi = true, diff_mode = 'none', max_output_lines = 50 },
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
||||
},
|
||||
},
|
||||
picker = nil,
|
||||
},
|
||||
picker = nil,
|
||||
runtime = { effective = {} },
|
||||
}
|
||||
|
||||
local function is_string_list(t)
|
||||
if type(t) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
for _, v in ipairs(t) do
|
||||
if type(v) ~= 'string' then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function has_tokens(cmd, required)
|
||||
if type(cmd) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
local s = table.concat(cmd, ' ')
|
||||
for _, tok in ipairs(required) do
|
||||
if not s:find(vim.pesc(tok), 1, true) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function validate_language(id, lang)
|
||||
vim.validate({
|
||||
extension = { lang.extension, 'string' },
|
||||
commands = { lang.commands, { 'table' } },
|
||||
})
|
||||
if lang.commands.build ~= nil then
|
||||
vim.validate({ build = { lang.commands.build, { 'table' } } })
|
||||
if not has_tokens(lang.commands.build, { '{source}', '{binary}' }) then
|
||||
error(('[cp.nvim] languages.%s.commands.build must include {source} and {binary}'):format(id))
|
||||
end
|
||||
for _, k in ipairs({ 'run', 'debug' }) do
|
||||
if lang.commands[k] then
|
||||
if not has_tokens(lang.commands[k], { '{binary}' }) then
|
||||
error(('[cp.nvim] languages.%s.commands.%s must include {binary}'):format(id, k))
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, k in ipairs({ 'run', 'debug' }) do
|
||||
if lang.commands[k] then
|
||||
if not has_tokens(lang.commands[k], { '{source}' }) then
|
||||
error(('[cp.nvim] languages.%s.commands.%s must include {source}'):format(id, k))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function merge_lang(base, ov)
|
||||
if not ov then
|
||||
return base
|
||||
end
|
||||
local out = vim.deepcopy(base)
|
||||
if ov.extension then
|
||||
out.extension = ov.extension
|
||||
end
|
||||
if ov.commands then
|
||||
out.commands = vim.tbl_deep_extend('force', out.commands or {}, ov.commands or {})
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
---@param cfg cp.Config
|
||||
local function build_runtime(cfg)
|
||||
cfg.runtime = cfg.runtime or { effective = {} }
|
||||
for plat, p in pairs(cfg.platforms) do
|
||||
vim.validate({
|
||||
enabled_languages = { p.enabled_languages, is_string_list, 'string[]' },
|
||||
default_language = { p.default_language, 'string' },
|
||||
})
|
||||
for _, lid in ipairs(p.enabled_languages) do
|
||||
if not cfg.languages[lid] then
|
||||
error(("[cp.nvim] platform %s references unknown language '%s'"):format(plat, lid))
|
||||
end
|
||||
end
|
||||
if not vim.tbl_contains(p.enabled_languages, p.default_language) then
|
||||
error(
|
||||
("[cp.nvim] platform %s default_language '%s' not in enabled_languages"):format(
|
||||
plat,
|
||||
p.default_language
|
||||
)
|
||||
)
|
||||
end
|
||||
cfg.runtime.effective[plat] = {}
|
||||
for _, lid in ipairs(p.enabled_languages) do
|
||||
local base = cfg.languages[lid]
|
||||
validate_language(lid, base)
|
||||
local eff = merge_lang(base, p.overrides and p.overrides[lid] or nil)
|
||||
validate_language(lid, eff)
|
||||
cfg.runtime.effective[plat][lid] = eff
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
---@param user_config cp.PartialConfig|nil
|
||||
---@return cp.Config
|
||||
function M.setup(user_config)
|
||||
vim.validate({
|
||||
user_config = { user_config, { 'table', 'nil' }, true },
|
||||
})
|
||||
|
||||
if user_config then
|
||||
vim.validate({
|
||||
contests = { user_config.contests, { 'table', 'nil' }, true },
|
||||
snippets = { user_config.snippets, { 'table', 'nil' }, true },
|
||||
hooks = { user_config.hooks, { 'table', 'nil' }, true },
|
||||
debug = { user_config.debug, { 'boolean', 'nil' }, true },
|
||||
scrapers = { user_config.scrapers, { 'table', 'nil' }, true },
|
||||
filename = { user_config.filename, { 'function', 'nil' }, true },
|
||||
run_panel = { user_config.run_panel, { 'table', 'nil' }, true },
|
||||
diff = { user_config.diff, { 'table', 'nil' }, true },
|
||||
picker = { user_config.picker, { 'string', 'nil' }, true },
|
||||
})
|
||||
|
||||
if user_config.contests then
|
||||
for contest_name, contest_config in pairs(user_config.contests) do
|
||||
vim.validate({
|
||||
[contest_name] = {
|
||||
contest_config,
|
||||
function(config)
|
||||
if type(config) ~= 'table' then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end,
|
||||
'contest configuration',
|
||||
},
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
if user_config.scrapers then
|
||||
for _, platform_name in ipairs(user_config.scrapers) do
|
||||
if type(platform_name) ~= 'string' then
|
||||
error(('Invalid scraper value type. Expected string, got %s'):format(type(platform_name)))
|
||||
end
|
||||
if not vim.tbl_contains(constants.PLATFORMS, platform_name) then
|
||||
error(
|
||||
("Invalid platform '%s' in scrapers config. Valid platforms: %s"):format(
|
||||
platform_name,
|
||||
table.concat(constants.PLATFORMS, ', ')
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if user_config.picker then
|
||||
if not vim.tbl_contains({ 'telescope', 'fzf-lua' }, user_config.picker) then
|
||||
error(("Invalid picker '%s'. Must be 'telescope' or 'fzf-lua'"):format(user_config.picker))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local config = vim.tbl_deep_extend('force', M.defaults, user_config or {})
|
||||
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
|
||||
local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), user_config or {})
|
||||
|
||||
vim.validate({
|
||||
before_run = {
|
||||
config.hooks.before_run,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
before_debug = {
|
||||
config.hooks.before_debug,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
setup_code = {
|
||||
config.hooks.setup_code,
|
||||
{ 'function', 'nil' },
|
||||
true,
|
||||
},
|
||||
hooks = { cfg.hooks, { 'table' } },
|
||||
ui = { cfg.ui, { 'table' } },
|
||||
})
|
||||
|
||||
vim.validate({
|
||||
ansi = {
|
||||
config.run_panel.ansi,
|
||||
'boolean',
|
||||
'ansi color parsing must be enabled xor disabled',
|
||||
},
|
||||
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 },
|
||||
})
|
||||
|
||||
vim.validate({
|
||||
ansi = { cfg.ui.run_panel.ansi, 'boolean' },
|
||||
diff_mode = {
|
||||
config.run_panel.diff_mode,
|
||||
function(value)
|
||||
return vim.tbl_contains({ 'none', 'vim', 'git' }, value)
|
||||
cfg.ui.run_panel.diff_mode,
|
||||
function(v)
|
||||
return vim.tbl_contains({ 'none', 'vim', 'git' }, v)
|
||||
end,
|
||||
"diff_mode must be 'none', 'vim', or 'git'",
|
||||
},
|
||||
next_test_key = {
|
||||
config.run_panel.next_test_key,
|
||||
function(value)
|
||||
return type(value) == 'string' and value ~= ''
|
||||
end,
|
||||
'next_test_key must be a non-empty string',
|
||||
},
|
||||
prev_test_key = {
|
||||
config.run_panel.prev_test_key,
|
||||
function(value)
|
||||
return type(value) == 'string' and value ~= ''
|
||||
end,
|
||||
'prev_test_key must be a non-empty string',
|
||||
},
|
||||
max_output_lines = {
|
||||
config.run_panel.max_output_lines,
|
||||
function(value)
|
||||
return type(value) == 'number' and value > 0 and value == math.floor(value)
|
||||
cfg.ui.run_panel.max_output_lines,
|
||||
function(v)
|
||||
return type(v) == 'number' and v > 0 and v == math.floor(v)
|
||||
end,
|
||||
'max_output_lines must be a positive integer',
|
||||
'positive integer',
|
||||
},
|
||||
git = { cfg.ui.diff.git, { 'table' } },
|
||||
})
|
||||
|
||||
vim.validate({
|
||||
git = { config.diff.git, { 'table', 'nil' }, true },
|
||||
})
|
||||
|
||||
for _, contest_config in pairs(config.contests) do
|
||||
for lang_name, lang_config in pairs(contest_config) do
|
||||
if type(lang_config) == 'table' and not lang_config.extension then
|
||||
if lang_name == 'cpp' then
|
||||
lang_config.extension = 'cpp'
|
||||
elseif lang_name == 'python' then
|
||||
lang_config.extension = 'py'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not contest_config.default_language then
|
||||
local available_langs = {}
|
||||
for lang_name, lang_config in pairs(contest_config) do
|
||||
if type(lang_config) == 'table' and lang_name ~= 'default_language' then
|
||||
table.insert(available_langs, lang_name)
|
||||
end
|
||||
end
|
||||
|
||||
if vim.tbl_isemtpy(available_langs) then
|
||||
error('No language configurations found')
|
||||
end
|
||||
|
||||
table.sort(available_langs)
|
||||
contest_config.default_language = available_langs[1]
|
||||
end
|
||||
for id, lang in pairs(cfg.languages) do
|
||||
validate_language(id, lang)
|
||||
end
|
||||
|
||||
build_runtime(cfg)
|
||||
|
||||
local ok, err = utils.check_required_runtime()
|
||||
if not ok then
|
||||
error('[cp.nvim] ' .. err)
|
||||
end
|
||||
|
||||
return config
|
||||
return cfg
|
||||
end
|
||||
|
||||
local current_config = nil
|
||||
|
||||
function M.set_current_config(config)
|
||||
current_config = config
|
||||
end
|
||||
|
||||
function M.get_config()
|
||||
return current_config or M.defaults
|
||||
end
|
||||
|
||||
---@param contest_id string
|
||||
|
|
@ -265,25 +282,9 @@ end
|
|||
local function default_filename(contest_id, problem_id)
|
||||
if problem_id then
|
||||
return (contest_id .. problem_id):lower()
|
||||
else
|
||||
return contest_id:lower()
|
||||
end
|
||||
return contest_id:lower()
|
||||
end
|
||||
|
||||
M.default_filename = default_filename
|
||||
|
||||
local current_config = nil
|
||||
|
||||
--- Set the config
|
||||
---@return nil
|
||||
function M.set_current_config(config)
|
||||
current_config = config
|
||||
end
|
||||
|
||||
--- Get the config
|
||||
---@return cp.Config
|
||||
function M.get_config()
|
||||
return current_config or M.defaults
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ M.canonical_filetypes = {
|
|||
[M.PYTHON] = 'python',
|
||||
}
|
||||
|
||||
---@type table<string, string>
|
||||
M.canonical_filetype_to_extension = {
|
||||
[M.CPP] = 'cc',
|
||||
[M.PYTHON] = 'py',
|
||||
}
|
||||
|
||||
---@type table<number, string>
|
||||
M.signal_codes = {
|
||||
[128] = 'SIGILL',
|
||||
|
|
|
|||
|
|
@ -4,14 +4,15 @@ local config_module = require('cp.config')
|
|||
local logger = require('cp.log')
|
||||
local snippets = require('cp.snippets')
|
||||
|
||||
if not vim.fn.has('nvim-0.10.0') then
|
||||
if vim.fn.has('nvim-0.10.0') == 0 then
|
||||
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR)
|
||||
return {}
|
||||
end
|
||||
|
||||
local user_config = {}
|
||||
local config = config_module.setup(user_config)
|
||||
local config = nil
|
||||
local snippets_initialized = false
|
||||
local initialized = false
|
||||
|
||||
--- Root handler for all `:CP ...` commands
|
||||
---@return nil
|
||||
|
|
@ -30,10 +31,11 @@ function M.setup(opts)
|
|||
snippets.setup(config)
|
||||
snippets_initialized = true
|
||||
end
|
||||
initialized = true
|
||||
end
|
||||
|
||||
function M.is_initialized()
|
||||
return true
|
||||
return initialized
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
local M = {}
|
||||
|
||||
local cache = require('cp.cache')
|
||||
local config = require('cp.config').get_config()
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
local scraper = require('cp.scraper')
|
||||
|
|
@ -22,17 +21,16 @@ local scraper = require('cp.scraper')
|
|||
|
||||
---@return cp.PlatformItem[]
|
||||
function M.get_platforms()
|
||||
local config = require('cp.config').get_config()
|
||||
local result = {}
|
||||
|
||||
for _, platform in ipairs(constants.PLATFORMS) do
|
||||
if config.contests[platform] then
|
||||
if config.platforms[platform] then
|
||||
table.insert(result, {
|
||||
id = platform,
|
||||
display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform,
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -4,44 +4,26 @@ local cache = require('cp.cache')
|
|||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
|
||||
---@return boolean
|
||||
function M.restore_from_current_file()
|
||||
local current_file = vim.fn.expand('%:p')
|
||||
if current_file == '' then
|
||||
logger.log('No file is currently open.', vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
cache.load()
|
||||
|
||||
local current_file = (vim.uv.fs_realpath(vim.fn.expand('%:p')) or vim.fn.expand('%:p'))
|
||||
local file_state = cache.get_file_state(current_file)
|
||||
if not file_state then
|
||||
logger.log(
|
||||
'No cached state found for current file. Use :CP <platform> <contest> [--{lang=<lang>,debug}...] first.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
logger.log('No cached state found for current file.', vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
|
||||
logger.log(
|
||||
('Restoring from cached state: %s %s %s'):format(
|
||||
file_state.platform,
|
||||
file_state.contest_id,
|
||||
file_state.problem_id
|
||||
)
|
||||
)
|
||||
|
||||
local setup = require('cp.setup')
|
||||
if not setup.set_platform(file_state.platform) then
|
||||
return false
|
||||
end
|
||||
|
||||
setup.set_platform(file_state.platform)
|
||||
state.set_contest_id(file_state.contest_id)
|
||||
state.set_problem_id(file_state.problem_id)
|
||||
|
||||
setup.setup_contest(
|
||||
file_state.platform,
|
||||
file_state.contest_id,
|
||||
file_state.language,
|
||||
file_state.problem_id
|
||||
file_state.problem_id,
|
||||
file_state.language
|
||||
)
|
||||
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -7,44 +7,40 @@
|
|||
---@field peak_mb number
|
||||
---@field signal string|nil
|
||||
|
||||
---@class SubstitutableCommand
|
||||
---@field source string substituted via '{source}'
|
||||
---@field binary string substitued via '{binary}'
|
||||
|
||||
local M = {}
|
||||
local constants = require('cp.constants')
|
||||
local logger = require('cp.log')
|
||||
local utils = require('cp.utils')
|
||||
|
||||
local filetype_to_language = constants.filetype_to_language
|
||||
|
||||
local function get_language_from_file(source_file, contest_config)
|
||||
local ext = vim.fn.fnamemodify(source_file, ':e')
|
||||
return filetype_to_language[ext] or contest_config.default_language
|
||||
end
|
||||
|
||||
---@param cmd_template string[]
|
||||
---@param substitutions SubstitutableCommand
|
||||
---@return string[] string normalized with substitutions
|
||||
local function substitute_template(cmd_template, substitutions)
|
||||
local out = {}
|
||||
for _, a in ipairs(cmd_template) do
|
||||
local s = a
|
||||
for k, v in pairs(substitutions) do
|
||||
s = s:gsub('{' .. k .. '}', v)
|
||||
for _, arg in ipairs(cmd_template) do
|
||||
if arg == '{source}' and substitutions.source then
|
||||
table.insert(out, substitutions.source)
|
||||
elseif arg == '{binary}' and substitutions.binary then
|
||||
table.insert(out, substitutions.binary)
|
||||
else
|
||||
table.insert(out, arg)
|
||||
end
|
||||
table.insert(out, s)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function M.build_command(cmd_template, executable, substitutions)
|
||||
local cmd = substitute_template(cmd_template, substitutions)
|
||||
if executable then
|
||||
table.insert(cmd, 1, executable)
|
||||
end
|
||||
return cmd
|
||||
function M.build_command(cmd_template, substitutions)
|
||||
return substitute_template(cmd_template, substitutions)
|
||||
end
|
||||
|
||||
function M.compile(language_config, substitutions)
|
||||
if not language_config.compile then
|
||||
return { code = 0, stdout = '' }
|
||||
end
|
||||
|
||||
local cmd = substitute_template(language_config.compile, substitutions)
|
||||
---@param compile_cmd string[]
|
||||
---@param substitutions SubstitutableCommand
|
||||
function M.compile(compile_cmd, substitutions)
|
||||
local cmd = substitute_template(compile_cmd, substitutions)
|
||||
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
||||
|
||||
local t0 = vim.uv.hrtime()
|
||||
|
|
@ -164,32 +160,20 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
|||
}
|
||||
end
|
||||
|
||||
function M.compile_problem(contest_config, is_debug)
|
||||
function M.compile_problem()
|
||||
local state = require('cp.state')
|
||||
local source_file = state.get_source_file()
|
||||
if not source_file then
|
||||
return { success = false, output = 'No source file found.' }
|
||||
end
|
||||
local config = require('cp.config').get_config()
|
||||
local platform = state.get_platform() or ''
|
||||
local language = config.platforms[platform].default_language
|
||||
local eff = config.runtime.effective[platform][language]
|
||||
local compile_config = eff and eff.commands and eff.commands.build
|
||||
|
||||
local language = get_language_from_file(source_file, contest_config)
|
||||
local language_config = contest_config[language]
|
||||
if not language_config then
|
||||
return { success = false, output = ('No configuration for language %s.'):format(language) }
|
||||
end
|
||||
|
||||
local binary_file = state.get_binary_file()
|
||||
local substitutions = { source = source_file, binary = binary_file }
|
||||
|
||||
local chosen = (is_debug and language_config.debug) and language_config.debug
|
||||
or language_config.compile
|
||||
if not chosen then
|
||||
if not compile_config then
|
||||
return { success = true, output = nil }
|
||||
end
|
||||
|
||||
local saved = language_config.compile
|
||||
language_config.compile = chosen
|
||||
local r = M.compile(language_config, substitutions)
|
||||
language_config.compile = saved
|
||||
local substitutions = { source = state.get_source_file(), binary = state.get_binary_file() }
|
||||
local r = M.compile(compile_config, substitutions)
|
||||
|
||||
if r.code ~= 0 then
|
||||
return { success = false, output = r.stdout or 'unknown error' }
|
||||
|
|
|
|||
|
|
@ -31,8 +31,11 @@
|
|||
|
||||
local M = {}
|
||||
local cache = require('cp.cache')
|
||||
local config = require('cp.config').get_config()
|
||||
local constants = require('cp.constants')
|
||||
local execute = require('cp.runner.execute')
|
||||
local logger = require('cp.log')
|
||||
local state = require('cp.state')
|
||||
|
||||
---@type RunPanelState
|
||||
local run_panel_state = {
|
||||
|
|
@ -90,42 +93,36 @@ local function create_sentinal_panel_data(test_cases)
|
|||
return out
|
||||
end
|
||||
|
||||
---@param language_config LanguageConfig
|
||||
---@param substitutions table<string, string>
|
||||
---@param cmd string[]
|
||||
---@return string[]
|
||||
local function build_command(language_config, substitutions)
|
||||
local execute = require('cp.runner.execute')
|
||||
return execute.build_command(language_config.test, language_config.executable, substitutions)
|
||||
local function build_command(cmd, substitutions)
|
||||
return execute.build_command(cmd, substitutions)
|
||||
end
|
||||
|
||||
---@param contest_config ContestConfig
|
||||
---@param cp_config cp.Config
|
||||
---@param test_case RanTestCase
|
||||
---@return { status: "pass"|"fail"|"tle"|"mle", actual: string, actual_highlights: Highlight[], error: string, stderr: string, time_ms: number, code: integer, ok: boolean, signal: string, tled: boolean, mled: boolean, rss_mb: number }
|
||||
local function run_single_test_case(contest_config, cp_config, test_case)
|
||||
local state = require('cp.state')
|
||||
local exec = require('cp.runner.execute')
|
||||
|
||||
local function run_single_test_case(test_case)
|
||||
local source_file = state.get_source_file()
|
||||
local ext = vim.fn.fnamemodify(source_file or '', ':e')
|
||||
local lang_name = constants.filetype_to_language[ext] or contest_config.default_language
|
||||
local language_config = contest_config[lang_name]
|
||||
|
||||
local binary_file = state.get_binary_file()
|
||||
local substitutions = { source = source_file, binary = binary_file }
|
||||
|
||||
local cmd = build_command(language_config, substitutions)
|
||||
local platform_config = config.platforms[state.get_platform() or '']
|
||||
local language = platform_config.default_language
|
||||
local eff = config.runtime.effective[state.get_platform() or ''][language]
|
||||
local run_template = eff and eff.commands and eff.commands.run or {}
|
||||
local cmd = build_command(run_template, substitutions)
|
||||
local stdin_content = (test_case.input or '') .. '\n'
|
||||
local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms) or 0
|
||||
local memory_mb = run_panel_state.constraints and run_panel_state.constraints.memory_mb or 0
|
||||
|
||||
local r = exec.run(cmd, stdin_content, timeout_ms, memory_mb)
|
||||
local r = execute.run(cmd, stdin_content, timeout_ms, memory_mb)
|
||||
|
||||
local ansi = require('cp.ui.ansi')
|
||||
local out = r.stdout or ''
|
||||
local highlights = {}
|
||||
if out ~= '' then
|
||||
if cp_config.run_panel.ansi then
|
||||
if config.ui.run_panel.ansi then
|
||||
local parsed = ansi.parse_ansi_text(out)
|
||||
out = table.concat(parsed.lines, '\n')
|
||||
highlights = parsed.highlights
|
||||
|
|
@ -134,7 +131,7 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
|||
end
|
||||
end
|
||||
|
||||
local max_lines = cp_config.run_panel.max_output_lines
|
||||
local max_lines = config.ui.run_panel.max_output_lines
|
||||
local lines = vim.split(out, '\n')
|
||||
if #lines > max_lines then
|
||||
local trimmed = {}
|
||||
|
|
@ -180,9 +177,8 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
|||
}
|
||||
end
|
||||
|
||||
---@param state table
|
||||
---@return boolean
|
||||
function M.load_test_cases(state)
|
||||
function M.load_test_cases()
|
||||
local tcs = cache.get_test_cases(
|
||||
state.get_platform() or '',
|
||||
state.get_contest_id() or '',
|
||||
|
|
@ -201,18 +197,16 @@ function M.load_test_cases(state)
|
|||
return #tcs > 0
|
||||
end
|
||||
|
||||
---@param contest_config ContestConfig
|
||||
---@param cp_config cp.Config
|
||||
---@param index number
|
||||
---@return boolean
|
||||
function M.run_test_case(contest_config, cp_config, index)
|
||||
function M.run_test_case(index)
|
||||
local tc = run_panel_state.test_cases[index]
|
||||
if not tc then
|
||||
return false
|
||||
end
|
||||
|
||||
tc.status = 'running'
|
||||
local r = run_single_test_case(contest_config, cp_config, tc)
|
||||
local r = run_single_test_case(tc)
|
||||
|
||||
tc.status = r.status
|
||||
tc.actual = r.actual
|
||||
|
|
@ -230,13 +224,11 @@ function M.run_test_case(contest_config, cp_config, index)
|
|||
return true
|
||||
end
|
||||
|
||||
---@param contest_config ContestConfig
|
||||
---@param cp_config cp.Config
|
||||
---@return RanTestCase[]
|
||||
function M.run_all_test_cases(contest_config, cp_config)
|
||||
function M.run_all_test_cases()
|
||||
local results = {}
|
||||
for i = 1, #run_panel_state.test_cases do
|
||||
M.run_test_case(contest_config, cp_config, i)
|
||||
M.run_test_case(i)
|
||||
results[i] = run_panel_state.test_cases[i]
|
||||
end
|
||||
return results
|
||||
|
|
@ -251,12 +243,11 @@ end
|
|||
---@return nil
|
||||
function M.handle_compilation_failure(output)
|
||||
local ansi = require('cp.ui.ansi')
|
||||
local config = require('cp.config').setup()
|
||||
|
||||
local txt
|
||||
local hl = {}
|
||||
|
||||
if config.run_panel.ansi then
|
||||
if config.ui.run_panel.ansi then
|
||||
local p = ansi.parse_ansi_text(output or '')
|
||||
txt = table.concat(p.lines, '\n')
|
||||
hl = p.highlights
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ local utils = require('cp.utils')
|
|||
local function syshandle(result)
|
||||
if result.code ~= 0 then
|
||||
local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error')
|
||||
logger.log(msg, vim.log.levels.ERROR)
|
||||
return { success = false, error = msg }
|
||||
end
|
||||
|
||||
|
|
@ -114,7 +113,7 @@ function M.scrape_contest_metadata(platform, contest_id, callback)
|
|||
on_exit = function(result)
|
||||
if not result or not result.success then
|
||||
logger.log(
|
||||
('Failed to scrape metadata for %s contest %s.'):format(platform, contest_id),
|
||||
("Failed to scrape metadata for %s contest '%s'."):format(platform, contest_id),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
|
|
@ -122,7 +121,7 @@ function M.scrape_contest_metadata(platform, contest_id, callback)
|
|||
local data = result.data or {}
|
||||
if not data.problems or #data.problems == 0 then
|
||||
logger.log(
|
||||
('No problems returned for %s contest %s.'):format(platform, contest_id),
|
||||
("No problems returned for %s contest '%s'."):format(platform, contest_id),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return
|
||||
|
|
@ -161,7 +160,7 @@ function M.scrape_all_tests(platform, contest_id, callback)
|
|||
end
|
||||
if ev.error and ev.problem_id then
|
||||
logger.log(
|
||||
('Failed to load tests for %s/%s: %s'):format(contest_id, ev.problem_id, ev.error),
|
||||
("Failed to load tests for problem '%s': %s"):format(contest_id, ev.problem_id, ev.error),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -17,14 +17,7 @@ function M.set_platform(platform)
|
|||
)
|
||||
return false
|
||||
end
|
||||
|
||||
if state.get_platform() == platform then
|
||||
logger.log(('platform already set to %s'):format(platform))
|
||||
else
|
||||
state.set_platform(platform)
|
||||
logger.log(('platform set to %s'):format(platform))
|
||||
end
|
||||
|
||||
state.set_platform(platform)
|
||||
return true
|
||||
end
|
||||
|
||||
|
|
@ -45,15 +38,9 @@ end
|
|||
|
||||
---@param platform string
|
||||
---@param contest_id string
|
||||
---@param language string|nil
|
||||
---@param problem_id string|nil
|
||||
function M.setup_contest(platform, contest_id, language, problem_id)
|
||||
local config = config_module.get_config()
|
||||
if not vim.tbl_contains(config.scrapers, platform) then
|
||||
logger.log(('Scraping disabled for %s.'):format(platform), vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
---@param language? string|nil
|
||||
function M.setup_contest(platform, contest_id, problem_id, language)
|
||||
state.set_contest_id(contest_id)
|
||||
cache.load()
|
||||
|
||||
|
|
@ -106,10 +93,7 @@ end
|
|||
function M.setup_problem(problem_id, language)
|
||||
local platform = state.get_platform()
|
||||
if not platform then
|
||||
logger.log(
|
||||
'No platform set. run :CP <platform> <contest> [--{lang=<lang>,debug}]',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
logger.log('No platform set.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -120,25 +104,18 @@ function M.setup_problem(problem_id, language)
|
|||
vim.schedule(function()
|
||||
vim.cmd.only({ mods = { silent = true } })
|
||||
|
||||
local source_file = state.get_source_file(language)
|
||||
if not source_file then
|
||||
return
|
||||
end
|
||||
local lang = language or config.platforms[platform].default_language
|
||||
local source_file = state.get_source_file(lang)
|
||||
vim.cmd.e(source_file)
|
||||
local source_buf = vim.api.nvim_get_current_buf()
|
||||
|
||||
if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == '' then
|
||||
local has_luasnip, luasnip = pcall(require, 'luasnip')
|
||||
if has_luasnip then
|
||||
local filetype = vim.api.nvim_get_option_value('filetype', { buf = source_buf })
|
||||
local language_name = constants.filetype_to_language[filetype]
|
||||
local canonical_language = constants.canonical_filetypes[language_name] or language_name
|
||||
local prefixed_trigger = ('cp.nvim/%s.%s'):format(platform, canonical_language)
|
||||
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { prefixed_trigger })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, #prefixed_trigger })
|
||||
local ok, luasnip = pcall(require, 'luasnip')
|
||||
if ok then
|
||||
local trigger = ('cp.nvim/%s.%s'):format(platform, lang)
|
||||
vim.api.nvim_buf_set_lines(0, 0, -1, false, { trigger })
|
||||
vim.api.nvim_win_set_cursor(0, { 1, #trigger })
|
||||
vim.cmd.startinsert({ bang = true })
|
||||
|
||||
vim.schedule(function()
|
||||
if luasnip.expandable() then
|
||||
luasnip.expand()
|
||||
|
|
@ -159,13 +136,13 @@ function M.setup_problem(problem_id, language)
|
|||
vim.fn.expand('%:p'),
|
||||
platform,
|
||||
state.get_contest_id() or '',
|
||||
state.get_problem_id(),
|
||||
language
|
||||
state.get_problem_id() or '',
|
||||
lang
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
function M.navigate_problem(direction, language)
|
||||
function M.navigate_problem(direction)
|
||||
if direction == 0 then
|
||||
return
|
||||
end
|
||||
|
|
@ -176,10 +153,7 @@ function M.navigate_problem(direction, language)
|
|||
local current_problem_id = state.get_problem_id()
|
||||
|
||||
if not platform or not contest_id or not current_problem_id then
|
||||
logger.log(
|
||||
'No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug}] first.',
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
logger.log('No platform configured.', vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
|
|
@ -198,14 +172,13 @@ function M.navigate_problem(direction, language)
|
|||
|
||||
local problems = contest_data.problems
|
||||
local index = contest_data.index_map[current_problem_id]
|
||||
|
||||
local new_index = index + direction
|
||||
if new_index < 1 or new_index > #problems then
|
||||
return
|
||||
end
|
||||
|
||||
require('cp.ui.panel').disable()
|
||||
M.setup_contest(platform, contest_id, language, problems[new_index].id)
|
||||
M.setup_contest(platform, contest_id, problems[new_index].id)
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -72,18 +72,18 @@ function M.get_source_file(language)
|
|||
end
|
||||
|
||||
local config = require('cp.config').get_config()
|
||||
local contest_config = config.contests[M.get_platform()]
|
||||
if not contest_config then
|
||||
local plat = M.get_platform()
|
||||
local platform_cfg = config.platforms[plat]
|
||||
if not platform_cfg then
|
||||
return nil
|
||||
end
|
||||
|
||||
local target_language = language or contest_config.default_language
|
||||
local language_config = contest_config[target_language]
|
||||
if not language_config or not language_config.extension then
|
||||
local target_language = language or platform_cfg.default_language
|
||||
local eff = config.runtime.effective[plat] and config.runtime.effective[plat][target_language]
|
||||
or nil
|
||||
if not eff or not eff.extension then
|
||||
return nil
|
||||
end
|
||||
|
||||
return base_name .. '.' .. language_config.extension
|
||||
return base_name .. '.' .. eff.extension
|
||||
end
|
||||
|
||||
function M.get_binary_file()
|
||||
|
|
|
|||
|
|
@ -12,13 +12,81 @@ local M = {}
|
|||
|
||||
local logger = require('cp.log')
|
||||
|
||||
---@param raw_output string|table
|
||||
local dyn_hl_cache = {}
|
||||
|
||||
---@param s string|table
|
||||
---@return string
|
||||
function M.bytes_to_string(raw_output)
|
||||
if type(raw_output) == 'string' then
|
||||
return raw_output
|
||||
function M.bytes_to_string(s)
|
||||
if type(s) == 'string' then
|
||||
return s
|
||||
end
|
||||
return table.concat(vim.tbl_map(string.char, raw_output))
|
||||
return table.concat(vim.tbl_map(string.char, s))
|
||||
end
|
||||
|
||||
---@param fg table|nil
|
||||
---@param bold boolean
|
||||
---@param italic boolean
|
||||
---@return string|nil
|
||||
local function ensure_hl_for(fg, bold, italic)
|
||||
if not fg and not bold and not italic then
|
||||
return nil
|
||||
end
|
||||
|
||||
local base = 'CpAnsi'
|
||||
local suffix
|
||||
local opts = {}
|
||||
|
||||
if fg and fg.kind == 'named' then
|
||||
suffix = fg.name
|
||||
elseif fg and fg.kind == 'xterm' then
|
||||
suffix = ('X%03d'):format(fg.idx)
|
||||
local function xterm_to_hex(n)
|
||||
if n >= 0 and n <= 15 then
|
||||
local key = 'terminal_color_' .. n
|
||||
return vim.g[key]
|
||||
end
|
||||
if n >= 16 and n <= 231 then
|
||||
local c = n - 16
|
||||
local r = math.floor(c / 36) % 6
|
||||
local g = math.floor(c / 6) % 6
|
||||
local b = c % 6
|
||||
local function level(x)
|
||||
return x == 0 and 0 or 55 + 40 * x
|
||||
end
|
||||
return ('#%02x%02x%02x'):format(level(r), level(g), level(b))
|
||||
end
|
||||
local l = 8 + 10 * (n - 232)
|
||||
return ('#%02x%02x%02x'):format(l, l, l)
|
||||
end
|
||||
opts.fg = xterm_to_hex(fg.idx) or 'NONE'
|
||||
elseif fg and fg.kind == 'rgb' then
|
||||
suffix = ('Rgb%02x%02x%02x'):format(fg.r, fg.g, fg.b)
|
||||
opts.fg = ('#%02x%02x%02x'):format(fg.r, fg.g, fg.b)
|
||||
end
|
||||
|
||||
local parts = { base }
|
||||
if bold then
|
||||
table.insert(parts, 'Bold')
|
||||
end
|
||||
if italic then
|
||||
table.insert(parts, 'Italic')
|
||||
end
|
||||
if suffix then
|
||||
table.insert(parts, suffix)
|
||||
end
|
||||
local name = table.concat(parts)
|
||||
|
||||
if not dyn_hl_cache[name] then
|
||||
if bold then
|
||||
opts.bold = true
|
||||
end
|
||||
if italic then
|
||||
opts.italic = true
|
||||
end
|
||||
vim.api.nvim_set_hl(0, name, opts)
|
||||
dyn_hl_cache[name] = true
|
||||
end
|
||||
return name
|
||||
end
|
||||
|
||||
---@param text string
|
||||
|
|
@ -38,22 +106,7 @@ function M.parse_ansi_text(text)
|
|||
}
|
||||
|
||||
local function get_highlight_group()
|
||||
if not ansi_state.bold and not ansi_state.italic and not ansi_state.foreground then
|
||||
return nil
|
||||
end
|
||||
|
||||
local parts = { 'CpAnsi' }
|
||||
if ansi_state.bold then
|
||||
table.insert(parts, 'Bold')
|
||||
end
|
||||
if ansi_state.italic then
|
||||
table.insert(parts, 'Italic')
|
||||
end
|
||||
if ansi_state.foreground then
|
||||
table.insert(parts, ansi_state.foreground)
|
||||
end
|
||||
|
||||
return table.concat(parts)
|
||||
return ensure_hl_for(ansi_state.foreground, ansi_state.bold, ansi_state.italic)
|
||||
end
|
||||
|
||||
local function apply_highlight(start_line, start_col, end_col)
|
||||
|
|
@ -137,6 +190,7 @@ end
|
|||
|
||||
---@param ansi_state table
|
||||
---@param code_string string
|
||||
---@return nil
|
||||
function M.update_ansi_state(ansi_state, code_string)
|
||||
if code_string == '' or code_string == '0' then
|
||||
ansi_state.bold = false
|
||||
|
|
@ -146,40 +200,60 @@ function M.update_ansi_state(ansi_state, code_string)
|
|||
end
|
||||
|
||||
local codes = vim.split(code_string, ';', { plain = true })
|
||||
local idx = 1
|
||||
while idx <= #codes do
|
||||
local num = tonumber(codes[idx])
|
||||
|
||||
for _, code in ipairs(codes) do
|
||||
local num = tonumber(code)
|
||||
if num then
|
||||
if num == 1 then
|
||||
ansi_state.bold = true
|
||||
elseif num == 3 then
|
||||
ansi_state.italic = true
|
||||
elseif num == 22 then
|
||||
ansi_state.bold = false
|
||||
elseif num == 23 then
|
||||
ansi_state.italic = false
|
||||
elseif num >= 30 and num <= 37 then
|
||||
local colors = { 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' }
|
||||
ansi_state.foreground = colors[num - 29]
|
||||
elseif num >= 90 and num <= 97 then
|
||||
local colors = {
|
||||
'BrightBlack',
|
||||
'BrightRed',
|
||||
'BrightGreen',
|
||||
'BrightYellow',
|
||||
'BrightBlue',
|
||||
'BrightMagenta',
|
||||
'BrightCyan',
|
||||
'BrightWhite',
|
||||
}
|
||||
ansi_state.foreground = colors[num - 89]
|
||||
elseif num == 39 then
|
||||
ansi_state.foreground = nil
|
||||
if num == 1 then
|
||||
ansi_state.bold = true
|
||||
elseif num == 3 then
|
||||
ansi_state.italic = true
|
||||
elseif num == 22 then
|
||||
ansi_state.bold = false
|
||||
elseif num == 23 then
|
||||
ansi_state.italic = false
|
||||
elseif num and num >= 30 and num <= 37 then
|
||||
local colors = { 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' }
|
||||
ansi_state.foreground = { kind = 'named', name = colors[num - 29] }
|
||||
elseif num and num >= 90 and num <= 97 then
|
||||
local colors = {
|
||||
'BrightBlack',
|
||||
'BrightRed',
|
||||
'BrightGreen',
|
||||
'BrightYellow',
|
||||
'BrightBlue',
|
||||
'BrightMagenta',
|
||||
'BrightCyan',
|
||||
'BrightWhite',
|
||||
}
|
||||
ansi_state.foreground = { kind = 'named', name = colors[num - 89] }
|
||||
elseif num == 39 then
|
||||
ansi_state.foreground = nil
|
||||
elseif num == 38 or num == 48 then
|
||||
local is_fg = (num == 38)
|
||||
local mode = tonumber(codes[idx + 1] or '')
|
||||
if mode == 5 and codes[idx + 2] then
|
||||
local pal = tonumber(codes[idx + 2]) or 0
|
||||
if is_fg then
|
||||
ansi_state.foreground = { kind = 'xterm', idx = pal }
|
||||
end
|
||||
idx = idx + 2
|
||||
elseif mode == 2 and codes[idx + 2] and codes[idx + 3] and codes[idx + 4] then
|
||||
local r = tonumber(codes[idx + 2]) or 0
|
||||
local g = tonumber(codes[idx + 3]) or 0
|
||||
local b = tonumber(codes[idx + 4]) or 0
|
||||
if is_fg then
|
||||
ansi_state.foreground = { kind = 'rgb', r = r, g = g, b = b }
|
||||
end
|
||||
idx = idx + 4
|
||||
end
|
||||
end
|
||||
|
||||
idx = idx + 1
|
||||
end
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.setup_highlight_groups()
|
||||
local color_map = {
|
||||
Black = vim.g.terminal_color_0,
|
||||
|
|
@ -202,7 +276,7 @@ function M.setup_highlight_groups()
|
|||
|
||||
if vim.tbl_count(color_map) < 16 then
|
||||
logger.log(
|
||||
'ansi terminal colors (vim.g.terminal_color_*) not configured. ANSI colors will not display properly. ',
|
||||
'ansi terminal colors (vim.g.terminal_color_*) not configured. ANSI colors will not display properly.',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
end
|
||||
|
|
@ -218,7 +292,6 @@ function M.setup_highlight_groups()
|
|||
for color_name, terminal_color in pairs(color_map) do
|
||||
local parts = { 'CpAnsi' }
|
||||
local opts = { fg = terminal_color or 'NONE' }
|
||||
|
||||
if combo.bold then
|
||||
table.insert(parts, 'Bold')
|
||||
opts.bold = true
|
||||
|
|
@ -228,7 +301,6 @@ function M.setup_highlight_groups()
|
|||
opts.italic = true
|
||||
end
|
||||
table.insert(parts, color_name)
|
||||
|
||||
local hl_name = table.concat(parts)
|
||||
vim.api.nvim_set_hl(0, hl_name, opts)
|
||||
end
|
||||
|
|
@ -239,4 +311,30 @@ function M.setup_highlight_groups()
|
|||
vim.api.nvim_set_hl(0, 'CpAnsiBoldItalic', { bold = true, italic = true })
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@return string[]
|
||||
function M.debug_ansi_tokens(text)
|
||||
local out = {}
|
||||
local i = 1
|
||||
while true do
|
||||
local s, e, codes, cmd = text:find('\027%[([%d;]*)([a-zA-Z])', i)
|
||||
if not s then
|
||||
break
|
||||
end
|
||||
table.insert(out, ('ESC[%s%s'):format(codes, cmd))
|
||||
i = e + 1
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
---@param s string
|
||||
---@return string
|
||||
function M.hex_dump(s)
|
||||
local t = {}
|
||||
for i = 1, #s do
|
||||
t[#t + 1] = ('%02X'):format(s:byte(i))
|
||||
end
|
||||
return table.concat(t, ' ')
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ function M.update_diff_panes(
|
|||
actual_content = actual_content
|
||||
end
|
||||
|
||||
local desired_mode = is_compilation_failure and 'single' or config.run_panel.diff_mode
|
||||
local desired_mode = is_compilation_failure and 'single' or config.ui.run_panel.diff_mode
|
||||
local highlight = require('cp.ui.highlight')
|
||||
local diff_namespace = highlight.create_namespace()
|
||||
local ansi_namespace = vim.api.nvim_create_namespace('cp_ansi_highlights')
|
||||
|
|
|
|||
|
|
@ -90,10 +90,8 @@ function M.toggle_interactive()
|
|||
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)
|
||||
local compile_result = execute.compile_problem()
|
||||
if not compile_result.success then
|
||||
require('cp.runner.run').handle_compilation_failure(compile_result.output)
|
||||
return
|
||||
|
|
@ -120,7 +118,8 @@ function M.toggle_interactive()
|
|||
state.set_active_panel('interactive')
|
||||
end
|
||||
|
||||
function M.toggle_run_panel(is_debug)
|
||||
---@param debug? boolean
|
||||
function M.toggle_run_panel(debug)
|
||||
if state.get_active_panel() == 'run' then
|
||||
if current_diff_layout then
|
||||
current_diff_layout.cleanup()
|
||||
|
|
@ -191,7 +190,7 @@ 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
|
||||
if debug and config.hooks and config.hooks.before_debug then
|
||||
config.hooks.before_debug(state)
|
||||
end
|
||||
|
||||
|
|
@ -199,7 +198,7 @@ function M.toggle_run_panel(is_debug)
|
|||
local input_file = state.get_input_file()
|
||||
logger.log(('run panel: checking test cases for %s'):format(input_file or 'none'))
|
||||
|
||||
if not run.load_test_cases(state) then
|
||||
if not run.load_test_cases() then
|
||||
logger.log('no test cases found', vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
|
@ -264,37 +263,29 @@ function M.toggle_run_panel(is_debug)
|
|||
local modes = { 'none', 'git', 'vim' }
|
||||
local current_idx = nil
|
||||
for i, mode in ipairs(modes) do
|
||||
if config.run_panel.diff_mode == mode then
|
||||
if config.ui.run_panel.diff_mode == mode then
|
||||
current_idx = i
|
||||
break
|
||||
end
|
||||
end
|
||||
current_idx = current_idx or 1
|
||||
config.run_panel.diff_mode = modes[(current_idx % #modes) + 1]
|
||||
config.ui.run_panel.diff_mode = modes[(current_idx % #modes) + 1]
|
||||
refresh_run_panel()
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', config.run_panel.next_test_key, function()
|
||||
vim.keymap.set('n', '<c-n>', function()
|
||||
navigate_test_case(1)
|
||||
end, { buffer = buf, silent = true })
|
||||
vim.keymap.set('n', config.run_panel.prev_test_key, function()
|
||||
vim.keymap.set('n', '<c-p>', function()
|
||||
navigate_test_case(-1)
|
||||
end, { buffer = buf, silent = true })
|
||||
end
|
||||
|
||||
vim.keymap.set('n', config.run_panel.next_test_key, function()
|
||||
navigate_test_case(1)
|
||||
end, { buffer = test_buffers.tab_buf, silent = true })
|
||||
vim.keymap.set('n', config.run_panel.prev_test_key, function()
|
||||
navigate_test_case(-1)
|
||||
end, { buffer = test_buffers.tab_buf, silent = true })
|
||||
|
||||
setup_keybindings_for_buffer(test_buffers.tab_buf)
|
||||
|
||||
local execute = require('cp.runner.execute')
|
||||
local contest_config = config.contests[state.get_platform() or '']
|
||||
local compile_result = execute.compile_problem(contest_config, is_debug)
|
||||
local compile_result = execute.compile_problem()
|
||||
if compile_result.success then
|
||||
run.run_all_test_cases(contest_config, config)
|
||||
run.run_all_test_cases()
|
||||
else
|
||||
run.handle_compilation_failure(compile_result.output)
|
||||
end
|
||||
|
|
@ -302,7 +293,7 @@ function M.toggle_run_panel(is_debug)
|
|||
refresh_run_panel()
|
||||
|
||||
vim.schedule(function()
|
||||
if config.run_panel.ansi then
|
||||
if config.ui.run_panel.ansi then
|
||||
local ansi = require('cp.ui.ansi')
|
||||
ansi.setup_highlight_groups()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -265,14 +265,26 @@ class AtcoderScraper(BaseScraper):
|
|||
|
||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||
async def impl(cid: str) -> MetadataResult:
|
||||
rows = await asyncio.to_thread(_scrape_tasks_sync, cid)
|
||||
try:
|
||||
rows = await asyncio.to_thread(_scrape_tasks_sync, cid)
|
||||
except requests.HTTPError as e:
|
||||
if e.response is not None and e.response.status_code == 404:
|
||||
return self._create_metadata_error(
|
||||
f"No problems found for contest {cid}", cid
|
||||
)
|
||||
raise
|
||||
|
||||
problems = _to_problem_summaries(rows)
|
||||
if not problems:
|
||||
return self._create_metadata_error(
|
||||
f"No problems found for contest {cid}", cid
|
||||
)
|
||||
|
||||
return MetadataResult(
|
||||
success=True, error="", contest_id=cid, problems=problems
|
||||
success=True,
|
||||
error="",
|
||||
contest_id=cid,
|
||||
problems=problems,
|
||||
)
|
||||
|
||||
return await self._safe_execute("metadata", impl, contest_id)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue