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
|
### Basic Usage
|
||||||
|
|
||||||
1. **Find a contest or problem** on the judge website
|
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
|
:CP codeforces 1848
|
||||||
|
|
|
||||||
239
doc/cp.nvim.txt
239
doc/cp.nvim.txt
|
|
@ -32,16 +32,13 @@ COMMANDS *cp-commands*
|
||||||
Automatically detects platform, contest, problem,
|
Automatically detects platform, contest, problem,
|
||||||
and language from cached state. Use this after
|
and language from cached state. Use this after
|
||||||
switching files to restore your CP environment.
|
switching files to restore your CP environment.
|
||||||
Requires previous setup with full :CP command.
|
|
||||||
|
|
||||||
Setup Commands ~
|
Setup Commands ~
|
||||||
:CP {platform} {contest_id} {problem_id} [--lang={language}]
|
:CP {platform} {contest_id}
|
||||||
Full setup: set platform, load contest metadata,
|
Full setup: set platform and load contest metadata.
|
||||||
and set up specific problem. Scrapes test cases
|
Scrapes test cases and creates source file.
|
||||||
and creates source file.
|
|
||||||
Example: >
|
Example: >
|
||||||
:CP codeforces 1933 a
|
:CP codeforces 1933
|
||||||
:CP codeforces 1933 a --lang=python
|
|
||||||
<
|
<
|
||||||
:CP {platform} {contest_id}
|
:CP {platform} {contest_id}
|
||||||
Contest setup: set platform, load contest metadata,
|
Contest setup: set platform, load contest metadata,
|
||||||
|
|
@ -52,10 +49,6 @@ COMMANDS *cp-commands*
|
||||||
Example: >
|
Example: >
|
||||||
:CP atcoder abc324
|
:CP atcoder abc324
|
||||||
:CP codeforces 1951
|
:CP codeforces 1951
|
||||||
<
|
|
||||||
:CP {platform} Platform setup: set platform only.
|
|
||||||
Example: >
|
|
||||||
:CP cses
|
|
||||||
<
|
<
|
||||||
Action Commands ~
|
Action Commands ~
|
||||||
:CP run [--debug] Toggle run panel for individual test case
|
:CP run [--debug] Toggle run panel for individual test case
|
||||||
|
|
@ -65,12 +58,13 @@ COMMANDS *cp-commands*
|
||||||
Requires contest setup first.
|
Requires contest setup first.
|
||||||
|
|
||||||
:CP pick Launch configured picker for interactive
|
:CP pick Launch configured picker for interactive
|
||||||
platform/contest/problem selection.
|
platform/contest selection.
|
||||||
|
|
||||||
Navigation Commands ~
|
Navigation Commands ~
|
||||||
:CP next Navigate to next problem in current contest.
|
:CP next Navigate to next problem in current contest.
|
||||||
Stops at last problem (no wrapping).
|
Stops at last problem (no wrapping).
|
||||||
|
|
||||||
|
|
||||||
Navigation Commands ~
|
Navigation Commands ~
|
||||||
:CP prev Navigate to previous problem in current contest.
|
:CP prev Navigate to previous problem in current contest.
|
||||||
Stops at first problem (no wrapping).
|
Stops at first problem (no wrapping).
|
||||||
|
|
@ -79,7 +73,7 @@ COMMANDS *cp-commands*
|
||||||
:CP cache clear [contest]
|
:CP cache clear [contest]
|
||||||
Clear the cache data (contest list, problem
|
Clear the cache data (contest list, problem
|
||||||
data, file states) for the specified contest,
|
data, file states) for the specified contest,
|
||||||
or all contests if none specified
|
or all contests if none specified.
|
||||||
|
|
||||||
:CP cache read
|
:CP cache read
|
||||||
View the cache in a pretty-printed lua buffer.
|
View the cache in a pretty-printed lua buffer.
|
||||||
|
|
@ -89,22 +83,15 @@ Command Flags ~
|
||||||
*cp-flags*
|
*cp-flags*
|
||||||
Flags can be used with setup and action commands:
|
Flags can be used with setup and action commands:
|
||||||
|
|
||||||
--lang={language} Specify language for the problem.
|
--debug Use the debug command template.
|
||||||
--lang {language} Alternative syntax for language specification.
|
For compiled languages, this selects
|
||||||
Supported languages: cpp, python
|
`commands.debug` (a debug *build*) instead of
|
||||||
Example: >
|
`commands.build`. For interpreted languages,
|
||||||
:CP atcoder abc324 a --lang=python
|
this selects `commands.debug` in place of
|
||||||
:CP b --lang cpp
|
`commands.run`.
|
||||||
<
|
|
||||||
--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.
|
|
||||||
Example: >
|
Example: >
|
||||||
:CP run --debug
|
:CP run --debug
|
||||||
<
|
<
|
||||||
Note: Debug compilation may be slower but provides
|
|
||||||
better error reporting for runtime issues.
|
|
||||||
|
|
||||||
Template Variables ~
|
Template Variables ~
|
||||||
*cp-template-vars*
|
*cp-template-vars*
|
||||||
|
|
@ -116,7 +103,7 @@ Template Variables ~
|
||||||
• {problem} Problem identifier (e.g. "a", "b")
|
• {problem} Problem identifier (e.g. "a", "b")
|
||||||
|
|
||||||
Example template: >
|
Example template: >
|
||||||
compile = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
|
build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
|
||||||
< Would expand to: >
|
< Would expand to: >
|
||||||
g++ abc324a.cpp -o build/abc324a.run -std=c++17
|
g++ abc324a.cpp -o build/abc324a.run -std=c++17
|
||||||
<
|
<
|
||||||
|
|
@ -129,31 +116,52 @@ Here's an example configuration with lazy.nvim: >lua
|
||||||
{
|
{
|
||||||
'barrett-ruth/cp.nvim',
|
'barrett-ruth/cp.nvim',
|
||||||
cmd = 'CP',
|
cmd = 'CP',
|
||||||
|
build = 'uv sync',
|
||||||
opts = {
|
opts = {
|
||||||
contests = {
|
languages = {
|
||||||
default = {
|
|
||||||
cpp = {
|
cpp = {
|
||||||
compile = { 'g++', '{source}', '-o', '{binary}',
|
extension = 'cc',
|
||||||
'-std=c++17', '-fdiagnostic-colors=always' },
|
commands = {
|
||||||
test = { '{binary}' },
|
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}' },
|
||||||
debug = { 'g++', '{source}', '-o', '{binary}',
|
run = { '{binary}' },
|
||||||
'-std=c++17', '-g',
|
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
|
||||||
'-fdiagnostic-colors=always'
|
'{source}', '-o', '{binary}' },
|
||||||
'-fsanitize=address,undefined' },
|
},
|
||||||
},
|
},
|
||||||
python = {
|
python = {
|
||||||
test = { 'python3', '{source}' },
|
extension = 'py',
|
||||||
|
commands = {
|
||||||
|
run = { 'python', '{source}' },
|
||||||
|
debug = { 'python', '{source}' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
platforms = {
|
||||||
|
cses = {
|
||||||
|
enabled_languages = { 'cpp', 'python' },
|
||||||
|
default_language = 'cpp',
|
||||||
|
overrides = {
|
||||||
|
cpp = { extension = 'cpp', commands = { build = { ... } } }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
atcoder = {
|
||||||
|
enabled_languages = { 'cpp', 'python' },
|
||||||
|
default_language = 'cpp',
|
||||||
|
},
|
||||||
|
codeforces = {
|
||||||
|
enabled_languages = { 'cpp', 'python' },
|
||||||
|
default_language = 'cpp',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
snippets = {},
|
snippets = {},
|
||||||
debug = false,
|
debug = false,
|
||||||
scrapers = { 'atcoder', 'codeforces', 'cses' },
|
|
||||||
|
ui = {
|
||||||
run_panel = {
|
run_panel = {
|
||||||
ansi = true,
|
ansi = true,
|
||||||
diff_mode = 'vim',
|
diff_mode = 'vim',
|
||||||
next_test_key = '<c-n>',
|
|
||||||
prev_test_key = '<c-p>',
|
|
||||||
max_output_lines = 50,
|
max_output_lines = 50,
|
||||||
},
|
},
|
||||||
diff = {
|
diff = {
|
||||||
|
|
@ -162,75 +170,105 @@ Here's an example configuration with lazy.nvim: >lua
|
||||||
'--word-diff-regex=.', '--no-prefix' },
|
'--word-diff-regex=.', '--no-prefix' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
picker = 'telescope', -- 'telescope', 'fzf-lua', or nil (disabled)
|
picker = 'telescope',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
<
|
<
|
||||||
|
|
||||||
By default, all contests are configured to use C++ with the g++ compiler and ISO standard
|
By default, C++ (g++ with ISO C++17) and Python are preconfigured under
|
||||||
17. Python is also configured with the system executable python as a non-default option. Consult lua/cp/config.lua for
|
`languages`. Platforms select which languages are enabled and which one is
|
||||||
more information.
|
the default; per-platform overrides can tweak `extension` or `commands`.
|
||||||
|
|
||||||
For example, to run CodeForces contests with Python, only the following config
|
For example, to run CodeForces contests with Python by default:
|
||||||
is required:
|
|
||||||
|
|
||||||
|
>lua
|
||||||
{
|
{
|
||||||
contests = {
|
platforms = {
|
||||||
codeforces = {
|
codeforces = {
|
||||||
default_langauge = 'python'
|
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*
|
*cp.Config*
|
||||||
Fields: ~
|
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.
|
{hooks} (|cp.Hooks|) Hook functions called at various stages.
|
||||||
{snippets} (table[]) LuaSnip snippet definitions.
|
{snippets} (table[]) LuaSnip snippet definitions.
|
||||||
{debug} (boolean, default: false) Show info messages
|
{debug} (boolean, default: false) Show info messages.
|
||||||
during operation.
|
{scrapers} (string[]) Supported platform ids.
|
||||||
{scrapers} (table<string>) List of enabled scrapers.
|
{filename} (function, optional)
|
||||||
Default: all scrapers enabled
|
function(contest, contest_id, problem_id, config, language): string
|
||||||
{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)
|
|
||||||
Should return full filename with extension.
|
Should return full filename with extension.
|
||||||
(default: concatenates contest_id and problem_id, lowercased)
|
(default: concatenates contest_id and problem_id, lowercased)
|
||||||
|
{ui} (|CpUI|) UI settings: run panel, diff backend, picker.
|
||||||
|
|
||||||
*cp.ContestConfig*
|
*cp.PlatformConfig*
|
||||||
Fields: ~
|
Replaced by |CpPlatform|. Platforms no longer inline language tables.
|
||||||
{cpp} (|LanguageConfig|) C++ language configuration.
|
|
||||||
{python} (|LanguageConfig|) Python language configuration.
|
|
||||||
{default_language} (string, default: "cpp") Default language when
|
|
||||||
--lang not specified.
|
|
||||||
|
|
||||||
*cp.LanguageConfig*
|
*CpPlatform*
|
||||||
Fields: ~
|
Fields: ~
|
||||||
{compile} (string[], optional) Compile command template with
|
{enabled_languages} (string[]) Language ids enabled on this platform.
|
||||||
{source}, {binary} placeholders.
|
{default_language} (string) One of {enabled_languages}.
|
||||||
{test} (string[]) Test execution command template.
|
{overrides} (table<string,|CpPlatformOverrides|>, optional)
|
||||||
{debug} (string[], optional) Debug compile command template.
|
Per-language overrides of {extension} and/or {commands}.
|
||||||
{extension} (string) File extension (e.g. "cc", "py").
|
|
||||||
{executable} (string, optional) Executable name for interpreted languages.
|
*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*
|
*cp.RunPanelConfig*
|
||||||
Fields: ~
|
Fields: ~
|
||||||
{ansi} (boolean, default: true) Enable ANSI color parsing and
|
{ansi} (boolean, default: true) Enable ANSI color parsing
|
||||||
highlighting. When true, compiler output and test results
|
and highlighting.
|
||||||
display with colored syntax highlighting. When false,
|
{diff_mode} (string, default: "none") Diff backend: "none",
|
||||||
ANSI escape codes are stripped for plain text display.
|
"vim", or "git".
|
||||||
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.
|
|
||||||
{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*
|
||||||
|
|
@ -251,10 +289,9 @@ is required:
|
||||||
Fields: ~
|
Fields: ~
|
||||||
{before_run} (function, optional) Called before test panel opens.
|
{before_run} (function, optional) Called before test panel opens.
|
||||||
function(state: cp.State)
|
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)
|
function(state: cp.State)
|
||||||
{setup_code} (function, optional) Called after source file is opened.
|
{setup_code} (function, optional) Called after source file is opened.
|
||||||
Good for configuring buffer settings.
|
|
||||||
function(state: cp.State)
|
function(state: cp.State)
|
||||||
|
|
||||||
Hook functions receive the cp.nvim state object (cp.State). See the 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
|
URL format: https://atcoder.jp/contests/abc123/tasks/abc123_a
|
||||||
|
|
||||||
Usage examples: >
|
Usage examples: >
|
||||||
:CP atcoder abc324 a " Full setup: problem A from contest ABC324
|
|
||||||
:CP atcoder abc324 " Contest setup: load contest metadata only
|
: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 ~
|
Codeforces ~
|
||||||
*cp-codeforces*
|
*cp-codeforces*
|
||||||
URL format: https://codeforces.com/contest/1234/problem/A
|
URL format: https://codeforces.com/contest/1234/problem/A
|
||||||
|
|
||||||
Usage examples: >
|
Usage examples: >
|
||||||
:CP codeforces 1934 a " Full setup: problem A from contest 1934
|
|
||||||
:CP codeforces 1934 " Contest setup: load contest metadata only
|
: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 ~
|
CSES ~
|
||||||
*cp-cses*
|
*cp-cses*
|
||||||
URL format: https://cses.fi/problemset/task/1068
|
URL format: https://cses.fi/problemset/task/1068
|
||||||
|
|
||||||
Usage examples: >
|
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
|
: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*
|
RUN PANEL KEYMAPS *cp-test-keys*
|
||||||
<c-n> Navigate to next test case (configurable via
|
<c-n> Navigate to next test case
|
||||||
run_panel.next_test_key)
|
<c-p> Navigate to previous test case
|
||||||
<c-p> Navigate to previous test case (configurable via
|
|
||||||
run_panel.prev_test_key)
|
|
||||||
t Cycle through diff modes: none → git → vim
|
t Cycle through diff modes: none → git → vim
|
||||||
q Exit run panel and restore layout
|
q Exit run panel and restore layout
|
||||||
<c-q> Exit interactive terminal 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' },
|
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
|
end
|
||||||
|
|
||||||
---@param platform string
|
---@param platform string
|
||||||
|
|
@ -105,7 +107,7 @@ function M.set_contest_data(platform, contest_id, problems)
|
||||||
local out = {
|
local out = {
|
||||||
name = prev.name,
|
name = prev.name,
|
||||||
display_name = prev.display_name,
|
display_name = prev.display_name,
|
||||||
problems = vim.deepcopy(problems),
|
problems = problems,
|
||||||
index_map = {},
|
index_map = {},
|
||||||
}
|
}
|
||||||
for i, p in ipairs(out.problems) do
|
for i, p in ipairs(out.problems) do
|
||||||
|
|
@ -207,32 +209,27 @@ function M.get_constraints(platform, contest_id, problem_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param file_path string
|
---@param file_path string
|
||||||
---@return FileState?
|
---@return FileState|nil
|
||||||
function M.get_file_state(file_path)
|
function M.get_file_state(file_path)
|
||||||
if not cache_data.file_states then
|
M.load()
|
||||||
return nil
|
cache_data.file_states = cache_data.file_states or {}
|
||||||
end
|
|
||||||
|
|
||||||
return cache_data.file_states[file_path]
|
return cache_data.file_states[file_path]
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param file_path string
|
---@param path string
|
||||||
---@param platform string
|
---@param platform string
|
||||||
---@param contest_id string
|
---@param contest_id string
|
||||||
---@param problem_id? string
|
---@param problem_id string
|
||||||
---@param language? string
|
---@param language string|nil
|
||||||
function M.set_file_state(file_path, platform, contest_id, problem_id, language)
|
function M.set_file_state(path, platform, contest_id, problem_id, language)
|
||||||
if not cache_data.file_states then
|
M.load()
|
||||||
cache_data.file_states = {}
|
cache_data.file_states = cache_data.file_states or {}
|
||||||
end
|
cache_data.file_states[path] = {
|
||||||
|
|
||||||
cache_data.file_states[file_path] = {
|
|
||||||
platform = platform,
|
platform = platform,
|
||||||
contest_id = contest_id,
|
contest_id = contest_id,
|
||||||
problem_id = problem_id,
|
problem_id = problem_id,
|
||||||
language = language,
|
language = language,
|
||||||
}
|
}
|
||||||
|
|
||||||
M.save()
|
M.save()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -255,7 +252,7 @@ end
|
||||||
function M.set_contest_summaries(platform, contests)
|
function M.set_contest_summaries(platform, contests)
|
||||||
cache_data[platform] = cache_data[platform] or {}
|
cache_data[platform] = cache_data[platform] or {}
|
||||||
for _, contest in ipairs(contests) do
|
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].display_name = contest.display_name
|
||||||
cache_data[platform][contest.id].name = contest.name
|
cache_data[platform][contest.id].name = contest.name
|
||||||
end
|
end
|
||||||
|
|
@ -284,4 +281,6 @@ function M.get_data_pretty()
|
||||||
return vim.inspect(cache_data)
|
return vim.inspect(cache_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
M._cache = cache_data
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ local actions = constants.ACTIONS
|
||||||
---@class ParsedCommand
|
---@class ParsedCommand
|
||||||
---@field type string
|
---@field type string
|
||||||
---@field error string?
|
---@field error string?
|
||||||
---@field language? string
|
|
||||||
---@field debug? boolean
|
---@field debug? boolean
|
||||||
---@field action? string
|
---@field action? string
|
||||||
---@field message? string
|
---@field message? string
|
||||||
|
|
@ -27,26 +26,10 @@ local function parse_command(args)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local language = nil
|
local debug = vim.tbl_contains(args, '--debug')
|
||||||
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 filtered_args = vim.tbl_filter(function(arg)
|
local filtered_args = vim.tbl_filter(function(arg)
|
||||||
return not (arg:match('^--lang') or arg == language or arg == '--debug')
|
return arg ~= '--debug'
|
||||||
end, args)
|
end, args)
|
||||||
|
|
||||||
local first = filtered_args[1]
|
local first = filtered_args[1]
|
||||||
|
|
@ -68,7 +51,7 @@ local function parse_command(args)
|
||||||
return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand }
|
return { type = 'error', message = 'unknown cache subcommand: ' .. subcommand }
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
return { type = 'action', action = first, language = language, debug = debug }
|
return { type = 'action', action = first, debug = debug }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -83,12 +66,11 @@ local function parse_command(args)
|
||||||
type = 'contest_setup',
|
type = 'contest_setup',
|
||||||
platform = first,
|
platform = first,
|
||||||
contest = filtered_args[2],
|
contest = filtered_args[2],
|
||||||
language = language,
|
|
||||||
}
|
}
|
||||||
elseif #filtered_args == 3 then
|
elseif #filtered_args == 3 then
|
||||||
return {
|
return {
|
||||||
type = 'error',
|
type = 'error',
|
||||||
message = 'Setup contests with :CP <platform> <contest_id> [--{lang=<lang>,debug}]',
|
message = 'Setup contests with :CP <platform> <contest_id>',
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
return { type = 'error', message = 'Too many arguments' }
|
return { type = 'error', message = 'Too many arguments' }
|
||||||
|
|
@ -129,9 +111,9 @@ function M.handle_command(opts)
|
||||||
elseif cmd.action == 'run' then
|
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)
|
||||||
elseif cmd.action == 'prev' then
|
elseif cmd.action == 'prev' then
|
||||||
setup.navigate_problem(-1, cmd.language)
|
setup.navigate_problem(-1)
|
||||||
elseif cmd.action == 'pick' then
|
elseif cmd.action == 'pick' then
|
||||||
local picker = require('cp.commands.picker')
|
local picker = require('cp.commands.picker')
|
||||||
picker.handle_pick_action()
|
picker.handle_pick_action()
|
||||||
|
|
@ -142,7 +124,7 @@ function M.handle_command(opts)
|
||||||
elseif cmd.type == 'contest_setup' then
|
elseif cmd.type == 'contest_setup' then
|
||||||
local setup = require('cp.setup')
|
local setup = require('cp.setup')
|
||||||
if setup.set_platform(cmd.platform) then
|
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
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ local logger = require('cp.log')
|
||||||
function M.handle_pick_action()
|
function M.handle_pick_action()
|
||||||
local config = config_module.get_config()
|
local config = config_module.get_config()
|
||||||
|
|
||||||
if not config.picker then
|
if not (config.ui and config.ui.picker) then
|
||||||
logger.log(
|
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
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -18,7 +18,8 @@ function M.handle_pick_action()
|
||||||
|
|
||||||
local picker
|
local picker
|
||||||
|
|
||||||
if config.picker == 'telescope' then
|
local picker_name = config.ui.picker
|
||||||
|
if picker_name == 'telescope' then
|
||||||
local ok = pcall(require, 'telescope')
|
local ok = pcall(require, 'telescope')
|
||||||
if not ok then
|
if not ok then
|
||||||
logger.log(
|
logger.log(
|
||||||
|
|
@ -34,7 +35,7 @@ function M.handle_pick_action()
|
||||||
end
|
end
|
||||||
|
|
||||||
picker = telescope_picker
|
picker = telescope_picker
|
||||||
elseif config.picker == 'fzf-lua' then
|
elseif picker_name == 'fzf-lua' then
|
||||||
local ok, _ = pcall(require, 'fzf-lua')
|
local ok, _ = pcall(require, 'fzf-lua')
|
||||||
if not ok then
|
if not ok then
|
||||||
logger.log(
|
logger.log(
|
||||||
|
|
|
||||||
|
|
@ -1,262 +1,279 @@
|
||||||
---@class LanguageConfig
|
-- lua/cp/config.lua
|
||||||
---@field compile? string[] Compile command template
|
---@class CpLangCommands
|
||||||
---@field test string[] Test execution command template
|
---@field build? string[]
|
||||||
---@field debug? string[] Debug command template
|
---@field run? string[]
|
||||||
---@field executable? string Executable name
|
---@field debug? string[]
|
||||||
---@field version? number Language version
|
|
||||||
---@field extension? string File extension
|
|
||||||
|
|
||||||
---@class ContestConfig
|
---@class CpLanguage
|
||||||
---@field cpp LanguageConfig
|
---@field extension string
|
||||||
---@field python LanguageConfig
|
---@field commands CpLangCommands
|
||||||
---@field default_language? string
|
|
||||||
|
---@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
|
---@class Hooks
|
||||||
---@field before_run? fun(state: cp.State)
|
---@field before_run? fun(state: cp.State)
|
||||||
---@field before_debug? fun(state: cp.State)
|
---@field before_debug? fun(state: cp.State)
|
||||||
---@field setup_code? fun(state: cp.State)
|
---@field setup_code? fun(state: cp.State)
|
||||||
|
|
||||||
---@class RunPanelConfig
|
---@class CpUI
|
||||||
---@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
|
|
||||||
---@field run_panel RunPanelConfig
|
---@field run_panel RunPanelConfig
|
||||||
---@field diff DiffConfig
|
---@field diff DiffConfig
|
||||||
---@field picker string|nil
|
---@field picker string|nil
|
||||||
|
|
||||||
---@class cp.PartialConfig
|
---@class cp.Config
|
||||||
---@field contests? table<string, ContestConfig>
|
---@field languages table<string, CpLanguage>
|
||||||
---@field snippets? any[]
|
---@field platforms table<string, CpPlatform>
|
||||||
---@field hooks? Hooks
|
---@field hooks Hooks
|
||||||
---@field debug? boolean
|
---@field snippets any[]
|
||||||
---@field scrapers? string[]
|
---@field debug boolean
|
||||||
|
---@field scrapers string[]
|
||||||
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
---@field filename? fun(contest: string, contest_id: string, problem_id?: string, config: cp.Config, language?: string): string
|
||||||
---@field run_panel? RunPanelConfig
|
---@field ui CpUI
|
||||||
---@field diff? DiffConfig
|
---@field runtime { effective: table<string, table<string, CpLanguage>> } -- computed
|
||||||
---@field picker? string|nil
|
|
||||||
|
---@class cp.PartialConfig: cp.Config
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local constants = require('cp.constants')
|
local constants = require('cp.constants')
|
||||||
local utils = require('cp.utils')
|
local utils = require('cp.utils')
|
||||||
|
|
||||||
local default_contest_config = {
|
-- defaults per the new single schema
|
||||||
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',
|
|
||||||
}
|
|
||||||
|
|
||||||
---@type cp.Config
|
---@type cp.Config
|
||||||
M.defaults = {
|
M.defaults = {
|
||||||
contests = {
|
languages = {
|
||||||
codeforces = default_contest_config,
|
cpp = {
|
||||||
atcoder = default_contest_config,
|
extension = 'cc',
|
||||||
cses = default_contest_config,
|
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 = {},
|
snippets = {},
|
||||||
hooks = {
|
hooks = { before_run = nil, before_debug = nil, setup_code = nil },
|
||||||
before_run = nil,
|
|
||||||
before_debug = nil,
|
|
||||||
setup_code = nil,
|
|
||||||
},
|
|
||||||
debug = false,
|
debug = false,
|
||||||
scrapers = constants.PLATFORMS,
|
scrapers = constants.PLATFORMS,
|
||||||
filename = nil,
|
filename = nil,
|
||||||
run_panel = {
|
ui = {
|
||||||
ansi = true,
|
run_panel = { ansi = true, diff_mode = 'none', max_output_lines = 50 },
|
||||||
diff_mode = 'none',
|
|
||||||
next_test_key = '<c-n>',
|
|
||||||
prev_test_key = '<c-p>',
|
|
||||||
max_output_lines = 50,
|
|
||||||
},
|
|
||||||
diff = {
|
diff = {
|
||||||
git = {
|
git = {
|
||||||
args = { 'diff', '--no-index', '--word-diff=plain', '--word-diff-regex=.', '--no-prefix' },
|
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
|
---@param user_config cp.PartialConfig|nil
|
||||||
---@return cp.Config
|
---@return cp.Config
|
||||||
function M.setup(user_config)
|
function M.setup(user_config)
|
||||||
vim.validate({
|
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
|
||||||
user_config = { user_config, { 'table', 'nil' }, true },
|
local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), user_config or {})
|
||||||
})
|
|
||||||
|
|
||||||
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({
|
vim.validate({
|
||||||
before_run = {
|
hooks = { cfg.hooks, { 'table' } },
|
||||||
config.hooks.before_run,
|
ui = { cfg.ui, { 'table' } },
|
||||||
{ 'function', 'nil' },
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
before_debug = {
|
|
||||||
config.hooks.before_debug,
|
|
||||||
{ 'function', 'nil' },
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
setup_code = {
|
|
||||||
config.hooks.setup_code,
|
|
||||||
{ 'function', 'nil' },
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
vim.validate({
|
vim.validate({
|
||||||
ansi = {
|
before_run = { cfg.hooks.before_run, { 'function', 'nil' }, true },
|
||||||
config.run_panel.ansi,
|
before_debug = { cfg.hooks.before_debug, { 'function', 'nil' }, true },
|
||||||
'boolean',
|
setup_code = { cfg.hooks.setup_code, { 'function', 'nil' }, true },
|
||||||
'ansi color parsing must be enabled xor disabled',
|
})
|
||||||
},
|
|
||||||
|
vim.validate({
|
||||||
|
ansi = { cfg.ui.run_panel.ansi, 'boolean' },
|
||||||
diff_mode = {
|
diff_mode = {
|
||||||
config.run_panel.diff_mode,
|
cfg.ui.run_panel.diff_mode,
|
||||||
function(value)
|
function(v)
|
||||||
return vim.tbl_contains({ 'none', 'vim', 'git' }, value)
|
return vim.tbl_contains({ 'none', 'vim', 'git' }, v)
|
||||||
end,
|
end,
|
||||||
"diff_mode must be 'none', 'vim', or 'git'",
|
"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 = {
|
max_output_lines = {
|
||||||
config.run_panel.max_output_lines,
|
cfg.ui.run_panel.max_output_lines,
|
||||||
function(value)
|
function(v)
|
||||||
return type(value) == 'number' and value > 0 and value == math.floor(value)
|
return type(v) == 'number' and v > 0 and v == math.floor(v)
|
||||||
end,
|
end,
|
||||||
'max_output_lines must be a positive integer',
|
'positive integer',
|
||||||
},
|
},
|
||||||
|
git = { cfg.ui.diff.git, { 'table' } },
|
||||||
})
|
})
|
||||||
|
|
||||||
vim.validate({
|
for id, lang in pairs(cfg.languages) do
|
||||||
git = { config.diff.git, { 'table', 'nil' }, true },
|
validate_language(id, lang)
|
||||||
})
|
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
if not contest_config.default_language then
|
build_runtime(cfg)
|
||||||
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
|
|
||||||
end
|
|
||||||
|
|
||||||
local ok, err = utils.check_required_runtime()
|
local ok, err = utils.check_required_runtime()
|
||||||
if not ok then
|
if not ok then
|
||||||
error('[cp.nvim] ' .. err)
|
error('[cp.nvim] ' .. err)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
---@param contest_id string
|
---@param contest_id string
|
||||||
|
|
@ -265,25 +282,9 @@ end
|
||||||
local function default_filename(contest_id, problem_id)
|
local function default_filename(contest_id, problem_id)
|
||||||
if problem_id then
|
if problem_id then
|
||||||
return (contest_id .. problem_id):lower()
|
return (contest_id .. problem_id):lower()
|
||||||
else
|
end
|
||||||
return contest_id:lower()
|
return contest_id:lower()
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
M.default_filename = default_filename
|
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
|
return M
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ M.canonical_filetypes = {
|
||||||
[M.PYTHON] = 'python',
|
[M.PYTHON] = 'python',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
---@type table<string, string>
|
||||||
|
M.canonical_filetype_to_extension = {
|
||||||
|
[M.CPP] = 'cc',
|
||||||
|
[M.PYTHON] = 'py',
|
||||||
|
}
|
||||||
|
|
||||||
---@type table<number, string>
|
---@type table<number, string>
|
||||||
M.signal_codes = {
|
M.signal_codes = {
|
||||||
[128] = 'SIGILL',
|
[128] = 'SIGILL',
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,15 @@ local config_module = require('cp.config')
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
local snippets = require('cp.snippets')
|
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)
|
logger.log('Requires nvim-0.10.0+', vim.log.levels.ERROR)
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local user_config = {}
|
local user_config = {}
|
||||||
local config = config_module.setup(user_config)
|
local config = nil
|
||||||
local snippets_initialized = false
|
local snippets_initialized = false
|
||||||
|
local initialized = false
|
||||||
|
|
||||||
--- Root handler for all `:CP ...` commands
|
--- Root handler for all `:CP ...` commands
|
||||||
---@return nil
|
---@return nil
|
||||||
|
|
@ -30,10 +31,11 @@ function M.setup(opts)
|
||||||
snippets.setup(config)
|
snippets.setup(config)
|
||||||
snippets_initialized = true
|
snippets_initialized = true
|
||||||
end
|
end
|
||||||
|
initialized = true
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.is_initialized()
|
function M.is_initialized()
|
||||||
return true
|
return initialized
|
||||||
end
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
local cache = require('cp.cache')
|
local cache = require('cp.cache')
|
||||||
local config = require('cp.config').get_config()
|
|
||||||
local constants = require('cp.constants')
|
local constants = require('cp.constants')
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
local scraper = require('cp.scraper')
|
local scraper = require('cp.scraper')
|
||||||
|
|
@ -22,17 +21,16 @@ local scraper = require('cp.scraper')
|
||||||
|
|
||||||
---@return cp.PlatformItem[]
|
---@return cp.PlatformItem[]
|
||||||
function M.get_platforms()
|
function M.get_platforms()
|
||||||
|
local config = require('cp.config').get_config()
|
||||||
local result = {}
|
local result = {}
|
||||||
|
|
||||||
for _, platform in ipairs(constants.PLATFORMS) do
|
for _, platform in ipairs(constants.PLATFORMS) do
|
||||||
if config.contests[platform] then
|
if config.platforms[platform] then
|
||||||
table.insert(result, {
|
table.insert(result, {
|
||||||
id = platform,
|
id = platform,
|
||||||
display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform,
|
display_name = constants.PLATFORM_DISPLAY_NAMES[platform] or platform,
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return result
|
return result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,44 +4,26 @@ local cache = require('cp.cache')
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
local state = require('cp.state')
|
local state = require('cp.state')
|
||||||
|
|
||||||
|
---@return boolean
|
||||||
function M.restore_from_current_file()
|
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()
|
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)
|
local file_state = cache.get_file_state(current_file)
|
||||||
if not file_state then
|
if not file_state then
|
||||||
logger.log(
|
logger.log('No cached state found for current file.', vim.log.levels.ERROR)
|
||||||
'No cached state found for current file. Use :CP <platform> <contest> [--{lang=<lang>,debug}...] first.',
|
|
||||||
vim.log.levels.ERROR
|
|
||||||
)
|
|
||||||
return false
|
return false
|
||||||
end
|
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')
|
local setup = require('cp.setup')
|
||||||
if not setup.set_platform(file_state.platform) then
|
setup.set_platform(file_state.platform)
|
||||||
return false
|
|
||||||
end
|
|
||||||
|
|
||||||
state.set_contest_id(file_state.contest_id)
|
state.set_contest_id(file_state.contest_id)
|
||||||
state.set_problem_id(file_state.problem_id)
|
state.set_problem_id(file_state.problem_id)
|
||||||
|
|
||||||
setup.setup_contest(
|
setup.setup_contest(
|
||||||
file_state.platform,
|
file_state.platform,
|
||||||
file_state.contest_id,
|
file_state.contest_id,
|
||||||
file_state.language,
|
file_state.problem_id,
|
||||||
file_state.problem_id
|
file_state.language
|
||||||
)
|
)
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -7,44 +7,40 @@
|
||||||
---@field peak_mb number
|
---@field peak_mb number
|
||||||
---@field signal string|nil
|
---@field signal string|nil
|
||||||
|
|
||||||
|
---@class SubstitutableCommand
|
||||||
|
---@field source string substituted via '{source}'
|
||||||
|
---@field binary string substitued via '{binary}'
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
local constants = require('cp.constants')
|
local constants = require('cp.constants')
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
local utils = require('cp.utils')
|
local utils = require('cp.utils')
|
||||||
|
|
||||||
local filetype_to_language = constants.filetype_to_language
|
---@param cmd_template string[]
|
||||||
|
---@param substitutions SubstitutableCommand
|
||||||
local function get_language_from_file(source_file, contest_config)
|
---@return string[] string normalized with substitutions
|
||||||
local ext = vim.fn.fnamemodify(source_file, ':e')
|
|
||||||
return filetype_to_language[ext] or contest_config.default_language
|
|
||||||
end
|
|
||||||
|
|
||||||
local function substitute_template(cmd_template, substitutions)
|
local function substitute_template(cmd_template, substitutions)
|
||||||
local out = {}
|
local out = {}
|
||||||
for _, a in ipairs(cmd_template) do
|
for _, arg in ipairs(cmd_template) do
|
||||||
local s = a
|
if arg == '{source}' and substitutions.source then
|
||||||
for k, v in pairs(substitutions) do
|
table.insert(out, substitutions.source)
|
||||||
s = s:gsub('{' .. k .. '}', v)
|
elseif arg == '{binary}' and substitutions.binary then
|
||||||
|
table.insert(out, substitutions.binary)
|
||||||
|
else
|
||||||
|
table.insert(out, arg)
|
||||||
end
|
end
|
||||||
table.insert(out, s)
|
|
||||||
end
|
end
|
||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.build_command(cmd_template, executable, substitutions)
|
function M.build_command(cmd_template, substitutions)
|
||||||
local cmd = substitute_template(cmd_template, substitutions)
|
return substitute_template(cmd_template, substitutions)
|
||||||
if executable then
|
|
||||||
table.insert(cmd, 1, executable)
|
|
||||||
end
|
|
||||||
return cmd
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.compile(language_config, substitutions)
|
---@param compile_cmd string[]
|
||||||
if not language_config.compile then
|
---@param substitutions SubstitutableCommand
|
||||||
return { code = 0, stdout = '' }
|
function M.compile(compile_cmd, substitutions)
|
||||||
end
|
local cmd = substitute_template(compile_cmd, substitutions)
|
||||||
|
|
||||||
local cmd = substitute_template(language_config.compile, substitutions)
|
|
||||||
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
||||||
|
|
||||||
local t0 = vim.uv.hrtime()
|
local t0 = vim.uv.hrtime()
|
||||||
|
|
@ -164,32 +160,20 @@ function M.run(cmd, stdin, timeout_ms, memory_mb)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.compile_problem(contest_config, is_debug)
|
function M.compile_problem()
|
||||||
local state = require('cp.state')
|
local state = require('cp.state')
|
||||||
local source_file = state.get_source_file()
|
local config = require('cp.config').get_config()
|
||||||
if not source_file then
|
local platform = state.get_platform() or ''
|
||||||
return { success = false, output = 'No source file found.' }
|
local language = config.platforms[platform].default_language
|
||||||
end
|
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)
|
if not compile_config then
|
||||||
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
|
|
||||||
return { success = true, output = nil }
|
return { success = true, output = nil }
|
||||||
end
|
end
|
||||||
|
|
||||||
local saved = language_config.compile
|
local substitutions = { source = state.get_source_file(), binary = state.get_binary_file() }
|
||||||
language_config.compile = chosen
|
local r = M.compile(compile_config, substitutions)
|
||||||
local r = M.compile(language_config, substitutions)
|
|
||||||
language_config.compile = saved
|
|
||||||
|
|
||||||
if r.code ~= 0 then
|
if r.code ~= 0 then
|
||||||
return { success = false, output = r.stdout or 'unknown error' }
|
return { success = false, output = r.stdout or 'unknown error' }
|
||||||
|
|
|
||||||
|
|
@ -31,8 +31,11 @@
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
local cache = require('cp.cache')
|
local cache = require('cp.cache')
|
||||||
|
local config = require('cp.config').get_config()
|
||||||
local constants = require('cp.constants')
|
local constants = require('cp.constants')
|
||||||
|
local execute = require('cp.runner.execute')
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
|
local state = require('cp.state')
|
||||||
|
|
||||||
---@type RunPanelState
|
---@type RunPanelState
|
||||||
local run_panel_state = {
|
local run_panel_state = {
|
||||||
|
|
@ -90,42 +93,36 @@ local function create_sentinal_panel_data(test_cases)
|
||||||
return out
|
return out
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param language_config LanguageConfig
|
---@param cmd string[]
|
||||||
---@param substitutions table<string, string>
|
|
||||||
---@return string[]
|
---@return string[]
|
||||||
local function build_command(language_config, substitutions)
|
local function build_command(cmd, substitutions)
|
||||||
local execute = require('cp.runner.execute')
|
return execute.build_command(cmd, substitutions)
|
||||||
return execute.build_command(language_config.test, language_config.executable, substitutions)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param contest_config ContestConfig
|
|
||||||
---@param cp_config cp.Config
|
|
||||||
---@param test_case RanTestCase
|
---@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 }
|
---@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 function run_single_test_case(test_case)
|
||||||
local state = require('cp.state')
|
|
||||||
local exec = require('cp.runner.execute')
|
|
||||||
|
|
||||||
local source_file = state.get_source_file()
|
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 binary_file = state.get_binary_file()
|
||||||
local substitutions = { source = source_file, binary = 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 stdin_content = (test_case.input or '') .. '\n'
|
||||||
local timeout_ms = (run_panel_state.constraints and run_panel_state.constraints.timeout_ms) or 0
|
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 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 ansi = require('cp.ui.ansi')
|
||||||
local out = r.stdout or ''
|
local out = r.stdout or ''
|
||||||
local highlights = {}
|
local highlights = {}
|
||||||
if out ~= '' then
|
if out ~= '' then
|
||||||
if cp_config.run_panel.ansi then
|
if config.ui.run_panel.ansi then
|
||||||
local parsed = ansi.parse_ansi_text(out)
|
local parsed = ansi.parse_ansi_text(out)
|
||||||
out = table.concat(parsed.lines, '\n')
|
out = table.concat(parsed.lines, '\n')
|
||||||
highlights = parsed.highlights
|
highlights = parsed.highlights
|
||||||
|
|
@ -134,7 +131,7 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
||||||
end
|
end
|
||||||
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')
|
local lines = vim.split(out, '\n')
|
||||||
if #lines > max_lines then
|
if #lines > max_lines then
|
||||||
local trimmed = {}
|
local trimmed = {}
|
||||||
|
|
@ -180,9 +177,8 @@ local function run_single_test_case(contest_config, cp_config, test_case)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param state table
|
|
||||||
---@return boolean
|
---@return boolean
|
||||||
function M.load_test_cases(state)
|
function M.load_test_cases()
|
||||||
local tcs = cache.get_test_cases(
|
local tcs = cache.get_test_cases(
|
||||||
state.get_platform() or '',
|
state.get_platform() or '',
|
||||||
state.get_contest_id() or '',
|
state.get_contest_id() or '',
|
||||||
|
|
@ -201,18 +197,16 @@ function M.load_test_cases(state)
|
||||||
return #tcs > 0
|
return #tcs > 0
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param contest_config ContestConfig
|
|
||||||
---@param cp_config cp.Config
|
|
||||||
---@param index number
|
---@param index number
|
||||||
---@return boolean
|
---@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]
|
local tc = run_panel_state.test_cases[index]
|
||||||
if not tc then
|
if not tc then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
tc.status = 'running'
|
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.status = r.status
|
||||||
tc.actual = r.actual
|
tc.actual = r.actual
|
||||||
|
|
@ -230,13 +224,11 @@ function M.run_test_case(contest_config, cp_config, index)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param contest_config ContestConfig
|
|
||||||
---@param cp_config cp.Config
|
|
||||||
---@return RanTestCase[]
|
---@return RanTestCase[]
|
||||||
function M.run_all_test_cases(contest_config, cp_config)
|
function M.run_all_test_cases()
|
||||||
local results = {}
|
local results = {}
|
||||||
for i = 1, #run_panel_state.test_cases do
|
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]
|
results[i] = run_panel_state.test_cases[i]
|
||||||
end
|
end
|
||||||
return results
|
return results
|
||||||
|
|
@ -251,12 +243,11 @@ end
|
||||||
---@return nil
|
---@return nil
|
||||||
function M.handle_compilation_failure(output)
|
function M.handle_compilation_failure(output)
|
||||||
local ansi = require('cp.ui.ansi')
|
local ansi = require('cp.ui.ansi')
|
||||||
local config = require('cp.config').setup()
|
|
||||||
|
|
||||||
local txt
|
local txt
|
||||||
local hl = {}
|
local hl = {}
|
||||||
|
|
||||||
if config.run_panel.ansi then
|
if config.ui.run_panel.ansi then
|
||||||
local p = ansi.parse_ansi_text(output or '')
|
local p = ansi.parse_ansi_text(output or '')
|
||||||
txt = table.concat(p.lines, '\n')
|
txt = table.concat(p.lines, '\n')
|
||||||
hl = p.highlights
|
hl = p.highlights
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ local utils = require('cp.utils')
|
||||||
local function syshandle(result)
|
local function syshandle(result)
|
||||||
if result.code ~= 0 then
|
if result.code ~= 0 then
|
||||||
local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error')
|
local msg = 'Scraper failed: ' .. (result.stderr or 'Unknown error')
|
||||||
logger.log(msg, vim.log.levels.ERROR)
|
|
||||||
return { success = false, error = msg }
|
return { success = false, error = msg }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -114,7 +113,7 @@ function M.scrape_contest_metadata(platform, contest_id, callback)
|
||||||
on_exit = function(result)
|
on_exit = function(result)
|
||||||
if not result or not result.success then
|
if not result or not result.success then
|
||||||
logger.log(
|
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
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -122,7 +121,7 @@ function M.scrape_contest_metadata(platform, contest_id, callback)
|
||||||
local data = result.data or {}
|
local data = result.data or {}
|
||||||
if not data.problems or #data.problems == 0 then
|
if not data.problems or #data.problems == 0 then
|
||||||
logger.log(
|
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
|
vim.log.levels.ERROR
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
@ -161,7 +160,7 @@ function M.scrape_all_tests(platform, contest_id, callback)
|
||||||
end
|
end
|
||||||
if ev.error and ev.problem_id then
|
if ev.error and ev.problem_id then
|
||||||
logger.log(
|
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
|
vim.log.levels.WARN
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -17,14 +17,7 @@ function M.set_platform(platform)
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if state.get_platform() == platform then
|
|
||||||
logger.log(('platform already set to %s'):format(platform))
|
|
||||||
else
|
|
||||||
state.set_platform(platform)
|
state.set_platform(platform)
|
||||||
logger.log(('platform set to %s'):format(platform))
|
|
||||||
end
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -45,15 +38,9 @@ end
|
||||||
|
|
||||||
---@param platform string
|
---@param platform string
|
||||||
---@param contest_id string
|
---@param contest_id string
|
||||||
---@param language string|nil
|
|
||||||
---@param problem_id string|nil
|
---@param problem_id string|nil
|
||||||
function M.setup_contest(platform, contest_id, language, problem_id)
|
---@param language? string|nil
|
||||||
local config = config_module.get_config()
|
function M.setup_contest(platform, contest_id, problem_id, language)
|
||||||
if not vim.tbl_contains(config.scrapers, platform) then
|
|
||||||
logger.log(('Scraping disabled for %s.'):format(platform), vim.log.levels.WARN)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
|
|
||||||
state.set_contest_id(contest_id)
|
state.set_contest_id(contest_id)
|
||||||
cache.load()
|
cache.load()
|
||||||
|
|
||||||
|
|
@ -106,10 +93,7 @@ end
|
||||||
function M.setup_problem(problem_id, language)
|
function M.setup_problem(problem_id, language)
|
||||||
local platform = state.get_platform()
|
local platform = state.get_platform()
|
||||||
if not platform then
|
if not platform then
|
||||||
logger.log(
|
logger.log('No platform set.', vim.log.levels.ERROR)
|
||||||
'No platform set. run :CP <platform> <contest> [--{lang=<lang>,debug}]',
|
|
||||||
vim.log.levels.ERROR
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -120,25 +104,18 @@ function M.setup_problem(problem_id, language)
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
vim.cmd.only({ mods = { silent = true } })
|
vim.cmd.only({ mods = { silent = true } })
|
||||||
|
|
||||||
local source_file = state.get_source_file(language)
|
local lang = language or config.platforms[platform].default_language
|
||||||
if not source_file then
|
local source_file = state.get_source_file(lang)
|
||||||
return
|
|
||||||
end
|
|
||||||
vim.cmd.e(source_file)
|
vim.cmd.e(source_file)
|
||||||
local source_buf = vim.api.nvim_get_current_buf()
|
local source_buf = vim.api.nvim_get_current_buf()
|
||||||
|
|
||||||
if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == '' then
|
if vim.api.nvim_buf_get_lines(source_buf, 0, -1, true)[1] == '' then
|
||||||
local has_luasnip, luasnip = pcall(require, 'luasnip')
|
local ok, luasnip = pcall(require, 'luasnip')
|
||||||
if has_luasnip then
|
if ok then
|
||||||
local filetype = vim.api.nvim_get_option_value('filetype', { buf = source_buf })
|
local trigger = ('cp.nvim/%s.%s'):format(platform, lang)
|
||||||
local language_name = constants.filetype_to_language[filetype]
|
vim.api.nvim_buf_set_lines(0, 0, -1, false, { trigger })
|
||||||
local canonical_language = constants.canonical_filetypes[language_name] or language_name
|
vim.api.nvim_win_set_cursor(0, { 1, #trigger })
|
||||||
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 })
|
|
||||||
vim.cmd.startinsert({ bang = true })
|
vim.cmd.startinsert({ bang = true })
|
||||||
|
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
if luasnip.expandable() then
|
if luasnip.expandable() then
|
||||||
luasnip.expand()
|
luasnip.expand()
|
||||||
|
|
@ -159,13 +136,13 @@ function M.setup_problem(problem_id, language)
|
||||||
vim.fn.expand('%:p'),
|
vim.fn.expand('%:p'),
|
||||||
platform,
|
platform,
|
||||||
state.get_contest_id() or '',
|
state.get_contest_id() or '',
|
||||||
state.get_problem_id(),
|
state.get_problem_id() or '',
|
||||||
language
|
lang
|
||||||
)
|
)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.navigate_problem(direction, language)
|
function M.navigate_problem(direction)
|
||||||
if direction == 0 then
|
if direction == 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -176,10 +153,7 @@ function M.navigate_problem(direction, language)
|
||||||
local current_problem_id = state.get_problem_id()
|
local current_problem_id = state.get_problem_id()
|
||||||
|
|
||||||
if not platform or not contest_id or not current_problem_id then
|
if not platform or not contest_id or not current_problem_id then
|
||||||
logger.log(
|
logger.log('No platform configured.', vim.log.levels.ERROR)
|
||||||
'No platform configured. Use :CP <platform> <contest> [--{lang=<lang>,debug}] first.',
|
|
||||||
vim.log.levels.ERROR
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -198,14 +172,13 @@ function M.navigate_problem(direction, language)
|
||||||
|
|
||||||
local problems = contest_data.problems
|
local problems = contest_data.problems
|
||||||
local index = contest_data.index_map[current_problem_id]
|
local index = contest_data.index_map[current_problem_id]
|
||||||
|
|
||||||
local new_index = index + direction
|
local new_index = index + direction
|
||||||
if new_index < 1 or new_index > #problems then
|
if new_index < 1 or new_index > #problems then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
require('cp.ui.panel').disable()
|
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
|
end
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|
|
||||||
|
|
@ -72,18 +72,18 @@ function M.get_source_file(language)
|
||||||
end
|
end
|
||||||
|
|
||||||
local config = require('cp.config').get_config()
|
local config = require('cp.config').get_config()
|
||||||
local contest_config = config.contests[M.get_platform()]
|
local plat = M.get_platform()
|
||||||
if not contest_config then
|
local platform_cfg = config.platforms[plat]
|
||||||
|
if not platform_cfg then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
local target_language = language or platform_cfg.default_language
|
||||||
local target_language = language or contest_config.default_language
|
local eff = config.runtime.effective[plat] and config.runtime.effective[plat][target_language]
|
||||||
local language_config = contest_config[target_language]
|
or nil
|
||||||
if not language_config or not language_config.extension then
|
if not eff or not eff.extension then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
return base_name .. '.' .. eff.extension
|
||||||
return base_name .. '.' .. language_config.extension
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function M.get_binary_file()
|
function M.get_binary_file()
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,81 @@ local M = {}
|
||||||
|
|
||||||
local logger = require('cp.log')
|
local logger = require('cp.log')
|
||||||
|
|
||||||
---@param raw_output string|table
|
local dyn_hl_cache = {}
|
||||||
|
|
||||||
|
---@param s string|table
|
||||||
---@return string
|
---@return string
|
||||||
function M.bytes_to_string(raw_output)
|
function M.bytes_to_string(s)
|
||||||
if type(raw_output) == 'string' then
|
if type(s) == 'string' then
|
||||||
return raw_output
|
return s
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
---@param text string
|
---@param text string
|
||||||
|
|
@ -38,22 +106,7 @@ function M.parse_ansi_text(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
local function get_highlight_group()
|
local function get_highlight_group()
|
||||||
if not ansi_state.bold and not ansi_state.italic and not ansi_state.foreground then
|
return ensure_hl_for(ansi_state.foreground, ansi_state.bold, ansi_state.italic)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function apply_highlight(start_line, start_col, end_col)
|
local function apply_highlight(start_line, start_col, end_col)
|
||||||
|
|
@ -137,6 +190,7 @@ end
|
||||||
|
|
||||||
---@param ansi_state table
|
---@param ansi_state table
|
||||||
---@param code_string string
|
---@param code_string string
|
||||||
|
---@return nil
|
||||||
function M.update_ansi_state(ansi_state, code_string)
|
function M.update_ansi_state(ansi_state, code_string)
|
||||||
if code_string == '' or code_string == '0' then
|
if code_string == '' or code_string == '0' then
|
||||||
ansi_state.bold = false
|
ansi_state.bold = false
|
||||||
|
|
@ -146,10 +200,10 @@ function M.update_ansi_state(ansi_state, code_string)
|
||||||
end
|
end
|
||||||
|
|
||||||
local codes = vim.split(code_string, ';', { plain = true })
|
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
|
if num == 1 then
|
||||||
ansi_state.bold = true
|
ansi_state.bold = true
|
||||||
elseif num == 3 then
|
elseif num == 3 then
|
||||||
|
|
@ -158,10 +212,10 @@ function M.update_ansi_state(ansi_state, code_string)
|
||||||
ansi_state.bold = false
|
ansi_state.bold = false
|
||||||
elseif num == 23 then
|
elseif num == 23 then
|
||||||
ansi_state.italic = false
|
ansi_state.italic = false
|
||||||
elseif num >= 30 and num <= 37 then
|
elseif num and num >= 30 and num <= 37 then
|
||||||
local colors = { 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' }
|
local colors = { 'Black', 'Red', 'Green', 'Yellow', 'Blue', 'Magenta', 'Cyan', 'White' }
|
||||||
ansi_state.foreground = colors[num - 29]
|
ansi_state.foreground = { kind = 'named', name = colors[num - 29] }
|
||||||
elseif num >= 90 and num <= 97 then
|
elseif num and num >= 90 and num <= 97 then
|
||||||
local colors = {
|
local colors = {
|
||||||
'BrightBlack',
|
'BrightBlack',
|
||||||
'BrightRed',
|
'BrightRed',
|
||||||
|
|
@ -172,14 +226,34 @@ function M.update_ansi_state(ansi_state, code_string)
|
||||||
'BrightCyan',
|
'BrightCyan',
|
||||||
'BrightWhite',
|
'BrightWhite',
|
||||||
}
|
}
|
||||||
ansi_state.foreground = colors[num - 89]
|
ansi_state.foreground = { kind = 'named', name = colors[num - 89] }
|
||||||
elseif num == 39 then
|
elseif num == 39 then
|
||||||
ansi_state.foreground = nil
|
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
|
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
|
end
|
||||||
|
idx = idx + 4
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
idx = idx + 1
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
---@return nil
|
||||||
function M.setup_highlight_groups()
|
function M.setup_highlight_groups()
|
||||||
local color_map = {
|
local color_map = {
|
||||||
Black = vim.g.terminal_color_0,
|
Black = vim.g.terminal_color_0,
|
||||||
|
|
@ -218,7 +292,6 @@ function M.setup_highlight_groups()
|
||||||
for color_name, terminal_color in pairs(color_map) do
|
for color_name, terminal_color in pairs(color_map) do
|
||||||
local parts = { 'CpAnsi' }
|
local parts = { 'CpAnsi' }
|
||||||
local opts = { fg = terminal_color or 'NONE' }
|
local opts = { fg = terminal_color or 'NONE' }
|
||||||
|
|
||||||
if combo.bold then
|
if combo.bold then
|
||||||
table.insert(parts, 'Bold')
|
table.insert(parts, 'Bold')
|
||||||
opts.bold = true
|
opts.bold = true
|
||||||
|
|
@ -228,7 +301,6 @@ function M.setup_highlight_groups()
|
||||||
opts.italic = true
|
opts.italic = true
|
||||||
end
|
end
|
||||||
table.insert(parts, color_name)
|
table.insert(parts, color_name)
|
||||||
|
|
||||||
local hl_name = table.concat(parts)
|
local hl_name = table.concat(parts)
|
||||||
vim.api.nvim_set_hl(0, hl_name, opts)
|
vim.api.nvim_set_hl(0, hl_name, opts)
|
||||||
end
|
end
|
||||||
|
|
@ -239,4 +311,30 @@ function M.setup_highlight_groups()
|
||||||
vim.api.nvim_set_hl(0, 'CpAnsiBoldItalic', { bold = true, italic = true })
|
vim.api.nvim_set_hl(0, 'CpAnsiBoldItalic', { bold = true, italic = true })
|
||||||
end
|
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
|
return M
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@ function M.update_diff_panes(
|
||||||
actual_content = actual_content
|
actual_content = actual_content
|
||||||
end
|
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 highlight = require('cp.ui.highlight')
|
||||||
local diff_namespace = highlight.create_namespace()
|
local diff_namespace = highlight.create_namespace()
|
||||||
local ansi_namespace = vim.api.nvim_create_namespace('cp_ansi_highlights')
|
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(('mksession! %s'):format(state.saved_interactive_session))
|
||||||
vim.cmd('silent only')
|
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 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
|
if not compile_result.success then
|
||||||
require('cp.runner.run').handle_compilation_failure(compile_result.output)
|
require('cp.runner.run').handle_compilation_failure(compile_result.output)
|
||||||
return
|
return
|
||||||
|
|
@ -120,7 +118,8 @@ function M.toggle_interactive()
|
||||||
state.set_active_panel('interactive')
|
state.set_active_panel('interactive')
|
||||||
end
|
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 state.get_active_panel() == 'run' then
|
||||||
if current_diff_layout then
|
if current_diff_layout then
|
||||||
current_diff_layout.cleanup()
|
current_diff_layout.cleanup()
|
||||||
|
|
@ -191,7 +190,7 @@ 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 debug and config.hooks and config.hooks.before_debug then
|
||||||
config.hooks.before_debug(state)
|
config.hooks.before_debug(state)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -199,7 +198,7 @@ function M.toggle_run_panel(is_debug)
|
||||||
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'))
|
||||||
|
|
||||||
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)
|
logger.log('no test cases found', vim.log.levels.WARN)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -264,37 +263,29 @@ function M.toggle_run_panel(is_debug)
|
||||||
local modes = { 'none', 'git', 'vim' }
|
local modes = { 'none', 'git', 'vim' }
|
||||||
local current_idx = nil
|
local current_idx = nil
|
||||||
for i, mode in ipairs(modes) do
|
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
|
current_idx = i
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
current_idx = current_idx or 1
|
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()
|
refresh_run_panel()
|
||||||
end, { buffer = buf, silent = true })
|
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)
|
navigate_test_case(1)
|
||||||
end, { buffer = buf, silent = true })
|
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)
|
navigate_test_case(-1)
|
||||||
end, { buffer = buf, silent = true })
|
end, { buffer = buf, silent = true })
|
||||||
end
|
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)
|
setup_keybindings_for_buffer(test_buffers.tab_buf)
|
||||||
|
|
||||||
local execute = require('cp.runner.execute')
|
local execute = require('cp.runner.execute')
|
||||||
local contest_config = config.contests[state.get_platform() or '']
|
local compile_result = execute.compile_problem()
|
||||||
local compile_result = execute.compile_problem(contest_config, is_debug)
|
|
||||||
if compile_result.success then
|
if compile_result.success then
|
||||||
run.run_all_test_cases(contest_config, config)
|
run.run_all_test_cases()
|
||||||
else
|
else
|
||||||
run.handle_compilation_failure(compile_result.output)
|
run.handle_compilation_failure(compile_result.output)
|
||||||
end
|
end
|
||||||
|
|
@ -302,7 +293,7 @@ function M.toggle_run_panel(is_debug)
|
||||||
refresh_run_panel()
|
refresh_run_panel()
|
||||||
|
|
||||||
vim.schedule(function()
|
vim.schedule(function()
|
||||||
if config.run_panel.ansi then
|
if config.ui.run_panel.ansi then
|
||||||
local ansi = require('cp.ui.ansi')
|
local ansi = require('cp.ui.ansi')
|
||||||
ansi.setup_highlight_groups()
|
ansi.setup_highlight_groups()
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -265,14 +265,26 @@ class AtcoderScraper(BaseScraper):
|
||||||
|
|
||||||
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
async def scrape_contest_metadata(self, contest_id: str) -> MetadataResult:
|
||||||
async def impl(cid: str) -> MetadataResult:
|
async def impl(cid: str) -> MetadataResult:
|
||||||
|
try:
|
||||||
rows = await asyncio.to_thread(_scrape_tasks_sync, cid)
|
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)
|
problems = _to_problem_summaries(rows)
|
||||||
if not problems:
|
if not problems:
|
||||||
return self._create_metadata_error(
|
return self._create_metadata_error(
|
||||||
f"No problems found for contest {cid}", cid
|
f"No problems found for contest {cid}", cid
|
||||||
)
|
)
|
||||||
|
|
||||||
return MetadataResult(
|
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)
|
return await self._safe_execute("metadata", impl, contest_id)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue