├── .github └── workflows │ └── test.yml ├── .gitignore ├── Cask ├── Changes ├── Makefile ├── README.md ├── anzu.el └── image ├── anzu-any-position.png ├── anzu-replace-demo-noquery.gif ├── anzu-replace-demo.gif ├── anzu-threshold.png └── anzu.gif /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: ci-checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-20.04 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | emacs_version: [25.3, 26.3, 27.2] 18 | allow_failure: [false] 19 | include: 20 | - emacs_version: snapshot 21 | allow_failure: true 22 | continue-on-error: ${{ matrix.allow_failure }} 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - uses: actions/setup-python@v2 27 | - uses: purcell/setup-emacs@master 28 | with: 29 | version: ${{ matrix.emacs_version }} 30 | - uses: conao3/setup-cask@master 31 | 32 | - name: Log Emacs version 33 | run: 'make version' 34 | 35 | - name: Run lint check 36 | run: 'make lint' 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.cask/ 2 | *.elc 3 | *-autoloads.* 4 | -------------------------------------------------------------------------------- /Cask: -------------------------------------------------------------------------------- 1 | (source gnu) 2 | (source melpa) 3 | 4 | (package-file "anzu.el") 5 | 6 | (development 7 | (depends-on "elisp-lint") 8 | (depends-on "powerline") 9 | (depends-on "migemo")) 10 | -------------------------------------------------------------------------------- /Changes: -------------------------------------------------------------------------------- 1 | Revision history for anzu.el 2 | 3 | Revision 0.64 2020/12/02 nokamoto 4 | - default anzu-search-threshold and anzu-replace-threshold is 1000 (#113) 5 | - support non-contiguous regions like rectangle region (#115) 6 | 7 | Revision 0.62 2016/08/18 syohex 8 | - Don't search over bounds(#80) 9 | - Correct variable type attribute(#76 Thanks Matus Goljer) 10 | 11 | Revision 0.61 2016/06/17 syohex 12 | - Don't overwrite user's case-fold-search(#75) 13 | - Implement anzu's isearch-query-replace commands(#72) 14 | 15 | Revision 0.60 2016/01/31 syohex 16 | - Add replacement threshold(#66) 17 | - Refactoring code 18 | 19 | Revision 0.59 2015/10/15 syohex 20 | - Fix replacement around region issue(#63 Thanks akicho8) 21 | 22 | Revision 0.58 2015/10/09 syohex 23 | - Fix byte-compile warning(#63 Thanks tarsius) 24 | 25 | Revision 0.57 2015/09/29 syohex 26 | - Fix about migemo exception(#62 Reported by kosh04) 27 | 28 | Revision 0.56 2015/09/13 syohex 29 | - Fix literal replacement issue(#60 Thanks kosh04) 30 | 31 | Revision 0.55 2015/09/11 syohex 32 | - anzu--status declare as buffer local variable 33 | 34 | Revision 0.54 2015/08/03 syohex 35 | - Correct removed hook function name. This makes anzu information is shown if anzu-mode 36 | is disabled. 37 | 38 | Revision 0.53 2015/05/18 syohex 39 | - Fix raising invalid regexp error issue(Thanks kosh04) 40 | - Valid replaced regular expression in regexp replace commands 41 | 42 | Revision 0.52 2015/02/22 syohex 43 | - Fix default input issue(Reported by pronobis) 44 | 45 | Revision 0.51 2015/02/18 syohex 46 | - Improve new history feature of Emacs 25 47 | (This feature works on both Emacs 24 and 25) 48 | 49 | Revision 0.50 2015/02/11 syohex 50 | - Fix Emacs 24 issue(Reported by LefterisJP) 51 | Can't use both anzu replace commands and builtin replacements commands 52 | 53 | Revision 0.49 2015/02/04 syohex 54 | - Fix zero width replacement causes infinite loop issue(Reported by proofit404) 55 | 56 | Revision 0.48 2015/01/28 syohex 57 | - Fix case when expression(,(...)) cannot be compiled. 58 | 59 | Revision 0.47 2015/01/21 syohex 60 | - Fix displaying wrong current position in replacement commands 61 | 62 | Revision 0.46 2015/01/10 syohex 63 | - Refactoring for evil-anzu 64 | 65 | Revision 0.45 2015/01/09 syohex 66 | - Disable blink-matching-paren in replace commands 67 | 68 | Revision 0.44 2014/12/16 syohex 69 | - Improve replacement commands 70 | - show case sensitive replacement text 71 | - Fix storing replacement history format issue 72 | - We could not use anzu replace commands with normal replacement 73 | commands such as query-replace etc. 74 | 75 | Revision 0.43 2014/11/30 syohex 76 | - Re-factoring for using migemo variable(Thanks lunaryorn) 77 | 78 | Revision 0.42 2014/11/28 syohex 79 | - Replacement by case sensitive for at-cursor commands 80 | 81 | Revision 0.41 2014/10/06 syohex 82 | - Improve checking whether using migemo 83 | 84 | Revision 0.40 2014/10/03 syohex 85 | - Fix replace command bug when using '^' or 'no' command 86 | 87 | Revision 0.39 2014/09/19 syohex 88 | - Fix bug case when bounds-of-thing-at-point returns nil 89 | 90 | Revision 0.38 2014/09/06 syohex 91 | - Improve replace command suggested by DamienCassou 92 | 93 | Revision 0.37 2014/08/27 syohex 94 | - Fix for isearch toggle commands and symbol search 95 | - Improve mode line in replacing commands 96 | 97 | Revision 0.36 2014/07/04 syohex 98 | - Implement replacement only specified lines 99 | 100 | Revision 0.35 2014/04/23 syohex 101 | - Change function name for maintenance 102 | 103 | Revision 0.34 2014/03/28 syohex 104 | - Fix replacement issue 105 | Reported by purcell. 106 | 107 | Revision 0.33 2014/03/19 syohex 108 | - Fix byte compile warnings for Emacs 24.3 109 | 110 | Revision 0.32 2014/03/08 syohex 111 | - Specify Emacs version 112 | 113 | Revision 0.31 2014/03/07 syohex 114 | - Enable lexical-binding 115 | - Update requirement cl-lib.el version 116 | 117 | Revision 0.30 2014/02/02 syohex 118 | - Fix wrong prompt issue 119 | 120 | Revision 0.29 2014/02/02 syohex 121 | - Add no query replace command 122 | - Use cl-lib instead of cl.el 123 | 124 | Revision 0.28 2014/01/29 syohex 125 | - Fix ignore case issue 126 | 127 | Revision 0.27 2014/01/27 syohex 128 | - Fix non prefix issue 129 | 130 | Revision 0.26 2014/01/16 syohex 131 | - Fix #13 issue 132 | https://github.com/syohex/emacs-anzu/issues/13 133 | Reported by fukamachi 134 | 135 | Revision 0.25 2014/01/16 syohex 136 | - Support backward replacement for Emacs 24.4 137 | 138 | Revision 0.24 2013/11/20 syohex 139 | - Fix invalid return type of 'anzu-replace-to-string-separator' 140 | 141 | Revision 0.23 2013/11/08 syohex 142 | - Add 'anzu-replace-to-string-separator' for separator of 'to' string 143 | 144 | Revision 0.22 2013/11/06 syohex 145 | - Fix issue when using isearch-repeat-{forward,backward} 146 | 147 | Revision 0.21 2013/11/05 syohex 148 | - Fix different behavior from query-replace-regexp 149 | 150 | Revision 0.20 2013/11/01 syohex 151 | - Fix case of 0 width match such as '^', '$' 152 | - Improve replace commands(We can use \#, \1 in \,(...)) 153 | - Re-factoring 154 | 155 | Revision 0.19 2013/10/30 syohex 156 | - Fix autoload cookie 157 | - Implement anzu-query-replace-at-cursor-thing 158 | 159 | Revision 0.18 2013/10/30 syohex 160 | - Add switch of deactivate region feature 161 | - Improve anzu-query-replace-at-cursor behavior 162 | 163 | Revision 0.17 2013/10/28 syohex 164 | - Fix case fold issue 165 | 166 | Revision 0.16 2013/10/27 syohex 167 | - Improve for performance at not regexp replace command 168 | 169 | Revision 0.15 2013/10/26 syohex 170 | - Improve replacement commands 171 | 172 | Revision 0.14 2013/10/26 syohex 173 | - Implement 'anzu-query-replace-at-cursor' 174 | 175 | Revision 0.13 2013/10/23 syohex 176 | - Add feature like evil's replacement 177 | 178 | Revision 0.12 2013/10/21 syohex 179 | - Re-factoring: reduce global variables 180 | 181 | Revision 0.11 2013/10/21 syohex 182 | - Change mode-line message at replace commands 183 | 184 | Revision 0.10 2013/10/20 syohex 185 | - Add feature that highlight replaced texts 186 | 187 | Revision 0.09 2013/10/19 syohex 188 | - Implement 'anzu-replace-query' and 'anzu-replace-query-regexp' 189 | 190 | Revision 0.08 2013/10/16 syohex 191 | - Fix that error message is displayed when regexp validation is failed 192 | 193 | Revision 0.07 2013/10/01 syohex 194 | - Introduce anzu-search-threshold 195 | 196 | Revision 0.06 2013/09/30 syohex 197 | - Introduce anzu-minimum-input-length 198 | This is useful for migemo users 199 | 200 | Revision 0.05 2013/09/28 syohex 201 | Thanks Steve Purcell !! 202 | - Improve description line 203 | 204 | Revision 0.04 2013/09/28 syohex 205 | Thanks Bozhidar Batsov!! 206 | - Improve documentation 207 | - Fix byte compile warning 208 | 209 | Revision 0.03 2013/09/22 syohex 210 | - Check isearch-regexp value for checking regexp search 211 | 212 | Revision 0.02 2013/09/22 syohex 213 | - Support display search information at any position in mode-line 214 | (Suggest by gvol) 215 | - Fix case that input is anchor(Report by dakrone) 216 | - Improve non regexp search 217 | 218 | Revision 0.01 2013/09/20 syohex 219 | - Initial version 220 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY : clean distclean lint test version 2 | 3 | EMACS ?= emacs 4 | CASK ?= cask 5 | 6 | LOADPATH = -L . 7 | 8 | ELPA_DIR = $(shell EMACS=$(EMACS) $(CASK) package-directory) 9 | AUTOLOADS = $(wildcard *-autoloads.el*) 10 | ELS = $(filter-out $(AUTOLOADS),$(wildcard *.el)) 11 | OBJECTS = $(ELS:.el=.elc) 12 | BACKUPS = $(ELS:.el=.el~) 13 | 14 | version: elpa 15 | $(CASK) exec $(EMACS) --version 16 | 17 | lint: elpa 18 | $(CASK) exec $(EMACS) -Q --batch $(LOADPATH) \ 19 | -l elisp-lint.el \ 20 | -f elisp-lint-files-batch \ 21 | --no-checkdoc \ 22 | --no-fill-column \ 23 | $(ELS) 24 | 25 | # test: elpa 26 | # $(CASK) exec $(EMACS) -Q -batch $(LOADPATH) \ 27 | # -l test/test.el \ 28 | # -f ert-run-tests-batch-and-exit 29 | 30 | elpa: $(ELPA_DIR) 31 | $(ELPA_DIR): Cask 32 | mkdir -p $(ELPA_DIR)/gnupg && \ 33 | chmod 700 $(ELPA_DIR)/gnupg && \ 34 | echo "disable-ipv6" > $(ELPA_DIR)/gnupg/dirmngr.conf && \ 35 | for i in {1..3}; do \ 36 | gpg --keyserver keyserver.ubuntu.com \ 37 | --homedir $(ELPA_DIR)/gnupg \ 38 | --recv-keys 066DAFCB81E42C40 \ 39 | && break || sleep 15; \ 40 | done 41 | $(CASK) install 42 | touch $@ 43 | 44 | clean: 45 | rm -rf $(OBJECTS) $(BACKUPS) $(AUTOLOADS) 46 | 47 | distclean: 48 | rm -rf .cask 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # anzu.el 2 | 3 | [![melpa badge][melpa-badge]][melpa-link] 4 | [![melpa stable badge][melpa-stable-badge]][melpa-stable-link] 5 | [![gh actions badge][gh-actions-badge]][gh-actions-link] 6 | 7 | ## Introduction 8 | 9 | `anzu.el` is an Emacs port of [anzu.vim](https://github.com/osyo-manga/vim-anzu). 10 | `anzu.el` provides a minor mode which displays *current match* and *total matches* 11 | information in the mode-line in various search modes. 12 | 13 | ## New Maintainer 14 | 15 | This package has a new maintainer as of 2024, @LemonBreezes. If you have any 16 | questions or concerns, please feel free to open an issue on GitHub or to send me 17 | an email. 18 | 19 | ## Screenshot 20 | 21 | ![Screencast of anzu.gif](image/anzu.gif) 22 | 23 | 24 | ## Requirements 25 | 26 | - Emacs 24 or higher 27 | - `cl-lib` 0.5 or higher (you don't need to install `cl-lib` if you use Emacs 24.3 or higher) 28 | 29 | 30 | ## Installation 31 | 32 | You can install `anzu.el` from [MELPA](https://melpa.org/) with `package.el` 33 | 34 | ``` 35 | M-x package-install anzu 36 | ``` 37 | 38 | 39 | ## Basic Usage 40 | 41 | ##### `global-anzu-mode` 42 | 43 | Enable global anzu mode: 44 | 45 | ```lisp 46 | (global-anzu-mode +1) 47 | ``` 48 | 49 | ##### `anzu-mode` 50 | 51 | Enable anzu minor mode: 52 | 53 | ```lisp 54 | (anzu-mode +1) 55 | ``` 56 | 57 | ##### `anzu-query-replace` 58 | 59 | Same as `query-replace` except displays anzu information in the 60 | mode-line. 61 | 62 | ##### `anzu-query-replace-regexp` 63 | 64 | Same as `query-replace-regexp` except displays anzu information in the 65 | mode-line. 66 | 67 | ![Screencast of anzu-query-replace-regexp](image/anzu-replace-demo.gif) 68 | 69 | You can replace key bindings for the standard definitions of 70 | `query-replace` and `query-replace-regexp` with their anzu versions by 71 | adding this snippet to your configuration: 72 | 73 | ```lisp 74 | (global-set-key [remap query-replace] 'anzu-query-replace) 75 | (global-set-key [remap query-replace-regexp] 'anzu-query-replace-regexp) 76 | ``` 77 | 78 | ##### `anzu-query-replace-at-cursor` 79 | 80 | Works like `anzu-query-replace` except the *from-string* is the symbol 81 | at the cursor. 82 | 83 | ##### `anzu-query-replace-at-cursor-thing` 84 | 85 | Works like `anzu-query-replace-at-cursor` except the replacement is 86 | constrained to the region specified by the variable 87 | `anzu-replace-at-cursor-thing`. See the variable's description in 88 | the customization section for additional details. 89 | 90 | Be careful not to confuse this variable with the identically named 91 | function (see below). 92 | 93 | ##### `anzu-replace-at-cursor-thing` 94 | 95 | Like `anzu-query-replace-at-cursor-thing`, but doesn't query for 96 | confirmation before making the substitution. 97 | 98 | Be careful not to confuse this function with the identically named 99 | customization variable. See the discussion in the 100 | `anzu-query-replace-at-cursor-thing` section. 101 | 102 | ![Screencast of anzu-replace-at-cursor-thing](image/anzu-replace-demo-noquery.gif) 103 | 104 | ##### `anzu-isearch-query-replace` 105 | 106 | The anzu version of `isearch-query-replace`. 107 | 108 | ##### `anzu-isearch-query-replace-regexp` 109 | 110 | The anzu version of `isearch-query-replace-regexp`. 111 | 112 | ## Customization 113 | 114 | ##### `anzu-mode-line` 115 | 116 | Face of mode-line anzu information 117 | 118 | ##### `anzu-mode-line-no-match` 119 | 120 | Face of mode-line at no matching case 121 | 122 | ##### `anzu-replace-highlight` 123 | 124 | Face of from-string of replacement 125 | 126 | ##### `anzu-replace-to` 127 | 128 | Face of to-string of replacement 129 | 130 | ##### `anzu-mode-line-update-function` 131 | 132 | Function which constructs mode-line string. anzu.el puts its output to mode-line. It is called at searching, inputting replaced word, replacing. This must be non-nil. 133 | 134 | The function takes 2 integer arguments, current position and total match number. You can get current-state from `anzu--state`(`'search`, `'replace-query`, `replace`). 135 | 136 | ```lisp 137 | (defun my/anzu-update-func (here total) 138 | (when anzu--state 139 | (let ((status (cl-case anzu--state 140 | (search (format "<%d/%d>" here total)) 141 | (replace-query (format "(%d Replaces)" total)) 142 | (replace (format "<%d/%d>" here total))))) 143 | (propertize status 'face 'anzu-mode-line)))) 144 | 145 | (custom-set-variables 146 | '(anzu-mode-line-update-function #'my/anzu-update-func)) 147 | ``` 148 | 149 | ##### `anzu-cons-mode-line-p`(Default is `t`) 150 | 151 | Set `nil` if you want to display anzu information at any position in mode-line. 152 | `anzu.el` cons search information head of `mode-line` as default. 153 | 154 | For example, show search information tail of `minor-mode-alist` 155 | 156 | ```lisp 157 | (setq anzu-cons-mode-line-p nil) 158 | (setcar (cdr (assq 'isearch-mode minor-mode-alist)) 159 | '(:eval (anzu--update-mode-line))) 160 | ``` 161 | 162 | ##### Screenshot 163 | 164 | ![anzu-any-position](image/anzu-any-position.png) 165 | 166 | 167 | ##### `anzu-mode-lighter` 168 | 169 | Mode name in `mode-line`. Default is ` Anzu`. 170 | 171 | 172 | ##### `anzu-input-idle-delay`(Default is `0.05`) 173 | 174 | Delay second of updating mode-line information when you input from-string 175 | 176 | ##### `anzu-regexp-search-commands` 177 | 178 | Commands which have regexp input. If the last command is a member of this list, 179 | `anzu.el` treats input as regular expression. 180 | 181 | The default value is `'(isearch-forward-regexp isearch-backward-regexp)`. 182 | 183 | ##### `anzu-use-migemo`(Default is `nil`) 184 | 185 | Set to `t` if you use [migemo](https://github.com/emacs-jp/migemo). 186 | 187 | ##### `anzu-search-threshold`(Default is `nil`) 188 | 189 | Threshold of searched words. If there are searched word more than this value, 190 | `anzu.el` stops to search and display total number like `1000+`(as default). 191 | If this value is `nil`, `anzu.el` counts all words. 192 | 193 | ![anzu-threshold](image/anzu-threshold.png) 194 | 195 | ##### `anzu-replace-threshold`(Default is `nil`) 196 | 197 | Threshold of replacement overlay. If this value is `nil`, 198 | 199 | ##### `anzu-minimum-input-length`(Default is 1) 200 | 201 | Minimum input length to enable anzu. This parameter is useful for `migemo` users. 202 | Searching 1 or 2 characters with `migemo` is too heavy if buffer is so large. 203 | Please set 3 or higher if you frequently edit such file. 204 | 205 | ##### `anzu-deactivate-region`(Default is `nil`) 206 | 207 | Deactivate region at anzu replace command if this value is non-nil. 208 | It is hard to see with anzu replace command when region is active. 209 | 210 | ##### `anzu-replace-at-cursor-thing`(Default is 'defun) 211 | 212 | Describes the type of *thing* used by the `anzu-*-thing` functions. 213 | It can be set to any symbol that is a valid argument for the 214 | `thing-at-point` function, including e.g. `defun`, `word`, and 215 | `page`. See the documentation for `thing-at-point` for additional 216 | information. 217 | 218 | ##### `anzu-replace-to-string-separator`(Default is "") 219 | 220 | Separator of `to` string. 221 | 222 | 223 | ## Sample Configuration 224 | 225 | ```lisp 226 | (require 'anzu) 227 | (global-anzu-mode +1) 228 | 229 | (set-face-attribute 'anzu-mode-line nil 230 | :foreground "yellow" :weight 'bold) 231 | 232 | (custom-set-variables 233 | '(anzu-mode-lighter "") 234 | '(anzu-deactivate-region t) 235 | '(anzu-search-threshold 1000) 236 | '(anzu-replace-threshold 50) 237 | '(anzu-replace-to-string-separator " => ")) 238 | 239 | (define-key isearch-mode-map [remap isearch-query-replace] #'anzu-isearch-query-replace) 240 | (define-key isearch-mode-map [remap isearch-query-replace-regexp] #'anzu-isearch-query-replace-regexp) 241 | ``` 242 | 243 | [melpa-link]: https://melpa.org/#/anzu 244 | [melpa-stable-link]: https://stable.melpa.org/#/anzu 245 | [gh-actions-link]: https://github.com/emacsorphanage/anzu/actions 246 | [melpa-badge]: https://melpa.org/packages/anzu-badge.svg 247 | [melpa-stable-badge]: https://stable.melpa.org/packages/anzu-badge.svg 248 | [gh-actions-badge]: https://github.com/emacsorphanage/anzu/workflows/ci-checks/badge.svg 249 | -------------------------------------------------------------------------------- /anzu.el: -------------------------------------------------------------------------------- 1 | ;;; anzu.el --- Show number of matches in mode-line while searching -*- lexical-binding: t; -*- 2 | 3 | ;; Copyright (C) 2016-2020 Syohei YOSHIDA and Neil Okamoto 4 | 5 | ;; Author: Syohei YOSHIDA 6 | ;; Maintainer: LemonBreezes 7 | ;; Homepage: https://github.com/emacsorphanage/anzu 8 | ;; Version: 0.66 9 | ;; Package-Requires: ((emacs "25.1")) 10 | 11 | ;; This file is part of GNU Emacs. 12 | 13 | ;; This program is free software; you can redistribute it and/or modify 14 | ;; it under the terms of the GNU General Public License as published by 15 | ;; the Free Software Foundation, either version 3 of the License, or 16 | ;; (at your option) any later version. 17 | 18 | ;; This program is distributed in the hope that it will be useful, 19 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 20 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 | ;; GNU General Public License for more details. 22 | 23 | ;; You should have received a copy of the GNU General Public License 24 | ;; along with this program. If not, see . 25 | 26 | ;;; Commentary: 27 | 28 | ;; `anzu.el' is an Emacs port of `anzu.vim'. 29 | ;; 30 | ;; `anzu.el' provides a minor mode which displays the match count for 31 | ;; various search commands in the mode-line, using the format 32 | ;; `current/total'. This makes it easy to know how many matches your 33 | ;; search query has in the current buffer. 34 | 35 | ;; To use this package, add following code to your init.el or .emacs 36 | ;; 37 | ;; (global-anzu-mode +1) 38 | ;; 39 | 40 | ;;; Code: 41 | 42 | (require 'cl-lib) 43 | (require 'thingatpt) 44 | 45 | (defgroup anzu nil 46 | "Show the current search's match count in the mode-line." 47 | :group 'isearch) 48 | 49 | (defcustom anzu-mode-lighter " Anzu" 50 | "Mode-line lighter for `anzu-mode'." 51 | :type 'string) 52 | 53 | (defcustom anzu-cons-mode-line-p t 54 | "Whether anzu should display itself in the mode-line. 55 | Set to nil if you put anzu in your mode-line manually." 56 | :type 'boolean) 57 | 58 | (defcustom anzu-minimum-input-length 1 59 | "Minimum search query length required to enable anzu." 60 | :type 'integer) 61 | 62 | (defcustom anzu-search-threshold 1000 63 | "Search match count limit." 64 | :type '(choice (integer :tag "Threshold of search") 65 | (const :tag "No threshold" nil))) 66 | 67 | (defcustom anzu-replace-threshold 1000 68 | "Limit of replacement overlays." 69 | :type '(choice (integer :tag "Threshold of replacement overlays") 70 | (const :tag "No threshold" nil))) 71 | 72 | (defcustom anzu-use-migemo nil 73 | "Whether to use migemo." 74 | :type 'boolean) 75 | 76 | (defcustom anzu-mode-line-update-function #'anzu--update-mode-line-default 77 | "Function which returns the mode-line string. This must be non-nil." 78 | :type 'function) 79 | 80 | (defcustom anzu-regexp-search-commands '(isearch-forward-regexp 81 | isearch-backward-regexp) 82 | "Search functions which use regexp." 83 | :type '(repeat function)) 84 | 85 | (defcustom anzu-input-idle-delay 0.05 86 | "Delay in seconds between mode-line updates in replace commands." 87 | :type 'number) 88 | 89 | (defcustom anzu-deactivate-region nil 90 | "Whether to deactivate region when anzu is used with a region replace command." 91 | :type 'boolean) 92 | 93 | (defcustom anzu-replace-at-cursor-thing 'defun 94 | "Thing to replace. See `thing-at-point' for valid options." 95 | :type 'symbol) 96 | 97 | (defcustom anzu-replace-to-string-separator "" 98 | "Separator of `to' string." 99 | :type 'string) 100 | 101 | (defface anzu-mode-line 102 | '((t (:foreground "magenta" :weight bold))) 103 | "Anzu's mode-line indicator face.") 104 | 105 | (defface anzu-mode-line-no-match 106 | '((t (:inherit anzu-mode-line))) 107 | "Anzu's mode-line indicator face, used when no matches are found.") 108 | 109 | (defface anzu-replace-highlight 110 | '((t :inherit query-replace)) 111 | "Replacement highlighting face.") 112 | 113 | (defface anzu-match-1 114 | '((((class color) (background light)) 115 | :background "aquamarine" :foreground "black") 116 | (((class color) (background dark)) 117 | :background "limegreen" :foreground "black") 118 | (t :inverse-video t)) 119 | "First group of match.") 120 | 121 | (defface anzu-match-2 122 | '((((class color) (background light)) 123 | :background "springgreen" :foreground "black") 124 | (((class color) (background dark)) 125 | :background "yellow" :foreground "black") 126 | (t :inverse-video t)) 127 | "Second group of match.") 128 | 129 | (defface anzu-match-3 130 | '((((class color) (background light)) 131 | :background "yellow" :foreground "black") 132 | (((class color) (background dark)) 133 | :background "aquamarine" :foreground "black") 134 | (t :inverse-video t)) 135 | "Third group of match.") 136 | 137 | (defface anzu-replace-to 138 | '((((class color) (background light)) 139 | :foreground "red") 140 | (((class color) (background dark)) 141 | :foreground "yellow")) 142 | "Replacement highlighting face.") 143 | 144 | (defvar anzu--total-matched 0) 145 | (defvar anzu--current-position 0) 146 | (defvar anzu--overflow-p nil) 147 | (defvar anzu--last-isearch-string nil) 148 | (defvar anzu--cached-positions nil) 149 | (defvar anzu--last-command nil) 150 | (defvar anzu--state nil) 151 | (defvar anzu--cached-count 0) 152 | (defvar anzu--last-replace-input "") 153 | (defvar anzu--last-search-state nil) 154 | (defvar anzu--last-replaced-count nil) 155 | (defvar anzu--outside-point nil) 156 | (defvar anzu--history nil) 157 | (defvar anzu--query-defaults nil) 158 | (defvar anzu--region-noncontiguous nil) 159 | (defvar anzu--update-timer nil) 160 | 161 | (defun anzu--validate-regexp (regexp) 162 | (condition-case nil 163 | (progn 164 | (string-match-p regexp "") 165 | t) 166 | (invalid-regexp nil))) 167 | 168 | (defsubst anzu--construct-position-info (count overflow positions) 169 | (list :count count :overflow overflow :positions positions)) 170 | 171 | (defsubst anzu--case-fold-search () 172 | (if isearch-mode 173 | isearch-case-fold-search 174 | case-fold-search)) 175 | 176 | (defsubst anzu--word-search-p () 177 | (and (not (memq anzu--last-command anzu-regexp-search-commands)) 178 | (not isearch-regexp))) 179 | 180 | (defsubst anzu--isearch-regexp-function () 181 | (or (bound-and-true-p isearch-regexp-function) 182 | (bound-and-true-p isearch-word))) 183 | 184 | (defun anzu--transform-input (str) 185 | (cond ((eq (anzu--isearch-regexp-function) 'isearch-symbol-regexp) 186 | (setq str (isearch-symbol-regexp str))) 187 | ((anzu--word-search-p) 188 | (setq str (regexp-quote str))) 189 | (t str))) 190 | 191 | (defsubst anzu--use-migemo-p () 192 | (when anzu-use-migemo 193 | (unless (featurep 'migemo) 194 | (error "Error: migemo is not loaded")) 195 | (bound-and-true-p migemo-isearch-enable-p))) 196 | 197 | (defun anzu--search-all-position (str) 198 | (unless anzu--last-command 199 | (setq anzu--last-command last-command)) 200 | (let ((input (anzu--transform-input str))) 201 | (if (not (anzu--validate-regexp input)) 202 | anzu--cached-positions 203 | (save-excursion 204 | (goto-char (point-min)) 205 | (let ((positions '()) 206 | (count 0) 207 | (overflow nil) 208 | (finish nil) 209 | (search-func (if (anzu--use-migemo-p) 210 | (lambda (word &optional bound noerror count) 211 | (with-no-warnings 212 | (migemo-forward word bound noerror count))) 213 | #'re-search-forward)) 214 | (case-fold-search (anzu--case-fold-search))) 215 | (while (and (not finish) (funcall search-func input nil t)) 216 | (push (cons (match-beginning 0) (match-end 0)) positions) 217 | (cl-incf count) 218 | (when (= (match-beginning 0) (match-end 0)) ;; Case of anchor such as "^" 219 | (if (eobp) 220 | (setq finish t) 221 | (forward-char 1))) 222 | (when (and anzu-search-threshold (>= count anzu-search-threshold)) 223 | (setq overflow t finish t))) 224 | (let ((result (anzu--construct-position-info count overflow (reverse positions)))) 225 | (setq anzu--cached-positions (copy-sequence result)) 226 | result)))))) 227 | 228 | (defun anzu--where-is-here (positions here) 229 | ;; don't use loop for emacs 27 bug 230 | (let ((poss positions) 231 | (index 1) 232 | (ret 0)) 233 | (while poss 234 | (let ((pos (car poss))) 235 | (if (and (>= here (car pos)) (<= here (cdr pos))) 236 | (setq ret index poss nil) 237 | (setq poss (cdr poss) index (1+ index))))) 238 | ret)) 239 | 240 | (defun anzu--use-result-cache-p (input) 241 | (and (eq (anzu--isearch-regexp-function) (car anzu--last-search-state)) 242 | (eq isearch-regexp (cdr anzu--last-search-state)) 243 | (string= input anzu--last-isearch-string) 244 | (not (eq last-command 'isearch-toggle-case-fold)))) 245 | 246 | (defun anzu--update (query) 247 | (when (>= (length query) anzu-minimum-input-length) 248 | (let ((result (if (anzu--use-result-cache-p query) 249 | anzu--cached-positions 250 | (anzu--search-all-position query)))) 251 | (let ((curpos (anzu--where-is-here (plist-get result :positions) (point)))) 252 | (setq anzu--total-matched (plist-get result :count) 253 | anzu--overflow-p (plist-get result :overflow) 254 | anzu--current-position curpos 255 | anzu--last-search-state (cons (anzu--isearch-regexp-function) isearch-regexp) 256 | anzu--last-isearch-string query) 257 | (force-mode-line-update))))) 258 | 259 | (defun anzu--update-post-hook () 260 | (anzu--update isearch-string)) 261 | 262 | (defconst anzu--mode-line-format '(:eval (anzu--update-mode-line))) 263 | 264 | (defsubst anzu--mode-line-not-set-p () 265 | (and (listp mode-line-format) 266 | (member anzu--mode-line-format mode-line-format))) 267 | 268 | (defun anzu--cons-mode-line-search () 269 | (anzu--cons-mode-line 'search)) 270 | 271 | (defun anzu--cons-mode-line (state) 272 | (setq anzu--state state) 273 | (when (and anzu-cons-mode-line-p (listp mode-line-format) (not (anzu--mode-line-not-set-p))) 274 | (setq mode-line-format (cons anzu--mode-line-format mode-line-format)))) 275 | 276 | (defsubst anzu--reset-status () 277 | (setq anzu--total-matched 0 278 | anzu--current-position 0 279 | anzu--state nil 280 | anzu--last-command nil 281 | anzu--last-isearch-string nil 282 | anzu--overflow-p nil 283 | anzu--region-noncontiguous nil)) 284 | 285 | (defun anzu--reset-mode-line () 286 | (anzu--reset-status) 287 | (when (and anzu-cons-mode-line-p (anzu--mode-line-not-set-p)) 288 | (setq mode-line-format (delete anzu--mode-line-format mode-line-format)))) 289 | 290 | (defsubst anzu--format-here-position (here total) 291 | (if (and anzu--overflow-p (zerop here)) 292 | (format "%d+" total) 293 | here)) 294 | 295 | (defun anzu--update-mode-line-default (here total) 296 | (when anzu--state 297 | (let ((status (cl-case anzu--state 298 | (search (format "(%s/%d%s)" 299 | (anzu--format-here-position here total) 300 | total (if anzu--overflow-p "+" ""))) 301 | (replace-query (format "(%d replace)" total)) 302 | (replace (format "(%d/%d)" here total)))) 303 | (face (if (and (zerop total) (not (string= isearch-string ""))) 304 | 'anzu-mode-line-no-match 305 | 'anzu-mode-line))) 306 | (propertize status 'face face)))) 307 | 308 | (defun anzu--update-mode-line () 309 | (funcall anzu-mode-line-update-function anzu--current-position anzu--total-matched)) 310 | 311 | ;;;###autoload 312 | (define-minor-mode anzu-mode 313 | "Minor mode which displays the current search's match count in the mode-line." 314 | :init-value nil 315 | :global nil 316 | :lighter anzu-mode-lighter 317 | (if anzu-mode 318 | (progn 319 | (setq-local anzu--state nil) 320 | (add-hook 'isearch-update-post-hook #'anzu--update-post-hook nil t) 321 | (add-hook 'isearch-mode-hook #'anzu--cons-mode-line-search nil t) 322 | (add-hook 'isearch-mode-end-hook #'anzu--reset-mode-line nil t)) 323 | (remove-hook 'isearch-update-post-hook #'anzu--update-post-hook t) 324 | (remove-hook 'isearch-mode-hook #'anzu--cons-mode-line-search t) 325 | (remove-hook 'isearch-mode-end-hook #'anzu--reset-mode-line t) 326 | (anzu--reset-mode-line))) 327 | 328 | (defun anzu--turn-on () 329 | (unless (minibufferp) 330 | (anzu-mode +1))) 331 | 332 | ;;;###autoload 333 | (define-globalized-minor-mode global-anzu-mode anzu-mode anzu--turn-on) 334 | 335 | (defsubst anzu--query-prompt-base (use-region use-regexp) 336 | (concat "Query replace" 337 | (if current-prefix-arg " word" "") 338 | (if use-regexp " regexp" "") 339 | (if use-region " in region" "")) ) 340 | 341 | (defun anzu--query-prompt (use-region use-regexp at-cursor isearch-p) 342 | (let ((prompt (anzu--query-prompt-base use-region use-regexp))) 343 | (if (and anzu--query-defaults (not at-cursor) (not isearch-p)) 344 | (format "%s (default %s -> %s) " 345 | prompt 346 | (query-replace-descr (caar anzu--query-defaults)) 347 | (query-replace-descr (cdar anzu--query-defaults))) 348 | prompt))) 349 | 350 | (defvar anzu--replaced-markers nil) 351 | (defsubst anzu--set-marker (beg buf) 352 | (let ((m (make-marker))) 353 | (set-marker m beg buf) 354 | (push m anzu--replaced-markers))) 355 | 356 | (defun anzu--make-overlay (begin end face prio) 357 | (let ((ov (make-overlay begin end))) 358 | (overlay-put ov 'face face) 359 | (overlay-put ov 'priority prio) 360 | (overlay-put ov 'anzu-overlay t) 361 | ov)) 362 | 363 | (defun anzu--add-match-group-overlay (match-data groups) 364 | (when (>= groups 3) 365 | (anzu--make-overlay (cl-fifth match-data) (cl-sixth match-data) 366 | 'anzu-match-3 1001)) 367 | (when (>= groups 2) 368 | (anzu--make-overlay (cl-third match-data) (cl-fourth match-data) 369 | 'anzu-match-2 1001)) 370 | (anzu--make-overlay (cl-first match-data) (cl-second match-data) 371 | 'anzu-match-1 1001)) 372 | 373 | (defun anzu--add-overlay (beg end) 374 | (let* ((match-data (match-data)) 375 | (groups (/ (- (length match-data) 2) 2))) 376 | (when (>= groups 1) 377 | (anzu--add-match-group-overlay (cddr match-data) groups)) 378 | (let ((ov (anzu--make-overlay beg end 'anzu-replace-highlight 1000))) 379 | (overlay-put ov 'from-string (buffer-substring-no-properties beg end)) 380 | (overlay-put ov 'anzu-replace t)))) 381 | 382 | (defsubst anzu--cleanup-markers () 383 | (mapc (lambda (m) (set-marker m nil)) anzu--replaced-markers) 384 | (setq anzu--replaced-markers nil)) 385 | 386 | (defun anzu2--put-overlay-p (beg end overlay-beg overlay-end) 387 | (if anzu--region-noncontiguous 388 | (cl-loop for (b . e) in (cl-loop for region in anzu--region-noncontiguous 389 | when (and (>= (car region) overlay-beg) (<= (cdr region) overlay-end)) 390 | collect region) 391 | thereis (and (>= beg b overlay-beg) (<= end e overlay-end))) 392 | (and (>= beg overlay-beg) (<= end overlay-end)))) 393 | 394 | (defun anzu--convert-for-lax-whitespace (str use-regexp) 395 | (if use-regexp 396 | (if replace-regexp-lax-whitespace 397 | (replace-regexp-in-string "\\s-+" search-whitespace-regexp str 398 | nil t) 399 | str) 400 | (if replace-lax-whitespace 401 | (replace-regexp-in-string "\\s-+" 402 | search-whitespace-regexp 403 | (regexp-quote str) 404 | nil t) 405 | (regexp-quote str)))) 406 | 407 | ;; Return highlighted count 408 | (defun anzu--count-and-highlight-matched (buf str replace-beg replace-end 409 | use-regexp overlay-limit case-sensitive) 410 | (anzu--cleanup-markers) 411 | (setq str (anzu--convert-for-lax-whitespace str use-regexp)) 412 | (if (not (anzu--validate-regexp str)) 413 | anzu--cached-count 414 | (with-current-buffer buf 415 | (save-excursion 416 | (let* ((backward (> replace-beg replace-end)) 417 | (overlay-beg (if backward (max replace-end overlay-limit) replace-beg)) 418 | (overlay-end (if backward replace-beg (min replace-end overlay-limit)))) 419 | (goto-char replace-beg) 420 | (let ((count 0) 421 | (overlayed 0) 422 | (finish nil) 423 | (cmp-func (if backward #'< #'>)) 424 | (search-func (if backward #'re-search-backward #'re-search-forward)) 425 | (step (if backward -1 1)) 426 | (case-fold-search (if case-sensitive 427 | nil 428 | (anzu--case-fold-search)))) 429 | (while (and (not finish) (funcall search-func str replace-end t)) 430 | (if anzu--region-noncontiguous 431 | (when (cl-loop for (b . e) in anzu--region-noncontiguous 432 | thereis (and (>= (point) b) (<= (point) e))) 433 | (cl-incf count)) 434 | (cl-incf count)) 435 | (let ((beg (match-beginning 0)) 436 | (end (match-end 0))) 437 | (when (= beg end) 438 | (if (eobp) 439 | (setq finish t) 440 | (forward-char step))) 441 | (when (and replace-end (funcall cmp-func (point) replace-end)) 442 | (setq finish t)) 443 | (when (and (not finish) (anzu2--put-overlay-p beg end overlay-beg overlay-end)) 444 | (cl-incf overlayed) 445 | (anzu--add-overlay beg end)))) 446 | (setq anzu--cached-count count) 447 | overlayed)))))) 448 | 449 | (defun anzu--search-outside-visible (buf input beg end use-regexp) 450 | (let* ((regexp (if use-regexp input (regexp-quote input))) 451 | (backward (> beg end)) 452 | (searchfn (if backward #'re-search-backward #'re-search-forward))) 453 | (when (anzu--validate-regexp regexp) 454 | (with-selected-window (get-buffer-window buf) 455 | (goto-char beg) 456 | (when (funcall searchfn regexp end t) 457 | (setq anzu--outside-point (match-beginning 0)) 458 | (let ((overlay-limit (anzu--overlay-limit backward))) 459 | (anzu--count-and-highlight-matched buf input beg end use-regexp 460 | overlay-limit nil))))))) 461 | 462 | (defconst anzu--from-to-separator 463 | (propertize 464 | (or (ignore-errors 465 | (if (char-displayable-p ?\u2192) " \u2192 " " -> ")) 466 | " -> ") 467 | 'face 'minibuffer-prompt)) 468 | 469 | (defsubst anzu--separator () 470 | (propertize "\0" 'display anzu--from-to-separator 'separator t)) 471 | 472 | (defun anzu--check-minibuffer-input (buf beg end use-regexp overlay-limit) 473 | (let* ((content (minibuffer-contents)) 474 | (to (when (and (string-match (anzu--separator) content) 475 | (get-text-property (match-beginning 0) 'separator content)) 476 | (substring-no-properties content (match-end 0)))) 477 | (from (or (and to (substring-no-properties content 0 (match-beginning 0))) 478 | content)) 479 | (empty-p (string= from "")) 480 | (overlayed (if empty-p 481 | (setq anzu--cached-count 0) 482 | (anzu--count-and-highlight-matched buf from beg end use-regexp 483 | overlay-limit nil)))) 484 | (when anzu--outside-point 485 | (setq anzu--outside-point nil) 486 | (with-selected-window (get-buffer-window buf) 487 | (goto-char beg))) 488 | (when (and (not empty-p) (zerop overlayed)) 489 | (anzu--search-outside-visible buf from beg end use-regexp)) 490 | (when to 491 | (setq anzu--last-replace-input "") 492 | (anzu--append-replaced-string to buf beg end use-regexp overlay-limit from)) 493 | (setq anzu--total-matched anzu--cached-count) 494 | (force-mode-line-update))) 495 | 496 | (defun anzu--clear-overlays (buf beg end) 497 | (with-current-buffer buf 498 | (dolist (ov (overlays-in (or beg (point-min)) (or end (point-max)))) 499 | (when (overlay-get ov 'anzu-overlay) 500 | (delete-overlay ov))))) 501 | 502 | (defun anzu--transform-from-to-history () 503 | (let ((separator (anzu--separator))) 504 | (append (mapcar (lambda (from-to) 505 | (concat (query-replace-descr (car from-to)) 506 | separator 507 | (query-replace-descr (cdr from-to)))) 508 | anzu--query-defaults) 509 | (symbol-value query-replace-from-history-variable)))) 510 | 511 | (defun anzu--read-from-string (prompt beg end use-regexp overlay-limit) 512 | (let ((curbuf (current-buffer)) 513 | (blink-matching-paren nil) 514 | (anzu--history (anzu--transform-from-to-history)) 515 | is-input) 516 | (unwind-protect 517 | (minibuffer-with-setup-hook 518 | #'(lambda () 519 | (setq anzu--update-timer 520 | (run-with-idle-timer 521 | (max anzu-input-idle-delay 0.01) 522 | 'repeat 523 | (lambda () 524 | (anzu--clear-overlays curbuf nil nil) 525 | (with-selected-window (or (active-minibuffer-window) 526 | (minibuffer-window)) 527 | (anzu--check-minibuffer-input 528 | curbuf beg end use-regexp overlay-limit)))))) 529 | (prog1 (read-from-minibuffer (format "%s: " prompt) 530 | nil nil nil 'anzu--history nil t) 531 | (setq is-input t))) 532 | (when anzu--update-timer 533 | (cancel-timer anzu--update-timer) 534 | (setq anzu--update-timer nil) 535 | (unless is-input 536 | (goto-char beg)))))) 537 | 538 | (defun anzu--query-validate-from-regexp (from) 539 | (when (string-match "\\(?:\\`\\|[^\\]\\)\\(?:\\\\\\\\\\)*\\(\\\\[nt]\\)" from) 540 | (let ((match (match-string 1 from))) 541 | (cond 542 | ((string= match "\\n") 543 | (message "`\\n' here doesn't match a newline; type C-q C-j instead!!")) 544 | ((string= match "\\t") 545 | (message "\\t' here doesn't match a tab; to do that, just type TAB!!"))) 546 | (sit-for 2)))) 547 | 548 | (defun anzu--query-from-string (prompt beg end use-regexp overlay-limit) 549 | (let* ((from (anzu--read-from-string prompt beg end use-regexp overlay-limit)) 550 | (is-empty (string= from ""))) 551 | (when (and (not is-empty) (not anzu--query-defaults)) 552 | (setq anzu--last-replaced-count anzu--total-matched)) 553 | (if (and is-empty anzu--query-defaults) 554 | (cons (query-replace-descr (caar anzu--query-defaults)) 555 | (query-replace-compile-replacement 556 | (query-replace-descr (cdar anzu--query-defaults)) use-regexp)) 557 | (add-to-history query-replace-from-history-variable from nil t) 558 | (when use-regexp 559 | (unless (anzu--validate-regexp from) 560 | (error "'%s' is an invalid regular expression" from)) 561 | (anzu--query-validate-from-regexp from)) 562 | from))) 563 | 564 | (defun anzu--compile-replace-text (str) 565 | (let ((compiled (ignore-errors 566 | (query-replace-compile-replacement str t)))) 567 | (when compiled 568 | (cond ((stringp compiled) compiled) 569 | ((and (consp compiled) (functionp (car compiled))) 570 | compiled) 571 | ((and (consp compiled) (stringp (car compiled))) 572 | (car compiled)))))) 573 | 574 | (defun anzu--evaluate-occurrence (ov to-regexp replacements fixed-case from-regexp) 575 | (let ((from-string (overlay-get ov 'from-string)) 576 | (compiled (anzu--compile-replace-text to-regexp))) 577 | (if (not compiled) 578 | "" 579 | (with-temp-buffer 580 | (insert from-string) 581 | (goto-char (point-min)) 582 | (when (re-search-forward from-regexp nil t) 583 | (or (ignore-errors 584 | (if (consp compiled) 585 | (replace-match (funcall (car compiled) (cdr compiled) 586 | replacements) fixed-case) 587 | (replace-match compiled fixed-case)) 588 | (buffer-substring (point-min) (point-max))) 589 | "")))))) 590 | 591 | (defun anzu--overlay-sort (a b) 592 | (< (overlay-start a) (overlay-start b))) 593 | 594 | (defsubst anzu--overlays-in-range (beg end) 595 | (cl-loop for ov in (overlays-in (min beg end) (max beg end)) 596 | when (overlay-get ov 'anzu-replace) 597 | collect ov into anzu-overlays 598 | finally 599 | return 600 | (let ((sorted (sort anzu-overlays 'anzu--overlay-sort))) 601 | (if anzu-replace-threshold 602 | (cl-subseq sorted 0 (min (length sorted) anzu-replace-threshold)) 603 | sorted)))) 604 | 605 | (defsubst anzu--propertize-to-string (str) 606 | (let ((separator (or anzu-replace-to-string-separator ""))) 607 | (propertize (concat separator str) 'face 'anzu-replace-to))) 608 | 609 | (defsubst anzu--replaced-literal-string (ov replaced from) 610 | (let ((str (buffer-substring-no-properties 611 | (overlay-start ov) (overlay-end ov)))) 612 | ;; Needed to do `(string-match from str)' instead of `(string-match str from)', 613 | ;; because lax whitespace means `from' can be a regexp. 614 | (when (string-match from str) 615 | (replace-match replaced (not case-fold-search) t str)))) 616 | 617 | (defun anzu--append-replaced-string (content buf beg end use-regexp overlay-limit from) 618 | (let ((replacements 0)) 619 | (unless (string= content anzu--last-replace-input) 620 | (setq anzu--last-replace-input content) 621 | (with-current-buffer buf 622 | (let ((case-fold-search (anzu--case-fold-search)) 623 | (pattern (anzu--convert-for-lax-whitespace from use-regexp))) 624 | (dolist (ov (anzu--overlays-in-range beg (min end overlay-limit))) 625 | (let ((replace-evaled 626 | (if (not use-regexp) 627 | (anzu--replaced-literal-string ov content pattern) 628 | (prog1 (anzu--evaluate-occurrence ov content replacements 629 | (not case-fold-search) pattern) 630 | (cl-incf replacements))))) 631 | (overlay-put ov 'after-string (anzu--propertize-to-string replace-evaled))))))))) 632 | 633 | (defsubst anzu--outside-overlay-limit (orig-beg orig-limit) 634 | (save-excursion 635 | (goto-char (+ anzu--outside-point (- orig-limit orig-beg))) 636 | (line-end-position))) 637 | 638 | (defun anzu--read-to-string (from prompt beg end use-regexp overlay-limit) 639 | (let ((curbuf (current-buffer)) 640 | (orig-beg beg) 641 | (to-prompt (format "%s %s with: " prompt (query-replace-descr from))) 642 | (history-add-new-input nil) 643 | (blink-matching-paren nil) 644 | is-input) 645 | (setq anzu--last-replace-input "") 646 | (when anzu--outside-point 647 | (setq beg anzu--outside-point 648 | overlay-limit (anzu--outside-overlay-limit orig-beg overlay-limit) 649 | anzu--outside-point nil)) 650 | (unwind-protect 651 | (minibuffer-with-setup-hook 652 | #'(lambda () 653 | (setq anzu--update-timer 654 | (run-with-idle-timer 655 | (max anzu-input-idle-delay 0.01) 656 | 'repeat 657 | (lambda () 658 | (with-selected-window (or (active-minibuffer-window) 659 | (minibuffer-window)) 660 | (anzu--append-replaced-string 661 | (minibuffer-contents) 662 | curbuf beg end use-regexp overlay-limit from)))))) 663 | (prog1 (read-from-minibuffer to-prompt 664 | nil nil nil 665 | query-replace-from-history-variable nil t) 666 | (setq is-input t))) 667 | (when anzu--update-timer 668 | (cancel-timer anzu--update-timer) 669 | (setq anzu--update-timer nil) 670 | (unless is-input 671 | (goto-char orig-beg)))))) 672 | 673 | (defun anzu--query-replace-read-to (from prompt beg end use-regexp overlay-limit) 674 | (query-replace-compile-replacement 675 | (let ((to (anzu--read-to-string from prompt beg end use-regexp overlay-limit))) 676 | (add-to-history query-replace-to-history-variable to nil t) 677 | (add-to-history 'anzu--query-defaults (cons from to) nil t) 678 | to) 679 | use-regexp)) 680 | 681 | (defun anzu--overlay-limit (backward) 682 | (save-excursion 683 | (move-to-window-line (if backward 1 -1)) 684 | (forward-line (if backward -1 1)) 685 | (point))) 686 | 687 | (defun anzu--query-from-at-cursor (buf beg end overlay-limit) 688 | (let ((symbol (thing-at-point 'symbol))) 689 | (unless symbol 690 | (error "No symbol at cursor!!")) 691 | (let ((symbol-regexp (concat "\\_<" (regexp-quote symbol) "\\_>"))) 692 | (anzu--count-and-highlight-matched buf symbol-regexp beg end t overlay-limit t) 693 | (setq anzu--total-matched anzu--cached-count) 694 | (force-mode-line-update) 695 | symbol-regexp))) 696 | 697 | (defun anzu--query-from-isearch-string (buf beg end use-regexp overlay-limit) 698 | (anzu--count-and-highlight-matched buf isearch-string beg end use-regexp overlay-limit t) 699 | (setq anzu--total-matched anzu--cached-count) 700 | (force-mode-line-update) 701 | (add-to-history query-replace-from-history-variable isearch-string nil t) 702 | isearch-string) 703 | 704 | (defun anzu--thing-begin (thing) 705 | (let ((bound (bounds-of-thing-at-point thing))) 706 | (if bound 707 | (car bound) 708 | (let ((fallback-bound (bounds-of-thing-at-point 'symbol))) 709 | (if fallback-bound 710 | (car fallback-bound) 711 | (point)))))) 712 | 713 | (defsubst anzu--thing-end (thing) 714 | (let ((bound (bounds-of-thing-at-point thing))) 715 | (if bound 716 | (cdr bound) 717 | (point-max)))) 718 | 719 | (defun anzu--region-begin (use-region thing backward) 720 | (cond (use-region (region-beginning)) 721 | (backward (point)) 722 | (thing (anzu--thing-begin thing)) 723 | (current-prefix-arg (line-beginning-position)) 724 | (t (point)))) 725 | 726 | (defsubst anzu--line-end-position (num) 727 | (save-excursion 728 | (forward-line (1- num)) 729 | (line-end-position))) 730 | 731 | (defun anzu--region-end (use-region thing backward) 732 | (cond (use-region (region-end)) 733 | (backward (point-min)) 734 | (current-prefix-arg 735 | (anzu--line-end-position (prefix-numeric-value current-prefix-arg))) 736 | (thing (anzu--thing-end thing)) 737 | (t (point-max)))) 738 | 739 | (defun anzu--begin-thing (at-cursor thing) 740 | (cond ((and at-cursor thing) thing) 741 | ((and at-cursor (not thing)) 'symbol) 742 | (t nil))) 743 | 744 | (defun anzu--replace-backward-p (prefix) 745 | ;; This variable was introduced in Emacs 24.4, I should fix this 746 | ;; variable to version variable 747 | (and (boundp 'list-matching-lines-prefix-face) 748 | (and prefix (< prefix 0)))) 749 | 750 | (defun anzu--construct-perform-replace-arguments (from to delimited beg end backward query) 751 | (if backward 752 | (list from to query t delimited nil nil beg end backward anzu--region-noncontiguous) 753 | (list from to query t delimited nil nil beg end nil anzu--region-noncontiguous))) 754 | 755 | (defun anzu--construct-query-replace-arguments (from to delimited beg end backward) 756 | (if backward 757 | (list from to delimited beg end backward anzu--region-noncontiguous) 758 | (list from to delimited beg end nil anzu--region-noncontiguous))) 759 | 760 | (defsubst anzu--current-replaced-index (curpoint) 761 | (cl-loop for m in anzu--replaced-markers 762 | for i = 1 then (1+ i) 763 | for pos = (marker-position m) 764 | when (= pos curpoint) 765 | return i)) 766 | 767 | (defadvice replace-highlight (before anzu-replace-highlight activate) 768 | (when (and (eq anzu--state 'replace) anzu--replaced-markers) 769 | (let ((index (anzu--current-replaced-index (ad-get-arg 0)))) 770 | (when (or (not index) (/= index anzu--current-position)) 771 | (force-mode-line-update) 772 | (setq anzu--current-position (or index 1)))))) 773 | 774 | (defun anzu--set-replaced-markers (from beg end use-regexp) 775 | (save-excursion 776 | (goto-char beg) 777 | (cl-loop with curbuf = (current-buffer) 778 | with backward = (> beg end) 779 | with input = (if use-regexp from (regexp-quote from)) 780 | with search-func = (if backward #'re-search-backward #'re-search-forward) 781 | with cmp-func = (if backward #'< #'>) 782 | with step = (if backward -1 1) 783 | while (funcall search-func input end t) 784 | do 785 | (progn 786 | (anzu--set-marker (match-beginning 0) curbuf) 787 | (when (= (match-beginning 0) (match-end 0)) 788 | (if (eobp) 789 | (cl-return nil) 790 | (forward-char step))) 791 | (when (and end (funcall cmp-func (point) end)) 792 | (cl-return nil)))))) 793 | 794 | (cl-defun anzu--query-replace-common (use-regexp 795 | &key at-cursor thing prefix-arg (query t) isearch-p) 796 | (anzu--cons-mode-line 'replace-query) 797 | (when (and (use-region-p) (region-noncontiguous-p)) 798 | (setq anzu--region-noncontiguous (funcall region-extract-function 'bounds))) 799 | (let* ((use-region (use-region-p)) 800 | (orig-point (point)) 801 | (backward (anzu--replace-backward-p prefix-arg)) 802 | (overlay-limit (anzu--overlay-limit backward)) 803 | (beg (anzu--region-begin use-region (anzu--begin-thing at-cursor thing) backward)) 804 | (end (anzu--region-end use-region thing backward)) 805 | (prompt (anzu--query-prompt use-region use-regexp at-cursor isearch-p)) 806 | (delimited (and current-prefix-arg (not (eq current-prefix-arg '-)))) 807 | (curbuf (current-buffer)) 808 | (clear-overlay nil)) 809 | (when (and anzu-deactivate-region use-region) 810 | (deactivate-mark t)) 811 | (unwind-protect 812 | (let* ((from (cond ((and at-cursor beg) 813 | (setq delimited nil) 814 | (anzu--query-from-at-cursor curbuf beg end overlay-limit)) 815 | (isearch-p 816 | (anzu--query-from-isearch-string 817 | curbuf beg end use-regexp overlay-limit)) 818 | (t (anzu--query-from-string 819 | prompt beg end use-regexp overlay-limit)))) 820 | (to (cond ((consp from) 821 | (prog1 (cdr from) 822 | (setq from (car from) 823 | anzu--total-matched anzu--last-replaced-count))) 824 | ((string-match "\0" from) 825 | (let ((replaced (substring-no-properties from (match-end 0)))) 826 | (setq from (substring-no-properties from 0 (match-beginning 0))) 827 | (if use-regexp 828 | (anzu--compile-replace-text replaced) 829 | replaced))) 830 | (t 831 | (anzu--query-replace-read-to 832 | from prompt beg end use-regexp overlay-limit))))) 833 | (anzu--clear-overlays curbuf (min beg end) (max beg end)) 834 | (anzu--set-replaced-markers from beg end use-regexp) 835 | (setq anzu--state 'replace anzu--current-position 0 836 | anzu--replaced-markers (reverse anzu--replaced-markers) 837 | clear-overlay t) 838 | (let ((case-fold-search (and case-fold-search (not at-cursor)))) 839 | (if use-regexp 840 | (apply #'perform-replace (anzu--construct-perform-replace-arguments 841 | from to delimited beg end backward query)) 842 | (apply #'query-replace (anzu--construct-query-replace-arguments 843 | from to delimited beg end backward))))) 844 | (progn 845 | (unless clear-overlay 846 | (anzu--clear-overlays curbuf (min beg end) (max beg end))) 847 | (when (zerop anzu--current-position) 848 | (goto-char orig-point)) 849 | (anzu--cleanup-markers) 850 | (anzu--reset-mode-line) 851 | (force-mode-line-update))))) 852 | 853 | ;;;###autoload 854 | (defun anzu-query-replace-at-cursor () 855 | "Replace the symbol at point." 856 | (interactive) 857 | (anzu--query-replace-common t :at-cursor t)) 858 | 859 | ;;;###autoload 860 | (defun anzu-query-replace-at-cursor-thing () 861 | "Replace the thing at point, determined by variable `anzu-replace-at-cursor-thing'." 862 | (interactive) 863 | (anzu--query-replace-common t :at-cursor t :thing anzu-replace-at-cursor-thing)) 864 | 865 | ;;;###autoload 866 | (defun anzu-query-replace (arg) 867 | "Anzu version of `query-replace'." 868 | (interactive "p") 869 | (anzu--query-replace-common nil :prefix-arg arg)) 870 | 871 | ;;;###autoload 872 | (defun anzu-query-replace-regexp (arg) 873 | "Anzu version of `query-replace-regexp'." 874 | (interactive "p") 875 | (anzu--query-replace-common t :prefix-arg arg)) 876 | 877 | ;;;###autoload 878 | (defun anzu-replace-at-cursor-thing () 879 | "Like `anzu-query-replace-at-cursor-thing', but without the query." 880 | (interactive) 881 | (let ((orig (point-marker))) 882 | (anzu--query-replace-common t 883 | :at-cursor t 884 | :thing anzu-replace-at-cursor-thing 885 | :query nil) 886 | (goto-char (marker-position orig)) 887 | (set-marker orig nil))) 888 | 889 | (defun anzu--isearch-query-replace-common (use-regexp arg) 890 | (isearch-done nil t) 891 | (isearch-clean-overlays) 892 | (let ((isearch-recursive-edit nil) 893 | (backward (< (prefix-numeric-value arg) 0))) 894 | (when (and isearch-other-end 895 | (if backward 896 | (> isearch-other-end (point)) 897 | (< isearch-other-end (point))) 898 | (not (and transient-mark-mode mark-active 899 | (if backward 900 | (> (mark) (point)) 901 | (< (mark) (point)))))) 902 | (goto-char isearch-other-end)) 903 | (anzu--query-replace-common use-regexp :prefix-arg arg :isearch-p t))) 904 | 905 | ;;;###autoload 906 | (defun anzu-isearch-query-replace (arg) 907 | "Anzu version of `isearch-query-replace'." 908 | (interactive "p") 909 | (anzu--isearch-query-replace-common nil arg)) 910 | 911 | ;;;###autoload 912 | (defun anzu-isearch-query-replace-regexp (arg) 913 | "Anzu version of `isearch-query-replace-regexp'." 914 | (interactive "p") 915 | (anzu--isearch-query-replace-common t arg)) 916 | 917 | (provide 'anzu) 918 | 919 | ;; Local Variables: 920 | ;; indent-tabs-mode: nil 921 | ;; fill-column: 85 922 | ;; End: 923 | 924 | ;;; anzu.el ends here 925 | -------------------------------------------------------------------------------- /image/anzu-any-position.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsorphanage/anzu/21cb5ab2295614372cb9f1a21429381e49a6255f/image/anzu-any-position.png -------------------------------------------------------------------------------- /image/anzu-replace-demo-noquery.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsorphanage/anzu/21cb5ab2295614372cb9f1a21429381e49a6255f/image/anzu-replace-demo-noquery.gif -------------------------------------------------------------------------------- /image/anzu-replace-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsorphanage/anzu/21cb5ab2295614372cb9f1a21429381e49a6255f/image/anzu-replace-demo.gif -------------------------------------------------------------------------------- /image/anzu-threshold.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsorphanage/anzu/21cb5ab2295614372cb9f1a21429381e49a6255f/image/anzu-threshold.png -------------------------------------------------------------------------------- /image/anzu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emacsorphanage/anzu/21cb5ab2295614372cb9f1a21429381e49a6255f/image/anzu.gif --------------------------------------------------------------------------------