summaryrefslogtreecommitdiff
path: root/modules/custom-line-paragraph.el
blob: 17b6cdf4804bc7055cb4d95b4e8c45b32fdc2c42 (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
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
;;; custom-line-paragraph.el ---  -*- coding: utf-8; lexical-binding: t; -*-

;;; Commentary:
;;
;; This module provides line and paragraph manipulation utilities.
;; These utilities enhance text editing and formatting capabilities.
;;
;; 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)
;; - removing duplicate lines
;; - removing lines containing specific text
;; - underlining text with a custom character
;;
;; Bound to keymap prefix  C-; l
;;
;;; 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)
  (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)
        (deactivate-mark))
    ;; No region - only join if there's a previous line
    (when (> (line-number-at-pos) 1)
      (join-line))
    (end-of-line)
    (newline)))

(defun cj/join-paragraph ()
  "Join all lines in the current paragraph using `cj/join-line-or-region'."
  (interactive)
  (require 'expand-region)
  (er/mark-paragraph)
  (cj/join-line-or-region)
  (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: ")
  (if (string-empty-p text)
      (message "Empty search string - nothing to remove")
    (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 And Paragraph Keymap -------------------------

(defvar-keymap cj/line-and-paragraph-map
  :doc "Keymap for line and paragraph operations."
  "j" #'cj/join-line-or-region
  "J" #'cj/join-paragraph
  "d" #'cj/duplicate-line-or-region
  "c" (lambda () (interactive) (cj/duplicate-line-or-region t))
  "R" #'cj/remove-duplicate-lines-region-or-buffer
  "r" #'cj/remove-lines-containing
  "u" #'cj/underscore-line)
(keymap-set cj/custom-keymap "l" cj/line-and-paragraph-map)

(with-eval-after-load 'which-key
  (which-key-add-key-based-replacements "C-; l" "line and paragraph menu")
  (which-key-add-key-based-replacements "C-; l c" "duplicate and comment"))

(provide 'custom-line-paragraph)
;;; custom-line-paragraph.el ends here.