diff options
| -rw-r--r-- | modules/custom-ordering.el | 96 | ||||
| -rw-r--r-- | tests/test-custom-ordering-number-lines.el | 181 | ||||
| -rw-r--r-- | tests/test-custom-ordering-reverse-lines.el | 131 | ||||
| -rw-r--r-- | tests/test-custom-ordering-toggle-quotes.el | 155 |
4 files changed, 562 insertions, 1 deletions
diff --git a/modules/custom-ordering.el b/modules/custom-ordering.el index 034216d6..abc9995a 100644 --- a/modules/custom-ordering.el +++ b/modules/custom-ordering.el @@ -4,9 +4,14 @@ ;; Text transformation and sorting utilities for reformatting data structures. ;; -;; Main functions: +;; Array/list formatting: ;; - arrayify/listify - convert lines to comma-separated format (with/without quotes, brackets) ;; - unarrayify - convert arrays back to separate lines +;; +;; Line manipulation: +;; - toggle-quotes - swap double ↔ single quotes +;; - reverse-lines - reverse line order +;; - number-lines - add line numbers with custom format (supports zero-padding) ;; - alphabetize-region - sort words alphabetically ;; - comma-separated-text-to-lines - split CSV text into lines ;; @@ -15,6 +20,8 @@ ;;; Code: +(require 'cl-lib) + ;; cj/custom-keymap defined in keybindings.el (eval-when-compile (defvar cj/custom-keymap)) (defvar cj/ordering-map) @@ -89,6 +96,90 @@ START and END identify the active region." (delete-region start end) (insert insertion))) +(defun cj/--toggle-quotes (start end) + "Internal implementation: Toggle between double and single quotes. +START and END define the region to operate on. +Swaps all double quotes with single quotes and vice versa. +Returns the transformed string without modifying the buffer." + (when (> start end) + (error "Invalid region: start (%d) is greater than end (%d)" start end)) + (let ((text (buffer-substring start end))) + (with-temp-buffer + (insert text) + (goto-char (point-min)) + ;; Use a placeholder to avoid double-swapping + (while (search-forward "\"" nil t) + (replace-match "\001" nil t)) + (goto-char (point-min)) + (while (search-forward "'" nil t) + (replace-match "\"" nil t)) + (goto-char (point-min)) + (while (search-forward "\001" nil t) + (replace-match "'" nil t)) + (buffer-string)))) + +(defun cj/toggle-quotes (start end) + "Toggle between double and single quotes in region between START and END. +START and END identify the active region." + (interactive "r") + (let ((insertion (cj/--toggle-quotes start end))) + (delete-region start end) + (insert insertion))) + +(defun cj/--reverse-lines (start end) + "Internal implementation: Reverse the order of lines in region. +START and END define the region to operate on. +Returns the transformed string without modifying the buffer." + (when (> start end) + (error "Invalid region: start (%d) is greater than end (%d)" start end)) + (let ((lines (split-string (buffer-substring start end) "\n"))) + (mapconcat #'identity (nreverse lines) "\n"))) + +(defun cj/reverse-lines (start end) + "Reverse the order of lines in region between START and END. +START and END identify the active region." + (interactive "r") + (let ((insertion (cj/--reverse-lines start end))) + (delete-region start end) + (insert insertion))) + +(defun cj/--number-lines (start end format-string zero-pad) + "Internal implementation: Number lines in region with custom format. +START and END define the region to operate on. +FORMAT-STRING is the format for each line, with N as placeholder for number. + Example: \"N. \" produces \"1. \", \"2. \", etc. + Example: \"[N] \" produces \"[1] \", \"[2] \", etc. +ZERO-PAD when non-nil pads numbers with zeros for alignment. + Example with 100 lines: \"001\", \"002\", ..., \"100\". +Returns the transformed string without modifying the buffer." + (when (> start end) + (error "Invalid region: start (%d) is greater than end (%d)" start end)) + (let* ((lines (split-string (buffer-substring start end) "\n")) + (line-count (length lines)) + (width (if zero-pad (length (number-to-string line-count)) 1)) + (format-spec (if zero-pad (format "%%0%dd" width) "%d"))) + (mapconcat + (lambda (pair) + (let* ((num (car pair)) + (line (cdr pair)) + (num-str (format format-spec num))) + (concat (replace-regexp-in-string "N" num-str format-string) line))) + (cl-loop for line in lines + for i from 1 + collect (cons i line)) + "\n"))) + +(defun cj/number-lines (start end format-string zero-pad) + "Number lines in region between START and END with custom format. +START and END identify the active region. +FORMAT-STRING is the format for each line, with N as placeholder for number. + Example: \"N. \" produces \"1. \", \"2. \", etc. +ZERO-PAD when non-nil (prefix argument) pads numbers with zeros." + (interactive "r\nMFormat string (use N for number): \nP") + (let ((insertion (cj/--number-lines start end format-string zero-pad))) + (delete-region start end) + (insert insertion))) + (defun cj/--alphabetize-region (start end) "Internal implementation: Alphabetize words in region. START and END define the region to operate on. @@ -154,6 +245,9 @@ Returns the transformed string without modifying the buffer." "l" #'cj/listify "j" #'cj/arrayify-json "p" #'cj/arrayify-python + "q" #'cj/toggle-quotes + "r" #'cj/reverse-lines + "n" #'cj/number-lines "A" #'cj/alphabetize-region "L" #'cj/comma-separated-text-to-lines) diff --git a/tests/test-custom-ordering-number-lines.el b/tests/test-custom-ordering-number-lines.el new file mode 100644 index 00000000..adda84f0 --- /dev/null +++ b/tests/test-custom-ordering-number-lines.el @@ -0,0 +1,181 @@ +;;; test-custom-ordering-number-lines.el --- Tests for cj/--number-lines -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/--number-lines function from custom-ordering.el +;; +;; This function numbers lines in a region with a customizable format. +;; The format string uses "N" as a placeholder for the line number. +;; Optionally supports zero-padding for alignment. +;; +;; Examples: +;; Input: "apple\nbanana\ncherry" +;; Format: "N. " +;; Output: "1. apple\n2. banana\n3. cherry" +;; +;; With zero-padding and 100 lines: +;; "001. line\n002. line\n...\n100. line" +;; +;; We test the NON-INTERACTIVE implementation (cj/--number-lines) to avoid +;; mocking user input. This follows our testing best practice of +;; separating business logic from UI interaction. + +;;; Code: + +(require 'ert) +(require 'testutil-general) +(require 'cl-lib) + +;; 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-ordering) + +;;; Test Helpers + +(defun test-number-lines (input-text format-string zero-pad) + "Test cj/--number-lines on INPUT-TEXT. +FORMAT-STRING is the format template. +ZERO-PAD enables zero-padding. +Returns the transformed string." + (with-temp-buffer + (insert input-text) + (cj/--number-lines (point-min) (point-max) format-string zero-pad))) + +;;; Normal Cases - Standard Format "N. " + +(ert-deftest test-number-lines-standard-format () + "Should number lines with standard format." + (let ((result (test-number-lines "apple\nbanana\ncherry" "N. " nil))) + (should (string= result "1. apple\n2. banana\n3. cherry")))) + +(ert-deftest test-number-lines-two-lines () + "Should number two lines." + (let ((result (test-number-lines "first\nsecond" "N. " nil))) + (should (string= result "1. first\n2. second")))) + +(ert-deftest test-number-lines-single-line () + "Should number single line." + (let ((result (test-number-lines "only" "N. " nil))) + (should (string= result "1. only")))) + +;;; Normal Cases - Alternative Formats + +(ert-deftest test-number-lines-parenthesis-format () + "Should number with parenthesis format." + (let ((result (test-number-lines "a\nb\nc" "N) " nil))) + (should (string= result "1) a\n2) b\n3) c")))) + +(ert-deftest test-number-lines-bracket-format () + "Should number with bracket format." + (let ((result (test-number-lines "x\ny\nz" "[N] " nil))) + (should (string= result "[1] x\n[2] y\n[3] z")))) + +(ert-deftest test-number-lines-no-space-format () + "Should number without space." + (let ((result (test-number-lines "a\nb" "N." nil))) + (should (string= result "1.a\n2.b")))) + +(ert-deftest test-number-lines-custom-format () + "Should number with custom format." + (let ((result (test-number-lines "foo\nbar" "Item N: " nil))) + (should (string= result "Item 1: foo\nItem 2: bar")))) + +;;; Normal Cases - Zero Padding + +(ert-deftest test-number-lines-zero-pad-single-digit () + "Should not pad when max is single digit." + (let ((result (test-number-lines "a\nb\nc" "N. " t))) + (should (string= result "1. a\n2. b\n3. c")))) + +(ert-deftest test-number-lines-zero-pad-double-digit () + "Should pad to 2 digits when max is 10-99." + (let* ((lines (make-list 12 "line")) + (input (mapconcat #'identity lines "\n")) + (result (test-number-lines input "N. " t)) + (result-lines (split-string result "\n"))) + (should (string-prefix-p "01. " (nth 0 result-lines))) + (should (string-prefix-p "09. " (nth 8 result-lines))) + (should (string-prefix-p "10. " (nth 9 result-lines))) + (should (string-prefix-p "12. " (nth 11 result-lines))))) + +(ert-deftest test-number-lines-zero-pad-triple-digit () + "Should pad to 3 digits when max is 100+." + (let* ((lines (make-list 105 "x")) + (input (mapconcat #'identity lines "\n")) + (result (test-number-lines input "N. " t)) + (result-lines (split-string result "\n"))) + (should (string-prefix-p "001. " (nth 0 result-lines))) + (should (string-prefix-p "099. " (nth 98 result-lines))) + (should (string-prefix-p "100. " (nth 99 result-lines))) + (should (string-prefix-p "105. " (nth 104 result-lines))))) + +;;; Boundary Cases + +(ert-deftest test-number-lines-empty-string () + "Should handle empty string." + (let ((result (test-number-lines "" "N. " nil))) + (should (string= result "1. ")))) + +(ert-deftest test-number-lines-empty-lines () + "Should number empty lines." + (let ((result (test-number-lines "\n\n" "N. " nil))) + (should (string= result "1. \n2. \n3. ")))) + +(ert-deftest test-number-lines-with-existing-numbers () + "Should number lines that already have content." + (let ((result (test-number-lines "1. old\n2. old" "N. " nil))) + (should (string= result "1. 1. old\n2. 2. old")))) + +(ert-deftest test-number-lines-multiple-N-in-format () + "Should replace multiple N occurrences." + (let ((result (test-number-lines "a\nb" "N-N. " nil))) + (should (string= result "1-1. a\n2-2. b")))) + +(ert-deftest test-number-lines-long-content () + "Should number lines with long content." + (let* ((long-line (make-string 100 ?x)) + (input (format "%s\n%s" long-line long-line)) + (result (test-number-lines input "N. " nil))) + (should (string-prefix-p "1. " result)) + (should (string-match "2\\. " result)))) + +;;; Normal Cases - No Zero Padding vs Zero Padding + +(ert-deftest test-number-lines-comparison-no-pad-vs-pad () + "Should show difference between no padding and padding." + (let* ((input "a\nb\nc\nd\ne\nf\ng\nh\ni\nj") + (no-pad (test-number-lines input "N. " nil)) + (with-pad (test-number-lines input "N. " t)) + (no-pad-lines (split-string no-pad "\n")) + (with-pad-lines (split-string with-pad "\n"))) + ;; Without padding: "1. ", "10. " + (should (string-prefix-p "1. " (nth 0 no-pad-lines))) + (should (string-prefix-p "10. " (nth 9 no-pad-lines))) + ;; With padding: "01. ", "10. " + (should (string-prefix-p "01. " (nth 0 with-pad-lines))) + (should (string-prefix-p "10. " (nth 9 with-pad-lines))))) + +;;; Error Cases + +(ert-deftest test-number-lines-start-greater-than-end () + "Should error when start > end." + (should-error + (with-temp-buffer + (insert "line1\nline2") + (cj/--number-lines (point-max) (point-min) "N. " nil)) + :type 'error)) + +(ert-deftest test-number-lines-empty-region () + "Should handle empty region (start == end)." + (with-temp-buffer + (insert "line1\nline2") + (let ((pos (/ (+ (point-min) (point-max)) 2))) + (should (string= "1. " (cj/--number-lines pos pos "N. " nil)))))) + +(provide 'test-custom-ordering-number-lines) +;;; test-custom-ordering-number-lines.el ends here diff --git a/tests/test-custom-ordering-reverse-lines.el b/tests/test-custom-ordering-reverse-lines.el new file mode 100644 index 00000000..3c71362d --- /dev/null +++ b/tests/test-custom-ordering-reverse-lines.el @@ -0,0 +1,131 @@ +;;; test-custom-ordering-reverse-lines.el --- Tests for cj/--reverse-lines -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/--reverse-lines function from custom-ordering.el +;; +;; This function reverses the order of lines in a region. +;; The first line becomes last, last becomes first, etc. +;; +;; Examples: +;; Input: "line1\nline2\nline3" +;; Output: "line3\nline2\nline1" +;; +;; We test the NON-INTERACTIVE implementation (cj/--reverse-lines) 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-ordering) + +;;; Test Helpers + +(defun test-reverse-lines (input-text) + "Test cj/--reverse-lines on INPUT-TEXT. +Returns the transformed string." + (with-temp-buffer + (insert input-text) + (cj/--reverse-lines (point-min) (point-max)))) + +;;; Normal Cases + +(ert-deftest test-reverse-lines-three-lines () + "Should reverse three lines." + (let ((result (test-reverse-lines "line1\nline2\nline3"))) + (should (string= result "line3\nline2\nline1")))) + +(ert-deftest test-reverse-lines-two-lines () + "Should reverse two lines." + (let ((result (test-reverse-lines "first\nsecond"))) + (should (string= result "second\nfirst")))) + +(ert-deftest test-reverse-lines-many-lines () + "Should reverse many lines." + (let ((result (test-reverse-lines "a\nb\nc\nd\ne"))) + (should (string= result "e\nd\nc\nb\na")))) + +(ert-deftest test-reverse-lines-with-content () + "Should reverse lines with actual content." + (let ((result (test-reverse-lines "apple banana\ncherry date\negg fig"))) + (should (string= result "egg fig\ncherry date\napple banana")))) + +(ert-deftest test-reverse-lines-bidirectional () + "Should reverse back and forth correctly." + (let* ((original "line1\nline2\nline3") + (reversed (test-reverse-lines original)) + (back (test-reverse-lines reversed))) + (should (string= reversed "line3\nline2\nline1")) + (should (string= back original)))) + +;;; Boundary Cases + +(ert-deftest test-reverse-lines-empty-string () + "Should handle empty string." + (let ((result (test-reverse-lines ""))) + (should (string= result "")))) + +(ert-deftest test-reverse-lines-single-line () + "Should handle single line (no change)." + (let ((result (test-reverse-lines "single line"))) + (should (string= result "single line")))) + +(ert-deftest test-reverse-lines-empty-lines () + "Should reverse including empty lines." + (let ((result (test-reverse-lines "a\n\nb"))) + (should (string= result "b\n\na")))) + +(ert-deftest test-reverse-lines-trailing-newline () + "Should handle trailing newline." + (let ((result (test-reverse-lines "line1\nline2\n"))) + (should (string= result "\nline2\nline1")))) + +(ert-deftest test-reverse-lines-only-newlines () + "Should reverse lines that are only newlines." + (let ((result (test-reverse-lines "\n\n\n"))) + (should (string= result "\n\n\n")))) + +(ert-deftest test-reverse-lines-numbers () + "Should reverse numbered lines." + (let ((result (test-reverse-lines "1\n2\n3\n4\n5"))) + (should (string= result "5\n4\n3\n2\n1")))) + +(ert-deftest test-reverse-lines-very-long () + "Should reverse very long list." + (let* ((lines (mapcar #'number-to-string (number-sequence 1 100))) + (input (mapconcat #'identity lines "\n")) + (result (test-reverse-lines input)) + (result-lines (split-string result "\n"))) + (should (= 100 (length result-lines))) + (should (string= "100" (car result-lines))) + (should (string= "1" (car (last result-lines)))))) + +;;; Error Cases + +(ert-deftest test-reverse-lines-start-greater-than-end () + "Should error when start > end." + (should-error + (with-temp-buffer + (insert "line1\nline2") + (cj/--reverse-lines (point-max) (point-min))) + :type 'error)) + +(ert-deftest test-reverse-lines-empty-region () + "Should handle empty region (start == end)." + (with-temp-buffer + (insert "line1\nline2") + (let ((pos (/ (+ (point-min) (point-max)) 2))) + (should (string= "" (cj/--reverse-lines pos pos)))))) + +(provide 'test-custom-ordering-reverse-lines) +;;; test-custom-ordering-reverse-lines.el ends here diff --git a/tests/test-custom-ordering-toggle-quotes.el b/tests/test-custom-ordering-toggle-quotes.el new file mode 100644 index 00000000..e11305ee --- /dev/null +++ b/tests/test-custom-ordering-toggle-quotes.el @@ -0,0 +1,155 @@ +;;; test-custom-ordering-toggle-quotes.el --- Tests for cj/--toggle-quotes -*- lexical-binding: t; -*- + +;;; Commentary: +;; Tests for the cj/--toggle-quotes function from custom-ordering.el +;; +;; This function toggles between double quotes and single quotes. +;; All " become ' and all ' become ". +;; +;; Examples: +;; Input: "apple", "banana" +;; Output: 'apple', 'banana' +;; +;; Input: 'hello', 'world' +;; Output: "hello", "world" +;; +;; We test the NON-INTERACTIVE implementation (cj/--toggle-quotes) 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-ordering) + +;;; Test Helpers + +(defun test-toggle-quotes (input-text) + "Test cj/--toggle-quotes on INPUT-TEXT. +Returns the transformed string." + (with-temp-buffer + (insert input-text) + (cj/--toggle-quotes (point-min) (point-max)))) + +;;; Normal Cases - Double to Single + +(ert-deftest test-toggle-quotes-double-to-single () + "Should convert double quotes to single quotes." + (let ((result (test-toggle-quotes "\"apple\", \"banana\""))) + (should (string= result "'apple', 'banana'")))) + +(ert-deftest test-toggle-quotes-single-double-quote () + "Should convert single double quote." + (let ((result (test-toggle-quotes "\""))) + (should (string= result "'")))) + +(ert-deftest test-toggle-quotes-multiple-double-quotes () + "Should convert multiple double quotes." + (let ((result (test-toggle-quotes "\"hello\" \"world\" \"test\""))) + (should (string= result "'hello' 'world' 'test'")))) + +;;; Normal Cases - Single to Double + +(ert-deftest test-toggle-quotes-single-to-double () + "Should convert single quotes to double quotes." + (let ((result (test-toggle-quotes "'apple', 'banana'"))) + (should (string= result "\"apple\", \"banana\"")))) + +(ert-deftest test-toggle-quotes-single-single-quote () + "Should convert single single quote." + (let ((result (test-toggle-quotes "'"))) + (should (string= result "\"")))) + +(ert-deftest test-toggle-quotes-multiple-single-quotes () + "Should convert multiple single quotes." + (let ((result (test-toggle-quotes "'hello' 'world' 'test'"))) + (should (string= result "\"hello\" \"world\" \"test\"")))) + +;;; Normal Cases - Mixed Quotes + +(ert-deftest test-toggle-quotes-mixed () + "Should toggle mixed quotes." + (let ((result (test-toggle-quotes "\"double\" 'single'"))) + (should (string= result "'double' \"single\"")))) + +(ert-deftest test-toggle-quotes-bidirectional () + "Should toggle back and forth correctly." + (let* ((original "\"apple\", \"banana\"") + (toggled (test-toggle-quotes original)) + (back (test-toggle-quotes toggled))) + (should (string= toggled "'apple', 'banana'")) + (should (string= back original)))) + +;;; Normal Cases - With Text Content + +(ert-deftest test-toggle-quotes-preserves-content () + "Should preserve content while toggling quotes." + (let ((result (test-toggle-quotes "var x = \"hello world\";"))) + (should (string= result "var x = 'hello world';")))) + +(ert-deftest test-toggle-quotes-sql-style () + "Should toggle SQL-style quotes." + (let ((result (test-toggle-quotes "SELECT * FROM users WHERE name='John'"))) + (should (string= result "SELECT * FROM users WHERE name=\"John\"")))) + +(ert-deftest test-toggle-quotes-multiline () + "Should toggle quotes across multiple lines." + (let ((result (test-toggle-quotes "\"line1\"\n\"line2\"\n\"line3\""))) + (should (string= result "'line1'\n'line2'\n'line3'")))) + +;;; Boundary Cases + +(ert-deftest test-toggle-quotes-empty-string () + "Should handle empty string." + (let ((result (test-toggle-quotes ""))) + (should (string= result "")))) + +(ert-deftest test-toggle-quotes-no-quotes () + "Should handle text with no quotes." + (let ((result (test-toggle-quotes "hello world"))) + (should (string= result "hello world")))) + +(ert-deftest test-toggle-quotes-only-double-quotes () + "Should handle string with only double quotes." + (let ((result (test-toggle-quotes "\"\"\"\""))) + (should (string= result "''''")))) + +(ert-deftest test-toggle-quotes-only-single-quotes () + "Should handle string with only single quotes." + (let ((result (test-toggle-quotes "''''"))) + (should (string= result "\"\"\"\"")))) + +(ert-deftest test-toggle-quotes-adjacent-quotes () + "Should handle adjacent quotes." + (let ((result (test-toggle-quotes "\"\"''"))) + (should (string= result "''\"\"")))) + +;;; Error Cases + +(ert-deftest test-toggle-quotes-start-greater-than-end () + "Should error when start > end." + (should-error + (with-temp-buffer + (insert "\"hello\"") + (cj/--toggle-quotes (point-max) (point-min))) + :type 'error)) + +(ert-deftest test-toggle-quotes-empty-region () + "Should handle empty region (start == end)." + (with-temp-buffer + (insert "\"hello\"") + (let ((pos (/ (+ (point-min) (point-max)) 2))) + (should (string= "" (cj/--toggle-quotes pos pos)))))) + +(provide 'test-custom-ordering-toggle-quotes) +;;; test-custom-ordering-toggle-quotes.el ends here |
