]> code.delx.au - gnu-emacs-elpa/blob - easy-kill.el
Require cl-lib
[gnu-emacs-elpa] / easy-kill.el
1 ;;; easy-kill.el --- kill things easily -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2013-2014 Leo Liu
4
5 ;; Author: Leo Liu <sdl.web@gmail.com>
6 ;; Version: 0.9.1
7 ;; Package-Requires: ((emacs "24") (cl-lib "0.5"))
8 ;; Keywords: convenience
9 ;; Created: 2013-08-12
10 ;; URL: https://github.com/leoliu/easy-kill
11
12 ;; This program is free software; you can redistribute it and/or modify
13 ;; it under the terms of the GNU General Public License as published by
14 ;; the Free Software Foundation, either version 3 of the License, or
15 ;; (at your option) any later version.
16
17 ;; This program is distributed in the hope that it will be useful,
18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 ;; GNU General Public License for more details.
21
22 ;; You should have received a copy of the GNU General Public License
23 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
24
25 ;;; Commentary:
26
27 ;; `easy-kill' aims to be a drop-in replacement for `kill-ring-save'.
28 ;;
29 ;; To use: (global-set-key [remap kill-ring-save] 'easy-kill)
30
31 ;; `easy-mark' is similar to `easy-kill' but marks the region
32 ;; immediately. It can be a handy replacement for `mark-sexp' allowing
33 ;; `+'/`-' to do list-wise expanding/shrinking.
34 ;;
35 ;; To use: (global-set-key [remap mark-sexp] 'easy-mark)
36
37 ;; Please send bug reports or feature requests to:
38 ;; https://github.com/leoliu/easy-kill/issues
39
40 ;;; Code:
41
42 (require 'cl-lib)
43 (require 'thingatpt)
44
45 (eval-and-compile
46 (cond
47 ((fboundp 'set-transient-map) nil)
48 ((fboundp 'set-temporary-overlay-map) ; new in 24.3
49 (defalias 'set-transient-map 'set-temporary-overlay-map))
50 (t
51 (defun set-transient-map (map &optional keep-pred)
52 (let* ((clearfunsym (make-symbol "clear-temporary-overlay-map"))
53 (overlaysym (make-symbol "t"))
54 (alist (list (cons overlaysym map)))
55 (clearfun
56 `(lambda ()
57 (unless ,(cond ((null keep-pred) nil)
58 ((eq t keep-pred)
59 `(eq this-command
60 (lookup-key ',map
61 (this-command-keys-vector))))
62 (t `(funcall ',keep-pred)))
63 (set ',overlaysym nil) ;Just in case.
64 (remove-hook 'pre-command-hook ',clearfunsym)
65 (setq emulation-mode-map-alists
66 (delq ',alist emulation-mode-map-alists))))))
67 (set overlaysym overlaysym)
68 (fset clearfunsym clearfun)
69 (add-hook 'pre-command-hook clearfunsym)
70 (push alist emulation-mode-map-alists))))))
71
72 (defcustom easy-kill-alist
73 '((?w . word)
74 (?s . sexp)
75 (?l . list)
76 (?f . filename)
77 (?d . defun)
78 (?e . line)
79 (?b . buffer-file-name)
80 (?D . defun-name))
81 "A list of (CHAR . THING).
82 CHAR is used immediately following `easy-kill' to select THING."
83 :type '(repeat (cons character symbol))
84 :group 'killing)
85
86 (defcustom easy-kill-try-things '(url email line)
87 "A list of things for `easy-kill' to try."
88 :type '(repeat symbol)
89 :group 'killing)
90
91 (defcustom easy-mark-try-things '(url email sexp)
92 "A list of things for `easy-mark' to try."
93 :type '(repeat symbol)
94 :group 'killing)
95
96 (defface easy-kill-selection '((t (:inherit secondary-selection)))
97 "Faced used to highlight kill candidate."
98 :group 'killing)
99
100 (defface easy-kill-origin '((t (:inverse-video t :inherit error)))
101 "Faced used to highlight the origin."
102 :group 'killing)
103
104 (defvar easy-kill-base-map
105 (let ((map (make-sparse-keymap)))
106 (define-key map "-" 'easy-kill-shrink)
107 (define-key map "+" 'easy-kill-expand)
108 (define-key map "=" 'easy-kill-expand)
109 (define-key map "@" 'easy-kill-append)
110 (define-key map [remap set-mark-command] 'easy-kill-mark-region)
111 (define-key map [remap kill-region] 'easy-kill-region)
112 (define-key map [remap keyboard-quit] 'easy-kill-abort)
113 (mapc (lambda (d)
114 (define-key map (number-to-string d) 'easy-kill-digit-argument))
115 (number-sequence 0 9))
116 map))
117
118 (defun easy-kill-map ()
119 "Build the keymap according to `easy-kill-alist'."
120 (let ((map (make-sparse-keymap)))
121 (set-keymap-parent map easy-kill-base-map)
122 (mapc (lambda (c)
123 ;; (define-key map (vector meta-prefix-char c) 'easy-kill-select)
124 (define-key map (char-to-string c) 'easy-kill-thing))
125 (mapcar 'car easy-kill-alist))
126 map))
127
128 (defvar easy-kill-inhibit-message nil)
129
130 (defun easy-kill-message-nolog (format-string &rest args)
131 "Same as `message' except not writing to *Messages* buffer.
132 Do nothing if `easy-kill-inhibit-message' is non-nil."
133 (unless easy-kill-inhibit-message
134 (let (message-log-max)
135 (apply 'message format-string args))))
136
137 (defvar easy-kill-candidate nil)
138 (defvar easy-kill-append nil)
139 (defvar easy-kill-mark nil)
140
141 (defun easy-kill-init-candidate (n)
142 (let ((o (make-overlay (point) (point))))
143 (unless easy-kill-mark
144 (overlay-put o 'face 'easy-kill-selection))
145 (overlay-put o 'origin (point))
146 ;; Use higher priority to avoid shadowing by, for example,
147 ;; `hl-line-mode'.
148 (overlay-put o 'priority 999)
149 (when easy-kill-mark
150 (let ((i (make-overlay (point) (point))))
151 (overlay-put i 'priority (1+ (overlay-get o 'priority)))
152 (overlay-put i 'face 'easy-kill-origin)
153 (overlay-put i 'as (propertize " " 'face 'easy-kill-origin))
154 (overlay-put o 'origin-indicator i)))
155 (setq easy-kill-candidate o)
156 (save-restriction
157 ;; Work around http://debbugs.gnu.org/15808; not needed in 24.4.
158 (narrow-to-region (max (point-min) (- (point) 1000))
159 (min (point-max) (+ (point) 1000)))
160 (let ((easy-kill-inhibit-message t))
161 (cl-dolist (thing easy-kill-try-things)
162 (easy-kill-thing thing n)
163 (or (string= (easy-kill-candidate) "")
164 (cl-return)))))
165 o))
166
167 (defun easy-kill-indicate-origin ()
168 (let ((i (overlay-get easy-kill-candidate 'origin-indicator))
169 (origin (overlay-get easy-kill-candidate 'origin)))
170 (cond
171 ((not (overlayp i)) nil)
172 ((= origin (point))
173 (overlay-put i 'after-string nil))
174 ((memq (char-after origin) '(?\t ?\n))
175 (overlay-put i 'after-string (overlay-get i 'as)))
176 (t (move-overlay i origin (1+ origin))
177 (overlay-put i 'after-string nil)))))
178
179 (defun easy-kill-candidate ()
180 "Get the kill candidate as a string.
181 If the overlay specified by variable `easy-kill-candidate' has
182 non-zero length, it is the string covered by the overlay.
183 Otherwise, it is the value of the overlay's candidate property."
184 (with-current-buffer (overlay-buffer easy-kill-candidate)
185 (or (if (/= (overlay-start easy-kill-candidate)
186 (overlay-end easy-kill-candidate))
187 (filter-buffer-substring (overlay-start easy-kill-candidate)
188 (overlay-end easy-kill-candidate))
189 (overlay-get easy-kill-candidate 'candidate))
190 "")))
191
192 (defun easy-kill-adjust-candidate (thing &optional beg end)
193 "Adjust kill candidate to THING, BEG, END.
194 If BEG is a string, shrink the overlay to zero length and set its
195 candidate property instead."
196 (let* ((o easy-kill-candidate)
197 (beg (or beg (overlay-start o)))
198 (end (or end (overlay-end o))))
199 (overlay-put o 'thing thing)
200 (if (stringp beg)
201 (progn
202 (move-overlay o (point) (point))
203 (overlay-put o 'candidate beg)
204 (let ((easy-kill-inhibit-message nil))
205 (easy-kill-message-nolog "%s" beg)))
206 (move-overlay o beg end))
207 (cond (easy-kill-mark (easy-kill-mark-region)
208 (easy-kill-indicate-origin))
209 ((and interprogram-cut-function
210 (not (string= (easy-kill-candidate) "")))
211 (funcall interprogram-cut-function (easy-kill-candidate))))))
212
213 (defun easy-kill-save-candidate ()
214 (unless (string= (easy-kill-candidate) "")
215 ;; Don't modify the clipboard here since it is called in
216 ;; `pre-command-hook' per `easy-kill-activate-keymap' and will
217 ;; confuse `yank' if it is current command. Also
218 ;; `easy-kill-adjust-candidate' already did that.
219 (let ((interprogram-cut-function nil)
220 (interprogram-paste-function nil))
221 (kill-new (if easy-kill-append
222 (concat (car kill-ring) (easy-kill-candidate))
223 (easy-kill-candidate))
224 easy-kill-append))
225 t))
226
227 (defun easy-kill-destroy-candidate ()
228 (let ((hook (make-symbol "easy-kill-destroy-candidate")))
229 (fset hook `(lambda ()
230 (let ((o ,easy-kill-candidate))
231 (when o
232 (let ((i (overlay-get o 'origin-indicator)))
233 (and (overlayp i) (delete-overlay i)))
234 (delete-overlay o)))
235 (remove-hook 'post-command-hook ',hook)))
236 ;; Run in `post-command-hook' so that exit commands can still use
237 ;; `easy-kill-candidate'.
238 (add-hook 'post-command-hook hook)))
239
240 (defun easy-kill-expand ()
241 (interactive)
242 (easy-kill-thing nil '+))
243
244 (defun easy-kill-digit-argument (n)
245 (interactive
246 (list (- (logand (if (integerp last-command-event)
247 last-command-event
248 (get last-command-event 'ascii-character))
249 ?\177)
250 ?0)))
251 (easy-kill-thing nil n))
252
253 (defun easy-kill-shrink ()
254 (interactive)
255 (easy-kill-thing nil '-))
256
257 ;; helper for `easy-kill-thing'.
258 (defun easy-kill-thing-forward (n)
259 (let ((thing (overlay-get easy-kill-candidate 'thing))
260 (direction (if (cl-minusp n) -1 +1))
261 (start (overlay-start easy-kill-candidate))
262 (end (overlay-end easy-kill-candidate)))
263 (when (and thing (/= n 0))
264 (let ((new-end (save-excursion
265 (goto-char end)
266 (with-demoted-errors
267 (cl-dotimes (_ (abs n))
268 (forward-thing thing direction)
269 (when (<= (point) start)
270 (forward-thing thing 1)
271 (cl-return))))
272 (point))))
273 (when (/= end new-end)
274 (easy-kill-adjust-candidate thing nil new-end)
275 t)))))
276
277 (defun easy-kill-thing (&optional thing n inhibit-handler)
278 ;; N can be -, + and digits
279 (interactive
280 (list (cdr (assq last-command-event easy-kill-alist))
281 (prefix-numeric-value current-prefix-arg)))
282 (let ((thing (or thing (overlay-get easy-kill-candidate 'thing)))
283 (n (or n 1)))
284 (when easy-kill-mark
285 (goto-char (overlay-get easy-kill-candidate 'origin)))
286 (cond
287 ((and (not inhibit-handler)
288 (fboundp (intern-soft (format "easy-kill-on-%s" thing))))
289 (funcall (intern (format "easy-kill-on-%s" thing)) n))
290 ((or (eq thing (overlay-get easy-kill-candidate 'thing))
291 (memq n '(+ -)))
292 (easy-kill-thing-forward (pcase n
293 (`+ 1)
294 (`- -1)
295 (n n))))
296 (t (let ((bounds (bounds-of-thing-at-point thing)))
297 (if (not bounds)
298 (easy-kill-message-nolog "No `%s'" thing)
299 (easy-kill-adjust-candidate thing (car bounds) (cdr bounds))
300 (easy-kill-thing-forward (1- n))))))
301 (when easy-kill-mark
302 (easy-kill-adjust-candidate (overlay-get easy-kill-candidate 'thing)))))
303
304 (put 'easy-kill-abort 'easy-kill-exit t)
305 (defun easy-kill-abort ()
306 (interactive)
307 (when easy-kill-mark
308 ;; The after-string may interfere with `goto-char'.
309 (overlay-put (overlay-get easy-kill-candidate 'origin-indicator)
310 'after-string nil)
311 (goto-char (overlay-get easy-kill-candidate 'origin))
312 (setq deactivate-mark t))
313 (ding))
314
315 (put 'easy-kill-region 'easy-kill-exit t)
316 (defun easy-kill-region ()
317 "Kill current selection and exit."
318 (interactive "*")
319 (let ((beg (overlay-start easy-kill-candidate))
320 (end (overlay-end easy-kill-candidate)))
321 (if (= beg end)
322 (easy-kill-message-nolog "Empty region")
323 (kill-region beg end))))
324
325 (put 'easy-kill-mark-region 'easy-kill-exit t)
326 (defun easy-kill-mark-region ()
327 (interactive)
328 (let ((beg (overlay-start easy-kill-candidate))
329 (end (overlay-end easy-kill-candidate)))
330 (if (= beg end)
331 (easy-kill-message-nolog "Empty region")
332 (set-mark beg)
333 (goto-char end)
334 (activate-mark))))
335
336 (put 'easy-kill-append 'easy-kill-exit t)
337 (defun easy-kill-append ()
338 (interactive)
339 (setq easy-kill-append t)
340 (when (easy-kill-save-candidate)
341 (and interprogram-cut-function
342 (funcall interprogram-cut-function (car kill-ring)))
343 (setq deactivate-mark t)
344 (easy-kill-message-nolog "Appended")))
345
346 (defun easy-kill-activate-keymap ()
347 (let ((map (easy-kill-map)))
348 (set-transient-map
349 map
350 (lambda ()
351 ;; Prevent any error from activating the keymap forever.
352 (condition-case err
353 (or (and (not (and (symbolp this-command)
354 (get this-command 'easy-kill-exit)))
355 (or (eq this-command
356 (lookup-key map (this-single-command-keys)))
357 (let ((cmd (key-binding
358 (this-single-command-keys) nil t)))
359 (command-remapping cmd nil (list map)))))
360 (ignore
361 (easy-kill-destroy-candidate)
362 (unless (or easy-kill-mark
363 (and (symbolp this-command)
364 (get this-command 'easy-kill-exit)))
365 (easy-kill-save-candidate))))
366 (error (message "%s:%s" this-command (error-message-string err))
367 nil))))))
368
369 ;;;###autoload
370 (defun easy-kill (&optional n)
371 "Kill thing at point in the order of region, url, email and line.
372 Temporally activate additional key bindings as follows:
373
374 letters => select or expand selection according to `easy-kill-alist';
375 0..9 => expand selection by that number;
376 +,=/- => expand or shrink selection;
377 @ => append selection to previous kill;
378 C-w => kill selection;
379 C-SPC => turn selection into an active region;
380 C-g => abort;
381 others => save selection and exit."
382 (interactive "p")
383 (if (use-region-p)
384 (if (fboundp 'rectangle-mark-mode)
385 (with-no-warnings ; new in 24.4
386 (kill-ring-save (region-beginning) (region-end) t))
387 (kill-ring-save (region-beginning) (region-end)))
388 (setq easy-kill-mark nil)
389 (setq easy-kill-append (eq last-command 'kill-region))
390 (easy-kill-init-candidate n)
391 (when (zerop (buffer-size))
392 (easy-kill-message-nolog "Warn: `easy-kill' activated in empty buffer"))
393 (easy-kill-activate-keymap)))
394
395 ;;;###autoload
396 (defalias 'easy-mark-sexp 'easy-mark
397 "Use `easy-mark' instead. The alias may be removed in future.")
398
399 ;;;###autoload
400 (defun easy-mark (&optional n)
401 "Similar to `easy-kill' (which see) but for marking."
402 (interactive "p")
403 (let ((easy-kill-try-things easy-mark-try-things))
404 (setq easy-kill-mark t)
405 (easy-kill-init-candidate n)
406 (easy-kill-activate-keymap)
407 (unless (overlay-get easy-kill-candidate 'thing)
408 (overlay-put easy-kill-candidate 'thing 'sexp)
409 (easy-kill-thing 'sexp n))))
410
411 ;;;; Extended things
412
413 ;;; Handler for `buffer-file-name'.
414
415 (defun easy-kill-on-buffer-file-name (n)
416 "Get `buffer-file-name' or `default-directory'.
417 If N is zero, remove the directory part; -, remove the file name
418 part; +, full path."
419 (if easy-kill-mark
420 (easy-kill-message-nolog "Not supported in `easy-mark'")
421 (let ((file (or buffer-file-name default-directory)))
422 (when file
423 (let* ((file (directory-file-name file))
424 (text (pcase n
425 (`- (file-name-directory file))
426 ((pred (eq 0)) (file-name-nondirectory file))
427 (_ file))))
428 (easy-kill-adjust-candidate 'buffer-file-name text))))))
429
430 ;;; Handler for `defun-name'.
431
432 (defun easy-kill-on-defun-name (_n)
433 "Get current defun name."
434 (if easy-kill-mark
435 (easy-kill-message-nolog "Not supported in `easy-mark'")
436 (let ((defun-name (add-log-current-defun)))
437 (if defun-name
438 (easy-kill-adjust-candidate 'defun-name defun-name)
439 (easy-kill-message-nolog "No `defun-name' at point")))))
440
441 ;;; Handler for `url'.
442
443 (defun easy-kill-on-url (&optional _n)
444 "Get url at point or from char properties.
445 Char properties `help-echo', `shr-url' and `w3m-href-anchor' are
446 inspected."
447 (if (or easy-kill-mark (bounds-of-thing-at-point 'url))
448 (easy-kill-thing 'url nil t)
449 (cl-labels ((get-url (text)
450 (when (stringp text)
451 (with-temp-buffer
452 (insert text)
453 (and (bounds-of-thing-at-point 'url)
454 (thing-at-point 'url))))))
455 (cl-dolist (p '(help-echo shr-url w3m-href-anchor))
456 (pcase-let* ((`(,text . ,ov)
457 (get-char-property-and-overlay (point) p))
458 (url (or (get-url text)
459 (get-url (and ov (overlay-get ov p))))))
460 (when url
461 (easy-kill-adjust-candidate 'url url)
462 (cl-return url)))))))
463
464 ;;; Handler for `sexp' and `list'.
465
466 (defvar up-list-fn) ; Dynamically bound
467
468 (defun easy-kill-backward-up ()
469 (let ((ppss (syntax-ppss)))
470 (condition-case nil
471 (progn
472 (funcall (or (bound-and-true-p up-list-fn) #'up-list) -1)
473 ;; `up-list' may jump to another string.
474 (when (and (nth 3 ppss) (< (point) (nth 8 ppss)))
475 (goto-char (nth 8 ppss))))
476 (scan-error (and (nth 3 ppss) (goto-char (nth 8 ppss)))))))
477
478 (defun easy-kill-forward-down (point &optional bound)
479 (condition-case nil
480 (progn
481 (easy-kill-backward-up)
482 (backward-prefix-chars)
483 (if (and (or (not bound) (> (point) bound))
484 (/= point (point)))
485 (easy-kill-forward-down (point) bound)
486 (goto-char point)))
487 (scan-error (goto-char point))))
488
489 (defun easy-kill-bounds-of-list (n)
490 (save-excursion
491 (pcase n
492 (`+ (goto-char (overlay-start easy-kill-candidate))
493 (easy-kill-backward-up))
494 (`- (easy-kill-forward-down
495 (point) (overlay-start easy-kill-candidate)))
496 (_ (error "Unsupported argument `%s'" n)))
497 (bounds-of-thing-at-point 'sexp)))
498
499 (defvar nxml-sexp-element-flag)
500
501 (defun easy-kill-on-nxml-element (n)
502 (let ((nxml-sexp-element-flag t)
503 (up-list-fn 'nxml-up-element))
504 (cond
505 ((memq n '(+ -))
506 (let ((bounds (easy-kill-bounds-of-list n)))
507 (when bounds
508 (easy-kill-adjust-candidate 'list (car bounds) (cdr bounds)))))
509 ((eq 'list (overlay-get easy-kill-candidate 'thing))
510 (let ((new-end (save-excursion
511 (goto-char (overlay-end easy-kill-candidate))
512 (forward-sexp n)
513 (point))))
514 (when (and new-end (/= new-end (overlay-end easy-kill-candidate)))
515 (easy-kill-adjust-candidate 'list nil new-end))))
516 (t (save-excursion
517 (ignore-errors (easy-kill-backward-up))
518 (easy-kill-thing 'sexp n t)
519 (overlay-put easy-kill-candidate 'thing 'list))))))
520
521 (defun easy-kill-find-js2-node (beg end &optional inner)
522 (eval-and-compile (require 'js2-mode))
523 (let* ((node (js2-node-at-point))
524 (last-node node))
525 (while (progn
526 (if (or (js2-ast-root-p node)
527 (and (<= (js2-node-abs-pos node) beg)
528 (>= (js2-node-abs-end node) end)
529 (or inner
530 (not (and (= (js2-node-abs-pos node) beg)
531 (= (js2-node-abs-end node) end))))))
532 nil
533 (setq last-node node
534 node (js2-node-parent node))
535 t)))
536 (if inner last-node node)))
537
538 (defun easy-kill-on-js2-node (n)
539 (let ((node (pcase n
540 ((or `+ `-)
541 (easy-kill-find-js2-node (overlay-start easy-kill-candidate)
542 (overlay-end easy-kill-candidate)
543 (eq n '-)))
544 ((guard (eq 'list (overlay-get easy-kill-candidate 'thing)))
545 (error "List forward not supported in js2-mode"))
546 (_ (js2-node-at-point)))))
547 (easy-kill-adjust-candidate 'list
548 (js2-node-abs-pos node)
549 (js2-node-abs-end node))))
550
551 (defun easy-kill-on-list (n)
552 (cond
553 ((derived-mode-p 'nxml-mode)
554 (easy-kill-on-nxml-element n))
555 ((derived-mode-p 'js2-mode)
556 (easy-kill-on-js2-node n))
557 ((memq n '(+ -))
558 (let ((bounds (easy-kill-bounds-of-list n)))
559 (when bounds
560 (easy-kill-adjust-candidate 'list (car bounds) (cdr bounds)))))
561 (t (easy-kill-thing 'list n t))))
562
563 (defun easy-kill-on-sexp (n)
564 (if (memq n '(+ -))
565 (easy-kill-on-list n)
566 (easy-kill-thing 'sexp n t)))
567
568 (provide 'easy-kill)
569 ;;; easy-kill.el ends here