-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathselection-highlight-mode.el
221 lines (184 loc) · 8.76 KB
/
selection-highlight-mode.el
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
;;; selection-highlight-mode.el --- Auto highlights matches to the current active region -*- lexical-binding: t; -*-
;; Copyright (C) 2023 Isaac Ballone
;; Author: Isaac Ballone <[email protected]>
;; URL: https://github.com/balloneij/selection-highlight-mode
;; Keywords: matching
;; Version: 0.1.0
;; Package-Requires: ((emacs "25.1"))
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; Provides a global minor mode that automatically highlights exact matches
;; to the active region. Similar to VS Code's Selection Highlight.
;;; Code:
;;; Settings
(defgroup selection-highlight nil
"Auto highlights matches to the current active region."
:group 'convenience)
(defcustom selection-highlight-mode-min-length 2
"The minimum length of selection before highlighting matches."
:group 'selection-highlight
:type 'natnum)
;;; State
(defvar selection-highlight-mode--active-search nil)
(defvar selection-highlight-mode--window-states nil)
;;; Faces
(defface selection-highlight-mode-match-face
'((t :inherit region))
"Face for highlighting matches to the active region.
See `selection-highlight-mode-alternate-match-face' for the other face."
:group 'selection-highlight)
(defface selection-highlight-mode-alternate-match-face
'((t :inherit region :weight bold))
"Alternate face for highlighting adjacent matches to the active region.
See `selection-highlight-mode-match-face' for the other face."
:group 'selection-highlight)
;;; Helpers
(defmacro selection-highlight-mode--without-case-fold-search (&rest body)
"Temporarily disable `case-fold-search' for the current buffer and execute BODY."
(declare (indent 0))
`(let ((old-case-fold-search case-fold-search))
(setq case-fold-search nil)
(unwind-protect
(progn ,@body)
(setq case-fold-search old-case-fold-search))))
(defun selection-highlight-mode--face-at-point (pos)
"If the face at POS is a match, return the face used."
(let (face-found)
(dolist (overlay (overlays-at pos))
(when (and (not face-found)
(eq (overlay-get overlay 'category) 'selection-highlight-mode-match))
(setq face-found (overlay-get overlay 'face))))
face-found))
(defun selection-highlight-mode--zebra-stripe-face (beg adjacent?)
"Determine the face to use at point BEG.
Alternate the face used when ADJACENT?."
(if (and adjacent?
(eq 'selection-highlight-mode-match-face
(selection-highlight-mode--face-at-point (1- beg))))
'selection-highlight-mode-alternate-match-face
'selection-highlight-mode-match-face))
(defun selection-highlight-mode--create-overlay (beg end adjacent?)
"Create an overlay from BEG to END.
ADJACENT? must be true if the last match ends where this overlay begins."
(let ((overlay (make-overlay beg end))
(face (selection-highlight-mode--zebra-stripe-face beg adjacent?)))
(overlay-put overlay 'category 'selection-highlight-mode-match)
(overlay-put overlay 'face face)
overlay))
(defun selection-highlight-mode--highlight-next-match (str bound)
"Find and highlight next match of STR, limited by BOUND.
Returns overlay or nil."
(let* ((start-pos (point))
(end (search-forward str bound t 1)))
(when end
(let* ((beg (- end (length str)))
(adjacent? (= beg start-pos)))
(if (and (region-active-p) (= beg (region-beginning)))
;; recur
(selection-highlight-mode--highlight-next-match str bound)
(selection-highlight-mode--create-overlay beg end adjacent?))))))
(defun selection-highlight-mode--highlight-matches (str start end)
"Highlight all matches of STR from START to END.
Returns the new overlays"
(selection-highlight-mode--without-case-fold-search
(save-excursion
(goto-char start)
(let (overlays
(searching? t))
(while searching?
(if-let ((new-overlay (selection-highlight-mode--highlight-next-match str end)))
(push new-overlay overlays)
(setq searching? nil)))
overlays))))
(defun selection-highlight-mode--save-window-state (window start end overlays)
"Save START, END, and OVERLAYS to `selection-highlight-mode--window-states'.
Keys off WINDOW."
(setq selection-highlight-mode--window-states
(assq-delete-all window selection-highlight-mode--window-states))
(push `(,window . ((start . ,start)
(end . ,end)
(overlays . ,overlays)))
selection-highlight-mode--window-states))
(defun selection-highlight-mode--refresh-window-p (window new-start new-end)
"Determine if the NEW-START and NEW-END of WINDOW warrant re-highlighting."
(let* ((state (alist-get window selection-highlight-mode--window-states))
(start (alist-get 'start state))
(end (alist-get 'end state)))
(or (not start) (not end)
(< new-start start)
(> new-end end))))
(defun selection-highlight-mode--window-overlays (window)
"Get overlays used in WINDOW from `selection-highlight-mode--window-states'."
(alist-get 'overlays (alist-get window selection-highlight-mode--window-states)))
(defun selection-highlight-mode--clear-window (window)
"Delete overlays used in WINDOW."
(dolist (overlay (selection-highlight-mode--window-overlays window))
(delete-overlay overlay)))
(defun selection-highlight-mode--clear-all-windows ()
"Delete all overlays in all windows."
(dolist (window (mapcar 'car selection-highlight-mode--window-states))
(selection-highlight-mode--clear-window window))
(setq selection-highlight-mode--window-states '()))
(defun selection-highlight-mode--highlight-window (window str)
"Highlight matches to STR in WINDOW."
(let ((start (window-start window))
(end (window-end window t)))
(when (selection-highlight-mode--refresh-window-p window start end)
(selection-highlight-mode--clear-window window)
(with-current-buffer (window-buffer window)
(let ((matches (selection-highlight-mode--highlight-matches str start end)))
(selection-highlight-mode--save-window-state window start end matches))))))
(defun selection-highlight-mode-get-selection ()
"Get the active selection string or nil."
(when (region-active-p)
(let* ((beg (region-beginning))
(block-cursor? (and (fboundp 'evil-visual-state-p)
(evil-visual-state-p)))
(end (+ (region-end) (if block-cursor? 1 0))))
(when (>= (abs (- end beg)) selection-highlight-mode-min-length)
(buffer-substring beg end)))))
;;; Hooks
;;;###autoload
(defun selection-highlight-mode-window-scroll-hook (window _start)
"Re-highlight WINDOW to account for change to the visible buffer."
(when selection-highlight-mode--active-search
(selection-highlight-mode--highlight-window window selection-highlight-mode--active-search)))
;;;###autoload
(defun selection-highlight-mode-post-command-hook ()
"Highlight all live windows to match the current selection."
(if-let ((selection (selection-highlight-mode-get-selection)))
(when (not (eq selection-highlight-mode--active-search selection))
(setq selection-highlight-mode--active-search selection)
(selection-highlight-mode--clear-all-windows)
(walk-windows (lambda (window)
(selection-highlight-mode--highlight-window window selection))))
(when selection-highlight-mode--active-search
(setq selection-highlight-mode--active-search nil)
(selection-highlight-mode--clear-all-windows))))
;;; Modes
;;;###autoload
(define-minor-mode selection-highlight-mode
"Automatically highlight matches to the current selection in active windows."
:init-value nil
:global t
(if selection-highlight-mode
;; on
(progn
(add-hook 'window-scroll-functions 'selection-highlight-mode-window-scroll-hook)
(add-hook 'post-command-hook 'selection-highlight-mode-post-command-hook))
;; off
(remove-hook 'window-scroll-functions 'selection-highlight-mode-window-scroll-hook)
(remove-hook 'post-command-hook 'selection-highlight-mode-post-command-hook)
(setq selection-highlight-mode--active-search nil)
(selection-highlight-mode--clear-all-windows)))
(provide 'selection-highlight-mode)
;;; selection-highlight-mode.el ends here