A Neovim plugin that provides a stateful task system focused on integration with build systems.
Tasks in this plugin are provided by modules that implement functionality for a specific build system. Modules can have custom parameters which user can set via :Task set_module_param
(like current target or build type). Tasks consists of one or more commands and have args
and env
parameters to set arguments and environment variable respectively. All this settings are serializable and will be stored in configuration file in your project directory.
:Task
command.You can also write your own module.
Use the command :Task
with one of the following arguments:
Argument(s) | Description |
---|---|
start <module> <task> |
Starting a task from a module. |
set_module_param <module> <param> |
Set parameter for a module. All parameters are module-specific. |
set_task_param <module> <param> <task> |
Set parameter for a task from a module. The parameter can be arg or env . |
cancel |
Cancel currently running task. |
Modules and tasks will be autocompleted.
Module name can be auto
, in which case the first module that satisfies the condition will be used.
To configure the plugin, you can call require('tasks').setup(values)
, where values
is a dictionary with the parameters you want to override. Here are the defaults:
local Path = require('plenary.path')
require('tasks').setup({
default_params = { -- Default module parameters with which `neovim.json` will be created.
cmake = {
cmd = 'cmake', -- CMake executable to use, can be changed using `:Task set_module_param cmake cmd`.
build_type = 'Debug', -- Build type, can be changed using `:Task set_module_param cmake build_type`.
build_kit = 'default', -- default build kit, can be changed using `:Task set_module_param cmake build_kit`.
dap_name = 'codelldb', -- DAP configuration name from `require('dap').configurations`. If there is no such configuration, a new one with this name as `type` will be created.
build_dir = tostring(Path:new('{cwd}', 'build', '{build_kit}', '{build_type}')), -- Build directory. The expressions `{cwd}`, `{build_kit}` and `{build_type}` will be expanded with the corresponding text values. Could be a function that return the path to the build directory.
cmake_kits_file = nil, -- set path to JSON file containing cmake kits
cmake_build_types_file = nil, -- set path to JSON file containing cmake kits
clangd_cmdline = {
'clangd',
'--background-index',
'--clang-tidy',
'--header-insertion=never',
'--completion-style=detailed',
'--offset-encoding=utf-8',
'--pch-storage=memory',
'--cross-file-rename',
'-j=4',
}, -- command line for invoking clangd - this array will be extended with --compile-commands-dir and --query-driver after each cmake configure with parameters inferred from build_kit, build_type and build_dir
},
zig = {
cmd = 'zig', -- zig command which will be invoked
dap_name = 'codelldb', -- DAP configuration name from `require('dap').configurations`. If there is no such configuration, a new one with this name as `type` will be created.
build_type = 'Debug', -- build type, can be changed using `:Task set_module_param zig build_type`
build_step = 'install', -- build step, cah be changed using `:Task set_module_param zig build_step`
},
cargo = {
dap_name = 'codelldb', -- DAP configuration name from `require('dap').configurations`. If there is no such configuration, a new one with this name as `type` will be created.
},
npm = {
cmd = 'npm', -- npm command which will be invoked. If using yarn or pnpm, change here.
working_directory = vim.loop.cwd(), -- working directory in which NPM will be invoked
},
make = {
cmd = 'make', -- make command which will be invoked
args = {
all = { '-j10', 'all' }, -- :Task start make all → make -j10 all
build = {}, -- :Task start make build → make
nuke = { 'clean' }, -- :Task start make nuke → make clean
},
},
},
save_before_run = true,
params_file = 'neovim.json',
quickfix = {
pos = 'botright',
height = 12,
only_on_error = false,
},
dap_open_command = function() return require('dap').repl.open() end,
configuration
task using :Task start cmake configure
.:Task set_module_param cmake target
. All module parameters are specific to modules. Since CMake can't run targets like Cargo, we introduced a parameter to select the same target for building (appropriate arguments will be passed to CMake automatically) and running.:Task set_task_param cmake run
.:Task start cmake run
or build and debug using :Task start cmake debug
. You can pass additional arguments to these commands, which will be temporarily added to the arguments from the previous step.You can define your custom build types in a JSON file, path to which needs to be specified in the default_params
object as described above. An example JSON file with CMake build types looks like this:
{
"debug" : {
"build_type" : "Debug",
"cmake_usr_args" : {
"MY_TREAT_WARNINGS_AS_ERRORS": "OFF"
}
},
"dev-release": {
"build_type" : "RelWithDebInfo",
"cmake_usr_args": {
"MY_TREAT_WARNINGS_AS_ERRORS": "ON"
}
},
"release": {
"build_type" : "Release",
"cmake_usr_args": {
"MY_TREAT_WARNINGS_AS_ERRORS": "ON"
}
}
}
The structure of the JSON is in the form:
{
"build_type_name": {
"build_type": "this will be passed to -DCMAKE_BUILD_TYPE",
"cmake_usr_args": { // optional, will be passed to cmake configure whenever build_type_name is active
"cmake_variable1": "value1",
"cmake_variable2": "value2",
...
}
},
...
}
This allows for custom cmake user arguments for each build type that will always be applied, regardless of selected CMake kit.
You can define your custom build kits (or toolchains) in a JSON file, path to which needs to be specified in the default_params
object as described above. An example JSON file with CMake kits looks like this:
{
"xcode-clang": {
"compilers": {
"C": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc",
"CXX": "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++"
},
"query_driver" : "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang++",
"generator": "Xcode",
"build_type_aware": false
},
"clang": {
"compilers": {
"C": "/usr/bin/clang",
"CXX": "/usr/bin/clang++"
},
"query_driver" : "/usr/bin/clang++",
"generator": "Ninja"
},
"clang-xcode-13.4.1": {
"compilers": {
"C": "/usr/bin/clang",
"CXX": "/usr/bin/clang++"
},
"query_driver" : "/usr/bin/clang++",
"environment_variables": {
"DEVELOPER_DIR": "/Applications/xcode-versions/Xcode-13.4.1.app/Contents/Developer"
},
"generator": "Ninja"
},
"emscripten-3.1.17": {
"toolchain_file": "~/.conan/data/emsdk_installer/3.1.17/microblink/stable/package/3c9d8fb1b383ce27cf8b0942dcbdbb6add8cbee9/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake",
"environment_variables": {
"EM_CONFIG": "/Users/dodo/.conan/data/emsdk_installer/3.1.17/microblink/stable/package/3c9d8fb1b383ce27cf8b0942dcbdbb6add8cbee9/.emscripten",
"EM_CACHE": "/Users/dodo/.conan/data/emsdk_installer/3.1.17/microblink/stable/package/3c9d8fb1b383ce27cf8b0942dcbdbb6add8cbee9/.emscripten_cache"
},
"cmake_usr_args": {
"MY_EMSCRIPTEN_EMRUN_BROWSER": "chrome",
"MY_EMSCRIPTEN_EMRUN_SILENCE_TIMEOUT": "300",
"MY_EMSCRIPTEN_EMRUN_SERVE_AFTER_CLOSE": "ON",
"MY_EMSCRIPTEN_EMRUN_BROWSER_ARGS": "--remote-debugging-port=9622"
},
"query_driver" : "~/.conan/data/emsdk_installer/3.1.17/microblink/stable/package/3c9d8fb1b383ce27cf8b0942dcbdbb6add8cbee9/upstream/emscripten/em++",
"generator": "Ninja"
}
}
The structure of the JSON is in the form:
{
"build_kit_name": {
"compilers": { // optional, if provided it will pass -DCMAKE_C_COMPILER and -DCMAKE_CXX_COMPILER to cmake during configuration, not needed when toolchain_file is provided or default compiler is used
"C": "/path/to/c/compiler/to/be/used",
"CXX": "/path/to/c++/compiler/to/be/used",
},
"toolchain_file": "/path/to/cmake/toolchain/file", // also optional, if provided, this will be passed to -DCMAKE_TOOLCHAIN_FILE
"environment_variables": { // optional, if provided, given environment variables will be set for every invocation of any cmake task
"VAR1": "value1",
"VAR2": "value2",
...
},
"cmake_usr_args": { // optional, will be passed to cmake configure whenever build_kit_name is active
"cmake_variable1": "value1",
"cmake_variable2": "value2",
...
},
"query_driver": "/path/to/query_driver/compiler", // optional, if provied, it will be passed as --query-driver` parameter to clangd when build_kit_name is active
"generator": "cmake-generator", // will be passed as -G option to cmake. If not provided, Ninja will be used. Keep in mind that not all generators support creating compile_commands.json file, which is needed for clangd to work. However, you can still use those generators for quickly creating CMake build directories (e.g. for Xcode)
"build_type_aware": true|false, // optional, indicates whether generator is aware of CMAKE_BUILD_TYPE. For example, IDE generators need to have this set to false to prevent invoking cmake with -DCMAKE_BUILD_TYPE=active_build_type
},
...
}
CMake with Kits task module will generate following tasks:
configure
— invokes cmake with parameters for active build_kit
and build_type
, or active configure_preset
if presets are availableconfigureDebug
- same as configure
, but starts the CMake debugging sessionbuild
— builds the currenly selected targetbuild_all
— builds all targetsbuild_current_file
— builds target associated with now opened source file (Ninja generator only)run
— builds and then runs the selected targetdebug
— builds and then starts nvim-dap
debug session for the selected targetclean
— cleans the build directoryctest
— invokes ctest
purge
— deletes the cmake binary directory (unix only)reconfigure
— equivalent to purge
+ configure
Here is convenient Lua code for configuring mapping for tasks generated by this plugin:
vim.keymap.set( "n", "<leader>cC", [[:Task start cmake configure<cr>]], { silent = true } )
vim.keymap.set( "n", "<leader>cD", [[:Task start cmake configureDebug<cr>]], { silent = true } )
vim.keymap.set( "n", "<leader>cP", [[:Task start cmake reconfigure<cr>]], { silent = true } )
vim.keymap.set( "n", "<leader>cT", [[:Task start cmake ctest<cr>]], { silent = true } )
vim.keymap.set( "n", "<leader>cK", [[:Task start cmake clean<cr>]], { silent = true } )
vim.keymap.set( "n", "<leader>ct", [[:Task set_module_param cmake target<cr>]], { silent = true } )
vim.keymap.set( "n", "<C-c>", [[:Task cancel<cr>]], { silent = true } )
vim.keymap.set( "n", "<leader>cr", [[:Task start cmake run<cr>]], { silent = true } )
vim.keymap.set( "n", "<F7>", [[:Task start cmake debug<cr>]], { silent = true } )
vim.keymap.set( "n", "<leader>cb", [[:Task start cmake build<cr>]], { silent = true } )
vim.keymap.set( "n", "<leader>cB", [[:Task start cmake build_all<cr>]], { silent = true } )
-- open ccmake in embedded terminal
local function openCCMake()
local build_dir = tostring( require( 'tasks.cmake_utils.cmake_utils' ).getBuildDir() )
vim.cmd( [[bo sp term://ccmake ]] .. build_dir )
end
vim.keymap.set( "n", "<leader>cc", openCCMake, { silent = true } )
-- if project is using presets, provide preset selection for both <leader>cv and <leader>ck
-- if not, provide build type (<leader>cv) and kit (<leader>ck) selection
local function selectPreset()
local availablePresets = cmake_presets.parse( 'buildPresets' )
vim.ui.select( availablePresets, { prompt = 'Select build preset' }, function( choice, idx )
if not idx then
return
end
local projectConfig = ProjectConfig:new()
if not projectConfig[ 'cmake' ] then
projectConfig[ 'cmake' ] = {}
end
projectConfig[ 'cmake' ][ 'build_preset' ] = choice
-- autoselect will invoke projectConfig:write()
cmake_utils.autoselectConfigurePresetFromCurrentBuildPreset( projectConfig )
end)
end
local function selectBuildKitOrPreset()
if cmake_utils.shouldUsePresets() then
selectPreset()
else
tasks.set_module_param( 'cmake', 'build_kit' )
end
end
vim.keymap.set( "n", "<leader>ck", selectBuildKitOrPreset, { silent = true } )
local function selectBuildTypeOrPreset()
if cmake_utils.shouldUsePresets() then
selectPreset()
else
tasks.set_module_param( 'cmake', 'build_type' )
end
end
vim.keymap.set( "n", "<leader>cv", selectBuildTypeOrPreset, { silent = true } )
clangd
config on NeoVim startupTo correctly boot the clangd
LSP on NeoVim startup, you can configure LSP to immediately use correct clangd
arguments with following code:
require( 'lspconfig' )[ 'clangd' ].setup({
cmd = require( 'tasks.cmake_utils.cmake_utils' ).currentClangdArgs(),
})
local Path = require('plenary.path')
local lualine = require( 'lualine' )
local ProjectConfig = require( 'tasks.project_config' )
local function cmakeStatus()
local cmake_config = ProjectConfig:new()[ 'cmake' ]
local cmakelists_dir = cmake_config.source_dir and cmake_config.source_dir or vim.loop.cwd()
if ( Path:new( cmakelists_dir ) / 'CMakeLists.txt' ):exists() then
local cmake_utils = require( 'tasks.cmake_utils.cmake_utils' )
if cmake_utils.shouldUsePresets( cmake_config ) then
local preset = cmake_config.build_preset or 'not selected'
local cmakeTarget = cmake_config.target and cmake_config.target or 'all'
return 'CMake preset: ' .. preset .. ', target: ' .. cmakeTarget
else
local cmakeBuildType = cmake_config.build_type or 'not selected'
local cmakeKit = cmake_config.build_kit or 'not selected'
local cmakeTarget = cmake_config.target or 'all'
return 'CMake variant: ' .. cmakeBuildType .. ', kit: ' .. cmakeKit .. ', target: ' .. cmakeTarget
end
else
return ''
end
end
lualine.setup({
sections = {
lualine_c = { { 'filename', path = 1 } },
lualine_x = { 'encoding', 'fileformat', 'filetype', cmakeStatus }
}
})
:Task set_task_param cargo run
.:Task set_task_param cargo global_cargo_args
.:Task start cargo run
or build and debug using :Task start cargo debug
.Cargo module doesn't have a target
param which specific to CMake because cargo run
automatically pick the binary. If there is multiple binaries, you can set which one you want to run using --bin
or --project
in step 2 as you do in CLI.
<target>
with :Task start make <target>
.To override targets or add custom make
options, configure the appropriate task:
require('tasks').setup({
default_params = {
...
make = {
cmd = 'make',
args = {
all = { '-j10', 'all' }, -- :Task start make all → make -j10 all
build = {}, -- :Task start make build → make
nuke = { 'clean' }, -- :Task start make nuke → make clean
},
},
...
}
})
main
:Task start zig run_file
:Task start zig debug_file
build.zig
file:Task set_module_param zig build_step
.:Task set_module_param zig build_type
:Task start zig build
build
- invokes zig build
with step configured as build_step
with build type configured as build_type
clean
- deletes the zig-out
folder (works only on Unix shells at the moment)clean_cache
- deletes the .zig-cache
folder (works only on Unix shells at the moment)clean_all
- invokes forst clean
, then clean_cache
run_file
- invokes zig run
for currently open buffer. Obeys build_type
debug_file
- starts debugger for currently open buffer (effectively as run_file
, but under debugger)test_file
- invokes zig test
for currently open buffer. Obeys build_type
.debug_test_file
-same as test_file
, but under debuggerrun_current_test
- invokes zig test
for currently open buffer with test filter set to a first test above current cursor location. If test filter cannot be calculated, all tests in current file are run (behavior same as test_file
).debug_current_test
- same as run_current_test
, but under debugger:Task start npm install
:Task start npm run <script>
For example, imagine that your package.json
contains lines like these:
{
"scripts": {
"clean": "rimraf build dist",
"lint": "eslint --ext ts -c .eslintrc.json src",
"start": "NODE_PATH=$(pwd)/node_modules node $(pwd)/../core/scripts/https-serve.js dist",
"rollup": "rollup -c rollup.config.js",
}
}
You can map then those commands with code like this:
vim.keymap.set( "n", "<leader>ni", [[:Task start npm install<cr>]] )
vim.keymap.set( "n", "<leader>nl", [[:Task start npm run lint<cr>]] )
vim.keymap.set( "n", "<leader>nr", [[:Task start npm run rollup<cr>]] )
vim.keymap.set( "n", "<leader>ns", [[:Task start npm run clean<cr>]] )
vim.keymap.set( "n", "<leader>ns", [[:Task start npm run start<cr>]] )
To create a module just put a lua file under lua/tasks/module
in your configuration or submit your module as a PR. In this module you need to return a table with the following fields:
{
params = {
-- A table of parameter names. Possible values:
'parameter_name1', -- A string parameter, on setting user will be prompted with vim.ui.input.
parameter_name2 = { 'one', 'two' }, -- A table with possible values, on setting user will be prompted with vim.ui.select to pick one of these values.
parameter_name3 = func, -- A function that generates a string or a table.
}
condition = function() return Path:new('file'):exists() end -- A function that returns `true` if this module could be applied to this directory. Used when `auto` is used as module name.
tasks = {
-- A table of module tasks. Possible values:
task_name1 = {
-- Required parameters:
cmd = 'command' -- Command to execute.
-- Optional parameters:
cwd = 'directory' -- Command working directory. Default to current working directory.
after_success = callback -- A callback to execute on success.
dap_name = 'dap_name' -- A debug adapter name. If exists, the task will be launched through the adapter. Usually taken from a module parameter. Implies ignoring all streams below.
-- Disable a stream output to quickfix. If both are disabled, quickfix will not show up. If you want to capture output of a stream in a next task, you need to disable it.
ignore_stdout = true,
ignore_stderr = true,
},
task_name2 = func1, -- A function that returns a table as above. Accepts configuration for this module and previous job.
task_name3 = { func2, func3 }, -- A list of functions as above. Tasks will be executed in chain.
}
}
For a more complex example take a look at cargo.lua.
You can also edit existing modules in right in your config. Just import a module using require('tasks.module.module_name')
and add/remove/modify any fields from the above.