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]"