emacs-indentation


Custom indentation in GNU Emacs

This page is an attempt to make more clear what we have to do to have custom indentation for Scheme code when editing with GNU Emacs.

To customise the Scheme mode of Emacs we add a function to the scheme-mode-hook in the .emacs configuration file:

(add-hook 'scheme-mode-hook 'my-scheme-mode-hook)

(defun my-scheme-mode-hook ()
  "Custom Scheme mode hook."
  (interactive)
  (make-local-variable 'scheme-indent-function)
  ...)

scheme-indent-function is invoked to compute the number of spaces to be used to indent a given line of code.

Using the built in indentation features

Emacs can indent forms in this way: the first n arguments have deep indentation, the rest of the arguments have normal indentation. Let's take this form:

 (myform (one) (two) (three) (four) (five) (six) (seven)) 

if we do nothing we get the following:

 (myform 
  (one) 
  (two) 
  (three) 
  (four) 
  (five) 
  (six) 
  (seven)) 

if we put in my-scheme-mode-hook:

(put 'myform 'scheme-indent-function 1)

we get:

 (myform 
     (one) 
   (two) 
   (three) 
   (four) 
   (five) 
   (six) 
   (seven)) 

if we put:

(put 'myform 'scheme-indent-function 2)

we get:

 (myform 
     (one) 
     (two) 
   (three) 
   (four) 
   (five) 
   (six) 
   (seven)) 

if we put:

(put 'myform 'scheme-indent-function 3)

we get:

 (myform 
     (one) 
     (two) 
     (three) 
   (four) 
   (five) 
   (six) 
   (seven)) 

got it?

Indenting custom forms

Let's say that we want to indent the compensate form like this:

 (compensate 
     (body) 
     (body) 
   (with 
    (other) 
    (other))) 

that is: 4 spaces for the body and 2 spaces for the with form. The following customisation works with GNU Emacs 21.3. While reading the following and the Emacs documentation: we have to recall that sexp in Emacs jargon is an expression delimited by parentheses, in the case of Scheme code a sexp is a form.

To obtain custom indentation for compensate we register a new function, in the body of my-scheme-mode-hook:

(put 'compensate 'scheme-indent-function 'my-scheme-indent-compensate)

this means: whenever Emacs needs to indent a line inside the compensate sexp, invoke the function my-scheme-indent-compensate.

The function must work in the following cases:

indent-region (C-M-\)

lisp-indent-line (tab key)

indent-sexp (C-M-q)

In the first two cases the function can return a number representing the column to indent a line to: the function is invoked once for each line.

But in the last case: if the return value is a number the function is invoked only once for the first line of the sexp, and that number is taken as column number to indent each of the following lines in the sexp. If this is not what we want: we have to return a list of two elements, the first being the column to indent the current line to and the second being the (point) at the beginning of the containing sexp. That way we tell Emacs to invoke the function once for each line.

There is no way inside the function to understand which case we are in: so for compensate we always return the list of two arguments.

To understand what is going on:

  1. documentation of calculate-lisp-indent in lisp-mode.el;
  2. documentation of parse-partial-sexp in the elisp guide, the state argument of the function appears to be a parse-partial-sexp return value;
  3. reverse engineer scheme-indent-function in scheme.el, this function is invoked by its last form:
    (funcall method state indent-point normal-indent)
    
 (defun my-scheme-indent-compensate (state indent-point normal-indent) 
   (let ((containing-sexp-start (elt state 1)) 
         containing-sexp-point 
         containing-sexp-column 
         body-indent) 
     ;;Move to the start of containing sexp, calculate its 
     ;;indentation, store its point and move past the function 
     ;;symbol so that we can use 'parse-partial-sexp'. 
     ;; 
     ;;'lisp-indent-function' guarantees that there is at least 
     ;;one word or symbol character following open paren of 
     ;;containing sexp. 
     (forward-char 1) 
     (goto-char containing-sexp-start) 
     (setq containing-sexp-point (point)) 
     (setq containing-sexp-column (current-column)) 
     (setq body-indent (+ lisp-body-indent containing-sexp-column)) 
     (forward-char 1)    ;Move past the open paren. 
     (forward-sexp 1)    ;Move to the next sexp. 
  
     ;;Now go back to the beginning of the line holding 
     ;;the indentation point. 
     (parse-partial-sexp (point) indent-point 1 t) 
     (while (and (< (point) indent-point) 
                 (condition-case () 
                     (progn 
                       (forward-sexp 1) 
                       (parse-partial-sexp (point) indent-point 1 t)) 
                   (error nil)))) 
     ;;Point is sitting on the first char of the line. 
     (forward-to-indentation 0)  ;Move to the first non-blank char. 
     (forward-char 1)            ;Move past the open paren. 
     ;;Point is sitting on first character of sexp. 
     (list (if (looking-at "\\<with\\>") 
               body-indent 
             (+ 2 body-indent)) containing-sexp-point))) 

category-emacs