mirror of
https://github.com/harivansh-afk/cp.nvim.git
synced 2026-04-17 19:03:54 +00:00
Compare commits
23 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff5ba39a59 | ||
|
|
760e7d7731 | ||
|
|
49e4233b3f | ||
|
|
622620f6d0 | ||
|
|
976838d981 | ||
|
|
06f72bbe2b | ||
|
|
6045042dfb | ||
|
|
c192afc5d7 | ||
|
|
b6f3398bbc | ||
|
|
e02a29bd40 | ||
|
|
0f9715298e | ||
|
|
2148d9bd07 | ||
|
|
1162e7046b | ||
|
|
b36ffba63a | ||
|
|
04d0c124cf | ||
|
|
da433068ef | ||
|
|
51504b0121 | ||
|
|
49df7e015d | ||
|
|
029ea125b9 | ||
|
|
43193c3762 | ||
|
|
de2bc07532 | ||
|
|
041e09ac04 | ||
|
|
d23b4e59d1 |
24 changed files with 632 additions and 1697 deletions
3
.envrc
3
.envrc
|
|
@ -1,3 +0,0 @@
|
|||
VIRTUAL_ENV="$PWD/.venv"
|
||||
PATH_add "$VIRTUAL_ENV/bin"
|
||||
export VIRTUAL_ENV
|
||||
112
.github/workflows/ci.yaml
vendored
112
.github/workflows/ci.yaml
vendored
|
|
@ -1,112 +0,0 @@
|
|||
name: ci
|
||||
on:
|
||||
workflow_call:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
lua: ${{ steps.changes.outputs.lua }}
|
||||
python: ${{ steps.changes.outputs.python }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dorny/paths-filter@v3
|
||||
id: changes
|
||||
with:
|
||||
filters: |
|
||||
lua:
|
||||
- 'lua/**'
|
||||
- 'spec/**'
|
||||
- 'plugin/**'
|
||||
- 'after/**'
|
||||
- 'ftdetect/**'
|
||||
- '*.lua'
|
||||
- '.luarc.json'
|
||||
- 'stylua.toml'
|
||||
- 'selene.toml'
|
||||
python:
|
||||
- 'scripts/**'
|
||||
- 'scrapers/**'
|
||||
- 'tests/**'
|
||||
- 'pyproject.toml'
|
||||
- 'uv.lock'
|
||||
|
||||
lua-format:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: JohnnyMorganz/stylua-action@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: 2.1.0
|
||||
args: --check .
|
||||
|
||||
lua-lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: NTBBloodbath/selene-action@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --display-style quiet .
|
||||
|
||||
lua-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mrcjkb/lua-typecheck-action@v0
|
||||
with:
|
||||
checklevel: Warning
|
||||
directories: lua
|
||||
configpath: .luarc.json
|
||||
|
||||
python-format:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
- run: uv tool install ruff
|
||||
- run: ruff format --check .
|
||||
|
||||
python-lint:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
- run: uv tool install ruff
|
||||
- run: ruff check .
|
||||
|
||||
python-typecheck:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
- run: uv sync --dev
|
||||
- run: uvx ty check .
|
||||
|
||||
python-test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.python == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/setup-uv@v4
|
||||
- run: uv sync --dev
|
||||
- run: uv run camoufox fetch
|
||||
- run: uv run pytest tests/ -v
|
||||
4
.github/workflows/test.yaml
vendored
4
.github/workflows/test.yaml
vendored
|
|
@ -44,9 +44,7 @@ jobs:
|
|||
- uses: actions/checkout@v4
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
- name: Install dependencies with pytest
|
||||
- name: Install dependencies
|
||||
run: uv sync --dev
|
||||
- name: Fetch camoufox data
|
||||
run: uv run camoufox fetch
|
||||
- name: Run Python tests
|
||||
run: uv run pytest tests/ -v
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -14,3 +14,6 @@ __pycache__
|
|||
.claude/
|
||||
|
||||
node_modules/
|
||||
|
||||
.envrc
|
||||
.direnv/
|
||||
|
|
|
|||
|
|
@ -28,11 +28,12 @@ Install using your package manager of choice or via
|
|||
luarocks install cp.nvim
|
||||
```
|
||||
|
||||
## Optional Dependencies
|
||||
## Dependencies
|
||||
|
||||
- [uv](https://docs.astral.sh/uv/) for problem scraping
|
||||
- GNU [time](https://www.gnu.org/software/time/) and
|
||||
[timeout](https://www.gnu.org/software/coreutils/manual/html_node/timeout-invocation.html)
|
||||
- [uv](https://docs.astral.sh/uv/) or [nix](https://nixos.org/) for problem
|
||||
scraping
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
|
|
|||
407
doc/cp.nvim.txt
407
doc/cp.nvim.txt
|
|
@ -19,188 +19,13 @@ REQUIREMENTS *cp-requirements*
|
|||
- uv package manager (https://docs.astral.sh/uv/)
|
||||
|
||||
==============================================================================
|
||||
COMMANDS *cp-commands*
|
||||
SETUP *cp-setup*
|
||||
|
||||
:CP *:CP*
|
||||
cp.nvim uses a single :CP command with intelligent argument parsing:
|
||||
|
||||
Setup Commands ~
|
||||
:CP {platform} {contest_id} [--lang {language}]
|
||||
Full setup: set platform and load contest metadata.
|
||||
Scrapes test cases and creates source file.
|
||||
--lang: Use specific language (default: platform default)
|
||||
Examples: >
|
||||
:CP codeforces 1933
|
||||
:CP codeforces 1933 --lang python
|
||||
<
|
||||
View Commands ~
|
||||
:CP run [all|n|n,m,...] [--debug]
|
||||
Run tests in I/O view (see |cp-io-view|).
|
||||
Lightweight split showing test verdicts.
|
||||
|
||||
Execution modes:
|
||||
• :CP run Combined: single execution with all tests
|
||||
(auto-switches to individual when multiple samples)
|
||||
• :CP run all Individual: N separate executions
|
||||
• :CP run n Individual: run test n only
|
||||
• :CP run n,m,... Individual: run specific tests (e.g. nth and mth)
|
||||
|
||||
--debug: Use debug build (builds to build/<name>.dbg)
|
||||
|
||||
Combined mode runs all test inputs in one execution (matching
|
||||
platform behavior for multi-test problems). When a problem has
|
||||
multiple independent sample test cases, :CP run auto-switches to
|
||||
individual mode to run each sample separately.
|
||||
|
||||
Examples: >
|
||||
:CP run " Combined: all tests, one execution
|
||||
:CP run all " Individual: all tests, N executions
|
||||
:CP run 2 " Individual: test 2 only
|
||||
:CP run 1,3,5 " Individual: tests 1, 3, and 5
|
||||
:CP run all --debug " Individual with debug build
|
||||
<
|
||||
:CP panel [--debug] [n]
|
||||
Open full-screen test panel (see |cp-panel|).
|
||||
Aggregate table with diff modes for detailed analysis.
|
||||
Optional [n] focuses on specific test.
|
||||
--debug: Use debug build (with sanitizers, etc.)
|
||||
Examples: >
|
||||
:CP panel " All tests
|
||||
:CP panel --debug 3 " Test 3, debug build
|
||||
<
|
||||
|
||||
:CP pick [--lang {language}]
|
||||
Launch configured picker for interactive
|
||||
platform/contest selection.
|
||||
--lang: Pre-select language for chosen contest.
|
||||
Example: >
|
||||
:CP pick
|
||||
:CP pick --lang python
|
||||
<
|
||||
|
||||
:CP interact [script]
|
||||
Open an interactive terminal for the current problem.
|
||||
If an executable interactor is provided, runs the compiled
|
||||
binary against the source file (see
|
||||
*cp-interact*). Otherwise, runs the source
|
||||
file. Only valid for interactive problems.
|
||||
|
||||
Navigation Commands ~
|
||||
:CP next [--lang {language}]
|
||||
Navigate to next problem in current contest.
|
||||
Stops at last problem (no wrapping).
|
||||
--lang: Use specific language for next problem.
|
||||
By default, preserves current file's language if
|
||||
enabled for the new problem, otherwise uses platform
|
||||
default.
|
||||
Examples: >
|
||||
:CP next
|
||||
:CP next --lang python
|
||||
<
|
||||
:CP prev [--lang {language}]
|
||||
Navigate to previous problem in current contest.
|
||||
Stops at first problem (no wrapping).
|
||||
--lang: Use specific language for previous problem.
|
||||
By default, preserves current file's language if
|
||||
enabled for the new problem, otherwise uses platform
|
||||
default.
|
||||
Examples: >
|
||||
:CP prev
|
||||
:CP prev --lang cpp
|
||||
<
|
||||
:CP {problem_id} [--lang {language}]
|
||||
Jump to problem {problem_id} in a contest.
|
||||
Requires that a contest has already been set up.
|
||||
--lang: Use specific language for this problem.
|
||||
Examples: >
|
||||
:CP B
|
||||
:CP C --lang python
|
||||
<
|
||||
|
||||
Edit Commands ~
|
||||
:CP edit [n]
|
||||
Open grid test editor showing all test cases.
|
||||
Tests displayed as 2×N grid (2 rows, N columns):
|
||||
• Top row: Test inputs (editable)
|
||||
• Bottom row: Expected outputs (editable)
|
||||
|
||||
Optional [n]: Jump cursor to test n's input buffer
|
||||
|
||||
Changes saved to both cache and disk on exit,
|
||||
taking effect immediately in :CP run and CLI.
|
||||
|
||||
Keybindings (configurable via |EditConfig|):
|
||||
q Save all and exit editor
|
||||
]t Jump to next test column
|
||||
[t Jump to previous test column
|
||||
gd Delete current test column
|
||||
ga Add new test column at end
|
||||
<c-w> Normal window navigation
|
||||
|
||||
Examples: >
|
||||
:CP edit " Edit all tests
|
||||
:CP edit 3 " Edit all, start at test 3
|
||||
<
|
||||
|
||||
State Restoration ~
|
||||
:CP Restore state from current file.
|
||||
Automatically detects platform, contest, problem,
|
||||
and language from cached state. Use this after
|
||||
switching files to restore your CP environment.
|
||||
|
||||
Cache Commands ~
|
||||
:CP cache clear [platform] [contest]
|
||||
Clear cache data at different granularities:
|
||||
• No args: Clear all cached data
|
||||
• [platform]: Clear all data for a platform
|
||||
• [platform] [contest]: Clear specific contest
|
||||
Examples: >
|
||||
:CP cache clear
|
||||
:CP cache clear codeforces
|
||||
:CP cache clear codeforces 1848
|
||||
<
|
||||
:CP cache read
|
||||
View the cache in a pretty-printed lua buffer.
|
||||
Exit with q.
|
||||
|
||||
Template Variables ~
|
||||
*cp-template-vars*
|
||||
Command templates support variable substitution using {variable} syntax:
|
||||
|
||||
• {source} Source file path (e.g. "abc324a.cpp")
|
||||
• {binary} Output binary path (e.g. "build/abc324a.run" or
|
||||
"build/abc324a.dbg" for debug builds)
|
||||
|
||||
Example template: >
|
||||
build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
|
||||
< Would expand to: >
|
||||
g++ abc324a.cpp -o build/abc324a.run -std=c++17
|
||||
<
|
||||
Debug Builds ~
|
||||
*cp-debug-builds*
|
||||
The --debug flag uses the debug command configuration instead of build:
|
||||
|
||||
• Normal build: commands.build → outputs to build/<name>.run
|
||||
• Debug build: commands.debug → outputs to build/<name>.dbg
|
||||
|
||||
Debug builds typically include sanitizers (address, undefined behavior) to
|
||||
catch memory errors, buffer overflows, and other issues. Both binaries
|
||||
coexist, so you can switch between normal and debug mode without
|
||||
recompiling.
|
||||
|
||||
Example debug configuration: >
|
||||
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}' },
|
||||
}
|
||||
}
|
||||
}
|
||||
Load cp.nvim with your package manager. For example, with lazy.nvim: >lua
|
||||
{ 'barrettruth/cp.nvim' }
|
||||
<
|
||||
The plugin works automatically with no configuration required. For
|
||||
customization, see |cp-config|.
|
||||
|
||||
==============================================================================
|
||||
CONFIGURATION *cp-config*
|
||||
|
|
@ -425,12 +250,232 @@ run CSES problems with Rust using the single schema:
|
|||
print("Source file: " .. state.get_source_file())
|
||||
end,
|
||||
setup_io_input = function(bufnr, state)
|
||||
-- Custom setup for input buffer
|
||||
vim.api.nvim_set_option_value('number', false, { buf = bufnr })
|
||||
end
|
||||
}
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
COMMANDS *cp-commands*
|
||||
|
||||
:CP *:CP*
|
||||
cp.nvim uses a single :CP command with intelligent argument parsing:
|
||||
|
||||
Setup Commands ~
|
||||
:CP {platform} {contest_id} [--lang {language}]
|
||||
Full setup: set platform and load contest metadata.
|
||||
Scrapes test cases and creates source file.
|
||||
--lang: Use specific language (default: platform default)
|
||||
Examples: >
|
||||
:CP codeforces 1933
|
||||
:CP codeforces 1933 --lang python
|
||||
<
|
||||
View Commands ~
|
||||
:CP run [all|n|n,m,...] [--debug]
|
||||
Run tests in I/O view (see |cp-io-view|).
|
||||
Lightweight split showing test verdicts.
|
||||
|
||||
Execution modes:
|
||||
• :CP run Combined: single execution with all tests
|
||||
(auto-switches to individual when multiple samples)
|
||||
• :CP run all Individual: N separate executions
|
||||
• :CP run n Individual: run test n only
|
||||
• :CP run n,m,... Individual: run specific tests (e.g. nth and mth)
|
||||
|
||||
--debug: Use debug build (builds to build/<name>.dbg)
|
||||
|
||||
Combined mode runs all test inputs in one execution (matching
|
||||
platform behavior for multi-test problems). When a problem has
|
||||
multiple independent sample test cases, :CP run auto-switches to
|
||||
individual mode to run each sample separately.
|
||||
|
||||
Examples: >
|
||||
:CP run " Combined: all tests, one execution
|
||||
:CP run all " Individual: all tests, N executions
|
||||
:CP run 2 " Individual: test 2 only
|
||||
:CP run 1,3,5 " Individual: tests 1, 3, and 5
|
||||
:CP run all --debug " Individual with debug build
|
||||
<
|
||||
:CP panel [--debug] [n]
|
||||
Open full-screen test panel (see |cp-panel|).
|
||||
Aggregate table with diff modes for detailed analysis.
|
||||
Optional [n] focuses on specific test.
|
||||
--debug: Use debug build (with sanitizers, etc.)
|
||||
Examples: >
|
||||
:CP panel " All tests
|
||||
:CP panel --debug 3 " Test 3, debug build
|
||||
<
|
||||
|
||||
:CP pick [--lang {language}]
|
||||
Launch configured picker for interactive
|
||||
platform/contest selection.
|
||||
--lang: Pre-select language for chosen contest.
|
||||
Example: >
|
||||
:CP pick
|
||||
:CP pick --lang python
|
||||
<
|
||||
|
||||
:CP interact [script]
|
||||
Open an interactive terminal for the current problem.
|
||||
If an executable interactor is provided, runs the compiled
|
||||
binary against the source file (see
|
||||
*cp-interact*). Otherwise, runs the source
|
||||
file. Only valid for interactive problems.
|
||||
|
||||
Navigation Commands ~
|
||||
:CP next [--lang {language}]
|
||||
Navigate to next problem in current contest.
|
||||
Stops at last problem (no wrapping).
|
||||
--lang: Use specific language for next problem.
|
||||
By default, preserves current file's language if
|
||||
enabled for the new problem, otherwise uses platform
|
||||
default.
|
||||
Examples: >
|
||||
:CP next
|
||||
:CP next --lang python
|
||||
<
|
||||
:CP prev [--lang {language}]
|
||||
Navigate to previous problem in current contest.
|
||||
Stops at first problem (no wrapping).
|
||||
--lang: Use specific language for previous problem.
|
||||
By default, preserves current file's language if
|
||||
enabled for the new problem, otherwise uses platform
|
||||
default.
|
||||
Examples: >
|
||||
:CP prev
|
||||
:CP prev --lang cpp
|
||||
<
|
||||
:CP {problem_id} [--lang {language}]
|
||||
Jump to problem {problem_id} in a contest.
|
||||
Requires that a contest has already been set up.
|
||||
--lang: Use specific language for this problem.
|
||||
Examples: >
|
||||
:CP B
|
||||
:CP C --lang python
|
||||
<
|
||||
|
||||
Edit Commands ~
|
||||
:CP edit [n]
|
||||
Open grid test editor showing all test cases.
|
||||
Tests displayed as 2×N grid (2 rows, N columns):
|
||||
• Top row: Test inputs (editable)
|
||||
• Bottom row: Expected outputs (editable)
|
||||
|
||||
Optional [n]: Jump cursor to test n's input buffer
|
||||
|
||||
Changes saved to both cache and disk on exit,
|
||||
taking effect immediately in :CP run and CLI.
|
||||
|
||||
Keybindings (configurable via |EditConfig|):
|
||||
q Save all and exit editor
|
||||
]t Jump to next test column
|
||||
[t Jump to previous test column
|
||||
gd Delete current test column
|
||||
ga Add new test column at end
|
||||
<c-w> Normal window navigation
|
||||
|
||||
Examples: >
|
||||
:CP edit " Edit all tests
|
||||
:CP edit 3 " Edit all, start at test 3
|
||||
<
|
||||
|
||||
State Restoration ~
|
||||
:CP Restore state from current file.
|
||||
Automatically detects platform, contest, problem,
|
||||
and language from cached state. Use this after
|
||||
switching files to restore your CP environment.
|
||||
|
||||
Cache Commands ~
|
||||
:CP cache clear [platform] [contest]
|
||||
Clear cache data at different granularities:
|
||||
• No args: Clear all cached data
|
||||
• [platform]: Clear all data for a platform
|
||||
• [platform] [contest]: Clear specific contest
|
||||
Examples: >
|
||||
:CP cache clear
|
||||
:CP cache clear codeforces
|
||||
:CP cache clear codeforces 1848
|
||||
<
|
||||
:CP cache read
|
||||
View the cache in a pretty-printed lua buffer.
|
||||
Exit with q.
|
||||
|
||||
Template Variables ~
|
||||
*cp-template-vars*
|
||||
Command templates support variable substitution using {variable} syntax:
|
||||
|
||||
• {source} Source file path (e.g. "abc324a.cpp")
|
||||
• {binary} Output binary path (e.g. "build/abc324a.run" or
|
||||
"build/abc324a.dbg" for debug builds)
|
||||
|
||||
Example template: >
|
||||
build = { 'g++', '{source}', '-o', '{binary}', '-std=c++17' }
|
||||
< Would expand to: >
|
||||
g++ abc324a.cpp -o build/abc324a.run -std=c++17
|
||||
<
|
||||
Debug Builds ~
|
||||
*cp-debug-builds*
|
||||
The --debug flag uses the debug command configuration instead of build:
|
||||
|
||||
• Normal build: commands.build → outputs to build/<name>.run
|
||||
• Debug build: commands.debug → outputs to build/<name>.dbg
|
||||
|
||||
Debug builds typically include sanitizers (address, undefined behavior) to
|
||||
catch memory errors, buffer overflows, and other issues. Both binaries
|
||||
coexist, so you can switch between normal and debug mode without
|
||||
recompiling.
|
||||
|
||||
Example debug configuration: >
|
||||
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}' },
|
||||
}
|
||||
}
|
||||
}
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
MAPPINGS *cp-mappings*
|
||||
|
||||
cp.nvim provides <Plug> mappings for all primary actions. These dispatch
|
||||
through the same code path as |:CP|.
|
||||
|
||||
*<Plug>(cp-run)*
|
||||
<Plug>(cp-run) Run tests in I/O view. Equivalent to :CP run.
|
||||
|
||||
*<Plug>(cp-panel)*
|
||||
<Plug>(cp-panel) Open full-screen test panel. Equivalent to :CP panel.
|
||||
|
||||
*<Plug>(cp-edit)*
|
||||
<Plug>(cp-edit) Open the test case editor. Equivalent to :CP edit.
|
||||
|
||||
*<Plug>(cp-next)*
|
||||
<Plug>(cp-next) Navigate to the next problem. Equivalent to :CP next.
|
||||
|
||||
*<Plug>(cp-prev)*
|
||||
<Plug>(cp-prev) Navigate to the previous problem. Equivalent to :CP prev.
|
||||
|
||||
*<Plug>(cp-pick)*
|
||||
<Plug>(cp-pick) Launch the contest picker. Equivalent to :CP pick.
|
||||
|
||||
*<Plug>(cp-interact)*
|
||||
<Plug>(cp-interact) Open interactive mode. Equivalent to :CP interact.
|
||||
|
||||
Example configuration: >lua
|
||||
vim.keymap.set('n', '<leader>cr', '<Plug>(cp-run)')
|
||||
vim.keymap.set('n', '<leader>cp', '<Plug>(cp-panel)')
|
||||
vim.keymap.set('n', '<leader>ce', '<Plug>(cp-edit)')
|
||||
vim.keymap.set('n', '<leader>cn', '<Plug>(cp-next)')
|
||||
vim.keymap.set('n', '<leader>cN', '<Plug>(cp-prev)')
|
||||
vim.keymap.set('n', '<leader>cc', '<Plug>(cp-pick)')
|
||||
vim.keymap.set('n', '<leader>ci', '<Plug>(cp-interact)')
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
LANGUAGE SELECTION *cp-lang-selection*
|
||||
|
||||
|
|
|
|||
43
flake.lock
generated
Normal file
43
flake.lock
generated
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1771008912,
|
||||
"narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a82ccc39b39b621151d6732718e3e250109076fa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs",
|
||||
"systems": "systems"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1689347949,
|
||||
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default-linux",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
72
flake.nix
Normal file
72
flake.nix
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
systems.url = "github:nix-systems/default-linux";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
systems,
|
||||
}:
|
||||
let
|
||||
eachSystem = nixpkgs.lib.genAttrs (import systems);
|
||||
pkgsFor = system: nixpkgs.legacyPackages.${system};
|
||||
|
||||
mkPythonEnv =
|
||||
pkgs:
|
||||
pkgs.python312.withPackages (ps: [
|
||||
ps.backoff
|
||||
ps.beautifulsoup4
|
||||
ps.curl-cffi
|
||||
ps.httpx
|
||||
ps.ndjson
|
||||
ps.pydantic
|
||||
ps.requests
|
||||
]);
|
||||
|
||||
mkPlugin =
|
||||
pkgs:
|
||||
let
|
||||
pythonEnv = mkPythonEnv pkgs;
|
||||
in
|
||||
pkgs.vimUtils.buildVimPlugin {
|
||||
pname = "cp-nvim";
|
||||
version = "0-unstable-${self.shortRev or self.dirtyShortRev or "dev"}";
|
||||
src = self;
|
||||
postPatch = ''
|
||||
substituteInPlace lua/cp/utils.lua \
|
||||
--replace-fail "local _nix_python = nil" \
|
||||
"local _nix_python = '${pythonEnv.interpreter}'"
|
||||
'';
|
||||
nvimSkipModule = [
|
||||
"cp.pickers.telescope"
|
||||
"cp.version"
|
||||
];
|
||||
passthru = { inherit pythonEnv; };
|
||||
meta.description = "Competitive programming plugin for Neovim";
|
||||
};
|
||||
in
|
||||
{
|
||||
overlays.default = final: prev: {
|
||||
vimPlugins = prev.vimPlugins // {
|
||||
cp-nvim = mkPlugin final;
|
||||
};
|
||||
};
|
||||
|
||||
packages = eachSystem (system: {
|
||||
default = mkPlugin (pkgsFor system);
|
||||
pythonEnv = mkPythonEnv (pkgsFor system);
|
||||
});
|
||||
|
||||
devShells = eachSystem (system: {
|
||||
default = (pkgsFor system).mkShell {
|
||||
packages = with (pkgsFor system); [
|
||||
uv
|
||||
python312
|
||||
];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -292,7 +292,15 @@ end
|
|||
---@return cp.Config
|
||||
function M.setup(user_config)
|
||||
vim.validate({ user_config = { user_config, { 'table', 'nil' }, true } })
|
||||
local cfg = vim.tbl_deep_extend('force', vim.deepcopy(M.defaults), user_config or {})
|
||||
local defaults = vim.deepcopy(M.defaults)
|
||||
if user_config and user_config.platforms then
|
||||
for plat in pairs(defaults.platforms) do
|
||||
if not user_config.platforms[plat] then
|
||||
defaults.platforms[plat] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local cfg = vim.tbl_deep_extend('force', defaults, user_config or {})
|
||||
|
||||
if not next(cfg.languages) then
|
||||
error('[cp.nvim] At least one language must be configured')
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ local utils = require('cp.utils')
|
|||
local function check()
|
||||
vim.health.start('cp.nvim [required] ~')
|
||||
|
||||
utils.setup_python_env()
|
||||
|
||||
if vim.fn.has('nvim-0.10.0') == 1 then
|
||||
vim.health.ok('Neovim 0.10.0+ detected')
|
||||
else
|
||||
|
|
@ -16,22 +18,37 @@ local function check()
|
|||
vim.health.error('Windows is not supported')
|
||||
end
|
||||
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
vim.health.ok('uv executable found')
|
||||
local r = vim.system({ 'uv', '--version' }, { text = true }):wait()
|
||||
if utils.is_nix_build() then
|
||||
local source = utils.is_nix_discovered() and 'runtime discovery' or 'flake install'
|
||||
vim.health.ok('Nix Python environment detected (' .. source .. ')')
|
||||
local py = utils.get_nix_python()
|
||||
vim.health.info('Python: ' .. py)
|
||||
local r = vim.system({ py, '--version' }, { text = true }):wait()
|
||||
if r.code == 0 then
|
||||
vim.health.info('uv version: ' .. r.stdout:gsub('\n', ''))
|
||||
vim.health.info('Python version: ' .. r.stdout:gsub('\n', ''))
|
||||
end
|
||||
else
|
||||
vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)')
|
||||
end
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
vim.health.ok('uv executable found')
|
||||
local r = vim.system({ 'uv', '--version' }, { text = true }):wait()
|
||||
if r.code == 0 then
|
||||
vim.health.info('uv version: ' .. r.stdout:gsub('\n', ''))
|
||||
end
|
||||
else
|
||||
vim.health.warn('uv not found (install https://docs.astral.sh/uv/ for scraping)')
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
if vim.fn.isdirectory(venv_dir) == 1 then
|
||||
vim.health.ok('Python virtual environment found at ' .. venv_dir)
|
||||
else
|
||||
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
||||
if vim.fn.executable('nix') == 1 then
|
||||
vim.health.info('nix available but Python environment not resolved via nix')
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
if vim.fn.isdirectory(venv_dir) == 1 then
|
||||
vim.health.ok('Python virtual environment found at ' .. venv_dir)
|
||||
else
|
||||
vim.health.info('Python virtual environment not set up (created on first scrape)')
|
||||
end
|
||||
end
|
||||
|
||||
local time_cap = utils.time_capability()
|
||||
|
|
@ -41,7 +58,7 @@ local function check()
|
|||
vim.health.error('GNU time not found: ' .. (time_cap.reason or ''))
|
||||
end
|
||||
|
||||
local timeout_cap = utils.time_capability()
|
||||
local timeout_cap = utils.timeout_capability()
|
||||
if timeout_cap.ok then
|
||||
vim.health.ok('GNU timeout found: ' .. timeout_cap.path)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -15,21 +15,25 @@ local initialized = false
|
|||
|
||||
local function ensure_initialized()
|
||||
if initialized then
|
||||
return
|
||||
end
|
||||
if vim.g.cp_config then
|
||||
vim.deprecate('vim.g.cp_config', 'vim.g.cp', 'v0.7.6', 'cp.nvim', false)
|
||||
vim.g.cp = vim.g.cp or vim.g.cp_config
|
||||
return true
|
||||
end
|
||||
local user_config = vim.g.cp or {}
|
||||
local config = config_module.setup(user_config)
|
||||
config_module.set_current_config(config)
|
||||
local ok, result = pcall(config_module.setup, user_config)
|
||||
if not ok then
|
||||
local msg = tostring(result):gsub('^.+:%d+: ', '')
|
||||
vim.notify(msg, vim.log.levels.ERROR)
|
||||
return false
|
||||
end
|
||||
config_module.set_current_config(result)
|
||||
initialized = true
|
||||
return true
|
||||
end
|
||||
|
||||
---@return nil
|
||||
function M.handle_command(opts)
|
||||
ensure_initialized()
|
||||
if not ensure_initialized() then
|
||||
return
|
||||
end
|
||||
local commands = require('cp.commands')
|
||||
commands.handle_command(opts)
|
||||
end
|
||||
|
|
@ -40,7 +44,7 @@ end
|
|||
|
||||
---@deprecated Use `vim.g.cp` instead
|
||||
function M.setup(user_config)
|
||||
vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.1.0', 'cp.nvim', false)
|
||||
vim.deprecate('require("cp").setup()', 'vim.g.cp', 'v0.7.7', 'cp.nvim', false)
|
||||
|
||||
if user_config then
|
||||
vim.g.cp = vim.tbl_deep_extend('force', vim.g.cp or {}, user_config)
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ end
|
|||
function M.compile(compile_cmd, substitutions, on_complete)
|
||||
local cmd = substitute_template(compile_cmd, substitutions)
|
||||
local sh = table.concat(cmd, ' ') .. ' 2>&1'
|
||||
logger.log('compile: ' .. sh)
|
||||
|
||||
local t0 = vim.uv.hrtime()
|
||||
vim.system({ 'sh', '-c', sh }, { text = false }, function(r)
|
||||
|
|
@ -119,6 +120,7 @@ function M.run(cmd, stdin, timeout_ms, memory_mb, on_complete)
|
|||
local sec = math.ceil(timeout_ms / 1000)
|
||||
local timeout_prefix = ('%s -k 1s %ds '):format(timeout_bin, sec)
|
||||
local sh = prefix .. timeout_prefix .. ('%s -v sh -c %q 2>&1'):format(time_bin, prog)
|
||||
logger.log('run: ' .. sh)
|
||||
|
||||
local t0 = vim.uv.hrtime()
|
||||
vim.system({ 'sh', '-c', sh }, { stdin = stdin, text = true }, function(r)
|
||||
|
|
|
|||
|
|
@ -25,10 +25,22 @@ end
|
|||
---@param args string[]
|
||||
---@param opts { sync?: boolean, ndjson?: boolean, on_event?: fun(ev: table), on_exit?: fun(result: table) }
|
||||
local function run_scraper(platform, subcommand, args, opts)
|
||||
if not utils.setup_python_env() then
|
||||
local msg = 'no Python environment available (install uv or nix)'
|
||||
logger.log(msg, vim.log.levels.ERROR)
|
||||
if opts and opts.on_exit then
|
||||
opts.on_exit({ success = false, error = msg })
|
||||
end
|
||||
return { success = false, error = msg }
|
||||
end
|
||||
|
||||
local plugin_path = utils.get_plugin_path()
|
||||
local cmd = { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. platform, subcommand }
|
||||
local cmd = utils.get_python_cmd(platform, plugin_path)
|
||||
vim.list_extend(cmd, { subcommand })
|
||||
vim.list_extend(cmd, args)
|
||||
|
||||
logger.log('scraper cmd: ' .. table.concat(cmd, ' '))
|
||||
|
||||
local env = vim.fn.environ()
|
||||
env.VIRTUAL_ENV = ''
|
||||
env.PYTHONPATH = ''
|
||||
|
|
@ -41,31 +53,32 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
local buf = ''
|
||||
|
||||
local handle
|
||||
handle = uv.spawn(
|
||||
cmd[1],
|
||||
{ args = vim.list_slice(cmd, 2), stdio = { nil, stdout, stderr }, env = env },
|
||||
function(code, signal)
|
||||
if buf ~= '' and opts.on_event then
|
||||
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
|
||||
if ok_tail then
|
||||
opts.on_event(ev_tail)
|
||||
end
|
||||
buf = ''
|
||||
end
|
||||
if opts.on_exit then
|
||||
opts.on_exit({ success = (code == 0), code = code, signal = signal })
|
||||
end
|
||||
if not stdout:is_closing() then
|
||||
stdout:close()
|
||||
end
|
||||
if not stderr:is_closing() then
|
||||
stderr:close()
|
||||
end
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
handle = uv.spawn(cmd[1], {
|
||||
args = vim.list_slice(cmd, 2),
|
||||
stdio = { nil, stdout, stderr },
|
||||
env = env,
|
||||
cwd = plugin_path,
|
||||
}, function(code, signal)
|
||||
if buf ~= '' and opts.on_event then
|
||||
local ok_tail, ev_tail = pcall(vim.json.decode, buf)
|
||||
if ok_tail then
|
||||
opts.on_event(ev_tail)
|
||||
end
|
||||
buf = ''
|
||||
end
|
||||
)
|
||||
if opts.on_exit then
|
||||
opts.on_exit({ success = (code == 0), code = code, signal = signal })
|
||||
end
|
||||
if not stdout:is_closing() then
|
||||
stdout:close()
|
||||
end
|
||||
if not stderr:is_closing() then
|
||||
stderr:close()
|
||||
end
|
||||
if handle and not handle:is_closing() then
|
||||
handle:close()
|
||||
end
|
||||
end)
|
||||
|
||||
if not handle then
|
||||
logger.log('Failed to start scraper process', vim.log.levels.ERROR)
|
||||
|
|
@ -102,7 +115,7 @@ local function run_scraper(platform, subcommand, args, opts)
|
|||
return
|
||||
end
|
||||
|
||||
local sysopts = { text = true, timeout = 30000, env = env }
|
||||
local sysopts = { text = true, timeout = 30000, env = env, cwd = plugin_path }
|
||||
if opts and opts.sync then
|
||||
local result = vim.system(cmd, sysopts):wait()
|
||||
return syshandle(result)
|
||||
|
|
|
|||
|
|
@ -160,6 +160,8 @@ function M.setup_contest(platform, contest_id, problem_id, language)
|
|||
vim.bo[bufnr].buftype = ''
|
||||
vim.bo[bufnr].swapfile = false
|
||||
|
||||
state.set_language(lang)
|
||||
|
||||
if cfg.hooks and cfg.hooks.setup_code and not vim.b[bufnr].cp_setup_done then
|
||||
local ok = pcall(cfg.hooks.setup_code, state)
|
||||
if ok then
|
||||
|
|
|
|||
|
|
@ -121,13 +121,22 @@ function M.toggle_interactive(interactor_cmd)
|
|||
end
|
||||
local orchestrator =
|
||||
vim.fn.fnamemodify(utils.get_plugin_path() .. '/scripts/interact.py', ':p')
|
||||
cmdline = table.concat({
|
||||
'uv',
|
||||
'run',
|
||||
vim.fn.shellescape(orchestrator),
|
||||
vim.fn.shellescape(interactor),
|
||||
vim.fn.shellescape(binary),
|
||||
}, ' ')
|
||||
if utils.is_nix_build() then
|
||||
cmdline = table.concat({
|
||||
vim.fn.shellescape(utils.get_nix_python()),
|
||||
vim.fn.shellescape(orchestrator),
|
||||
vim.fn.shellescape(interactor),
|
||||
vim.fn.shellescape(binary),
|
||||
}, ' ')
|
||||
else
|
||||
cmdline = table.concat({
|
||||
'uv',
|
||||
'run',
|
||||
vim.fn.shellescape(orchestrator),
|
||||
vim.fn.shellescape(interactor),
|
||||
vim.fn.shellescape(binary),
|
||||
}, ' ')
|
||||
end
|
||||
else
|
||||
cmdline = vim.fn.shellescape(binary)
|
||||
end
|
||||
|
|
|
|||
150
lua/cp/utils.lua
150
lua/cp/utils.lua
|
|
@ -2,6 +2,9 @@ local M = {}
|
|||
|
||||
local logger = require('cp.log')
|
||||
|
||||
local _nix_python = nil
|
||||
local _nix_discovered = false
|
||||
|
||||
local uname = vim.loop.os_uname()
|
||||
|
||||
local _time_cached = false
|
||||
|
|
@ -57,7 +60,11 @@ local function find_gnu_time()
|
|||
|
||||
_time_cached = true
|
||||
_time_path = nil
|
||||
_time_reason = 'GNU time not found'
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
_time_reason = 'GNU time not found (install via: brew install coreutils)'
|
||||
else
|
||||
_time_reason = 'GNU time not found'
|
||||
end
|
||||
return _time_path, _time_reason
|
||||
end
|
||||
|
||||
|
|
@ -79,27 +86,101 @@ function M.get_plugin_path()
|
|||
return vim.fn.fnamemodify(plugin_path, ':h:h:h')
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.is_nix_build()
|
||||
return _nix_python ~= nil
|
||||
end
|
||||
|
||||
---@return string|nil
|
||||
function M.get_nix_python()
|
||||
return _nix_python
|
||||
end
|
||||
|
||||
---@return boolean
|
||||
function M.is_nix_discovered()
|
||||
return _nix_discovered
|
||||
end
|
||||
|
||||
---@param module string
|
||||
---@param plugin_path string
|
||||
---@return string[]
|
||||
function M.get_python_cmd(module, plugin_path)
|
||||
if _nix_python then
|
||||
return { _nix_python, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
return { 'uv', 'run', '--directory', plugin_path, '-m', 'scrapers.' .. module }
|
||||
end
|
||||
|
||||
local python_env_setup = false
|
||||
|
||||
---@return boolean
|
||||
local function discover_nix_python()
|
||||
local cache_dir = vim.fn.stdpath('cache') .. '/cp-nvim'
|
||||
local cache_file = cache_dir .. '/nix-python'
|
||||
|
||||
local f = io.open(cache_file, 'r')
|
||||
if f then
|
||||
local cached = f:read('*l')
|
||||
f:close()
|
||||
if cached and vim.fn.executable(cached) == 1 then
|
||||
_nix_python = cached
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local plugin_path = M.get_plugin_path()
|
||||
vim.notify('[cp.nvim] Building Python environment with nix...', vim.log.levels.INFO)
|
||||
vim.cmd.redraw()
|
||||
local result = vim
|
||||
.system(
|
||||
{ 'nix', 'build', plugin_path .. '#pythonEnv', '--no-link', '--print-out-paths' },
|
||||
{ text = true }
|
||||
)
|
||||
:wait()
|
||||
|
||||
if result.code ~= 0 then
|
||||
logger.log('nix build #pythonEnv failed: ' .. (result.stderr or ''), vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
local store_path = result.stdout:gsub('%s+$', '')
|
||||
local python_path = store_path .. '/bin/python3'
|
||||
|
||||
if vim.fn.executable(python_path) ~= 1 then
|
||||
logger.log('nix python not executable at ' .. python_path, vim.log.levels.WARN)
|
||||
return false
|
||||
end
|
||||
|
||||
vim.fn.mkdir(cache_dir, 'p')
|
||||
f = io.open(cache_file, 'w')
|
||||
if f then
|
||||
f:write(python_path)
|
||||
f:close()
|
||||
end
|
||||
|
||||
_nix_python = python_path
|
||||
_nix_discovered = true
|
||||
return true
|
||||
end
|
||||
|
||||
---@return boolean success
|
||||
function M.setup_python_env()
|
||||
if python_env_setup then
|
||||
return true
|
||||
end
|
||||
|
||||
local plugin_path = M.get_plugin_path()
|
||||
local venv_dir = plugin_path .. '/.venv'
|
||||
|
||||
if vim.fn.executable('uv') == 0 then
|
||||
logger.log(
|
||||
'uv is not installed. Install it to enable problem scraping: https://docs.astral.sh/uv/',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return false
|
||||
if _nix_python then
|
||||
logger.log('Python env: nix (python=' .. _nix_python .. ')')
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
|
||||
if vim.fn.isdirectory(venv_dir) == 0 then
|
||||
logger.log('Setting up Python environment for scrapers...')
|
||||
if vim.fn.executable('uv') == 1 then
|
||||
local plugin_path = M.get_plugin_path()
|
||||
logger.log('Python env: uv sync (dir=' .. plugin_path .. ')')
|
||||
vim.notify('[cp.nvim] Setting up Python environment...', vim.log.levels.INFO)
|
||||
vim.cmd.redraw()
|
||||
|
||||
local env = vim.fn.environ()
|
||||
env.VIRTUAL_ENV = ''
|
||||
env.PYTHONPATH = ''
|
||||
|
|
@ -108,14 +189,33 @@ function M.setup_python_env()
|
|||
.system({ 'uv', 'sync' }, { cwd = plugin_path, text = true, env = env })
|
||||
:wait()
|
||||
if result.code ~= 0 then
|
||||
logger.log('Failed to setup Python environment: ' .. result.stderr, vim.log.levels.ERROR)
|
||||
logger.log(
|
||||
'Failed to setup Python environment: ' .. (result.stderr or ''),
|
||||
vim.log.levels.ERROR
|
||||
)
|
||||
return false
|
||||
end
|
||||
logger.log('Python environment setup complete.')
|
||||
if result.stderr and result.stderr ~= '' then
|
||||
logger.log('uv sync stderr: ' .. result.stderr:gsub('%s+$', ''))
|
||||
end
|
||||
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
|
||||
python_env_setup = true
|
||||
return true
|
||||
if vim.fn.executable('nix') == 1 then
|
||||
logger.log('Python env: nix discovery')
|
||||
if discover_nix_python() then
|
||||
python_env_setup = true
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
logger.log(
|
||||
'No Python environment available. Install uv (https://docs.astral.sh/uv/) or use nix.',
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return false
|
||||
end
|
||||
|
||||
--- Configure the buffer with good defaults
|
||||
|
|
@ -162,20 +262,12 @@ function M.check_required_runtime()
|
|||
|
||||
local time = M.time_capability()
|
||||
if not time.ok then
|
||||
return false, 'GNU time not found: ' .. (time.reason or '')
|
||||
return false, time.reason
|
||||
end
|
||||
|
||||
local timeout = M.timeout_capability()
|
||||
if not timeout.ok then
|
||||
return false, 'GNU timeout not found: ' .. (timeout.reason or '')
|
||||
end
|
||||
|
||||
if vim.fn.executable('uv') ~= 1 then
|
||||
return false, 'uv not found (https://docs.astral.sh/uv/)'
|
||||
end
|
||||
|
||||
if not M.setup_python_env() then
|
||||
return false, 'failed to set up Python virtual environment'
|
||||
return false, timeout.reason
|
||||
end
|
||||
|
||||
return true
|
||||
|
|
@ -225,7 +317,11 @@ local function find_gnu_timeout()
|
|||
|
||||
_timeout_cached = true
|
||||
_timeout_path = nil
|
||||
_timeout_reason = 'GNU timeout not found'
|
||||
if uname and uname.sysname == 'Darwin' then
|
||||
_timeout_reason = 'GNU timeout not found (install via: brew install coreutils)'
|
||||
else
|
||||
_timeout_reason = 'GNU timeout not found'
|
||||
end
|
||||
return _timeout_path, _timeout_reason
|
||||
end
|
||||
|
||||
|
|
|
|||
0
new
0
new
|
|
@ -154,3 +154,17 @@ end, {
|
|||
return {}
|
||||
end,
|
||||
})
|
||||
|
||||
local function cp_action(action)
|
||||
return function()
|
||||
require('cp').handle_command({ fargs = { action } })
|
||||
end
|
||||
end
|
||||
|
||||
vim.keymap.set('n', '<Plug>(cp-run)', cp_action('run'), { desc = 'CP run tests' })
|
||||
vim.keymap.set('n', '<Plug>(cp-panel)', cp_action('panel'), { desc = 'CP open panel' })
|
||||
vim.keymap.set('n', '<Plug>(cp-edit)', cp_action('edit'), { desc = 'CP edit test cases' })
|
||||
vim.keymap.set('n', '<Plug>(cp-next)', cp_action('next'), { desc = 'CP next problem' })
|
||||
vim.keymap.set('n', '<Plug>(cp-prev)', cp_action('prev'), { desc = 'CP previous problem' })
|
||||
vim.keymap.set('n', '<Plug>(cp-pick)', cp_action('pick'), { desc = 'CP pick contest' })
|
||||
vim.keymap.set('n', '<Plug>(cp-interact)', cp_action('interact'), { desc = 'CP interactive mode' })
|
||||
|
|
|
|||
|
|
@ -12,8 +12,6 @@ dependencies = [
|
|||
"ndjson>=0.3.1",
|
||||
"pydantic>=2.11.10",
|
||||
"requests>=2.32.5",
|
||||
"scrapling[fetchers]>=0.3.5",
|
||||
"types-requests>=2.32.4.20250913",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import re
|
|||
from typing import Any
|
||||
|
||||
import httpx
|
||||
from scrapling.fetchers import Fetcher
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
|
|
@ -50,8 +50,9 @@ def _extract_memory_limit(html: str) -> float:
|
|||
|
||||
|
||||
def _fetch_html_sync(url: str) -> str:
|
||||
response = Fetcher.get(url)
|
||||
return str(response.body)
|
||||
response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_S)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
class CodeChefScraper(BaseScraper):
|
||||
|
|
|
|||
|
|
@ -2,13 +2,12 @@
|
|||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from scrapling.fetchers import Fetcher
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
from .base import BaseScraper
|
||||
from .models import (
|
||||
|
|
@ -19,10 +18,6 @@ from .models import (
|
|||
TestCase,
|
||||
)
|
||||
|
||||
# suppress scrapling logging - https://github.com/D4Vinci/Scrapling/issues/31)
|
||||
logging.getLogger("scrapling").setLevel(logging.CRITICAL)
|
||||
|
||||
|
||||
BASE_URL = "https://codeforces.com"
|
||||
API_CONTEST_LIST_URL = f"{BASE_URL}/api/contest.list"
|
||||
TIMEOUT_SECONDS = 30
|
||||
|
|
@ -83,7 +78,7 @@ def _extract_title(block: Tag) -> tuple[str, str]:
|
|||
|
||||
def _extract_samples(block: Tag) -> tuple[list[TestCase], bool]:
|
||||
st = block.find("div", class_="sample-test")
|
||||
if not st:
|
||||
if not isinstance(st, Tag):
|
||||
return [], False
|
||||
|
||||
input_pres: list[Tag] = [
|
||||
|
|
@ -140,10 +135,9 @@ def _is_interactive(block: Tag) -> bool:
|
|||
|
||||
def _fetch_problems_html(contest_id: str) -> str:
|
||||
url = f"{BASE_URL}/contest/{contest_id}/problems"
|
||||
page = Fetcher.get(
|
||||
url,
|
||||
)
|
||||
return page.html_content
|
||||
response = curl_requests.get(url, impersonate="chrome", timeout=TIMEOUT_SECONDS)
|
||||
response.raise_for_status()
|
||||
return response.text
|
||||
|
||||
|
||||
def _parse_all_blocks(html: str) -> list[dict[str, Any]]:
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ from typing import Any
|
|||
import httpx
|
||||
import pytest
|
||||
import requests
|
||||
from scrapling import fetchers
|
||||
from curl_cffi import requests as curl_requests
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
FIX = Path(__file__).resolve().parent / "fixtures"
|
||||
|
|
@ -136,12 +136,15 @@ def run_scraper_offline(fixture_text):
|
|||
|
||||
case "codeforces":
|
||||
|
||||
class MockCodeForcesPage:
|
||||
class MockCurlResponse:
|
||||
def __init__(self, html: str):
|
||||
self.html_content = html
|
||||
self.text = html
|
||||
|
||||
def _mock_stealthy_fetch(url: str, **kwargs):
|
||||
return MockCodeForcesPage(_router_codeforces(url=url))
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def _mock_curl_get(url: str, **kwargs):
|
||||
return MockCurlResponse(_router_codeforces(url=url))
|
||||
|
||||
def _mock_requests_get(url: str, **kwargs):
|
||||
if "api/contest.list" in url:
|
||||
|
|
@ -172,7 +175,7 @@ def run_scraper_offline(fixture_text):
|
|||
raise AssertionError(f"Unexpected requests.get call: {url}")
|
||||
|
||||
return {
|
||||
"Fetcher.get": _mock_stealthy_fetch,
|
||||
"curl_requests.get": _mock_curl_get,
|
||||
"requests.get": _mock_requests_get,
|
||||
}
|
||||
|
||||
|
|
@ -212,21 +215,23 @@ def run_scraper_offline(fixture_text):
|
|||
return MockResponse(data)
|
||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||
|
||||
class MockCodeChefPage:
|
||||
class MockCodeChefCurlResponse:
|
||||
def __init__(self, html: str):
|
||||
self.body = html
|
||||
self.status = 200
|
||||
self.text = html
|
||||
|
||||
def _mock_stealthy_fetch(url: str, **kwargs):
|
||||
def raise_for_status(self):
|
||||
pass
|
||||
|
||||
def _mock_curl_get(url: str, **kwargs):
|
||||
if "/problems/" in url:
|
||||
problem_id = url.rstrip("/").split("/")[-1]
|
||||
html = fixture_text(f"codechef/{problem_id}.html")
|
||||
return MockCodeChefPage(html)
|
||||
return MockCodeChefCurlResponse(html)
|
||||
raise AssertionError(f"No fixture for CodeChef url={url!r}")
|
||||
|
||||
return {
|
||||
"__offline_get_async": __offline_get_async,
|
||||
"Fetcher.get": _mock_stealthy_fetch,
|
||||
"curl_requests.get": _mock_curl_get,
|
||||
}
|
||||
|
||||
case _:
|
||||
|
|
@ -245,7 +250,7 @@ def run_scraper_offline(fixture_text):
|
|||
offline_fetches = _make_offline_fetches(scraper_name)
|
||||
|
||||
if scraper_name == "codeforces":
|
||||
fetchers.Fetcher.get = offline_fetches["Fetcher.get"]
|
||||
curl_requests.get = offline_fetches["curl_requests.get"]
|
||||
requests.get = offline_fetches["requests.get"]
|
||||
elif scraper_name == "atcoder":
|
||||
ns._fetch = offline_fetches["_fetch"]
|
||||
|
|
@ -254,7 +259,7 @@ def run_scraper_offline(fixture_text):
|
|||
httpx.AsyncClient.get = offline_fetches["__offline_fetch_text"]
|
||||
elif scraper_name == "codechef":
|
||||
httpx.AsyncClient.get = offline_fetches["__offline_get_async"]
|
||||
fetchers.Fetcher.get = offline_fetches["Fetcher.get"]
|
||||
curl_requests.get = offline_fetches["curl_requests.get"]
|
||||
|
||||
scraper_class = getattr(ns, scraper_classes[scraper_name])
|
||||
scraper = scraper_class()
|
||||
|
|
|
|||
|
|
@ -6,11 +6,6 @@ from scrapers.models import (
|
|||
TestsResult,
|
||||
)
|
||||
|
||||
MODEL_FOR_MODE = {
|
||||
"metadata": MetadataResult,
|
||||
"contests": ContestListResult,
|
||||
}
|
||||
|
||||
MATRIX = {
|
||||
"cses": {
|
||||
"metadata": ("introductory_problems",),
|
||||
|
|
@ -43,17 +38,16 @@ def test_scraper_offline_fixture_matrix(run_scraper_offline, scraper, mode):
|
|||
assert rc in (0, 1), f"Bad exit code {rc}"
|
||||
assert objs, f"No JSON output for {scraper}:{mode}"
|
||||
|
||||
if mode in ("metadata", "contests"):
|
||||
Model = MODEL_FOR_MODE[mode]
|
||||
model = Model.model_validate(objs[-1])
|
||||
assert model is not None
|
||||
if mode == "metadata":
|
||||
model = MetadataResult.model_validate(objs[-1])
|
||||
assert model.success is True
|
||||
if mode == "metadata":
|
||||
assert model.url
|
||||
assert len(model.problems) >= 1
|
||||
assert all(isinstance(p.id, str) and p.id for p in model.problems)
|
||||
else:
|
||||
assert len(model.contests) >= 1
|
||||
assert model.url
|
||||
assert len(model.problems) >= 1
|
||||
assert all(isinstance(p.id, str) and p.id for p in model.problems)
|
||||
elif mode == "contests":
|
||||
model = ContestListResult.model_validate(objs[-1])
|
||||
assert model.success is True
|
||||
assert len(model.contests) >= 1
|
||||
else:
|
||||
assert len(objs) >= 1, "No test objects returned"
|
||||
validated_any = False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue