xeluxee / competitest.nvim

CompetiTest.nvim is a Neovim plugin for Competitive Programming: it can manage and check testcases, download problems and contests from online judges and much more
GNU Lesser General Public License v3.0
381 stars 15 forks source link
competitive-programming neovim neovim-lua neovim-lua-plugin neovim-plugin nvim

CompetiTest.nvim

![Neovim](https://img.shields.io/badge/NeoVim-0.5+-%2357A143.svg?&style=for-the-badge&logo=neovim) ![Lua](https://img.shields.io/badge/Lua-%232C2D72.svg?style=for-the-badge&logo=lua) ![License](https://img.shields.io/github/license/xeluxee/competitest.nvim?style=for-the-badge&logo=gnu) ## Competitive Programming with Neovim made Easy ![competitest_popup_ui](https://user-images.githubusercontent.com/88047141/149839002-280069e5-0c71-4aec-8e39-4443a1c44f5c.png) *CompetiTest's popup UI* ![competitest_split_ui](https://user-images.githubusercontent.com/88047141/183751179-e07e2a4d-e2eb-468b-ba34-bb737cba4557.png) *CompetiTest's split UI*

competitest.nvim is a testcase manager and checker. It saves you time in competitive programming contests by automating common tasks related to testcase management. It can compile, run and test your solutions across all the available testcases, displaying results in a nice interactive user interface.

Features

Installation

NOTE: this plugins requires Neovim ≥ 0.5

Install with vim-plug:

Plug 'MunifTanjim/nui.nvim'        " it's a dependency
Plug 'xeluxee/competitest.nvim'

Install with packer.nvim:

use {
    'xeluxee/competitest.nvim',
    requires = 'MunifTanjim/nui.nvim',
    config = function() require('competitest').setup() end
}

Install with lazy.nvim:

{
    'xeluxee/competitest.nvim',
    dependencies = 'MunifTanjim/nui.nvim',
    config = function() require('competitest').setup() end,
}

If you are using another package manager note that this plugin depends on nui.nvim, hence it should be installed as a dependency.

Usage

To load this plugin call setup():

require('competitest').setup() -- to use default configuration
require('competitest').setup { -- to customize settings
    -- put here configuration
}

To see all the available settings see configuration.

Usage notes

Storing testcases in multiple text files

Storing testcases in a single file

Anyway you can forget about these rules if you use :CompetiTest add_testcase and :CompetiTest edit_testcase, that handle these things for you.

When launching the following commands make sure the focused buffer is the one containing the source code file.

Add or Edit a testcase

Launch :CompetiTest add_testcase to add a new testcase.\ Launch :CompetiTest edit_testcase to edit an existing testcase. If you want to specify testcase number directly in the command line you can use :CompetiTest edit_testcase x, where x is a number representing the testcase you want to edit.

To jump between input and output windows press either <C-h>, <C-l>, or <C-i>. To save and close testcase editor press <C-s> or :wq.

Of course these keybindings can be customized: see editor_uinormal_mode_mappings and editor_uiinsert_mode_mappings in configuration

Remove a testcase

Launch :CompetiTest delete_testcase. If you want to specify testcase number directly in the command line you can use :CompetiTest delete_testcase x, where x is a number representing the testcase you want to remove.

Convert testcases

Testcases can be stored in multiple text files or in a single msgpack encoded file.\ Launch :CompetiTest convert to change testcases storage method: you can convert a single file into multiple files or vice versa. One of the following arguments is needed:

NOTE: this command only converts already existing testcases files without changing CompetiTest configuration. To choose the storage method to use you have to configure testcases_use_single_file option, that is false by default. Anyway storage method can be automatically detected when option testcases_auto_detect_storage is true.

Run testcases

Launch :CompetiTest run. CompetiTest's interface will appear and you'll be able to view details about a testcase by moving the cursor over its entry. You can close the UI by pressing q, Q or :q.\ If you're using a compiled language and you don't want to recompile your program launch :CompetiTest run_no_compile.\ If you have previously closed the UI and you want to re-open it without re-executing testcases or recompiling launch :CompetiTest show_ui.

Control processes

View details

Of course all these keybindings can be customized: see runner_uimappings in configuration

Receive testcases, problems and contests

NOTE: to get this feature working you need to install competitive-companion extension in your browser.

Thanks to its integration with competitive-companion, CompetiTest can download contents from competitive programming platforms:

After launching one of these commands click on the green plus button in your browser to start downloading.\ For further customization see receive options in configuration.

Customize folder structure

By default CompetiTest stores received problems and contests in current working directory. You can change this behavior through the options received_problems_path, received_contests_directory and received_contests_problems_path. See receive modifiers for further details.\ Here are some tips:

Templates for received problems and contests

When downloading a problem or a contest, source code templates can be configured for different file types. See template_file option in configuration.\ Receive modifiers can be used inside template files to insert details about received problems. To enable this feature set evaluate_template_modifiers to true. Template example for C++:

// Problem: $(PROBLEM)
// Contest: $(CONTEST)
// Judge: $(JUDGE)
// URL: $(URL)
// Memory Limit: $(MEMLIM)
// Time Limit: $(TIMELIM)
// Start: $(DATE)

#include <iostream>
using namespace std;
int main() {
    cout << "This is a template file" << endl;
    cerr << "Problem name is $(PROBLEM)" << endl;
    return 0;
}

Configuration

Full configuration

Here you can find CompetiTest default configuration

require('competitest').setup {
    local_config_file_name = ".competitest.lua",

    floating_border = "rounded",
    floating_border_highlight = "FloatBorder",
    picker_ui = {
        width = 0.2,
        height = 0.3,
        mappings = {
            focus_next = { "j", "<down>", "<Tab>" },
            focus_prev = { "k", "<up>", "<S-Tab>" },
            close = { "<esc>", "<C-c>", "q", "Q" },
            submit = { "<cr>" },
        },
    },
    editor_ui = {
        popup_width = 0.4,
        popup_height = 0.6,
        show_nu = true,
        show_rnu = false,
        normal_mode_mappings = {
            switch_window = { "<C-h>", "<C-l>", "<C-i>" },
            save_and_close = "<C-s>",
            cancel = { "q", "Q" },
        },
        insert_mode_mappings = {
            switch_window = { "<C-h>", "<C-l>", "<C-i>" },
            save_and_close = "<C-s>",
            cancel = "<C-q>",
        },
    },
    runner_ui = {
        interface = "popup",
        selector_show_nu = false,
        selector_show_rnu = false,
        show_nu = true,
        show_rnu = false,
        mappings = {
            run_again = "R",
            run_all_again = "<C-r>",
            kill = "K",
            kill_all = "<C-k>",
            view_input = { "i", "I" },
            view_output = { "a", "A" },
            view_stdout = { "o", "O" },
            view_stderr = { "e", "E" },
            toggle_diff = { "d", "D" },
            close = { "q", "Q" },
        },
        viewer = {
            width = 0.5,
            height = 0.5,
            show_nu = true,
            show_rnu = false,
            close_mappings = { "q", "Q" },
        },
    },
    popup_ui = {
        total_width = 0.8,
        total_height = 0.8,
        layout = {
            { 4, "tc" },
            { 5, { { 1, "so" }, { 1, "si" } } },
            { 5, { { 1, "eo" }, { 1, "se" } } },
        },
    },
    split_ui = {
        position = "right",
        relative_to_editor = true,
        total_width = 0.3,
        vertical_layout = {
            { 1, "tc" },
            { 1, { { 1, "so" }, { 1, "eo" } } },
            { 1, { { 1, "si" }, { 1, "se" } } },
        },
        total_height = 0.4,
        horizontal_layout = {
            { 2, "tc" },
            { 3, { { 1, "so" }, { 1, "si" } } },
            { 3, { { 1, "eo" }, { 1, "se" } } },
        },
    },

    save_current_file = true,
    save_all_files = false,
    compile_directory = ".",
    compile_command = {
        c = { exec = "gcc", args = { "-Wall", "$(FNAME)", "-o", "$(FNOEXT)" } },
        cpp = { exec = "g++", args = { "-Wall", "$(FNAME)", "-o", "$(FNOEXT)" } },
        rust = { exec = "rustc", args = { "$(FNAME)" } },
        java = { exec = "javac", args = { "$(FNAME)" } },
    },
    running_directory = ".",
    run_command = {
        c = { exec = "./$(FNOEXT)" },
        cpp = { exec = "./$(FNOEXT)" },
        rust = { exec = "./$(FNOEXT)" },
        python = { exec = "python", args = { "$(FNAME)" } },
        java = { exec = "java", args = { "$(FNOEXT)" } },
    },
    multiple_testing = -1,
    maximum_time = 5000,
    output_compare_method = "squish",
    view_output_diff = false,

    testcases_directory = ".",
    testcases_use_single_file = false,
    testcases_auto_detect_storage = true,
    testcases_single_file_format = "$(FNOEXT).testcases",
    testcases_input_file_format = "$(FNOEXT)_input$(TCNUM).txt",
    testcases_output_file_format = "$(FNOEXT)_output$(TCNUM).txt",

    companion_port = 27121,
    receive_print_message = true,
    template_file = false,
    evaluate_template_modifiers = false,
    date_format = "%c",
    received_files_extension = "cpp",
    received_problems_path = "$(CWD)/$(PROBLEM).$(FEXT)",
    received_problems_prompt_path = true,
    received_contests_directory = "$(CWD)",
    received_contests_problems_path = "$(PROBLEM).$(FEXT)",
    received_contests_prompt_directory = true,
    received_contests_prompt_extension = true,
    open_received_problems = true,
    open_received_contests = true,
    replace_received_testcases = false,
}

Explanation

Local configuration

You can use a different configuration for every different folder by creating a file called .competitest.lua (this name can be changed configuring the option local_config_file_name). It will affect every file contained in that folder and in subfolders. A table containing valid options must be returned, see the following example.

-- .competitest.lua content
return {
    multiple_testing = 3,
    maximum_time = 2500,
    testcases_input_file_format = "in_$(TCNUM).txt",
    testcases_output_file_format = "ans_$(TCNUM).txt",
    testcases_single_file_format = "$(FNOEXT).tc",
}

Available modifiers

Modifiers are substrings that will be replaced by another string, depending on the modifier and the context. They're used to tweak some options.

File-format modifiers

You can use them to define commands or to customize testcases files naming through options testcases_single_file_format, testcases_input_file_format and testcases_output_file_format.

Modifier Meaning
$() insert a dollar
$(HOME) user home directory
$(FNAME) file name
$(FNOEXT) file name without extension
$(FEXT) file extension
$(FABSPATH) absolute path of current file
$(ABSDIR) absolute path of folder that contains file
$(TCNUM) testcase number

Receive modifiers

You can use them to customize the options received_problems_path, received_contests_directory, received_contests_problems_path and to insert problem details inside template files. See also tips for customizing folder structure for received problems and contests.

Modifier Meaning
$() insert a dollar
$(HOME) user home directory
$(CWD) current working directory
$(FEXT) preferred file extension
$(PROBLEM) problem name, name field
$(GROUP) judge and contest name, group field
$(JUDGE) judge name (first part of group, before hyphen)
$(CONTEST) contest name (second part of group, after hyphen)
$(URL) problem url, url field
$(MEMLIM) available memory, memoryLimit field
$(TIMELIM) time limit, timeLimit field
$(JAVA_MAIN_CLASS) almost always "Main", mainClass field
$(JAVA_TASK_CLASS) classname-friendly version of problem name, taskClass field
$(DATE) current date and time (based on date_format), it can be used only inside template files

Fields are referred to received tasks.

Customize compile and run commands

Languages as C, C++, Rust, Java and Python are supported by default.\ Of course you can customize commands used for compiling and for running your programs. You can also add languages that aren't supported by default.

require('competitest').setup {
    compile_command = {
        cpp       = { exec = 'g++',           args = {'$(FNAME)', '-o', '$(FNOEXT)'} },
        some_lang = { exec = 'some_compiler', args = {'$(FNAME)'} },
    },
    run_command = {
        cpp       = { exec = './$(FNOEXT)' },
        some_lang = { exec = 'some_interpreter', args = {'$(FNAME)'} },
    },
}

See file-format modifiers to better understand how dollar notation works.

NOTE: if your language isn't compiled you can ignore compile_command section.

Feel free to open a PR or an issue if you think it's worth adding a new language among default ones.

Customize UI layout

You can customize testcase runner user interface by defining windows positions and sizes trough a table describing a layout. This is possible both for popup and split UI.

Every window is identified by a string representing its name and a number representing the proportion between its size and the sizes of other windows. To define a window use a lua table made by a number and a string. An example is { 1.5, "tc" }.\ Windows can be named as follows:

A layout is a list made by windows or layouts (recursively defined). To define a layout use a lua table containing a list of windows or layouts.

Sample code Result
``` lua layout = { { 2, "tc" }, { 3, { { 1, "so" }, { 1, "si" }, } }, { 3, { { 1, "eo" }, { 1, "se" }, } }, } ``` ![layout1](https://user-images.githubusercontent.com/88047141/183749940-b720e9b2-557d-460c-99d0-99a2a03a81bd.png)
``` lua layout = { { 1, { { 1, "so" }, { 1, { { 1, "tc" }, { 1, "se" }, } }, } }, { 1, { { 1, "eo" }, { 1, "si" }, } }, } ``` ![layout2](https://user-images.githubusercontent.com/88047141/183750135-6dbd39ac-2fd4-4c10-be5f-034c1966929f.png)

Statusline and winbar integration

When using split UI windows name can be displayed in statusline or in winbar. In each CompetiTest buffer there's a local variable called competitest_title, that is a string representing window name. You can get its value using nvim_buf_get_var(buffer_number, 'competitest_title').\ See the second screenshot for an example statusline used with split UI.

Highlights

You can customize CompetiTest highlight groups. Their default values are:

hi CompetiTestRunning cterm=bold     gui=bold
hi CompetiTestDone    cterm=none     gui=none
hi CompetiTestCorrect ctermfg=green  guifg=#00ff00
hi CompetiTestWarning ctermfg=yellow guifg=orange
hi CompetiTestWrong   ctermfg=red    guifg=#ff0000

Roadmap

Contributing

If you have any suggestion to give or if you encounter any trouble don't hesitate to open a new issue.\ Pull Requests are welcome! 🎉

License

GNU Lesser General Public License version 3 (LGPL v3) or, at your option, any later version

Copyright © 2021-2023 xeluxee

CompetiTest.nvim is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

CompetiTest.nvim is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License along with CompetiTest.nvim. If not, see https://www.gnu.org/licenses/.