Customize Elisp plist indentation
Asked Answered
A

3

15

I don't like how plists are indented in Elisp.

;; current   desired   Python (for comparison)
;; '(a 1     '(a 1     {'a': 1,
;;     b 2     b 2      'b': 2,
;;     c 3)    c 3)     'c': 3}

Tried on M-x emacs-version 24.3.1, ran emacs -Q, typed the plist and pressed C-x h C-M-\.

This indentation makes sense when it isn't a list:

(mapcar (lambda (x) (x + 1))
        '(1 2 3 4))

How do I change formatting settings so that only plists (or, if that's impossible, all quoted lists) have the desired rectangular indentation, but indentation of everything else stays the same? I need this stored locally in an .el file, so that when I edit this file, it is indented as desired, but this behavior doesn't end up anywhere else.

Applaud answered 4/3, 2014 at 8:45 Comment(2)
For this reason and many others, I recommend the use of alists rather than plists.Scarper
See also emacs.stackexchange.com/questions/10230/… which is a duplicate of this with some very instructive answers.Ruella
T
12

Found it:

(setq lisp-indent-function 'common-lisp-indent-function)

Here's a sample file:

(setq x  '(a 1
           b 2
           c 3))

;;; Local Variables:
;;; lisp-indent-function: common-lisp-indent-function
;;; End:

I'll just dump my whole indentation config here:

(setq lisp-indent-function 'common-lisp-indent-function)
(put 'cl-flet 'common-lisp-indent-function
     (get 'flet 'common-lisp-indent-function))
(put 'cl-labels 'common-lisp-indent-function
     (get 'labels 'common-lisp-indent-function))
(put 'if 'common-lisp-indent-function 2)
(put 'dotimes-protect 'common-lisp-indent-function
     (get 'when 'common-lisp-indent-function))
Trifurcate answered 4/3, 2014 at 8:54 Comment(10)
Why do you need it file-local? The doc says it might be risky:)Trifurcate
So the user gets the file from you and wants to edit it?Trifurcate
I still don't get how a user should edit a library. Do they send the changes back to you?Trifurcate
Do you know how to make only plists behave as in Common Lisp, but everything else stay the same?Applaud
@sindikat There is none, because there is no way for the indentation engine to figure out whether your quoted form is a plist or not. After all, plists just look like function calls.Threonine
@lunaryorn, not true - the quote kind of gives them away:)Trifurcate
@lunaryorn I suspect you're right, lists (all kinds) look just like function calls to the indentation engine, even if there's a quote. I think common-lisp-indent-function works around by setting general indent to 1 and then redefine indent for all def functions.Applaud
@Trifurcate Er, not quite. What about quoted code, as in eval-after-load?Threonine
@sindikat Sounds like a reasonable conjecture. Presumably, common-lisp-indent-function checks whether the leading symbol has a function definition. Which will make the indentation depend on the currently loaded libraries, of course, which has drawbacks as well.Threonine
it seems like lists that begin with a :key symbol should be easy to detect though, right?Gavrilla
E
5

You can fix this (in my opinion) bug by overriding lisp-indent-function. The original source of the hack was this Github Gist, which was referenced with some more explanation from this Emacs Stack Exchange answer.

However, I was very uncomfortable overriding a core function like this. For one, it's very opaque—how is a reader supposed to tell what is changed? And worse—what if the official definition of lisp-indent-function changed in the future? How would I know that I needed to update my hack?

As a response, I created the library el-patch, which is specifically designed to address this problem. After installing the package, you can override lisp-indent-function as follows:

(el-patch-defun lisp-indent-function (indent-point state)
  "This function is the normal value of the variable `lisp-indent-function'.
The function `calculate-lisp-indent' calls this to determine
if the arguments of a Lisp function call should be indented specially.

INDENT-POINT is the position at which the line being indented begins.
Point is located at the point to indent under (for default indentation);
STATE is the `parse-partial-sexp' state for that position.

If the current line is in a call to a Lisp function that has a non-nil
property `lisp-indent-function' (or the deprecated `lisp-indent-hook'),
it specifies how to indent.  The property value can be:

* `defun', meaning indent `defun'-style
  (this is also the case if there is no property and the function
  has a name that begins with \"def\", and three or more arguments);

* an integer N, meaning indent the first N arguments specially
  (like ordinary function arguments), and then indent any further
  arguments like a body;

* a function to call that returns the indentation (or nil).
  `lisp-indent-function' calls this function with the same two arguments
  that it itself received.

This function returns either the indentation to use, or nil if the
Lisp function does not specify a special indentation."
  (el-patch-let (($cond (and (elt state 2)
                             (el-patch-wrap 1 1
                               (or (not (looking-at "\\sw\\|\\s_"))
                                   (looking-at ":")))))
                 ($then (progn
                          (if (not (> (save-excursion (forward-line 1) (point))
                                      calculate-lisp-indent-last-sexp))
                              (progn (goto-char calculate-lisp-indent-last-sexp)
                                     (beginning-of-line)
                                     (parse-partial-sexp (point)
                                                         calculate-lisp-indent-last-sexp 0 t)))
                          ;; Indent under the list or under the first sexp on the same
                          ;; line as calculate-lisp-indent-last-sexp.  Note that first
                          ;; thing on that line has to be complete sexp since we are
                          ;; inside the innermost containing sexp.
                          (backward-prefix-chars)
                          (current-column)))
                 ($else (let ((function (buffer-substring (point)
                                                          (progn (forward-sexp 1) (point))))
                              method)
                          (setq method (or (function-get (intern-soft function)
                                                         'lisp-indent-function)
                                           (get (intern-soft function) 'lisp-indent-hook)))
                          (cond ((or (eq method 'defun)
                                     (and (null method)
                                          (> (length function) 3)
                                          (string-match "\\`def" function)))
                                 (lisp-indent-defform state indent-point))
                                ((integerp method)
                                 (lisp-indent-specform method state
                                                       indent-point normal-indent))
                                (method
                                 (funcall method indent-point state))))))
    (let ((normal-indent (current-column))
          (el-patch-add
            (orig-point (point))))
      (goto-char (1+ (elt state 1)))
      (parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t)
      (el-patch-swap
        (if $cond
            ;; car of form doesn't seem to be a symbol
            $then
          $else)
        (cond
         ;; car of form doesn't seem to be a symbol, or is a keyword
         ($cond $then)
         ((and (save-excursion
                 (goto-char indent-point)
                 (skip-syntax-forward " ")
                 (not (looking-at ":")))
               (save-excursion
                 (goto-char orig-point)
                 (looking-at ":")))
          (save-excursion
            (goto-char (+ 2 (elt state 1)))
            (current-column)))
         (t $else))))))
Eclat answered 21/3, 2017 at 2:48 Comment(0)
D
0

Here is another less heavyweight solution, based on emacsql-fix-vector-indentation. An advice around calculate-lisp-indent is sufficient.

This only works for plists that use keywords as keys, but that covers a majority of plists. To make this work on quoted lists instead, you could change the looking-at regexp to detect the ' or "`", but that will not cover, say, a nested list.

This can further be packaged up into a minor mode if there is a need to turn it off.

(defun my/inside-plist? ()
  "Is point situated inside a plist?

We determine a plist to be a list that starts with a keyword."
  (let ((start (point)))
    (save-excursion
      (beginning-of-defun)
      (let ((sexp (nth 1 (parse-partial-sexp (point) start))))
        (when sexp
          (setf (point) sexp)
          (looking-at (rx "(" (* (syntax whitespace)) ":")))))))
(define-advice calculate-lisp-indent (:around (func &rest args)
                                      plist)
  (if (save-excursion
        (beginning-of-line)
        (my/inside-plist?))
      (let ((lisp-indent-offset 1))
        (apply func args))
    (apply func args)))
Demurral answered 11/6, 2022 at 0:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.