tweak(emacs-lisp): elisp indentation for data/plists
This was adapted from https://www.reddit.com/r/emacs/comments/d7x7x8/finally_fixing_indentation_of_quoted_lists/. It fixes the indentation of quoted data (and plist keywords) so they're indented like data, rather than function arguments, like so: BEFORE: `(foo bar baz doom emacs) '(:foo 1 :bar 2 :baz 3) '(:foo 1 2 3 :bar 4) (:foo 1 :bar 2) (:foo 1 ;; test comment :bar 2) (:foo 1 2 :bar 3) AFTER: `(foo bar baz doom emacs) '(:foo 1 :bar 2 :baz 3) '(:foo 1 2 3 :bar 4) ;; only align unquoted keywords if keywords start each line: (:foo 1 :bar 2) (:foo 1 ;; test comment :bar 2) (:foo 1 2 :bar 3) Also, I added a way to declare that plists in an macro's arguments should be indented like data: (put 'map! 'indent-plists-as-data t) BEFORE: (map! :localleader :map emacs-lisp-mode-map (:prefix ("d" . "debug") "f" #'+emacs-lisp/edebug-instrument-defun-on "F" #'+emacs-lisp/edebug-instrument-defun-off)) AFTER: (map! :localleader :map emacs-lisp-mode-map (:prefix ("d" . "debug") "f" #'+emacs-lisp/edebug-instrument-defun-on "F" #'+emacs-lisp/edebug-instrument-defun-off)) There was a third improvement I was hoping to include, namely, proper indentation of interpolated forms: BEFORE: `(foo bar ,(if t 'baz 'boo)) `(foo bar (if t baz boo)) AFTER: `(foo bar ,(if t 'baz 'boo)) `(foo bar (if t baz boo)) But this was removed because it breaks indentation for quoted macro forms (or dynamic elisp programming): BEFORE: (good) `(with-temp-buffer (if (always) (message "Hello %s" user-login-name) (message "Goodbye %s" user-login-name))) AFTER: (bad) `(with-temp-buffer (if (always) (message "Hello %s" user-login-name) (message "Goodbye %s" user-login-name))) Ref: https://www.reddit.com/r/emacs/comments/d7x7x8/finally_fixing_indentation_of_quoted_lists/'
This commit is contained in:
parent
3fe1641937
commit
e71daf5cc3
2 changed files with 219 additions and 9 deletions
|
@ -365,9 +365,218 @@ library/userland functions"
|
|||
(throw 'matcher t)))))))
|
||||
nil))
|
||||
|
||||
;; HACK Fontification is already expensive enough. We byte-compile
|
||||
;; `+emacs-lisp-highlight-vars-and-faces' and `+emacs-lisp-truncate-pin' to
|
||||
;; ensure they run as fast as possible:
|
||||
(dolist (fn '(+emacs-lisp-highlight-vars-and-faces +emacs-lisp-truncate-pin))
|
||||
(unless (byte-code-function-p (symbol-function fn))
|
||||
(with-no-warnings (byte-compile fn))))
|
||||
|
||||
;;
|
||||
;;; Advice
|
||||
|
||||
;;;###autoload (put 'map! 'indent-plists-as-data t)
|
||||
;;;###autoload
|
||||
(defun +emacs-lisp--calculate-lisp-indent-a (&optional parse-start)
|
||||
"Add better indentation for quoted and backquoted lists.
|
||||
|
||||
Intended as :override advice for `calculate-lisp-indent'.
|
||||
|
||||
Adapted from 'https://www.reddit.com/r/emacs/comments/d7x7x8/finally_fixing_indentation_of_quoted_lists/'."
|
||||
;; This line because `calculate-lisp-indent-last-sexp` was defined with
|
||||
;; `defvar` with it's value ommited, marking it special and only defining it
|
||||
;; locally. So if you don't have this, you'll get a void variable error.
|
||||
(defvar calculate-lisp-indent-last-sexp)
|
||||
(save-excursion
|
||||
(beginning-of-line)
|
||||
(let ((indent-point (point))
|
||||
state
|
||||
;; setting this to a number inhibits calling hook
|
||||
(desired-indent nil)
|
||||
(retry t)
|
||||
calculate-lisp-indent-last-sexp containing-sexp)
|
||||
(cond ((or (markerp parse-start) (integerp parse-start))
|
||||
(goto-char parse-start))
|
||||
((null parse-start)
|
||||
(beginning-of-defun))
|
||||
((setq state parse-start)))
|
||||
(unless state
|
||||
;; Find outermost containing sexp
|
||||
(while (< (point) indent-point)
|
||||
(setq state (parse-partial-sexp (point) indent-point 0))))
|
||||
;; Find innermost containing sexp
|
||||
(while (and retry
|
||||
state
|
||||
(> (elt state 0) 0))
|
||||
(setq retry nil)
|
||||
(setq calculate-lisp-indent-last-sexp (elt state 2))
|
||||
(setq containing-sexp (elt state 1))
|
||||
;; Position following last unclosed open.
|
||||
(goto-char (1+ containing-sexp))
|
||||
;; Is there a complete sexp since then?
|
||||
(if (and calculate-lisp-indent-last-sexp
|
||||
(> calculate-lisp-indent-last-sexp (point)))
|
||||
;; Yes, but is there a containing sexp after that?
|
||||
(let ((peek (parse-partial-sexp calculate-lisp-indent-last-sexp
|
||||
indent-point 0)))
|
||||
(if (setq retry (car (cdr peek))) (setq state peek)))))
|
||||
(if retry
|
||||
nil
|
||||
;; Innermost containing sexp found
|
||||
(goto-char (1+ containing-sexp))
|
||||
(if (not calculate-lisp-indent-last-sexp)
|
||||
;; indent-point immediately follows open paren. Don't call hook.
|
||||
(setq desired-indent (current-column))
|
||||
;; Find the start of first element of containing sexp.
|
||||
(parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t)
|
||||
(cond ((looking-at "\\s(")
|
||||
;; First element of containing sexp is a list. Indent under
|
||||
;; that list.
|
||||
)
|
||||
((> (save-excursion (forward-line 1) (point))
|
||||
calculate-lisp-indent-last-sexp)
|
||||
;; This is the first line to start within the containing sexp.
|
||||
;; It's almost certainly a function call.
|
||||
(if (or
|
||||
;; Containing sexp has nothing before this line except the
|
||||
;; first element. Indent under that element.
|
||||
(= (point) calculate-lisp-indent-last-sexp)
|
||||
|
||||
(or
|
||||
;; Align keywords in plists if each newline begins with
|
||||
;; a keyword. This is useful for "unquoted plist
|
||||
;; function" macros, like `map!' and `defhydra'.
|
||||
(when-let ((first (elt state 1))
|
||||
(char (char-after (1+ first))))
|
||||
(and (eq char ?:)
|
||||
(ignore-errors
|
||||
(or (save-excursion
|
||||
(goto-char first)
|
||||
;; FIXME Can we avoid `syntax-ppss'?
|
||||
(when-let* ((parse-sexp-ignore-comments t)
|
||||
(end (scan-lists (point) 1 0))
|
||||
(depth (ppss-depth (syntax-ppss))))
|
||||
(and (re-search-forward "^\\s-*:" end t)
|
||||
(= (ppss-depth (syntax-ppss))
|
||||
(1+ depth)))))
|
||||
(save-excursion
|
||||
(cl-loop for pos in (reverse (elt state 9))
|
||||
unless (memq (char-after (1+ pos)) '(?: ?\())
|
||||
do (goto-char (1+ pos))
|
||||
for fn = (read (current-buffer))
|
||||
if (symbolp fn)
|
||||
return (function-get fn 'indent-plists-as-data)))))))
|
||||
|
||||
;; Check for quotes or backquotes around.
|
||||
(let ((positions (elt state 9))
|
||||
(quotep 0))
|
||||
(while positions
|
||||
(let ((point (pop positions)))
|
||||
(or (when-let (char (char-before point))
|
||||
(cond
|
||||
((eq char ?\())
|
||||
((memq char '(?\' ?\`))
|
||||
(or (save-excursion
|
||||
(goto-char (1+ point))
|
||||
(skip-chars-forward "( ")
|
||||
(when-let (fn (ignore-errors (read (current-buffer))))
|
||||
(if (and (symbolp fn)
|
||||
(fboundp fn)
|
||||
;; Only special forms and
|
||||
;; macros have special
|
||||
;; indent needs.
|
||||
(not (functionp fn)))
|
||||
(setq quotep 0))))
|
||||
(cl-incf quotep)))
|
||||
((memq char '(?, ?@))
|
||||
(setq quotep 0))))
|
||||
;; If the spelled out `quote' or `backquote'
|
||||
;; are used, let's assume
|
||||
(save-excursion
|
||||
(goto-char (1+ point))
|
||||
(and (looking-at-p "\\(\\(?:back\\)?quote\\)[\t\n\f\s]+(")
|
||||
(cl-incf quotep 2)))
|
||||
(setq quotep (max 0 (1- quotep))))))
|
||||
(> quotep 0))))
|
||||
;; Containing sexp has nothing before this line except the
|
||||
;; first element. Indent under that element.
|
||||
nil
|
||||
;; Skip the first element, find start of second (the first
|
||||
;; argument of the function call) and indent under.
|
||||
(progn (forward-sexp 1)
|
||||
(parse-partial-sexp (point)
|
||||
calculate-lisp-indent-last-sexp
|
||||
0 t)))
|
||||
(backward-prefix-chars))
|
||||
(t
|
||||
;; Indent beneath first sexp on same line as
|
||||
;; `calculate-lisp-indent-last-sexp'. Again, it's almost
|
||||
;; certainly a function call.
|
||||
(goto-char calculate-lisp-indent-last-sexp)
|
||||
(beginning-of-line)
|
||||
(parse-partial-sexp (point) calculate-lisp-indent-last-sexp
|
||||
0 t)
|
||||
(backward-prefix-chars)))))
|
||||
;; Point is at the point to indent under unless we are inside a string.
|
||||
;; Call indentation hook except when overridden by lisp-indent-offset or
|
||||
;; if the desired indentation has already been computed.
|
||||
(let ((normal-indent (current-column)))
|
||||
(cond ((elt state 3)
|
||||
;; Inside a string, don't change indentation.
|
||||
nil)
|
||||
((and (integerp lisp-indent-offset) containing-sexp)
|
||||
;; Indent by constant offset
|
||||
(goto-char containing-sexp)
|
||||
(+ (current-column) lisp-indent-offset))
|
||||
;; in this case calculate-lisp-indent-last-sexp is not nil
|
||||
(calculate-lisp-indent-last-sexp
|
||||
(or
|
||||
;; try to align the parameters of a known function
|
||||
(and lisp-indent-function
|
||||
(not retry)
|
||||
(funcall lisp-indent-function indent-point state))
|
||||
;; If the function has no special alignment or it does not apply
|
||||
;; to this argument, try to align a constant-symbol under the
|
||||
;; last preceding constant symbol, if there is such one of the
|
||||
;; last 2 preceding symbols, in the previous uncommented line.
|
||||
(and (save-excursion
|
||||
(goto-char indent-point)
|
||||
(skip-chars-forward " \t")
|
||||
(looking-at ":"))
|
||||
;; The last sexp may not be at the indentation where it
|
||||
;; begins, so find that one, instead.
|
||||
(save-excursion
|
||||
(goto-char calculate-lisp-indent-last-sexp)
|
||||
;; Handle prefix characters and whitespace following an
|
||||
;; open paren. (Bug#1012)
|
||||
(backward-prefix-chars)
|
||||
(while (not (or (looking-back "^[ \t]*\\|([ \t]+"
|
||||
(line-beginning-position))
|
||||
(and containing-sexp
|
||||
(>= (1+ containing-sexp) (point)))))
|
||||
(forward-sexp -1)
|
||||
(backward-prefix-chars))
|
||||
(setq calculate-lisp-indent-last-sexp (point)))
|
||||
(> calculate-lisp-indent-last-sexp
|
||||
(save-excursion
|
||||
(goto-char (1+ containing-sexp))
|
||||
(parse-partial-sexp (point) calculate-lisp-indent-last-sexp 0 t)
|
||||
(point)))
|
||||
(let ((parse-sexp-ignore-comments t)
|
||||
indent)
|
||||
(goto-char calculate-lisp-indent-last-sexp)
|
||||
(or (and (looking-at ":")
|
||||
(setq indent (current-column)))
|
||||
(and (< (line-beginning-position)
|
||||
(prog2 (backward-sexp) (point)))
|
||||
(looking-at ":")
|
||||
(setq indent (current-column))))
|
||||
indent))
|
||||
;; another symbols or constants not preceded by a constant as
|
||||
;; defined above.
|
||||
normal-indent))
|
||||
;; in this case calculate-lisp-indent-last-sexp is nil
|
||||
(desired-indent)
|
||||
(normal-indent))))))
|
||||
|
||||
;; HACK: These functions are called often and as part of performance-sensitive
|
||||
;; processes, so we compile them if the file isn't already compiled.
|
||||
(mapc #'doom-compile-function '(+emacs-lisp-highlight-vars-and-faces
|
||||
+emacs-lisp-truncate-pin
|
||||
+emacs-lisp--calculate-lisp-indent-a))
|
||||
|
||||
;;; autoload.el ends here
|
||||
|
|
|
@ -60,9 +60,10 @@ See `+emacs-lisp-non-package-mode' for details.")
|
|||
mode-name "Elisp"
|
||||
;; Don't treat autoloads or sexp openers as outline headers, we have
|
||||
;; hideshow for that.
|
||||
outline-regexp +emacs-lisp-outline-regexp
|
||||
;; Fixed indenter that intends plists sensibly.
|
||||
lisp-indent-function #'+emacs-lisp-indent-function)
|
||||
outline-regexp +emacs-lisp-outline-regexp)
|
||||
|
||||
;; Fixed indenter that intends plists sensibly.
|
||||
(advice-add #'calculate-lisp-indent :override #'+emacs-lisp--calculate-lisp-indent-a)
|
||||
|
||||
;; variable-width indentation is superior in elisp. Otherwise, `dtrt-indent'
|
||||
;; and `editorconfig' would force fixed indentation on elisp.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue