1 ;;; iedit.el --- Edit multiple regions in the same way simultaneously.
3 ;; Copyright (C) 2010, 2011, 2012 Victor Ren
5 ;; Time-stamp: <2012-10-22 14:14:53 Victor Ren>
6 ;; Author: Victor Ren <victorhge@gmail.com>
7 ;; Keywords: occurrence region simultaneous refactoring
9 ;; X-URL: http://www.emacswiki.org/emacs/Iedit
10 ;; Compatibility: GNU Emacs: 22.x, 23.x, 24.x
12 ;; This file is not part of GNU Emacs, but it is distributed under
13 ;; the same terms as GNU Emacs.
15 ;; GNU Emacs is free software: you can redistribute it and/or modify
16 ;; it under the terms of the GNU General Public License as published by
17 ;; the Free Software Foundation, either version 3 of the License, or
18 ;; (at your option) any later version.
20 ;; GNU Emacs is distributed in the hope that it will be useful,
21 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
22 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 ;; GNU General Public License for more details.
25 ;; You should have received a copy of the GNU General Public License
26 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
30 ;; This package is an Emacs minor mode and allows you to edit one occurrence of
31 ;; some text in a buffer (possibly narrowed) or region, and simultaneously have
32 ;; other occurrences edited in the same way.
34 ;; Normal scenario of iedit-mode is like:
36 ;; - Highlight certain contents - by press C-;
37 ;; All occurrences of a symbol, string in the buffer or a region may be
38 ;; highlighted corresponding to current mark, point and prefix argument.
39 ;; Refer to the document of `iedit-mode' for details.
41 ;; - Edit one of the occurrences
42 ;; The change is applied to other occurrences simultaneously.
44 ;; - Finish - by pressing C-; again
46 ;; You can also use Iedit mode as a quick way to temporarily show only the
47 ;; buffer lines that match the current text being edited. This gives you the
48 ;; effect of a temporary `keep-lines' or `occur'. To get this effect, hit C-'
49 ;; when in Iedit mode - it toggles hiding non-matching lines.
51 ;; Renaming refactoring is convenient in Iedit mode
53 ;; - The symbol under point is selected as occurrence by default and only
54 ;; complete symbols are matched
55 ;; - With digit prefix argument 0, only symbols in current function are matched
56 ;; - Restricting symbols in current region can be done by pressing C-; again
57 ;; - Last renaming refactoring is remembered and can be applied to other buffers
60 ;; There are also some other facilities you may never think about. Refer to the
61 ;; document of function `iedit-mode' (C-h f iedit-mode RET) for more details.
63 ;; The code was developed and fully tested on Gnu Emacs 24.0.93, partially
64 ;; tested on Gnu Emacs 22. If you have any compatible problem, please let me
68 ;; - Add more easy access keys for whole occurrence
71 ;; Adam Lindberg <eproxus@gmail.com> added a case sensitivity option that can be toggled.
73 ;; Tassilo Horn <tassilo@member.fsf.org> added an option to match only complete
74 ;; words, not inside words
76 ;; Le Wang <l26wang@gmail.com> proposed to match only complete symbols, not
77 ;; inside symbols, contributed rectangle support
81 (eval-when-compile (require 'cl))
84 (defcustom iedit-current-symbol-default t
85 "If no-nil, use current symbol by default for the occurrence."
89 (defcustom iedit-only-at-symbol-boundaries t
90 "If no-nil, matches have to start and end at symbol boundaries.
91 For example, when invoking command `iedit-mode' on the \"in\" in the
92 sentence \"The king in the castle...\", the \"king\" is not
97 (defcustom iedit-toggle-key-default (kbd "C-;")
98 "If no-nil, the key is inserted into global-map, isearch-mode-map, esc-map and help-map."
102 (defvar iedit-mode-hook nil
103 "Function(s) to call after starting up an iedit.")
105 (defvar iedit-mode-end-hook nil
106 "Function(s) to call after terminating an iedit.")
108 (defvar iedit-mode nil) ;; Name of the minor mode
110 (defvar iedit-only-complete-symbol-local nil
111 "This is buffer local variable which indicates the occurrence
112 only matches complete symbol.")
114 (defvar iedit-only-complete-symbol-global nil
115 "This is global variable which indicates the last global occurrence
116 only matches complete symbol.")
118 (defvar iedit-last-occurrence-local nil
119 "This is buffer local variable which is the occurrence when
120 Iedit mode is turned off last time.")
122 (defvar iedit-last-occurrence-global nil
123 "This is global variable which is the occurrence when
124 Iedit mode is turned off last time.")
126 (defvar iedit-last-initial-string-global nil
127 "This is a global variable which is the last initial occurrence string.")
129 (defvar iedit-initial-string-local nil
130 "This is buffer local variable which is the initial string to start Iedit mode.")
131 (defvar iedit-initial-region nil
132 "This is buffer local variable which is the initial region
133 where Iedit mode is started from.")
136 (make-variable-buffer-local 'iedit-mode)
137 (make-variable-buffer-local 'iedit-only-complete-symbol-local)
138 (make-variable-buffer-local 'iedit-last-occurrence-local)
139 (make-variable-buffer-local 'iedit-initial-string-local)
140 (make-variable-buffer-local 'iedit-initial-region)
143 (or (assq 'iedit-mode minor-mode-alist)
144 (nconc minor-mode-alist
145 (list '(iedit-mode iedit-mode))))
147 ;;; Define iedit help map.
148 (eval-when-compile (require 'help-macro))
150 (defvar iedit-help-map
151 (let ((map (make-sparse-keymap)))
152 (define-key map (char-to-string help-char) 'iedit-help-for-help)
153 (define-key map [help] 'iedit-help-for-help)
154 (define-key map [f1] 'iedit-help-for-help)
155 (define-key map "?" 'iedit-help-for-help)
156 (define-key map "b" 'iedit-describe-bindings)
157 (define-key map "k" 'iedit-describe-key)
158 (define-key map "m" 'iedit-describe-mode)
159 (define-key map "q" 'help-quit)
161 "Keymap for characters following the Help key for Iedit mode.")
164 iedit-help-for-help-internal
165 (purecopy "Type a help option: [bkm] or ?")
166 "You have typed %THIS-KEY%, the help character. Type a Help option:
167 \(Type \\<help-map>\\[help-quit] to exit the Help command.)
169 b Display all Iedit key bindings.
170 k KEYS Display full documentation of Iedit key sequence.
171 m Display documentation of Iedit mode.
173 You can't type here other help keys available in the global help map,
174 but outside of this help window when you type them in Iedit mode,
175 they exit Iedit mode before displaying global help."
178 (defun iedit-help-for-help ()
179 "Display Iedit help menu."
181 (let (same-window-buffer-names same-window-regexps)
182 (iedit-help-for-help-internal)))
184 (defun iedit-describe-bindings ()
185 "Show a list of all keys defined in Iedit mode, and their definitions.
186 This is like `describe-bindings', but displays only Iedit keys."
188 (let (same-window-buffer-names
190 (keymap (substitute-command-keys "\\{iedit-mode-keymap}\\{iedit-mode-occurrence-keymap}")))
191 (with-help-window "*Help*"
192 (with-current-buffer standard-output
193 (princ "Iedit Mode Bindings: ")
196 (defun iedit-describe-key ()
197 "Display documentation of the function invoked by Iedit mode key."
199 (let (same-window-buffer-names same-window-regexps)
200 (call-interactively 'describe-key)))
202 (defun iedit-describe-mode ()
203 "Display documentation of Iedit mode."
205 (let (same-window-buffer-names same-window-regexps)
206 (describe-function 'iedit-mode)))
208 ;;; Default key bindings:
209 (when iedit-toggle-key-default
210 (define-key global-map iedit-toggle-key-default 'iedit-mode)
211 (define-key isearch-mode-map iedit-toggle-key-default 'iedit-mode-from-isearch)
212 (define-key esc-map iedit-toggle-key-default 'iedit-execute-last-modification)
213 (define-key help-map iedit-toggle-key-default 'iedit-mode-toggle-on-function))
215 ;; Avoid to restore Iedit mode when restoring desktop
216 (add-to-list 'desktop-minor-mode-handlers
219 ;;; Define iedit help map.
220 (eval-when-compile (require 'help-macro))
222 (defvar iedit-mode-occurrence-keymap
223 (let ((map (make-sparse-keymap)))
224 (set-keymap-parent map iedit-occurrence-keymap-default)
225 (define-key map (kbd "M-H") 'iedit-restrict-function)
226 (define-key map (kbd "M-C") 'iedit-toggle-case-sensitive)
228 "Keymap used within overlays in Iedit mode.")
230 (defvar iedit-mode-keymap
231 (let ((map (make-sparse-keymap)))
232 (set-keymap-parent map iedit-lib-keymap)
233 (define-key map (char-to-string help-char) iedit-help-map)
234 (define-key map [help] iedit-help-map)
235 (define-key map [f1] iedit-help-map)
237 "Keymap used while Iedit mode is enabled.")
239 ;;; Define Iedit mode map
240 (or (assq 'iedit-mode minor-mode-map-alist)
241 (setq minor-mode-map-alist
242 (cons (cons 'iedit-mode iedit-mode-keymap) minor-mode-map-alist)))
244 ;; Avoid to restore Iedit mode when restoring desktop
245 (add-to-list 'desktop-minor-mode-handlers
249 (defun iedit-mode (&optional arg)
251 This command behaves differently, depending on the mark, point,
252 prefix argument and variable `iedit-transient-mark-sensitive'.
254 If Iedit mode is off, turn Iedit mode on.
256 When Iedit mode is turned on, all the occurrences of the current
257 region in the buffer (possibly narrowed) or a region are
258 highlighted. If one occurrence is modified, the change are
259 propagated to all other occurrences simultaneously.
261 If region is not active, the current symbol (returns from
262 `current-word') is used as the occurrence by default. The
263 occurrences of the current symbol, but not include occurrences
264 that are part of other symbols, are highlighted. If you still
265 want to match all the occurrences, even though they are parts of
266 other symbols, you may have to mark the symbol first.
268 In the above two situations, with digit prefix argument 0, only
269 occurrences in current function are matched. This is good for
270 renaming refactoring in programming.
272 You can also switch to Iedit mode from isearch mode directly. The
273 current search string is used as occurrence. All occurrences of
274 the current search string are highlighted.
276 With an universal prefix argument, the occurrence when Iedit mode
277 is turned off last time in current buffer is used as occurrence.
278 This is intended to recover last Iedit mode which is turned off.
279 If region active, Iedit mode is limited within the current
282 With repeated universal prefix argument, the occurrence when
283 Iedit mode is turned off last time (might be in other buffer) is
284 used as occurrence. If region active, Iedit mode is limited
285 within the current region.
287 If Iedit mode is on and region is active, Iedit mode is
288 restricted in the region, e.g. the occurrences outside of the
291 If Iedit mode is on and region is active, with an universal
292 prefix argument, Iedit mode is restricted outside of the region,
293 e.g. the occurrences in the region is excluded.
295 Turn off Iedit mode in other situations.
298 \\{iedit-mode-keymap}
299 Keymap used within overlays:
300 \\{iedit-mode-occurrence-keymap}"
304 (iedit-mode-on-action arg)
305 (setq iedit-only-complete-symbol-global iedit-only-complete-symbol-local))
306 (iedit-barf-if-lib-active)
309 (beg (if (eq major-mode 'occur-edit-mode) ; skip the first occurrence
310 (next-single-char-property-change 1 'read-only)
314 (= 4 (prefix-numeric-value arg))
315 iedit-last-occurrence-local)
316 (setq occurrence iedit-last-occurrence-local)
317 (setq complete-symbol iedit-only-complete-symbol-local))
319 (= 16 (prefix-numeric-value arg))
320 iedit-last-initial-string-global)
321 (setq occurrence iedit-last-initial-string-global)
322 (setq complete-symbol iedit-only-complete-symbol-global))
323 ((iedit-region-active)
324 (setq occurrence (buffer-substring-no-properties
326 ((and iedit-current-symbol-default (current-word t))
327 (setq occurrence (current-word))
328 (when iedit-only-at-symbol-boundaries
329 (setq complete-symbol t)))
330 (t (error "No candidate of the occurrence, cannot enable Iedit mode")))
332 (if (= 0 (prefix-numeric-value arg))
335 (setq beg (region-beginning))
336 (setq end (region-end)))
337 (when (iedit-region-active)
338 (setq beg (region-beginning))
339 (setq end (region-end)))))
340 (setq iedit-only-complete-symbol-local complete-symbol)
341 (setq mark-active nil)
342 (run-hooks 'deactivate-mark-hook)
343 (setq iedit-initial-string-local occurrence)
344 (iedit-start (iedit-regexp-quote occurrence) beg end))))
346 (defun iedit-mode-from-isearch (regexp)
347 "Start Iedit mode using last search string as the regexp."
350 ((functionp isearch-word)
351 (funcall isearch-word isearch-string))
352 (isearch-word (word-search-regexp isearch-string))
353 (isearch-regexp isearch-string)
354 (t (regexp-quote isearch-string)))))
356 (if (or isearch-regexp isearch-word)
358 (setq iedit-initial-string-local isearch-string))
360 (setq mark-active nil)
361 (run-hooks 'deactivate-mark-hook)
362 (iedit-start regexp (point-min) (point-max))
363 ;; TODO: reconsider how to avoid the loop in iedit-same-length
364 (if (iedit-same-length)
367 (message "Matches are not the same length.")))
369 (defun iedit-start (occurrence-regexp beg end)
370 "Start Iedit mode for the `occurrence-regexp' in the current buffer."
371 (setq iedit-unmatched-lines-invisible iedit-unmatched-lines-invisible-default)
372 (setq iedit-initial-region (list beg end))
373 (iedit-start2 occurrence-regexp beg end)
374 (run-hooks 'iedit-mode-hook)
375 (add-hook 'kbd-macro-termination-hook 'iedit-done nil t)
376 (add-hook 'change-major-mode-hook 'iedit-done nil t)
377 (add-hook 'iedit-aborting-hook 'iedit-done nil t))
379 (defun iedit-regexp-quote (exp)
380 "Return a regexp string."
381 (if iedit-only-complete-symbol-local
382 (concat "\\_<" (regexp-quote exp) "\\_>")
385 (defun iedit-start2 (occurrence-regexp beg end)
386 "Refresh Iedit mode."
387 (setq iedit-occurrence-keymap iedit-mode-occurrence-keymap)
392 (iedit-make-occurrences-overlays occurrence-regexp beg end)))
394 'font-lock-warning-face))
395 (force-mode-line-update))
399 Save the current occurrence string locally and globally. Save
400 the initial string globally."
401 (when iedit-buffering
402 (iedit-stop-buffering))
403 (setq iedit-last-occurrence-local (iedit-current-occurrence-string))
404 (setq iedit-last-occurrence-global iedit-last-occurrence-local)
405 (setq iedit-last-initial-string-global iedit-initial-string-local)
409 (setq iedit-initial-string-local nil)
410 (setq iedit-mode nil)
411 (force-mode-line-update)
412 (remove-hook 'kbd-macro-termination-hook 'iedit-done t)
413 (remove-hook 'change-major-mode-hook 'iedit-done t)
414 (remove-hook 'iedit-aborting-hook 'iedit-done t)
415 (run-hooks 'iedit-mode-end-hook))
417 (defun iedit-mode-on-action (&optional arg)
418 "Turn off Iedit mode or restrict it in a region if region is active."
419 (if (iedit-region-active)
420 ;; Restrict iedit-mode
421 (let ((beg (region-beginning))
423 (if (null (iedit-find-overlay beg end 'iedit-occurrence-overlay-name arg))
425 (iedit-restrict-region beg end arg)
426 (iedit-first-occurrence)))
431 (defun iedit-mode-toggle-on-function ()
432 "Toggle Iedit mode on current function."
436 (defun iedit-execute-last-modification (&optional arg)
437 "Apply last modification in Iedit mode to the current buffer or an active region."
439 (or (and iedit-last-initial-string-global
440 (not (string= iedit-last-initial-string-global iedit-last-occurrence-global)))
441 (error "No modification available"))
442 (let ((occurrence-exp (regexp-quote iedit-last-initial-string-global))
443 (replacement iedit-last-occurrence-global)
444 (case-fold-search (not iedit-case-sensitive))
446 (when case-fold-search
447 (setq occurrence-exp (downcase occurrence-exp))
448 (setq replacement (downcase replacement)))
449 (if iedit-only-complete-symbol-global
450 (setq occurrence-exp (concat "\\_<" occurrence-exp "\\_>")))
451 (when (iedit-region-active)
452 (setq beg (region-beginning))
453 (setq end (region-end)))
454 (perform-replace occurrence-exp replacement t t nil nil nil beg end)))
456 (defun iedit-apply-global-modification ()
457 "Apply last global modification."
459 (if (and iedit-last-initial-string-global
460 (string= iedit-initial-string-local iedit-last-initial-string-global)
461 (not (string= iedit-last-initial-string-global iedit-last-occurrence-global)))
462 (iedit-replace-occurrences iedit-last-occurrence-global)
463 (message "No global modification available.")))
466 (defun iedit-restrict-function(&optional arg)
467 "Restricting Iedit mode in current function."
471 (iedit-restrict-region (region-beginning) (region-end) arg))
472 (message "Restricted in current function, %d matches."
473 (length iedit-occurrences-overlays)))
475 (defun iedit-restrict-region (beg end &optional inclusive)
476 "Restricting Iedit mode in a region."
477 (when iedit-buffering
478 (iedit-stop-buffering))
479 (setq iedit-last-occurrence-local (iedit-current-occurrence-string))
480 (setq mark-active nil)
481 (run-hooks 'deactivate-mark-hook)
483 (iedit-cleanup-occurrences-overlays beg end inclusive)
484 (if iedit-unmatched-lines-invisible
485 (iedit-hide-unmatched-lines iedit-occurrence-context-lines))
486 (setq iedit-mode (propertize
487 (concat " Iedit:" (number-to-string
488 (length iedit-occurrences-overlays)))
489 'face 'font-lock-warning-face))
490 (force-mode-line-update))
492 (defun iedit-toggle-case-sensitive ()
493 "Toggle case-sensitive matching occurrences.
494 Todo: how about region"
496 (setq iedit-case-sensitive (not iedit-case-sensitive))
498 (iedit-stop-buffering))
499 (setq iedit-last-occurrence-local (iedit-current-occurrence-string))
500 (when iedit-last-occurrence-local
501 (remove-overlays nil nil iedit-occurrence-overlay-name t)
503 (iedit-start2 (iedit-regexp-quote iedit-last-occurrence-local)
504 (car iedit-initial-region)
505 (cadr iedit-initial-region))))
509 ;;; iedit.el ends here
511 ;; LocalWords: iedit el MERCHANTABILITY kbd isearch todo ert Lindberg Tassilo
512 ;; LocalWords: eval defgroup defcustom boolean defvar assq alist nconc
513 ;; LocalWords: substring cadr keymap defconst purecopy bkm defun princ prev
514 ;; LocalWords: iso lefttab backtab upcase downcase concat setq autoload arg
515 ;; LocalWords: refactoring propertize cond goto nreverse progn rotatef eq elp
516 ;; LocalWords: dolist pos unmatch args ov sReplace iedit's cdr quote'ed