diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53af842..06b4b44 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,9 @@ jobs: - name: Lint the project run: make lint + - name: Check the indentation + run: make indent + test: runs-on: ubuntu-latest # continue-on-error: ${{matrix.emacs_version == 'snapshot'}} diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml new file mode 100644 index 0000000..37a97df --- /dev/null +++ b/.markdownlint-cli2.yaml @@ -0,0 +1,14 @@ +config: + default: true + + line-length: + line_length: 100 + code_blocks: false + tables: false + + no-duplicate-heading: + allow_different_nesting: true + + no-inline-html: false + + first-line-heading: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b24bb6..f0b9fed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog + + + ## main (unreleased) - Fix indentation of namespaced maps in `fixed` indentation style. diff --git a/Eldev b/Eldev index b704571..e558029 100644 --- a/Eldev +++ b/Eldev @@ -34,3 +34,30 @@ `(:or ,eldev-standard-excludes "./clojure-mode-tests/*")) (setf eldev-lint-ignored-fileset `(:or ,eldev-lint-ignored-fileset "./clojure-mode-tests/*")) + +(defvar clojure-ts-mode-indent-check-fix nil + "Whether `eldev indent' should rewrite the misindented files.") + +(eldev-defcommand clojure-ts-mode-indent-check-command (&rest parameters) + "Check that every Emacs Lisp source file of the project is indented the way +Emacs would indent it. Pass `--fix' to reindent the offenders in place +instead of only reporting them." + :command indent + :category testing + (when parameters + (signal 'eldev-wrong-command-usage `(t "Unexpected command parameters"))) + (eldev-load-project-dependencies 'test) + (load (expand-file-name "test/indent-check.el" eldev-project-dir) nil t) + (unless (zerop (clojure-ts-mode-indent-check clojure-ts-mode-indent-check-fix)) + (unless clojure-ts-mode-indent-check-fix + (signal 'eldev-error `("Some files are misindented; run `eldev indent --fix'"))))) + +(eldev-defbooloptions clojure-ts-mode-indent-check-do-fix + clojure-ts-mode-indent-check-just-check + clojure-ts-mode-indent-check-fix + ("Reindent the misindented files in place" + :options (--fix)) + ("Only report the misindented lines" + :options (--check) + :hidden-if :default) + :for-command indent) diff --git a/Makefile b/Makefile index 3e61353..aee750d 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: clean compile lint test all +.PHONY: clean compile lint indent test all .DEFAULT_GOAL := all clean: @@ -7,6 +7,10 @@ clean: lint: clean eldev lint -c +# Checks that the sources are indented the way Emacs would indent them. +indent: + eldev indent + # Checks for byte-compilation warnings. compile: clean eldev -dtT compile --warnings-as-errors @@ -14,4 +18,4 @@ compile: clean test: clean eldev -dtT -p test -all: clean compile lint test +all: clean compile lint indent test diff --git a/test/clojure-ts-mode-cycling-test.el b/test/clojure-ts-mode-cycling-test.el index 81eef67..cc7f754 100644 --- a/test/clojure-ts-mode-cycling-test.el +++ b/test/clojure-ts-mode-cycling-test.el @@ -70,14 +70,14 @@ (clojure-ts-cycle-privacy)) (when-refactoring-it "should also work from the beginning of a sexp" - "(defn- add [a b] + "(defn- add [a b] (+ a b))" - "(defn add [a b] + "(defn add [a b] (+ a b))" - (backward-sexp) - (clojure-ts-cycle-privacy)) + (backward-sexp) + (clojure-ts-cycle-privacy)) (when-refactoring-it "should use metadata when clojure-use-metadata-for-privacy is set to true" "(defn add [a b] diff --git a/test/clojure-ts-mode-fill-paragraph-test.el b/test/clojure-ts-mode-fill-paragraph-test.el index a7e2dde..dace98e 100644 --- a/test/clojure-ts-mode-fill-paragraph-test.el +++ b/test/clojure-ts-mode-fill-paragraph-test.el @@ -34,11 +34,11 @@ \"This is a very long docstring that should be reformatted using fill-paragraph function.\" [] (pringln \"Hello world\"))" - (goto-char 40) - (prog-fill-reindent-defun) - (expect (buffer-substring-no-properties (point-min) (point-max)) - :to-equal - "(ns foo) + (goto-char 40) + (prog-fill-reindent-defun) + (expect (buffer-substring-no-properties (point-min) (point-max)) + :to-equal + "(ns foo) (defn hello-world \"This is a very long docstring that should be reformatted using @@ -50,11 +50,11 @@ (with-clojure-ts-buffer "(ns foo) ;; This is a very long comment that should be reformatted using fill-paragraph function." - (goto-char 20) - (prog-fill-reindent-defun) - (expect (buffer-substring-no-properties (point-min) (point-max)) - :to-equal - "(ns foo) + (goto-char 20) + (prog-fill-reindent-defun) + (expect (buffer-substring-no-properties (point-min) (point-max)) + :to-equal + "(ns foo) ;; This is a very long comment that should be reformatted using ;; fill-paragraph function.")))) diff --git a/test/clojure-ts-mode-font-lock-test.el b/test/clojure-ts-mode-font-lock-test.el index be008f8..908d6bf 100644 --- a/test/clojure-ts-mode-font-lock-test.el +++ b/test/clojure-ts-mode-font-lock-test.el @@ -43,7 +43,7 @@ Assumes the current buffer is already fontified." (let ((start-face (get-text-property start 'face)) (all-faces (cl-loop for i from start to end collect (get-text-property - i 'face)))) + i 'face)))) (if (cl-every (lambda (face) (equal face start-face)) all-faces) start-face 'various-faces))) @@ -152,14 +152,14 @@ DESCRIPTION is the description of the spec." ("(deftest a (is (= 1 1)))" ("deftest" font-lock-keyword-face)) - ;; TODO: copied from clojure-mode, but failing - ;; ("(defne [x y])" (2 6 font-lock-keyword-face)) - ;; ("(defnm a b)" (2 6 font-lock-keyword-face)) - ;; ("(defnu)" (2 6 font-lock-keyword-face)) - ;; ("(defnc [a])" (2 6 font-lock-keyword-face)) - ;; ("(defna)" (2 6 font-lock-keyword-face)) - ;; ("(deftask a)" (2 8 font-lock-keyword-face)) - ;; ("(defstate a :start \"b\" :stop \"c\")" (2 9 font-lock-keyword-face)) + ;; TODO: copied from clojure-mode, but failing + ;; ("(defne [x y])" (2 6 font-lock-keyword-face)) + ;; ("(defnm a b)" (2 6 font-lock-keyword-face)) + ;; ("(defnu)" (2 6 font-lock-keyword-face)) + ;; ("(defnc [a])" (2 6 font-lock-keyword-face)) + ;; ("(defna)" (2 6 font-lock-keyword-face)) + ;; ("(deftask a)" (2 8 font-lock-keyword-face)) + ;; ("(defstate a :start \"b\" :stop \"c\")" (2 9 font-lock-keyword-face)) ) @@ -351,8 +351,9 @@ DESCRIPTION is the description of the spec." (when-fontifying-it "should highlight keywords" (":foo" ("foo" clojure-ts-keyword-face)) ("::foo" ("foo" clojure-ts-keyword-face)) - (":my.ns/bar" ("my.ns" font-lock-type-face) - ("bar" clojure-ts-keyword-face)))) + (":my.ns/bar" + ("my.ns" font-lock-type-face) + ("bar" clojure-ts-keyword-face)))) ;;;; Strings @@ -370,10 +371,12 @@ DESCRIPTION is the description of the spec." (";;; heading" (";;; heading" font-lock-comment-face))) (when-fontifying-it "should highlight discard expressions" - ("#_foo" ("#_" font-lock-comment-delimiter-face) - ("foo" font-lock-comment-face)) - ("#_(+ 1 2)" ("#_" font-lock-comment-delimiter-face) - ("(+ 1 2)" font-lock-comment-face))) + ("#_foo" + ("#_" font-lock-comment-delimiter-face) + ("foo" font-lock-comment-face)) + ("#_(+ 1 2)" + ("#_" font-lock-comment-delimiter-face) + ("(+ 1 2)" font-lock-comment-face))) (it "should highlight comment macro name as delimiter" (with-fontified-clojure-ts-buffer "(comment (+ 1 2))" @@ -453,8 +456,9 @@ DESCRIPTION is the description of the spec." (describe "regex-highlighting" (when-fontifying-it "should highlight regex literals" - ("#\"pattern\"" ("#" font-lock-punctuation-face) - ("pattern" font-lock-regexp-face)))) + ("#\"pattern\"" + ("#" font-lock-punctuation-face) + ("pattern" font-lock-regexp-face)))) ;;;; Builtin macros diff --git a/test/clojure-ts-mode-indentation-test.el b/test/clojure-ts-mode-indentation-test.el index fe48df5..cd1e68f 100644 --- a/test/clojure-ts-mode-indentation-test.el +++ b/test/clojure-ts-mode-indentation-test.el @@ -43,17 +43,17 @@ represents the expected position of point. DESCRIPTION is a string with the description of the spec." (declare (indent 1)) `(it ,description - (let* ((after ,after) - (expected-cursor-pos (1+ (clojure-ts--s-index-of "|" after))) - (expected-state (delete ?| after))) - (with-clojure-ts-buffer ,before - (goto-char (point-min)) - (search-forward "|") - (delete-char -1) - (font-lock-ensure) - (indent-according-to-mode) - (expect (buffer-string) :to-equal expected-state) - (expect (point) :to-equal expected-cursor-pos))))) + (let* ((after ,after) + (expected-cursor-pos (1+ (clojure-ts--s-index-of "|" after))) + (expected-state (delete ?| after))) + (with-clojure-ts-buffer ,before + (goto-char (point-min)) + (search-forward "|") + (delete-char -1) + (font-lock-ensure) + (indent-according-to-mode) + (expect (buffer-string) :to-equal expected-state) + (expect (point) :to-equal expected-cursor-pos))))) @@ -698,7 +698,7 @@ DESCRIPTION is a string with the description of the spec." (with-clojure-ts-buffer-point " (let [a-long-name 10 b |20])" - (call-interactively #'clojure-ts-align) + (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " (let [a-long-name 10 b 20])")) @@ -713,7 +713,7 @@ b |20])" :a #long \"1234\" :b {:this \"is\" :nested \"map\"}}])" - (call-interactively #'clojure-ts-align) + (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " (let [^long my-map {:hello \"World\" ;Hello :foo @@ -730,7 +730,7 @@ b |20])" |123 \"Hello\" 99999 \"World\" 234 nil)" - (call-interactively #'clojure-ts-align) + (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " (condp = 2 123 \"Hello\" @@ -741,7 +741,7 @@ b |20])" (with-clojure-ts-buffer-point " #?(:clj 2 |:cljs 2)" - (call-interactively #'clojure-ts-align) + (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " #?(:clj 2 :cljs 2)"))) @@ -750,7 +750,7 @@ b |20])" (with-clojure-ts-buffer-point " #?(:clj 2 |:cljs 2)" - (setq-local clojure-ts-align-reader-conditionals t) + (setq-local clojure-ts-align-reader-conditionals t) (call-interactively #'clojure-ts-align) (expect (buffer-string) :to-equal " #?(:clj 2 @@ -758,8 +758,8 @@ b |20])" (it "should remove extra commas" (with-clojure-ts-buffer-point "{|:a 2, ,:c 4}" - (call-interactively #'clojure-ts-align) - (expect (buffer-string) :to-equal "{:a 2, :c 4}")))) + (call-interactively #'clojure-ts-align) + (expect (buffer-string) :to-equal "{:a 2, :c 4}")))) (describe "clojure-ts-align-forms-automatically" ;; Copied from `clojure-mode' @@ -826,29 +826,29 @@ b |20])" " {c d :this-is-a-form b}" - " + " {c d :this-is b}" - " + " {c d :this b}" - " + " {c d :a b}" - " + " (let [c d this-is-a-form b])" - " + " (let [c d this-is b])" - " + " (let [c d this b])" - " + " (let [c d a b])") diff --git a/test/clojure-ts-mode-util-test.el b/test/clojure-ts-mode-util-test.el index c2321f5..58b751f 100644 --- a/test/clojure-ts-mode-util-test.el +++ b/test/clojure-ts-mode-util-test.el @@ -143,7 +143,7 @@ (in-ns 'foo3) | (ns foo4)" - (expect (clojure-ts-find-ns) :to-equal "foo3")) + (expect (clojure-ts-find-ns) :to-equal "foo3")) (with-clojure-ts-buffer "(ns foo) (ns-unmap *ns* 'map) (ns.misleading 1 2 3)" diff --git a/test/indent-check.el b/test/indent-check.el new file mode 100644 index 0000000..0752c46 --- /dev/null +++ b/test/indent-check.el @@ -0,0 +1,155 @@ +;;; indent-check.el --- Check the indentation of the sources -*- lexical-binding: t; -*- + +;; Copyright © 2026 Bozhidar Batsov + +;; This file is not part of GNU Emacs. + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; Reindents every Emacs Lisp source file of the project and reports the lines +;; that differ from what is checked in. Run it with `eldev indent', or `eldev +;; indent --fix' to have the offenders rewritten in place. +;; +;; Buttercup and the test suite have to be loaded before anything is checked. +;; How a macro call gets indented comes from the `declare' form of the macro, +;; which only exists once that macro is defined, so without them `describe', +;; `it' and the helpers in test-helper.el all fall back to the default function +;; indentation and practically every spec looks misindented. +;; +;; Indentation depends on the Emacs version, so the verdict that counts is the +;; one from the Emacs the lint job runs. + +;;; Code: + +(require 'buttercup) +(require 'clojure-ts-mode) +(require 'subr-x) + +(defconst clojure-ts-mode-indent-check-file (or load-file-name buffer-file-name) + "The file this code lives in, so that it can skip loading itself.") + +(defconst clojure-ts-mode-indent-check-excluded-dirs + '(".git" ".eldev" "clojure-mode-tests") + "Directories that hold no Emacs Lisp of ours.") + +(defconst clojure-ts-mode-indent-check-lone-comment-regexp + (rx bos (* blank) ";" (or (not (any ";")) eos)) + "Matches a line holding a comment that opens with a single semicolon. +`indent-region' aligns those to `comment-column' instead of to the +surrounding code, which is not something worth policing.") + +(defun clojure-ts-mode-indent-check--test-dir () + "Return the directory holding the test suite." + (file-name-directory clojure-ts-mode-indent-check-file)) + +(defun clojure-ts-mode-indent-check--project-dir () + "Return the root directory of the project." + (file-name-directory (directory-file-name + (clojure-ts-mode-indent-check--test-dir)))) + +(defun clojure-ts-mode-indent-check--load-macros () + "Load the test suite, so that its macros carry indentation metadata." + (let ((test-dir (clojure-ts-mode-indent-check--test-dir))) + (add-to-list 'load-path test-dir) + (dolist (file (directory-files test-dir t "\\.el\\'")) + (unless (equal file clojure-ts-mode-indent-check-file) + (load file nil t))))) + +(defun clojure-ts-mode-indent-check--files () + "Return the Emacs Lisp source files of the project, in a stable order." + (let ((files (directory-files-recursively + (clojure-ts-mode-indent-check--project-dir) + "\\.el\\'" + nil + (lambda (dir) + (not (member (file-name-nondirectory (directory-file-name dir)) + clojure-ts-mode-indent-check-excluded-dirs)))))) + (sort (seq-remove (lambda (file) + (string-suffix-p "-autoloads.el" file)) + files) + #'string<))) + +(defun clojure-ts-mode-indent-check--exempt-p (line) + "Return non-nil if the indentation of LINE should be left alone. +Blank lines would only gain trailing whitespace, and a lone semicolon +comment gets aligned to `comment-column' rather than to the code." + (or (string-blank-p line) + (string-match-p clojure-ts-mode-indent-check-lone-comment-regexp line))) + +(defun clojure-ts-mode-indent-check--indent (lines file) + "Return LINES of FILE as Emacs would indent them. +FILE is only used to give the temporary buffer a name, so that +file-local variables and the like resolve the way they do on disk." + (let ((indented (with-temp-buffer + (setq buffer-file-name file) + (insert (string-join lines "\n")) + (emacs-lisp-mode) + (setq-local indent-tabs-mode nil) + ;; The progress reporter would drown out the report. + (let ((inhibit-message t)) + (indent-region (point-min) (point-max))) + (setq buffer-file-name nil) + (split-string (buffer-string) "\n")))) + ;; `indent-region' only ever rewrites leading whitespace, so the two lists + ;; line up. Bail out rather than guess if that ever stops holding. + (unless (= (length lines) (length indented)) + (error "Reindenting %s changed the number of lines" file)) + (seq-mapn (lambda (before after) + (if (clojure-ts-mode-indent-check--exempt-p before) before after)) + lines indented))) + +(defun clojure-ts-mode-indent-check--diverging-lines (before after) + "Return the numbers of the lines where the lists BEFORE and AFTER differ." + (let ((line 0) + (lines nil)) + (while (or before after) + (setq line (1+ line)) + (unless (equal (car before) (car after)) + (push line lines)) + (setq before (cdr before) + after (cdr after))) + (nreverse lines))) + +(defun clojure-ts-mode-indent-check (&optional fix) + "Check that the Emacs Lisp sources of the project are correctly indented. + +Report the misindented lines of every file, rewriting the file when FIX +is non-nil. Return the number of files that needed reindenting." + (clojure-ts-mode-indent-check--load-macros) + (let ((offenders 0)) + (dolist (file (clojure-ts-mode-indent-check--files)) + (let* ((lines (split-string (with-temp-buffer + (insert-file-contents file) + (buffer-string)) + "\n")) + (indented (clojure-ts-mode-indent-check--indent lines file)) + (diverging (clojure-ts-mode-indent-check--diverging-lines + lines indented))) + (when diverging + (setq offenders (1+ offenders)) + (message "%s: %s" + (file-relative-name + file (clojure-ts-mode-indent-check--project-dir)) + (mapconcat #'number-to-string diverging ", ")) + (when fix + (write-region (string-join indented "\n") nil file nil 'silent))))) + (cond + ((zerop offenders) (message "Indentation is fine.")) + (fix (message "Reindented %d file(s)." offenders))) + offenders)) + +(provide 'indent-check) +;;; indent-check.el ends here diff --git a/test/test-helper.el b/test/test-helper.el index 0c64888..8c6845a 100644 --- a/test/test-helper.el +++ b/test/test-helper.el @@ -113,10 +113,10 @@ DESCRIPTION is a string with the description of the spec." "Create a temporary directory and bind its to TEMP-DIR while evaluating BODY. Removes the temp directory at the end of evaluation." `(let ((,temp-dir (make-temp-file "" t))) - (unwind-protect - (progn - ,@body) - (delete-directory ,temp-dir t)))) + (unwind-protect + (progn + ,@body) + (delete-directory ,temp-dir t)))) (provide 'test-helper) ;;; test-helper.el ends here