diff options
Diffstat (limited to 'modules')
| -rw-r--r-- | modules/config-utilities.el | 1 | ||||
| -rw-r--r-- | modules/custom-comments.el | 666 | ||||
| -rw-r--r-- | modules/custom-file-buffer.el | 79 | ||||
| -rw-r--r-- | modules/custom-line-paragraph.el | 14 | ||||
| -rw-r--r-- | modules/custom-whitespace.el | 133 |
5 files changed, 712 insertions, 181 deletions
diff --git a/modules/config-utilities.el b/modules/config-utilities.el index d1538256..ea92f19a 100644 --- a/modules/config-utilities.el +++ b/modules/config-utilities.el @@ -108,7 +108,6 @@ Recompile natively when supported, otherwise fall back to byte compilation." (find-lisp-find-files user-emacs-directory "")) (message "Done. Compiled files removed under %s" user-emacs-directory)) (keymap-set cj/debug-config-keymap "c d" 'cj/delete-emacs-home-compiled-files) -(keymap-set cj/debug-config-keymap "c d" 'cj/delete-emacs-home-compiled-files) (defun cj/compile-this-elisp-buffer () "Compile the current .el: prefer native (.eln), else .elc. Message if neither." diff --git a/modules/custom-comments.el b/modules/custom-comments.el index 101ba092..b4e51b2c 100644 --- a/modules/custom-comments.el +++ b/modules/custom-comments.el @@ -13,11 +13,50 @@ ;; These utilities help create consistent, well-formatted code comments and section headers. ;; Bound to keymap prefix: C-; C ;; +;; Comment Style Patterns: +;; +;; inline-border: +;; ========== inline-border ========== +;; +;; simple-divider: +;; ==================================== +;; simple-divider +;; ==================================== +;; +;; padded-divider: +;; ==================================== +;; padded-divider +;; ==================================== +;; +;; box: +;; ************************************ +;; * box * +;; ************************************ +;; +;; heavy-box: +;; ************************************ +;; * * +;; * heavy-box * +;; * * +;; ************************************ +;; +;; unicode-box: +;; ┌──────────────────────────────────┐ +;; │ unicode-box │ +;; └──────────────────────────────────┘ +;; +;; block-banner: +;; /************************************ +;; * block-banner +;; ************************************/ +;; ;;; Code: (eval-when-compile (defvar cj/custom-keymap)) ;; cj/custom-keymap defined in keybindings.el (autoload 'cj/join-line-or-region "custom-line-paragraph" nil t) +;; ======================== Comment Manipulation Functions ===================== + ;; --------------------------- Delete Buffer Comments -------------------------- (defun cj/delete-buffer-comments () @@ -38,146 +77,545 @@ (orig-fill-column fill-column)) (uncomment-region beg end) (setq fill-column (- fill-column 3)) - (cj/join-line-or-region beg end) + (cj/join-line-or-region) (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 ----------------------------- +;; ======================== Comment Generation Functions ======================= + +;; ----------------------------- Inline Border --------------------------------- + +(defun cj/--comment-inline-border (cmt-start cmt-end decoration-char text length) + "Internal implementation: Generate single-line centered comment with decoration. +CMT-START and CMT-END are the comment syntax strings. +DECORATION-CHAR is the character to use for borders (string). +TEXT is the comment text (will be centered). +LENGTH is the total width of the line." + (let* ((current-column-pos (current-column)) + (text-length (length text)) + (comment-start-len (+ (length cmt-start) + (if (equal cmt-start ";") 1 0))) ; doubled semicolon + ;; Calculate available space for decoration + text + spaces + (available-width (- length current-column-pos + comment-start-len + (if (string-empty-p cmt-end) 0 (1+ (length cmt-end))) + 1)) ; space after comment-start + ;; Space for decoration on each side (excluding text and its surrounding spaces) + (space-on-each-side (/ (- available-width + text-length + (if (> text-length 0) 2 0)) ; spaces around text + 2)) + (min-space 2)) + ;; Validate we have enough space + (when (< space-on-each-side min-space) + (error "Length %d is too small for text '%s' (need at least %d more chars)" + length text (- min-space space-on-each-side))) + ;; Generate the line + (insert cmt-start) + (when (equal cmt-start ";") + (insert cmt-start)) + (insert " ") + ;; Left decoration + (dotimes (_ space-on-each-side) + (insert decoration-char)) + ;; Text with spaces + (when (> text-length 0) + (insert " " text " ")) + ;; Right decoration (handle odd-length text) + (dotimes (_ (if (= (% text-length 2) 0) + (- space-on-each-side 1) + space-on-each-side)) + (insert decoration-char)) + ;; Comment end + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline))) + +(defun cj/comment-inline-border (&optional decoration-char) + "Insert single-line comment with TEXT centered around DECORATION-CHAR borders. +DECORATION-CHAR defaults to \"#\" if not provided. +Uses the lesser of `fill-column\\=' or 80 for line length." + (interactive) + (let* ((comment-start (if (and (boundp 'comment-start) comment-start) + comment-start + (read-string "Comment start character(s): "))) + (comment-end (if (and (boundp 'comment-end) comment-end) + comment-end + "")) + (decoration-char (or decoration-char "#")) + (text (capitalize (string-trim (read-from-minibuffer "Comment: ")))) + (length (min fill-column 80))) + (cj/--comment-inline-border comment-start comment-end decoration-char text length))) + +;; ---------------------------- Simple Divider --------------------------------- + +(defun cj/--comment-simple-divider (cmt-start cmt-end decoration-char text length) + "Internal implementation: Generate a simple divider comment. +CMT-START and CMT-END are the comment syntax strings. +DECORATION-CHAR is the character to use for the divider lines. +TEXT is the comment text. +LENGTH is the total width of each line." + (let* ((current-column-pos (current-column)) + (min-length (+ current-column-pos + (length cmt-start) + (if (equal cmt-start ";") 1 0) ; doubled semicolon + 1 ; space after comment-start + 3 ; minimum decoration chars + (if (string-empty-p cmt-end) 0 (1+ (length cmt-end)))))) + (when (< length min-length) + (error "Length %d is too small to generate comment (minimum %d)" length min-length)) + (let* ((available-width (- length current-column-pos + (length cmt-start) + (if (string-empty-p cmt-end) 0 (1+ (length cmt-end))))) + (line (make-string available-width (string-to-char decoration-char)))) + ;; Top line + (insert cmt-start) + (when (equal cmt-start ";") (insert cmt-start)) + (insert " ") + (insert line) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline) + + ;; Text line + (dotimes (_ current-column-pos) (insert " ")) + (insert cmt-start) + (when (equal cmt-start ";") (insert cmt-start)) + (insert " " text) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline) + + ;; Bottom line + (dotimes (_ current-column-pos) (insert " ")) + (insert cmt-start) + (when (equal cmt-start ";") (insert cmt-start)) + (insert " ") + (insert line) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline)))) + +(defun cj/comment-simple-divider () + "Insert a simple divider comment banner. +Prompts for decoration character, text, and length option." + (interactive) + (let* ((comment-start (if (and (boundp 'comment-start) comment-start) + comment-start + (read-string "Comment start character(s): "))) + (comment-end (if (and (boundp 'comment-end) comment-end) + comment-end + "")) + (decoration-char (read-string "Decoration character (default =): " nil nil "=")) + (text (read-string "Comment text: ")) + (length-option (completing-read "Length: " + '("fill-column" "half-column" "match-text") + nil t nil nil "fill-column")) + (length (cond + ((string= length-option "fill-column") fill-column) + ((string= length-option "half-column") (/ fill-column 2)) + ((string= length-option "match-text") + (+ (length comment-start) + (if (equal comment-start ";") 1 0) + 1 ; space after comment-start + (length text) + (if (string-empty-p comment-end) 0 (1+ (length comment-end)))))))) + (cj/--comment-simple-divider comment-start comment-end decoration-char text length))) + +;; ---------------------------- Padded Divider --------------------------------- + +(defun cj/--comment-padded-divider (cmt-start cmt-end decoration-char text length padding) + "Internal implementation: Generate a padded divider comment. +CMT-START and CMT-END are the comment syntax strings. +DECORATION-CHAR is the character to use for the divider lines. +TEXT is the comment text. +LENGTH is the total width of each line. +PADDING is the number of spaces before the text." + (when (< padding 0) + (error "Padding %d cannot be negative" padding)) + (let* ((current-column-pos (current-column)) + (min-length (+ current-column-pos + (length cmt-start) + (if (equal cmt-start ";") 1 0) ; doubled semicolon + 1 ; space after comment-start + 3 ; minimum decoration chars + (if (string-empty-p cmt-end) 0 (1+ (length cmt-end)))))) + (when (< length min-length) + (error "Length %d is too small to generate comment (minimum %d)" length min-length)) + (let* ((available-width (- length current-column-pos + (length cmt-start) + (if (string-empty-p cmt-end) 0 (1+ (length cmt-end))))) + (line (make-string available-width (string-to-char decoration-char)))) + ;; Top line + (insert cmt-start) + (when (equal cmt-start ";") (insert cmt-start)) + (insert " ") + (insert line) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline) + + ;; Text line with padding + (dotimes (_ current-column-pos) (insert " ")) + (insert cmt-start) + (when (equal cmt-start ";") (insert cmt-start)) + (insert " ") + (dotimes (_ padding) (insert " ")) + (insert text) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline) + + ;; Bottom line + (dotimes (_ current-column-pos) (insert " ")) + (insert cmt-start) + (when (equal cmt-start ";") (insert cmt-start)) + (insert " ") + (insert line) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline)))) -(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 line with the appropriate comment symbols for the current mode." +(defun cj/comment-padded-divider () + "Insert a padded divider comment banner. +Prompts for decoration character, text, padding, and length option." (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)))))) + (let* ((comment-start (if (and (boundp 'comment-start) comment-start) + comment-start + (read-string "Comment start character(s): "))) + (comment-end (if (and (boundp 'comment-end) comment-end) + comment-end + "")) + (decoration-char (read-string "Decoration character (default =): " nil nil "=")) + (text (read-string "Comment text: ")) + (padding (string-to-number (read-string "Padding spaces (default 2): " nil nil "2"))) + (length-option (completing-read "Length: " + '("fill-column" "half-column" "match-text") + nil t nil nil "fill-column")) + (length (cond + ((string= length-option "fill-column") fill-column) + ((string= length-option "half-column") (/ fill-column 2)) + ((string= length-option "match-text") + (+ (length comment-start) + (if (equal comment-start ";") 1 0) + 1 ; space after comment-start + padding + (length text) + (if (string-empty-p comment-end) 0 (1+ (length comment-end)))))))) + (cj/--comment-padded-divider comment-start comment-end decoration-char text length padding))) ;; -------------------------------- Comment Box -------------------------------- +(defun cj/--comment-box (cmt-start cmt-end decoration-char text length) + "Internal implementation: Generate a 3-line box comment with centered text. +CMT-START and CMT-END are the comment syntax strings. +DECORATION-CHAR is the character to use for borders. +TEXT is the comment text (centered). +LENGTH is the total width of each line." + (let* ((current-column-pos (current-column)) + (comment-char (if (equal cmt-start ";") ";;" cmt-start)) + (comment-end-char (if (string-empty-p cmt-end) comment-char cmt-end)) + (min-length (+ current-column-pos + (length comment-char) + 2 ; spaces around content + (length comment-end-char) + 6))) ; minimum: 3 border chars + text space + 3 border chars + (when (< length min-length) + (error "Length %d is too small to generate comment (minimum %d)" length min-length)) + (let* ((available-width (- length current-column-pos + (length comment-char) + (length comment-end-char) + 2)) ; spaces around content + (border-line (make-string available-width (string-to-char decoration-char))) + (text-length (length text)) + ;; For text line: need space for decoration + space + text + space + decoration + (text-available (- available-width 4)) ; 2 for side decorations, 2 for spaces + (padding-each-side (max 1 (/ (- text-available text-length) 2))) + (right-padding (if (= (% (- text-available text-length) 2) 0) + padding-each-side + (1+ padding-each-side)))) + ;; Top border + (insert comment-char " " border-line " " comment-end-char) + (newline) + + ;; Centered text line with side borders + (dotimes (_ current-column-pos) (insert " ")) + (insert comment-char " " decoration-char " ") + (dotimes (_ padding-each-side) (insert " ")) + (insert text) + (dotimes (_ right-padding) (insert " ")) + (insert " " decoration-char " " comment-end-char) + (newline) + + ;; Bottom border + (dotimes (_ current-column-pos) (insert " ")) + (insert comment-char " " border-line " " comment-end-char) + (newline)))) + (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." + "Insert a 3-line comment box with centered text. +Prompts for decoration character, text, and uses `fill-column' for length." + (interactive) + (let* ((comment-start (if (and (boundp 'comment-start) comment-start) + comment-start + (read-string "Comment start character(s): "))) + (comment-end (if (and (boundp 'comment-end) comment-end) + comment-end + "")) + (decoration-char (read-string "Decoration character (default -): " nil nil "-")) + (text (capitalize (string-trim (read-from-minibuffer "Comment: ")))) + (length (min fill-column 80))) + (cj/--comment-box comment-start comment-end decoration-char text length))) + +;; ------------------------------ Heavy Box ------------------------------------ + +(defun cj/--comment-heavy-box (cmt-start cmt-end decoration-char text length) + "Internal implementation: Generate a heavy box comment with blank lines. +CMT-START and CMT-END are the comment syntax strings. +DECORATION-CHAR is the character to use for borders. +TEXT is the comment text (centered). +LENGTH is the total width of each line." + (let* ((current-column-pos (current-column)) + (comment-char (if (equal cmt-start ";") ";;" cmt-start)) + (comment-end-char (if (string-empty-p cmt-end) comment-char cmt-end)) + (available-width (- length current-column-pos + (length comment-char) + (length comment-end-char) + 2)) ; spaces around content + (border-line (make-string available-width (string-to-char decoration-char))) + (text-length (length text)) + (padding-each-side (max 1 (/ (- available-width text-length) 2))) + (right-padding (if (= (% (- available-width text-length) 2) 0) + padding-each-side + (1+ padding-each-side)))) + ;; Top border + (insert comment-char " " border-line " " comment-end-char) + (newline) + + ;; Empty line with side borders + (dotimes (_ current-column-pos) (insert " ")) + (insert decoration-char) + (dotimes (_ available-width) (insert " ")) + (insert " " decoration-char) + (newline) + + ;; Centered text line + (dotimes (_ current-column-pos) (insert " ")) + (insert decoration-char " ") + (dotimes (_ padding-each-side) (insert " ")) + (insert text) + (dotimes (_ right-padding) (insert " ")) + (insert " " decoration-char) + (newline) + + ;; Empty line with side borders + (dotimes (_ current-column-pos) (insert " ")) + (insert decoration-char) + (dotimes (_ available-width) (insert " ")) + (insert " " decoration-char) + (newline) + + ;; Bottom border + (dotimes (_ current-column-pos) (insert " ")) + (insert comment-char " " border-line " " comment-end-char) + (newline))) + +(defun cj/comment-heavy-box () + "Insert a heavy box comment with blank lines around centered text. +Prompts for decoration character, text, and length option." + (interactive) + (let* ((comment-start (if (and (boundp 'comment-start) comment-start) + comment-start + (read-string "Comment start character(s): "))) + (comment-end (if (and (boundp 'comment-end) comment-end) + comment-end + "")) + (decoration-char (read-string "Decoration character (default *): " nil nil "*")) + (text (read-string "Comment text: ")) + (length-option (completing-read "Length: " + '("fill-column" "half-column" "padded-text") + nil t nil nil "fill-column")) + (length (cond + ((string= length-option "fill-column") fill-column) + ((string= length-option "half-column") (/ fill-column 2)) + ((string= length-option "padded-text") + (+ (current-column) + (length (if (equal comment-start ";") ";;" comment-start)) + 2 ; decoration char + space + 4 ; minimum padding (2 on each side) + (length text) + (if (string-empty-p comment-end) + 1 ; just the side decoration + (1+ (length comment-end)))))))) + (cj/--comment-heavy-box comment-start comment-end decoration-char text length))) + +;; ---------------------------- Unicode Box ------------------------------------ + +(defun cj/--comment-unicode-box (cmt-start cmt-end text length box-style) + "Internal implementation: Generate a unicode box comment. +CMT-START and CMT-END are the comment syntax strings. +TEXT is the comment text. +LENGTH is the total width of each line. +BOX-STYLE is either \\='single or \\='double for line style." + (let* ((current-column-pos (current-column)) + (comment-char (if (equal cmt-start ";") ";;" cmt-start)) + (min-length (+ current-column-pos + (length comment-char) + 1 ; space after comment-char + 5 ; minimum: corner + corner + padding + (if (string-empty-p cmt-end) 0 (1+ (length cmt-end)))))) + (when (< length min-length) + (error "Length %d is too small to generate comment (minimum %d)" length min-length)) + (let* ((available-width (- length current-column-pos + (length comment-char) + (if (string-empty-p cmt-end) 0 (1+ (length cmt-end))) + 3)) ; box corners and padding + (top-left (if (eq box-style 'double) "╔" "┌")) + (top-right (if (eq box-style 'double) "╗" "┐")) + (bottom-left (if (eq box-style 'double) "╚" "└")) + (bottom-right (if (eq box-style 'double) "╝" "┘")) + (horizontal (if (eq box-style 'double) "═" "─")) + (vertical (if (eq box-style 'double) "║" "│")) + (text-padding (- available-width (length text) 2))) + ;; Top line + (insert comment-char " " top-left) + (dotimes (_ available-width) (insert horizontal)) + (insert top-right) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline) + + ;; Text line + (dotimes (_ current-column-pos) (insert " ")) + (insert comment-char " " vertical " " text) + (dotimes (_ text-padding) (insert " ")) + (insert " " vertical) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline) + + ;; Bottom line + (dotimes (_ current-column-pos) (insert " ")) + (insert comment-char " " bottom-left) + (dotimes (_ available-width) (insert horizontal)) + (insert bottom-right) + (when (not (string-empty-p cmt-end)) + (insert " " cmt-end)) + (newline)))) + +(defun cj/comment-unicode-box () + "Insert a unicode box comment. +Prompts for text, box style, and length option." + (interactive) + (let* ((comment-start (if (and (boundp 'comment-start) comment-start) + comment-start + (read-string "Comment start character(s): "))) + (comment-end (if (and (boundp 'comment-end) comment-end) + comment-end + "")) + (text (read-string "Comment text: ")) + (box-style (intern (completing-read "Box style: " + '("single" "double") + nil t nil nil "single"))) + (length-option (completing-read "Length: " + '("fill-column" "half-column" "padded-text") + nil t nil nil "fill-column")) + (length (cond + ((string= length-option "fill-column") fill-column) + ((string= length-option "half-column") (/ fill-column 2)) + ((string= length-option "padded-text") + (+ (current-column) + (length (if (equal comment-start ";") ";;" comment-start)) + 5 ; box chars and spaces + (length text) + (if (string-empty-p comment-end) 0 (1+ (length comment-end)))))))) + (cj/--comment-unicode-box comment-start comment-end text length box-style))) + +;; ---------------------------- Block Banner ----------------------------------- + +(defun cj/--comment-block-banner (cmt-start cmt-end decoration-char text length) + "Internal implementation: Generate a block banner comment (JSDoc/Doxygen style). +CMT-START should be the block comment start (e.g., '/*'). +CMT-END should be the block comment end (e.g., '*/'). +DECORATION-CHAR is the character to use for the border line. +TEXT is the comment text. +LENGTH is the total width of each line." + (let* ((current-column-pos (current-column)) + (min-length (+ current-column-pos + (length cmt-start) + 3))) ; minimum: 3 decoration chars + (when (< length min-length) + (error "Length %d is too small to generate comment (minimum %d)" length min-length)) + (let* ((available-width (- length current-column-pos (length cmt-start) 1)) + (border-line (make-string available-width (string-to-char decoration-char)))) + ;; Top line + (insert cmt-start border-line) + (newline) + + ;; Text line + (dotimes (_ current-column-pos) (insert " ")) + (insert " " decoration-char " " text) + (newline) + + ;; Bottom line + (dotimes (_ current-column-pos) (insert " ")) + (insert " ") + (dotimes (_ (- available-width (length cmt-end))) + (insert decoration-char)) + (insert cmt-end) + (newline)))) + +(defun cj/comment-block-banner () + "Insert a block banner comment (JSDoc/Doxygen style). +Prompts for decoration character, text, and length option." (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))))) + (let* ((comment-start (if (and (boundp 'comment-start) comment-start + (string-match-p "/\\*" comment-start)) + comment-start + (read-string "Block comment start (e.g., /*): " nil nil "/*"))) + (comment-end (if (and (boundp 'comment-end) comment-end + (not (string-empty-p comment-end))) + comment-end + (read-string "Block comment end (e.g., */): " nil nil "*/"))) + (decoration-char (read-string "Decoration character (default *): " nil nil "*")) + (text (read-string "Comment text: ")) + (length-option (completing-read "Length: " + '("fill-column" "half-column" "match-text") + nil t nil nil "fill-column")) + (length (cond + ((string= length-option "fill-column") fill-column) + ((string= length-option "half-column") (/ fill-column 2)) + ((string= length-option "match-text") + (+ (current-column) + (length comment-start) + 2 ; space + decoration + (length text)))))) + (cj/--comment-block-banner comment-start comment-end decoration-char text length))) ;; ------------------------------- Comment Hyphen ------------------------------ (defun cj/comment-hyphen() "Insert a centered comment with `-' (hyphens) on each side. -Leverages cj/comment-centered." +Leverages cj/comment-inline-border." (interactive) - (cj/comment-centered "-")) + (cj/comment-inline-border "-")) ;; ------------------------------- Comment Keymap ------------------------------ (defvar-keymap cj/comment-map :doc "Keymap for code comment operations" "r" #'cj/comment-reformat - "c" #'cj/comment-centered + "d" #'cj/delete-buffer-comments + "c" #'cj/comment-inline-border "-" #'cj/comment-hyphen + "s" #'cj/comment-simple-divider + "p" #'cj/comment-padded-divider "b" #'cj/comment-box - "D" #'cj/delete-buffer-comments) + "h" #'cj/comment-heavy-box + "u" #'cj/comment-unicode-box + "n" #'cj/comment-block-banner) (keymap-set cj/custom-keymap "C" cj/comment-map) (with-eval-after-load 'which-key diff --git a/modules/custom-file-buffer.el b/modules/custom-file-buffer.el index 6ed19d73..e0224a32 100644 --- a/modules/custom-file-buffer.el +++ b/modules/custom-file-buffer.el @@ -81,35 +81,82 @@ Sends directly to the system spooler with no header." ;; ------------------------- Buffer And File Operations ------------------------ -(defun cj/move-buffer-and-file (dir) - "Move both current buffer and the file it visits to DIR." - (interactive "DMove buffer and file (to new directory): ") +(defun cj/--move-buffer-and-file (dir &optional ok-if-exists) + "Internal implementation: Move buffer and file to DIR. +If OK-IF-EXISTS is nil and target exists, signal an error. +If OK-IF-EXISTS is non-nil, overwrite existing file. +Returns t on success, nil if buffer not visiting a file." (let* ((name (buffer-name)) (filename (buffer-file-name)) + (dir (expand-file-name dir)) (dir - (if (string-match dir "\\(?:/\\|\\\\)$") + (if (string-match "[/\\\\]$" dir) (substring dir 0 -1) dir)) (newname (concat dir "/" name))) (if (not filename) - (message "Buffer '%s' is not visiting a file!" name) - (progn (copy-file filename newname 1) (delete-file filename) - (set-visited-file-name newname) (set-buffer-modified-p nil) t)))) + (progn + (message "Buffer '%s' is not visiting a file!" name) + nil) + (progn (copy-file filename newname ok-if-exists) + (delete-file filename) + (set-visited-file-name newname) + (set-buffer-modified-p nil) + t)))) + +(defun cj/move-buffer-and-file (dir) + "Move both current buffer and the file it visits to DIR. +When called interactively, prompts for confirmation if target file exists." + (interactive (list (read-directory-name "Move buffer and file (to new directory): "))) + (let* ((target (expand-file-name (buffer-name) (expand-file-name dir)))) + (condition-case _ + (cj/--move-buffer-and-file dir nil) + (file-already-exists + (if (yes-or-no-p (format "File %s exists; overwrite? " target)) + (cj/--move-buffer-and-file dir t) + (message "File not moved")))))) + +(defun cj/--rename-buffer-and-file (new-name &optional ok-if-exists) + "Internal implementation: Rename buffer and file to NEW-NAME. +NEW-NAME can be just a basename or a full path to move to different directory. +If OK-IF-EXISTS is nil and target exists, signal an error. +If OK-IF-EXISTS is non-nil, overwrite existing file. +Returns t on success, nil if buffer not visiting a file." + (let ((filename (buffer-file-name)) + (new-basename (file-name-nondirectory new-name))) + (if (not filename) + (progn + (message "Buffer '%s' is not visiting a file!" (buffer-name)) + nil) + ;; Check if a buffer with the new name already exists + (when (and (get-buffer new-basename) + (not (eq (get-buffer new-basename) (current-buffer)))) + (error "A buffer named '%s' already exists" new-basename)) + ;; Expand new-name to absolute path (preserves directory if just basename) + (let ((expanded-name (expand-file-name new-name + (file-name-directory filename)))) + (rename-file filename expanded-name ok-if-exists) + (rename-buffer new-basename) + (set-visited-file-name expanded-name) + (set-buffer-modified-p nil) + t)))) (defun cj/rename-buffer-and-file (new-name) - "Rename both current buffer and the file it visits to NEW-NAME." + "Rename both current buffer and the file it visits to NEW-NAME. +When called interactively, prompts for confirmation if target file exists." (interactive (list (if (not (buffer-file-name)) (user-error "Buffer '%s' is not visiting a file!" (buffer-name)) (read-string "Rename buffer and file (to new name): " (file-name-nondirectory (buffer-file-name)))))) - (let ((filename (buffer-file-name))) - (if (get-buffer new-name) - (message "A buffer named '%s' already exists!" new-name) - (progn - (rename-file filename new-name 1) - (rename-buffer new-name) - (set-visited-file-name new-name) - (set-buffer-modified-p nil))))) + (condition-case err + (cj/--rename-buffer-and-file new-name nil) + (file-already-exists + (if (yes-or-no-p (format "File %s exists; overwrite? " new-name)) + (cj/--rename-buffer-and-file new-name t) + (message "File not renamed"))) + (error + ;; Handle buffer-already-exists and other errors + (message "%s" (error-message-string err))))) (defun cj/delete-buffer-and-file () "Kill the current buffer and delete the file it visits." diff --git a/modules/custom-line-paragraph.el b/modules/custom-line-paragraph.el index e66b024d..7f0baef9 100644 --- a/modules/custom-line-paragraph.el +++ b/modules/custom-line-paragraph.el @@ -1,14 +1,13 @@ ;;; custom-line-paragraph.el --- -*- coding: utf-8; lexical-binding: t; -*- - +;; Author: Craig Jennings <c@cjennings.net> +;; ;;; Commentary: ;; -;; This module provides line and paragraph manipulation utilities. -;; These utilities enhance text editing and formatting capabilities. +;; This module provides the following line and paragraph manipulation utilities: ;; -;; Functions include: ;; - joining lines in a region or the current line with the previous one -;; - joining entire paragraphs into single lines -;; - duplicating lines or regions (with optional commenting) +;; - joining separate lines into a single paragraph +;; - duplicating lines or regions (optional commenting) ;; - removing duplicate lines ;; - removing lines containing specific text ;; - underlining text with a custom character @@ -18,6 +17,9 @@ ;;; Code: +(eval-when-compile (defvar cj/custom-keymap)) ;; defined in keybindings.el +(declare-function er/mark-paragraph "expand-region") ;; for cj/join-paragraph + (defun cj/join-line-or-region () "Join lines in the active region or join the current line with the previous one." (interactive) 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 |
