├── 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 |
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 |
--------------------------------------------------------------------------------