├── README.md ├── lua └── codeforces-nvim │ ├── codeforces.lua │ ├── commands.lua │ ├── init.lua │ ├── setup.lua │ ├── user_commands.lua │ └── utils.lua ├── plugin └── codeforces.lua ├── stylua.toml └── tests └── codeforces.lua /README.md: -------------------------------------------------------------------------------- 1 | # codeforces-nvim 2 | 3 |

4 | 5 | Last commit 6 | 7 | 8 | License 9 | 10 | 11 | Stars 12 | 13 |

14 | 15 | 16 | https://github.com/user-attachments/assets/5d40ebe9-784f-4755-9af8-6de668ae0a52 17 | 18 | 19 | ## Installation 📦 20 | You can install `codeforces-nvim` using different package managers. 21 | 22 | --- 23 | 24 | ### [Lazy](https://github.com/folke/lazy.nvim) 💤 25 | ```lua 26 | local spec = { 27 | "yunusey/codeforces-nvim", 28 | dependencies = { "nvim-lua/plenary.nvim" } -- optional, used for testing 29 | } 30 | 31 | spec.config = function() 32 | require('codeforces-nvim').setup { 33 | use_term_toggle = true, 34 | cf_path = "/path/to/desired/codeforces/folder", 35 | timeout = 15000, 36 | compiler = { 37 | cpp = { "g++", "@.cpp", "-o", "@" }, 38 | py = {} 39 | }, 40 | run = { 41 | cpp = { "@" }, 42 | py = { "python3", "@.py" } 43 | }, 44 | notify = function(title, message, type) 45 | local notify = require('notify') 46 | if message == nil then 47 | notify(title, type, { 48 | render = "minimal", 49 | }) 50 | else 51 | notify(message, type, { 52 | title = title, 53 | }) 54 | end 55 | end 56 | } 57 | end 58 | 59 | return spec 60 | ``` 61 | 62 | ### [Packer](https://github.com/wbthomason/packer.nvim) 📦 63 | ```lua 64 | use { 65 | "yunusey/codeforces-nvim", 66 | config = function() 67 | require('codeforces-nvim').setup { 68 | use_term_toggle = true, 69 | cf_path = "/path/to/desired/codeforces/folder", 70 | compiler = { 71 | cpp = { "g++", "@.cpp", "-o", "@" }, 72 | py = {} 73 | }, 74 | run = { 75 | cpp = { "@" }, 76 | py = { "python3", "@.py" } 77 | }, 78 | notify = function(title, message, type) 79 | local notify = require('notify') 80 | if message == nil then 81 | notify(title, type, { 82 | render = "minimal", 83 | }) 84 | else 85 | notify(message, type, { 86 | title = title, 87 | }) 88 | end 89 | end 90 | } 91 | end 92 | } 93 | ``` 94 | 95 | 96 | ## Usage 📝 97 | Using this plugin is very easy, you just need to follow these steps: 98 | 99 | ### Installing [codeforces-extractor](https://github.com/yunusey/codeforces-extractor/) 🌻 100 | This tool is used to extract problem information from codeforces. It is written in Rust and is working pretty fast. You can use it as a separate tool and probably as a library with some modifications if you have other ideas. Anyway, for Neovim purposes, you just need to install the tool to your path. You can do this by running: 101 | ```bash 102 | cargo install --git https://github.com/yunusey/codeforces-extractor 103 | ``` 104 | If you are able to run `codeforces-extractor --help` without any errors, you are good to go (the plugin will check if `codeforces-extractor` is on your `PATH`). If you have done a local installation, you need to pass `extractor_path = /path/to/codeforces/extractor` to `setup` function you've seen above. 105 | 106 | ### Setting up Templates 🎨 107 | This step is very important! the `cf_path` you provided in `setup` will be used to check if you have any templates on `cf_path/contests/templates` directory. If you set your `extension` to be `cpp`, for instance, you need to have a file at `cf_path/contests/templates/template.cpp`. You can place multiple templates for different languages here if you are writing in many different languages and switching between them very often. 108 | 109 | ### Setting up the Plugin 🎉 110 | We are almost done! As you can also agree, you need to let the plugin know what command to run for compilation and running. By default, for `cpp`, which is the language most competitive programmers use, it will use the following compile command: 111 | ```lua 112 | { "g++", "@.cpp", "-o", "@" } 113 | ``` 114 | Here, `@` acts as the current problem (e.g. a, b, c1, c3, etc.). The default run command for `cpp` is: 115 | ```lua 116 | { "@" } 117 | ``` 118 | If you have custom languages that you would like to use, hopefully not Java `:)`, you can follow this example above to write similar compile and run commands. 119 | 120 | ### Ready to Go! 🚀 121 | Okay, you are ready to go! You need to start your journey by running `:EnterContest `. Here, `` refers to `https://codeforces.com/contest//problems`. For `https://codeforces.com/contest/1790/problems`, you need to run `:EnterContest 1790` for example. If everything goes nicely, this will fetch problems using `codeforces-extractor` to `cf_path/contests/tests/`, copy your template to `cf_path/contests/solutions/` for each problem and open up the first problem for you in your terminal. 122 | 123 | #### Testing your code 🧪 124 | There are three different ways to test your code. 125 | 126 | ##### Using the tests from codeforces 127 | You just need to run `:TestCurrent`. This will compile your code, run it by passing each of the inputs fetched from codeforces, and compare your output with the expected output. If they are not the same, it will open your output and expected output in `diff` mode in Neovim. 128 | 129 | ##### Using your own tests 130 | I added this feature so that if you have a custom test you want to run, you can do this one time and run it again and again. You need to run `:CreateTestCase` and it will open up a buffer. There, you need to type in your input and once you are done, you need to switch to normal mode and press enter. This will save your input to `cf_path/solutions//custom-.in` and run it on terminal. There, you can check your output. 131 | 132 | ##### Run directly on terminal 133 | Maybe you want to *freestyle* your input like you would've done normally. Then, you just need to run `:RunCurrent`. This will compile your code, and run it directly on your terminal. There, you can type in your input and check your output. 134 | 135 | ## Using [toggleterm.nvim](https://github.com/akinsho/toggleterm.nvim) ⚙️ 136 | I enjoy using toggleterm myself and would recommend to anyone interested. By default, the plugin will try running it using `:TermExec` but if you don't want this feature and want to run it directly on terminal, you need to set `use_term_toggle = false` on your setup. 137 | 138 | ## Using [notify.nvim](https://github.com/rcarriga/nvim-notify) 📢 139 | If you would like some fancy notifications from `codeforces-nvim`, you can use notify. To do that, you need to first install the plugin of course and then, you can copy my setup above, specifically this part: 140 | ```lua 141 | notify = function(title, message, type) 142 | local notify = require('notify') 143 | if message == nil then 144 | notify(title, type, { 145 | render = "minimal", 146 | }) 147 | else 148 | notify(message, type, { 149 | title = title, 150 | }) 151 | end 152 | end 153 | ``` 154 | There is a default `notify` function `codeforces-nvim` defines using `vim.notify`, though. So, this is completely up to you to decide which one you would prefer. Though, I strongly suggest using [notify.nvim](https://github.com/rcarriga/nvim-notify). 155 | 156 | ## Timeout for Running Code 🕒 157 | There is a `timeout` parameter you can override in your `setup` function. This is passed directly to your `run` command. I think this can be useful especially when you expect more input than there actually is supposed to be. This value is set to `15000` milliseconds by default. If you think this is too much, I think so, you can most definitely change this to a more reasonable value like `5000` milliseconds. 158 | 159 | ## Thanks! ✨ 160 | Thanks for reading! If you liked the plugin, you can consider leaving a star. If you encounter any issues or have a good idea to enhance the plugin, make sure to [open up an issue](https://github.com/yunusey/codeforces-nvim/issues). 161 | 162 | ## Acknowledgements 🏆 163 | I've recently seen that there is a new plugin that is getting popular. I think you should most definitely check it out as well - I didn't really get a chance to check it in detail, but it looks like it supports codeforces as well: [assistant.nvim](https://github.com/A7Lavinraj/assistant.nvim) 164 | 165 | ## Credits 🎖️ 166 | - [codeforces-extractor](https://github.com/yunusey/codeforces-extractor/) 167 | - [notify.nvim](https://github.com/rcarriga/nvim-notify/) 168 | - [toggleterm.nvim](https://github.com/akinsho/toggleterm.nvim/) 169 | - [lazy.nvim](https://github.com/folke/lazy.nvim) 170 | - [packer.nvim](https://github.com/wbthomason/packer.nvim) 171 | - [plenary.nvim](https://github.com/nvim-lua/plenary.nvim) 172 | - [assistant.nvim](https://github.com/A7Lavinraj/assistant.nvim) 173 | - [Neovim](https://neovim.io/) 174 | - [Lua](https://www.lua.org/) 175 | - [Codeforces](https://codeforces.com/) 176 | -------------------------------------------------------------------------------- /lua/codeforces-nvim/codeforces.lua: -------------------------------------------------------------------------------- 1 | local M = require("codeforces-nvim.setup") 2 | local utils = require("codeforces-nvim.utils") 3 | 4 | --- @type string | nil 5 | M.current_contest = nil 6 | --- @type integer 7 | M.current_problem = 0 8 | --- @type string[] 9 | M.problems = {} 10 | 11 | --- @param problem string 12 | --- @param extension string | nil 13 | --- @return string 14 | --- Returns the file path of the solution to the problem with the given `extension` 15 | --- if the `extension` is `nil`, the file path is returned without **any** extension 16 | --- which can be used for executables (e.g. `*.cpp` -> `*`) 17 | M.get_solution_file = function(problem, extension) 18 | local filename = string.lower(problem) 19 | if extension ~= nil then filename = filename .. "." .. extension end 20 | return vim.fs.joinpath(M.paths.solutions, M.current_contest, filename) 21 | end 22 | 23 | --- @param problem string 24 | --- @param test_name string 25 | --- @param extension string | nil 26 | --- @return string 27 | --- Returns the file path of the test case: `/path/to/contest///` 28 | --- Then, the user just needs to add the appropriate extension (`.in` or `.out`) 29 | M.get_test_file = function(problem, test_name, extension) 30 | local filename = test_name 31 | if extension ~= nil then filename = filename .. extension end 32 | return vim.fs.joinpath(M.paths.tests, M.current_contest, problem, filename) 33 | end 34 | 35 | --- @param cont string | nil 36 | --- Fetches the `cont` contest and sets the `current_contest` 37 | M.enter_contest = function(cont) 38 | M.current_contest = cont 39 | if M.current_contest == nil or M.current_contest == "" then 40 | M.current_contest = vim.fn.input({ 41 | prompt = "Please enter the contest: ", 42 | default = "17", 43 | }) 44 | end 45 | 46 | local test_directory = vim.fs.joinpath(M.paths.tests, M.current_contest) 47 | local found_contest = vim.fn.isdirectory(test_directory) == 1 48 | 49 | local exit_function = function() 50 | M.create_contest(M.current_contest) 51 | M.current_problem = 1 52 | 53 | if #M.problems == 0 then return end 54 | 55 | local solution_file = M.get_solution_file(M.problems[M.current_problem], M.options.extension) 56 | vim.fn.chdir(vim.fs.joinpath(M.paths.solutions, M.current_contest)) 57 | vim.cmd(":tabnew " .. solution_file) 58 | vim.api.nvim_win_set_cursor(0, { M.options.lines[M.options.extension], 0 }) 59 | end 60 | 61 | if found_contest == false then 62 | M.fetch_problems(M.current_contest, test_directory, exit_function) 63 | else 64 | exit_function() 65 | end 66 | end 67 | 68 | --- @param contest string 69 | --- @param save_dir string 70 | --- @param exit_function function 71 | --- Fetches the problems for the `contest` using `codeforces-extractor` 72 | --- and calls the `exit_function` 73 | M.fetch_problems = function(contest, save_dir, exit_function) 74 | vim.fn.jobstart({ M.options.extractor_path, contest, "--save-path", save_dir }, { 75 | on_stdout = function(_, data) end, 76 | on_stderr = function(_, data) 77 | if utils.check_data(data) == false then return end 78 | M.options.notify("Codeforces Extractor", vim.inspect(data), "error") 79 | end, 80 | on_exit = function(_, _) 81 | exit_function() 82 | end, 83 | }) 84 | end 85 | 86 | --- @param contest string 87 | --- Creates the corresponding folder for the current `contest`, copies the 88 | --- template files accordingly and sets the `problems` variable for further use 89 | M.create_contest = function(contest) 90 | local template_path = 91 | vim.fs.joinpath(M.paths.templates, string.format("template.%s", M.options.extension)) 92 | local sol_folder = vim.fs.joinpath(M.paths.solutions, contest) 93 | local test_folder = vim.fs.joinpath(M.paths.tests, contest) 94 | 95 | if vim.fn.filereadable(template_path) == 0 then 96 | M.options.notify( 97 | string.format("Template for `%s` language not found", M.options.extension), 98 | string.format("Please add a template for the current language at: %s", template_path), 99 | "error" 100 | ) 101 | return 102 | end 103 | 104 | vim.fn.mkdir(sol_folder, "p") 105 | 106 | local problems = {} 107 | for problem in vim.fs.dir(test_folder) do 108 | table.insert(problems, problem) 109 | local solution_path = M.get_solution_file(problem, M.options.extension) 110 | if vim.fn.filereadable(solution_path) == 0 then utils.copy_file(template_path, solution_path) end 111 | end 112 | 113 | M.problems = problems 114 | end 115 | 116 | --- @return string 117 | --- Returns the id of the current contest 118 | M.get_current_contest = function() 119 | return M.current_contest 120 | end 121 | 122 | return M 123 | -------------------------------------------------------------------------------- /lua/codeforces-nvim/commands.lua: -------------------------------------------------------------------------------- 1 | local codeforces = require("codeforces-nvim.codeforces") 2 | local utils = require("codeforces-nvim.utils") 3 | 4 | local M = {} 5 | 6 | --- @param problem string 7 | --- @return string[] 8 | --- Returns the command to compile the given `problem` 9 | local get_compile_command = function(problem) 10 | local executable = codeforces.get_solution_file(problem, nil) 11 | local command = codeforces.options.compiler[codeforces.options.extension] 12 | local new_command = {} 13 | 14 | for _, i in ipairs(command) do 15 | local modified = string.gsub(i, "@", executable) 16 | table.insert(new_command, modified) 17 | end 18 | 19 | return new_command 20 | end 21 | 22 | --- @param problem string 23 | --- @return string[] 24 | --- Returns the command to run the given `problem` 25 | local get_run_command = function(problem) 26 | local executable = codeforces.get_solution_file(problem, nil) 27 | local command = codeforces.options.run[codeforces.options.extension] 28 | local new_command = {} 29 | 30 | for _, i in ipairs(command) do 31 | local modified = string.gsub(i, "@", executable) 32 | table.insert(new_command, modified) 33 | end 34 | return new_command 35 | end 36 | 37 | --- @param problem string 38 | --- @return string 39 | --- @see get_run_command 40 | --- Returns the command to run the given `problem`. Unlike `get_run_command`, 41 | --- this one returns a concatenated `string` since it runs directly in terminal 42 | local get_terminal_run_command = function(problem) 43 | local executable = codeforces.get_solution_file(problem, nil) 44 | local command = codeforces.options.run[codeforces.options.extension] 45 | local new_command = table.concat(command, " "):gsub("@", executable) 46 | return new_command 47 | end 48 | 49 | --- @param compiler string[] 50 | --- @see get_compile_command 51 | --- Using the compile command generated by `get_compile_command`, compile the given `problem` 52 | --- and call the `exit_function` once done 53 | M.compile = function(compiler, exit_function) 54 | if compiler == nil or compiler == {} then return end 55 | 56 | local errors = false 57 | local all_data = {} 58 | 59 | vim.fn.jobstart(compiler, { 60 | on_stderr = function(_, data) 61 | if utils.check_data(data) == false then return end 62 | errors = true 63 | for _, i in pairs(data) do 64 | table.insert(all_data, i) 65 | end 66 | end, 67 | on_exit = function(_, _) 68 | if errors then 69 | codeforces.options.notify("Compilation Failed ✗", table.concat(all_data, "\n"), "error") 70 | else 71 | codeforces.options.notify("Compilation Succeeded ✓", nil, "success") 72 | exit_function() 73 | end 74 | end, 75 | }) 76 | end 77 | 78 | --- @param errors table[] 79 | --- This function aims to open a `diffview` window in Neovim that shows 80 | --- the differences between the `user_output` and `output` 81 | M.handle_differences = function(errors) 82 | local user_output_content = {} 83 | local expected_output_content = {} 84 | for _, i in pairs(errors) do 85 | local test_case = i[1] 86 | table.insert(user_output_content, "Test Case: " .. test_case) 87 | table.insert(expected_output_content, "Test Case: " .. test_case) 88 | 89 | local user_output = i[2] 90 | local expected_output = i[3] 91 | for _, j in pairs(user_output) do 92 | table.insert(user_output_content, j) 93 | end 94 | for _, j in pairs(expected_output) do 95 | table.insert(expected_output_content, j) 96 | end 97 | end 98 | local user_output_file = vim.fn.tempname() 99 | local expected_output_file = vim.fn.tempname() 100 | vim.fn.writefile(user_output_content, user_output_file) 101 | vim.fn.writefile(expected_output_content, expected_output_file) 102 | 103 | vim.cmd(":tabnew " .. user_output_file) 104 | vim.cmd(":vert diffsplit " .. expected_output_file) 105 | end 106 | 107 | --- @param problem string 108 | --- Using the run command generated by `get_run_command`, run the given `problem` 109 | --- for each test case and check if the answer is correct 110 | M.test_problem = function(problem) 111 | local compiler = get_compile_command(problem) 112 | local run = function() 113 | local test_path = vim.fs.joinpath(codeforces.paths.tests, codeforces.get_current_contest(), problem) 114 | local errors = {} 115 | for test_case in vim.fs.dir(test_path) do 116 | -- we don't want to go through the same test case twice. 117 | if test_case:match("%.out$") then goto continue end 118 | 119 | test_case = utils.trim_extension(test_case) 120 | local output_path = codeforces.get_test_file(problem, test_case, ".out") 121 | local input_path = codeforces.get_test_file(problem, test_case, ".in") 122 | 123 | local run_command = get_run_command(problem) 124 | local output = vim.system(run_command, { 125 | stdin = vim.fn.readfile(input_path), 126 | timeout = codeforces.timeout, -- 15 seconds 127 | }):wait() 128 | 129 | local user_output = vim.fn.split(output.stdout, "\n") 130 | local expected_output = vim.fn.readfile(output_path) 131 | 132 | local same = utils.compare(user_output, expected_output) 133 | local message = "" 134 | local type = nil 135 | if same == true then 136 | message = "Test Case #" .. test_case .. " success ✓" 137 | type = "success" 138 | else 139 | table.insert(errors, { test_case, user_output, expected_output }) 140 | message = "Test Case #" .. test_case .. " failed x" 141 | type = "error" 142 | end 143 | 144 | codeforces.options.notify(message, nil, type) 145 | 146 | ::continue:: 147 | end 148 | 149 | if not vim.tbl_isempty(errors) then 150 | M.handle_differences(errors) 151 | else 152 | codeforces.options.notify("All tests passed!", nil, "success") 153 | end 154 | end 155 | 156 | if utils.check_data(compiler) then 157 | M.compile(compiler, run) 158 | else 159 | run() 160 | end 161 | end 162 | 163 | --- @param problem string 164 | --- @see get_terminal_run_command 165 | --- Using the run command generated by `get_terminal_run_command`, run the given `problem` in a terminal 166 | M.run_normally = function(problem) 167 | local compiler = get_compile_command(problem) 168 | local run = function() 169 | local run_command = get_terminal_run_command(problem) 170 | local cmd = string.format(':TermExec cmd="%s"', run_command) 171 | if codeforces.options.use_term_toggle == true then 172 | vim.cmd(cmd) 173 | else 174 | vim.cmd(":terminal ") 175 | vim.cmd(":set number!") 176 | vim.cmd(":set relativenumber!") 177 | vim.api.nvim_paste(run_command, true, 3) 178 | end 179 | end 180 | 181 | if utils.check_data(compiler) then 182 | M.compile(compiler, run) 183 | else 184 | run() 185 | end 186 | end 187 | 188 | --- @param problem string 189 | --- Opens up a new buffer for the user to write a custom test case 190 | M.create_custom_test_case = function(problem) 191 | local buffer = vim.api.nvim_create_buf(true, true) 192 | vim.api.nvim_buf_set_name(buffer, "Your Test Case For Problem #" .. problem) 193 | 194 | local x, y = 195 | math.ceil(vim.api.nvim_win_get_height(0) / 2 - 10), math.ceil(vim.api.nvim_win_get_width(0) / 2 - 50) 196 | local win = vim.api.nvim_open_win( 197 | buffer, 198 | true, 199 | { relative = "editor", width = 100, height = 20, row = x, col = y, anchor = "NW", border = "rounded" } 200 | ) 201 | 202 | vim.api.nvim_buf_set_keymap(buffer, "n", "", "", { 203 | callback = function() 204 | M.save_custom_test(problem, buffer, win) 205 | M.run_custom_test(problem) 206 | end, 207 | }) 208 | end 209 | 210 | --- @param problem string 211 | --- @param buf number 212 | --- @param win number 213 | --- Saves the custom test case written by the user 214 | M.save_custom_test = function(problem, buf, win) 215 | local input = vim.api.nvim_buf_get_lines(buf, 0, -1, false) 216 | local file_path = vim.fs.joinpath( 217 | codeforces.paths.solutions, 218 | codeforces.get_current_contest(), 219 | string.format("custom-%s.in", problem) 220 | ) 221 | vim.fn.writefile(input, file_path) 222 | vim.api.nvim_win_close(win, true) 223 | vim.api.nvim_buf_delete(buf, { force = true }) 224 | end 225 | 226 | --- @param problem string 227 | --- @see get_run_command 228 | --- Using the run command generated by `get_run_command`, run the given `problem` with the generated 229 | --- custom test case 230 | M.run_custom_test = function(problem) 231 | local file_path = vim.fs.joinpath( 232 | codeforces.paths.solutions, 233 | codeforces.get_current_contest(), 234 | string.format("custom-%s.in", problem) 235 | ) 236 | local command = string.format('"%s < %s"', table.concat(get_run_command(problem)), file_path) 237 | if codeforces.options.use_term_toggle == true then 238 | vim.cmd(":TermExec cmd=" .. command) 239 | else 240 | vim.cmd(":terminal ") 241 | vim.cmd(":set number!") 242 | vim.cmd(":set relativenumber!") 243 | vim.api.nvim_paste(command, true, 3) 244 | end 245 | end 246 | 247 | return M 248 | -------------------------------------------------------------------------------- /lua/codeforces-nvim/init.lua: -------------------------------------------------------------------------------- 1 | local codeforces = require("codeforces-nvim.codeforces") 2 | local setup = require("codeforces-nvim.setup") 3 | local user_commands = require("codeforces-nvim.user_commands") 4 | 5 | local M = { 6 | user_commands = user_commands.user_commands, 7 | enter_contest = codeforces.enter_contest, 8 | get_current_contest = codeforces.get_current_contest, 9 | get_test = codeforces.get_test, 10 | } 11 | 12 | for _, i in ipairs(setup) do 13 | M[i] = setup[i] 14 | end 15 | M.setup = setup.setup 16 | 17 | return M 18 | -------------------------------------------------------------------------------- /lua/codeforces-nvim/setup.lua: -------------------------------------------------------------------------------- 1 | local M = { 2 | options = { 3 | --- @type string 4 | --- The path to the Codeforces directory 5 | cf_path = vim.fs.joinpath(vim.fn.getenv("HOME"), "codeforces"), 6 | 7 | --- @type string 8 | --- The path to the Codeforces extractor directory (default: `codeforces-extractor`) 9 | --- You don't need to modify this if it is on your `PATH` 10 | extractor_path = "codeforces-extractor", 11 | 12 | --- @type string 13 | --- The extension of the language you would like to use 14 | extension = "cpp", 15 | 16 | --- @type table 17 | --- When opened a new tab with the current problem, it will place your cursor 18 | --- at this line - useful if you have a ton of functions above your main method 19 | lines = { 20 | cpp = 6, 21 | py = 3, 22 | }, 23 | 24 | --- @type integer 25 | --- The timeout for the run command (in milliseconds) 26 | timeout = 15000, 27 | 28 | --- @type table 29 | --- The commands to compile and run the given problem 30 | --- You can use `@` as a placeholder for the current problem 31 | compiler = { 32 | py = {}, 33 | cpp = { "g++", "@.cpp", "-o", "@" }, 34 | }, 35 | 36 | --- @type table 37 | --- The run command to run for each language. It will treat `@` symbols 38 | --- as placeholders for the current problem. The program will pass in the 39 | --- input file as stdin - don't worry about it :D 40 | run = { 41 | py = { "python3", "@.py" }, 42 | cpp = { "@" }, 43 | }, 44 | 45 | --- @type boolean 46 | --- Whether to use [toggleterm.nvim](https://github.com/akinsho/toggleterm.nvim) 47 | --- Highly recommended 48 | use_term_toggle = true, 49 | --- @param title string 50 | --- @param message string | nil 51 | --- @param type "success" | "error" 52 | --- Notification function. Uses `vim.print` and sets log level to 53 | --- `vim.log.levels.WARN` for "success" and `vim.log.levels.ERROR` for "error" 54 | --- I recommend using [nvim-notify](https://github.com/rcarriga/nvim-notify) 55 | --- and maybe use a function like this: 56 | --- ```lua 57 | --- function (title, message, type) 58 | --- if message == nil then 59 | --- vim.notify(title, type, { 60 | --- render = "minimal", 61 | --- }) 62 | --- else 63 | --- vim.notify(message, type, { 64 | --- title = title, 65 | --- }) 66 | --- end 67 | --- end 68 | --- ``` 69 | notify = function(title, message, type) 70 | local log_level = type == "success" and vim.log.levels.WARN or vim.log.levels.ERROR 71 | message = title .. (message ~= nil and "\n" .. message or "") 72 | vim.notify(message, log_level) 73 | end, 74 | }, 75 | paths = { 76 | contests = nil, 77 | tests = nil, 78 | solutions = nil, 79 | templates = nil, 80 | }, 81 | } 82 | 83 | --- @param cf_path string | nil 84 | --- Sets the paths. If no path provided as `cf_path`, it will use the default path (`$HOME/codeforces/contests`) 85 | local setup_paths = function(cf_path) 86 | M.paths.contests = vim.fs.joinpath(cf_path, "contests") 87 | M.paths.tests = vim.fs.joinpath(M.paths.contests, "test") 88 | M.paths.solutions = vim.fs.joinpath(M.paths.contests, "solutions") 89 | M.paths.templates = vim.fs.joinpath(M.paths.contests, "templates") 90 | 91 | vim.fn.mkdir(cf_path, "p") 92 | vim.fn.mkdir(M.paths.contests, "p") 93 | vim.fn.mkdir(M.paths.tests, "p") 94 | vim.fn.mkdir(M.paths.solutions, "p") 95 | vim.fn.mkdir(M.paths.templates, "p") 96 | end 97 | 98 | --- @param config table 99 | --- Setup function 100 | M.setup = function(config) 101 | M.options = vim.tbl_deep_extend("force", M.options, config or {}) 102 | 103 | if M.options.extractor_path == nil or vim.fn.executable(M.options.extractor_path) == 0 then 104 | M.options.notify( 105 | "Codeforces Extractor", 106 | "Hi! It looks like you didn't install codeforces-extractor. Please follow the installation instructions here: https://github.com/yunusey/codeforces-extractor", 107 | "error" 108 | ) 109 | end 110 | 111 | setup_paths(M.options.cf_path) 112 | end 113 | 114 | return M 115 | -------------------------------------------------------------------------------- /lua/codeforces-nvim/user_commands.lua: -------------------------------------------------------------------------------- 1 | local codeforces = require("codeforces-nvim.codeforces") 2 | local commands = require("codeforces-nvim.commands") 3 | 4 | local M = {} 5 | 6 | --- @param jump integer 7 | --- Jumps to the next `jump`-th question - most of the time, jump = 1 (if called `:QNext`) 8 | M.next_question = function(jump) 9 | -- Lua is 1-indexed :D 10 | codeforces.current_problem = (codeforces.current_problem + jump - 1) % #codeforces.problems + 1 11 | local solution_file = codeforces.get_solution_file( 12 | codeforces.problems[codeforces.current_problem], 13 | codeforces.options.extension 14 | ) 15 | vim.cmd(":tabnew" .. solution_file) 16 | vim.api.nvim_win_set_cursor(0, { codeforces.options.lines[codeforces.options.extension], 0 }) 17 | end 18 | 19 | --- Creates all the user commands used by the plugin 20 | M.user_commands = function() 21 | vim.api.nvim_create_user_command("EnterContest", function(args) 22 | local contest = args["args"] 23 | codeforces.enter_contest(contest) 24 | end, { nargs = "?" }) 25 | vim.api.nvim_create_user_command("QNext", function(args) 26 | local jump = args["args"] 27 | if jump == nil or jump == "" then 28 | M.next_question(1) 29 | else 30 | M.next_question(jump) 31 | end 32 | end, { nargs = "?" }) 33 | vim.api.nvim_create_user_command("TestCurrent", function() 34 | commands.test_problem(codeforces.problems[codeforces.current_problem]) 35 | end, {}) 36 | vim.api.nvim_create_user_command("RunCurrent", function() 37 | commands.run_normally(codeforces.problems[codeforces.current_problem]) 38 | end, {}) 39 | vim.api.nvim_create_user_command("CreateTestCase", function() 40 | commands.create_custom_test_case(codeforces.problems[codeforces.current_problem]) 41 | end, {}) 42 | vim.api.nvim_create_user_command("RetrieveLastTestCase", function() 43 | commands.run_custom_test(codeforces.problems[codeforces.current_problem]) 44 | end, {}) 45 | end 46 | 47 | return M 48 | -------------------------------------------------------------------------------- /lua/codeforces-nvim/utils.lua: -------------------------------------------------------------------------------- 1 | --- @module codeforces-nvim.utils 2 | --- Utility functions 3 | local M = {} 4 | 5 | --- @param filename string 6 | --- @return string 7 | --- Returns the filename without the extension 8 | M.trim_extension = function(filename) 9 | return filename:match("(.+)%..+$") or filename 10 | end 11 | 12 | --- @param str string 13 | --- @return string 14 | --- Returns the string without the spaces in the beginning and the end 15 | --- e.g. ` hello ` -> `hello` 16 | M.trim = function(str) 17 | return (str:gsub("^%s*(.-)%s*$", "%1")) 18 | end 19 | 20 | M.getRidOfSpaces = function(a) 21 | -- This function's goal is to get rid of the spaces in the beginning and the end of each line. 22 | 23 | local i = 1 24 | while true do 25 | -- If there's a character that is a non-whitespaces character, then you found the beginning. 26 | local s = string.match(string.sub(a, i, i), "%S") 27 | if s ~= nil then break end 28 | i = i + 1 29 | end 30 | 31 | local j = #a 32 | while true do 33 | -- If there's a character that is a non-whitespaces character, then you found the end. 34 | local s = string.match(string.sub(a, j, j), "%S") 35 | if s ~= nil then break end 36 | j = j - 1 37 | end 38 | 39 | return string.sub(a, i, j) 40 | end 41 | 42 | --- @param data string[] 43 | --- @return boolean 44 | --- Returns `true` if the `data` is valid 45 | --- Sometimes, the data produced by `vim.fn.jobstart` can be 46 | --- empty (e.g. `{ "" }`) and it can be challenging to check if there is anything 47 | --- important in it. This function aims to iterate over the `data` and check 48 | --- if there is anything important in it 49 | M.check_data = function(data) 50 | if data == nil or data == {} or data == "" then return false end 51 | 52 | if type(data) == "table" then 53 | for _, i in pairs(data) do 54 | if i ~= nil and i ~= {} and i ~= "" then return true end 55 | end 56 | return false 57 | elseif type(data) == "string" then 58 | return data ~= string.match(data, "%s+") 59 | end 60 | 61 | return true 62 | end 63 | 64 | --- @param source string 65 | --- @param destination string 66 | --- Copies the file from `source` to `destination` 67 | M.copy_file = function(source, destination) 68 | local file = io.open(destination, "w") 69 | if file ~= nil then 70 | for i in io.lines(source) do 71 | file:write(i .. "\n") 72 | end 73 | file:close() 74 | end 75 | end 76 | 77 | --- @param lines string[] 78 | --- @param output_lines string[] 79 | --- @return boolean 80 | --- Returns `true` if the `lines` and `output_lines` are the same 81 | --- **NOTE**: Please notice that this is merely a very basic comparison. 82 | --- It will only check if the lines (after trimming the spaces only from 83 | --- the beginning and the end) are the same. There might be cases where 84 | --- the spaces between two elements do not matter. You will need to decide 85 | --- it in `diffview`. Also, it will ignore any empty lines 86 | M.compare = function(lines, output_lines) 87 | local i = 1 88 | local j = 1 89 | while i <= #lines or j <= #output_lines do 90 | while i <= #lines and M.trim(lines[i]) == "" do 91 | i = i + 1 92 | end 93 | while j <= #output_lines and M.trim(output_lines[j]) == "" do 94 | j = j + 1 95 | end 96 | 97 | if i > #lines or j > #output_lines then 98 | return i > #lines and j > #output_lines 99 | end 100 | 101 | local lhs, rhs = M.trim(lines[i]), M.trim(output_lines[j]) 102 | if lhs ~= rhs then 103 | return false 104 | end 105 | 106 | i = i + 1 107 | j = j + 1 108 | end 109 | return true 110 | end 111 | 112 | return M 113 | -------------------------------------------------------------------------------- /plugin/codeforces.lua: -------------------------------------------------------------------------------- 1 | --- This file just loads the plugin - executed by Lazy, Packer, etc. 2 | 3 | local codeforces = require("codeforces-nvim") 4 | codeforces.user_commands() -- load the user commands (`EnterContest`, etc.) 5 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | call_parentheses = "Always" 2 | collapse_simple_statement = "ConditionalOnly" 3 | column_width = 110 4 | indent_type = "Spaces" 5 | indent_width = 4 6 | line_endings = "Unix" 7 | quote_style = "AutoPreferDouble" 8 | 9 | [sort_requires] 10 | enabled = true 11 | -------------------------------------------------------------------------------- /tests/codeforces.lua: -------------------------------------------------------------------------------- 1 | local codeforces = require("codeforces-nvim.codeforces") 2 | 3 | describe("fetch_problems", function() 4 | it("should fetch problems and save them", function() 5 | local co = coroutine.running() 6 | local save_dir = "/tmp/test" -- TODO: find a better way 7 | codeforces.fetch_problems("1790", save_dir, function() 8 | coroutine.resume(co, 0) 9 | end) 10 | 11 | local exit_code = coroutine.yield() 12 | assert(exit_code == 0) 13 | 14 | local problems = {} 15 | for i in vim.fs.dir(save_dir) do 16 | table.insert(problems, i) 17 | end 18 | assert.same({ "A", "B", "C", "D", "E", "F", "G" }, problems) 19 | 20 | -- FIX: Find a better way 21 | vim.system({ "rm", "-rf", save_dir }) 22 | end) 23 | end) 24 | --------------------------------------------------------------------------------