diff options
| author | Craig Jennings <c@cjennings.net> | 2025-10-26 20:48:20 -0500 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2025-10-26 20:48:20 -0500 |
| commit | 1066d0410660d1820064c757abe7b5aad6e6070f (patch) | |
| tree | 4bfebf42ce7cf54475d43989fdd0836d385cc306 /modules/custom-whitespace.el | |
| parent | f14d0e933b3e8e7ff138a3c19a4dbc04d478dba5 (diff) | |
Refactor 4 whitespace manipulation functions (remove-leading-trailing,
collapse-whitespace, delete-blank-lines, hyphenate-whitespace) to use
interactive/non-interactive pattern for testability. Add 73 tests across
4 test files covering normal, boundary, and error cases. Fix linter warnings
and add proper input validation.
Diffstat (limited to 'modules/custom-whitespace.el')
| -rw-r--r-- | modules/custom-whitespace.el | 133 |
1 files changed, 89 insertions, 44 deletions
diff --git a/modules/custom-whitespace.el b/modules/custom-whitespace.el index a69d6138..f2a9d60a 100644 --- a/modules/custom-whitespace.el +++ b/modules/custom-whitespace.el @@ -17,14 +17,32 @@ ;;; Code: +(eval-when-compile (defvar cj/custom-keymap)) ;; cj/custom-keymap defined in keybindings.el ;;; ---------------------- Whitespace Operations And Keymap --------------------- +;; ------------------- Remove Leading/Trailing Whitespace --------------------- + +(defun cj/--remove-leading-trailing-whitespace (start end) + "Internal implementation: Remove leading and trailing whitespace. +START and END define the region to operate on. +Removes leading whitespace (^[ \\t]+) and trailing whitespace ([ \\t]+$). +Preserves interior 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)) + (while (re-search-forward "^[ \t]+" nil t) (replace-match "")) + (goto-char (point-min)) + (while (re-search-forward "[ \t]+$" nil t) (replace-match ""))))) + (defun cj/remove-leading-trailing-whitespace () "Remove leading and trailing whitespace in a region, line, or buffer. When called interactively: - If a region is active, operate on the region. -- If called with a \[universal-argument] prefix, operate on the entire buffer. +- If called with a \\[universal-argument] prefix, operate on the entire buffer. - Otherwise, operate on the current line." (interactive) (let ((start (cond (current-prefix-arg (point-min)) @@ -33,36 +51,57 @@ When called interactively: (end (cond (current-prefix-arg (point-max)) ((use-region-p) (region-end)) (t (line-end-position))))) - (save-excursion - (save-restriction - (narrow-to-region start end) - (goto-char (point-min)) - (while (re-search-forward "^[ \t]+" nil t) (replace-match "")) - (goto-char (point-min)) - (while (re-search-forward "[ \t]+$" nil t) (replace-match "")))))) + (cj/--remove-leading-trailing-whitespace start end))) + +;; ----------------------- Collapse Whitespace --------------------------------- + +(defun cj/--collapse-whitespace (start end) + "Internal implementation: Collapse whitespace to single spaces. +START and END define the region to operate on. +Converts tabs to spaces, removes leading/trailing whitespace, +and collapses multiple spaces to single space. +Preserves newlines and line structure." + (when (> start end) + (error "Invalid region: start (%d) is greater than end (%d)" start end)) + (save-excursion + (save-restriction + (narrow-to-region start end) + ;; Replace all tabs with space + (goto-char (point-min)) + (while (search-forward "\t" nil t) + (replace-match " " nil t)) + ;; Remove leading and trailing spaces (but not newlines) + (goto-char (point-min)) + (while (re-search-forward "^[ \t]+\\|[ \t]+$" nil t) + (replace-match "" nil nil)) + ;; Ensure only one space between words (but preserve newlines) + (goto-char (point-min)) + (while (re-search-forward "[ \t]\\{2,\\}" nil t) + (replace-match " " nil nil))))) (defun cj/collapse-whitespace-line-or-region () "Collapse whitespace to one space in the current line or active region. -Ensure there is exactly one space between words and remove leading and trailing whitespace." +Ensure there is exactly one space between words and remove leading and +trailing whitespace." (interactive) + (let* ((region-active (use-region-p)) + (beg (if region-active (region-beginning) (line-beginning-position))) + (end (if region-active (region-end) (line-end-position)))) + (cj/--collapse-whitespace beg end))) + +;; ------------------------ Delete Blank Lines --------------------------------- + +(defun cj/--delete-blank-lines (start end) + "Internal implementation: Delete blank lines between START and END. +Blank lines are lines containing only whitespace or nothing. +Uses the regexp ^[[:space:]]*$ to match blank lines." + (when (> start end) + (error "Invalid region: start (%d) is greater than end (%d)" start end)) (save-excursion - (let* ((region-active (use-region-p)) - (beg (if region-active (region-beginning) (line-beginning-position))) - (end (if region-active (region-end) (line-end-position)))) - (save-restriction - (narrow-to-region beg end) - ;; Replace all tabs with space - (goto-char (point-min)) - (while (search-forward "\t" nil t) - (replace-match " " nil t)) - ;; Remove leading and trailing spaces - (goto-char (point-min)) - (while (re-search-forward "^\\s-+\\|\\s-+$" nil t) - (replace-match "" nil nil)) - ;; Ensure only one space between words/symbols - (goto-char (point-min)) - (while (re-search-forward "\\s-\\{2,\\}" nil t) - (replace-match " " nil nil)))))) + (save-restriction + (widen) + ;; Regexp "^[[:space:]]*$" matches lines of zero or more spaces/tabs/newlines. + (flush-lines "^[[:space:]]*$" start end)))) (defun cj/delete-blank-lines-region-or-buffer (start end) "Delete blank lines between START and END. @@ -73,32 +112,38 @@ Signal a user error and do nothing when the user declines. Restore point to its original position after deletion." (interactive (if (use-region-p) - ;; grab its boundaries if there's a region - (list (region-beginning) (region-end)) - ;; or ask if user intended operating on whole buffer - (if (yes-or-no-p "Delete blank lines in entire buffer? ") - (list (point-min) (point-max)) - (user-error "Aborted")))) - (save-excursion - (save-restriction - (widen) - ;; Regexp "^[[:space:]]*$" matches lines of zero or more spaces/tabs. - (flush-lines "^[[:space:]]*$" start end))) + ;; grab its boundaries if there's a region + (list (region-beginning) (region-end)) + ;; or ask if user intended operating on whole buffer + (if (yes-or-no-p "Delete blank lines in entire buffer? ") + (list (point-min) (point-max)) + (user-error "Aborted")))) + (cj/--delete-blank-lines start end) ;; Return nil (Emacs conventions). Point is already restored. nil) +;; ------------------------- Hyphenate Whitespace ------------------------------ + +(defun cj/--hyphenate-whitespace (start end) + "Internal implementation: Replace whitespace runs with hyphens. +START and END define the region to operate on. +Replaces all runs of spaces, tabs, newlines, and carriage returns with hyphens." + (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/hyphenate-whitespace-in-region (start end) "Replace runs of whitespace between START and END with hyphens. Operate on the active region designated by START and END." (interactive "*r") (if (use-region-p) - (save-excursion - (save-restriction - (narrow-to-region start end) - (goto-char (point-min)) - (while (re-search-forward "[ \t\n\r]+" nil t) - (replace-match "-")))) - (message "No region; nothing to hyphenate."))) + (cj/--hyphenate-whitespace start end) + (message "No region; nothing to hyphenate."))) ;; Whitespace operations prefix and keymap |
