├── .gitignore ├── Cask ├── .travis.yml ├── README.org ├── plur-test.el └── plur.el /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | /.cask 3 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package-file "plur.el") 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: generic 2 | sudo: false 3 | 4 | env: 5 | matrix: 6 | - EVM_EMACS=emacs-24.4-travis 7 | - EVM_EMACS=emacs-24.5-travis 8 | - EVM_EMACS=emacs-git-snapshot-travis 9 | 10 | before_install: 11 | - git clone https://github.com/rejeep/evm.git /home/travis/.evm 12 | - export PATH="/home/travis/.evm/bin:$PATH" 13 | - evm config path /tmp 14 | - evm install --use $EVM_EMACS 15 | 16 | script: 17 | - emacs --version 18 | - emacs --batch --funcall batch-byte-compile plur.el 19 | - emacs --batch -L . --load ert --load plur-test --funcall ert-run-tests-batch-and-exit 20 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * Plur 2 | [[https://travis-ci.org/xuchunyang/plur][file:https://travis-ci.org/xuchunyang/plur.svg?branch=master]] [[https://melpa.org/#/plur][file:https://melpa.org/packages/plur-badge.svg]] 3 | 4 | This package introduces a new syntax =...{subexp1,subexp2,...}...= to search and replace a 5 | group of words. Three commands are provided by this package: 6 | 7 | - ~plur-isearch-forward~ 8 | - ~plur-query-replace~ 9 | - ~plur-replace~ 10 | 11 | ** Replace example 12 | 13 | To replace "mouse" with "cat" and "mice" with "cats" using: 14 | 15 | #+BEGIN_SRC undefined 16 | M-x plur-query-replace RET m{ouse,ice} RET cat{,s} RET 17 | #+END_SRC 18 | 19 | For more examples, 20 | 21 | - Facility to Building 22 | 23 | facilit{y,ies} building{,s} 24 | 25 | - Mouse to Trackpad 26 | 27 | m{ouse,ice} trackpad{,s} 28 | 29 | - Swap Emacs and Vim 30 | 31 | {emacs,vim} {vim,emacs} 32 | 33 | ** Search example 34 | 35 | To search "mouse" and "mice" using: 36 | 37 | #+BEGIN_SRC undefined 38 | M-x plur-isearch-forward RET m{ouse,ice} 39 | #+END_SRC 40 | 41 | ** Requirements 42 | 43 | - Emacs 24.4 or higher 44 | 45 | ** Installation 46 | 47 | *** MELPA 48 | 49 | Plur is available from [[https://melpa.org][Melpa]]. You can install it using: 50 | 51 | #+BEGIN_SRC undefined 52 | M-x package-install RET plur RET 53 | #+END_SRC 54 | 55 | *** Manually 56 | 57 | Make sure plur.el is saved in a directory in you ~load-path~ and load it. Add something 58 | like 59 | 60 | #+BEGIN_SRC emacs-lisp 61 | (add-to-list 'load-path "path/to/plur/") 62 | (require 'plur) 63 | #+END_SRC 64 | 65 | to your init file. 66 | 67 | ** Acknowledge 68 | 69 | This package is inspired by [[https://github.com/tpope/vim-abolish][vim-abolish]]. 70 | 71 | # Local Variables: 72 | # fill-column: 90 73 | # End: 74 | -------------------------------------------------------------------------------- /plur-test.el: -------------------------------------------------------------------------------- 1 | ;;; plur-test.el --- Tests of plur.el -*- lexical-binding: t; -*- 2 | 3 | (require 'plur) 4 | 5 | (ert-deftest plur-split-string-test () 6 | (should (equal (plur-split-string "pre-") '("pre-"))) 7 | (should (equal (plur-split-string "{AAA,BBB}") '(("AAA,BBB")))) 8 | (should (equal (plur-split-string "pre-{AAA,BBB}") '("pre-" ("AAA,BBB")))) 9 | (should (equal (plur-split-string "pre-{AAA,BBB}-ing") '("pre-" ("AAA,BBB") "-ing"))) 10 | (should (equal (plur-split-string "beg-{A,B}-mid-{C,D}-end") '("beg-" ("A,B") "-mid-" ("C,D") "-end")))) 11 | 12 | (ert-deftest plur-build-regexp-test () 13 | (should (string= (plur-build-regexp "child{,ren}") (rx (and "child" (or "" "ren"))))) 14 | (should (string= (plur-build-regexp "beg-{A,B}-mid-{C,D}-end") (rx (and "beg-" (or "A" "B") "-mid-" (or "C" "D") "-end")))) 15 | (should (string= (plur-build-regexp "{emacs,vim}") (rx (or "emacs" "vim"))))) 16 | 17 | (ert-deftest plur-replace-test () 18 | (cl-flet ((do-replace (text from-string to-string) 19 | (with-temp-buffer 20 | (insert text) 21 | (plur-replace from-string to-string nil 1 (point-max)) 22 | (buffer-string)))) 23 | (should (string= "The plural form of adult is adults" 24 | (do-replace "The plural form of child is children" "child{,ren}" "adult{,s}"))) 25 | ;; Case 26 | (should (string= "The plural form of Adult is ADULTS" 27 | (do-replace "The plural form of Child is CHILDREN" "child{,ren}" "adult{,s}"))) 28 | ;; Swap 29 | (should (string= "vim and emacs" 30 | (do-replace "emacs and vim" "{emacs,vim}" "{vim,emacs}"))))) 31 | 32 | ;;; plur-test.el ends here 33 | -------------------------------------------------------------------------------- /plur.el: -------------------------------------------------------------------------------- 1 | ;;; plur.el --- Easily search and replace multiple variants of a word -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016 Chunyang Xu 4 | 5 | ;; Author: Chunyang Xu 6 | ;; URL: https://github.com/xuchunyang/plur 7 | ;; Version: 0.1 8 | ;; Package-Requires: ((emacs "24.4")) 9 | 10 | ;; This program is free software; you can redistribute it and/or modify 11 | ;; it under the terms of the GNU General Public License as published by 12 | ;; the Free Software Foundation, either version 3 of the License, or 13 | ;; (at your option) any later version. 14 | 15 | ;; This program is distributed in the hope that it will be useful, 16 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 17 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 | ;; GNU General Public License for more details. 19 | 20 | ;; You should have received a copy of the GNU General Public License 21 | ;; along with this program. If not, see . 22 | 23 | ;;; Commentary: 24 | ;; * Plur 25 | 26 | ;; This package introduces a new syntax =...{subexp1,subexp2,...}...= to search and 27 | ;; replace a group of words. Three commands are provided by this package: 28 | ;; 29 | ;; - ~plur-isearch-forward~ 30 | ;; - ~plur-query-replace~ 31 | ;; - ~plur-replace~ 32 | ;; 33 | ;; ** Replace example 34 | ;; 35 | ;; To replace "mouse" with "cat" and "mice" with "cats" using: 36 | ;; 37 | ;; #+BEGIN_SRC undefined 38 | ;; M-x plur-query-replace RET m{ouse,ice} RET cat{,s} RET 39 | ;; #+END_SRC 40 | ;; 41 | ;; For more examples, 42 | ;; 43 | ;; - Facility to Building 44 | ;; 45 | ;; facilit{y,ies} building{,s} 46 | ;; 47 | ;; - Mouse to Trackpad 48 | ;; 49 | ;; m{ouse,ice} trackpad{,s} 50 | ;; 51 | ;; - Swap Emacs and Vim 52 | ;; 53 | ;; {emacs,vim} {vim,emacs} 54 | ;; 55 | ;; ** Search example 56 | ;; 57 | ;; To search "mouse" and "mice" using: 58 | ;; 59 | ;; #+BEGIN_SRC undefined 60 | ;; M-x plur-isearch-forward RET m{ouse,ice} 61 | ;; #+END_SRC 62 | ;; 63 | ;; ** Requirements 64 | ;; 65 | ;; - Emacs 24.4 or higher 66 | ;; 67 | ;; ** Installation 68 | ;; 69 | ;; *** MELPA 70 | ;; 71 | ;; Plur is available from [[https://melpa.org][Melpa]]. You can install it using: 72 | ;; 73 | ;; #+BEGIN_SRC undefined 74 | ;; M-x package-install RET plur RET 75 | ;; #+END_SRC 76 | ;; 77 | ;; *** Manually 78 | ;; 79 | ;; Make sure plur.el is saved in a directory in you ~load-path~ and load it. Add something 80 | ;; like 81 | ;; 82 | ;; #+BEGIN_SRC emacs-lisp 83 | ;; (add-to-list 'load-path "path/to/plur/") 84 | ;; (require 'plur) 85 | ;; #+END_SRC 86 | ;; 87 | ;; to your init file. 88 | ;; 89 | ;; ** Acknowledge 90 | ;; 91 | ;; This package is inspired by [[https://github.com/tpope/vim-abolish][vim-abolish]]. 92 | 93 | ;;; Code: 94 | 95 | (require 'cl-lib) 96 | 97 | (defun plur-split-string (s) 98 | ;; "m{ice,ouse}" => ("m" ("ice,ouse")) 99 | (let ((start 0) strings) 100 | (while (string-match "{\\([^{}]*\\)}" s start) 101 | (let ((prefix (substring s start (match-beginning 0)))) 102 | (unless (string= "" prefix) 103 | (push prefix strings))) 104 | (push (list (match-string 1 s)) strings) 105 | (setq start (match-end 0))) 106 | (let ((tail (substring s start))) 107 | (unless (string= "" tail) 108 | (push tail strings))) 109 | (nreverse strings))) 110 | 111 | (defun plur-build-rx-form (strings) 112 | (let ((form '(and))) 113 | (dolist (item strings form) 114 | (setq form 115 | (append form (if (stringp item) 116 | (list item) 117 | (list (append '(or) (split-string (car item) ","))))))))) 118 | 119 | (defun plur-build-regexp (string) 120 | (rx-to-string 121 | (plur-build-rx-form 122 | (plur-split-string string)) 'no-group)) 123 | 124 | (defun plur-isearch-regexp (string &optional _lax) 125 | (plur-build-regexp string)) 126 | 127 | (put 'plur-isearch-regexp 'isearch-message-prefix "plur ") 128 | 129 | ;;;###autoload 130 | (defun plur-isearch-forward (&optional _not-plur no-recursive-edit) 131 | (interactive "P\np") 132 | (isearch-mode t nil nil (not no-recursive-edit) 'plur-isearch-regexp)) 133 | 134 | ;; This autoload cookie is just for package.el user who wants bind some key to this 135 | ;; command on `isearch-mode-map' in h{is,er} init file without requiring this feature. 136 | ;;;###autoload 137 | (defun plur-isearch-query-replace (&optional arg) 138 | "Start `plur-query-replace' from `plur-isearch-forward'." 139 | (interactive 140 | (list current-prefix-arg)) 141 | (barf-if-buffer-read-only) 142 | (let ((search-upper-case nil) 143 | (search-invisible isearch-invisible) 144 | (backward (and arg (eq arg '-))) 145 | (isearch-recursive-edit nil) 146 | (from-string isearch-string) 147 | to-string) 148 | (isearch-done nil t) 149 | (isearch-clean-overlays) 150 | (if (and isearch-other-end 151 | (if backward 152 | (> isearch-other-end (point)) 153 | (< isearch-other-end (point))) 154 | (not (and transient-mark-mode mark-active 155 | (if backward 156 | (> (mark) (point)) 157 | (< (mark) (point)))))) 158 | (goto-char isearch-other-end)) 159 | (set query-replace-from-history-variable 160 | (cons isearch-string 161 | (symbol-value query-replace-from-history-variable))) 162 | (setq to-string 163 | (query-replace-read-to 164 | from-string 165 | (concat "Query replace" 166 | (isearch--describe-regexp-mode isearch-regexp-function t) 167 | (if (and transient-mark-mode mark-active) " in region" "")) 168 | t)) 169 | (cl-destructuring-bind (from-string to-string) (plur-replace-subr from-string to-string) 170 | (perform-replace from-string to-string t t nil nil nil 171 | (if (and transient-mark-mode mark-active) (region-beginning)) 172 | (if (and transient-mark-mode mark-active) (region-end)))) 173 | (and isearch-recursive-edit (exit-recursive-edit)))) 174 | 175 | (defun plur-normalize-strings (strings) 176 | ;; ("m" ("ice,ouse") => (("m") ("ice" "ouse")) 177 | (let (result) 178 | (dolist (elt strings) 179 | (if (stringp elt) 180 | (push (list elt) result) 181 | (push (split-string (car elt) ",") result))) 182 | (nreverse result))) 183 | 184 | (defun plur-expand-string (string) 185 | ;; facilit{y,ies} => ("facility" "facilities") 186 | (let ((strings (plur-normalize-strings 187 | (plur-split-string string))) 188 | (results '("")) aux) 189 | (dolist (elt strings results) ; List 190 | (setq aux nil) 191 | (dolist (elt1 elt) ; String 192 | (dolist (prefix results) ; String 193 | (push (concat prefix elt1) aux))) 194 | (setq results (nreverse aux))))) 195 | 196 | (defun plur-string-all-upper-case-p (string) 197 | (string= string (upcase string))) 198 | 199 | (defun plur-string-all-lower-case-p (string) 200 | (string= string (downcase string))) 201 | 202 | (defun plur-string-capitalized-p (string) 203 | "Return non-nil if the first letter of STRING is upper case." 204 | (plur-string-all-upper-case-p (substring string 0 1))) 205 | 206 | (defun plur-string-preserve-upper-case (s1 s2) 207 | "Preserve upper case in S1 and S2. 208 | S1 and S1 are same string but in different case. 209 | For example, \"Foobar\", \"fooBar\" => \"FooBar\"." 210 | (apply 'string 211 | (cl-mapcar (lambda (c1 c2) 212 | (if (and (= (downcase c1) c1) 213 | (= (downcase c1) c2)) 214 | c1 215 | (upcase c1))) 216 | s1 s2))) 217 | 218 | (defun plur-replace-find-match (matches from-string) 219 | "Return a function as the cdr of replacement for `perform-replace'." 220 | (lambda (_data _count) 221 | ;; Simulate `case-replace' to preserve case in replacement 222 | ;; See (info "(emacs) Replacement and Lax Matches") 223 | (let* ((search (match-string 0)) 224 | (replacement 225 | (cdr (assoc (downcase search) matches)))) 226 | (if (or (not (plur-string-all-lower-case-p from-string)) ; if search contains any 227 | case-replace ; upper-case letter, 228 | case-fold-search) ; don't change case 229 | replacement 230 | (plur-string-preserve-upper-case 231 | (cond 232 | ((plur-string-all-lower-case-p search) (downcase replacement)) 233 | ((plur-string-all-upper-case-p search) (upcase replacement)) 234 | ((plur-string-capitalized-p search) (capitalize replacement)) 235 | (t replacement)) 236 | replacement))))) 237 | 238 | (defun plur-replace-subr (from-string to-string) 239 | "Return a list contains search and replacement for `perform-replace'." 240 | (let ((matches 241 | (cl-mapcar 'cons 242 | (mapcar 'downcase (plur-expand-string from-string)) 243 | (plur-expand-string to-string)))) 244 | (setq to-string 245 | (cons (plur-replace-find-match matches from-string) nil))) 246 | (setq from-string (plur-build-regexp from-string)) 247 | (list from-string to-string)) 248 | 249 | ;;;###autoload 250 | (defun plur-query-replace (from-string to-string &optional delimited start end backward) 251 | (interactive 252 | (let ((common 253 | (query-replace-read-args 254 | (concat "Plur Query replace" 255 | (if current-prefix-arg 256 | (if (eq current-prefix-arg '-) " backward" " word") 257 | "") 258 | (if (use-region-p) " in region" "")) 259 | nil))) 260 | (list (nth 0 common) (nth 1 common) (nth 2 common) 261 | (if (use-region-p) (region-beginning)) 262 | (if (use-region-p) (region-end)) 263 | (nth 3 common)))) 264 | (cl-destructuring-bind (from-string to-string) (plur-replace-subr from-string to-string) 265 | (perform-replace from-string to-string t t delimited nil nil start end backward))) 266 | 267 | ;;;###autoload 268 | (defun plur-replace (from-string to-string &optional delimited start end backward) 269 | (interactive 270 | (let ((common 271 | (query-replace-read-args 272 | (concat "Plur Replace" 273 | (if current-prefix-arg 274 | (if (eq current-prefix-arg '-) " backward" " word") 275 | "") 276 | (if (use-region-p) " in region" "")) 277 | t))) 278 | (list (nth 0 common) (nth 1 common) (nth 2 common) 279 | (if (use-region-p) (region-beginning)) 280 | (if (use-region-p) (region-end)) 281 | (nth 3 common)))) 282 | (cl-destructuring-bind (from-string to-string) (plur-replace-subr from-string to-string) 283 | (perform-replace from-string to-string nil t delimited nil nil start end backward))) 284 | 285 | 286 | (provide 'plur) 287 | 288 | ;; Local Variables: 289 | ;; fill-column: 90 290 | ;; End: 291 | 292 | ;;; plur.el ends here 293 | --------------------------------------------------------------------------------