Skip to content

mbfoss/easytasks.nvim

Repository files navigation

easytasks.nvim

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.


Table of contents


Features

  • One TOML file per project — tasks live in tasks.toml at 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_on and run prerequisites in sequence or in parallel before 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.

Requirements

  • Neovim ≥ 0.10
  • easydap.nvimoptional, required only for the debug task type.

The TOML engine is vendored, so there are no external Lua dependencies.

Installation

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 = {} calls require("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.nvim
require("easytasks").setup()

Quick start

  1. Create a tasks.toml in your project root:

    [tasks.build]
    type    = "shell"
    command = "make -j"
    
    [tasks.test]
    type       = "process"
    command    = "ctest --output-on-failure"
    depends_on = ["build"]
  2. Run a task:

    :Tasks

    Pick a task from the list. The status panel opens and streams its output. Running test first runs build (its dependency), then test.

  3. 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.

The tasks file

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.

Task types

Every task shares a common set of options; the fields below are specific to each type.

process

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.

shell

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).

composite

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"

debug

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.

Shared task options

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.

Expressions

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.

Built-in expressions

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()) }}"

Inline macros

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.

Quickfix matchers

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 :Tasks command

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>.

Status panel

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.

Editing support (LSP)

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.

Configuration

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().

Extending easytasks

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. loader is 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 or nil.
  • register_expression(name, fn, opts?) — add a {{ … }} expression; built-ins cannot be overridden. opts.desc shows in completion.

Credits & license

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.

About

Task runner for Neovim

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages