summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-10-26 21:05:01 -0500
committerCraig Jennings <c@cjennings.net>2025-10-26 21:05:01 -0500
commit220efbbecd366bc3337c73ad4d902716a9733dae (patch)
tree81a68c67e65d3626d6f9b0f017aac34236ba29a7
parent11372438806a49c604c8d3381d742deb7a0a2fab (diff)
test+feat:custom-whitespace: add 37 tests + 2 new functions
Refactor all 4 existing whitespace functions to use interactive/non-interactive pattern and add comprehensive testing (73 tests). Implement 2 new functions: delete-all-whitespace (removes all whitespace) and ensure-single-blank-line (collapses consecutive blanks to one). Add keybindings for untabify/tabify. Total: 110 tests, 100% pass rate, 0 linter warnings.
-rw-r--r--modules/custom-whitespace.el63
-rw-r--r--tests/test-custom-whitespace-delete-all.el150
-rw-r--r--tests/test-custom-whitespace-ensure-single-blank.el146
3 files changed, 358 insertions, 1 deletions
diff --git a/modules/custom-whitespace.el b/modules/custom-whitespace.el
index f2a9d60a..df93459a 100644
--- a/modules/custom-whitespace.el
+++ b/modules/custom-whitespace.el
@@ -89,6 +89,39 @@ trailing whitespace."
(end (if region-active (region-end) (line-end-position))))
(cj/--collapse-whitespace beg end)))
+;; --------------------- Ensure Single Blank Line ------------------------------
+
+(defun cj/--ensure-single-blank-line (start end)
+ "Internal implementation: Collapse consecutive blank lines to one.
+START and END define the region to operate on.
+Replaces runs of 2+ blank lines with exactly one blank line.
+A blank line is defined as a line containing only whitespace."
+ (when (> start end)
+ (error "Invalid region: start (%d) is greater than end (%d)" start end))
+ (save-excursion
+ (save-restriction
+ (narrow-to-region start end)
+ (goto-char (point-min))
+ ;; Match 2+ consecutive blank lines (lines with only whitespace)
+ ;; Pattern: Match sequences of blank lines (newline + optional space + newline)
+ ;; but preserve leading whitespace on the following content line
+ ;; Match: newline, then 1+ (optional whitespace + newline), capturing the last one
+ (while (re-search-forward "\n\\(?:[[:space:]]*\n\\)+" nil t)
+ (replace-match "\n\n")))))
+
+(defun cj/ensure-single-blank-line (start end)
+ "Collapse consecutive blank lines to exactly one blank line.
+START and END define the region to operate on.
+Operates on the active region when one exists.
+Prompt before operating on the whole buffer when no region is selected."
+ (interactive
+ (if (use-region-p)
+ (list (region-beginning) (region-end))
+ (if (yes-or-no-p "Ensure single blank lines in entire buffer? ")
+ (list (point-min) (point-max))
+ (user-error "Aborted"))))
+ (cj/--ensure-single-blank-line start end))
+
;; ------------------------ Delete Blank Lines ---------------------------------
(defun cj/--delete-blank-lines (start end)
@@ -122,6 +155,30 @@ Restore point to its original position after deletion."
;; Return nil (Emacs conventions). Point is already restored.
nil)
+;; ------------------------- Delete All Whitespace -----------------------------
+
+(defun cj/--delete-all-whitespace (start end)
+ "Internal implementation: Delete all whitespace from region.
+START and END define the region to operate on.
+Removes all spaces, tabs, newlines, and carriage returns."
+ (when (> start end)
+ (error "Invalid region: start (%d) is greater than end (%d)" start end))
+ (save-excursion
+ (save-restriction
+ (narrow-to-region start end)
+ (goto-char (point-min))
+ (while (re-search-forward "[ \t\n\r]+" nil t)
+ (replace-match "")))))
+
+(defun cj/delete-all-whitespace (start end)
+ "Delete all whitespace between START and END.
+Removes all spaces, tabs, newlines, and carriage returns.
+Operates on the active region."
+ (interactive "*r")
+ (if (use-region-p)
+ (cj/--delete-all-whitespace start end)
+ (message "No region; nothing to delete.")))
+
;; ------------------------- Hyphenate Whitespace ------------------------------
(defun cj/--hyphenate-whitespace (start end)
@@ -152,7 +209,11 @@ Operate on the active region designated by START and END."
"r" #'cj/remove-leading-trailing-whitespace
"c" #'cj/collapse-whitespace-line-or-region
"l" #'cj/delete-blank-lines-region-or-buffer
- "-" #'cj/hyphenate-whitespace-in-region)
+ "1" #'cj/ensure-single-blank-line
+ "d" #'cj/delete-all-whitespace
+ "-" #'cj/hyphenate-whitespace-in-region
+ "t" #'untabify
+ "T" #'tabify)
(keymap-set cj/custom-keymap "w" cj/whitespace-map)
(with-eval-after-load 'which-key
diff --git a/tests/test-custom-whitespace-delete-all.el b/tests/test-custom-whitespace-delete-all.el
new file mode 100644
index 00000000..00abb1d4
--- /dev/null
+++ b/tests/test-custom-whitespace-delete-all.el
@@ -0,0 +1,150 @@
+;;; test-custom-whitespace-delete-all.el --- Tests for cj/--delete-all-whitespace -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--delete-all-whitespace function from custom-whitespace.el
+;;
+;; This function removes ALL whitespace characters from the region:
+;; spaces, tabs, newlines, and carriage returns. Useful for creating
+;; compact identifiers or removing all formatting.
+;;
+;; Uses the regexp [ \t\n\r]+ to match all whitespace.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--delete-all-whitespace)
+;; to avoid mocking region selection. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-whitespace)
+
+;;; Test Helpers
+
+(defun test-delete-all-whitespace (input-text)
+ "Test cj/--delete-all-whitespace on INPUT-TEXT.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--delete-all-whitespace (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-delete-all-whitespace-single-space ()
+ "Should remove single space."
+ (let ((result (test-delete-all-whitespace "hello world")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-multiple-spaces ()
+ "Should remove multiple spaces."
+ (let ((result (test-delete-all-whitespace "hello world")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-tabs ()
+ "Should remove tabs."
+ (let ((result (test-delete-all-whitespace "hello\tworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-newlines ()
+ "Should remove newlines (joining lines)."
+ (let ((result (test-delete-all-whitespace "hello\nworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-mixed ()
+ "Should remove all types of whitespace."
+ (let ((result (test-delete-all-whitespace "hello \t\n world")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-multiple-words ()
+ "Should remove whitespace from multiple words."
+ (let ((result (test-delete-all-whitespace "one two three four")))
+ (should (string= result "onetwothreefour"))))
+
+(ert-deftest test-delete-all-whitespace-multiline ()
+ "Should remove all whitespace across multiple lines."
+ (let ((result (test-delete-all-whitespace "line1\nline2\nline3")))
+ (should (string= result "line1line2line3"))))
+
+(ert-deftest test-delete-all-whitespace-leading-trailing ()
+ "Should remove leading and trailing whitespace."
+ (let ((result (test-delete-all-whitespace " hello world ")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-carriage-returns ()
+ "Should handle carriage returns."
+ (let ((result (test-delete-all-whitespace "hello\r\nworld")))
+ (should (string= result "helloworld"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-delete-all-whitespace-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-delete-all-whitespace "")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-all-whitespace-no-whitespace ()
+ "Should handle text with no whitespace (no-op)."
+ (let ((result (test-delete-all-whitespace "helloworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-only-whitespace ()
+ "Should delete all content when only whitespace exists."
+ (let ((result (test-delete-all-whitespace " \t \n ")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-all-whitespace-single-char ()
+ "Should handle single character with surrounding whitespace."
+ (let ((result (test-delete-all-whitespace " x ")))
+ (should (string= result "x"))))
+
+(ert-deftest test-delete-all-whitespace-very-long-text ()
+ "Should handle very long text."
+ (let ((result (test-delete-all-whitespace "word word word word word word word word")))
+ (should (string= result "wordwordwordwordwordwordwordword"))))
+
+(ert-deftest test-delete-all-whitespace-single-whitespace ()
+ "Should delete single whitespace character."
+ (let ((result (test-delete-all-whitespace " ")))
+ (should (string= result ""))))
+
+(ert-deftest test-delete-all-whitespace-consecutive-newlines ()
+ "Should remove all consecutive newlines."
+ (let ((result (test-delete-all-whitespace "hello\n\n\nworld")))
+ (should (string= result "helloworld"))))
+
+(ert-deftest test-delete-all-whitespace-complex-structure ()
+ "Should handle complex whitespace patterns."
+ (let ((result (test-delete-all-whitespace " hello\n\t world \n foo\t\tbar ")))
+ (should (string= result "helloworldfoobar"))))
+
+;;; Error Cases
+
+(ert-deftest test-delete-all-whitespace-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "hello world")
+ (cj/--delete-all-whitespace (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-delete-all-whitespace-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "hello world")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--delete-all-whitespace pos pos)
+ ;; Should complete without error and not change buffer
+ (should (string= (buffer-string) "hello world")))))
+
+(provide 'test-custom-whitespace-delete-all)
+;;; test-custom-whitespace-delete-all.el ends here
diff --git a/tests/test-custom-whitespace-ensure-single-blank.el b/tests/test-custom-whitespace-ensure-single-blank.el
new file mode 100644
index 00000000..7cd03e79
--- /dev/null
+++ b/tests/test-custom-whitespace-ensure-single-blank.el
@@ -0,0 +1,146 @@
+;;; test-custom-whitespace-ensure-single-blank.el --- Tests for cj/--ensure-single-blank-line -*- lexical-binding: t; -*-
+
+;;; Commentary:
+;; Tests for the cj/--ensure-single-blank-line function from custom-whitespace.el
+;;
+;; This function collapses multiple consecutive blank lines to exactly one blank line.
+;; Different from delete-blank-lines which removes ALL blank lines, this function
+;; preserves blank lines but ensures no more than one blank line appears consecutively.
+;;
+;; A blank line is defined as a line containing only whitespace (spaces, tabs) or nothing.
+;; Uses the regexp (^[[:space:]]*$\n){2,} to match 2+ consecutive blank lines.
+;;
+;; We test the NON-INTERACTIVE implementation (cj/--ensure-single-blank-line)
+;; to avoid mocking user prompts. This follows our testing best practice
+;; of separating business logic from UI interaction.
+
+;;; Code:
+
+(require 'ert)
+(require 'testutil-general)
+
+;; Add modules directory to load path
+(add-to-list 'load-path (expand-file-name "modules" user-emacs-directory))
+
+;; Stub dependencies before loading the module
+(defvar cj/custom-keymap (make-sparse-keymap)
+ "Stub keymap for testing.")
+
+;; Now load the actual production module
+(require 'custom-whitespace)
+
+;;; Test Helpers
+
+(defun test-ensure-single-blank-line (input-text)
+ "Test cj/--ensure-single-blank-line on INPUT-TEXT.
+Returns the buffer string after operation."
+ (with-temp-buffer
+ (insert input-text)
+ (cj/--ensure-single-blank-line (point-min) (point-max))
+ (buffer-string)))
+
+;;; Normal Cases
+
+(ert-deftest test-ensure-single-blank-two-blanks ()
+ "Should collapse two blank lines to one."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-three-blanks ()
+ "Should collapse three blank lines to one."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\n\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-many-blanks ()
+ "Should collapse many blank lines to one."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\n\n\n\n\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-preserve-single ()
+ "Should preserve single blank lines (no-op)."
+ (let ((result (test-ensure-single-blank-line "line1\n\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-multiple-groups ()
+ "Should handle multiple groups of consecutive blanks."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\nline2\n\n\n\nline3")))
+ (should (string= result "line1\n\nline2\n\nline3"))))
+
+(ert-deftest test-ensure-single-blank-blanks-with-spaces ()
+ "Should handle blank lines with spaces only."
+ (let ((result (test-ensure-single-blank-line "line1\n \n \nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-blanks-with-tabs ()
+ "Should handle blank lines with tabs only."
+ (let ((result (test-ensure-single-blank-line "line1\n\t\t\n\t\t\nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-mixed-whitespace ()
+ "Should handle blank lines with mixed whitespace."
+ (let ((result (test-ensure-single-blank-line "line1\n \t \n \t \nline2")))
+ (should (string= result "line1\n\nline2"))))
+
+(ert-deftest test-ensure-single-blank-no-blanks ()
+ "Should handle text with no blank lines (no-op)."
+ (let ((result (test-ensure-single-blank-line "line1\nline2\nline3")))
+ (should (string= result "line1\nline2\nline3"))))
+
+;;; Boundary Cases
+
+(ert-deftest test-ensure-single-blank-empty-string ()
+ "Should handle empty string."
+ (let ((result (test-ensure-single-blank-line "")))
+ (should (string= result ""))))
+
+(ert-deftest test-ensure-single-blank-only-blanks ()
+ "Should collapse many blank lines to one blank line."
+ (let ((result (test-ensure-single-blank-line "\n\n\n\n")))
+ (should (string= result "\n\n"))))
+
+(ert-deftest test-ensure-single-blank-at-start ()
+ "Should collapse multiple blank lines at start to one."
+ (let ((result (test-ensure-single-blank-line "\n\n\nline1")))
+ (should (string= result "\n\nline1"))))
+
+(ert-deftest test-ensure-single-blank-at-end ()
+ "Should collapse multiple blank lines at end to one."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\n")))
+ (should (string= result "line1\n\n"))))
+
+(ert-deftest test-ensure-single-blank-single-line ()
+ "Should handle single line (no-op)."
+ (let ((result (test-ensure-single-blank-line "line1")))
+ (should (string= result "line1"))))
+
+(ert-deftest test-ensure-single-blank-complex-structure ()
+ "Should handle complex mix of content and blanks."
+ (let ((result (test-ensure-single-blank-line "line1\n\n\nline2\nline3\n\n\n\nline4")))
+ (should (string= result "line1\n\nline2\nline3\n\nline4"))))
+
+(ert-deftest test-ensure-single-blank-preserves-content ()
+ "Should not modify lines with content."
+ (let ((result (test-ensure-single-blank-line " line1 \n\n\n line2 ")))
+ (should (string= result " line1 \n\n line2 "))))
+
+;;; Error Cases
+
+(ert-deftest test-ensure-single-blank-start-greater-than-end ()
+ "Should error when start > end."
+ (should-error
+ (with-temp-buffer
+ (insert "line1\n\n\nline2")
+ (cj/--ensure-single-blank-line (point-max) (point-min)))
+ :type 'error))
+
+(ert-deftest test-ensure-single-blank-empty-region ()
+ "Should handle empty region (start == end) without error."
+ (with-temp-buffer
+ (insert "line1\n\n\nline2")
+ (let ((pos (/ (+ (point-min) (point-max)) 2)))
+ (cj/--ensure-single-blank-line pos pos)
+ ;; Should complete without error
+ (should (string-match-p "line1" (buffer-string))))))
+
+(provide 'test-custom-whitespace-ensure-single-blank)
+;;; test-custom-whitespace-ensure-single-blank.el ends here