Compare commits
38 commits
chore/add-
...
fix/vim-lo
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e8480821d | |||
| 4ccab9ee1f | |||
| 48c08825b2 | |||
| 4d58db8520 | |||
| 591f70a237 | |||
|
|
e989897c77 | ||
|
|
1c31abe3d6 | ||
| d3ac300ea0 | |||
| db5bd791f9 | |||
|
|
9fc34cb6fd | ||
|
|
484a4a56d0 | ||
| ff5ba39a59 | |||
| 760e7d7731 | |||
| 49e4233b3f | |||
| 622620f6d0 | |||
| 976838d981 | |||
| 06f72bbe2b | |||
| 6045042dfb | |||
| c192afc5d7 | |||
| b6f3398bbc | |||
| e02a29bd40 | |||
| 0f9715298e | |||
| 2148d9bd07 | |||
| 1162e7046b | |||
| b36ffba63a | |||
| 04d0c124cf | |||
| da433068ef | |||
| 51504b0121 | |||
| 49df7e015d | |||
| 029ea125b9 | |||
|
|
43193c3762 | ||
| de2bc07532 | |||
|
|
041e09ac04 | ||
| d23b4e59d1 | |||
|
|
19e71ac7fa | ||
| a54a06f939 | |||
|
|
b2c7f16890 | ||
| 276241447c |
30 changed files with 750 additions and 1792 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
|
||||
29
.github/workflows/quality.yaml
vendored
29
.github/workflows/quality.yaml
vendored
|
|
@ -28,6 +28,7 @@ jobs:
|
|||
- '*.lua'
|
||||
- '.luarc.json'
|
||||
- '*.toml'
|
||||
- 'vim.yaml'
|
||||
python:
|
||||
- 'scripts/**/.py'
|
||||
- 'scrapers/**/*.py'
|
||||
|
|
@ -45,11 +46,8 @@ jobs:
|
|||
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 .
|
||||
- uses: cachix/install-nix-action@v31
|
||||
- run: nix develop --command stylua --check .
|
||||
|
||||
lua-lint:
|
||||
name: Lua Lint Check
|
||||
|
|
@ -58,11 +56,8 @@ jobs:
|
|||
if: ${{ needs.changes.outputs.lua == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Lint with Selene
|
||||
uses: NTBBloodbath/selene-action@v1.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --display-style quiet .
|
||||
- uses: cachix/install-nix-action@v31
|
||||
- run: nix develop --command selene --display-style quiet .
|
||||
|
||||
lua-typecheck:
|
||||
name: Lua Type Check
|
||||
|
|
@ -127,15 +122,5 @@ jobs:
|
|||
if: ${{ needs.changes.outputs.markdown == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 8
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
- name: Install prettier
|
||||
run: pnpm add -g prettier@3.1.0
|
||||
- name: Check markdown formatting with prettier
|
||||
run: prettier --check .
|
||||
- uses: cachix/install-nix-action@v31
|
||||
- run: nix develop --command prettier --check .
|
||||
|
|
|
|||
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/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"runtime.version": "Lua 5.1",
|
||||
"runtime.version": "LuaJIT",
|
||||
"runtime.path": ["lua/?.lua", "lua/?/init.lua"],
|
||||
"diagnostics.globals": ["vim"],
|
||||
"workspace.library": ["$VIMRUNTIME/lua", "${3rd}/luv/library"],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
479
doc/cp.nvim.txt
479
doc/cp.nvim.txt
|
|
@ -18,6 +18,243 @@ REQUIREMENTS *cp-requirements*
|
|||
- Unix-like operating system
|
||||
- uv package manager (https://docs.astral.sh/uv/)
|
||||
|
||||
==============================================================================
|
||||
SETUP *cp-setup*
|
||||
|
||||
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*
|
||||
|
||||
Configuration is done via `vim.g.cp`. Set this before using the plugin:
|
||||
>lua
|
||||
vim.g.cp = {
|
||||
languages = {
|
||||
cpp = {
|
||||
extension = 'cc',
|
||||
commands = {
|
||||
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}',
|
||||
'-fdiagnostics-color=always' },
|
||||
run = { '{binary}' },
|
||||
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
|
||||
'{source}', '-o', '{binary}' },
|
||||
},
|
||||
},
|
||||
python = {
|
||||
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',
|
||||
},
|
||||
},
|
||||
open_url = true,
|
||||
debug = false,
|
||||
ui = {
|
||||
ansi = true,
|
||||
run = {
|
||||
width = 0.3,
|
||||
next_test_key = '<c-n>', -- or nil to disable
|
||||
prev_test_key = '<c-p>', -- or nil to disable
|
||||
},
|
||||
panel = {
|
||||
diff_modes = { 'side-by-side', 'git', 'vim' },
|
||||
max_output_lines = 50,
|
||||
},
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain',
|
||||
'--word-diff-regex=.', '--no-prefix' },
|
||||
},
|
||||
},
|
||||
picker = 'telescope',
|
||||
},
|
||||
}
|
||||
<
|
||||
|
||||
By default, C++ (g++ with ISO C++17) and Python are preconfigured under
|
||||
'languages'. Platforms select which languages are enabled and which one is
|
||||
the default; per-platform overrides can tweak 'extension' or 'commands'.
|
||||
|
||||
For example, to run CodeForces contests with Python by default:
|
||||
>lua
|
||||
vim.g.cp = {
|
||||
platforms = {
|
||||
codeforces = {
|
||||
default_language = 'python',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
Any language is supported provided the proper configuration. For example, to
|
||||
run CSES problems with Rust using the single schema:
|
||||
>lua
|
||||
vim.g.cp = {
|
||||
languages = {
|
||||
rust = {
|
||||
extension = 'rs',
|
||||
commands = {
|
||||
build = { 'rustc', '{source}', '-o', '{binary}' },
|
||||
run = { '{binary}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
platforms = {
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python', 'rust' },
|
||||
default_language = 'rust',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
*cp.Config*
|
||||
Fields: ~
|
||||
{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.
|
||||
{debug} (boolean, default: false) Show info messages.
|
||||
{scrapers} (string[]) Supported platform ids.
|
||||
{filename} (function, optional)
|
||||
function(contest, contest_id, problem_id, config, language): string
|
||||
Should return full filename with extension.
|
||||
(default: concatenates contest_id and problem_id, lowercased)
|
||||
{ui} (|CpUI|) UI settings: panel, diff backend, picker.
|
||||
{open_url} (boolean) Open the contest & problem url in the browser
|
||||
when a new contest is opened or the active problem changes.
|
||||
|
||||
*CpPlatform*
|
||||
Fields: ~
|
||||
{enabled_languages} (string[]) Language ids enabled on this platform.
|
||||
{default_language} (string) One of {enabled_languages}.
|
||||
{overrides} (table<string,|CpPlatformOverrides|>, optional)
|
||||
Per-language overrides of {extension} and/or {commands}.
|
||||
|
||||
*CpLanguage*
|
||||
Fields: ~
|
||||
{extension} (string) File extension without leading dot.
|
||||
{commands} (|CpLangCommands|) Command templates.
|
||||
|
||||
*CpLangCommands*
|
||||
Fields: ~
|
||||
{build} (string[], optional) For compiled languages.
|
||||
Must include {source} and {binary}.
|
||||
{run} (string[], optional) Runtime command.
|
||||
Compiled: must include {binary}.
|
||||
Interpreted: must include {source}.
|
||||
{debug} (string[], optional) Debug variant; same token rules
|
||||
as {build} (compiled) or {run} (interpreted).
|
||||
|
||||
*CpUI*
|
||||
Fields: ~
|
||||
{ansi} (boolean, default: true) Enable ANSI color parsing
|
||||
and highlighting in both I/O view and panel.
|
||||
{run} (|RunConfig|) I/O view configuration.
|
||||
{panel} (|PanelConfig|) Test panel behavior configuration.
|
||||
{diff} (|DiffConfig|) Diff backend configuration.
|
||||
{picker} (string|nil) 'telescope', 'fzf-lua', or nil.
|
||||
|
||||
*RunConfig*
|
||||
Fields: ~
|
||||
{width} (number, default: 0.3) Width of I/O view splits as
|
||||
fraction of screen (0.0 to 1.0).
|
||||
{next_test_key} (string|nil, default: '<c-n>') Keymap to navigate
|
||||
to next test in I/O view. Set to nil to disable.
|
||||
{prev_test_key} (string|nil, default: '<c-p>') Keymap to navigate
|
||||
to previous test in I/O view. Set to nil to disable.
|
||||
{format_verdict} (|VerdictFormatter|, default: nil) Custom verdict line
|
||||
formatter. See |cp-verdict-format|.
|
||||
|
||||
*EditConfig*
|
||||
Fields: ~
|
||||
{next_test_key} (string|nil, default: ']t') Jump to next test.
|
||||
{prev_test_key} (string|nil, default: '[t') Jump to previous test.
|
||||
{delete_test_key} (string|nil, default: 'gd') Delete current test.
|
||||
{add_test_key} (string|nil, default: 'ga') Add new test.
|
||||
{save_and_exit_key} (string|nil, default: 'q') Save and exit editor.
|
||||
All keys are nil-able. Set to nil to disable.
|
||||
|
||||
*cp.PanelConfig*
|
||||
Fields: ~
|
||||
{diff_modes} (string[], default: {'side-by-side', 'git', 'vim'})
|
||||
List of diff modes to cycle through with 't' key.
|
||||
First element is the default mode.
|
||||
Valid modes: 'side-by-side', 'git', 'vim'.
|
||||
{max_output_lines} (number, default: 50) Maximum lines of test output.
|
||||
|
||||
*cp.DiffConfig*
|
||||
Fields: ~
|
||||
{git} (|cp.DiffGitConfig|) Git diff backend configuration.
|
||||
|
||||
*cp.DiffGitConfig*
|
||||
Fields: ~
|
||||
{args} (string[]) Command-line arguments for git diff.
|
||||
Default: { 'diff', '--no-index', '--word-diff=plain',
|
||||
'--word-diff-regex=.', '--no-prefix' }
|
||||
• --no-index: Compare files outside git repository
|
||||
• --word-diff=plain: Character-level diff markers
|
||||
• --word-diff-regex=.: Split on every character
|
||||
• --no-prefix: Remove a/ b/ prefixes from output
|
||||
|
||||
*cp.Hooks*
|
||||
Fields: ~
|
||||
{before_run} (function, optional) Called before test panel opens.
|
||||
function(state: cp.State)
|
||||
{before_debug} (function, optional) Called before debug build/run.
|
||||
function(state: cp.State)
|
||||
{setup_code} (function, optional) Called after source file is opened.
|
||||
function(state: cp.State)
|
||||
{setup_io_input} (function, optional) Called when I/O input buffer created.
|
||||
function(bufnr: integer, state: cp.State)
|
||||
Default: helpers.clearcol (removes line numbers/columns)
|
||||
{setup_io_output} (function, optional) Called when I/O output buffer created.
|
||||
function(bufnr: integer, state: cp.State)
|
||||
Default: helpers.clearcol (removes line numbers/columns)
|
||||
|
||||
Hook functions receive the cp.nvim state object (|cp.State|). See
|
||||
|lua/cp/state.lua| for available methods and fields.
|
||||
|
||||
The I/O buffer hooks are called once when the buffers are first created
|
||||
during problem setup. Use these to customize buffer appearance (e.g.,
|
||||
remove line numbers, set custom options). Access helpers via:
|
||||
>lua
|
||||
local helpers = require('cp').helpers
|
||||
<
|
||||
Example usage:
|
||||
>lua
|
||||
hooks = {
|
||||
setup_code = function(state)
|
||||
print("Setting up " .. state.get_base_name())
|
||||
print("Source file: " .. state.get_source_file())
|
||||
end,
|
||||
setup_io_input = function(bufnr, state)
|
||||
vim.api.nvim_set_option_value('number', false, { buf = bufnr })
|
||||
end
|
||||
}
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
COMMANDS *cp-commands*
|
||||
|
||||
|
|
@ -203,232 +440,40 @@ Debug Builds ~
|
|||
<
|
||||
|
||||
==============================================================================
|
||||
CONFIGURATION *cp-config*
|
||||
MAPPINGS *cp-mappings*
|
||||
|
||||
Configuration is done via `vim.g.cp_config`. Set this before using the plugin:
|
||||
>lua
|
||||
vim.g.cp_config = {
|
||||
languages = {
|
||||
cpp = {
|
||||
extension = 'cc',
|
||||
commands = {
|
||||
build = { 'g++', '-std=c++17', '{source}', '-o', '{binary}',
|
||||
'-fdiagnostics-color=always' },
|
||||
run = { '{binary}' },
|
||||
debug = { 'g++', '-std=c++17', '-fsanitize=address,undefined',
|
||||
'{source}', '-o', '{binary}' },
|
||||
},
|
||||
},
|
||||
python = {
|
||||
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',
|
||||
},
|
||||
},
|
||||
open_url = true,
|
||||
debug = false,
|
||||
ui = {
|
||||
ansi = true,
|
||||
run = {
|
||||
width = 0.3,
|
||||
next_test_key = '<c-n>', -- or nil to disable
|
||||
prev_test_key = '<c-p>', -- or nil to disable
|
||||
},
|
||||
panel = {
|
||||
diff_modes = { 'side-by-side', 'git', 'vim' },
|
||||
max_output_lines = 50,
|
||||
},
|
||||
diff = {
|
||||
git = {
|
||||
args = { 'diff', '--no-index', '--word-diff=plain',
|
||||
'--word-diff-regex=.', '--no-prefix' },
|
||||
},
|
||||
},
|
||||
picker = 'telescope',
|
||||
},
|
||||
}
|
||||
<
|
||||
cp.nvim provides <Plug> mappings for all primary actions. These dispatch
|
||||
through the same code path as |:CP|.
|
||||
|
||||
By default, C++ (g++ with ISO C++17) and Python are preconfigured under
|
||||
'languages'. Platforms select which languages are enabled and which one is
|
||||
the default; per-platform overrides can tweak 'extension' or 'commands'.
|
||||
*<Plug>(cp-run)*
|
||||
<Plug>(cp-run) Run tests in I/O view. Equivalent to :CP run.
|
||||
|
||||
For example, to run CodeForces contests with Python by default:
|
||||
>lua
|
||||
vim.g.cp_config = {
|
||||
platforms = {
|
||||
codeforces = {
|
||||
default_language = 'python',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
Any language is supported provided the proper configuration. For example, to
|
||||
run CSES problems with Rust using the single schema:
|
||||
>lua
|
||||
vim.g.cp_config = {
|
||||
languages = {
|
||||
rust = {
|
||||
extension = 'rs',
|
||||
commands = {
|
||||
build = { 'rustc', '{source}', '-o', '{binary}' },
|
||||
run = { '{binary}' },
|
||||
},
|
||||
},
|
||||
},
|
||||
platforms = {
|
||||
cses = {
|
||||
enabled_languages = { 'cpp', 'python', 'rust' },
|
||||
default_language = 'rust',
|
||||
},
|
||||
},
|
||||
}
|
||||
<
|
||||
*cp.Config*
|
||||
Fields: ~
|
||||
{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.
|
||||
{debug} (boolean, default: false) Show info messages.
|
||||
{scrapers} (string[]) Supported platform ids.
|
||||
{filename} (function, optional)
|
||||
function(contest, contest_id, problem_id, config, language): string
|
||||
Should return full filename with extension.
|
||||
(default: concatenates contest_id and problem_id, lowercased)
|
||||
{ui} (|CpUI|) UI settings: panel, diff backend, picker.
|
||||
{open_url} (boolean) Open the contest & problem url in the browser
|
||||
when the contest is first opened.
|
||||
*<Plug>(cp-panel)*
|
||||
<Plug>(cp-panel) Open full-screen test panel. Equivalent to :CP panel.
|
||||
|
||||
*CpPlatform*
|
||||
Fields: ~
|
||||
{enabled_languages} (string[]) Language ids enabled on this platform.
|
||||
{default_language} (string) One of {enabled_languages}.
|
||||
{overrides} (table<string,|CpPlatformOverrides|>, optional)
|
||||
Per-language overrides of {extension} and/or {commands}.
|
||||
*<Plug>(cp-edit)*
|
||||
<Plug>(cp-edit) Open the test case editor. Equivalent to :CP edit.
|
||||
|
||||
*CpLanguage*
|
||||
Fields: ~
|
||||
{extension} (string) File extension without leading dot.
|
||||
{commands} (|CpLangCommands|) Command templates.
|
||||
*<Plug>(cp-next)*
|
||||
<Plug>(cp-next) Navigate to the next problem. Equivalent to :CP next.
|
||||
|
||||
*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).
|
||||
*<Plug>(cp-prev)*
|
||||
<Plug>(cp-prev) Navigate to the previous problem. Equivalent to :CP prev.
|
||||
|
||||
*CpUI*
|
||||
Fields: ~
|
||||
{ansi} (boolean, default: true) Enable ANSI color parsing
|
||||
and highlighting in both I/O view and panel.
|
||||
{run} (|RunConfig|) I/O view configuration.
|
||||
{panel} (|PanelConfig|) Test panel behavior configuration.
|
||||
{diff} (|DiffConfig|) Diff backend configuration.
|
||||
{picker} (string|nil) 'telescope', 'fzf-lua', or nil.
|
||||
*<Plug>(cp-pick)*
|
||||
<Plug>(cp-pick) Launch the contest picker. Equivalent to :CP pick.
|
||||
|
||||
*RunConfig*
|
||||
Fields: ~
|
||||
{width} (number, default: 0.3) Width of I/O view splits as
|
||||
fraction of screen (0.0 to 1.0).
|
||||
{next_test_key} (string|nil, default: '<c-n>') Keymap to navigate
|
||||
to next test in I/O view. Set to nil to disable.
|
||||
{prev_test_key} (string|nil, default: '<c-p>') Keymap to navigate
|
||||
to previous test in I/O view. Set to nil to disable.
|
||||
{format_verdict} (|VerdictFormatter|, default: nil) Custom verdict line
|
||||
formatter. See |cp-verdict-format|.
|
||||
*<Plug>(cp-interact)*
|
||||
<Plug>(cp-interact) Open interactive mode. Equivalent to :CP interact.
|
||||
|
||||
*EditConfig*
|
||||
Fields: ~
|
||||
{next_test_key} (string|nil, default: ']t') Jump to next test.
|
||||
{prev_test_key} (string|nil, default: '[t') Jump to previous test.
|
||||
{delete_test_key} (string|nil, default: 'gd') Delete current test.
|
||||
{add_test_key} (string|nil, default: 'ga') Add new test.
|
||||
{save_and_exit_key} (string|nil, default: 'q') Save and exit editor.
|
||||
All keys are nil-able. Set to nil to disable.
|
||||
|
||||
*cp.PanelConfig*
|
||||
Fields: ~
|
||||
{diff_modes} (string[], default: {'side-by-side', 'git', 'vim'})
|
||||
List of diff modes to cycle through with 't' key.
|
||||
First element is the default mode.
|
||||
Valid modes: 'side-by-side', 'git', 'vim'.
|
||||
{max_output_lines} (number, default: 50) Maximum lines of test output.
|
||||
|
||||
*cp.DiffConfig*
|
||||
Fields: ~
|
||||
{git} (|cp.DiffGitConfig|) Git diff backend configuration.
|
||||
|
||||
*cp.DiffGitConfig*
|
||||
Fields: ~
|
||||
{args} (string[]) Command-line arguments for git diff.
|
||||
Default: { 'diff', '--no-index', '--word-diff=plain',
|
||||
'--word-diff-regex=.', '--no-prefix' }
|
||||
• --no-index: Compare files outside git repository
|
||||
• --word-diff=plain: Character-level diff markers
|
||||
• --word-diff-regex=.: Split on every character
|
||||
• --no-prefix: Remove a/ b/ prefixes from output
|
||||
|
||||
*cp.Hooks*
|
||||
Fields: ~
|
||||
{before_run} (function, optional) Called before test panel opens.
|
||||
function(state: cp.State)
|
||||
{before_debug} (function, optional) Called before debug build/run.
|
||||
function(state: cp.State)
|
||||
{setup_code} (function, optional) Called after source file is opened.
|
||||
function(state: cp.State)
|
||||
{setup_io_input} (function, optional) Called when I/O input buffer created.
|
||||
function(bufnr: integer, state: cp.State)
|
||||
Default: helpers.clearcol (removes line numbers/columns)
|
||||
{setup_io_output} (function, optional) Called when I/O output buffer created.
|
||||
function(bufnr: integer, state: cp.State)
|
||||
Default: helpers.clearcol (removes line numbers/columns)
|
||||
|
||||
Hook functions receive the cp.nvim state object (|cp.State|). See
|
||||
|lua/cp/state.lua| for available methods and fields.
|
||||
|
||||
The I/O buffer hooks are called once when the buffers are first created
|
||||
during problem setup. Use these to customize buffer appearance (e.g.,
|
||||
remove line numbers, set custom options). Access helpers via:
|
||||
>lua
|
||||
local helpers = require('cp').helpers
|
||||
<
|
||||
Example usage:
|
||||
>lua
|
||||
hooks = {
|
||||
setup_code = function(state)
|
||||
print("Setting up " .. state.get_base_name())
|
||||
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
|
||||
}
|
||||
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)')
|
||||
<
|
||||
|
||||
==============================================================================
|
||||
|
|
|
|||
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
|
||||
}
|
||||
76
flake.nix
Normal file
76
flake.nix
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
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
|
||||
prettier
|
||||
stylua
|
||||
selene
|
||||
lua-language-server
|
||||
];
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
@ -38,7 +38,8 @@
|
|||
|
||||
local M = {}
|
||||
|
||||
local logger = require('cp.log')
|
||||
local CACHE_VERSION = 1
|
||||
|
||||
local cache_file = vim.fn.stdpath('data') .. '/cp-nvim.json'
|
||||
local cache_data = {}
|
||||
local loaded = false
|
||||
|
|
@ -65,9 +66,15 @@ function M.load()
|
|||
|
||||
local ok, decoded = pcall(vim.json.decode, table.concat(content, '\n'))
|
||||
if ok then
|
||||
cache_data = decoded
|
||||
if decoded._version ~= CACHE_VERSION then
|
||||
cache_data = {}
|
||||
M.save()
|
||||
else
|
||||
cache_data = decoded
|
||||
end
|
||||
else
|
||||
logger.log('Could not decode json in cache file', vim.log.levels.ERROR)
|
||||
cache_data = {}
|
||||
M.save()
|
||||
end
|
||||
loaded = true
|
||||
end
|
||||
|
|
@ -78,6 +85,7 @@ function M.save()
|
|||
vim.schedule(function()
|
||||
vim.fn.mkdir(vim.fn.fnamemodify(cache_file, ':h'), 'p')
|
||||
|
||||
cache_data._version = CACHE_VERSION
|
||||
local encoded = vim.json.encode(cache_data)
|
||||
local lines = vim.split(encoded, '\n')
|
||||
vim.fn.writefile(lines, cache_file)
|
||||
|
|
|
|||
|
|
@ -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,33 +5,50 @@ 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
|
||||
vim.health.error('cp.nvim requires Neovim 0.10.0+')
|
||||
end
|
||||
|
||||
local uname = vim.loop.os_uname()
|
||||
local uname = vim.uv.os_uname()
|
||||
if uname.sysname == 'Windows_NT' then
|
||||
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,17 +15,25 @@ local initialized = false
|
|||
|
||||
local function ensure_initialized()
|
||||
if initialized then
|
||||
return
|
||||
return true
|
||||
end
|
||||
local user_config = vim.g.cp_config or {}
|
||||
local config = config_module.setup(user_config)
|
||||
config_module.set_current_config(config)
|
||||
local user_config = vim.g.cp or {}
|
||||
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
|
||||
|
|
@ -34,4 +42,13 @@ function M.is_initialized()
|
|||
return initialized
|
||||
end
|
||||
|
||||
---@deprecated Use `vim.g.cp` instead
|
||||
function M.setup(user_config)
|
||||
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)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -20,52 +20,75 @@ local function syshandle(result)
|
|||
return { success = true, data = data }
|
||||
end
|
||||
|
||||
---@param env_map table<string, string>
|
||||
---@return string[]
|
||||
local function spawn_env_list(env_map)
|
||||
local out = {}
|
||||
for key, value in pairs(env_map) do
|
||||
out[#out + 1] = tostring(key) .. '=' .. tostring(value)
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
---@param platform string
|
||||
---@param subcommand string
|
||||
---@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 = ''
|
||||
env.CONDA_PREFIX = ''
|
||||
|
||||
if opts and opts.ndjson then
|
||||
local uv = vim.loop
|
||||
local uv = vim.uv
|
||||
local stdout = uv.new_pipe(false)
|
||||
local stderr = uv.new_pipe(false)
|
||||
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 = spawn_env_list(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 +125,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)
|
||||
|
|
|
|||
|
|
@ -121,6 +121,7 @@ end
|
|||
---@param language? string
|
||||
function M.setup_contest(platform, contest_id, problem_id, language)
|
||||
local old_platform, old_contest_id = state.get_platform(), state.get_contest_id()
|
||||
local old_problem_id = state.get_problem_id()
|
||||
|
||||
state.set_platform(platform)
|
||||
state.set_contest_id(contest_id)
|
||||
|
|
@ -133,7 +134,7 @@ function M.setup_contest(platform, contest_id, problem_id, language)
|
|||
end
|
||||
end
|
||||
|
||||
local is_new_contest = old_platform ~= platform and old_contest_id ~= contest_id
|
||||
local is_new_contest = old_platform ~= platform or old_contest_id ~= contest_id
|
||||
|
||||
cache.load()
|
||||
|
||||
|
|
@ -143,7 +144,10 @@ function M.setup_contest(platform, contest_id, problem_id, language)
|
|||
M.setup_problem(pid, language)
|
||||
start_tests(platform, contest_id, problems)
|
||||
|
||||
if config_module.get_config().open_url and is_new_contest and contest_data.url then
|
||||
local is_new_problem = old_problem_id ~= pid
|
||||
local should_open_url = config_module.get_config().open_url
|
||||
and (is_new_contest or is_new_problem)
|
||||
if should_open_url and contest_data.url then
|
||||
vim.ui.open(contest_data.url:format(pid))
|
||||
end
|
||||
end
|
||||
|
|
@ -160,6 +164,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
|
||||
|
|
@ -173,7 +179,7 @@ function M.setup_contest(platform, contest_id, problem_id, language)
|
|||
contest_id = contest_id,
|
||||
language = lang,
|
||||
requested_problem_id = problem_id,
|
||||
token = vim.loop.hrtime(),
|
||||
token = vim.uv.hrtime(),
|
||||
})
|
||||
|
||||
logger.log('Fetching contests problems...', vim.log.levels.INFO, true)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
154
lua/cp/utils.lua
154
lua/cp/utils.lua
|
|
@ -2,7 +2,10 @@ local M = {}
|
|||
|
||||
local logger = require('cp.log')
|
||||
|
||||
local uname = vim.loop.os_uname()
|
||||
local _nix_python = nil
|
||||
local _nix_discovered = false
|
||||
|
||||
local uname = vim.uv.os_uname()
|
||||
|
||||
local _time_cached = false
|
||||
local _time_path = nil
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -240,7 +336,7 @@ function M.timeout_capability()
|
|||
end
|
||||
|
||||
function M.cwd_executables()
|
||||
local uv = vim.uv or vim.loop
|
||||
local uv = vim.uv
|
||||
local req = uv.fs_scandir('.')
|
||||
if not req then
|
||||
return {}
|
||||
|
|
|
|||
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]]:
|
||||
|
|
|
|||
|
|
@ -1 +1,4 @@
|
|||
std = 'vim'
|
||||
|
||||
[lints]
|
||||
bad_string_escape = 'allow'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
30
vim.toml
30
vim.toml
|
|
@ -1,30 +0,0 @@
|
|||
[selene]
|
||||
base = "lua51"
|
||||
name = "vim"
|
||||
|
||||
[vim]
|
||||
any = true
|
||||
|
||||
[jit]
|
||||
any = true
|
||||
|
||||
[assert]
|
||||
any = true
|
||||
|
||||
[describe]
|
||||
any = true
|
||||
|
||||
[it]
|
||||
any = true
|
||||
|
||||
[before_each]
|
||||
any = true
|
||||
|
||||
[after_each]
|
||||
any = true
|
||||
|
||||
[spy]
|
||||
any = true
|
||||
|
||||
[stub]
|
||||
any = true
|
||||
26
vim.yaml
Normal file
26
vim.yaml
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
base: lua51
|
||||
name: vim
|
||||
lua_versions:
|
||||
- luajit
|
||||
globals:
|
||||
vim:
|
||||
any: true
|
||||
jit:
|
||||
any: true
|
||||
assert:
|
||||
any: true
|
||||
describe:
|
||||
any: true
|
||||
it:
|
||||
any: true
|
||||
before_each:
|
||||
any: true
|
||||
after_each:
|
||||
any: true
|
||||
spy:
|
||||
any: true
|
||||
stub:
|
||||
any: true
|
||||
bit:
|
||||
any: true
|
||||
Loading…
Add table
Add a link
Reference in a new issue