summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2025-10-12 14:41:58 -0500
committerCraig Jennings <c@cjennings.net>2025-10-12 14:42:29 -0500
commit147d5c265edf0615d2aa1d65319963d7399711de (patch)
tree84d272d0840c50940e06365eed5ce4935ec47748
parent5db41b5a2dc7107f2d3bd2c226e1dbc3a91719e8 (diff)
maint: remaining custom functions grouped into separate modules
-rw-r--r--init.el9
-rw-r--r--modules/custom-case.el108
-rw-r--r--modules/custom-comments.el196
-rw-r--r--modules/custom-datetime.el93
-rw-r--r--modules/custom-file-buffer.el2
-rw-r--r--modules/custom-functions.el864
-rw-r--r--modules/custom-line-paragraph.el127
-rw-r--r--modules/custom-misc.el90
-rw-r--r--modules/custom-ordering.el84
-rw-r--r--modules/custom-text-enclose.el70
-rw-r--r--modules/custom-whitespace.el106
-rw-r--r--todo.org5
12 files changed, 887 insertions, 867 deletions
diff --git a/init.el b/init.el
index 413ccf67..d702678c 100644
--- a/init.el
+++ b/init.el
@@ -22,8 +22,15 @@
;; -------------------------- Utilities And Libraries --------------------------
-(require 'custom-functions) ;; custom function library w/ keybindings
+(require 'custom-case) ;; operations for upper/lower/title case
+(require 'custom-comments) ;; operations with comments
+(require 'custom-datetime) ;; date/timestamps in various formats
(require 'custom-file-buffer) ;; custom buffer and file operations and keymap
+(require 'custom-line-paragraph) ;; operations on lines and paragraphs
+(require 'custom-misc) ;; miscellaneous functions
+(require 'custom-ordering) ;; ordering and sorting operations
+(require 'custom-text-enclose) ;; operations to append, prepend, and surround text
+(require 'custom-whitespace) ;; whitespace operations
(require 'external-open) ;; files to open outside of Emacs
(require 'media-utils) ;; download and play urls
diff --git a/modules/custom-case.el b/modules/custom-case.el
new file mode 100644
index 00000000..6baffeb7
--- /dev/null
+++ b/modules/custom-case.el
@@ -0,0 +1,108 @@
+;;; custom-case.el --- Custom Functions Handling Text Case -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(defun cj/upcase-dwim ()
+ "Upcase the active region, or upcase the symbol at point if no region."
+ (interactive)
+ (if (use-region-p)
+ (upcase-region (region-beginning) (region-end))
+ (let ((bounds (bounds-of-thing-at-point 'symbol)))
+ (if bounds
+ (upcase-region (car bounds) (cdr bounds))
+ (user-error "No symbol at point")))))
+
+(defun cj/downcase-dwim ()
+ "Downcase the active region, or downcase the symbol at point if no region."
+ (interactive)
+ (if (use-region-p)
+ (downcase-region (region-beginning) (region-end))
+ (let ((bounds (bounds-of-thing-at-point 'symbol)))
+ (if bounds
+ (downcase-region (car bounds) (cdr bounds))
+ (user-error "No symbol at point")))))
+
+(defun cj/title-case-region ()
+ "Capitalize the region in title case format.
+Title case is a capitalization convention where major words
+are capitalized,and most minor words are lowercase. Nouns,
+verbs (including linking verbs), adjectives, adverbs,pronouns,
+and all words of four letters or more are considered major words.
+Short (i.e., three letters or fewer) conjunctions, short prepositions,
+and all articles are considered minor words."
+ (interactive)
+ (let ((beg nil)
+ (end nil)
+ (prev-word-end nil)
+ ;; Allow capitals for skip characters after this, so:
+ ;; Warning: An Example
+ ;; Capitalizes the `An'.
+ (chars-skip-reset '(?: ?! ??))
+ ;; Don't capitalize characters directly after these. e.g.
+ ;; "Foo-bar" or "Foo\bar" or "Foo's".
+
+ (chars-separator '(?\\ ?- ?' ?.))
+
+ (word-chars "[:alnum:]")
+ (word-skip
+ (list "a" "an" "and" "as" "at" "but" "by"
+ "for" "if" "in" "is" "nor" "of"
+ "on" "or" "so" "the" "to" "yet"))
+ (is-first t))
+ (cond
+ ((region-active-p)
+ (setq beg (region-beginning))
+ (setq end (region-end)))
+ (t
+ (setq beg (line-beginning-position))
+ (setq end (line-end-position))))
+ (save-excursion
+ ;; work on uppercased text (e.g., headlines) by downcasing first
+ (downcase-region beg end)
+ (goto-char beg)
+
+ (while (< (point) end)
+ (setq prev-word-end (point))
+ (skip-chars-forward (concat "^" word-chars) end)
+ (let ((word-end
+ (save-excursion
+ (skip-chars-forward word-chars end)
+ (point))))
+
+ (unless (memq (char-before (point)) chars-separator)
+ (let* ((c-orig (char-to-string (char-after (point))))
+ (c-up (capitalize c-orig)))
+ (unless (string-equal c-orig c-up)
+ (let ((word (buffer-substring-no-properties (point) word-end)))
+ (when
+ (or
+ ;; Always allow capitalization.
+ is-first
+ ;; If it's not a skip word, allow.
+ (not (member word word-skip))
+ ;; Check the beginning of the previous word doesn't reset first.
+ (save-excursion
+ (and
+ (not (zerop
+ (skip-chars-backward "[:blank:]" prev-word-end)))
+ (memq (char-before (point)) chars-skip-reset))))
+ (delete-region (point) (1+ (point)))
+ (insert c-up))))))
+ (goto-char word-end)
+ (setq is-first nil))))))
+;; replace the capitalize-region keybinding to call title-case
+(global-set-key [remap capitalize-region] 'cj/title-case-region)
+
+;; Case-change operations prefix and keymap
+(define-prefix-command 'cj/case-map nil
+ "Keymap for case-change operations.")
+(define-key cj/custom-keymap "c" 'cj/case-map)
+(define-key cj/case-map "t" 'cj/title-case-region)
+(define-key cj/case-map "u" 'cj/upcase-dwim)
+(define-key cj/case-map "l" 'cj/downcase-dwim) ;; for "lower" case
+
+(provide 'custom-case)
+;;; custom-case.el ends here.
diff --git a/modules/custom-comments.el b/modules/custom-comments.el
new file mode 100644
index 00000000..2f508da4
--- /dev/null
+++ b/modules/custom-comments.el
@@ -0,0 +1,196 @@
+;;; custom-comments.el --- Custom Comment Operations -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(require 'custom-line-paragraph)
+
+;; --------------------------- Delete Buffer Comments --------------------------
+
+(defun cj/delete-buffer-comments ()
+ "Delete all comments within the current buffer."
+ (interactive)
+ (goto-char (point-min))
+ (let (kill-ring)
+ (comment-kill (count-lines (point-min) (point-max)))))
+
+;; ----------------------------- Comment User Edit -----------------------------
+
+(defun cj/comment-user-edit ()
+ "Reformat commented text in an indirect buffer."
+ (interactive)
+ (if mark-active
+ (uncomment-region beg end)
+ (let ((beg (region-beginning))
+ (end (copy-marker (region-end)))
+ (source buffer (current-buffer))
+ (indirect-buffer (make-indirect-buffer source-buffer))
+ (with-current-buffer indirect-buffer
+ ;; Narrow to the selected region
+ (narrow-to-region start end)
+ (goto-char (point-min))
+
+
+ )))
+ (comment-region beg end))
+ ;; if no region
+ (message "No region was selected. Select the comment lines to reformat."))
+
+;; ------------------------------ Comment Reformat -----------------------------
+
+(defun cj/comment-reformat ()
+ "Reformat commented text into a single paragraph."
+ (interactive)
+ (if mark-active
+ (let ((beg (region-beginning))
+ (end (copy-marker (region-end)))
+ (orig-fill-column fill-column))
+ (uncomment-region beg end)
+ (setq fill-column (- fill-column 3))
+ (cj/join-line-or-region beg end)
+ (comment-region beg end)
+ (setq fill-column orig-fill-column )))
+ ;; if no region
+ (message "No region was selected. Select the comment lines to reformat."))
+
+;; ------------------------------ Comment Centered -----------------------------
+
+(defun cj/comment-centered (&optional comment-char)
+ "Insert comment text centered around the COMMENT-CHAR character.
+Default to the hash character when COMMENT-CHAR is nil.
+Use the lesser of `fill-column' or 80 to calculate the comment length.
+Begin and end the line with the appropriate comment symbols for the current mode."
+ (interactive)
+ (if (not (char-or-string-p comment-char))
+ (setq comment-char "#"))
+ (let* ((comment (capitalize (string-trim (read-from-minibuffer "Comment: "))))
+ (fill-column (min fill-column 80))
+ (comment-length (length comment))
+ (comment-start-length (length comment-start))
+ (comment-end-length (length comment-end))
+ (current-column-pos (current-column))
+ (space-on-each-side (/ (- fill-column
+ current-column-pos
+ comment-length
+ (length comment-start)
+ (length comment-end)
+ ;; Single space on each side of comment
+ (if (> comment-length 0) 2 0)
+ ;; Single space after comment syntax sting
+ 1)
+ 2)))
+ (if (< space-on-each-side 2)
+ (message "Comment string is too big to fit in one line")
+ (progn
+ (insert comment-start)
+ (when (equal comment-start ";") ;; emacs-lisp line comments are ';;'
+ (insert comment-start)) ;; so insert comment-char again
+ (insert " ")
+ (dotimes (_ space-on-each-side) (insert comment-char))
+ (when (> comment-length 0) (insert " "))
+ (insert comment)
+ (when (> comment-length 0) (insert " "))
+ (dotimes (_ (if (= (% comment-length 2) 0)
+ (- space-on-each-side 1)
+ space-on-each-side))
+ (insert comment-char))
+ ;; Only insert trailing space and comment-end if comment-end is not empty
+ (when (not (string-empty-p comment-end))
+ (insert " ")
+ (insert comment-end))))))
+
+;; -------------------------------- Comment Box --------------------------------
+
+(defun cj/comment-box ()
+ "Insert a comment box around text that the user inputs.
+The box extends to the fill column, centers the text, and uses the current
+mode's comment syntax at both the beginning and end of each line. The box
+respects the current indentation level and avoids trailing whitespace."
+ (interactive)
+ (let* ((comment-char (if (equal comment-start ";") ";;"
+ (string-trim comment-start)))
+ (comment-end-char (if (string-empty-p comment-end)
+ comment-char
+ (string-trim comment-end)))
+ (line-char (if (equal comment-char ";;") "-" "#"))
+ (comment (capitalize (string-trim (read-from-minibuffer "Comment: "))))
+ (comment-length (length comment))
+ (current-column-pos (current-column))
+ (max-width (min fill-column 80))
+ ;; Calculate available width between comment markers
+ (available-width (- max-width
+ current-column-pos
+ (length comment-char)
+ (length comment-end-char)))
+ ;; Inner width is the width without the spaces after comment start and before comment end
+ (inner-width (- available-width 2))
+ ;; Calculate padding for each side of the centered text
+ (padding-each-side (max 1 (/ (- inner-width comment-length) 2)))
+ ;; Adjust for odd-length comments
+ (right-padding (if (= (% (- inner-width comment-length) 2) 0)
+ padding-each-side
+ (1+ padding-each-side))))
+
+ ;; Check if we have enough space
+ (if (< inner-width (+ comment-length 4)) ; minimum sensible width
+ (message "Comment string is too big to fit in one line")
+ (progn
+ ;; Top line - fill entirely with line characters except for space after comment start
+ (insert comment-char)
+ (insert " ")
+ (insert (make-string inner-width (string-to-char line-char)))
+ (insert " ")
+ (insert comment-end-char)
+ (newline)
+
+ ;; Add indentation on the new line to match current column
+ (dotimes (_ current-column-pos) (insert " "))
+
+ ;; Middle line with centered text
+ (insert comment-char)
+ (insert " ")
+ ;; Left padding
+ (dotimes (_ padding-each-side) (insert " "))
+ ;; The comment text
+ (insert comment)
+ ;; Right padding
+ (dotimes (_ right-padding) (insert " "))
+ (insert " ")
+ (insert comment-end-char)
+ (newline)
+
+ ;; Add indentation on the new line to match current column
+ (dotimes (_ current-column-pos) (insert " "))
+
+ ;; Bottom line - same as top line
+ (insert comment-char)
+ (insert " ")
+ (dotimes (_ inner-width) (insert line-char))
+ (insert " ")
+ (insert comment-end-char)
+ (newline)))))
+
+;; ------------------------------- Comment Hyphen ------------------------------
+
+(defun cj/comment-hyphen()
+ "Insert a centered comment with '-' (hyphens) on each side.
+Leverages cj/comment-centered."
+ (interactive)
+ (cj/comment-centered "-"))
+
+;; ------------------------------- Comment Keymap ------------------------------
+
+;; Comment styles & removal prefix and keymap
+(define-prefix-command 'cj/comment-map nil
+ "Keymap for comment styling and removal.")
+(define-key cj/custom-keymap "C" 'cj/comment-map)
+(define-key cj/comment-map "r" 'cj/comment-reformat)
+(define-key cj/comment-map "c" 'cj/comment-centered)
+(define-key cj/comment-map "-" 'cj/comment-hyphen)
+(define-key cj/comment-map "b" 'cj/comment-box)
+(define-key cj/comment-map "D" 'cj/delete-buffer-comments)
+
+(provide 'custom-comments)
+;;; custom-comments.el ends here.
diff --git a/modules/custom-datetime.el b/modules/custom-datetime.el
new file mode 100644
index 00000000..95d672c8
--- /dev/null
+++ b/modules/custom-datetime.el
@@ -0,0 +1,93 @@
+;;; custom-datetime.el --- -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+
+(defvar readable-date-time-format "%A, %B %d, %Y at %I:%M:%S %p %Z "
+ "Format string used by `cj/insert-readable-date-time'.
+
+See `format-time-string' for possible replacements.")
+
+(defun cj/insert-readable-date-time ()
+ "Insert the current date and time into the current buffer.
+
+Use `readable-date-time-format' for formatting."
+ (interactive)
+ (insert (format-time-string readable-date-time-format (current-time))))
+
+(defvar sortable-date-time-format "%Y-%m-%d %a @ %H:%M:%S %z "
+ "Format string used by `cj/insert-sortable-date-time'.
+
+See `format-time-string' for possible replacements.")
+
+(defun cj/insert-sortable-date-time ()
+ "Insert the current date and time into the current buffer.
+
+Use `sortable-date-time-format' for formatting."
+ (interactive)
+ (insert (format-time-string sortable-date-time-format (current-time))))
+
+(defvar sortable-time-format "%I:%M:%S %p %Z "
+ "Format string used by `cj/insert-sortable-time'.
+
+See `format-time-string' for possible replacements.")
+
+(defun cj/insert-sortable-time ()
+ "Insert the current time into the current buffer.
+
+Use `sortable-time-format' for formatting."
+ (interactive)
+ (insert (format-time-string sortable-time-format (current-time))))
+
+(defvar readable-time-format "%-I:%M %p "
+ "Format string used by `cj/insert-readable-time'.
+
+See `format-time-string' for possible replacements.")
+
+(defun cj/insert-readable-time ()
+ "Insert the current time into the current buffer.
+
+Use `readable-time-format' for formatting."
+ (interactive)
+ (insert (format-time-string readable-time-format (current-time))))
+
+(defvar sortable-date-format "%Y-%m-%d %a"
+ "Format string used by `cj/insert-sortable-date'.
+
+See `format-time-string' for possible replacements.")
+
+(defun cj/insert-sortable-date ()
+ "Insert the current date into the current buffer.
+
+Use `sortable-date-format' for formatting."
+ (interactive)
+ (insert (format-time-string sortable-date-format (current-time))))
+
+(defvar readable-date-format "%A, %B %d, %Y"
+ "Format string used by `cj/insert-readable-date'.
+
+See `format-time-string' for possible replacements.")
+
+(defun cj/insert-readable-date ()
+ "Insert the current date into the current buffer.
+
+Use `readable-date-format' for formatting."
+ (interactive)
+ (insert (format-time-string readable-date-format (current-time))))
+
+;; Date/time insertion prefix and keymap
+(define-prefix-command 'cj/datetime-map nil
+ "Keymap for inserting various date/time formats.")
+(define-key cj/custom-keymap "d" 'cj/datetime-map)
+(define-key cj/datetime-map "r" 'cj/insert-readable-date-time)
+(define-key cj/datetime-map "s" 'cj/insert-sortable-date-time)
+(define-key cj/datetime-map "t" 'cj/insert-sortable-time)
+(define-key cj/datetime-map "T" 'cj/insert-readable-time)
+(define-key cj/datetime-map "d" 'cj/insert-sortable-date)
+(define-key cj/datetime-map "D" 'cj/insert-readable-date)
+
+(provide 'custom-datetime)
+;;; custom-datetime.el ends here.
diff --git a/modules/custom-file-buffer.el b/modules/custom-file-buffer.el
index 2918ed97..a3362e05 100644
--- a/modules/custom-file-buffer.el
+++ b/modules/custom-file-buffer.el
@@ -93,8 +93,6 @@ Do not save the deleted text in the kill ring."
(delete-region (point) (point-min))
(message "Buffer contents removed to the beginning of the buffer."))
-
-
;; ------------------------- Print Buffer As Postscript ------------------------
;; prints using postscript for much nicer output
diff --git a/modules/custom-functions.el b/modules/custom-functions.el
deleted file mode 100644
index e9a05de3..00000000
--- a/modules/custom-functions.el
+++ /dev/null
@@ -1,864 +0,0 @@
-;;; custom-functions.el --- My Custom Functions and Keymaps -*- lexical-binding: t; coding: utf-8; -*-
-;; author Craig Jennings <c@cjennings.net>
-
-;;; Commentary:
-;;
-;; These are custom utility functions I use frequently.
-;; For convenience, they are bound to a custom keymap with a prefix of "C-;".
-
-;; Additional keymaps are created on top of this prefix to collect similar operations.
-;;
-;; C-; --- Custom Key Map
-;; C-; ) → jump to matching parenthesis
-;; C-; f → re-formats region or buffer (delete trailing whitespace, reindent, and untabify).
-;; C-; W → counts words in region or buffer displaying results in echo area.
-;; C-; / → replace common glyph fractions (½) to text (1/2) (text to glyph with C-u).
-;; C-; A → align text by regexp with spaces
-;; C-; | → toggle visibility of the fill-column indicator
-;;
-;; C-; b --- Buffer & File Operations
-;; C-; b m → move buffer and file to another directory
-;; C-; b r → rename buffer and its file simultaneously
-;; C-; b d → delete buffer and its file simultaneously
-;; C-; b l → copy file:// link of buffer’s source file
-;; C-; b c → copy entire buffer to the kill rung
-;; C-; b b → clear contents of buffer from point to beginnning
-;; C-; b e → clear contents of buffer from point to end
-;;
-;; C-; w --- Whitespace Operations
-;; C-; w r → remove leading/trailing whitespace from line or region (buffer with C-u).
-;; C-; w c → collapses runs of whitespace to one space.
-;; C-; w l → delete all blank lines in region or buffer
-;; C-; w h → hyphenate all whitespace in region
-;;
-;; C-; s --- Surround, Append & Prepend
-;; C-; s s → surround word or region with string
-;; C-; s a → append a string to each line
-;; C-; s p → prepend a string to each line
-;;
-;; C-; d --- Date/Time Insertion
-;; C-; d r → readable date and time : Sunday, August 31, 2025 at 04:07:02 PM CDT
-;; C-; d s → sortable date and time : 2025-08-31 Sun @ 16:07:30 -0500
-;; C-; d t → sortable time only : 04:07:50 PM CDT
-;; C-; d D → readable time only : 4:08 PM
-;; C-; d T → readable date only : Sunday, August 31, 2025
-;; C-; d d → sortable date only : 2025-08-31 Sun
-;;
-;; C-; l --- Line & Paragraph Operations
-;; C-; l j → join lines (or selected region of lines)
-;; C-; l J → join entire paragraph. guesses at the lines that constitute paragraph.
-;; C-; l d → duplicates the line or region
-;; C-; l r → remove duplicate lines from the buffer, keeping the first occurrence.
-;; C-; l R → remove lines containing specific text from the region or buffer.
-;; C-; l u → "underline" current line: repeat a chosen character to same length on line below.
-
-;;
-;; C-; m --- Comment Styling & Removal
-;; C-; m r → reformats selecton into a commented paragraph re-wrapping at fill column width.
-;; C-; m c → insert centered comment
-;; C-; m - → insert hyphen-style comment
-;; C-; m b → draw a comment box
-;; C-; m D → delete all comments in buffer
-;;
-;; C-; o --- Ordering & Sorting
-;; C-; o a → arrayify lines into quoted list
-;; C-; o u → unarrayify list into lines
-;; C-; o A → alphabetize items in region
-;; C-; o l → split comma-separated text onto lines
-;;
-;; C-; c --- Case-Change Operations
-;; C-; c t → Change selected text to Title Case : This is the Title of a Movie
-;; C-; c u → Change word or region to Upper Case : THIS IS THE TITLE OF A MOVIE
-;; C-; c d → Change word or region to Lower Case : this is the title of a movie
-
-;;; Code:
-
-(require 'subr-x)
-
-(use-package expand-region
- :demand t) ;; used w/in join paragraph
-
-;;; ----------------- Miscellaneous Functions And Custom Keymap -----------------
-
-(defun cj/jump-to-matching-paren ()
- "Jump to the matching parenthesis when point is on one.
-
-Signal a message when point is not on a parenthesis."
- (interactive)
- (cond ((looking-at "\\s\(\\|\\s\{\\|\\s\[")
- (forward-list))
- ((looking-back "\\s\)\\|\\s\}\\|\\s\\]")
- (backward-list))
- (t (message "Cursor doesn't follow parenthesis, so there's no match."))))
-
-(defun cj/format-region-or-buffer ()
- "Reformat the region or the entire buffer.
-
-Replaces tabs with spaces, deletes trailing whitespace, and reindents the region."
- (interactive)
- (let ((start-pos (if (use-region-p) (region-beginning) (point-min)))
- (end-pos (if (use-region-p) (region-end) (point-max))))
- (save-excursion
- (save-restriction
- (narrow-to-region start-pos end-pos)
- (untabify (point-min) (point-max)))
- (indent-region (point-min) (point-max))
- (delete-trailing-whitespace))))
-
-(defun cj/count-words-buffer-or-region ()
- "Count the number of words in the buffer or region.
-
-Display the result in the minibuffer and *Messages* buffer."
- (interactive)
- (let ((begin (point-min))
- (end (point-max))
- (area_type "the buffer"))
- (when mark-active
- (setq begin (region-beginning)
- end (region-end)
- area_type "the region"))
- (message (format "There are %d words in %s." (count-words begin end) area_type))))
-
-(defun cj/replace-fraction-glyphs (start end)
- "Replace common fraction glyphs between START and END.
-
-Operate on the buffer or region designated by START and END.
-Replace the text representations with glyphs when called with a \[universal-argument] prefix."
- (interactive (if (use-region-p)
- (list (region-beginning) (region-end))
- (list (point-min) (point-max))))
- (let ((replacements (if current-prefix-arg
- '(("1/4" . "¼")
- ("1/2" . "½")
- ("3/4" . "¾")
- ("1/3" . "⅓")
- ("2/3" . "⅔"))
- '(("¼" . "1/4")
- ("½" . "1/2")
- ("¾" . "3/4")
- ("⅓" . "1/3")
- ("⅔" . "2/3")))))
- (save-excursion
- (dolist (r replacements)
- (goto-char start)
- (while (search-forward (car r) end t)
- (replace-match (cdr r)))))))
-
-(defun cj/align-regexp-with-spaces (orig-fun &rest args)
- "Call ORIG-FUN with ARGS while temporarily disabling tabs for alignment.
-
-This advice ensures `align-regexp' uses spaces by binding `indent-tabs-mode' to nil."
- (let ((indent-tabs-mode nil))
- (apply orig-fun args)))
-
-(advice-remove 'align-regexp #'align-regexp-with-spaces) ; in case this is reloaded
-(advice-add 'align-regexp :around #'cj/align-regexp-with-spaces)
-
-
-(define-key cj/custom-keymap ")" 'cj/jump-to-matching-paren)
-(define-key cj/custom-keymap "f" 'cj/format-region-or-buffer)
-(define-key cj/custom-keymap "W" 'cj/count-words-buffer-or-region)
-(define-key cj/custom-keymap "/" 'cj/replace-fraction-glyphs)
-(define-key cj/custom-keymap "A" 'align-regexp)
-(define-key cj/custom-keymap "B" 'toggle-debug-on-error)
-(define-key cj/custom-keymap "|" 'display-fill-column-indicator-mode)
-
-;;; ---------------------- Whitespace Operations And Keymap ---------------------
-
-(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.
-- Otherwise, operate on the current line."
- (interactive)
- (let ((start (cond (current-prefix-arg (point-min))
- ((use-region-p) (region-beginning))
- (t (line-beginning-position))))
- (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 ""))))))
-
-(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."
- (interactive)
- (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))))))
-
-(defun cj/delete-blank-lines-region-or-buffer (start end)
- "Delete blank lines between START and END.
-
-Treat blank lines as lines that contain nothing or only whitespace.
-Operate on the active region when one exists.
-Prompt before operating on the whole buffer when no region is selected.
-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)))
- ;; Return nil (Emacs conventions). Point is already restored.
- nil)
-
-(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.")))
-
-
-;; Whitespace operations prefix and keymap
-(define-prefix-command 'cj/whitespace-map nil
- "Keymap for whitespace operations.")
-(define-key cj/custom-keymap "w" 'cj/whitespace-map)
-(define-key cj/whitespace-map "r" 'cj/remove-leading-trailing-whitespace)
-(define-key cj/whitespace-map "c" 'cj/collapse-whitespace-line-or-region)
-(define-key cj/whitespace-map "l" 'cj/delete-blank-lines-region-or-buffer)
-(define-key cj/whitespace-map "-" 'cj/hyphenate-whitespace-in-region)
-
-;;; ------------------------- Surround, Append, Prepend -------------------------
-
-(defun cj/surround-word-or-region ()
- "Surround the word at point or active region with a string read from the minibuffer."
- (interactive)
- (let ((str (read-string "Surround with: "))
- (regionp (use-region-p)))
- (save-excursion
- (if regionp
- (let ((beg (region-beginning))
- (end (region-end)))
- (goto-char end)
- (insert str)
- (goto-char beg)
- (insert str))
- (if (thing-at-point 'word)
- (let ((bounds (bounds-of-thing-at-point 'word)))
- (goto-char (cdr bounds))
- (insert str)
- (goto-char (car bounds))
- (insert str))
- (message "Can't insert around. No word at point and no region selected."))))))
-
-(defun cj/append-to-lines-in-region-or-buffer (str)
- "Append STR to the end of each line in the region or entire buffer."
- (interactive "sEnter string to append: ")
- (let ((start-pos (if (use-region-p)
- (region-beginning)
- (point-min)))
- (end-pos (if (use-region-p)
- (region-end)
- (point-max))))
- (save-excursion
- (goto-char start-pos)
- (while (< (point) end-pos)
- (move-end-of-line 1)
- (insert str)
- (forward-line 1)))))
-
-(defun cj/prepend-to-lines-in-region-or-buffer (str)
- "Prepend STR to the beginning of each line in the region or entire buffer."
- (interactive "sEnter string to prepend: ")
- (let ((start-pos (if (use-region-p)
- (region-beginning)
- (point-min)))
- (end-pos (if (use-region-p)
- (region-end)
- (point-max))))
- (save-excursion
- (goto-char start-pos)
- (while (< (point) end-pos)
- (beginning-of-line 1)
- (insert str)
- (forward-line 1)))))
-
-;; Surround, append, prepend prefix keymap
-(define-prefix-command 'cj/surround-map nil
- "Keymap for surrounding, appending, and prepending operations.")
-(define-key cj/custom-keymap "s" 'cj/surround-map)
-(define-key cj/surround-map "s" 'cj/surround-word-or-region)
-(define-key cj/surround-map "a" 'cj/append-to-lines-in-region-or-buffer)
-(define-key cj/surround-map "p" 'cj/prepend-to-lines-in-region-or-buffer)
-
-;;; -------------------------- Date And Time Insertion --------------------------
-
-(defvar readable-date-time-format "%A, %B %d, %Y at %I:%M:%S %p %Z "
- "Format string used by `cj/insert-readable-date-time'.
-
-See `format-time-string' for possible replacements.")
-
-(defun cj/insert-readable-date-time ()
- "Insert the current date and time into the current buffer.
-
-Use `readable-date-time-format' for formatting."
- (interactive)
- (insert (format-time-string readable-date-time-format (current-time))))
-
-(defvar sortable-date-time-format "%Y-%m-%d %a @ %H:%M:%S %z "
- "Format string used by `cj/insert-sortable-date-time'.
-
-See `format-time-string' for possible replacements.")
-
-(defun cj/insert-sortable-date-time ()
- "Insert the current date and time into the current buffer.
-
-Use `sortable-date-time-format' for formatting."
- (interactive)
- (insert (format-time-string sortable-date-time-format (current-time))))
-
-(defvar sortable-time-format "%I:%M:%S %p %Z "
- "Format string used by `cj/insert-sortable-time'.
-
-See `format-time-string' for possible replacements.")
-
-(defun cj/insert-sortable-time ()
- "Insert the current time into the current buffer.
-
-Use `sortable-time-format' for formatting."
- (interactive)
- (insert (format-time-string sortable-time-format (current-time))))
-
-(defvar readable-time-format "%-I:%M %p "
- "Format string used by `cj/insert-readable-time'.
-
-See `format-time-string' for possible replacements.")
-
-(defun cj/insert-readable-time ()
- "Insert the current time into the current buffer.
-
-Use `readable-time-format' for formatting."
- (interactive)
- (insert (format-time-string readable-time-format (current-time))))
-
-(defvar sortable-date-format "%Y-%m-%d %a"
- "Format string used by `cj/insert-sortable-date'.
-
-See `format-time-string' for possible replacements.")
-
-(defun cj/insert-sortable-date ()
- "Insert the current date into the current buffer.
-
-Use `sortable-date-format' for formatting."
- (interactive)
- (insert (format-time-string sortable-date-format (current-time))))
-
-(defvar readable-date-format "%A, %B %d, %Y"
- "Format string used by `cj/insert-readable-date'.
-
-See `format-time-string' for possible replacements.")
-
-(defun cj/insert-readable-date ()
- "Insert the current date into the current buffer.
-
-Use `readable-date-format' for formatting."
- (interactive)
- (insert (format-time-string readable-date-format (current-time))))
-
-;; Date/time insertion prefix and keymap
-(define-prefix-command 'cj/datetime-map nil
- "Keymap for inserting various date/time formats.")
-(define-key cj/custom-keymap "d" 'cj/datetime-map)
-(define-key cj/datetime-map "r" 'cj/insert-readable-date-time)
-(define-key cj/datetime-map "s" 'cj/insert-sortable-date-time)
-(define-key cj/datetime-map "t" 'cj/insert-sortable-time)
-(define-key cj/datetime-map "T" 'cj/insert-readable-time)
-(define-key cj/datetime-map "d" 'cj/insert-sortable-date)
-(define-key cj/datetime-map "D" 'cj/insert-readable-date)
-
-;;; ----------------------- Line And Paragraph Operations -----------------------
-
-
-(defun cj/join-line-or-region ()
- "Join lines in the active region or join the current line with the previous one."
- (interactive)
- (if (use-region-p)
- (let ((beg (region-beginning))
- (end (copy-marker (region-end))))
- (goto-char beg)
- (while (< (point) end)
- (join-line 1))
- (goto-char end)
- (newline))
- ;; No region - only join if there's a previous line
- (when (> (line-number-at-pos) 1)
- (join-line))
- (newline)))
-
-(defun cj/join-paragraph ()
- "Join all lines in the current paragraph using `cj/join-line-or-region'."
- (interactive)
- (er/mark-paragraph) ;; from package expand region
- (cj/join-line-or-region (region-beginning)(region-end))
- (forward-line))
-
-(defun cj/duplicate-line-or-region (&optional comment)
- "Duplicate the current line or active region below.
-
-Comment the duplicated text when optional COMMENT is non-nil."
- (interactive "P")
- (let* ((b (if (region-active-p) (region-beginning) (line-beginning-position)))
- (e (if (region-active-p) (region-end) (line-end-position)))
- (lines (split-string (buffer-substring-no-properties b e) "\n")))
- (save-excursion
- (goto-char e)
- (dolist (line lines)
- (open-line 1)
- (forward-line 1)
- (insert line)
- ;; If the COMMENT prefix argument is non-nil, comment the inserted text
- (when comment
- (comment-region (line-beginning-position) (line-end-position)))))))
-
-(defun cj/remove-duplicate-lines-region-or-buffer ()
- "Remove duplicate lines in the region or buffer, keeping the first occurrence.
-
-Operate on the active region when one exists; otherwise operate on the whole buffer."
- (interactive)
- (let ((start (if (use-region-p) (region-beginning) (point-min)))
- (end (if (use-region-p) (region-end) (point-max))))
- (save-excursion
- (let ((end-marker (copy-marker end)))
- (while
- (progn
- (goto-char start)
- (re-search-forward "^\\(.*\\)\n\\(\\(.*\n\\)*\\)\\1\n" end-marker t))
- (replace-match "\\1\n\\2"))))))
-
-
-(defun cj/remove-lines-containing (text)
- "Remove all lines containing TEXT.
-
-If region is active, operate only on the region, otherwise on entire buffer.
-The operation is undoable."
- (interactive "sRemove lines containing: ")
- (save-excursion
- (save-restriction
- (let ((region-active (use-region-p))
- (count 0))
- (when region-active
- (narrow-to-region (region-beginning) (region-end)))
- (goto-char (point-min))
- ;; Count lines before deletion
- (while (re-search-forward (regexp-quote text) nil t)
- (setq count (1+ count))
- (beginning-of-line)
- (forward-line))
- ;; Go back and delete
- (goto-char (point-min))
- (delete-matching-lines (regexp-quote text))
- ;; Report what was done
- (message "Removed %d line%s containing '%s' from %s"
- count
- (if (= count 1) "" "s")
- text
- (if region-active "region" "buffer"))))))
-
-(defun cj/underscore-line ()
- "Underline the current line by inserting a row of characters below it.
-
-If the line is empty or contains only whitespace, abort with a message."
- (interactive)
- (let ((line (buffer-substring-no-properties
- (line-beginning-position)
- (line-end-position))))
- (if (string-match-p "^[[:space:]]*$" line)
- (message "Line empty or only whitespace. Aborting.")
- (let* ((char (read-char "Enter character for underlining: "))
- (len (save-excursion
- (goto-char (line-end-position))
- (current-column))))
- (save-excursion
- (end-of-line)
- (insert "\n" (make-string len char)))))))
-
-
-;; Line & paragraph operations prefix and keymap
-(define-prefix-command 'cj/line-and-paragraph-map nil
- "Keymap for line and paragraph manipulation.")
-(define-key cj/custom-keymap "l" 'cj/line-and-paragraph-map)
-(define-key cj/line-and-paragraph-map "j" 'cj/join-line-or-region)
-(define-key cj/line-and-paragraph-map "J" 'cj/join-paragraph)
-(define-key cj/line-and-paragraph-map "d" 'cj/duplicate-line-or-region)
-(define-key cj/line-and-paragraph-map "R" 'cj/remove-duplicate-lines-region-or-buffer)
-(define-key cj/line-and-paragraph-map "r" 'cj/remove-lines-containing)
-(define-key cj/line-and-paragraph-map "u" 'cj/underscore-line)
-
-;;; ---------------------------------- Comments ---------------------------------
-
-(defun cj/comment-reformat ()
- "Reformat commented text into a single paragraph."
- (interactive)
-
- (if mark-active
- (let ((beg (region-beginning))
- (end (copy-marker (region-end)))
- (orig-fill-column fill-column))
- (uncomment-region beg end)
- (setq fill-column (- fill-column 3))
- (cj/join-line-or-region beg end)
- (comment-region beg end)
- (setq fill-column orig-fill-column )))
- ;; if no region
- (message "No region was selected. Select the comment lines to reformat."))
-
-(defun cj/comment-centered (&optional comment-char)
- "Insert comment text centered around the COMMENT-CHAR character.
-
-Default to the hash character when COMMENT-CHAR is nil.
-
-Use the lesser of `fill-column' or 80 to calculate the comment length.
-Begin and end the line with the appropriate comment symbols for the current mode."
- (interactive)
- (if (not (char-or-string-p comment-char))
- (setq comment-char "#"))
- (let* ((comment (capitalize (string-trim (read-from-minibuffer "Comment: "))))
- (fill-column (min fill-column 80))
- (comment-length (length comment))
- (comment-start-length (length comment-start))
- (comment-end-length (length comment-end))
- (current-column-pos (current-column))
- (space-on-each-side (/ (- fill-column
- current-column-pos
- comment-length
- (length comment-start)
- (length comment-end)
- ;; Single space on each side of comment
- (if (> comment-length 0) 2 0)
- ;; Single space after comment syntax sting
- 1)
- 2)))
- (if (< space-on-each-side 2)
- (message "Comment string is too big to fit in one line")
- (progn
- (insert comment-start)
- (when (equal comment-start ";") ; emacs-lisp line comments are ;;
- (insert comment-start)) ; so insert comment-char again
- (insert " ")
- (dotimes (_ space-on-each-side) (insert comment-char))
- (when (> comment-length 0) (insert " "))
- (insert comment)
- (when (> comment-length 0) (insert " "))
- (dotimes (_ (if (= (% comment-length 2) 0)
- (- space-on-each-side 1)
- space-on-each-side))
- (insert comment-char))
- ;; Only insert trailing space and comment-end if comment-end is not empty
- (when (not (string-empty-p comment-end))
- (insert " ")
- (insert comment-end))))))
-
-(defun cj/comment-box ()
- "Insert a comment box around text that the user inputs.
-
-The box extends to the fill column, centers the text, and uses the current
-mode's comment syntax at both the beginning and end of each line. The box
-respects the current indentation level and avoids trailing whitespace."
- (interactive)
- (let* ((comment-char (if (equal comment-start ";") ";;"
- (string-trim comment-start)))
- (comment-end-char (if (string-empty-p comment-end)
- comment-char
- (string-trim comment-end)))
- (line-char (if (equal comment-char ";;") "-" "#"))
- (comment (capitalize (string-trim (read-from-minibuffer "Comment: "))))
- (comment-length (length comment))
- (current-column-pos (current-column))
- (max-width (min fill-column 80))
- ;; Calculate available width between comment markers
- (available-width (- max-width
- current-column-pos
- (length comment-char)
- (length comment-end-char)))
- ;; Inner width is the width without the spaces after comment start and before comment end
- (inner-width (- available-width 2))
- ;; Calculate padding for each side of the centered text
- (padding-each-side (max 1 (/ (- inner-width comment-length) 2)))
- ;; Adjust for odd-length comments
- (right-padding (if (= (% (- inner-width comment-length) 2) 0)
- padding-each-side
- (1+ padding-each-side))))
-
- ;; Check if we have enough space
- (if (< inner-width (+ comment-length 4)) ; minimum sensible width
- (message "Comment string is too big to fit in one line")
- (progn
- ;; Top line - fill entirely with line characters except for space after comment start
- (insert comment-char)
- (insert " ")
- (insert (make-string inner-width (string-to-char line-char)))
- (insert " ")
- (insert comment-end-char)
- (newline)
-
- ;; Add indentation on the new line to match current column
- (dotimes (_ current-column-pos) (insert " "))
-
- ;; Middle line with centered text
- (insert comment-char)
- (insert " ")
- ;; Left padding
- (dotimes (_ padding-each-side) (insert " "))
- ;; The comment text
- (insert comment)
- ;; Right padding
- (dotimes (_ right-padding) (insert " "))
- (insert " ")
- (insert comment-end-char)
- (newline)
-
- ;; Add indentation on the new line to match current column
- (dotimes (_ current-column-pos) (insert " "))
-
- ;; Bottom line - same as top line
- (insert comment-char)
- (insert " ")
- (dotimes (_ inner-width) (insert line-char))
- (insert " ")
- (insert comment-end-char)
- (newline)))))
-
-(defun cj/comment-hyphen()
- "Insert a centered comment with '-' (hyphens) on each side."
- (interactive)
- (cj/comment-centered "-"))
-
-(defun cj/delete-buffer-comments ()
- "Delete all comments within the current buffer."
- (interactive)
- (goto-char (point-min))
- (let (kill-ring)
- (comment-kill (count-lines (point-min) (point-max)))))
-
-;; Comment styles & removal prefix and keymap
-(define-prefix-command 'cj/comment-map nil
- "Keymap for comment styling and removal.")
-(define-key cj/custom-keymap "C" 'cj/comment-map)
-(define-key cj/comment-map "r" 'cj/comment-reformat)
-(define-key cj/comment-map "c" 'cj/comment-centered)
-(define-key cj/comment-map "-" 'cj/comment-hyphen)
-(define-key cj/comment-map "b" 'cj/comment-box)
-(define-key cj/comment-map "D" 'cj/delete-buffer-comments)
-
-;;; ---------------------- Ordering And Sorting Operations ----------------------
-
-(defun cj/arrayify (start end quote)
- "Convert lines between START and END into quoted, comma-separated strings.
-
-START and END identify the active region.
-QUOTE specifies the quotation characters to surround each element."
- (interactive "r\nMQuotation character to use for array element: ")
- (let ((insertion
- (mapconcat
- (lambda (x) (format "%s%s%s" quote x quote))
- (split-string (buffer-substring start end)) ", ")))
- (delete-region start end)
- (insert insertion)))
-
-(defun cj/unarrayify (start end)
- "Convert quoted, comma-separated strings between START and END into separate lines.
-
-START and END identify the active region."
- (interactive "r")
- (let ((insertion
- (mapconcat
- (lambda (x) (replace-regexp-in-string "[\"']" "" x))
- (split-string (buffer-substring start end) ", ") "\n")))
- (delete-region start end)
- (insert insertion)))
-
-(defun cj/alphabetize-region ()
- "Alphabetize words in the active region and replace the original text.
-
-Produce a comma-separated list as the result."
- (interactive)
- (unless (use-region-p)
- (user-error "No region selected"))
- (let ((start (region-beginning))
- (end (region-end))
- (string (buffer-substring-no-properties (region-beginning) (region-end))))
- (delete-region start end)
- (goto-char start)
- (insert
- (mapconcat #'identity
- (sort (split-string string "[[:space:],]+" t)
- #'string-lessp)
- ", "))))
-
-(defun cj/comma-separated-text-to-lines ()
- "Break up comma-separated text in the active region so each item is on its own line."
- (interactive)
- (if (not (region-active-p))
- (error "No region selected"))
-
- (let ((beg (region-beginning))
- (end (region-end))
- (text (buffer-substring-no-properties (region-beginning) (region-end))))
- (with-temp-buffer
- (insert text)
- (goto-char (point-min))
- (while (search-forward "," nil t)
- (replace-match "\n" nil t))
- (delete-trailing-whitespace)
- (setq text (buffer-string)))
-
- (delete-region beg end)
- (goto-char beg)
- (insert text)))
-
-
-;; Ordering & sorting prefix and keymap
-(define-prefix-command 'cj/ordering-map nil
- "Keymap for text ordering and sorting operations.")
-(define-key cj/custom-keymap "o" 'cj/ordering-map)
-(define-key cj/ordering-map "a" 'cj/arrayify)
-(define-key cj/ordering-map "u" 'cj/unarrayify)
-(define-key cj/ordering-map "A" 'cj/alphabetize-region)
-(define-key cj/ordering-map "l" 'cj/comma-separated-text-to-lines)
-
-;;; --------------------------- Case Change Operations --------------------------
-
-(defun cj/title-case-region ()
- "Capitalize the region in title case format.
-
-Title case is a capitalization convention where major words
-are capitalized,and most minor words are lowercase. Nouns,
-verbs (including linking verbs), adjectives, adverbs,pronouns,
-and all words of four letters or more are considered major words.
-Short (i.e., three letters or fewer) conjunctions, short prepositions,
-and all articles are considered minor words."
- (interactive)
- (let ((beg nil)
- (end nil)
- (prev-word-end nil)
- ;; Allow capitals for skip characters after this, so:
- ;; Warning: An Example
- ;; Capitalizes the `An'.
- (chars-skip-reset '(?: ?! ??))
- ;; Don't capitalize characters directly after these. e.g.
- ;; "Foo-bar" or "Foo\bar" or "Foo's".
-
- (chars-separator '(?\\ ?- ?' ?.))
-
- (word-chars "[:alnum:]")
- (word-skip
- (list "a" "an" "and" "as" "at" "but" "by"
- "for" "if" "in" "is" "nor" "of"
- "on" "or" "so" "the" "to" "yet"))
- (is-first t))
- (cond
- ((region-active-p)
- (setq beg (region-beginning))
- (setq end (region-end)))
- (t
- (setq beg (line-beginning-position))
- (setq end (line-end-position))))
- (save-excursion
- ;; work on uppercased text (e.g., headlines) by downcasing first
- (downcase-region beg end)
- (goto-char beg)
-
- (while (< (point) end)
- (setq prev-word-end (point))
- (skip-chars-forward (concat "^" word-chars) end)
- (let ((word-end
- (save-excursion
- (skip-chars-forward word-chars end)
- (point))))
-
- (unless (memq (char-before (point)) chars-separator)
- (let* ((c-orig (char-to-string (char-after (point))))
- (c-up (capitalize c-orig)))
- (unless (string-equal c-orig c-up)
- (let ((word (buffer-substring-no-properties (point) word-end)))
- (when
- (or
- ;; Always allow capitalization.
- is-first
- ;; If it's not a skip word, allow.
- (not (member word word-skip))
- ;; Check the beginning of the previous word doesn't reset first.
- (save-excursion
- (and
- (not (zerop
- (skip-chars-backward "[:blank:]" prev-word-end)))
- (memq (char-before (point)) chars-skip-reset))))
- (delete-region (point) (1+ (point)))
- (insert c-up))))))
- (goto-char word-end)
- (setq is-first nil))))))
-
-;; replace the capitalize-region keybinding to call title-case
-(global-set-key [remap capitalize-region] 'cj/title-case-region)
-
-(defun cj/upcase-dwim ()
- "Upcase the active region, or upcase the symbol at point if no region."
- (interactive)
- (if (use-region-p)
- (upcase-region (region-beginning) (region-end))
- (let ((bounds (bounds-of-thing-at-point 'symbol)))
- (if bounds
- (upcase-region (car bounds) (cdr bounds))
- (user-error "No symbol at point")))))
-
-(defun cj/downcase-dwim ()
- "Downcase the active region, or downcase the symbol at point if no region."
- (interactive)
- (if (use-region-p)
- (downcase-region (region-beginning) (region-end))
- (let ((bounds (bounds-of-thing-at-point 'symbol)))
- (if bounds
- (downcase-region (car bounds) (cdr bounds))
- (user-error "No symbol at point")))))
-
-;; Case-change operations prefix and keymap
-(define-prefix-command 'cj/case-map nil
- "Keymap for case-change operations.")
-(define-key cj/custom-keymap "c" 'cj/case-map)
-(define-key cj/case-map "t" 'cj/title-case-region)
-(define-key cj/case-map "u" 'cj/upcase-dwim)
-(define-key cj/case-map "l" 'cj/downcase-dwim) ;; for "lower" case
-
-(provide 'custom-functions)
-;;; custom-functions.el ends here.
diff --git a/modules/custom-line-paragraph.el b/modules/custom-line-paragraph.el
new file mode 100644
index 00000000..334aa8d2
--- /dev/null
+++ b/modules/custom-line-paragraph.el
@@ -0,0 +1,127 @@
+;;; custom-line-paragraph.el --- -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(use-package expand-region
+ :demand t) ;; used w/in join paragraph
+
+(defun cj/join-line-or-region ()
+ "Join lines in the active region or join the current line with the previous one."
+ (interactive)
+ (if (use-region-p)
+ (let ((beg (region-beginning))
+ (end (copy-marker (region-end))))
+ (goto-char beg)
+ (while (< (point) end)
+ (join-line 1))
+ (goto-char end)
+ (newline))
+ ;; No region - only join if there's a previous line
+ (when (> (line-number-at-pos) 1)
+ (join-line))
+ (newline)))
+
+(defun cj/join-paragraph ()
+ "Join all lines in the current paragraph using `cj/join-line-or-region'."
+ (interactive)
+ (er/mark-paragraph) ;; from package expand region
+ (cj/join-line-or-region (region-beginning)(region-end))
+ (forward-line))
+
+(defun cj/duplicate-line-or-region (&optional comment)
+ "Duplicate the current line or active region below.
+
+Comment the duplicated text when optional COMMENT is non-nil."
+ (interactive "P")
+ (let* ((b (if (region-active-p) (region-beginning) (line-beginning-position)))
+ (e (if (region-active-p) (region-end) (line-end-position)))
+ (lines (split-string (buffer-substring-no-properties b e) "\n")))
+ (save-excursion
+ (goto-char e)
+ (dolist (line lines)
+ (open-line 1)
+ (forward-line 1)
+ (insert line)
+ ;; If the COMMENT prefix argument is non-nil, comment the inserted text
+ (when comment
+ (comment-region (line-beginning-position) (line-end-position)))))))
+
+(defun cj/remove-duplicate-lines-region-or-buffer ()
+ "Remove duplicate lines in the region or buffer, keeping the first occurrence.
+
+Operate on the active region when one exists; otherwise operate on the whole buffer."
+ (interactive)
+ (let ((start (if (use-region-p) (region-beginning) (point-min)))
+ (end (if (use-region-p) (region-end) (point-max))))
+ (save-excursion
+ (let ((end-marker (copy-marker end)))
+ (while
+ (progn
+ (goto-char start)
+ (re-search-forward "^\\(.*\\)\n\\(\\(.*\n\\)*\\)\\1\n" end-marker t))
+ (replace-match "\\1\n\\2"))))))
+
+
+(defun cj/remove-lines-containing (text)
+ "Remove all lines containing TEXT.
+
+If region is active, operate only on the region, otherwise on entire buffer.
+The operation is undoable."
+ (interactive "sRemove lines containing: ")
+ (save-excursion
+ (save-restriction
+ (let ((region-active (use-region-p))
+ (count 0))
+ (when region-active
+ (narrow-to-region (region-beginning) (region-end)))
+ (goto-char (point-min))
+ ;; Count lines before deletion
+ (while (re-search-forward (regexp-quote text) nil t)
+ (setq count (1+ count))
+ (beginning-of-line)
+ (forward-line))
+ ;; Go back and delete
+ (goto-char (point-min))
+ (delete-matching-lines (regexp-quote text))
+ ;; Report what was done
+ (message "Removed %d line%s containing '%s' from %s"
+ count
+ (if (= count 1) "" "s")
+ text
+ (if region-active "region" "buffer"))))))
+
+(defun cj/underscore-line ()
+ "Underline the current line by inserting a row of characters below it.
+
+If the line is empty or contains only whitespace, abort with a message."
+ (interactive)
+ (let ((line (buffer-substring-no-properties
+ (line-beginning-position)
+ (line-end-position))))
+ (if (string-match-p "^[[:space:]]*$" line)
+ (message "Line empty or only whitespace. Aborting.")
+ (let* ((char (read-char "Enter character for underlining: "))
+ (len (save-excursion
+ (goto-char (line-end-position))
+ (current-column))))
+ (save-excursion
+ (end-of-line)
+ (insert "\n" (make-string len char)))))))
+
+
+;; Line & paragraph operations prefix and keymap
+(define-prefix-command 'cj/line-and-paragraph-map nil
+ "Keymap for line and paragraph manipulation.")
+(define-key cj/custom-keymap "l" 'cj/line-and-paragraph-map)
+(define-key cj/line-and-paragraph-map "j" 'cj/join-line-or-region)
+(define-key cj/line-and-paragraph-map "J" 'cj/join-paragraph)
+(define-key cj/line-and-paragraph-map "d" 'cj/duplicate-line-or-region)
+(define-key cj/line-and-paragraph-map "R" 'cj/remove-duplicate-lines-region-or-buffer)
+(define-key cj/line-and-paragraph-map "r" 'cj/remove-lines-containing)
+(define-key cj/line-and-paragraph-map "u" 'cj/underscore-line)
+
+(provide 'custom-line-paragraph)
+;;; custom-line-paragraph.el ends here.
diff --git a/modules/custom-misc.el b/modules/custom-misc.el
new file mode 100644
index 00000000..4d6a359a
--- /dev/null
+++ b/modules/custom-misc.el
@@ -0,0 +1,90 @@
+;;; custom-misc.el --- -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+
+(defun cj/jump-to-matching-paren ()
+ "Jump to the matching parenthesis when point is on one.
+
+Signal a message when point is not on a parenthesis."
+ (interactive)
+ (cond ((looking-at "\\s\(\\|\\s\{\\|\\s\[")
+ (forward-list))
+ ((looking-back "\\s\)\\|\\s\}\\|\\s\\]")
+ (backward-list))
+ (t (message "Cursor doesn't follow parenthesis, so there's no match."))))
+
+(defun cj/format-region-or-buffer ()
+ "Reformat the region or the entire buffer.
+Replaces tabs with spaces, deletes trailing whitespace, and reindents the region."
+ (interactive)
+ (let ((start-pos (if (use-region-p) (region-beginning) (point-min)))
+ (end-pos (if (use-region-p) (region-end) (point-max))))
+ (save-excursion
+ (save-restriction
+ (narrow-to-region start-pos end-pos)
+ (untabify (point-min) (point-max)))
+ (indent-region (point-min) (point-max))
+ (delete-trailing-whitespace))))
+
+(defun cj/count-words-buffer-or-region ()
+ "Count the number of words in the buffer or region.
+Display the result in the minibuffer and *Messages* buffer."
+ (interactive)
+ (let ((begin (point-min))
+ (end (point-max))
+ (area_type "the buffer"))
+ (when mark-active
+ (setq begin (region-beginning)
+ end (region-end)
+ area_type "the region"))
+ (message (format "There are %d words in %s." (count-words begin end) area_type))))
+
+(defun cj/replace-fraction-glyphs (start end)
+ "Replace common fraction glyphs between START and END.
+Operate on the buffer or region designated by START and END.
+Replace the text representations with glyphs when called with a \[universal-argument] prefix."
+ (interactive (if (use-region-p)
+ (list (region-beginning) (region-end))
+ (list (point-min) (point-max))))
+ (let ((replacements (if current-prefix-arg
+ '(("1/4" . "¼")
+ ("1/2" . "½")
+ ("3/4" . "¾")
+ ("1/3" . "⅓")
+ ("2/3" . "⅔"))
+ '(("¼" . "1/4")
+ ("½" . "1/2")
+ ("¾" . "3/4")
+ ("⅓" . "1/3")
+ ("⅔" . "2/3")))))
+ (save-excursion
+ (dolist (r replacements)
+ (goto-char start)
+ (while (search-forward (car r) end t)
+ (replace-match (cdr r)))))))
+
+(defun cj/align-regexp-with-spaces (orig-fun &rest args)
+ "Call ORIG-FUN with ARGS while temporarily disabling tabs for alignment.
+
+This advice ensures `align-regexp' uses spaces by binding `indent-tabs-mode' to nil."
+ (let ((indent-tabs-mode nil))
+ (apply orig-fun args)))
+
+(advice-remove 'align-regexp #'align-regexp-with-spaces) ; in case this is reloaded
+(advice-add 'align-regexp :around #'cj/align-regexp-with-spaces)
+
+
+(define-key cj/custom-keymap ")" 'cj/jump-to-matching-paren)
+(define-key cj/custom-keymap "f" 'cj/format-region-or-buffer)
+(define-key cj/custom-keymap "W" 'cj/count-words-buffer-or-region)
+(define-key cj/custom-keymap "/" 'cj/replace-fraction-glyphs)
+(define-key cj/custom-keymap "A" 'align-regexp)
+(define-key cj/custom-keymap "B" 'toggle-debug-on-error)
+(define-key cj/custom-keymap "|" 'display-fill-column-indicator-mode)
+
+(provide 'custom-misc)
+;;; custom-misc.el ends here.
diff --git a/modules/custom-ordering.el b/modules/custom-ordering.el
new file mode 100644
index 00000000..020e9254
--- /dev/null
+++ b/modules/custom-ordering.el
@@ -0,0 +1,84 @@
+;;; custom-ordering.el --- -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+
+(defun cj/arrayify (start end quote)
+ "Convert lines between START and END into quoted, comma-separated strings.
+
+START and END identify the active region.
+QUOTE specifies the quotation characters to surround each element."
+ (interactive "r\nMQuotation character to use for array element: ")
+ (let ((insertion
+ (mapconcat
+ (lambda (x) (format "%s%s%s" quote x quote))
+ (split-string (buffer-substring start end)) ", ")))
+ (delete-region start end)
+ (insert insertion)))
+
+(defun cj/unarrayify (start end)
+ "Convert quoted, comma-separated strings between START and END into separate lines.
+
+START and END identify the active region."
+ (interactive "r")
+ (let ((insertion
+ (mapconcat
+ (lambda (x) (replace-regexp-in-string "[\"']" "" x))
+ (split-string (buffer-substring start end) ", ") "\n")))
+ (delete-region start end)
+ (insert insertion)))
+
+(defun cj/alphabetize-region ()
+ "Alphabetize words in the active region and replace the original text.
+
+Produce a comma-separated list as the result."
+ (interactive)
+ (unless (use-region-p)
+ (user-error "No region selected"))
+ (let ((start (region-beginning))
+ (end (region-end))
+ (string (buffer-substring-no-properties (region-beginning) (region-end))))
+ (delete-region start end)
+ (goto-char start)
+ (insert
+ (mapconcat #'identity
+ (sort (split-string string "[[:space:],]+" t)
+ #'string-lessp)
+ ", "))))
+
+(defun cj/comma-separated-text-to-lines ()
+ "Break up comma-separated text in the active region so each item is on its own line."
+ (interactive)
+ (if (not (region-active-p))
+ (error "No region selected"))
+
+ (let ((beg (region-beginning))
+ (end (region-end))
+ (text (buffer-substring-no-properties (region-beginning) (region-end))))
+ (with-temp-buffer
+ (insert text)
+ (goto-char (point-min))
+ (while (search-forward "," nil t)
+ (replace-match "\n" nil t))
+ (delete-trailing-whitespace)
+ (setq text (buffer-string)))
+
+ (delete-region beg end)
+ (goto-char beg)
+ (insert text)))
+
+
+;; Ordering & sorting prefix and keymap
+(define-prefix-command 'cj/ordering-map nil
+ "Keymap for text ordering and sorting operations.")
+(define-key cj/custom-keymap "o" 'cj/ordering-map)
+(define-key cj/ordering-map "a" 'cj/arrayify)
+(define-key cj/ordering-map "u" 'cj/unarrayify)
+(define-key cj/ordering-map "A" 'cj/alphabetize-region)
+(define-key cj/ordering-map "l" 'cj/comma-separated-text-to-lines)
+
+(provide 'custom-ordering)
+;;; custom-ordering.el ends here.
diff --git a/modules/custom-text-enclose.el b/modules/custom-text-enclose.el
new file mode 100644
index 00000000..7c3f2135
--- /dev/null
+++ b/modules/custom-text-enclose.el
@@ -0,0 +1,70 @@
+;;; custom-text-enclose.el --- -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+(defun cj/surround-word-or-region ()
+ "Surround the word at point or active region with a string read from the minibuffer."
+ (interactive)
+ (let ((str (read-string "Surround with: "))
+ (regionp (use-region-p)))
+ (save-excursion
+ (if regionp
+ (let ((beg (region-beginning))
+ (end (region-end)))
+ (goto-char end)
+ (insert str)
+ (goto-char beg)
+ (insert str))
+ (if (thing-at-point 'word)
+ (let ((bounds (bounds-of-thing-at-point 'word)))
+ (goto-char (cdr bounds))
+ (insert str)
+ (goto-char (car bounds))
+ (insert str))
+ (message "Can't insert around. No word at point and no region selected."))))))
+
+(defun cj/append-to-lines-in-region-or-buffer (str)
+ "Append STR to the end of each line in the region or entire buffer."
+ (interactive "sEnter string to append: ")
+ (let ((start-pos (if (use-region-p)
+ (region-beginning)
+ (point-min)))
+ (end-pos (if (use-region-p)
+ (region-end)
+ (point-max))))
+ (save-excursion
+ (goto-char start-pos)
+ (while (< (point) end-pos)
+ (move-end-of-line 1)
+ (insert str)
+ (forward-line 1)))))
+
+(defun cj/prepend-to-lines-in-region-or-buffer (str)
+ "Prepend STR to the beginning of each line in the region or entire buffer."
+ (interactive "sEnter string to prepend: ")
+ (let ((start-pos (if (use-region-p)
+ (region-beginning)
+ (point-min)))
+ (end-pos (if (use-region-p)
+ (region-end)
+ (point-max))))
+ (save-excursion
+ (goto-char start-pos)
+ (while (< (point) end-pos)
+ (beginning-of-line 1)
+ (insert str)
+ (forward-line 1)))))
+
+;; Surround, append, prepend prefix keymap
+(define-prefix-command 'cj/enclose-map nil
+ "Keymap for enclosing text: surrounding, appending, and prepending.")
+(define-key cj/custom-keymap "s" 'cj/enclose-map)
+(define-key cj/enclose-map "s" 'cj/surround-word-or-region)
+(define-key cj/enclose-map "a" 'cj/append-to-lines-in-region-or-buffer)
+(define-key cj/enclose-map "p" 'cj/prepend-to-lines-in-region-or-buffer)
+
+(provide 'custom-text-enclose)
+;;; custom-text-enclose.el ends here.
diff --git a/modules/custom-whitespace.el b/modules/custom-whitespace.el
new file mode 100644
index 00000000..48be080e
--- /dev/null
+++ b/modules/custom-whitespace.el
@@ -0,0 +1,106 @@
+;;; custom-whitespace.el --- -*- coding: utf-8; lexical-binding: t; -*-
+
+;;; Commentary:
+;;
+
+;;; Code:
+
+
+;;; ---------------------- Whitespace Operations And Keymap ---------------------
+
+(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.
+- Otherwise, operate on the current line."
+ (interactive)
+ (let ((start (cond (current-prefix-arg (point-min))
+ ((use-region-p) (region-beginning))
+ (t (line-beginning-position))))
+ (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 ""))))))
+
+(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."
+ (interactive)
+ (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))))))
+
+(defun cj/delete-blank-lines-region-or-buffer (start end)
+ "Delete blank lines between START and END.
+
+Treat blank lines as lines that contain nothing or only whitespace.
+Operate on the active region when one exists.
+Prompt before operating on the whole buffer when no region is selected.
+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)))
+ ;; Return nil (Emacs conventions). Point is already restored.
+ nil)
+
+(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.")))
+
+
+;; Whitespace operations prefix and keymap
+(define-prefix-command 'cj/whitespace-map nil
+ "Keymap for whitespace operations.")
+(define-key cj/custom-keymap "w" 'cj/whitespace-map)
+(define-key cj/whitespace-map "r" 'cj/remove-leading-trailing-whitespace)
+(define-key cj/whitespace-map "c" 'cj/collapse-whitespace-line-or-region)
+(define-key cj/whitespace-map "l" 'cj/delete-blank-lines-region-or-buffer)
+(define-key cj/whitespace-map "-" 'cj/hyphenate-whitespace-in-region)
+
+(provide 'custom-whitespace)
+;;; custom-whitespace.el ends here.
diff --git a/todo.org b/todo.org
index 3894cbb1..d4596469 100644
--- a/todo.org
+++ b/todo.org
@@ -992,6 +992,11 @@ Org-roam and its database sync run at startup. Load Org-roam only when Org is ac
** TODO [#C] Git Timemachine Litters Empty Buffers :bug:
Don't choose a revision and you'll see a blank buffer that needs to be killed
** TODO [#C] Org Keyword Discolored Until Reapplying Theme :bug:
+** TODO [#B] Selecting enter on flac file in dirvish causes error
+cond: external-open: unsupported host environment
+** TODO [#B] m4a files in playlist play in external mplayer not in EMMS
+Nothing in Messages buffer
+
* Emacs Config Next Release: 0.9
** DOING [#A] Jumper Package :enhancement:
*** Specification