summaryrefslogtreecommitdiff
path: root/modules/custom-misc.el
blob: 0c6d7ac84caee609314a0668159dce3bcbb5c877 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
;;; custom-misc.el --- Miscellaneous utility functions  -*- coding: utf-8; lexical-binding: t; -*-

;;; Commentary:
;;
;; This module provides various utility functions for text manipulation,
;; formatting, and navigation.  Features include:
;; - Jump between matching delimiters
;; - Format regions/buffers (untabify, reindent, remove trailing whitespace)
;; - Word counting with region awareness
;; - Fraction glyph conversion (¼ ↔ 1/4)
;; - Force align-regexp to use spaces instead of tabs
;;
;; All functions are bound to the cj/custom-keymap for easy access.
;;
;;; Code:

;; cj/custom-keymap defined in keybindings.el
(eval-when-compile (defvar cj/custom-keymap))

(defun cj/jump-to-matching-paren ()
  "Jump to the matching delimiter if point is on (or just after) one.
If not on a delimiter, show a message. Respects the current syntax table."
  (interactive)
  (let* ((ca (char-after))
		 (cb (char-before))
		 ;; Check if on opening paren
		 (open-p (and ca (eq (char-syntax ca) ?\()))
		 ;; Check if on or just after closing paren
		 (close-p (or (and ca (eq (char-syntax ca) ?\)))
					  (and cb (eq (char-syntax cb) ?\))))))
	(cond
	 ;; Jump forward from opening
	 (open-p
	  (condition-case err
		  (forward-sexp)
		(scan-error
		 (message "No matching delimiter: %s" (error-message-string err)))))
	 ;; Jump backward from closing
	 (close-p
	  (condition-case err
		  (backward-sexp)
		(scan-error
		 (message "No matching delimiter: %s" (error-message-string err)))))
	 ;; Not on delimiter
	 (t
	  (message "Point is not on a delimiter.")))))


(defun cj/format-region-or-buffer ()
  "Reformat the region or the entire buffer.
Replaces tabs with spaces, deletes trailing whitespace, and reindents."
  (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 (point-min) (point-max))))
	(message "Formatted %s" (if (use-region-p) "region" "buffer"))))

(defun cj/switch-to-previous-buffer ()
  "Switch to previously open buffer.
Repeated invocations toggle between the two most recently open buffers."
  (interactive)
  (switch-to-buffer (other-buffer (current-buffer) 1)))

(defun cj/count-words-buffer-or-region ()
  "Count the number of words in the buffer or region.
Display the result in the minibuffer."
  (interactive)
  (let* ((use-region (use-region-p))
		 (begin (if use-region (region-beginning) (point-min)))
		 (end (if use-region (region-end) (point-max)))
		 (area-type (if use-region "the region" "the buffer")))
	(message "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"))))
		(count 0))
	(save-excursion
	  (dolist (r replacements)
		(goto-char start)
		(while (search-forward (car r) end t)
		  (replace-match (cdr r))
		  (setq count (1+ count)))))
	(message "Replaced %d fraction%s" count (if (= count 1) "" "s"))))

(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)))

;; avoid double advice stacking in case the file is reloaded
(advice-remove 'align-regexp #'cj/align-regexp-with-spaces)
(advice-add    'align-regexp :around #'cj/align-regexp-with-spaces)

(keymap-set cj/custom-keymap ")" #'cj/jump-to-matching-paren)
(keymap-set cj/custom-keymap "f" #'cj/format-region-or-buffer)
(keymap-set cj/custom-keymap "W" #'cj/count-words-buffer-or-region)
(keymap-set cj/custom-keymap "/" #'cj/replace-fraction-glyphs)
(keymap-set cj/custom-keymap "A" #'align-regexp)
(keymap-set cj/custom-keymap "SPC" #'cj/switch-to-previous-buffer)
(keymap-set cj/custom-keymap "|" #'display-fill-column-indicator-mode)

(provide 'custom-misc)
;;; custom-misc.el ends here