diff options
Diffstat (limited to 'iedit-lib.el')
-rw-r--r-- | iedit-lib.el | 948 |
1 files changed, 948 insertions, 0 deletions
diff --git a/iedit-lib.el b/iedit-lib.el new file mode 100644 index 0000000..555133e --- /dev/null +++ b/iedit-lib.el @@ -0,0 +1,948 @@ +;;; iedit-lib.el --- APIs for editing multiple regions in the same way +;;; simultaneously. + +;; Copyright (C) 2010, 2011, 2012 Victor Ren + +;; Time-stamp: <2016-06-24 14:02:51 Victor Ren> +;; Author: Victor Ren <victorhge@gmail.com> +;; Keywords: occurrence region simultaneous rectangle refactoring +;; Version: 0.9.9 +;; X-URL: http://www.emacswiki.org/emacs/Iedit +;; Compatibility: GNU Emacs: 22.x, 23.x, 24.x + +;; This file is not part of GNU Emacs, but it is distributed under +;; the same terms as GNU Emacs. + +;; GNU Emacs 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. + +;; GNU Emacs 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 GNU Emacs. If not, see <http://www.gnu.org/licenses/>. + +;;; Commentary: + +;; This package is iedit APIs library that allow you to write your own minor mode. +;; The functionalities of the APIs: +;; - Create occurrence overlays +;; - Navigate in the occurrence overlays +;; - Modify the occurrences +;; - Hide/unhide +;; - Other basic support APIs + +;;; todo: +;; - Update comments for APIs +;; - Add more easy access keys for whole occurrence + +;;; Code: + +(eval-when-compile (require 'cl)) + +(defgroup iedit nil + "Edit multiple regions in the same way simultaneously." + :prefix "iedit-" + :group 'replace + :group 'convenience) + +(defface iedit-occurrence + '((t :inherit highlight)) + "*Face used for the occurrences' default values." + :group 'iedit) + +(defface iedit-read-only-occurrence + '((t :inherit region)) + "*Face used for the read-only occurrences' default values." + :group 'iedit) + +(defcustom iedit-case-sensitive-default t + "If no-nil, matching is case sensitive." + :type 'boolean + :group 'iedit) + +(defcustom iedit-transient-mark-sensitive t + "If no-nil, Iedit mode is sensitive to the Transient Mark mode. +It means Iedit works as expected only when regions are +highlighted. If you want to use iedit without Transient Mark +mode, set it as nil." + :type 'boolean + :group 'iedit) + +(defcustom iedit-overlay-priority 200 + "The priority of the overlay used to indicate matches." + :type 'integer + :group 'iedit) + +(defvar iedit-occurrences-overlays nil + "The occurrences slot contains a list of overlays used to +indicate the position of each editable occurrence. In addition, the +occurrence overlay is used to provide a different face +configurable via `iedit-occurrence'.") + +(defvar iedit-read-only-occurrences-overlays nil + "The occurrences slot contains a list of overlays used to +indicate the position of each read-only occurrence. In addition, the +occurrence overlay is used to provide a different face +configurable via `iedit-ready-only-occurrence'.") + +(defvar iedit-case-sensitive iedit-case-sensitive-default + "This is buffer local variable. +If no-nil, matching is case sensitive.") + +(defvar iedit-unmatched-lines-invisible nil + "This is buffer local variable which indicates whether +unmatched lines are hided.") + +(defvar iedit-forward-success t + "This is buffer local variable which indicates the moving +forward or backward successful") + +(defvar iedit-before-modification-string "" + "This is buffer local variable which is the buffer substring +that is going to be changed.") + +(defvar iedit-before-modification-undo-list nil + "This is buffer local variable which is the buffer undo list before modification.") + +;; `iedit-update-occurrences' gets called twice when change==0 and +;; occurrence is zero-width (beg==end) -- for front and back insertion. +(defvar iedit-skip-modification-once t + "Variable used to skip first modification hook run when +insertion against a zero-width occurrence.") + +(defvar iedit-aborting nil + "This is buffer local variable which indicates Iedit mode is aborting.") + +(defvar iedit-aborting-hook nil + "Functions to call before iedit-abort. Normally it should be mode exit function.") + +(defvar iedit-post-undo-hook-installed nil + "This is buffer local variable which indicated if +`iedit-post-undo' is installed in `post-command-hook'.") + +(defvar iedit-buffering nil + "This is buffer local variable which indicates iedit-mode is +buffering, which means the modification to the current occurrence +is not applied to other occurrences when it is true.") + +(defvar iedit-occurrence-context-lines 1 + "The number of lines before or after the occurrence.") + +(make-variable-buffer-local 'iedit-occurrences-overlays) +(make-variable-buffer-local 'iedit-read-only-occurrences-overlays) +(make-variable-buffer-local 'iedit-unmatched-lines-invisible) +(make-local-variable 'iedit-case-sensitive) +(make-variable-buffer-local 'iedit-forward-success) +(make-variable-buffer-local 'iedit-before-modification-string) +(make-variable-buffer-local 'iedit-before-modification-undo-list) +(make-variable-buffer-local 'iedit-skip-modification-once) +(make-variable-buffer-local 'iedit-aborting) +(make-variable-buffer-local 'iedit-buffering) +(make-variable-buffer-local 'iedit-post-undo-hook-installed) +(make-variable-buffer-local 'iedit-occurrence-context-lines) + +(defconst iedit-occurrence-overlay-name 'iedit-occurrence-overlay-name) +(defconst iedit-invisible-overlay-name 'iedit-invisible-overlay-name) + +;;; Define Iedit mode map +(defvar iedit-lib-keymap + (let ((map (make-sparse-keymap))) + ;; Default key bindings + (define-key map (kbd "TAB") 'iedit-next-occurrence) + (define-key map (kbd "<tab>") 'iedit-next-occurrence) + (define-key map (kbd "<S-tab>") 'iedit-prev-occurrence) + (define-key map (kbd "<S-iso-lefttab>") 'iedit-prev-occurrence) + (define-key map (kbd "<backtab>") 'iedit-prev-occurrence) + (define-key map (kbd "C-'") 'iedit-toggle-unmatched-lines-visible) + map) + "Keymap used while Iedit mode is enabled.") + +(defvar iedit-occurrence-keymap-default + (let ((map (make-sparse-keymap))) +;; (set-keymap-parent map iedit-lib-keymap) + (define-key map (kbd "M-U") 'iedit-upcase-occurrences) + (define-key map (kbd "M-L") 'iedit-downcase-occurrences) + (define-key map (kbd "M-R") 'iedit-replace-occurrences) + (define-key map (kbd "M-SPC") 'iedit-blank-occurrences) + (define-key map (kbd "M-D") 'iedit-delete-occurrences) + (define-key map (kbd "M-N") 'iedit-number-occurrences) + (define-key map (kbd "M-B") 'iedit-toggle-buffering) + (define-key map (kbd "M-<") 'iedit-goto-first-occurrence) + (define-key map (kbd "M->") 'iedit-goto-last-occurrence) + (define-key map (kbd "C-?") 'iedit-help-for-occurrences) + (define-key map [remap keyboard-escape-quit] 'iedit-quit) + (define-key map [remap keyboard-quit] 'iedit-quit) + map) + "Default keymap used within occurrence overlays.") + +(defvar iedit-occurrence-keymap 'iedit-occurrence-keymap-default + "Keymap used within occurrence overlays. +It should be set before occurrence overlay is created.") +(make-local-variable 'iedit-occurrence-keymap) + +(defun iedit-help-for-occurrences () + "Display `iedit-occurrence-keymap-default'" + (interactive) + (message (concat (substitute-command-keys "\\[iedit-upcase-occurrences]") "/" + (substitute-command-keys "\\[iedit-downcase-occurrences]") ":up/downcase " + (substitute-command-keys "\\[iedit-replace-occurrences]") ":replace " + (substitute-command-keys "\\[iedit-blank-occurrences]") ":blank " + (substitute-command-keys "\\[iedit-delete-occurrences]") ":delete " + (substitute-command-keys "\\[iedit-number-occurrences]") ":number " + (substitute-command-keys "\\[iedit-toggle-buffering]") ":buffering " + (substitute-command-keys "\\[iedit-goto-first-occurrence]") "/" + (substitute-command-keys "\\[iedit-goto-last-occurrence]") ":first/last " + ))) + +(defun iedit-quit () + "Quit the current mode." + (interactive) + (run-hooks 'iedit-aborting-hook)) + +(defun iedit-make-markers-overlays (markers) + "Create occurrence overlays on a list of markers." + (setq iedit-occurrences-overlays + (mapcar #'(lambda (marker) + (iedit-make-occurrence-overlay (car marker) (cdr marker))) + markers))) + +(defun iedit-make-occurrences-overlays (occurrence-regexp beg end) + "Create occurrence overlays for `occurrence-regexp' in a region. +Return the number of occurrences." + (setq iedit-aborting nil) + (setq iedit-occurrences-overlays nil) + (setq iedit-read-only-occurrences-overlays nil) + ;; Find and record each occurrence's markers and add the overlay to the occurrences + (let ((counter 0) + (case-fold-search (not iedit-case-sensitive))) + (save-excursion + (save-window-excursion + (goto-char end) + ;; todo: figure out why re-search-forward is slow without "recenter" + (recenter) + (goto-char beg) + (while (re-search-forward occurrence-regexp end t) + (let ((beginning (match-beginning 0)) + (ending (match-end 0))) + (if (text-property-not-all beginning ending 'read-only nil) + (push (iedit-make-read-only-occurrence-overlay beginning ending) + iedit-read-only-occurrences-overlays) + (push (iedit-make-occurrence-overlay beginning ending) + iedit-occurrences-overlays)) + (setq counter (1+ counter)))))) + counter)) + +(defun iedit-add-next-occurrence-overlay (occurrence-exp &optional point) + "Create next occurrence overlay for `occurrence-exp'." + (iedit-add-occurrence-overlay occurrence-exp point t)) + +(defun iedit-add-previous-occurrence-overlay (occurrence-exp &optional point) + "Create previous occurrence overlay for `occurrence-exp'." + (iedit-add-occurrence-overlay occurrence-exp point nil)) + +(defun iedit-add-occurrence-overlay (occurrence-exp point forward &optional bound) + "Create next or previous occurrence overlay for `occurrence-exp'. +Return the start position of the new occurrence if successful." + (or point + (setq point (point))) + (let ((case-fold-search (not iedit-case-sensitive)) + (pos nil)) + (save-excursion + (goto-char point) + (if (not (if forward + (re-search-forward occurrence-exp bound t) + (re-search-backward occurrence-exp bound t))) + (message "No more matches.") + (setq pos (match-beginning 0)) + (if (or (iedit-find-overlay-at-point (match-beginning 0) 'iedit-occurrence-overlay-name) + (iedit-find-overlay-at-point (match-end 0) 'iedit-occurrence-overlay-name)) + (error "Conflict region")) + (push (iedit-make-occurrence-overlay (match-beginning 0) + (match-end 0)) + iedit-occurrences-overlays) + (message "Add one match for \"%s\"." (iedit-printable occurrence-exp)) + (when iedit-unmatched-lines-invisible + (iedit-show-all) + (iedit-hide-unmatched-lines iedit-occurrence-context-lines)) + )) + pos)) + +(defun iedit-add-region-as-occurrence (beg end) + "Add region as an occurrence. +The length of the region must the same as other occurrences if +there are." + (or (= beg end) + (error "No region")) + (if (null iedit-occurrences-overlays) + (push + (iedit-make-occurrence-overlay beg end) + iedit-occurrences-overlays) + (or (= (- end beg) (iedit-occurrence-string-length)) + (error "Wrong region")) + (if (or (iedit-find-overlay-at-point beg 'iedit-occurrence-overlay-name) + (iedit-find-overlay-at-point end 'iedit-occurrence-overlay-name)) + (error "Conflict region")) + (push (iedit-make-occurrence-overlay beg end) + iedit-occurrences-overlays) + )) ;; todo test this function + +(defun iedit-cleanup () + "Clean up occurrence overlay, invisible overlay and local variables." + (remove-overlays nil nil iedit-occurrence-overlay-name t) + (iedit-show-all) + (setq iedit-occurrences-overlays nil) + (setq iedit-read-only-occurrences-overlays nil) + (setq iedit-aborting nil) + (setq iedit-before-modification-string "") + (setq iedit-before-modification-undo-list nil)) + +(defun iedit-make-occurrence-overlay (begin end) + "Create an overlay for an occurrence in Iedit mode. +Add the properties for the overlay: a face used to display a +occurrence's default value, and modification hooks to update +occurrences if the user starts typing." + (let ((occurrence (make-overlay begin end (current-buffer) nil t))) + (overlay-put occurrence iedit-occurrence-overlay-name t) + (overlay-put occurrence 'face 'iedit-occurrence) + (overlay-put occurrence 'keymap iedit-occurrence-keymap) + (overlay-put occurrence 'insert-in-front-hooks '(iedit-update-occurrences)) + (overlay-put occurrence 'insert-behind-hooks '(iedit-update-occurrences)) + (overlay-put occurrence 'modification-hooks '(iedit-update-occurrences)) + (overlay-put occurrence 'priority iedit-overlay-priority) + occurrence)) + +(defun iedit-make-read-only-occurrence-overlay (begin end) + "Create an overlay for an read-only occurrence in Iedit mode." + (let ((occurrence (make-overlay begin end (current-buffer) nil t))) + (overlay-put occurrence iedit-occurrence-overlay-name t) + (overlay-put occurrence 'face 'iedit-read-only-occurrence) + occurrence)) + +(defun iedit-make-unmatched-lines-overlay (begin end) + "Create an overlay for lines between two occurrences in Iedit mode." + (let ((unmatched-lines-overlay (make-overlay begin end (current-buffer) nil t))) + (overlay-put unmatched-lines-overlay iedit-invisible-overlay-name t) + (overlay-put unmatched-lines-overlay 'invisible 'iedit-invisible-overlay-name) + ;; (overlay-put unmatched-lines-overlay 'intangible t) + unmatched-lines-overlay)) + +(defun iedit-post-undo () + "Check if it is time to abort iedit after undo command is executed. + +This is added to `post-command-hook' when undo command is executed +in occurrences." + (if (iedit-same-length) + nil + (run-hooks 'iedit-aborting-hook)) + (remove-hook 'post-command-hook 'iedit-post-undo t) + (setq iedit-post-undo-hook-installed nil)) + +(defun iedit-reset-aborting () + "Turning Iedit mode off and reset `iedit-aborting'. + +This is added to `post-command-hook' when aborting Iedit mode is +decided. `iedit-aborting-hook' is postponed after the current +command is executed for avoiding `iedit-update-occurrences' +is called for a removed overlay." + (run-hooks 'iedit-aborting-hook) + (remove-hook 'post-command-hook 'iedit-reset-aborting t) + (setq iedit-aborting nil)) + +;; There are two ways to update all occurrences. One is to redefine all key +;; stroke map for overlay, the other is to figure out three basic modification +;; in the modification hook. This function chooses the latter. +(defun iedit-update-occurrences (occurrence after beg end &optional change) + "Update all occurrences. +This modification hook is triggered when a user edits any +occurrence and is responsible for updating all other +occurrences. Refer to `modification-hooks' for more details. +Current supported edits are insertion, yank, deletion and +replacement. If this modification is going out of the +occurrence, it will abort Iedit mode." + (if undo-in-progress + ;; If the "undo" change make occurrences different, it is going to mess up + ;; occurrences. So a check will be done after undo command is executed. + (when (not iedit-post-undo-hook-installed) + (add-hook 'post-command-hook 'iedit-post-undo nil t) + (setq iedit-post-undo-hook-installed t)) + (when (not iedit-aborting) + ;; before modification + (if (null after) + (if (or (< beg (overlay-start occurrence)) + (> end (overlay-end occurrence))) + (progn (setq iedit-aborting t) ; abort iedit-mode + (add-hook 'post-command-hook 'iedit-reset-aborting nil t)) + (setq iedit-before-modification-string + (buffer-substring-no-properties beg end)) + ;; Check if this is called twice before modification. When inserting + ;; into zero-width occurrence or between two conjoined occurrences, + ;; both insert-in-front-hooks and insert-behind-hooks will be + ;; called. Two calls will make `iedit-skip-modification-once' true. + (setq iedit-skip-modification-once (not iedit-skip-modification-once))) + ;; after modification + (when (not iedit-buffering) + (if iedit-skip-modification-once + ;; Skip the first hook + (setq iedit-skip-modification-once nil) + (setq iedit-skip-modification-once t) + (when (or (eq 0 change) ;; insertion + (eq beg end) ;; deletion + (not (string= iedit-before-modification-string + (buffer-substring-no-properties beg end)))) + (iedit-update-occurrences-2 occurrence after beg end change)))))))) + +(defun iedit-update-occurrences-2 (occurrence after beg end &optional change) + "" + (let ((inhibit-modification-hooks t) + (offset (- beg (overlay-start occurrence))) + (value (buffer-substring-no-properties beg end))) + (save-excursion + ;; insertion or yank + (if (= 0 change) + (dolist (another-occurrence iedit-occurrences-overlays) + (let* ((beginning (+ (overlay-start another-occurrence) offset)) + (ending (+ beginning (- end beg)))) + (when (not (eq another-occurrence occurrence)) + (goto-char beginning) + (insert-and-inherit value) + ;; todo: reconsider this change Quick fix for + ;; multi-occur occur-edit-mode: multi-occur depend on + ;; after-change-functions to update original + ;; buffer. Since inhibit-modification-hooks is set to + ;; non-nil, after-change-functions hooks are not going + ;; to be called for the changes of other occurrences. + ;; So run the hook here. + (run-hook-with-args 'after-change-functions + beginning + ending + change)) + (iedit-move-conjoined-overlays another-occurrence))) + ;; deletion + (dolist (another-occurrence (remove occurrence iedit-occurrences-overlays)) + (let ((beginning (+ (overlay-start another-occurrence) offset))) + (delete-region beginning (+ beginning change)) + (unless (eq beg end) ;; replacement + (goto-char beginning) + (insert-and-inherit value)) + (run-hook-with-args 'after-change-functions + beginning + (+ beginning (- beg end)) + change))))))) + +(defun iedit-next-occurrence () + "Move forward to the next occurrence in the `iedit'. +If the point is already in the last occurrences, you are asked to type +another `iedit-next-occurrence', it starts again from the +beginning of the buffer." + (interactive) + (let ((pos (point)) + (in-occurrence (get-char-property (point) 'iedit-occurrence-overlay-name))) + (when in-occurrence + (setq pos (next-single-char-property-change pos 'iedit-occurrence-overlay-name))) + (setq pos (next-single-char-property-change pos 'iedit-occurrence-overlay-name)) + (if (/= pos (point-max)) + (setq iedit-forward-success t) + (if (and iedit-forward-success in-occurrence) + (progn (message "This is the last occurrence.") + (setq iedit-forward-success nil)) + (progn + (if (get-char-property (point-min) 'iedit-occurrence-overlay-name) + (setq pos (point-min)) + (setq pos (next-single-char-property-change + (point-min) + 'iedit-occurrence-overlay-name))) + (setq iedit-forward-success t) + (message "Located the first occurrence.")))) + (when iedit-forward-success + (goto-char pos)))) + +(defun iedit-prev-occurrence () + "Move backward to the previous occurrence in the `iedit'. +If the point is already in the first occurrences, you are asked to type +another `iedit-prev-occurrence', it starts again from the end of +the buffer." + (interactive) + (let ((pos (point)) + (in-occurrence (get-char-property (point) 'iedit-occurrence-overlay-name))) + (when in-occurrence + (setq pos (previous-single-char-property-change pos 'iedit-occurrence-overlay-name))) + (setq pos (previous-single-char-property-change pos 'iedit-occurrence-overlay-name)) + ;; At the start of the first occurrence + (if (or (and (eq pos (point-min)) + (not (get-char-property (point-min) 'iedit-occurrence-overlay-name))) + (and (eq (point) (point-min)) + in-occurrence)) + (if (and iedit-forward-success in-occurrence) + (progn (message "This is the first occurrence.") + (setq iedit-forward-success nil)) + (progn + (setq pos (previous-single-char-property-change (point-max) 'iedit-occurrence-overlay-name)) + (if (not (get-char-property (- (point-max) 1) 'iedit-occurrence-overlay-name)) + (setq pos (previous-single-char-property-change pos 'iedit-occurrence-overlay-name))) + (setq iedit-forward-success t) + (message "Located the last occurrence."))) + (setq iedit-forward-success t)) + (when iedit-forward-success + (goto-char pos)))) + +(defun iedit-goto-first-occurrence () + "Move to the first occurrence." + (interactive) + (goto-char (iedit-first-occurrence)) + (setq iedit-forward-success t) + (message "Located the first occurrence.")) + +(defun iedit-first-occurrence () + "return the position of the first occurrence." + (if (get-char-property (point-min) 'iedit-occurrence-overlay-name) + (point-min) + (next-single-char-property-change + (point-min) 'iedit-occurrence-overlay-name))) + +(defun iedit-goto-last-occurrence () + "Move to the last occurrence." + (interactive) + (goto-char (iedit-last-occurrence)) + (setq iedit-forward-success t) + (message "Located the last occurrence.")) + +(defun iedit-last-occurrence () + "return the position of the last occurrence." + (let ((pos (previous-single-char-property-change (point-max) 'iedit-occurrence-overlay-name))) + (if (not (get-char-property (- (point-max) 1) 'iedit-occurrence-overlay-name)) + (setq pos (previous-single-char-property-change pos 'iedit-occurrence-overlay-name))) + pos)) + +(defun iedit-toggle-unmatched-lines-visible (&optional arg) + "Toggle whether to display unmatched lines. +A prefix ARG specifies how many lines before and after the +occurrences are not hided; negative is treated the same as zero. + +If no prefix argument, the prefix argument last time or default +value of `iedit-occurrence-context-lines' is used for this time." + (interactive "P") + (if (null arg) + ;; toggle visible + (progn (setq iedit-unmatched-lines-invisible (not iedit-unmatched-lines-invisible)) + (if iedit-unmatched-lines-invisible + (iedit-hide-unmatched-lines iedit-occurrence-context-lines) + (iedit-show-all))) + ;; reset invisible lines + (setq arg (prefix-numeric-value arg)) + (if (< arg 0) + (setq arg 0)) + (unless (and iedit-unmatched-lines-invisible + (= arg iedit-occurrence-context-lines)) + (when iedit-unmatched-lines-invisible + (remove-overlays nil nil iedit-invisible-overlay-name t)) + (setq iedit-occurrence-context-lines arg) + (setq iedit-unmatched-lines-invisible t) + (iedit-hide-unmatched-lines iedit-occurrence-context-lines)))) + +(defun iedit-show-all() + "Show hided lines." + (setq line-move-ignore-invisible nil) + (remove-from-invisibility-spec '(iedit-invisible-overlay-name . t)) + (remove-overlays nil nil iedit-invisible-overlay-name t)) + +(defun iedit-hide-unmatched-lines (context-lines) + "Hide unmatched lines using invisible overlay." + (let ((prev-occurrence-end 1) + (unmatched-lines nil)) + (save-excursion + (goto-char (iedit-first-occurrence)) + (while (/= (point) (point-max)) + ;; Now at the beginning of an occurrence + (let ((current-start (point))) + (forward-line (- context-lines)) + (let ((line-beginning (line-beginning-position))) + (if (> line-beginning prev-occurrence-end) + (push (list prev-occurrence-end (1- line-beginning)) unmatched-lines))) + ;; goto the end of the occurrence + (goto-char (next-single-char-property-change current-start 'iedit-occurrence-overlay-name))) + (let ((current-end (point))) + (forward-line context-lines) + (setq prev-occurrence-end (1+ (line-end-position))) + ;; goto the beginning of next occurrence + (goto-char (next-single-char-property-change current-end 'iedit-occurrence-overlay-name)))) + (if (< prev-occurrence-end (point-max)) + (push (list prev-occurrence-end (point-max)) unmatched-lines)) + (when unmatched-lines + (set (make-local-variable 'line-move-ignore-invisible) t) + (add-to-invisibility-spec '(iedit-invisible-overlay-name . t)) + (dolist (unmatch unmatched-lines) + (iedit-make-unmatched-lines-overlay (car unmatch) (cadr unmatch))))) + unmatched-lines)) + +;;;; functions for overlay keymap +(defun iedit-apply-on-occurrences (function &rest args) + "Call function for each occurrence." + (let ((inhibit-modification-hooks t)) + (save-excursion + (dolist (occurrence iedit-occurrences-overlays) + (apply function (overlay-start occurrence) (overlay-end occurrence) args))))) + +(defun iedit-upcase-occurrences () + "Covert occurrences to upper case." + (interactive "*") + (iedit-barf-if-buffering) + (iedit-apply-on-occurrences 'upcase-region)) + +(when (require 'multiple-cursors-core nil t) + (defun iedit-switch-to-mc-mode () + "Switch to multiple-cursors-mode. So that you can navigate +out of the occurrence and edit simutaneously with multiple +cursors." + (interactive "*") + (iedit-barf-if-buffering) + (let* ((ov (iedit-find-current-occurrence-overlay)) + (offset (- (point) (overlay-start ov))) + (master (point))) + (mc/save-excursion + (dolist (occurrence iedit-occurrences-overlays) + (goto-char (+ (overlay-start occurrence) offset)) + (unless (= master (point)) + (mc/create-fake-cursor-at-point)) + )) + (multiple-cursors-mode 1) + (run-hooks 'iedit-aborting-hook))) + + (define-key iedit-occurrence-keymap-default (kbd "M-M") 'iedit-switch-to-mc-mode)) + +(defun iedit-downcase-occurrences() + "Covert occurrences to lower case." + (interactive "*") + (iedit-barf-if-buffering) + (iedit-apply-on-occurrences 'downcase-region)) + +(defun iedit-replace-occurrences(&optional to-string) + "Replace occurrences with STRING. +This function preserves case." + (interactive "*") + (iedit-barf-if-buffering) + (let* ((ov (iedit-find-current-occurrence-overlay)) + (offset (- (point) (overlay-start ov))) + (from-string (buffer-substring-no-properties + (overlay-start ov) + (overlay-end ov))) + (from-string-lowercase (downcase from-string)) + (to-string (if (not to-string) + (read-string "Replace with: " + nil nil + from-string + nil) + to-string))) + (iedit-apply-on-occurrences + (lambda (beg end from-string-lowercase to-string) + (goto-char beg) + (search-forward from-string-lowercase end) + (replace-match to-string nil)) + from-string-lowercase to-string) + (goto-char (+ (overlay-start ov) offset)))) + +(defun iedit-blank-occurrences() + "Replace occurrences with blank spaces." + (interactive "*") + (iedit-barf-if-buffering) + (let* ((ov (car iedit-occurrences-overlays)) + (offset (- (point) (overlay-start ov))) + (count (- (overlay-end ov) (overlay-start ov)))) + (iedit-apply-on-occurrences + (lambda (beg end ) + (delete-region beg end) + (goto-char beg) + (insert-and-inherit (make-string count 32)))) + (goto-char (+ (overlay-start ov) offset)))) + +(defun iedit-delete-occurrences() + "Delete occurrences." + (interactive "*") + (iedit-barf-if-buffering) + (iedit-apply-on-occurrences 'delete-region)) + +;; todo: add cancel buffering function +(defun iedit-toggle-buffering () + "Toggle buffering. +This is intended to improve iedit's response time. If the number +of occurrences are huge, it might be slow to update all the +occurrences for each key stoke. When buffering is on, +modification is only applied to the current occurrence and will +be applied to other occurrences when buffering is off." + (interactive "*") + (if iedit-buffering + (iedit-stop-buffering) + (iedit-start-buffering)) + (message (concat "Modification Buffering " + (if iedit-buffering + "started." + "stopped.")))) + +(defun iedit-start-buffering () + "Start buffering." + (setq iedit-buffering t) + (setq iedit-before-modification-string (iedit-current-occurrence-string)) + (setq iedit-before-modification-undo-list buffer-undo-list) + (message "Start buffering editing...")) + +(defun iedit-stop-buffering () + "Stop buffering and apply the modification to other occurrences. +If current point is not at any occurrence, the buffered +modification is not going to be applied to other occurrences." + (let ((ov (iedit-find-current-occurrence-overlay))) + (when ov + (let* ((beg (overlay-start ov)) + (end (overlay-end ov)) + (modified-string (buffer-substring-no-properties beg end)) + (offset (- (point) beg)) ;; delete-region moves cursor + (inhibit-modification-hooks t)) + (when (not (string= iedit-before-modification-string modified-string)) + (save-excursion + ;; Rollback the current modification and buffer-undo-list. This is + ;; to avoid the inconsistency if user undoes modifications + (delete-region beg end) + (goto-char beg) + (insert-and-inherit iedit-before-modification-string) + (setq buffer-undo-list iedit-before-modification-undo-list) + (dolist (occurrence iedit-occurrences-overlays) ; todo:extract as a function + (let ((beginning (overlay-start occurrence)) + (ending (overlay-end occurrence))) + (delete-region beginning ending) + (unless (eq beg end) ;; replacement + (goto-char beginning) + (insert-and-inherit modified-string)) + (iedit-move-conjoined-overlays occurrence)))) + (goto-char (+ (overlay-start ov) offset)))))) + (setq iedit-buffering nil) + (message "Buffered modification applied.") + (setq iedit-before-modification-undo-list nil)) + +(defun iedit-move-conjoined-overlays (occurrence) + "This function keeps overlays conjoined after modification. +After modification, conjoined overlays may be overlapped." + (let ((beginning (overlay-start occurrence)) + (ending (overlay-end occurrence))) + (unless (= beginning (point-min)) + (let ((previous-overlay (iedit-find-overlay-at-point + (1- beginning) + 'iedit-occurrence-overlay-name))) + (if previous-overlay ; two conjoined occurrences + (move-overlay previous-overlay + (overlay-start previous-overlay) + beginning)))) + (unless (= ending (point-max)) + (let ((next-overlay (iedit-find-overlay-at-point + ending + 'iedit-occurrence-overlay-name))) + (if next-overlay ; two conjoined occurrences + (move-overlay next-overlay ending (overlay-end next-overlay))))))) + +(defvar iedit-number-line-counter 1 + "Occurrence number for 'iedit-number-occurrences.") + +(defun iedit-default-occurrence-number-format (start-at) + (concat "%" + (int-to-string + (length (int-to-string + (1- (+ (length iedit-occurrences-overlays) start-at))))) + "d ")) + +(defun iedit-number-occurrences (start-at &optional format-string) + "Insert numbers in front of the occurrences. +START-AT, if non-nil, should be a number from which to begin +counting. FORMAT, if non-nil, should be a format string to pass +to `format-string' along with the line count. When called +interactively with a prefix argument, prompt for START-AT and +FORMAT." + (interactive + (if current-prefix-arg + (let* ((start-at (read-number "Number to count from: " 1))) + (list start-at + (read-string "Format string: " + (iedit-default-occurrence-number-format + start-at)))) + (list 1 nil))) + (iedit-barf-if-buffering) + (unless format-string + (setq format-string (iedit-default-occurrence-number-format start-at))) + (let ((iedit-number-occurrence-counter start-at) + (inhibit-modification-hooks t)) + (save-excursion + (goto-char (iedit-first-occurrence)) + (while (/= (point) (point-max)) + (insert (format format-string iedit-number-occurrence-counter)) + (iedit-move-conjoined-overlays (iedit-find-current-occurrence-overlay)) + (setq iedit-number-occurrence-counter + (1+ iedit-number-occurrence-counter)) + (goto-char (next-single-char-property-change (point) 'iedit-occurrence-overlay-name)) + (goto-char (next-single-char-property-change (point) 'iedit-occurrence-overlay-name)))))) + + +;;; help functions +(defun iedit-find-current-occurrence-overlay () + "Return the current occurrence overlay at point or point - 1. +This function is supposed to be called in overlay keymap." + (or (iedit-find-overlay-at-point (point) 'iedit-occurrence-overlay-name) + (iedit-find-overlay-at-point (1- (point)) 'iedit-occurrence-overlay-name))) + +(defun iedit-find-overlay-at-point (point property) + "Return the overlay with PROPERTY at POINT." + (let ((overlays (overlays-at point)) + found) + (while (and overlays (not found)) + (let ((overlay (car overlays))) + (if (overlay-get overlay property) + (setq found overlay) + (setq overlays (cdr overlays))))) + found)) + +(defun iedit-same-column () + "Return t if all occurrences are at the same column." + (save-excursion + (let ((column (progn (goto-char (overlay-start (car iedit-occurrences-overlays))) + (current-column))) + (overlays (cdr iedit-occurrences-overlays)) + (same t)) + (while (and overlays same) + (let ((overlay (car overlays))) + (if (/= (progn (goto-char (overlay-start overlay)) + (current-column)) + column) + (setq same nil) + (setq overlays (cdr overlays))))) + same))) + +(defun iedit-same-length () + "Return t if all occurrences are the same length." + (save-excursion + (let ((length (iedit-occurrence-string-length)) + (overlays (cdr iedit-occurrences-overlays)) + (same t)) + (while (and overlays same) + (let ((ov (car overlays))) + (if (/= (- (overlay-end ov) (overlay-start ov)) + length) + (setq same nil) + (setq overlays (cdr overlays))))) + same))) + +;; This function might be called out of any occurrence +(defun iedit-current-occurrence-string () + "Return current occurrence string. +Return nil if occurrence string is empty string." + (let ((ov (or (iedit-find-current-occurrence-overlay) + (car iedit-occurrences-overlays)))) + (if ov + (let ((beg (overlay-start ov)) + (end (overlay-end ov))) + (if (/= beg end) + (buffer-substring-no-properties beg end) + nil)) + nil))) + +(defun iedit-occurrence-string-length () + "Return the length of current occurrence string." + (let ((ov (car iedit-occurrences-overlays))) + (- (overlay-end ov) (overlay-start ov)))) + +(defun iedit-find-overlay (beg end property &optional exclusive) + "Return a overlay with property in region, or out of the region if EXCLUSIVE is not nil." + (if exclusive + (or (iedit-find-overlay-in-region (point-min) beg property) + (iedit-find-overlay-in-region end (point-max) property)) + (iedit-find-overlay-in-region beg end property))) + +(defun iedit-find-overlay-in-region (beg end property) + "Return a overlay with property in region." + (let ((overlays (overlays-in beg end)) + found) + (while (and overlays (not found)) + (let ((overlay (car overlays))) + (if (and (overlay-get overlay property) + (>= (overlay-start overlay) beg) + (<= (overlay-end overlay) end)) + (setq found overlay) + (setq overlays (cdr overlays))))) + found)) + +(defun iedit-cleanup-occurrences-overlays (beg end &optional inclusive) + "Remove deleted overlays from list `iedit-occurrences-overlays'." + (if inclusive + (remove-overlays beg end iedit-occurrence-overlay-name t) + (remove-overlays (point-min) beg iedit-occurrence-overlay-name t) + (remove-overlays end (point-max) iedit-occurrence-overlay-name t)) + (let (overlays) + (dolist (overlay iedit-occurrences-overlays) + (if (overlay-buffer overlay) + (push overlay overlays))) + (setq iedit-occurrences-overlays overlays))) + +(defun iedit-printable (string) + "Return a omitted substring that is not longer than 50. +STRING is already `regexp-quote'ed" + (let ((first-newline-index (string-match "$" string)) + (length (length string))) + (if (and first-newline-index + (/= first-newline-index length)) + (if (< first-newline-index 50) + (concat (substring string 0 first-newline-index) "...") + (concat (substring string 0 50) "...")) + (if (> length 50) + (concat (substring string 0 50) "...") + string)))) + +(defun iedit-char-at-bol (&optional N) + "Get char position of the beginning of the current line. If `N' +is given, move forward (or backward) that many lines (using +`forward-line') and get the char position at the beginning of +that line." + (save-excursion + (forward-line (if N N 0)) + (point))) + +(defun iedit-char-at-eol (&optional N) + "Get char position of the end of the current line. If `N' is +given, move forward (or backward) that many lines (using +`forward-line') and get the char position at the end of that +line." + (save-excursion + (forward-line (if N N 0)) + (end-of-line) + (point))) + +(defun iedit-region-active () + "Return t if region is active and not empty. +If variable `iedit-transient-mark-sensitive' is t, active region +means `transient-mark-mode' is on and mark is active. Otherwise, +it just means mark is active." + (and (if iedit-transient-mark-sensitive + transient-mark-mode + t) + mark-active + (not (equal (mark) (point))))) + +(defun iedit-barf-if-lib-active() + "Signal error if Iedit lib is active." + (or (and (null iedit-occurrences-overlays) + (null iedit-read-only-occurrences-overlays)) + (error "Iedit lib is active"))) + +(defun iedit-barf-if-buffering() + "Signal error if Iedit lib is buffering." + (or (null iedit-buffering) + (error "Iedit is buffering"))) + +(provide 'iedit-lib) + +;;; iedit-lib.el ends here + +;; LocalWords: iedit el MERCHANTABILITY kbd isearch todo ert Lindberg Tassilo +;; LocalWords: eval rect defgroup defcustom boolean defvar assq alist nconc +;; LocalWords: substring cadr keymap defconst purecopy bkm defun princ prev +;; LocalWords: iso lefttab backtab upcase downcase concat setq autoload arg +;; LocalWords: refactoring propertize cond goto nreverse progn rotatef eq elp +;; LocalWords: dolist pos unmatch args ov sReplace iedit's cdr quote'ed |