summaryrefslogtreecommitdiff
path: root/modules/custom-whitespace.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/custom-whitespace.el')
-rw-r--r--modules/custom-whitespace.el196
1 files changed, 151 insertions, 45 deletions
diff --git a/modules/custom-whitespace.el b/modules/custom-whitespace.el
index a69d6138..df93459a 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,90 @@ 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)))
+
+;; --------------------- 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
- (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
+ (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)
+ "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
+ (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 +145,62 @@ 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)
+;; ------------------------- 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)
+ "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
@@ -107,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