Ruff Python formatting in Vim without plugins
The simplest way to integrate Ruff’s Python code formatter into Vim, no plugins or language servers needed. The same technique can also be used to integrate any command line formatter, for any file type.
There are lots of ways to add Ruff to Vim: Ruff’s own docs recommend using the vim-lsp plugin (which adds Language Server Protocol support to Vim), coc-pyright, or efm-langserver. There’s a standalone plugin just for Ruff called vim-ruff. Ruff can be added to the ALE plugin as a “fixer”. Etc.
But if all you want from Ruff is code formatting (not linting) there’s a simpler way to get that without any plugins or language servers - just add this to your vimrc file:
" ~/.vimrc
augroup equalprgs
autocmd!
autocmd FileType python setlocal equalprg=ruff\ format\ -
augroup END
You can now use Vim’s = command (see :help
=) to format Python code with Ruff.
For example:
- == to format the current line
- 2== to format the current line and the one below it
- =ip to format the current paragraph
- Select some code and hit = to format the selected code
How this works:
-
autocmd FileType python …configures a command to be executed whenever entering a Python file, so we can enable Ruff formatting in Python files only, without affecting what = does in other types of file. -
Wrapping the
autocmdin anaugroup(and withautocmd!as the group’s first line) is a common vimrc pattern that prevents theautocmdfrom being registered multiple times if you source your vimrc multiple times in a session: it causes the command to be cleared and re-created each time instead. -
setlocal sets an option only for the current buffer, without affecting any other (possibly non-Python) files Vim might have open.
-
Vim’s equalprg option specifices an external program for the = command to use for formatting.
equalprgprograms must accept the text to be formatted on stdin and return the formatted text on stdout.ruff format -is aruffcommand that does exactly that.
Reformatting the whole file
Surprisingly, Vim doesn’t have a simple command to reformat an entire file. You have to do gg to move the cursor to the start of the file, then = for reformat, then for the motion argument: G to move to the end of the file. You’re moving the cursor to the start of the file then asking Vim to reformat from the cursor to the end of the file, hence reformatting the entire file. So the full command to reformat the whole file is gg=G.
Your cursor will end up at the bottom of the file: you’ll have to do Ctrl + o twice to use Vim’s jump list to return to your original cursor position. Alternatively you can place a mark named f by prepending mf to the command, then restore the cursor to position f by appending `f to the end of the command: mfgg=G`f.
That’s a pretty long command! The following vimrc snippet adds a :Format
command (that you can type at Vim’s command line) to reformat the entire file,
along with a :Fmt alias and a keyboard shortcut <leader>f
(\f by default). The :Format command also uses
keepjumps so that
reformatting doesn’t modify your jumplist:
" ~/.vimrc
command! Format keepjumps normal! mfgg=G`f
command! Fmt Format
nnoremap <leader>f :Format<CR>
Automatically formatting on save
Some people like Vim to automatically reformat the file each time before saving it. If you want that, add something like this to your vimrc (replace the comma-separated list of filename extensions with the ones that you want to enable formatting-on-save for):
" ~/.vimrc
augroup autosave
autocmd!
autocmd BufWritePre *.py,*.js,*.html Format
augroup END
Sorting imports
ruff format doesn’t sort imports. To sort imports you have to use ruff
check. Specifically, ruff check --select I001 --fix-only - is the full
command to have Ruff sort any imports in the Python code (provided on stdin)
without doing any other linter checks or complaining about any violations.
If we want to both format code and sort imports in Vim we need an equalprg
that calls both ruff format and ruff check. Create a
~/.vim/equalprgs/python.py file with these contents:
#!/usr/bin/env python3
import functools
import subprocess
import sys
input_code = sys.stdin.read()
run = functools.partial(
subprocess.run,
capture_output=True,
text=True,
check=True,
)
try:
fmt = run(["ruff", "format", "-"], input=input_code)
check = run(
["ruff", "check", "--select", "I001", "--fix-only", "-"],
input=fmt.stdout,
)
except subprocess.CalledProcessError:
# Something went wrong with formatting the code.
# For example: perhaps input_code isn't valid Python.
# Print input_code so the code in Vim doesn't change.
print(input_code, end="")
# Exit with an error (Vim will notify the user of this).
sys.exit(1)
print(check.stdout, end="")
Change the equalprg setting in your vimrc to call the new Python script:
# ~/.vimrc
augroup vimrc_equalprgs
autocmd!
autocmd FileType python setlocal equalprg=~/.vim/equalprgs/python.py
augroup END
Now formatting code in Vim will also sort the imports! You can even select some imports and hit = to just sort those imports without affecting the rest of the file.
Reformatting indented sections of code
There’s a problem with using the = command to format just part of a file: a subsection of a file may no longer be valid Python code when taken out of the context of the rest of the file.
For example consider this function:
def sum(a, b):
result = a + b
print("Returning result: %i" % result)
return result
If you were to just select the body of this function (everything after the first line) and try to format it with = you’d get an error from Ruff. The reason is that you asked Ruff to format this code:
result = a + b
print("Returning result: %i" % result)
return result
Lines of code indented for no reason aren’t valid Python.
We can fix this by enhancing our equalprg script to remove any common
indentation before passing the code to Ruff:
#!/usr/bin/env python3
import functools
import subprocess
import sys
import textwrap
input_code = sys.stdin.read()
if not input_code:
sys.exit()
# Remove any common indentation from the lines of input_code.
dedented_code = textwrap.dedent(input_code)
run = functools.partial(
subprocess.run,
capture_output=True,
text=True,
check=True,
)
try:
fmt = run(["ruff", "format", "-"], input=dedented_code)
check = run(
["ruff", "check", "--select", "I001", "--fix-only", "-"],
input=fmt.stdout,
)
except subprocess.CalledProcessError:
print(input_code, end="")
sys.exit(1)
removed_indent = ""
# Figure out what common indentation (if any) was removed.
for input_line, dedented_line in zip(
input_code.splitlines(), dedented_code.splitlines()
):
if input_line and not input_line.isspace():
removed_indent = input_line[: len(input_line) - len(dedented_line)]
break
# Re-indent the formatted code before sending it back to Vim.
reindented_code = textwrap.indent(check.stdout, removed_indent)
print(reindented_code, end="")
Now you can use = to format just the body of a function, class, etc, or just a single line or set of lines within a block.
Of course, there’s lots of other ways for a subset of a Python file to not be valid on its own, besides just indentation. If you try to format just the signature of a method, or the opening line of a loop, etc, you’ll get an error from Ruff. At least some of these cases could be fixed by further enhancing our script, but it’s probably not worth the trouble. black-machiatto is a script that attempts to do this for the Black code formatter.
Formatting Python code in other files
The commands we’ve seen so far only use Ruff when editing a Python file, in any other file they use whatever formatting is configured for that file type.
What if we want to format some Python code within another type of file, for
example within <code> tags in an HTML file, or a code block in a Markdown file?
Here’s a vimrc snippet that adds :FormatPython and :FmtPython commands that
format the selected text as Python and work in any file type:
" ~/.vimrc
command! -range FormatPython <line1>,<line2>!~/.vim/equalprgs/python.py
command! -range FmtPython <line1>,<line2>FormatPython
Now if you’re in a Markdown file, for example, you can select a block of Python
code and do :'<,'>FormatPython to reformat the code using Ruff (thankfully
you don’t have to type the '<,'> bit: Vim enters that for you automatically
when you have some text selected and hit :).
It should also be possible to use
operatorfunc and
g@ to add a
Python-formatting operator (say <leader>fp) that accepts a
motion, so you can do things like <leader>fpip to reformat
the current paragraph as Python code when editing a non-Python file (the same
as =ip in a Python file). I’ll leave that one as an exercise for the reader!
equalprg versus formatprg
Vim actually has two commands for formatting text: as well as the =
command that we’ve been using (customizable via equalprg) there’s also a
gq command (customizable
via formatprg).
Vim’s docs don’t make it super clear why both commands exist or what each
should be used for, and many people use gq and formatprg rather
than = and equalprg to call external formatters like Ruff.
I think = and equalprg are more appropriate for calling external
code formatters.
= sort of formats code by default: if no equalprg is set the
default, internal behavior tries to fix the indentation of Python code (and
seems to understand a little of Python syntax to do so). So = seems
like the appropriate command to customize with smarter code formatting from Ruff.
gq, on the other hand, blindly hard-wraps text to
textwidth by default,
if no formatprg is set. The default behavior of gq isn’t
appropriate for Python code: it’ll break your code by blindly joining lines
together. It’s meant for hard-wrapping plain or prose text.
gq‘s default behavior can be useful when editing Python files: it’s perfect for hard-wrapping comments and multi-line strings (including docstrings) which code formatters like Ruff and Black usually don’t touch: select a comment or string and hit gq to re-wrap it (or use gqip to rewrap the current paragraph, gqq to wrap the current line, 2gqq to wrap the current line and the one below it, etc). I don’t want to lose this useful functionality by hijacking gq to call Ruff.
So customizing = to call Ruff and keeping gq‘s default behavior for when you want to re-wrap a comment or docstring seems right to me.
Incidentally, if you want higher-quality hard-wrapping from gq you
can set formatprg to call out to par, a
standalone text-wrapping program. There’s an old episode of Vimcasts about
it.
Conclusion
I said this was going to be the simplest way to add Ruff formatting to Vim and
I’ve ended up writing a 40-line Python script. So it goes. But for that we’re
able to format not just entire files but also subsets of code within files,
which most code formatting plugins can’t do. We’ve got import sorting as well
as formatting. Our equalprg-based approach can be used to integrate any
command line formatting programs for any file types we want in the same way,
without having to search for plugins. And we also have gq for
hard-wrapping comments and docstrings.