← Back to DevBytes

Vim Task Running: Complete Guide

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:

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.

— Ad —

Google AdSense will appear here after approval

← Back to all articles