A project-local task runner for Neovim. Declare your build, test, run, and
debug tasks once in a TOML file and launch them from inside the editor with
:Tasks — with schema-backed completion and diagnostics while you edit the
file, task dependencies, value expressions, quickfix parsing, and a live status
panel that streams each task's output.
Warning
Work in progress. The plugin is usable but under active development; the configuration format and public API may still change.
- Features
- Requirements
- Installation
- Quick start
- The tasks file
- Task types
- Shared task options
- Expressions
- Quickfix matchers
- The
:Taskscommand - Status panel
- Editing support (LSP)
- Configuration
- Extending easytasks
- Credits & license
- One TOML file per project — tasks live in
tasks.tomlat the project root; the presence of that file is what marks a directory as a project. - Built-in task types — run a program directly (
process), through a shell (shell), group other tasks (composite), or start a debug session (debug, via easydap.nvim). - Task dependencies — declare
depends_onand run prerequisites insequenceor inparallelbefore the task itself. - Concurrency policies — control what happens when a task is already running
(
wait,restart,refuse,parallel). - Value expressions — interpolate the current file, cwd, environment,
shell output, or interactive prompts into task values with a small
{{ … }}expression language, and define your own reusable inline macros. - Quickfix parsing — turn compiler/linter/test output into a populated quickfix list with a named matcher (GCC, TypeScript, Go, Rust, Python, and more built in).
- Schema-backed editing — a vendored in-process language server gives the tasks file completion, hover, diagnostics, code actions, and formatting driven by the live task schema.
- Live status panel — a bottom split with a tab per run streaming its output, plus an embedded scratch shell.
- Extensible — register your own task types, quickfix matchers, and expressions from Lua.
- Neovim ≥ 0.10
- easydap.nvim — optional, required
only for the
debugtask type.
The TOML engine is vendored, so there are no external Lua dependencies.
Using Neovim's built-in plugin manager, vim.pack (Neovim 0.12+; see
:help vim.pack):
vim.pack.add({
-- { src = "https://github.com/mbfoss/easydap.nvim" }, -- optional, only for `debug` tasks
{ src = "https://github.com/mbfoss/easytasks.nvim" },
})
require("easytasks").setup()Using lazy.nvim:
{
"mbfoss/easytasks.nvim",
-- optional, only for `debug` tasks:
-- dependencies = { "mbfoss/easydap.nvim" },
opts = {},
}
opts = {}callsrequire("easytasks").setup()with the defaults. Replace it with a table to override any configuration value.
Using paq-nvim:
require("paq"){
"mbfoss/easytasks.nvim",
}
-- then, somewhere in your config:
require("easytasks").setup()Using the built-in package mechanism (:h packages):
git clone https://github.com/mbfoss/easytasks.nvim \
~/.local/share/nvim/site/pack/plugins/start/easytasks.nvimrequire("easytasks").setup()-
Create a
tasks.tomlin your project root:[tasks.build] type = "shell" command = "make -j" [tasks.test] type = "process" command = "ctest --output-on-failure" depends_on = ["build"]
-
Run a task:
:Tasks
Pick a task from the list. The status panel opens and streams its output. Running
testfirst runsbuild(its dependency), thentest. -
Re-run the last task, or stop a running one:
:Tasks rerun :Tasks stop
While editing tasks.toml you get completion, hover docs, and inline
diagnostics for every field — see Editing support.
Tasks are defined under the [tasks] table, keyed by name. A task's name is
the header key ([tasks.<name>]) — you do not repeat it as a field. Every
task must declare a type.
# Optional: reusable inline expression macros (see “Expressions”).
[expressions]
outdir = "{{ projectdir }}/build"
[tasks.build]
type = "shell"
command = "cmake --build {{ outdir }}"
[tasks.run]
type = "process"
command = "{{ outdir }}/app --verbose"
depends_on = ["build"]The top-level document has just two tables:
| Key | Purpose |
|---|---|
[tasks] |
Task definitions, keyed by name ([tasks.<name>]). Required. |
[expressions] |
Named inline expression macros. Optional. |
Every task shares a common set of options; the fields below are specific to each type.
Runs a command directly, without a shell. A string command is split into argv using POSIX shell-word rules; an array is used verbatim (no splitting, globbing, or shell operators).
[tasks.lint]
type = "process"
command = "eslint src --format unix" # or ["eslint", "src", …]
cwd = "{{ projectdir }}"
env = { NODE_ENV = "development" }
quickfix_matcher = "linter"| Field | Type | Description |
|---|---|---|
command |
string | string[] | Required. Program + args. String is shell-word split; array as-is. |
cwd |
string | Working directory for the command. |
env |
table<string,string> | Environment variables to set. |
clear_env |
boolean | Pass env verbatim instead of merging it onto the current env. |
quickfix_matcher |
string | Name of a quickfix matcher to parse output. |
Runs a command string through the shell, so pipes, globs, redirection, and
&& all work.
[tasks.deploy]
type = "shell"
command = "npm run build && rsync -a dist/ server:/var/www"Fields are the same as process, except command must be a single string
(it is the shell command line).
A task with no command of its own — it exists purely to group other tasks
through its dependencies. Combine with depends_order to run them in sequence
or in parallel.
[tasks.ci]
type = "composite"
depends_on = ["lint", "test", "build"]
depends_order = "sequence"Starts a DAP debug session through easydap.nvim.
This task type is only available when easydap.nvim is installed — without it,
easytasks works normally and simply offers no debug type.
easytasks owns only the framework fields; the debugger vocabulary lives under
parameters (the adapter's native launch/attach body) with a few convenience
fields mapped onto it.
[tasks.debug-app]
type = "debug"
adapter = "codelldb"
request = "launch"
command = "{{ outdir }}/app --flag" # program + args
cwd = "{{ projectdir }}"| Field | Type | Description |
|---|---|---|
adapter |
string | Required. DAP adapter name (e.g. codelldb, delve, debugpy). |
request |
"launch" | "attach" |
Defaults to launch (or the adapter's default) when convenience fields are set. |
command |
string | string[] | Program + args, mapped onto the adapter's target/args roles. Implies launch. |
cwd |
string | Working directory for the debuggee. |
env |
table<string,string> | Environment for the debuggee. |
merge_env |
boolean | Merge env onto Neovim's environment instead of replacing it. |
pid |
integer | PID to attach to (attach only). |
host, port |
string, integer | Remote endpoint to attach to (attach only). |
parameters |
table | Native DAP launch/attach body, sent verbatim. Keys depend on adapter/request. |
raw_messages |
boolean | Capture the raw DAP protocol messages in a dedicated buffer. |
When the tasks-file LSP has easydap available, the parameters block is
completed and validated against the selected adapter's own schema.
These fields are available on every task type.
| Field | Type | Description |
|---|---|---|
type |
string | Required. The task type. |
if_running |
enum | What to do if the task is already running (see below). |
depends_on |
string[] | Task names that must complete successfully before this task runs. |
depends_order |
"sequence" | "parallel" |
How the depends_on tasks are executed. sequence = one after another. |
save_buffers |
boolean | table | Save modified project buffers before the task (and its dependencies) run. |
if_running values:
| Value | Behaviour |
|---|---|
wait |
Wait for the running instance to finish successfully. |
restart |
Stop the current instance and start a new one. |
refuse |
Do not start a new instance if one is already running. |
parallel |
Start a new instance alongside any existing ones. |
save_buffers can be true (save every modified project buffer) or a table
with glob filters:
[tasks.build]
type = "shell"
command = "make"
save_buffers = { include = ["src/**"], exclude = ["**/*.tmp"], include_hidden = false }Hidden files (dotfiles / files under dot-directories) are skipped unless
include_hidden = true.
Any task value can contain {{ … }} holes that are evaluated when the task
runs. The interior of a hole is a small expression language: function calls,
comma-separated arguments, string literals, numbers, booleans, and ..
concatenation. Nesting is function composition — f(g(x)).
[tasks.run]
type = "process"
command = "{{ projectdir }}/build/app"
cwd = "{{ filedir }}"
env = { API_KEY = "{{ env('API_KEY') }}", REV = "{{ shell('git rev-parse --short HEAD') }}" }If the entire trimmed value is a single hole, the expression's native value
is preserved (a number stays a number, a boolean a boolean, and a nil result
drops the field). Otherwise the value is string interpolation.
| Expression | Result |
|---|---|
file (filetype?) |
Absolute path of the current file. |
filename (filetype?) |
File name with extension. |
fileroot (filetype?) |
Absolute path without the extension. |
filedir |
Absolute directory of the current file. |
fileext |
Extension (without the dot). |
cwd |
The task's cwd, or the editor cwd. |
projectdir |
Absolute path of the project root (where the tasks file lives). |
env(NAME) |
Value of an environment variable. |
shell(CMD) |
Stdout of a shell command, trailing newlines stripped. |
prompt(TEXT, default?, completion?) |
Ask for input at run time. |
select-pid(prompt?) |
Pick a running process and yield its PID. |
lbrace |
A literal {{ (escape hatch; same as {{{{). |
Strings inside a hole use "…" or '…' and are always verbatim (no escape
sequences, no nested interpolation) — pick the quote your content lacks. To
build up a value, concatenate with ..:
command = "{{ shell('echo ' .. file()) }}"Define reusable named expressions under [expressions]. They may reference
built-ins, other inline macros, and their own positional arguments $1, $2, …
[expressions]
greet = "'Hello, ' .. $1 .. '!'"
tagged = "greet($1) .. ' [' .. env('USER') .. ']'"
outdir = "{{ projectdir }}/build/{{ $1 }}"
[tasks.run]
type = "shell"
command = "echo {{ tagged('world') }} && ls {{ outdir('release') }}"You can evaluate any expression against the current project without running a task:
:Tasks eval file
:Tasks eval {{ shell('git branch --show-current') }}See docs/expression-grammar.md for the full grammar.
Set quickfix_matcher on a process or shell task to parse its output into
the quickfix list as it streams. The list is cleared when the task starts and
populated line by line, so you can :copen and jump straight to errors.
Built-in matchers:
| Name | Tooling |
|---|---|
gcc |
GCC / Clang (incl. template “required from” chains) |
msvc |
MSVC (file(line): error CXXXX: …) |
tsc |
TypeScript compiler |
go |
Go compiler |
gotest |
go test output |
cargo |
Rust / Cargo (errors and panics) |
python |
Python tracebacks |
pytest |
pytest / unittest |
linter |
Generic file:line:col: CODE: msg (ESLint, Pylint, Flake8, Mypy, …) |
unix |
Generic file:line:col: message |
Register your own with register_qfmatcher.
The user command (named Tasks by default) is the single entry point. Called
with no argument it opens the task picker.
| Invocation | Action |
|---|---|
:Tasks / :Tasks run |
Pick a task to run (with a live preview of its definition). |
:Tasks rerun |
Re-run the last task. |
:Tasks stop |
Pick a running task to stop. |
:Tasks cancel |
Stop all running tasks. |
:Tasks shell |
Open a scratch shell tab in the status panel. |
:Tasks eval [expr] |
Evaluate an expression (or bare expression name) and echo it. |
:Tasks template |
Insert a task template at the cursor (only in the tasks file). |
:Tasks panel |
Toggle the status panel. |
:Tasks panel jump N |
Focus panel page/tab N (also :N Tasks panel jump). |
:Tasks panel remove |
Dispose a finished task tab. |
:Tasks panel clear |
Dispose all finished task tabs. |
Subcommands and task names complete on <Tab>.
Running a task opens a bottom split with a winbar of tabs — one per run,
each numbered, showing a status badge (▶ running, ✓ ok, ✗ failed, ⧗
waiting on dependencies). Each tab has:
- an info page with a timestamped run log, and
- a terminal page per spawned buffer, streaming stdout/stderr live.
Click a tab or use :Tasks panel jump N to switch pages. New output on an
inactive tab is flagged with an unread marker. Terminal pages autoscroll while
your cursor sits at the bottom, and stop following as soon as you scroll up.
:Tasks shell adds a plain interactive shell as its own tab.
Opening the tasks file attaches a vendored, in-process language server (it runs on a background thread, not a subprocess) that is driven by the live task schema — including any task types, adapters, and expressions you have registered. It provides:
- Completion — task types, field names, enum values, dependency task names,
and expression names/arguments inside
{{ … }}. - Diagnostics — schema validation, unknown fields, type errors, and malformed expressions, shown inline as you type.
- Hover — field and expression documentation.
- Code actions and formatting for the TOML document.
The tasks file gets its own easytasks filetype (it is not treated as generic
toml), so your existing TOML tooling is left untouched and no extra
Tree-sitter parser is pulled in.
Call setup() (directly, or via your plugin manager's opts). All fields are
optional; defaults shown:
require("easytasks").setup({
enabled = true, -- attach the LSP + register the command
command = "Tasks", -- name of the user command
tasks_filename = "tasks.toml", -- per-project tasks file (also the project marker)
storage_dir = ".easytasks", -- per-project state directory
})Toggle the plugin at runtime with require("easytasks").enable() /
require("easytasks").disable(), and check whether the cwd is an easytasks
project with require("easytasks").in_project().
The public API in require("easytasks") exposes three
extension points. Register before setup() so the new definitions are
included in the schema the LSP uses.
local easytasks = require("easytasks")
-- A custom task type (loader may be a module path, a factory fn, or a table).
easytasks.register_task_type("http", function()
return {
start = function(task, ctx, on_done)
-- … kick off work; call ctx.add_bufnr / ctx.report as needed …
on_done(true)
return function() --[[ cancel ]] end
end,
schema = { properties = { url = { type = "string" } }, required = { "url" } },
}
end)
-- A custom quickfix matcher for `process`/`shell` tasks.
easytasks.register_qfmatcher("myfmt", function(line, ctx)
local file, lnum, msg = line:match("^(%S+):(%d+):%s+(.+)$")
if file then
return { filename = file, lnum = tonumber(lnum), col = 1, text = msg, type = "E" }
end
end)
-- A custom expression, usable as `{{ hostname }}` in task values.
easytasks.register_expression("hostname", function(ctx)
return vim.uv.os_gethostname()
end, { desc = "The machine hostname" })register_task_type(name, loader)— add a task type.loaderis a module path string, a zero-arg factory, or a resolved definition table.register_qfmatcher(name, fn)— add a quickfix matcher;fn(line, ctx)returns a quickfix item ornil.register_expression(name, fn, opts?)— add a{{ … }}expression; built-ins cannot be overridden.opts.descshows in completion.
- TOML engine: tomltools.
- Debug support: easydap.nvim.
Released under the MIT License.
Contributing? See development.md for the repository layout, how to run the tests, and how the vendored TOML engine is maintained.