11

I hate the way that elisp (not sure if LISP in general) handles multiline docstrings.

(defun foo ()
  "This is
a multi
liner
docstring"
  (do-stuff))

I sure do wish that I could do something like

(defun foo ()
  (eval-when-compile 
    (concat
      "This is\n"
       "a multi\n"
       "line\n"
       "docstring"))
  (do-stuff))

so that the indentation was consistent.

Unfortunately, eval-when-compile does not do the job.

Does anyone have any ideas?

2
  • It should be fairly easy to create a macro that will expand into a defun. The drawback to that approach – and it is a big one – is that will confuse any software (other than the elisp compiler/interpreter) that is parsing your code looking for defuns. Oct 31, 2014 at 8:49
  • 3
    Funnily enough, the reason why your trick doesn't work is that eval-when-compile quotes its result (to turn it from a value to an expression). If it were a bit more clever and only quoted its result when it's not self-quoting, it would work.
    – Stefan
    Oct 31, 2014 at 13:37

4 Answers 4

8

Of course a my-defun macro is the easy way out. But a simpler solution would be

(advice-add 'eval-when-compile :filter-return
            (lambda (exp)
              (if (and (eq 'quote (car-safe exp))
                       (stringp (cadr exp)))
                  (cadr exp)
                exp)))

Which should make your trick work, at least in all the cases where the function is macroexpanded before it's actually defined, which should include the main use cases (e.g. if it'sloaded from a file, if it's byte-compiled, or if it's defined via M-C-x).

Still, this won't fix all the existing code, so maybe a better answer is something like:

;; -*- lexical-binding:t -*-

(defun my-shift-docstrings (orig ppss)
  (let ((face (funcall orig ppss)))
    (when (eq face 'font-lock-doc-face)
      (save-excursion
        (let ((start (point)))
          (parse-partial-sexp (point) (point-max) nil nil ppss 'syntax-table)
          (while (search-backward "\n" start t)
            (put-text-property (point) (1+ (point)) 'display
                               (propertize "\n  " 'cursor 0))))))
    face))

(add-hook 'emacs-lisp-mode-hook
          (lambda ()
            (font-lock-mode 1)
            (push 'display font-lock-extra-managed-props)
            (add-function :around (local 'font-lock-syntactic-face-function)
                          #'my-shift-docstrings)))

which should just shift the docstrings by 2 spaces, but only on the display side, without affecting the buffer's actual content.

3
  • 1
    I really like your second solution. But my irrational fear of advices makes me hinge at the first. :-)
    – Malabarba
    Oct 31, 2014 at 14:32
  • Was reminded of this, and looking at my-shift-docstrings I see that it shifts by two, which is appropriate when the defund is at the top level, not nested. But I admit that I occasionally put the funds within if or cond j statements if only as a way of disabling large blocks of code quickly. Q: is there a way of detecting the lexical nesting depth so that the correct indentation could be applied?
    – Krazy Glew
    Feb 26, 2021 at 1:30
  • @KrazyGlew: [ Hi Andy, long time no see! ] Of course you can replace the "\n " with something like (concat "\n" (make-string N ?\s)) and compute the N e.g. as (* 2 (car ppss)) (since (car ppss) should return the paren-nesting). Another option is something like (save-excursion (goto-char (nth 8 ppss)) (current-column)) which should give you the indentation of the opening double quote.
    – Stefan
    Feb 27, 2021 at 13:55
5

You could use a macro like this:

(defmacro my-defun (name arglist &rest forms)
  "Like `defun', but concatenates strings."
  (declare (indent defun))
  (let (doc-lines)
    (while (and (stringp (car-safe forms))
                (> (length forms) 1))
      (setq doc-lines
            (append doc-lines (list (pop forms)))))
    `(defun ,name ,arglist
       ,(mapconcat #'identity doc-lines "\n")
       ,@forms)))

Then you can define your functions like this:

(my-defun test (a)
  "Description"
  "asodksad"
  "ok"
  (interactive)
  (+ 1 a))

Still, I'd strongly recommend not going against the standards for such a marginal benefit. The “irregular indentation” that bothers you is just by 2 columns, not to mention it helps highlight the first line of documentation which is more important.

2
  • Actually, the body of a defun is evaluated (when the function is called) and it is macro-expanded when the function is defined. So his trick should/could work.
    – Stefan
    Oct 31, 2014 at 13:39
  • @Stefan That's true. Forgot eval-when-compile was a macro.
    – Malabarba
    Oct 31, 2014 at 14:04
0

Revisiting this, going some of the things that I have done with @Stefan's accepted answer - which addresses the example of indentation in my original question, and possibly more:

(Exaggerated code formatting, indentation, in an attempt to make more readable)

(defun ump-keys-disable ()
(eval-when-compile (concat (sanitize-docstring--Ag " |suppressing or disabling ump keys |=> do not pass through to other keymaps |i.e. (set-keymap-parent ump-keys-minor-mode-map ump-keys-disabled-map)") ump-keys-docstring) ) (interactive) (set-keymap-parent ump-keys-minor-mode-map ump-keys-disabled-map) )

as you can see I'm using @Stefan's suggestion of eval-when-compile with the defadvice.

with my function sanitize-docstring, that deletes whitespace from beginning of line to the | character

and in addition concatenating with a bit of boilerplate docstring common to more than one function.


Lest somebody say that this answer goes beyond the original scope of question, please let me point out that the question title was "Is there a better way to handle multiline docstrings in elisp?". Not specifically docstring indentation for defun, although that was the original motivating example.


Inspired to post this because I am thinking of further enhancements: e.g. to have a big meta-docstring for a package, out of which different pieces can be selected.


Also because, while I've known for years that eval-when-compile is not necessary for defvar and defconst. I just learned that this approach does not work with define-minor-mode.

I.e. these things - defun, defvar, define-minor-mode of their docstrings evaluated at different times in different ways.

Addressing that inconsistency may be the subject of a separate question.

-1

I've seen packages that define docstrings like this:

(defun my-function (x y) "
this is my docstring
that lines always lines up
across multiple lines."
  (+ x y))

Placing the first quote on the first line then starting the text on the next so they all line up. It's definitely not the standard but you wouldn't be the only one doing it.

2
  • 3
    That's a bad idea. In contexts such as Apropos, only the first line of the docstring is shown, so that first line should provide information (and stand on its own). This way you get an empty description. Nov 10, 2014 at 0:56
  • @Gilles 'SO- stop being evil' - IMHO that's a bug in context such as Apropos. it's not hard to show only the 1st non-blank line of the docstring. But of course, bugs that have lasted long enough become missed features, and then eventually become "that's the way it's always been"
    – Krazy Glew
    Apr 1 at 2:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.