1 ;;; easy-kill.el --- kill things easily -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2013-2014 Leo Liu
5 ;; Author: Leo Liu <sdl.web@gmail.com>
7 ;; Package-Requires: ((emacs "24") (cl-lib "0.5"))
8 ;; Keywords: convenience
10 ;; URL: https://github.com/leoliu/easy-kill
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.
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.
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/>.
27 ;; `easy-kill' aims to be a drop-in replacement for `kill-ring-save'.
29 ;; To use: (global-set-key [remap kill-ring-save] 'easy-kill)
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.
35 ;; To use: (global-set-key [remap mark-sexp] 'easy-mark)
37 ;; Please send bug reports or feature requests to:
38 ;; https://github.com/leoliu/easy-kill/issues
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))
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)))
57 (unless ,(cond ((null keep-pred) nil)
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))))))
72 (defcustom easy-kill-alist
79 (?b . buffer-file-name)
81 "A list of (CHAR . THING).
82 CHAR is used immediately following `easy-kill' to select THING."
83 :type '(repeat (cons character symbol))
86 (defcustom easy-kill-try-things '(url email line)
87 "A list of things for `easy-kill' to try."
88 :type '(repeat symbol)
91 (defcustom easy-mark-try-things '(url email sexp)
92 "A list of things for `easy-mark' to try."
93 :type '(repeat symbol)
96 (defface easy-kill-selection '((t (:inherit secondary-selection)))
97 "Faced used to highlight kill candidate."
100 (defface easy-kill-origin '((t (:inverse-video t :inherit error)))
101 "Faced used to highlight the origin."
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)
114 (define-key map (number-to-string d) 'easy-kill-digit-argument))
115 (number-sequence 0 9))
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)
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))
128 (defvar easy-kill-inhibit-message nil)
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))))
137 (defvar easy-kill-candidate nil)
138 (defvar easy-kill-append nil)
139 (defvar easy-kill-mark nil)
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,
148 (overlay-put o 'priority 999)
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)
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) "")
167 (defun easy-kill-indicate-origin ()
168 (let ((i (overlay-get easy-kill-candidate 'origin-indicator))
169 (origin (overlay-get easy-kill-candidate 'origin)))
171 ((not (overlayp i)) nil)
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)))))
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))
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)
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))))))
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))
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))
232 (let ((i (overlay-get o 'origin-indicator)))
233 (and (overlayp i) (delete-overlay i)))
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)))
240 (defun easy-kill-expand ()
242 (easy-kill-thing nil '+))
244 (defun easy-kill-digit-argument (n)
246 (list (- (logand (if (integerp last-command-event)
248 (get last-command-event 'ascii-character))
251 (easy-kill-thing nil n))
253 (defun easy-kill-shrink ()
255 (easy-kill-thing nil '-))
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
267 (cl-dotimes (_ (abs n))
268 (forward-thing thing direction)
269 (when (<= (point) start)
270 (forward-thing thing 1)
273 (when (/= end new-end)
274 (easy-kill-adjust-candidate thing nil new-end)
277 (defun easy-kill-thing (&optional thing n inhibit-handler)
278 ;; N can be -, + and digits
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)))
285 (goto-char (overlay-get easy-kill-candidate 'origin)))
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))
292 (easy-kill-thing-forward (pcase n
296 (t (let ((bounds (bounds-of-thing-at-point thing)))
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))))))
302 (easy-kill-adjust-candidate (overlay-get easy-kill-candidate 'thing)))))
304 (put 'easy-kill-abort 'easy-kill-exit t)
305 (defun easy-kill-abort ()
308 ;; The after-string may interfere with `goto-char'.
309 (overlay-put (overlay-get easy-kill-candidate 'origin-indicator)
311 (goto-char (overlay-get easy-kill-candidate 'origin))
312 (setq deactivate-mark t))
315 (put 'easy-kill-region 'easy-kill-exit t)
316 (defun easy-kill-region ()
317 "Kill current selection and exit."
319 (let ((beg (overlay-start easy-kill-candidate))
320 (end (overlay-end easy-kill-candidate)))
322 (easy-kill-message-nolog "Empty region")
323 (kill-region beg end))))
325 (put 'easy-kill-mark-region 'easy-kill-exit t)
326 (defun easy-kill-mark-region ()
328 (let ((beg (overlay-start easy-kill-candidate))
329 (end (overlay-end easy-kill-candidate)))
331 (easy-kill-message-nolog "Empty region")
336 (put 'easy-kill-append 'easy-kill-exit t)
337 (defun easy-kill-append ()
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")))
346 (defun easy-kill-activate-keymap ()
347 (let ((map (easy-kill-map)))
351 ;; Prevent any error from activating the keymap forever.
353 (or (and (not (and (symbolp this-command)
354 (get this-command 'easy-kill-exit)))
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)))))
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))
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:
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;
381 others => save selection and exit."
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)))
396 (defalias 'easy-mark-sexp 'easy-mark
397 "Use `easy-mark' instead. The alias may be removed in future.")
400 (defun easy-mark (&optional n)
401 "Similar to `easy-kill' (which see) but for marking."
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))))
413 ;;; Handler for `buffer-file-name'.
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
420 (easy-kill-message-nolog "Not supported in `easy-mark'")
421 (let ((file (or buffer-file-name default-directory)))
423 (let* ((file (directory-file-name file))
425 (`- (file-name-directory file))
426 ((pred (eq 0)) (file-name-nondirectory file))
428 (easy-kill-adjust-candidate 'buffer-file-name text))))))
430 ;;; Handler for `defun-name'.
432 (defun easy-kill-on-defun-name (_n)
433 "Get current defun name."
435 (easy-kill-message-nolog "Not supported in `easy-mark'")
436 (let ((defun-name (add-log-current-defun)))
438 (easy-kill-adjust-candidate 'defun-name defun-name)
439 (easy-kill-message-nolog "No `defun-name' at point")))))
441 ;;; Handler for `url'.
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
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)
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))))))
461 (easy-kill-adjust-candidate 'url url)
462 (cl-return url)))))))
464 ;;; Handler for `sexp' and `list'.
466 (defvar up-list-fn) ; Dynamically bound
468 (defun easy-kill-backward-up ()
469 (let ((ppss (syntax-ppss)))
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)))))))
478 (defun easy-kill-forward-down (point &optional bound)
481 (easy-kill-backward-up)
482 (backward-prefix-chars)
483 (if (and (or (not bound) (> (point) bound))
485 (easy-kill-forward-down (point) bound)
487 (scan-error (goto-char point))))
489 (defun easy-kill-bounds-of-list (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)))
499 (defvar nxml-sexp-element-flag)
501 (defun easy-kill-on-nxml-element (n)
502 (let ((nxml-sexp-element-flag t)
503 (up-list-fn 'nxml-up-element))
506 (let ((bounds (easy-kill-bounds-of-list n)))
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))
514 (when (and new-end (/= new-end (overlay-end easy-kill-candidate)))
515 (easy-kill-adjust-candidate 'list nil new-end))))
517 (ignore-errors (easy-kill-backward-up))
518 (easy-kill-thing 'sexp n t)
519 (overlay-put easy-kill-candidate 'thing 'list))))))
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))
526 (if (or (js2-ast-root-p node)
527 (and (<= (js2-node-abs-pos node) beg)
528 (>= (js2-node-abs-end node) end)
530 (not (and (= (js2-node-abs-pos node) beg)
531 (= (js2-node-abs-end node) end))))))
534 node (js2-node-parent node))
536 (if inner last-node node)))
538 (defun easy-kill-on-js2-node (n)
541 (easy-kill-find-js2-node (overlay-start easy-kill-candidate)
542 (overlay-end easy-kill-candidate)
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))))
551 (defun easy-kill-on-list (n)
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))
558 (let ((bounds (easy-kill-bounds-of-list n)))
560 (easy-kill-adjust-candidate 'list (car bounds) (cdr bounds)))))
561 (t (easy-kill-thing 'list n t))))
563 (defun easy-kill-on-sexp (n)
565 (easy-kill-on-list n)
566 (easy-kill-thing 'sexp n t)))
569 ;;; easy-kill.el ends here