1 ;;; easy-kill.el --- kill things easily -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2013 Leo Liu
5 ;; Author: Leo Liu <sdl.web@gmail.com>
7 ;; Package-Requires: ((emacs "24"))
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
34 ;; `easy-mark-sexp' can be a handy replacement for `mark-sexp' which
35 ;; allows you to use +,=/- to do list-wise expanding/shrinking.
37 ;; To use: (global-set-key [remap mark-sexp] 'easy-mark-sexp)
39 ;; Please send bug reports or feature requests to:
40 ;; https://github.com/leoliu/easy-kill/issues
44 (eval-when-compile (require 'cl))
48 (or (fboundp 'set-temporary-overlay-map) ; new in 24.3
49 (defun set-temporary-overlay-map (map &optional keep-pred)
50 (let* ((clearfunsym (make-symbol "clear-temporary-overlay-map"))
51 (overlaysym (make-symbol "t"))
52 (alist (list (cons overlaysym map)))
55 (unless ,(cond ((null keep-pred) nil)
59 (this-command-keys-vector))))
60 (t `(funcall ',keep-pred)))
61 (set ',overlaysym nil) ;Just in case.
62 (remove-hook 'pre-command-hook ',clearfunsym)
63 (setq emulation-mode-map-alists
64 (delq ',alist emulation-mode-map-alists))))))
65 (set overlaysym overlaysym)
66 (fset clearfunsym clearfun)
67 (add-hook 'pre-command-hook clearfunsym)
68 (push alist emulation-mode-map-alists)))))
70 (defcustom easy-kill-alist
77 (?b . buffer-file-name))
78 "A list of (CHAR . THING).
79 CHAR is used immediately following `easy-kill' to select THING."
80 :type '(repeat (cons character symbol))
83 (defcustom easy-kill-try-things '(url email line)
84 "A list of things for `easy-kill' to try."
85 :type '(repeat symbol)
88 (defface easy-kill-selection '((t (:inherit secondary-selection)))
89 "Faced used to highlight kill candidate."
92 (defface easy-kill-origin '((t (:inverse-video t :inherit error)))
93 "Faced used to highlight the origin."
96 (defvar easy-kill-base-map
97 (let ((map (make-sparse-keymap)))
98 (define-key map "-" 'easy-kill-shrink)
99 (define-key map "+" 'easy-kill-expand)
100 (define-key map "=" 'easy-kill-expand)
101 (define-key map "@" 'easy-kill-append)
102 (define-key map [remap set-mark-command] 'easy-kill-mark-region)
103 (define-key map [remap kill-region] 'easy-kill-region)
104 (define-key map [remap keyboard-quit] 'easy-kill-abort)
106 (define-key map (number-to-string d) 'easy-kill-digit-argument))
107 (number-sequence 0 9))
110 (defun easy-kill-map ()
111 "Build the keymap according to `easy-kill-alist'."
112 (let ((map (make-sparse-keymap)))
113 (set-keymap-parent map easy-kill-base-map)
115 ;; (define-key map (vector meta-prefix-char c) 'easy-kill-select)
116 (define-key map (char-to-string c) 'easy-kill-thing))
117 (mapcar 'car easy-kill-alist))
120 (defvar easy-kill-inhibit-message nil)
122 (defun easy-kill-message-nolog (format-string &rest args)
123 "Same as `message' except not writing to *Messages* buffer.
124 Do nothing if `easy-kill-inhibit-message' is non-nil."
125 (unless easy-kill-inhibit-message
126 (let (message-log-max)
127 (apply 'message format-string args))))
129 (defvar easy-kill-candidate nil)
130 (defvar easy-kill-append nil)
131 (defvar easy-kill-mark nil)
133 (defun easy-kill-init-candidate (n)
134 (let ((o (make-overlay (point) (point))))
135 (unless easy-kill-mark
136 (overlay-put o 'face 'easy-kill-selection))
137 (overlay-put o 'origin (point))
138 ;; Use higher priority to avoid shadowing by, for example,
140 (overlay-put o 'priority 999)
142 (let ((i (make-overlay (point) (point))))
143 (overlay-put i 'priority (1+ (overlay-get o 'priority)))
144 (overlay-put i 'face 'easy-kill-origin)
145 (overlay-put i 'as (propertize " " 'face 'easy-kill-origin))
146 (overlay-put o 'origin-indicator i)))
147 (setq easy-kill-candidate o)
148 (let ((easy-kill-inhibit-message t))
149 (dolist (thing easy-kill-try-things)
150 (easy-kill-thing thing n)
151 (or (string= (easy-kill-candidate) "")
155 (defun easy-kill-indicate-origin ()
156 (let ((i (overlay-get easy-kill-candidate 'origin-indicator))
157 (origin (overlay-get easy-kill-candidate 'origin)))
159 ((not (overlayp i)) nil)
161 (overlay-put i 'after-string nil))
162 ((memq (char-after origin) '(?\t ?\n))
163 (overlay-put i 'after-string (overlay-get i 'as)))
164 (t (move-overlay i origin (1+ origin))
165 (overlay-put i 'after-string nil)))))
167 (defun easy-kill-candidate ()
168 "Get the kill candidate as a string.
169 If the overlay specified by variable `easy-kill-candidate' has
170 non-zero length, it is the string covered by the overlay.
171 Otherwise, it is the value of the overlay's candidate property."
172 (with-current-buffer (overlay-buffer easy-kill-candidate)
173 (or (if (/= (overlay-start easy-kill-candidate)
174 (overlay-end easy-kill-candidate))
175 (filter-buffer-substring (overlay-start easy-kill-candidate)
176 (overlay-end easy-kill-candidate))
177 (overlay-get easy-kill-candidate 'candidate))
180 (defun easy-kill-adjust-candidate (thing &optional beg end)
181 "Adjust kill candidate to THING, BEG, END.
182 If BEG is a string, shrink the overlay to zero length and set its
183 candidate property instead."
184 (let* ((o easy-kill-candidate)
185 (beg (or beg (overlay-start o)))
186 (end (or end (overlay-end o))))
187 (overlay-put o 'thing thing)
190 (move-overlay o (point) (point))
191 (overlay-put o 'candidate beg)
192 (let ((easy-kill-inhibit-message nil))
193 (easy-kill-message-nolog "%s" beg)))
194 (move-overlay o beg end))
195 (cond (easy-kill-mark (easy-kill-mark-region)
196 (easy-kill-indicate-origin))
197 ((and interprogram-cut-function
198 (not (string= (easy-kill-candidate) "")))
199 (funcall interprogram-cut-function (easy-kill-candidate))))))
201 (defun easy-kill-save-candidate ()
202 (unless (string= (easy-kill-candidate) "")
203 ;; Don't modify the clipboard here since it is called in
204 ;; `pre-command-hook' per `easy-kill-activate-keymap' and will
205 ;; confuse `yank' if it is current command. Also
206 ;; `easy-kill-adjust-candidate' already did that.
207 (let ((interprogram-cut-function nil)
208 (interprogram-paste-function nil))
209 (kill-new (if easy-kill-append
210 (concat (car kill-ring) (easy-kill-candidate))
211 (easy-kill-candidate))
215 (defun easy-kill-destroy-candidate ()
216 (let ((hook (make-symbol "easy-kill-destroy-candidate")))
217 (fset hook `(lambda ()
218 (let ((o ,easy-kill-candidate))
220 (let ((i (overlay-get o 'origin-indicator)))
221 (and (overlayp i) (delete-overlay i)))
223 (remove-hook 'post-command-hook ',hook)))
224 ;; Run in `post-command-hook' so that exit commands can still use
225 ;; `easy-kill-candidate'.
226 (add-hook 'post-command-hook hook)))
228 (defun easy-kill-expand ()
230 (easy-kill-thing nil '+))
232 (defun easy-kill-digit-argument (n)
234 (list (- (logand (if (integerp last-command-event)
236 (get last-command-event 'ascii-character))
239 (easy-kill-thing nil n))
241 (defun easy-kill-shrink ()
243 (easy-kill-thing nil '-))
245 ;; helper for `easy-kill-thing'.
246 (defun easy-kill-thing-forward (n)
247 (let ((thing (overlay-get easy-kill-candidate 'thing))
248 (direction (if (minusp n) -1 +1))
249 (start (overlay-start easy-kill-candidate))
250 (end (overlay-end easy-kill-candidate)))
251 (when (and thing (/= n 0))
252 (let ((new-end (save-excursion
256 (forward-thing thing direction)
257 (when (<= (point) start)
258 (forward-thing thing 1)
261 (when (/= end new-end)
262 (easy-kill-adjust-candidate thing nil new-end)
265 (defun easy-kill-thing (&optional thing n inhibit-handler)
266 ;; N can be -, + and digits
268 (list (cdr (assq last-command-event easy-kill-alist))
269 (prefix-numeric-value current-prefix-arg)))
270 (let ((thing (or thing (overlay-get easy-kill-candidate 'thing)))
273 (goto-char (overlay-get easy-kill-candidate 'origin)))
275 ((and (not inhibit-handler)
276 (fboundp (intern-soft (format "easy-kill-on-%s" thing))))
277 (funcall (intern (format "easy-kill-on-%s" thing)) n))
278 ((or (eq thing (overlay-get easy-kill-candidate 'thing))
280 (easy-kill-thing-forward (pcase n
284 (t (let ((bounds (bounds-of-thing-at-point thing)))
286 (easy-kill-message-nolog "No `%s'" thing)
287 (easy-kill-adjust-candidate thing (car bounds) (cdr bounds))
288 (easy-kill-thing-forward (1- n))))))
290 (easy-kill-adjust-candidate (overlay-get easy-kill-candidate 'thing)))))
292 (put 'easy-kill-abort 'easy-kill-exit t)
293 (defun easy-kill-abort ()
296 ;; The after-string may interfere with `goto-char'.
297 (overlay-put (overlay-get easy-kill-candidate 'origin-indicator)
299 (goto-char (overlay-get easy-kill-candidate 'origin))
300 (setq deactivate-mark t))
303 (put 'easy-kill-region 'easy-kill-exit t)
304 (defun easy-kill-region ()
305 "Kill current selection and exit."
307 (let ((beg (overlay-start easy-kill-candidate))
308 (end (overlay-end easy-kill-candidate)))
310 (easy-kill-message-nolog "Empty region")
311 (kill-region beg end))))
313 (put 'easy-kill-mark-region 'easy-kill-exit t)
314 (defun easy-kill-mark-region ()
316 (let ((beg (overlay-start easy-kill-candidate))
317 (end (overlay-end easy-kill-candidate)))
319 (easy-kill-message-nolog "Empty region")
324 (put 'easy-kill-append 'easy-kill-exit t)
325 (defun easy-kill-append ()
327 (setq easy-kill-append t)
328 (when (easy-kill-save-candidate)
329 (setq deactivate-mark t)
330 (easy-kill-message-nolog "Appended")))
332 (defun easy-kill-activate-keymap ()
333 (let ((map (easy-kill-map)))
334 (set-temporary-overlay-map
337 ;; Prevent any error from activating the keymap forever.
339 (or (and (not (and (symbolp this-command)
340 (get this-command 'easy-kill-exit)))
341 (or (eq this-command (lookup-key map (this-single-command-keys)))
342 (let ((cmd (key-binding (this-single-command-keys) nil t)))
343 (command-remapping cmd nil (list map)))))
345 (easy-kill-destroy-candidate)
346 (unless (or easy-kill-mark
347 (and (symbolp this-command)
348 (get this-command 'easy-kill-exit)))
349 (easy-kill-save-candidate)))))))))
352 (defun easy-kill (&optional n)
353 "Kill thing at point in the order of region, url, email and line.
354 Temporally activate additional key bindings as follows:
356 letters => select or expand selection according to `easy-kill-alist';
357 0..9 => expand selection by that number;
358 +,=/- => expand or shrink selection;
359 @ => append selection to previous kill;
360 C-w => kill selection;
361 C-SPC => turn selection into an active region;
363 others => save selection and exit."
366 (if (fboundp 'rectangle-mark)
367 (with-no-warnings ; new in 24.4
368 (kill-ring-save (region-beginning) (region-end) t))
369 (kill-ring-save (region-beginning) (region-end)))
370 (setq easy-kill-mark nil)
371 (setq easy-kill-append (eq last-command 'kill-region))
372 (easy-kill-init-candidate n)
373 (when (zerop (buffer-size))
374 (easy-kill-message-nolog "Warn: `easy-kill' activated in empty buffer"))
375 (easy-kill-activate-keymap)))
378 (defun easy-mark (&optional n)
379 "Like `easy-kill' (which see) but for marking."
381 (setq easy-kill-mark t)
382 (easy-kill-init-candidate n)
383 (easy-kill-activate-keymap))
386 (defun easy-mark-sexp (&optional n)
388 (let ((easy-kill-try-things '(sexp)))
390 (unless (overlay-get easy-kill-candidate 'thing)
391 (overlay-put easy-kill-candidate 'thing 'sexp)
392 (easy-kill-thing 'sexp n))))
396 ;;; Handler for `buffer-file-name'.
398 (defun easy-kill-on-buffer-file-name (n)
399 "Get `buffer-file-name' or `default-directory'.
400 If N is zero, remove the directory part; -, remove the file name
401 party; +, full path."
403 (easy-kill-message-nolog "Not supported in `easy-mark'")
404 (let ((file (or buffer-file-name default-directory)))
406 (let* ((file (directory-file-name file))
408 (`- (file-name-directory file))
409 ((pred (eq 0)) (file-name-nondirectory file))
411 (easy-kill-adjust-candidate 'buffer-file-name text))))))
413 ;;; Handler for `url'.
415 (defun easy-kill-on-url (&optional _n)
416 "Get url at point or from char properties.
417 Char properties `help-echo', `shr-url' and `w3m-href-anchor' are
419 (if (or easy-kill-mark (bounds-of-thing-at-point 'url))
420 (easy-kill-thing 'url nil t)
421 (let ((get-url (lambda (text)
425 (and (bounds-of-thing-at-point 'url)
426 (thing-at-point 'url)))))))
427 (dolist (p '(help-echo shr-url w3m-href-anchor))
428 (pcase-let* ((`(,text . ,ov)
429 (get-char-property-and-overlay (point) p))
430 (url (or (funcall get-url text)
432 (and ov (overlay-get ov p))))))
434 (easy-kill-adjust-candidate 'url url)
437 ;;; Handler for `sexp' and `list'.
439 (defvar up-list-fn) ; Dynamically bound
441 (defun easy-kill-backward-up ()
442 (let ((ppss (syntax-ppss)))
445 (funcall (or (bound-and-true-p up-list-fn) #'up-list) -1)
446 ;; `up-list' may jump to another string.
447 (when (and (nth 3 ppss) (< (point) (nth 8 ppss)))
448 (goto-char (nth 8 ppss))))
449 (scan-error (and (nth 3 ppss) (goto-char (nth 8 ppss)))))))
451 (defun easy-kill-backward-down (point &optional bound)
454 (easy-kill-backward-up)
455 (backward-prefix-chars)
456 (if (and (or (not bound) (> (point) bound))
458 (easy-kill-backward-down (point) bound)
460 (scan-error (goto-char point))))
462 (defun easy-kill-bounds-of-list (n)
465 (`+ (goto-char (overlay-start easy-kill-candidate))
466 (easy-kill-backward-up))
467 (`- (easy-kill-backward-down
468 (point) (overlay-start easy-kill-candidate)))
469 (_ (error "Unsupported argument `%s'" n)))
470 (bounds-of-thing-at-point 'sexp)))
472 (defvar nxml-sexp-element-flag)
474 (defun easy-kill-on-nxml-element (n)
475 (let ((nxml-sexp-element-flag t)
476 (up-list-fn 'nxml-up-element))
479 (let ((bounds (easy-kill-bounds-of-list n)))
481 (easy-kill-adjust-candidate 'list (car bounds) (cdr bounds)))))
482 ((eq 'list (overlay-get easy-kill-candidate 'thing))
483 (let ((new-end (save-excursion
484 (goto-char (overlay-end easy-kill-candidate))
487 (when (and new-end (/= new-end (overlay-end easy-kill-candidate)))
488 (easy-kill-adjust-candidate 'list nil new-end))))
490 (ignore-errors (easy-kill-backward-up))
491 (easy-kill-thing 'sexp n t)
492 (overlay-put easy-kill-candidate 'thing 'list))))))
494 (defun easy-kill-on-list (n)
496 ((derived-mode-p 'nxml-mode)
497 (easy-kill-on-nxml-element n))
499 (let ((bounds (easy-kill-bounds-of-list n)))
501 (easy-kill-adjust-candidate 'list (car bounds) (cdr bounds)))))
502 (t (easy-kill-thing 'list n t))))
504 (defun easy-kill-on-sexp (n)
505 (let ((nxml-sexp-element-flag t))
507 (easy-kill-on-list n)
508 (easy-kill-thing 'sexp n t))))
511 ;;; easy-kill.el ends here