From f45e34f27c5838f75bdac5249aa18d802723ad7a Mon Sep 17 00:00:00 2001 From: Jason Jackson <550839+jasonjckn@users.noreply.github.com> Date: Sat, 31 Oct 2026 02:39:43 -0700 Subject: [PATCH] Indent docstring lines like clojure-mode, with or without Markdown 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. --- CHANGELOG.md | 1 + clojure-ts-mode.el | 27 ++++-- test/clojure-ts-mode-indentation-test.el | 115 +++++++++++++++++++++++ 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a458474..8a9b908 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - [#172](https://github.com/clojure-emacs/clojure-ts-mode/pull/172): Stop marking `clojure-ts-ensure-grammars`, `clojure-ts-auto-remap` and the options that turn the embedded parsers on and off as safe in `.dir-locals.el`, where they have no effect. - [#169](https://github.com/clojure-emacs/clojure-ts-mode/pull/169): Add `clojure-ts-remove-unused-keys` (`C-c C-r k`), which removes the names the top-level form at point destructures from maps but never uses, along with their `:or` defaults. - [#164](https://github.com/clojure-emacs/clojure-ts-mode/pull/164): Support `setopt-local` for `clojure-ts-indent-style`, `clojure-ts-extra-def-forms` and `clojure-ts-comment-macro-font-lock-body`, which Emacs 31 refused to set locally and Emacs 32 warned about in `.dir-locals.el`. +- [#168](https://github.com/clojure-emacs/clojure-ts-mode/pull/168): Indent docstrings like `clojure-mode`, moving the lines indented less than `clojure-ts-docstring-fill-prefix-width` to that column and keeping deeper ones, instead of leaving them alone with `clojure-ts-use-markdown-inline` and flattening them without it. ## 0.6.0 (2025-12-02) diff --git a/clojure-ts-mode.el b/clojure-ts-mode.el index 91f4cc8..e7288de 100644 --- a/clojure-ts-mode.el +++ b/clojure-ts-mode.el @@ -118,7 +118,7 @@ itself." :package-version '(clojure-ts-mode . "0.2.3")) (defcustom clojure-ts-docstring-fill-prefix-width 2 - "Width of `fill-prefix' when filling a docstring. + "Width of `fill-prefix' when filling or indenting a docstring. The default value conforms with the de facto convention for Clojure docstrings, aligning the second line with the opening double quotes on the third column." @@ -1667,6 +1667,20 @@ too. If NODE is nil, use BOL instead." (when-let* ((prev-sibling (treesit-node-prev-sibling node))) (clojure-ts--metadata-node-p prev-sibling))) +(defun clojure-ts--match-docstring-line (_node _parent bol) + "Match a continuation line of a docstring at BOL, using the Clojure parser." + ;; Markdown nodes cannot identify the enclosing Clojure docstring. + (when-let* ((string (treesit-parent-until (treesit-node-at bol 'clojure) + #'clojure-ts--string-node-p t))) + (and (> bol (treesit-node-start string)) + (clojure-ts--match-docstring nil string bol)))) + +(defun clojure-ts--docstring-indent-offset (_node _parent bol) + "Return the docstring indentation at BOL, preserving deeper indentation." + (save-excursion + (goto-char bol) + (max (current-indentation) clojure-ts-docstring-fill-prefix-width))) + (defun clojure-ts--anchor-parent-opening-paren (_node parent _bol) "Return position of PARENT start for NODE. @@ -1717,11 +1731,12 @@ used." ((parent-is "^list_lit$") parent 1) ((parent-is "^anon_fn_lit$") parent 2) (clojure-ts--match-with-metadata parent 0) - ;; This is slow and only matches when point is inside of a docstring and - ;; only when Markdown grammar is disabled. `indent-region' tries to match - ;; all the rules from top to bottom, so order matters here (the slowest - ;; rules should be at the bottom). - (clojure-ts--match-docstring parent 0)))) + ;; This is slow, and `indent-region' tries the rules from top to bottom, + ;; so it goes last. + (clojure-ts--match-docstring-line column-0 clojure-ts--docstring-indent-offset)) + ;; The lines of a docstring belong to this parser when Markdown is used. + (markdown-inline + (clojure-ts--match-docstring-line column-0 clojure-ts--docstring-indent-offset)))) (defun clojure-ts--configured-indent-rules () "Return the configured indent rules." diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index 93486ea..0983064 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -155,6 +155,121 @@ DESCRIPTION is a string with the description of the spec." ((string-equal symbol-name "my-when") '((:block 1)))))) +(describe "Docstring indentation" + (dolist (markdown-inline '(nil t)) + (describe (format "with markdown-inline %s" markdown-inline) + (it "should indent top-level docstrings like fontified clojure-mode" + (when markdown-inline + (assume (treesit-ready-p 'markdown-inline t))) + (let ((clojure-ts-use-markdown-inline markdown-inline)) + (with-clojure-ts-buffer "(defn example + \"Summary. +content + (code example) + +\" + [] + :ok) +" + (font-lock-ensure) + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "(defn example + \"Summary. + content + (code example) + + \" + [] + :ok) +")))) + (it "should use the absolute fill prefix in protocol-method docstrings" + (when markdown-inline + (assume (treesit-ready-p 'markdown-inline t))) + (let ((clojure-ts-use-markdown-inline markdown-inline)) + (with-clojure-ts-buffer "(defprotocol Example + (method [this] + \"Summary. +content + (code example) + +\")) +" + (font-lock-ensure) + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "(defprotocol Example + (method [this] + \"Summary. + content + (code example) + + \")) +")))) + (it "should indent a new docstring line to the fill prefix" + (when markdown-inline + (assume (treesit-ready-p 'markdown-inline t))) + (let ((clojure-ts-use-markdown-inline markdown-inline)) + (with-clojure-ts-buffer "(defn example + \"Summary. + \")" + (font-lock-ensure) + (goto-char (point-min)) + (search-forward "Summary.") + (newline-and-indent) + (expect (current-column) :to-equal 2) + ;; A deeper line keeps its indentation, even a blank one. + (forward-line 1) + (indent-for-tab-command) + (expect (current-indentation) :to-equal 6)))) + (it "should keep indenting docstrings after the indentation style is set" + (when markdown-inline + (assume (treesit-ready-p 'markdown-inline t))) + (let ((clojure-ts-use-markdown-inline markdown-inline)) + (with-clojure-ts-buffer "(defn example + \"Summary. +content\")" + (setopt clojure-ts-indent-style 'semantic) + (font-lock-ensure) + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "(defn example + \"Summary. + content\")")))) + (it "should preserve ordinary strings" + (when markdown-inline + (assume (treesit-ready-p 'markdown-inline t))) + (let ((clojure-ts-use-markdown-inline markdown-inline)) + (with-clojure-ts-buffer "(def value + \"Ordinary string. +content + (code example) + +\") +" + (font-lock-ensure) + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal "(def value + \"Ordinary string. +content + (code example) + +\") +")))) + (it "should preserve docstrings after fill-paragraph" + (when markdown-inline + (assume (treesit-ready-p 'markdown-inline t))) + (dolist (clojure-ts-docstring-fill-prefix-width '(2 4)) + (let ((clojure-ts-use-markdown-inline markdown-inline) + (clojure-ts-docstring-fill-column 35)) + (with-clojure-ts-buffer "(defn example + \"Summary. This paragraph is long enough to wrap across several lines when filled by M-q.\" + [] + :ok)" + (goto-char (point-min)) + (search-forward "Summary") + (fill-paragraph) + (let ((filled (buffer-string))) + (indent-region (point-min) (point-max)) + (expect (buffer-string) :to-equal filled))))))))) + (describe "indentation" (it "should not hang on end of buffer" (with-clojure-ts-buffer "(let [a b]"