https://github.com/user-attachments/assets/7f2523ab-8bc1-46eb-9e54-11b80cae18d1
tuna.nvim is a competitive programming plugin that integrates with Competitive Companion to download contests, runs your solution against testcases, supports stress testing, and much more.
With lazy.nvim:
{
"FrancescoDerme/tuna.nvim",
config = function()
require("tuna").setup({})
end,
}
With vim.pack:
vim.pack.add({ "https://github.com/FrancescoDerme/tuna.nvim" })
require("tuna").setup({})
:Tuna opens the menu.:Tuna download problem starts listening for a problem from Competitive Companion: press the green plus in your browser to download it.
A solution file is created from your template_file (once you set one), the testcases are written beside it, and the file opens.:Tuna run compiles, runs the testcases in parallel, and opens the runner UI.
The four panes show output, expected output, input and stderr.
This UI can do a lot, for example d toggles a diff of the two outputs.:Tuna submit hands the solution to whichever submit tool you have configured, reads that tool's output as it runs, and puts the judge's verdict in your statusline.:Tuna pin and :Tuna last help you move between problems as you upsolve, and :Tuna clean keeps your directories tidy.You can find a complete list of the commands in REFERENCE.md, along with a detailed explanation of how each one works. Here we aim to describe the most important features along with some basic workflows.
Testcases live on disk beside your solution, as a pair of text files each by default.
The file-name format in which testcases are written and discovered can be set in testcases_input_file_format, which accepts modifiers such as $(FNOEXT) and $(TCNUM).
It is a list of names: writing a testcase uses the first entry in the list, and discovering one tries the entries in order, the first that finds anything wins:
testcases_input_file_format = { "$(FNOEXT)_input$(TCNUM).txt", "input$(TCNUM).txt", "in.txt" }
Where the testcases are stored can be set in testcases_directory: it's relative to the source by default, but can be an absolute path unrelated to the problem.
Add, edit, and delete testcases with :Tuna testcase add|edit|delete.
This can also be done in place in the runner UI, which is usually where you want to be.
:Tuna testcase split breaks one testcase into several: mark the cases inside it with a line of -, and each bracketed region becomes a testcase of its own.
:Tuna run compiles (if necessary), runs every testcase in parallel (as many at once as you have cores, by default) and opens the runner UI.
You can also open the runner UI by :Tuna show_ui.
The whole grid is defined in popup_ui.layout, you can arrange it differently or move to runner_ui.interface = "split" for real windows instead of floats.
Each testcase is a row, and the compile step is a row too that shows warnings and errors if there are any, customizable at runner_ui.compile_layout.
The runner UI can do a lot, but it aims to stay intuitive by aligning to Neovim's defaults: the panes with a purple border are editable, and keys behave in them as in any buffer. From the read-only panes you can:
d,r and <C-r>,n and x,Each pane opens full-screen with the key in the pane's title, and <C-hjkl> move you between panes, also customizable.
Comparison between Output and Expected Output is driven by output_compare_method: "exact", "squish" (whitespace-insensitive, the default), { "float", tol = 1e-6 } for problems with a tolerance, or a method of your own in compare_methods.
:Tuna compare <method> overrides it for one problem.
:Tuna run can do four different things based on helper programs discovered by filename beside your solution.
| you write | :Tuna run becomes | what happens |
|---|---|---|
| nothing | normal | run your solution against every testcase |
gen.* + brute.* | stress test | generate an input, run your solution and the bruteforce, compare the outputs, repeat until they disagree and save the counterexample |
interactor.* | interactive | run your solution against the interactor |
checker.* | (any of the above) | a special judge decides correctness instead of string comparison |
Interactive mode is not tied to having an interactor present, in fact it has three sources that can provide the input: live (you), interactor (your interactor program) and feed (a stored testcase).
Every helper is found the same way: a file named after it beside your solution (with the names in the table above or those specified in tool_names), or one of your own that you point to with its option (checker, stress.generator, stress.bruteforce or interactive.interactor), which can be specified in a .tuna.lua file beside the solution.
The run mode, interactive's source and the checker are decided based on the helpers present until you force them: :Tuna run <mode> forces a mode, :Tuna run interactive <source> a source, :Tuna checker off plain comparison, and auto hands any of them back.
:Tuna scaffold checker|generator|bruteforce|interactor drops in a starter file, named as tool_names says, in your solution's language, or in the language configured at scaffold.language (for example, you may want to have a py bruteforce even if you are coding in cpp).
tuna ships cpp and py starters in its scaffolds/ folder, and a file of the same name in your scaffold.directory (by default tuna/scaffolds in your Neovim config) is used instead, so you can customize a starter or add a new language by simply dropping a file there.
Finally, when several solutions are present in one folder, :Tuna run all runs every one of them against the same testcases.
With Competitive Companion installed, :Tuna download problem (or contest, or testcases) opens a listener, and pressing the green plus in the browser does the rest.
:Tuna download persistently leaves the listener open for a whole session.
Where things land is decided by a path template, evaluated per problem:
downloaded_problems_path = "$(HOME)/cp/$(JUDGE)/$(PROBLEM)/main.$(FEXT)",
downloaded_contests_directory = "$(HOME)/cp/$(JUDGE)/$(CONTEST)",
$(JUDGE) and $(CONTEST) come from tuna's own parsing of what Competitive Companion sends, and you can override it per judge with judge_parsers.
Codeforces and AtCoder are normalized out of the box, including Codeforces' mirror hosts: tuna rewrites the URL to the main site so links still work after the contest is over and keeps the mirror for submitting while the round is live.
New files are created from template_file, with the problem's own details filled in for these placeholders:
// judge: $(JUDGE)
// contest: $(CONTEST)
// problem: $(PROBLEM)
// submit at: $(URL)
The template_file option takes a single path, a per-extension table, or an ordered list tried until one exists, which is what makes a per-judge template practical:
template_file = { "~/cp/template.$(JUDGE).cpp", "~/cp/template.cpp" },
When entering a contest, some judges, like AtCoder, place you in the problems page, from which pressing the green plus is convenient.
Others, like Codeforces, place you inside the first problem, so going to the problems page to download the contest at the start of the competition would lose precious seconds.
To solve this, when dealing with judges of the second type, you can create a scratch file via :Tuna scratch.
A scratch file also follows your template, and you can solve and submit the easiest problem of the contest in it (there are no testcases for tuna to run, and submitting won't work unless you add the problem's URL in a // submit at: header line, which defeats the purpose of being fast).
After having solved this problem, you can download the rest of the contest with :Tuna download sync, which folds your scratch into the first problem's file.
:Tuna submit hands the solution to a submit tool of your choosing and reads its output as it runs, so the judge's verdict arrives in your statusline:
submit = {
command = 'cf submit "$(URL)" "$(LANG)" "$(FABSPATH)"',
languages = { cpp = "C++", python = "Python 3" },
},
A downloaded problem knows the correct URL to submit at (even without the header line), while a hand-made one becomes submittable the moment you add a // submit at: line with the correct URL into its header.
submit.judges.<judge> overrides the submission tool per judge.
The browser provider exists for judges for which you don't have a submission tool: it opens the submit page with the task preselected and puts your source on the clipboard.
For a working setup of a submit tool see here.
In this example, the submission tool is submitter.
A step-by-step guide on setting this up, from pointing tuna at your tool to reading the verdict, is in the reference.
:Tuna next / :Tuna prev step between the problems of a contest.:Tuna last problem / :Tuna last contest go back to what you were working on.
The menu also provides access to your recent contests and problems.:Tuna pin puts the problem you are on aside to solve later, and pinning it again lets it go.
The menu provides access to your pinned problems.:Tuna clean removes files you created and never used (or the ones you only slightly modified, with a tolerance the command asks for when you run it), and then the directories they leave empty.
Every deletion is confirmed one at a time, with the file in front of you.
:Tuna lib copies a piece of your own library into the file you are writing.
The library is plain source files in the folder library.path points at, with the parts worth copying marked in place:
// TUNALIB: binary exp start
long long bexp(long long b, long long e, long long m) { ... }
// TUNALIB: binary exp end
Only files matching the current buffer's extension are offered.
:Tuna lib snippet lists every snippet at once, :Tuna lib search opens the same catalogue in telescope.
Keymaps make the tuna experience much better.
You can opt in to the default keymaps with one line: keymaps = { preset = "<leader>t" } gives <leader>tr run, <leader>tu runner UI, <leader>ts submit, <leader>tn/<leader>tp problem navigation, <leader>tdc download a contest and <leader>tds sync, <leader>tf pin, <leader>tl the library, <leader>tw the scratch, <leader>tm the menu, and more.
You can edit or drop any of them without giving up the rest.
Two optional lualine components can make for a better experience: the download listener indicator and the current problem's submit verdict.
Every tuna window has the filetype tuna.
Listing it in lualine's ignore_focus keeps the statusline describing the file underneath while a tuna window has focus.
require("lualine").setup({
options = {
ignore_focus = { "tuna" },
},
sections = {
lualine_x = {
{
function() return require("tuna").lualine_component() end,
cond = function() return require("tuna.download").is_downloading() end,
},
{
function() return require("tuna.submit").status() end,
cond = function() return require("tuna.submit").is_submitting() end,
color = function() return require("tuna.submit").status_hl() end,
},
},
},
})
Everything is optional: setup() takes a table shaped like the defaults, and anything you leave out keeps its default.
require("tuna").setup({
compile_command = {
cpp = { exec = "g++", args = { "-std=c++20", "-O2", "-Wall", "$(FNAME)", "-o", "$(FNOEXT)" } },
},
maximum_time = 3000,
template_file = { "~/cp/template.$(JUDGE).cpp", "~/cp/template.cpp" },
downloaded_problems_path = "$(HOME)/cp/$(JUDGE)/$(PROBLEM)/main.$(FEXT)",
keymaps = { preset = "<leader>t" },
})
This is an example of a complete configuration.
Settings can also live in a .tuna.lua in any folder above the file you are editing.
This way a contest can set its own time limit, template or testcase layout without touching your config.
You can find more information on how to customize your configuration in the reference, and every option is commented in lua/tuna/config.lua.
tuna.nvim is a ground-up rewrite of and successor to competitest.nvim, which is no longer maintained. A massive thank you to xeluxee and all the contributors to competitest.nvim for their great work.
https://github.com/user-attachments/assets/7f2523ab-8bc1-46eb-9e54-11b80cae18d1
tuna.nvim is a competitive programming plugin that integrates with Competitive Companion to download contests, runs your solution against testcases, supports stress testing, and much more.
With lazy.nvim:
{
"FrancescoDerme/tuna.nvim",
config = function()
require("tuna").setup({})
end,
}
With vim.pack:
vim.pack.add({ "https://github.com/FrancescoDerme/tuna.nvim" })
require("tuna").setup({})
:Tuna opens the menu.:Tuna download problem starts listening for a problem from Competitive Companion: press the green plus in your browser to download it.
A solution file is created from your template_file (once you set one), the testcases are written beside it, and the file opens.:Tuna run compiles, runs the testcases in parallel, and opens the runner UI.
The four panes show output, expected output, input and stderr.
This UI can do a lot, for example d toggles a diff of the two outputs.:Tuna submit hands the solution to whichever submit tool you have configured, reads that tool's output as it runs, and puts the judge's verdict in your statusline.:Tuna pin and :Tuna last help you move between problems as you upsolve, and :Tuna clean keeps your directories tidy.You can find a complete list of the commands in REFERENCE.md, along with a detailed explanation of how each one works. Here we aim to describe the most important features along with some basic workflows.
Testcases live on disk beside your solution, as a pair of text files each by default.
The file-name format in which testcases are written and discovered can be set in testcases_input_file_format, which accepts modifiers such as $(FNOEXT) and $(TCNUM).
It is a list of names: writing a testcase uses the first entry in the list, and discovering one tries the entries in order, the first that finds anything wins:
testcases_input_file_format = { "$(FNOEXT)_input$(TCNUM).txt", "input$(TCNUM).txt", "in.txt" }
Where the testcases are stored can be set in testcases_directory: it's relative to the source by default, but can be an absolute path unrelated to the problem.
Add, edit, and delete testcases with :Tuna testcase add|edit|delete.
This can also be done in place in the runner UI, which is usually where you want to be.
:Tuna testcase split breaks one testcase into several: mark the cases inside it with a line of -, and each bracketed region becomes a testcase of its own.
:Tuna run compiles (if necessary), runs every testcase in parallel (as many at once as you have cores, by default) and opens the runner UI.
You can also open the runner UI by :Tuna show_ui.
The whole grid is defined in popup_ui.layout, you can arrange it differently or move to runner_ui.interface = "split" for real windows instead of floats.
Each testcase is a row, and the compile step is a row too that shows warnings and errors if there are any, customizable at runner_ui.compile_layout.
The runner UI can do a lot, but it aims to stay intuitive by aligning to Neovim's defaults: the panes with a purple border are editable, and keys behave in them as in any buffer. From the read-only panes you can:
d,r and <C-r>,n and x,Each pane opens full-screen with the key in the pane's title, and <C-hjkl> move you between panes, also customizable.
Comparison between Output and Expected Output is driven by output_compare_method: "exact", "squish" (whitespace-insensitive, the default), { "float", tol = 1e-6 } for problems with a tolerance, or a method of your own in compare_methods.
:Tuna compare <method> overrides it for one problem.
:Tuna run can do four different things based on helper programs discovered by filename beside your solution.
| you write | :Tuna run becomes | what happens |
|---|---|---|
| nothing | normal | run your solution against every testcase |
gen.* + brute.* | stress test | generate an input, run your solution and the bruteforce, compare the outputs, repeat until they disagree and save the counterexample |
interactor.* | interactive | run your solution against the interactor |
checker.* | (any of the above) | a special judge decides correctness instead of string comparison |
Interactive mode is not tied to having an interactor present, in fact it has three sources that can provide the input: live (you), interactor (your interactor program) and feed (a stored testcase).
Every helper is found the same way: a file named after it beside your solution (with the names in the table above or those specified in tool_names), or one of your own that you point to with its option (checker, stress.generator, stress.bruteforce or interactive.interactor), which can be specified in a .tuna.lua file beside the solution.
The run mode, interactive's source and the checker are decided based on the helpers present until you force them: :Tuna run <mode> forces a mode, :Tuna run interactive <source> a source, :Tuna checker off plain comparison, and auto hands any of them back.
:Tuna scaffold checker|generator|bruteforce|interactor drops in a starter file, named as tool_names says, in your solution's language, or in the language configured at scaffold.language (for example, you may want to have a py bruteforce even if you are coding in cpp).
tuna ships cpp and py starters in its scaffolds/ folder, and a file of the same name in your scaffold.directory (by default tuna/scaffolds in your Neovim config) is used instead, so you can customize a starter or add a new language by simply dropping a file there.
Finally, when several solutions are present in one folder, :Tuna run all runs every one of them against the same testcases.
With Competitive Companion installed, :Tuna download problem (or contest, or testcases) opens a listener, and pressing the green plus in the browser does the rest.
:Tuna download persistently leaves the listener open for a whole session.
Where things land is decided by a path template, evaluated per problem:
downloaded_problems_path = "$(HOME)/cp/$(JUDGE)/$(PROBLEM)/main.$(FEXT)",
downloaded_contests_directory = "$(HOME)/cp/$(JUDGE)/$(CONTEST)",
$(JUDGE) and $(CONTEST) come from tuna's own parsing of what Competitive Companion sends, and you can override it per judge with judge_parsers.
Codeforces and AtCoder are normalized out of the box, including Codeforces' mirror hosts: tuna rewrites the URL to the main site so links still work after the contest is over and keeps the mirror for submitting while the round is live.
New files are created from template_file, with the problem's own details filled in for these placeholders:
// judge: $(JUDGE)
// contest: $(CONTEST)
// problem: $(PROBLEM)
// submit at: $(URL)
The template_file option takes a single path, a per-extension table, or an ordered list tried until one exists, which is what makes a per-judge template practical:
template_file = { "~/cp/template.$(JUDGE).cpp", "~/cp/template.cpp" },
When entering a contest, some judges, like AtCoder, place you in the problems page, from which pressing the green plus is convenient.
Others, like Codeforces, place you inside the first problem, so going to the problems page to download the contest at the start of the competition would lose precious seconds.
To solve this, when dealing with judges of the second type, you can create a scratch file via :Tuna scratch.
A scratch file also follows your template, and you can solve and submit the easiest problem of the contest in it (there are no testcases for tuna to run, and submitting won't work unless you add the problem's URL in a // submit at: header line, which defeats the purpose of being fast).
After having solved this problem, you can download the rest of the contest with :Tuna download sync, which folds your scratch into the first problem's file.
:Tuna submit hands the solution to a submit tool of your choosing and reads its output as it runs, so the judge's verdict arrives in your statusline:
submit = {
command = 'cf submit "$(URL)" "$(LANG)" "$(FABSPATH)"',
languages = { cpp = "C++", python = "Python 3" },
},
A downloaded problem knows the correct URL to submit at (even without the header line), while a hand-made one becomes submittable the moment you add a // submit at: line with the correct URL into its header.
submit.judges.<judge> overrides the submission tool per judge.
The browser provider exists for judges for which you don't have a submission tool: it opens the submit page with the task preselected and puts your source on the clipboard.
For a working setup of a submit tool see here.
In this example, the submission tool is submitter.
A step-by-step guide on setting this up, from pointing tuna at your tool to reading the verdict, is in the reference.
:Tuna next / :Tuna prev step between the problems of a contest.:Tuna last problem / :Tuna last contest go back to what you were working on.
The menu also provides access to your recent contests and problems.:Tuna pin puts the problem you are on aside to solve later, and pinning it again lets it go.
The menu provides access to your pinned problems.:Tuna clean removes files you created and never used (or the ones you only slightly modified, with a tolerance the command asks for when you run it), and then the directories they leave empty.
Every deletion is confirmed one at a time, with the file in front of you.
:Tuna lib copies a piece of your own library into the file you are writing.
The library is plain source files in the folder library.path points at, with the parts worth copying marked in place:
// TUNALIB: binary exp start
long long bexp(long long b, long long e, long long m) { ... }
// TUNALIB: binary exp end
Only files matching the current buffer's extension are offered.
:Tuna lib snippet lists every snippet at once, :Tuna lib search opens the same catalogue in telescope.
Keymaps make the tuna experience much better.
You can opt in to the default keymaps with one line: keymaps = { preset = "<leader>t" } gives <leader>tr run, <leader>tu runner UI, <leader>ts submit, <leader>tn/<leader>tp problem navigation, <leader>tdc download a contest and <leader>tds sync, <leader>tf pin, <leader>tl the library, <leader>tw the scratch, <leader>tm the menu, and more.
You can edit or drop any of them without giving up the rest.
Two optional lualine components can make for a better experience: the download listener indicator and the current problem's submit verdict.
Every tuna window has the filetype tuna.
Listing it in lualine's ignore_focus keeps the statusline describing the file underneath while a tuna window has focus.
require("lualine").setup({
options = {
ignore_focus = { "tuna" },
},
sections = {
lualine_x = {
{
function() return require("tuna").lualine_component() end,
cond = function() return require("tuna.download").is_downloading() end,
},
{
function() return require("tuna.submit").status() end,
cond = function() return require("tuna.submit").is_submitting() end,
color = function() return require("tuna.submit").status_hl() end,
},
},
},
})
Everything is optional: setup() takes a table shaped like the defaults, and anything you leave out keeps its default.
require("tuna").setup({
compile_command = {
cpp = { exec = "g++", args = { "-std=c++20", "-O2", "-Wall", "$(FNAME)", "-o", "$(FNOEXT)" } },
},
maximum_time = 3000,
template_file = { "~/cp/template.$(JUDGE).cpp", "~/cp/template.cpp" },
downloaded_problems_path = "$(HOME)/cp/$(JUDGE)/$(PROBLEM)/main.$(FEXT)",
keymaps = { preset = "<leader>t" },
})
This is an example of a complete configuration.
Settings can also live in a .tuna.lua in any folder above the file you are editing.
This way a contest can set its own time limit, template or testcase layout without touching your config.
You can find more information on how to customize your configuration in the reference, and every option is commented in lua/tuna/config.lua.
tuna.nvim is a ground-up rewrite of and successor to competitest.nvim, which is no longer maintained. A massive thank you to xeluxee and all the contributors to competitest.nvim for their great work.