forked from magnars/expand-region.el
-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
expand-region-core.el
322 lines (264 loc) · 12 KB
/
expand-region-core.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
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
;;; expand-region-core.el --- Increase selected region by semantic units. -*- lexical-binding: t; -*-
;; Copyright (C) 2011-2023 Free Software Foundation, Inc
;; Author: Magnar Sveen <magnars@gmail.com>
;; Keywords: marking region
;; 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:
;; The core functionality of expand-region.
;; See README.md
;;; Code:
(require 'expand-region-custom)
(declare-function er/expand-region "expand-region")
(defvar er/history '()
"A history of start and end points so we can contract after expanding.")
;; history is always local to a single buffer
(make-variable-buffer-local 'er/history)
(defvar er--space-str " \t\n")
(defvar er--blank-list (append er--space-str nil))
(defvar er--show-expansion-message nil)
(defvar er/try-expand-list nil
"A list of functions that are tried when expanding.")
(defvar er/save-mode-excursion nil
"A function to save excursion state when expanding.")
(defsubst er--first-invocation ()
"t if this is the first invocation of `er/expand-region' or `er/contract-region'."
(not (memq last-command '(er/expand-region er/contract-region))))
(defun er--prepare-expanding ()
(when (and (er--first-invocation)
(not (use-region-p)))
(push-mark nil t) ;; one for keeping starting position
(push-mark nil t)) ;; one for replace by set-mark in expansions
(when (not transient-mark-mode)
(setq-local transient-mark-mode (cons 'only transient-mark-mode))))
(defun er--copy-region-to-register ()
(when (and (stringp expand-region-autocopy-register)
(> (length expand-region-autocopy-register) 0))
(set-register (aref expand-region-autocopy-register 0)
(filter-buffer-substring (region-beginning) (region-end)))))
;; save-mark-and-excursion in Emacs 25 works like save-excursion did before
(eval-when-compile
(when (< emacs-major-version 25)
(defmacro save-mark-and-excursion (&rest body)
`(save-excursion ,@body))))
(defmacro er--save-excursion (&rest body)
`(let ((action (lambda ()
(save-mark-and-excursion ,@body))))
(if er/save-mode-excursion
(funcall er/save-mode-excursion action)
(funcall action))))
(defun er--expand-region-1 ()
"Increase selected region by semantic units.
Basically it runs all the mark-functions in `er/try-expand-list'
and chooses the one that increases the size of the region while
moving point or mark as little as possible."
(let* ((p1 (point))
(p2 (if (use-region-p) (mark) (point)))
(start (min p1 p2))
(end (max p1 p2))
(try-list er/try-expand-list)
(best-start (point-min))
(best-end (point-max))
;; (set-mark-default-inactive nil)
)
;; add hook to clear history on buffer changes
(unless er/history
(add-hook 'after-change-functions #'er/clear-history t t))
;; remember the start and end points so we can contract later
;; unless we're already at maximum size
(unless (and (= start best-start)
(= end best-end))
(push (cons p1 p2) er/history))
(when (and expand-region-skip-whitespace
(er--point-is-surrounded-by-white-space)
(= start end))
(skip-chars-forward er--space-str)
(setq start (point)))
(while try-list
(er--save-excursion
(ignore-errors
(funcall (car try-list))
(when (and (region-active-p)
(er--this-expansion-is-better start end best-start best-end))
(setq best-start (point))
(setq best-end (mark))
(when (and er--show-expansion-message (not (minibufferp)))
(message "%S" (car try-list))))))
(setq try-list (cdr try-list)))
(setq deactivate-mark nil)
;; if smart cursor enabled, decide to put it at start or end of region:
(if (and expand-region-smart-cursor
(not (= start best-start)))
(progn (goto-char best-end)
(set-mark best-start))
(goto-char best-start)
(set-mark best-end))
(er--copy-region-to-register)
(when (and (= best-start (point-min))
(= best-end (point-max))) ;; We didn't find anything new, so exit early
'early-exit)))
(defun er--this-expansion-is-better (start end best-start best-end)
"t if the current region is an improvement on previous expansions.
This is provided as a separate function for those that would like
to override the heuristic."
(and
(<= (point) start)
(>= (mark) end)
(> (- (mark) (point)) (- end start))
(or (> (point) best-start)
(and (= (point) best-start)
(< (mark) best-end)))))
;;;###autoload
(defun er/contract-region (arg)
"Contract the selected region to its previous size.
With prefix argument contracts that many times.
If prefix argument is negative calls `er/expand-region'.
If prefix argument is 0 it resets point and mark to their state
before calling `er/expand-region' for the first time."
(interactive "p")
(if (< arg 0)
(er/expand-region (- arg))
(when er/history
;; Be sure to reset them all if called with 0
(when (= arg 0)
(setq arg (length er/history)))
(when (not transient-mark-mode)
(setq-local transient-mark-mode (cons 'only transient-mark-mode)))
;; Advance through the list the desired distance
(while (and (cdr er/history)
(> arg 1))
(setq arg (- arg 1))
(setq er/history (cdr er/history)))
;; Reset point and mark
(let* ((last (pop er/history))
(start (car last))
(end (cdr last)))
(goto-char start)
(set-mark end)
(er--copy-region-to-register)
(when (eq start end)
(deactivate-mark)
(er/clear-history))))))
(defun er/prepare-for-more-expansions-internal (repeat-key-str)
"Return bindings and a message to inform user about them"
(let ((msg (format "Type %s to expand again" repeat-key-str))
(bindings (list (cons repeat-key-str '(er/expand-region 1)))))
;; If contract and expand are on the same binding, ignore contract
(unless (string-equal repeat-key-str expand-region-contract-fast-key)
(setq msg (concat msg (format ", %s to contract" expand-region-contract-fast-key)))
(push (cons expand-region-contract-fast-key '(er/contract-region 1)) bindings))
;; If reset and either expand or contract are on the same binding, ignore reset
(unless (or (string-equal repeat-key-str expand-region-reset-fast-key)
(string-equal expand-region-contract-fast-key expand-region-reset-fast-key))
(setq msg (concat msg (format ", %s to reset" expand-region-reset-fast-key)))
(push (cons expand-region-reset-fast-key '(er/expand-region 0)) bindings))
(cons msg bindings)))
(defun er/prepare-for-more-expansions ()
"Let one expand more by just pressing the last key."
(let* ((repeat-key (event-basic-type last-input-event))
(repeat-key-str (single-key-description repeat-key))
(msg-and-bindings (er/prepare-for-more-expansions-internal repeat-key-str))
(msg (car msg-and-bindings))
(bindings (cdr msg-and-bindings)))
(when repeat-key
(er/set-temporary-overlay-map
(let ((map (make-sparse-keymap)))
(dolist (binding bindings map)
(define-key map (read-kbd-macro (car binding))
`(lambda ()
(interactive)
(setq this-command `,(cadr ',binding))
(or (not expand-region-show-usage-message) (minibufferp) (message "%s" ,msg))
(eval `,(cdr ',binding))))))
t)
(or (not expand-region-show-usage-message) (minibufferp) (message "%s" msg)))))
(defalias 'er/set-temporary-overlay-map
(if (fboundp 'set-temporary-overlay-map) ;Emacs≥24.3
#'set-temporary-overlay-map
;; Backport this function from newer emacs versions
(lambda (map &optional keep-pred)
"Set a new keymap that will only exist for a short period of time.
The new keymap to use must be given in the MAP variable. When to
remove the keymap depends on user input and KEEP-PRED:
- if KEEP-PRED is nil (the default), the keymap disappears as
soon as any key is pressed, whether or not the key is in MAP;
- if KEEP-PRED is t, the keymap disappears as soon as a key *not*
in MAP is pressed;
- otherwise, KEEP-PRED must be a 0-arguments predicate that will
decide if the keymap should be removed (if predicate returns
nil) or kept (otherwise). The predicate will be called after
each key sequence."
(let* ((clearfunsym (make-symbol "clear-temporary-overlay-map"))
(overlaysym (make-symbol "t"))
(alist (list (cons overlaysym map)))
(clearfun
`(lambda ()
(unless ,(cond ((null keep-pred) nil)
((eq t keep-pred)
`(eq this-command
(lookup-key ',map
(this-command-keys-vector))))
(t `(funcall ',keep-pred)))
(remove-hook 'pre-command-hook ',clearfunsym)
(setq emulation-mode-map-alists
(delq ',alist emulation-mode-map-alists))))))
(set overlaysym overlaysym)
(fset clearfunsym clearfun)
(add-hook 'pre-command-hook clearfunsym)
(push alist emulation-mode-map-alists)))))
(advice-add 'keyboard-quit :before #'er--collapse-region-before)
(advice-add 'cua-cancel :before #'er--collapse-region-before)
(defun er--collapse-region-before (&rest _)
;; FIXME: Re-use `er--first-invocation'?
(when (memq last-command '(er/expand-region er/contract-region))
(er/contract-region 0)))
(advice-add 'minibuffer-keyboard-quit
:around #'er--collapse-region-minibuffer-keyboard-quit)
(defun er--collapse-region-minibuffer-keyboard-quit (orig-fun &rest args)
;; FIXME: Re-use `er--first-invocation'?
(if (memq last-command '(er/expand-region er/contract-region))
(er/contract-region 0)
(apply orig-fun args)))
(defun er/clear-history (&rest _)
"Clear the history."
(setq er/history '())
(remove-hook 'after-change-functions #'er/clear-history t))
(defun er--point-is-surrounded-by-white-space ()
(and (or (memq (char-before) er--blank-list)
(eq (point) (point-min)))
(memq (char-after) er--blank-list)))
(defun er/enable-mode-expansions (mode add-fn)
(add-hook (intern (format "%s-hook" mode)) add-fn)
(save-window-excursion ;; FIXME: Why?
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (derived-mode-p mode)
(funcall add-fn))))))
(defun er/enable-minor-mode-expansions (mode add-fn)
(add-hook (intern (format "%s-hook" mode)) add-fn)
(save-window-excursion
(dolist (buffer (buffer-list))
(with-current-buffer buffer
(when (symbol-value mode)
(funcall add-fn))))))
;; Some more performant version of `looking-back'
(defun er/looking-back-on-line (regexp)
"Version of `looking-back' that only checks current line."
(looking-back regexp (line-beginning-position)))
(defun er/looking-back-exact (s)
"Version of `looking-back' that only looks for exact matches, no regexp."
(string= s (buffer-substring (- (point) (length s))
(point))))
(defun er/looking-back-max (regexp count)
"Version of `looking-back' that only check COUNT chars back."
(looking-back regexp (max 1 (- (point) count))))
(provide 'expand-region-core)
;;; expand-region-core.el ends here