Skip to content

Docstring indentation, option B (pick one of #167/#168): indent docstrings like clojure-mode - #168

Merged
bbatsov merged 1 commit into
clojure-emacs:mainfrom
jasonjckn:docstring-indent-like-clojure-mode
Nov 7, 2026
Merged

bbatsov merged 1 commit into
clojure-emacs:mainfrom
jasonjckn:docstring-indent-like-clojure-mode

Conversation

@jasonjckn

@jasonjckn jasonjckn commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

This is one of two alternative PRs for the same problem: #167 leaves docstring text alone (like cljfmt), #168 indents docstrings like clojure-mode. Please merge at most one; we'll close the other.

indent-region changes the text of docstrings, and how it changes them depends on clojure-ts-use-markdown-inline and on whether the markdown-inline grammar is installed. With Markdown on, only the closing " moves. With Markdown off, every line moves to the opening quote's column + 1, which flattens indented code examples and undoes what M-q just did.

This PR indents docstring lines the way clojure-mode does, in both settings: a line indented less than clojure-ts-docstring-fill-prefix-width moves to that column, a deeper line keeps its indentation, and blank lines stay blank. It uses the same prefix as M-q, so M-q followed by indent-region changes nothing.

Background

We found this by comparing clojure-mode's and clojure-ts-mode's indent-region over 73 real-world files (as-is, and with indentation stripped), using a formatter that runs indent-region in batch Emacs.

What each tool does to a line inside a top-level docstring:

Line cljfmt 0.16.6 clojure-mode 5.19, fontified buffer clojure-ts-mode main, markdown-inline on (default) clojure-ts-mode main, markdown-inline off
content at col 0 unchanged → col 2 unchanged → col 3
content at col 4 (code example) unchanged unchanged unchanged → col 3
closing " at col 0 unchanged → col 2 → col 2 (quote column) → col 2
line M-q just filled (col 2) unchanged unchanged unchanged → col 3
protocol-method docstring unchanged unchanged closing " → quote col content → quote col + 1, closing " → quote col
ordinary (non-doc) string unchanged unchanged unchanged unchanged
  • clojure-mode only indents docstring lines when clojure-in-docstring-p is true, and that checks for font-lock-doc-face. So in an unfontified (batch) buffer clojure-mode never touches docstrings and looks like cljfmt; in a normal fontified buffer it moves lines indented less than clojure-docstring-fill-prefix-width to that column and keeps deeper lines.
  • History: docstring indentation was added on purpose in 177ac05 ("Indent within docstrings - not working 100%"). The Markdown injection in Jank Support #23 (a16c6b4) moved docstring content into the markdown-inline parser, and the rule's comment now says it "only matches … when Markdown grammar is disabled".
  • Mechanism: with Markdown on, treesit-language-at a content line is markdown-inline, and treesit-simple-indent-rules has only a clojure entry, so those lines are left alone. The closing " belongs to the Clojure parser (parent str_lit), so clojure-ts--match-docstring fires and anchors it at the opening quote.
  • M-q: clojure-ts--fill-paragraph uses clojure-ts-docstring-fill-prefix-width (2) spaces from column 0, like clojure-mode. With Markdown off, indent-region then moves those lines to column 3.

This PR matches clojure-mode in normal fontified buffers. #167 matches cljfmt instead.

How it works

  • clojure-ts--match-docstring-line finds the Clojure str_lit at the line with (treesit-node-at bol 'clojure) and reuses clojure-ts--match-docstring, so it works whichever parser the line belongs to.
  • The rule anchors at column-0 with an offset function: the line's current indentation or (length (clojure-ts--docstring-fill-prefix)), whichever is larger (0 for a blank line).
  • The rule is registered for clojure, and also for markdown-inline when Markdown is in use (clojure-ts--configured-indent-rules now gets markdown-available).

Examples

Input:

(defn example
  "Summary.
content
    (code example)

"
  []
  :ok)

clojure-ts-mode main, markdown-inline on:

(defn example
  "Summary.
content
    (code example)

  "
  []
  :ok)

clojure-ts-mode main, markdown-inline off:

(defn example
  "Summary.
   content
   (code example)

  "
  []
  :ok)

With this PR (either setting), and clojure-mode 5.19 in a fontified buffer:

(defn example
  "Summary.
  content
    (code example)

  "
  []
  :ok)

cljfmt (same as the input):

(defn example
  "Summary.
content
    (code example)

"
  []
  :ok)

Tests

This PR adds a Docstring indentation group, run once with clojure-ts-use-markdown-inline nil and once with t (the t case is skipped with assume when the markdown-inline grammar isn't installed):

  • should indent top-level docstrings like fontified clojure-mode: expected output taken from clojure-mode 5.19 in a fontified buffer. Fails on main in both settings, passes with this PR.
  • should use the absolute fill prefix in protocol-method docstrings: fails on main in both settings, passes with this PR.
  • should preserve docstrings after fill-paragraph (M-q, then indent-region, with prefix widths 2 and 4): fails on main with Markdown off, passes with this PR.
  • should preserve ordinary strings: a control; passes on main and with this PR.

How it was checked locally (Eldev isn't installed here, so these are the same steps run directly):

  • The full buttercup suite passes (0 failed) on Emacs 32.0.50 and 31.1.50. No Emacs 30.x was available locally, so it hasn't been run on CI's 30.1.
  • Byte-compiling with byte-compile-error-on-warn gives no warnings; checkdoc is clean; the changed .el files are indented as emacs-lisp-mode indents them.

Notes

  • Like clojure-mode, this still changes a docstring's value when a line is indented less than the prefix.
  • The prefix is counted from column 0, as M-q does, not from the opening quote. So in a nested docstring, such as a protocol method's, continuation lines go to column 2 even though the quote is deeper:
    (defprotocol Example
      (method [this]
        "Summary.
      content
        (code example)
    
      "))
    clojure-mode 5.19 leaves protocol-method docstrings alone, because it doesn't fontify them as docstrings.
  • If another grammar is injected into docstrings later (e.g. regex), the rule needs an entry for that language too.
  • The fixed indentation style is unchanged; it never had a docstring rule.

About this PR

The code, tests and this description were generated by an AI (Claude, by Anthropic), at @jasonjckn's request. If AI-generated contributions are a problem for this project, please feel free to close this PR and #167 right away.


  • The commits are consistent with our contribution guidelines.
  • You've run M-x checkdoc and fixed any warnings in the code you've written.
  • You've updated the changelog (if adding/changing user-visible functionality).
  • You've updated the readme (if adding/changing user-visible functionality).

@jasonjckn jasonjckn changed the title Docstring indentation, option B (pick one of #167/#B): indent docstrings like clojure-mode Docstring indentation, option B (pick one of #167/#168): indent docstrings like clojure-mode Oct 31, 2026
clojure-mode moves a docstring line indented less than
clojure-docstring-fill-prefix-width to that column and keeps deeper
lines, so code examples keep their indentation and indent-region after
M-q changes nothing.  clojure-ts-mode left the lines alone when Markdown
was in use, as they belong to the markdown-inline parser, which had no
indentation rules, and moved every line to the column after the opening
quote when it wasn't.

The rule is registered for both parsers, and does nothing in a buffer
without a markdown-inline parser.
@bbatsov
bbatsov force-pushed the docstring-indent-like-clojure-mode branch from 3c750b3 to f45e34f Compare November 7, 2026 07:10
@bbatsov
bbatsov merged commit 30bccb0 into clojure-emacs:main Nov 7, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants