-- Leo's gemini proxy

-- Connecting to tilde.pink:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini;

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


v:lnum holds the first line to be formatted

v:count holds the number of lines



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:


it always formats the entire file, not the motion text

it will not format code with unbalanced parens, leaving it as is

guix style clobbers guile scripts called via other hash lines

in this particular code, the systemlist call might be neovim specific and require fixing for Vim proper


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 <Enter> 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







-- Response ended

-- Page fetched on Sun May 19 10:03:28 2024