Introduction to Vim Task Running
Running tasks from within Vim is one of the most powerful ways to supercharge your workflow. Instead of constantly switching between your editor and a terminal, you can compile code, run tests, execute scripts, and view output without ever leaving your editing environment. This guide walks through everything you need to know about running tasks in Vim, from the built-in :! command to modern asynchronous plugins.
What Is Vim Task Running?
Task running in Vim refers to the ability to execute external shell commands, build scripts, test suites, and other automation tools directly from the editor. Vim provides several native mechanisms for this, and the plugin ecosystem has extended these capabilities significantly with asynchronous execution, output buffering, and integration with language-specific tooling.
At its core, task running means you can:
- Execute shell commands without leaving Vim
- Capture and review command output in a buffer
- Run long-running processes asynchronously
- Bind frequently used commands to key mappings
- Integrate with build systems like Make, npm, cargo, or gradle
Why Task Running Matters
Context switching is expensive. Every time you leave Vim to open a terminal, run a command, and return, you lose focus and momentum. By running tasks inside Vim, you maintain your editing context, keep your cursor position, and can immediately act on compiler errors or test failures. This is especially valuable in large codebases where the edit-compile-fix cycle repeats hundreds of times per day.
Beyond convenience, in-editor task running enables tighter integration with Vim's quickfix and location lists. When a compiler or linter produces output in a recognized format, Vim can parse it and let you jump directly to the offending line with a single keystroke. This transforms error resolution from a manual hunt into a streamlined navigation flow.
Built-in Task Running Methods
The Bang Command
The simplest way to run a task in Vim is the :! command. It executes a shell command and displays the output temporarily. You press Enter to return to your buffer.
" Run a simple shell command
:!ls -la
" Compile a C file
:!gcc -o myprogram main.c
" Run a Python script
:!python3 script.py
While simple, the bang command is synchronous, meaning Vim is blocked until the command finishes. This is fine for quick tasks but problematic for long builds or test suites.
The Make Command
Vim has a built-in :make command that runs make in the current directory and populates the quickfix list with any errors. This is one of the most underrated features of Vim.
" Run make
:make
" Run make with a specific target
:make test
" Run make with arguments
:make clean all
The magic of :make is that it uses the errorformat option to parse compiler output. If you are working with a language whose compiler output matches the default errorformat, you can immediately use :copen to see errors and :cnext / :cprev to jump between them.
You can customize what command :make runs by setting makeprg:
" Use npm test instead of make
:set makeprg=npm\ test
" Use cargo build for Rust
:set makeprg=cargo\ build
" Use pytest for Python
:set makeprg=pytest
Reading Command Output Into a Buffer
The :read command with a bang inserts the output of a shell command into your current buffer at the cursor position.
" Insert the current date
:read !date
" Insert a list of files
:read !ls -1 *.py
Similarly, :r !command is the shorthand form. This is useful for generating boilerplate or inserting dynamic content.
Capturing Output With Redir
The :redir command captures Vim command output, which can be combined with external commands for more complex workflows.
:redir @a
:silent make
:redir END
This stores the output of :make into register a, which you can then paste into a buffer for inspection.
Asynchronous Task Running
Why Asynchronous Matters
Synchronous commands block Vim. If you run a test suite that takes thirty seconds, you cannot edit code during that time. Asynchronous task running solves this by executing commands in the background and delivering results when ready. Vim 8 introduced native job and channel support, making this possible without external dependencies.
Using Vim 8 Jobs
Vim 8's job_start() function lets you run commands asynchronously. Here is a basic example that runs a shell command and writes output to a temporary file:
function! RunAsync(cmd)
let output_file = tempname()
let job = job_start(a:cmd, {
\ 'out_io': 'file',
\ 'out_name': output_file,
\ 'callback': function('s:OnJobExit', [output_file])
\ })
endfunction
function! s:OnJobExit(output_file, job, event) abort
execute 'split' a:output_file
setlocal bufhidden=delete noswapfile
endfunction
" Run a command asynchronously
call RunAsync('npm test')
This creates a job, directs its output to a temp file, and opens that file in a split when the job completes. While the job runs, you can continue editing.
Neovim's Job Control
Neovim provides a slightly different API using jobstart(). The concepts are similar but the function signatures differ:
function! RunAsyncNvim(cmd)
let output = []
let job = jobstart(a:cmd, {
\ 'on_stdout': function('s:OnOutput'),
\ 'on_stderr': function('s:OnOutput'),
\ 'on_exit': function('s:OnExit'),
\ })
endfunction
function! s:OnOutput(job, data, event) abort
" Process output lines
endfunction
function! s:OnExit(job, exit_code, event) abort
echo 'Task finished with exit code: ' . a:exit_code
endfunction
call RunAsyncNvim(['npm', 'test'])
Popular Task Running Plugins
Vim Dispatch
Tim Pope's vim-dispatch is one of the most popular task running plugins. It provides :Dispatch and :Make commands that run tasks asynchronously when possible and fall back to synchronous execution otherwise.
" Run a command in the background
:Dispatch cargo test
" Run make asynchronously
:Make
" Set a default dispatch command for a filetype
let g:dispatch = 'npm test'
Dispatch also supports starting a persistent compiler or REPL with :Start, which opens a terminal or tmux pane for interactive use.
Vim Test
The vim-test plugin is specifically designed for running tests. It understands many test frameworks and can run the nearest test, the current file, or the entire suite.
" Run the test nearest to the cursor
:TestNearest
" Run all tests in the current file
:TestFile
" Run the entire test suite
:TestSuite
" Run the last test again
:TestLast
You can configure which strategy vim-test uses for execution:
let test#strategy = 'dispatch'
" or
let test#strategy = 'neoterm'
" or
let test#strategy = 'terminal'
AsyncRun
The asyncrun.vim plugin provides a simple :AsyncRun command that works on both Vim 8 and Neovim. Output is sent to the quickfix list, making it easy to navigate errors.
" Run a command asynchronously
:AsyncRun make
" Run tests
:AsyncRun npm test
" Open the quickfix window automatically
let g:asyncrun_open = 8
Neomake and ALE
For linting and continuous checking, neomake and ALE are the go-to plugins. They run linters and formatters asynchronously as you edit, surfacing warnings and errors in the sign column and location list.
" Enable ALE linting on save
let g:ale_lint_on_save = 1
" Configure linters for specific filetypes
let g:ale_linters = {
\ 'python': ['flake8', 'pylint'],
\ 'javascript': ['eslint'],
\ 'rust': ['cargo'],
\ }
Building a Custom Task Runner
If you want fine-grained control, you can build your own task runner. The following example creates a reusable function that runs a command asynchronously and displays output in a new buffer, with support for a configurable command per project.
" Define project-specific commands
let g:task_commands = {
\ 'build': 'make',
\ 'test': 'make test',
\ 'lint': 'make lint',
\ }
function! RunTask(name) abort
if !has_key(g:task_commands, a:name)
echoerr 'Unknown task: ' . a:name
return
endif
let cmd = g:task_commands[a:name]
let buf = bufnr('%')
" Create or reuse an output buffer
botright 15new
setlocal buftype=nofile bufhidden=wipe noswapfile
let output_buf = bufnr('%')
" Run the command and pipe output to the buffer
call termopen(cmd, {
\ 'on_exit': {job, exit_code, event ->
\ execute('echo "Task completed with exit code: " . ' . exit_code)
\ }
\ })
" Return to the original buffer
execute buf 'wincmd w'
endfunction
" Create commands for each task
command! Build call RunTask('build')
command! Test call RunTask('test')
command! Lint call RunTask('lint')
This approach gives you a clean, extensible foundation. You can add tasks, customize output handling, and even parse results to populate the quickfix list.
Integrating With the Quickfix List
The quickfix list is Vim's built-in mechanism for navigating errors. Many task runners automatically populate it, but you can also do it manually using :cexpr or :cfile.
" Populate quickfix from a command's output
:cexpr system('make 2>&1')
" Populate quickfix from a file
:cfile errors.txt
" Open the quickfix window
:copen
" Navigate errors
:cnext
:cprev
:cfirst
:clast
For custom tools that do not produce output in a standard format, you can write a custom errorformat:
" Example errorformat for a custom tool
:set errorformat=%f:%l:%c:\ %m
" This matches: filename:line:column: message
Best Practices
Keep Tasks Fast
Long-running synchronous tasks disrupt your flow. Whenever possible, use asynchronous execution. If a task takes more than a few seconds, it should run in the background so you can continue editing.
Use Filetype-Specific Configuration
Different languages have different build and test commands. Use autocommands to set makeprg and other options based on filetype:
augroup TaskConfig
autocmd!
autocmd FileType python setlocal makeprg=pytest
autocmd FileType rust setlocal makeprg=cargo\ build
autocmd FileType javascript setlocal makeprg=npm\ test
autocmd FileType go setlocal makeprg=go\ build\ ./...
augroup END
Map Frequently Used Tasks
Bind your most common tasks to memorable keys. A common convention is using the leader key followed by a short mnemonic:
" Build with leader-b
nnoremap <leader>b :Make<CR>
" Test with leader-t
nnoremap <leader>t :TestFile<CR>
" Test nearest with leader-T
nnoremap <leader>T :TestNearest<CR>
" Lint with leader-l
nnoremap <leader>l :Make lint<CR>
Handle Output Gracefully
Large outputs can slow Vim down or fill your screen. Consider truncating output, writing to a file, or using a dedicated output buffer. The quickfix list is ideal for error output because it provides built-in navigation.
Make Tasks Repeatable
Use :Make or a custom :RunLast command to repeat the last task without retyping it. This is invaluable during tight edit-test cycles:
command! RunLast call RunTask(g:last_task)
function! RunTask(name) abort
let g:last_task = a:name
" ... run the task ...
endfunction
Use Project Configuration Files
For project-specific tasks, store configuration in a file like .vim/tasks.vim or use editorconfig-style files. You can source these automatically:
if filereadable('.vim/tasks.vim')
source .vim/tasks.vim
endif
Putting It All Together
Here is a complete configuration that combines several techniques into a cohesive task running setup:
" --- Plugin setup (using vim-plug) ---
call plug#begin('~/.vim/plugged')
Plug 'tpope/vim-dispatch'
Plug 'janko/vim-test'
Plug 'skywind3000/asyncrun.vim'
call plug#end()
" --- AsyncRun opens quickfix automatically ---
let g:asyncrun_open = 8
" --- vim-test uses dispatch strategy ---
let test#strategy = 'dispatch'
" --- Filetype-specific makeprg ---
augroup TaskConfig
autocmd!
autocmd FileType python setlocal makeprg=python\ -m\ pytest
autocmd FileType rust setlocal makeprg=cargo\ test
autocmd FileType javascript setlocal makeprg=npm\ test
autocmd FileType go setlocal makeprg=go\ test\ ./...
autocmd FileType c setlocal makeprg=make
augroup END
" --- Key mappings ---
nnoremap <leader>b :Make<CR>
nnoremap <leader>t :TestFile<CR>
nnoremap <leader>T :TestNearest<CR>
nnoremap <leader>s :TestSuite<CR>
nnoremap <leader>l :AsyncRun make lint<CR>
nnoremap <leader>r :AsyncRun
" --- Quickfix navigation ---
nnoremap <leader>cn :cnext<CR>
nnoremap <leader>cp :cprev<CR>
nnoremap <leader>co :copen<CR>
nnoremap <leader>cc :cclose<CR>
This configuration gives you asynchronous builds, intelligent test running, quickfix navigation, and filetype-aware defaults, all bound to convenient leader mappings. Whether you are working on a small script or a large monorepo, this setup scales with your needs and keeps you inside Vim where you are most productive. Task running is not just a convenience feature; it is a fundamental part of an efficient Vim workflow that minimizes context switches and maximizes the speed of your edit-build-test loop.