Zprint is a Clojure formatter. By default, it fully reformats a top-level form, replacing all the existing indentation and newlines with its own. This works well if you don’t mind the disruption.
I often work in a codebase where such heavy-handed reformatting would be
unwelcome, so I use zprint in
:respect-nl
mode. This usually results in less code movement, but sometimes takes
more vertical space than optimal, since zprint isn’t allowed to fold
existing newlines.
Easy way
The easy way is to configure zprint per project.
;; ~/.zprintrc
{:search-config? true}
;; src/project/.zprintrc
{:style :respect-nl}
And then configure LazyVim to use zprint for Clojure:
-- ~/.config/nvim/lua/plugins/zprint.lua
return {
'stevearc/conform.nvim',
opts = {
formatters_by_ft = {
clojure = { 'zprint' },
},
}
}
This works but there’s no easy way to toggle to the other mode on demand, except for changing the config.
Hard way
I use multiple key mappings to run zprint different ways. The main mapping uses
whatever is configured in .zprintrc. The alternate mappings choose explicitly
between :respect-nl and :respect-nl-off.
Here are the default mappings provided by LazyVim:
<space>cfreformats the current file.gqis the format operator, so it expects a visual selection or motion. Normally,gqqwould format the current line, but for zprint this means to reformat the current top-level form.
I augment these with an additional mapping:
=is usually the indent operator, but I map it to do the same asgqbecause I don’t need separate indent and format operations. Therefore, with zprint,==reformats the current top-level form.
And then, to run zprint with an explicit style:
<space>=1uses:respect-nl-off.<space>=2uses:respect-nl.<space>=3uses:indent-only.
Like the other mappings, I can repeat the last key to reformat the current
top-level form. For example, <space>=11 has zprint fully reformat the current
function.
What’s hard?
The hard part is mapping an operator. This means that the new mappings should work with a following motion, and they should work equally well with a visual selection.
You should read the doc behind that link, then this utility function will make sense:
-- ~/.config/nvim/lua/config/keymaps.lua
---@class (exact) OperatorRegisterOpts
---@field setup function
---@field execute function | string
---@field cleanup function
---@param name string
---@param opts OperatorRegisterOpts
function operator_register(name, opts)
_G[name] = function(motion_type)
if motion_type == nil then
vim.opt.opfunc = 'v:lua.' .. name
return 'g@' -- calls back to this function
end
-- boilerplate save, see :help g@
local sel_save = vim.opt.selection
local reg_save = vim.fn.getreginfo('"')
local cb_save = vim.opt.clipboard
local visual_marks_save = { vim.fn.getpos("'<"), vim.fn.getpos("'>") }
-- boilerplate setup
vim.opt.clipboard = ''
vim.opt.selection = 'inclusive'
-- custom setup
local status, result = pcall(opts.setup, motion_type)
local saved = status and result or nil
local err = not status and result or nil
if status then
-- convert motion to visual
local commands = {
char = '`[v`]',
line = '`[V`]',
block = '`[\\<c-v>`]',
}
-- execute
if type(opts.execute) == 'string' then
vim.cmd('noautocmd keepjumps normal! ' .. commands[motion_type] .. opts.execute)
else
status, result = pcall(opts.execute --[[@as function]], motion_type)
if not status then
err = result
end
end
-- custom cleanup
status, result = pcall(opts.cleanup, saved)
if not status then
err = result
end
end
-- boilerplate cleanup
vim.fn.setreg('"', reg_save)
vim.fn.setpos("'<", visual_marks_save[0])
vim.fn.setpos("'>", visual_marks_save[1])
vim.opt.clipboard = cb_save
vim.opt.selection = sel_save
-- if setup/execute/cleanup failed, raise error
if err then
error(err)
end
end
end
Reformatting prose
Did you know you can reformat prose at the current textwidth with
gw? It’s a handy alternative to
gq.
To demonstrate operator_register, here’s a mapping that I use for
reformatting prose at 80 columns,
regardless of the current textwidth:
-- ~/.config/nvim/lua/config/keymaps.lua
-- Reformat current paragraph with 80 textwidth.
operator_register('op_reformat_prose', {
setup = function()
local saved = {
textwidth = vim.opt.textwidth,
autoindent = vim.opt.autoindent,
indentexpr = vim.opt.indentexpr,
}
vim.opt.textwidth = 80
vim.opt.autoindent = true
vim.opt.indentexpr = 'indent()'
return saved
end,
execute = 'gw',
cleanup = function(saved)
vim.opt.textwidth = saved.textwidth
vim.opt.autoindent = saved.autoindent
vim.opt.indentexpr = saved.indentexpr
end,
})
-- Bind to gW since the default prose-reformatting is gw.
vim.keymap.set(
{'n', 'x'},
'gW',
'v:lua.op_reformat_prose()',
{ desc = 'Reformat prose (80 columns)', expr = true, silent = true }
)
-- For completeness, operate immediately on the current line with gWgW.
-- This is hardly ergonomic. Most of the time I want to reformat an entire
-- paragraph, which is gWap.
vim.keymap.set(
'n',
'gWgW',
"v:lua.op_reformat_prose() .. '_'",
{ desc = 'Reformat prose (80 columns)', expr = true, silent = true }
)
Zprint mappings
With the operator_register function in hand, here’s the code for the zprint
mappings.
-- ~/.config/nvim/lua/plugins/zprint.lua
---@param style string
local function zprinter(style)
return {
meta = {
url = 'https://github.com/kkinnear/zprint',
description = string.format('Zprint with %s', style),
},
command = 'zprint',
args = { string.format('{:style %s}', style) },
range_args = function(self, ctx)
return {
string.format(
'{:style %s :input {:range {:start %d :end %d :use-previous-!zprint? true :continue-after-!zprint-error? true}}}',
style,
ctx.range.start[1] - 1,
ctx.range['end'][1] - 1
),
}
end,
}
end
return {
'stevearc/conform.nvim',
opts = {
formatters = {
zprint_respect = zprinter(':respect-nl'),
zprint_disrespect = zprinter(':respect-nl-off'),
zprint_indent = zprinter(':indent-only'),
},
formatters_by_ft = {
-- This is the formatter that will be used by <leader>cf, gq operator, and
-- also the = operator via keymaps.lua. See
-- https://github.com/stevearc/conform.nvim/blob/master/lua/conform/formatters/zprint.lua
clojure = { 'zprint' },
},
-- Custom opts entry, not consumed by conform. See keymaps.lua
formatters_by_ft_alt = {
clojure = {
-- First alt, available as <leader>=1
-- or conform.format({opts: {formatters: {'zprint_disrespect'}}})
{ 'zprint_disrespect' },
-- Second alt, available as <leader>=2
-- or conform.format({opts: {formatters: {'zprint_respect'}}})
{ 'zprint_respect' },
-- Third alt, available as <leader>=3
-- or conform.format({opts: {formatters: {'zprint_indent'}}})
{ 'zprint_indent' },
},
},
},
}
-- ~/.config/nvim/lua/config/keymaps.lua
-- Format code with =
vim.keymap.set({ 'n', 'x' }, '=', 'gq', { desc = 'Format code', remap = true })
vim.keymap.set('n', '==', 'gqq', { desc = 'Format code', remap = true })
-- Reformat code with alternate formatter with <leader>=1, <leader>=2, etc.
for i = 1, 9 do
operator_register('op_reformat_code_alt_' .. i, {
setup = function()
local conform = require('conform')
local saved = {
formatters_by_ft = conform.formatters_by_ft,
}
-- Our alternate formatters are stored in the opts for conform.nvim, and we
-- have to fetch them from there because they don't carry into conform.
local alt_formatters = LazyVim.opts('conform.nvim').formatters_by_ft_alt
conform.formatters_by_ft = {}
for k, v in pairs(alt_formatters) do
conform.formatters_by_ft[k] = v[i]
end
return saved
end,
execute = 'gq',
cleanup = function(saved)
local conform = require('conform')
conform.formatters_by_ft = saved.formatters_by_ft
end,
})
vim.keymap.set(
{ 'n', 'x' },
'<leader>=' .. i,
'v:lua.op_reformat_code_alt_' .. i .. '()',
{ desc = 'Format code (alt ' .. i .. ')', expr = true, silent = true }
)
vim.keymap.set(
'n',
'<leader>=' .. i .. i,
'v:lua.op_reformat_code_alt_' .. i .. "() .. '_'",
{ desc = 'Format code (alt ' .. i .. ')', expr = true, silent = true }
)
end
∿