So Many Ways to Work with Comments
I’ve been using Emacs for over 20 years and I still keep discovering (and rediscovering) comment-related commands and variables. You’d think that after two decades I’d have comments figured out, but it turns out there’s a surprising amount of depth hiding behind a few keybindings.
What prompted this article was my recent work on
neocaml, a tree-sitter based major mode
for OCaml. OCaml uses (* ... *) block comments – no line comments at all –
and that unusual syntax forced me to dig deeper into how Emacs handles comments
internally. I learned more about comment variables in the past few months than in
the previous 20 years combined.
The Swiss Army Knife: M-;
I wrote about comment-dwim back in my Comment Commands
Redux article, but I don’t
think I did it justice. M-; is genuinely one of the most context-sensitive
commands in Emacs. Here’s a breakdown of what it does depending on where you
invoke it:
With an active region: It calls comment-region to comment out the selected
code. But if the region already consists entirely of comments, it calls
uncomment-region instead. So it’s effectively a toggle.1
On an empty line: It inserts a comment (using comment-start and
comment-end) and places point between the delimiters, properly indented.
On a line with code but no comment: It adds an end-of-line comment, indented
to comment-column (default 32). This is the classic “inline comment”
workflow – write your code, hit M-;, type your annotation.
On a line that already has an end-of-line comment: It jumps to that comment
and reindents it. Pressing M-; again just keeps you there.
With a prefix argument (C-u M-;): It kills the first comment on the
current line.
That’s five distinct behaviors from a single keybinding. No wonder people
find it confusing at first. If you want something simpler, comment-line (C-x
C-;, added in Emacs 25.1) just toggles comments on the current line or region
– nothing more, nothing less.
Continuing Comments: M-j
I also wrote about M-j years ago in Continue a Comment on the Next
Line. The
command (comment-indent-new-line2) breaks the current line and
continues the comment on the next line with proper indentation.
For languages with line comments (//, #, ;;), this works great out of the
box – it just inserts the comment delimiter on the new line. But for languages
with block comments like OCaml’s (* ... *), the default behavior is less
helpful: it closes the current comment and opens a new one:
(* some text about something. *)
(* |
What you actually want is to continue the same comment:
(* some text about something.
|
This is controlled by two variables that I suspect most people have never heard of:
-
comment-multi-line– when non-nil, tells commands likeM-jto continue the current comment rather than closing and reopening it. -
comment-line-break-function– the function thatM-jactually calls to do its work. Major modes can set this to customize the line-breaking behavior inside comments.
In neocaml, I set comment-multi-line to t and provide a custom
comment-line-break-function that uses tree-sitter to find the column where the
comment body text starts, then indents the new line to match:
(setq-local comment-multi-line t)
(setq-local comment-line-break-function #'neocaml--comment-indent-new-line)
The implementation is straightforward – walk up the tree-sitter AST to find the
enclosing comment node, compute the body column from the opening delimiter, and
indent accordingly. Now M-j inside (** documentation *) produces a new line
indented to align with the text after (**.
Filling Comments: M-q
While I was at it I also had to teach M-q (fill-paragraph) about OCaml
comments. By default, fill-paragraph doesn’t know where a (* ... *) comment
starts and ends, so it either does nothing useful or mangles things.
The fix was setting fill-paragraph-function to a custom function that uses
tree-sitter to find the comment boundaries, narrows to the body text (excluding
the (* and *) delimiters), and fills within that region. The fill prefix is
computed from the body start column so continuation lines align properly:
(* This is a long comment that gets
wrapped at the fill column, with
continuation lines properly
indented *)
The Comment Variable Zoo
Working on all of this made me realize just how many comment-related variables Emacs exposes for major modes to configure. Here are the ones I ended up caring about:
| Variable | Purpose |
|---|---|
comment-start |
Opening delimiter ("(* " for OCaml) |
comment-end |
Closing delimiter (" *)" for OCaml) |
comment-start-skip |
Regexp to skip the opening delimiter |
comment-multi-line |
Continue comments vs. close-and-reopen |
comment-line-break-function |
What M-j calls |
comment-column |
Column for end-of-line comments |
comment-style |
How comment-region formats comments |
fill-paragraph-function |
Custom fill behavior |
Most of these have sensible defaults for line-comment languages, which is why you
can go 20 years without thinking about them. But the moment you deal with block
comments, especially unusual ones like OCaml’s nested (* ... *), you
discover the full machinery.
Closing Thoughts
I find it remarkable that after all these years Emacs can still surprise me. A
handful of comment commands – M-;, M-j, M-q – and a set of buffer-local
variables give you a comment editing experience that’s both powerful and
deeply customizable. Most users never need to think about the underlying
variables, but they’re there when you need them, and they compose nicely.
If you’re a major mode author, pay attention to these variables. Getting comments right is one of those things that users notice immediately when it breaks and never think about when it works.
That’s all I have for you today. Keep hacking!
-
This is the one behavior of
comment-dwimthat’s somewhat inconvenient – to uncomment the current line you have to select it first.comment-line(C-x C-;) handles this more gracefully. ↩ -
In older Emacs versions this was called
indent-new-comment-line. The modern name iscomment-indent-new-line, but both work. ↩