Scheme Code formatting in Vim

2023-09-17 @rrobin

This is how I format scheme code in Vim. Admittedly my setup is only useful if you follow the GUIX style, but maybe the notes on Vim are useful too.

A quick intro to code formatting in Vim

Vim provides a few commands to format your code. I think the most common methods are to rely on (gq) and =. In some cases people use LSP provided formatters, but I don't cover it here.

={motion}

'=' filters lines through an external program (defined in 'equalprg'). The purpose of the command is to indent lines.

If 'equalprg' is not defined, then Vim will fallback to an internal implementation. This fallback will depend on other options, in particular if 'lisp', 'indentexpr' or 'cindent'.

If 'lisp' is set, the default in scheme, then 'indentexpr' will be completely ignored. This can be changed by changing 'lispoptions'.

gq[motion]

gq - formats lines that a motion moves over. This uses either an external program, custom expression, or an internal implementation.

'formatprg' is an option that holds the path of a program, used to format code with the (gq) operator. The program takes input on stdin and returns output on stdout.

'formatexpr' holds a vim expression used to format code (usually a function call) with the (gq) operator. This option takes precedence over 'formatprg'. Input is defined by the following variables

Formatting scheme in GUIX

My main target is to format code according to the Guix rules. Guix provides a tool for this called guix style, that rewrites a file in place:

guix style --whole-file filename.scm

This clashes a bit with the way that Vim formats code, since it expects to process the entire file, while Vim can try to format a range of lines, for a buffer without a file.

Here is an example function for a formatexpr. Note that you should only set this in a buffer option (e.g. via autocommands for scheme files):

function! s:guixStyle()
	let tmpfile = tempname()
	" Unlike the usual formatexpr we ignore the actual motion range
	" because guix style needs a full file to be formatted
	"let startl = v:lnum
	"let endl = v:lnum + v:count - 1
	let startl = 1 " deletebufline does not accept 0 here
	let endl = "$"
	let oldlines = getline(startl, endl)

	if writefile(oldlines, tmpfile, "s") <> 0
		echohl ErrorMsg
		echom "Failed to setup tmp file for guix style"
		echohl None
		return 0
	endif

	let out = systemlist(['guix', 'style', '--whole-file', tmpfile], '')
	if v:shell_error
		echohl ErrorMsg
		echom "Failed to run guix style:"
		for line in out
			echom "    "..line
		endfor
		echom "Could not format scheme"
		echohl None
	else
		let newlines = readfile(tmpfile)
		call deletebufline("", startl, endl)
		call setline(startl, newlines)
	endif

	return 0
endfunction

setlocal formatexpr=s:guixStyle()

The code is relatively straightforward, it copies buffer contents into a temporary file and calls guix style, replacing buffer lines on success. There are a few caveats though:

That still leaves us with '='. What happens when we try to indent scheme code in Vim? According to the documentation if options 'lisp' and 'autoindent' are set then:

When is typed in insert mode set the indent for the next line to Lisp standards (well, sort of).

so it is likely that 'equalprg' is not used (since 'lisp' is on). This means that most other options are ignored.

In particular the default settings for lisp indentation seem to always indent code to align with the operator (based on 'lispwords'), rather than a fixed number of spaces (2 in guix style).

This is not what I want, so I've adjusted this with a custom 'indentexpr'.

function! s:schemeIndent(line)
	if a:line <= 1
		return 0
	endif

	let prevline = a:line-1
	let previndent = indent(prevline)
	let openp = count(getline(prevline), "(")
	let closep = count(getline(prevline), ")")
	if openp == closep
		return previndent
	elseif openp > closep
		return previndent + shiftwidth()
	else
		return previndent - shiftwidth()
	endif
endfunction

setlocal indentexpr=s:schemeIndent(v:lnum)
setlocal expandtab
setlocal shiftwidth=2
setlocal tabstop=2
setlocal softtabstop=2
setlocal lispoptions=expr:1

However this is a pretty naive implementation that works by counting parens and relying on the indentation of the previous line.

References

=> GUIX - Formatting Code | Riastradh's Lisp Style Rules

Proxy Information
Original URL
gemini://tilde.pink/~rrobin/2023-09-17-scheme-code-formatting-in-vim.gmi
Status Code
Success (20)
Meta
text/gemini;
Capsule Response Time
22.185142 milliseconds
Gemini-to-HTML Time
0.896363 milliseconds

This content has been proxied by September (ba2dc).