From 175bf6cd5594847b758b176f51cb2fd174252c27 Mon Sep 17 00:00:00 2001 From: Lev Lamberov Date: Sat, 10 Dec 2016 23:47:22 +0500 Subject: New upstream version 0.62 --- .gitignore | 1 + Cask | 8 + Changes | 215 +++++++++ README.md | 221 ++++++++++ anzu.el | 844 ++++++++++++++++++++++++++++++++++++ image/anzu-any-position.png | Bin 0 -> 33418 bytes image/anzu-replace-demo-noquery.gif | Bin 0 -> 164247 bytes image/anzu-replace-demo.gif | Bin 0 -> 257182 bytes image/anzu-threshold.png | Bin 0 -> 40380 bytes image/anzu.gif | Bin 0 -> 867415 bytes 10 files changed, 1289 insertions(+) create mode 100644 .gitignore create mode 100644 Cask create mode 100644 Changes create mode 100644 README.md create mode 100644 anzu.el create mode 100644 image/anzu-any-position.png create mode 100644 image/anzu-replace-demo-noquery.gif create mode 100644 image/anzu-replace-demo.gif create mode 100644 image/anzu-threshold.png create mode 100644 image/anzu.gif diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64b1053 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.cask/ diff --git a/Cask b/Cask new file mode 100644 index 0000000..3352d70 --- /dev/null +++ b/Cask @@ -0,0 +1,8 @@ +(source gnu) +(source melpa) + +(package-file "anzu.el") + +(development + (depends-on "powerline") + (depends-on "migemo")) diff --git a/Changes b/Changes new file mode 100644 index 0000000..2029f9e --- /dev/null +++ b/Changes @@ -0,0 +1,215 @@ +Revision history for anzu.el + +Revision 0.62 2016/08/18 syohex + - Don't search over bounds(#80) + - Correct variable type attribute(#76 Thanks Matus Goljer) + +Revision 0.61 2016/06/17 syohex + - Don't overwrite user's case-fold-search(#75) + - Implement anzu's isearch-query-replace commands(#72) + +Revision 0.60 2016/01/31 syohex + - Add replacement threshold(#66) + - Refactoring code + +Revision 0.59 2015/10/15 syohex + - Fix replacement around region issue(#63 Thanks akicho8) + +Revision 0.58 2015/10/09 syohex + - Fix byte-compile warning(#63 Thanks tarsius) + +Revision 0.57 2015/09/29 syohex + - Fix about migemo exception(#62 Reported by kosh04) + +Revision 0.56 2015/09/13 syohex + - Fix literal replacement issue(#60 Thanks kosh04) + +Revision 0.55 2015/09/11 syohex + - anzu--status declare as buffer local variable + +Revision 0.54 2015/08/03 syohex + - Correct removed hook function name. This makes anzu information is shown if anzu-mode + is disabled. + +Revision 0.53 2015/05/18 syohex + - Fix raising invalid regexp error issue(Thanks kosh04) + - Valid replaced regular expression in regexp replace commands + +Revision 0.52 2015/02/22 syohex + - Fix default input issue(Reported by pronobis) + +Revision 0.51 2015/02/18 syohex + - Improve new history feature of Emacs 25 + (This feature works on both Emacs 24 and 25) + +Revision 0.50 2015/02/11 syohex + - Fix Emacs 24 issue(Reported by LefterisJP) + Can't use both anzu replace commands and builtin replacements commands + +Revision 0.49 2015/02/04 syohex + - Fix zero width replacement causes infinite loop issue(Reported by proofit404) + +Revision 0.48 2015/01/28 syohex + - Fix case when expression(,(...)) cannot be compiled. + +Revision 0.47 2015/01/21 syohex + - Fix displaying wrong current position in replacement commands + +Revision 0.46 2015/01/10 syohex + - Refactoring for evil-anzu + +Revision 0.45 2015/01/09 syohex + - Disable blink-matching-paren in replace commands + +Revision 0.44 2014/12/16 syohex + - Improve replacement commands + - show case sensitive replacement text + - Fix storing replacement history format issue + - We could not use anzu replace commands with normal replacement + commands such as query-replace etc. + +Revision 0.43 2014/11/30 syohex + - Re-factoring for using migemo variable(Thanks lunaryorn) + +Revision 0.42 2014/11/28 syohex + - Replacement by case sensitive for at-cursor commands + +Revision 0.41 2014/10/06 syohex + - Improve checking whether using migemo + +Revision 0.40 2014/10/03 syohex + - Fix replace command bug when using '^' or 'no' command + +Revision 0.39 2014/09/19 syohex + - Fix bug case when bounds-of-thing-at-point returns nil + +Revision 0.38 2014/09/06 syohex + - Improve replace command suggested by DamienCassou + +Revision 0.37 2014/08/27 syohex + - Fix for isearch toggle commands and symbol search + - Improve mode line in replacing commands + +Revision 0.36 2014/07/04 syohex + - Implement replacement only specified lines + +Revision 0.35 2014/04/23 syohex + - Change function name for maintenance + +Revision 0.34 2014/03/28 syohex + - Fix replacement issue + Reported by purcell. + +Revision 0.33 2014/03/19 syohex + - Fix byte compile warnings for Emacs 24.3 + +Revision 0.32 2014/03/08 syohex + - Specify Emacs version + +Revision 0.31 2014/03/07 syohex + - Enable lexical-binding + - Update requirement cl-lib.el version + +Revision 0.30 2014/02/02 syohex + - Fix wrong prompt issue + +Revision 0.29 2014/02/02 syohex + - Add no query replace command + - Use cl-lib instead of cl.el + +Revision 0.28 2014/01/29 syohex + - Fix ignore case issue + +Revision 0.27 2014/01/27 syohex + - Fix non prefix issue + +Revision 0.26 2014/01/16 syohex + - Fix #13 issue + https://github.com/syohex/emacs-anzu/issues/13 + Reported by fukamachi + +Revision 0.25 2014/01/16 syohex + - Support backward replacement for Emacs 24.4 + +Revision 0.24 2013/11/20 syohex + - Fix invalid return type of 'anzu-replace-to-string-separator' + +Revision 0.23 2013/11/08 syohex + - Add 'anzu-replace-to-string-separator' for separator of 'to' string + +Revision 0.22 2013/11/06 syohex + - Fix issue when using isearch-repeat-{forward,backward} + +Revision 0.21 2013/11/05 syohex + - Fix different behavior from query-replace-regexp + +Revision 0.20 2013/11/01 syohex + - Fix case of 0 width match such as '^', '$' + - Improve replace commands(We can use \#, \1 in \,(...)) + - Re-factoring + +Revision 0.19 2013/10/30 syohex + - Fix autoload cookie + - Implement anzu-query-replace-at-cursor-thing + +Revision 0.18 2013/10/30 syohex + - Add switch of deactivate region feature + - Improve anzu-query-replace-at-cursor behavior + +Revision 0.17 2013/10/28 syohex + - Fix case fold issue + +Revision 0.16 2013/10/27 syohex + - Improve for performance at not regexp replace command + +Revision 0.15 2013/10/26 syohex + - Improve replacement commands + +Revision 0.14 2013/10/26 syohex + - Implement 'anzu-query-replace-at-cursor' + +Revision 0.13 2013/10/23 syohex + - Add feature like evil's replacement + +Revision 0.12 2013/10/21 syohex + - Re-factoring: reduce global variables + +Revision 0.11 2013/10/21 syohex + - Change mode-line message at replace commands + +Revision 0.10 2013/10/20 syohex + - Add feature that highlight replaced texts + +Revision 0.09 2013/10/19 syohex + - Implement 'anzu-replace-query' and 'anzu-replace-query-regexp' + +Revision 0.08 2013/10/16 syohex + - Fix that error message is displayed when regexp validation is failed + +Revision 0.07 2013/10/01 syohex + - Introduce anzu-search-threshold + +Revision 0.06 2013/09/30 syohex + - Introduce anzu-minimum-input-length + This is useful for migemo users + +Revision 0.05 2013/09/28 syohex + Thanks Steve Purcell !! + - Improve description line + +Revision 0.04 2013/09/28 syohex + Thanks Bozhidar Batsov!! + - Improve documentation + - Fix byte compile warning + +Revision 0.03 2013/09/22 syohex + - Check isearch-regexp value for checking regexp search + +Revision 0.02 2013/09/22 syohex + - Support display search information at any position in mode-line + (Suggest by gvol) + - Fix case that input is anchor(Report by dakrone) + - Improve non regexp search + +Revision 0.01 2013/09/20 syohex + - Initial version diff --git a/README.md b/README.md new file mode 100644 index 0000000..4d63dbe --- /dev/null +++ b/README.md @@ -0,0 +1,221 @@ +# anzu.el [![melpa badge][melpa-badge]][melpa-link] [![melpa stable badge][melpa-stable-badge]][melpa-stable-link] + +## Introduction + +`anzu.el` is an Emacs port of [anzu.vim](https://github.com/osyo-manga/vim-anzu). +`anzu.el` provides a minor mode which displays *current match* and *total matches* +information in the mode-line in various search modes. + + +## Screenshot + +![anzu.gif](image/anzu.gif) + + +## Requirements + +- Emacs 24 or higher +- `cl-lib` 0.5 or higher (you don't need to install `cl-lib` if you use Emacs 24.3 or higher) + + +## Installation + +You can install `anzu.el` from [MELPA](https://melpa.org/) with `package.el` + +``` + M-x package-install anzu +``` + + +## Basic Usage + +##### `global-anzu-mode` + +Enable global anzu mode: + +```lisp +(global-anzu-mode +1) +``` + +##### `anzu-mode` + +Enable anzu minor mode: + +```lisp +(anzu-mode +1) +``` + +##### `anzu-query-replace` + +Same as `query-replace` except anzu information in mode-line + +##### `anzu-query-replace-regexp` + +Same as `query-replace-regexp` except anzu information in mode-line + + +Add following S-exp in your configuration if you want to use anzu's replace commands by default. + +```lisp +(global-set-key [remap query-replace] 'anzu-query-replace) +(global-set-key [remap query-replace-regexp] 'anzu-query-replace-regexp) +``` + +[anzu-replace-demo](image/anzu-replace-demo.gif) + + +##### `anzu-query-replace-at-cursor` + +Same as `anzu-query-replace` except *from-string* is symbol at cursor + +##### `anzu-query-replace-at-cursor-thing` + +Same as `anzu-query-replace-at-cursor` except replaced region is +specified by `anzu-replace-at-cursor-thing`. + +##### `anzu-replace-at-cursor-thing` + +Same as `anzu-query-replace-at-cursor-thing` except not query. +This command is useful in refactoring such as changing variable name +in the function. + +![anzu-replace-demo](image/anzu-replace-demo-noquery.gif) + + +##### `anzu-isearch-query-replace` + +Anzu version of `isearch-query-replace` + +##### `anzu-isearch-query-replace-regexp` + +Anzu version of `isearch-query-replace-regexp` + +## Customization + +##### `anzu-mode-line` + +Face of mode-line anzu information + +##### `anzu-replace-highlight` + +Face of from-string of replacement + +##### `anzu-replace-to` + +Face of to-string of replacement + +##### `anzu-mode-line-update-function` + +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. + +The function takes 2 integer arguments, current position and total match number. You can get current-state from `anzu--state`(`'search`, `'replace-query`, `replace`). + +```lisp +(defun my/anzu-update-func (here total) + (when anzu--state + (let ((status (cl-case anzu--state + (search (format "<%d/%d>" here total)) + (replace-query (format "(%d Replaces)" total)) + (replace (format "<%d/%d>" here total))))) + (propertize status 'face 'anzu-mode-line)))) + +(custom-set-variables + '(anzu-mode-line-update-function #'my/anzu-update-func)) +``` + +##### `anzu-cons-mode-line-p`(Default is `t`) + +Set `nil` if you want to display anzu information at any position in mode-line. +`anzu.el` cons search information head of `mode-line` as default. + +For example, show search information tail of `minor-mode-alist` + +```lisp +(setq anzu-cons-mode-line-p nil) +(setcar (cdr (assq 'isearch-mode minor-mode-alist)) + '(:eval (anzu--update-mode-line))) +``` + +##### Screenshot + +![anzu-any-position](image/anzu-any-position.png) + + +##### `anzu-mode-lighter` + +Mode name in `mode-line`. Default is ` Anzu`. + + +##### `anzu-input-idle-delay`(Default is `0.05`) + +Delay second of updating mode-line information when you input from-string + +##### `anzu-regexp-search-commands` + +Commands which have regexp input. If the last command is a member of this list, +`anzu.el` treats input as regular expression. + +The default value is `'(isearch-forward-regexp isearch-backward-regexp)`. + +##### `anzu-use-migemo`(Default is `nil`) + +Set to `t` if you use [migemo](https://github.com/emacs-jp/migemo). + +##### `anzu-search-threshold`(Default is `nil`) + +Threshold of searched words. If there are searched word more than this value, +`anzu.el` stops to search and display total number like `1000+`(as default). +If this value is `nil`, `anzu.el` counts all words. + +![anzu-threshold](image/anzu-threshold.png) + +##### `anzu-replace-threshold`(Default is `nil`) + +Threshold of replacement overlay. If this value is `nil`, + +##### `anzu-minimum-input-length`(Default is 1) + +Minimum input length to enable anzu. This parameter is useful for `migemo` users. +Searching 1 or 2 characters with `migemo` is too heavy if buffer is so large. +Please set 3 or higher if you frequently edit such file. + +##### `anzu-deactivate-region`(Default is `nil`) + +Deactivate region at anzu replace command if this value is non-nil. +It is hard to see with anzu replace command when region is active. + + +##### `anzu-replace-at-cursor-thing`(Default is 'defun) + +Thing at point of `anzu-query-replace-at-cursor-thing`. +This parameter is same as `thing-at-point`. + +##### `anzu-replace-to-string-separator`(Default is "") + +Separator of `to` string. + + +## Sample Configuration + +```lisp +(require 'anzu) +(global-anzu-mode +1) + +(set-face-attribute 'anzu-mode-line nil + :foreground "yellow" :weight 'bold) + +(custom-set-variables + '(anzu-mode-lighter "") + '(anzu-deactivate-region t) + '(anzu-search-threshold 1000) + '(anzu-replace-threshold 50) + '(anzu-replace-to-string-separator " => ")) + +(define-key isearch-mode-map [remap isearch-query-replace] #'anzu-isearch-query-replace) +(define-key isearch-mode-map [remap isearch-query-replace-regexp] #'anzu-isearch-query-replace-regexp) +``` + +[melpa-link]: https://melpa.org/#/anzu +[melpa-stable-link]: https://stable.melpa.org/#/anzu +[melpa-badge]: https://melpa.org/packages/anzu-badge.svg +[melpa-stable-badge]: https://stable.melpa.org/packages/anzu-badge.svg diff --git a/anzu.el b/anzu.el new file mode 100644 index 0000000..8612f4b --- /dev/null +++ b/anzu.el @@ -0,0 +1,844 @@ +;;; anzu.el --- Show number of matches in mode-line while searching -*- lexical-binding: t; -*- + +;; Copyright (C) 2016 by Syohei YOSHIDA + +;; Author: Syohei YOSHIDA +;; URL: https://github.com/syohex/emacs-anzu +;; Version: 0.62 +;; Package-Requires: ((cl-lib "0.5") (emacs "24")) + +;; This program 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. + +;; This program 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 this program. If not, see . + +;;; Commentary: + +;; `anzu.el' is an Emacs port of `anzu.vim'. +;; +;; `anzu.el' provides a minor mode which displays 'current match/total +;; matches' in the mode-line in various search modes. This makes it +;; easy to understand how many matches there are in the current buffer +;; for your search query. + +;; To use this package, add following code to your init.el or .emacs +;; +;; (global-anzu-mode +1) +;; + +;;; Code: + +(require 'cl-lib) +(require 'thingatpt) + +(declare-function migemo-forward 'migemo) + +(defgroup anzu nil + "Show searched position in mode-line" + :group 'isearch) + +(defcustom anzu-mode-lighter " Anzu" + "Lighter of anzu-mode" + :type 'string) + +(defcustom anzu-cons-mode-line-p t + "Set nil if you use your own mode-line setting" + :type 'boolean) + +(defcustom anzu-minimum-input-length 1 + "Minimum input length to enable anzu" + :type 'integer) + +(defcustom anzu-search-threshold nil + "Limit of search number" + :type '(choice (integer :tag "Threshold of search") + (const :tag "No threshold" nil))) + +(defcustom anzu-replace-threshold nil + "Limit of replacement overlays." + :type '(choice (integer :tag "Threshold of replacement overlays") + (const :tag "No threshold" nil))) + +(defcustom anzu-use-migemo nil + "Flag of using migemo" + :type 'boolean) + +(defcustom anzu-mode-line-update-function #'anzu--update-mode-line-default + "Function which return mode-line string. This must be non-nil." + :type 'function) + +(defcustom anzu-regexp-search-commands '(isearch-forward-regexp + isearch-backward-regexp) + "Search function which use regexp." + :type '(repeat function)) + +(defcustom anzu-input-idle-delay 0.05 + "Idle second for updating modeline at replace commands" + :type 'number) + +(defcustom anzu-deactivate-region nil + "Deactive region if you use anzu a replace command with region" + :type 'boolean) + +(defcustom anzu-replace-at-cursor-thing 'defun + "Replace thing. This parameter is same as `thing-at-point'" + :type 'symbol) + +(defcustom anzu-replace-to-string-separator "" + "Separator of `to' string" + :type 'string) + +(defface anzu-mode-line + '((t (:foreground "magenta" :weight bold))) + "face of anzu modeline") + +(defface anzu-replace-highlight + '((t :inherit query-replace)) + "highlight of replaced string") + +(defface anzu-match-1 + '((((class color) (background light)) + :background "aquamarine" :foreground "black") + (((class color) (background dark)) + :background "limegreen" :foreground "black") + (t :inverse-video t)) + "First group of match.") + +(defface anzu-match-2 + '((((class color) (background light)) + :background "springgreen" :foreground "black") + (((class color) (background dark)) + :background "yellow" :foreground "black") + (t :inverse-video t)) + "Second group of match.") + +(defface anzu-match-3 + '((((class color) (background light)) + :background "yellow" :foreground "black") + (((class color) (background dark)) + :background "aquamarine" :foreground "black") + (t :inverse-video t)) + "Third group of match.") + +(defface anzu-replace-to + '((((class color) (background light)) + :foreground "red") + (((class color) (background dark)) + :foreground "yellow")) + "highlight of replace string") + +(defvar anzu--total-matched 0) +(defvar anzu--current-position 0) +(defvar anzu--overflow-p nil) +(defvar anzu--last-isearch-string nil) +(defvar anzu--cached-positions nil) +(defvar anzu--last-command nil) +(defvar anzu--state nil) +(defvar anzu--cached-count 0) +(defvar anzu--last-replace-input "") +(defvar anzu--last-search-state nil) +(defvar anzu--last-replaced-count nil) +(defvar anzu--outside-point nil) +(defvar anzu--history nil) +(defvar anzu--query-defaults nil) + +(defun anzu--validate-regexp (regexp) + (condition-case nil + (progn + (string-match-p regexp "") + t) + (invalid-regexp nil))) + +(defsubst anzu--construct-position-info (count overflow positions) + (list :count count :overflow overflow :positions positions)) + +(defsubst anzu--case-fold-search (input) + (when case-fold-search + (let ((case-fold-search nil)) + (not (string-match-p "[A-Z]" input))))) + +(defsubst anzu--word-search-p () + (and (not (memq anzu--last-command anzu-regexp-search-commands)) + (not isearch-regexp))) + +(defun anzu--transform-input (str) + (cond ((eq isearch-word 'isearch-symbol-regexp) + (setq str (isearch-symbol-regexp str))) + ((anzu--word-search-p) + (setq str (regexp-quote str))) + (t str))) + +(defsubst anzu--use-migemo-p () + (when anzu-use-migemo + (unless (featurep 'migemo) + (error "Error: migemo is not loaded")) + (bound-and-true-p migemo-isearch-enable-p))) + +(defun anzu--search-all-position (str) + (unless anzu--last-command + (setq anzu--last-command last-command)) + (let ((input (anzu--transform-input str))) + (if (not (anzu--validate-regexp input)) + anzu--cached-positions + (save-excursion + (goto-char (point-min)) + (let ((positions '()) + (count 0) + (overflow nil) + (finish nil) + (search-func (if (anzu--use-migemo-p) + (lambda (word &optional bound noerror count) + (ignore-errors + (migemo-forward word bound noerror count))) + #'re-search-forward)) + (case-fold-search (anzu--case-fold-search input))) + (while (and (not finish) (funcall search-func input nil t)) + (push (cons (match-beginning 0) (match-end 0)) positions) + (cl-incf count) + (when (= (match-beginning 0) (match-end 0)) ;; Case of anchor such as "^" + (if (eobp) + (setq finish t) + (forward-char 1))) + (when (and anzu-search-threshold (>= count anzu-search-threshold)) + (setq overflow t finish t))) + (let ((result (anzu--construct-position-info count overflow (reverse positions)))) + (setq anzu--cached-positions (copy-sequence result)) + result)))))) + +(defun anzu--where-is-here (positions here) + (cl-loop for (start . end) in positions + for i = 1 then (1+ i) + when (and (>= here start) (<= here end)) + return i + finally return 0)) + +(defun anzu--use-result-cache-p (input) + (and (eq isearch-word (car anzu--last-search-state)) + (eq isearch-regexp (cdr anzu--last-search-state)) + (string= input anzu--last-isearch-string))) + +(defun anzu--update (query) + (when (>= (length query) anzu-minimum-input-length) + (let ((result (if (anzu--use-result-cache-p query) + anzu--cached-positions + (anzu--search-all-position query)))) + (let ((curpos (anzu--where-is-here (plist-get result :positions) (point)))) + (setq anzu--total-matched (plist-get result :count) + anzu--overflow-p (plist-get result :overflow) + anzu--current-position curpos + anzu--last-search-state (cons isearch-word isearch-regexp) + anzu--last-isearch-string query) + (force-mode-line-update))))) + +(defun anzu--update-post-hook () + (anzu--update isearch-string)) + +(defconst anzu--mode-line-format '(:eval (anzu--update-mode-line))) + +(defsubst anzu--mode-line-not-set-p () + (and (listp mode-line-format) + (member anzu--mode-line-format mode-line-format))) + +(defun anzu--cons-mode-line-search () + (anzu--cons-mode-line 'search)) + +(defun anzu--cons-mode-line (state) + (setq anzu--state state) + (when (and anzu-cons-mode-line-p (not (anzu--mode-line-not-set-p))) + (setq mode-line-format (cons anzu--mode-line-format mode-line-format)))) + +(defsubst anzu--reset-status () + (setq anzu--total-matched 0 + anzu--current-position 0 + anzu--state nil + anzu--last-command nil + anzu--last-isearch-string nil + anzu--overflow-p nil)) + +(defun anzu--reset-mode-line () + (anzu--reset-status) + (when (and anzu-cons-mode-line-p (anzu--mode-line-not-set-p)) + (setq mode-line-format (delete anzu--mode-line-format mode-line-format)))) + +(defsubst anzu--format-here-position (here total) + (if (and anzu--overflow-p (zerop here)) + (format "%d+" total) + here)) + +(defun anzu--update-mode-line-default (here total) + (when anzu--state + (let ((status (cl-case anzu--state + (search (format "(%s/%d%s)" + (anzu--format-here-position here total) + total (if anzu--overflow-p "+" ""))) + (replace-query (format "(%d replace)" total)) + (replace (format "(%d/%d)" here total))))) + (propertize status 'face 'anzu-mode-line)))) + +(defun anzu--update-mode-line () + (funcall anzu-mode-line-update-function anzu--current-position anzu--total-matched)) + +;;;###autoload +(define-minor-mode anzu-mode + "minor-mode which display search information in mode-line." + :init-value nil + :global nil + :lighter anzu-mode-lighter + (if anzu-mode + (progn + (set (make-local-variable 'anzu--state) nil) + (add-hook 'isearch-update-post-hook #'anzu--update-post-hook nil t) + (add-hook 'isearch-mode-hook #'anzu--cons-mode-line-search nil t) + (add-hook 'isearch-mode-end-hook #'anzu--reset-mode-line nil t)) + (remove-hook 'isearch-update-post-hook #'anzu--update-post-hook t) + (remove-hook 'isearch-mode-hook #'anzu--cons-mode-line-search t) + (remove-hook 'isearch-mode-end-hook #'anzu--reset-mode-line t) + (anzu--reset-mode-line))) + +(defun anzu--turn-on () + (unless (minibufferp) + (anzu-mode +1))) + +;;;###autoload +(define-globalized-minor-mode global-anzu-mode anzu-mode anzu--turn-on) + +(defsubst anzu--query-prompt-base (use-region use-regexp) + (concat "Query replace" + (if current-prefix-arg " word" "") + (if use-regexp " regexp" "") + (if use-region " in region" "")) ) + +(defun anzu--query-prompt (use-region use-regexp at-cursor isearch-p) + (let ((prompt (anzu--query-prompt-base use-region use-regexp))) + (if (and anzu--query-defaults (not at-cursor) (not isearch-p)) + (format "%s (default %s -> %s) " + prompt + (query-replace-descr (caar anzu--query-defaults)) + (query-replace-descr (cdar anzu--query-defaults))) + prompt))) + +(defvar anzu--replaced-markers nil) +(defsubst anzu--set-marker (beg buf) + (let ((m (make-marker))) + (set-marker m beg buf) + (push m anzu--replaced-markers))) + +(defun anzu--make-overlay (begin end face prio) + (let ((ov (make-overlay begin end))) + (overlay-put ov 'face face) + (overlay-put ov 'priority prio) + (overlay-put ov 'anzu-overlay t) + ov)) + +(defun anzu--add-match-group-overlay (match-data groups) + (when (>= groups 3) + (anzu--make-overlay (cl-fifth match-data) (cl-sixth match-data) + 'anzu-match-3 1001)) + (when (>= groups 2) + (anzu--make-overlay (cl-third match-data) (cl-fourth match-data) + 'anzu-match-2 1001)) + (anzu--make-overlay (cl-first match-data) (cl-second match-data) + 'anzu-match-1 1001)) + +(defun anzu--add-overlay (beg end) + (let* ((match-data (match-data)) + (groups (/ (- (length match-data) 2) 2))) + (when (>= groups 1) + (anzu--add-match-group-overlay (cddr match-data) groups)) + (let ((ov (anzu--make-overlay beg end 'anzu-replace-highlight 1000))) + (overlay-put ov 'from-string (buffer-substring-no-properties beg end)) + (overlay-put ov 'anzu-replace t)))) + +(defsubst anzu--cleanup-markers () + (mapc (lambda (m) (set-marker m nil)) anzu--replaced-markers) + (setq anzu--replaced-markers nil)) + +;; Return highlighted count +(defun anzu--count-and-highlight-matched (buf str replace-beg replace-end + use-regexp overlay-limit case-sensitive) + (anzu--cleanup-markers) + (when (not use-regexp) + (setq str (regexp-quote str))) + (if (not (anzu--validate-regexp str)) + anzu--cached-count + (with-current-buffer buf + (save-excursion + (let* ((overlay-beg replace-beg) + (overlay-end (min replace-end overlay-limit))) + (goto-char overlay-beg) + (let ((count 0) + (overlayed 0) + (finish nil) + (case-fold-search (if case-sensitive + nil + (anzu--case-fold-search str)))) + (while (and (not finish) (re-search-forward str replace-end t)) + (cl-incf count) + (let ((beg (match-beginning 0)) + (end (match-end 0))) + (when (= beg end) + (if (eobp) + (setq finish t) + (forward-char 1))) + (when (and replace-end (> (point) replace-end)) + (setq finish t)) + (when (and (>= beg overlay-beg) (<= end overlay-end) (not finish)) + (cl-incf overlayed) + (anzu--add-overlay beg end)))) + (setq anzu--cached-count count) + overlayed)))))) + +(defun anzu--search-outside-visible (buf input beg end use-regexp) + (let ((searchfn (if use-regexp #'re-search-forward #'search-forward))) + (when (or (not use-regexp) (anzu--validate-regexp input)) + (with-selected-window (get-buffer-window buf) + (goto-char beg) + (when (funcall searchfn input end t) + (setq anzu--outside-point (match-beginning 0)) + (let ((overlay-limit (anzu--overlay-limit))) + (anzu--count-and-highlight-matched buf input beg end use-regexp + overlay-limit nil))))))) + +(defconst anzu--from-to-separator + (propertize + (or (ignore-errors + (if (char-displayable-p ?\u2192) " \u2192 " " -> ")) + " -> ") + 'face 'minibuffer-prompt)) + +(defsubst anzu--separator () + (propertize "\0" 'display anzu--from-to-separator 'separator t)) + +(defun anzu--check-minibuffer-input (buf beg end use-regexp overlay-limit) + (let* ((content (minibuffer-contents)) + (to (when (and (string-match (anzu--separator) content) + (get-text-property (match-beginning 0) 'separator content)) + (substring-no-properties content (match-end 0)))) + (from (or (and to (substring-no-properties content 0 (match-beginning 0))) + content)) + (empty-p (string= from "")) + (overlayed (if empty-p + (setq anzu--cached-count 0) + (anzu--count-and-highlight-matched buf from beg end use-regexp + overlay-limit nil)))) + (when anzu--outside-point + (setq anzu--outside-point nil) + (with-selected-window (get-buffer-window buf) + (goto-char beg))) + (when (and (not empty-p) (zerop overlayed)) + (anzu--search-outside-visible buf from beg end use-regexp)) + (when to + (setq anzu--last-replace-input "") + (anzu--append-replaced-string to buf beg end use-regexp overlay-limit from)) + (setq anzu--total-matched anzu--cached-count) + (force-mode-line-update))) + +(defun anzu--clear-overlays (buf beg end) + (with-current-buffer buf + (dolist (ov (overlays-in (or beg (point-min)) (or end (point-max)))) + (when (overlay-get ov 'anzu-overlay) + (delete-overlay ov))))) + +(defun anzu--transform-from-to-history () + (let ((separator (anzu--separator))) + (append (mapcar (lambda (from-to) + (concat (query-replace-descr (car from-to)) + separator + (query-replace-descr (cdr from-to)))) + anzu--query-defaults) + (symbol-value query-replace-from-history-variable)))) + +(defun anzu--read-from-string (prompt beg end use-regexp overlay-limit) + (let ((curbuf (current-buffer)) + (blink-matching-paren nil) + (anzu--history (anzu--transform-from-to-history)) + timer is-input) + (unwind-protect + (minibuffer-with-setup-hook + #'(lambda () + (setq timer (run-with-idle-timer + (max anzu-input-idle-delay 0.01) + 'repeat + (lambda () + (anzu--clear-overlays curbuf nil nil) + (with-selected-window (or (active-minibuffer-window) + (minibuffer-window)) + (anzu--check-minibuffer-input + curbuf beg end use-regexp overlay-limit)))))) + (prog1 (read-from-minibuffer (format "%s: " prompt) + nil nil nil 'anzu--history nil t) + (setq is-input t))) + (when timer + (cancel-timer timer) + (setq timer nil) + (unless is-input + (goto-char beg)))))) + +(defun anzu--query-validate-from-regexp (from) + (when (string-match "\\(?:\\`\\|[^\\]\\)\\(?:\\\\\\\\\\)*\\(\\\\[nt]\\)" from) + (let ((match (match-string 1 from))) + (cond + ((string= match "\\n") + (message "`\\n' here doesn't match a newline; type C-q C-j instead!!")) + ((string= match "\\t") + (message "\\t' here doesn't match a tab; to do that, just type TAB!!"))) + (sit-for 2)))) + +(defun anzu--query-from-string (prompt beg end use-regexp overlay-limit) + (let* ((from (anzu--read-from-string prompt beg end use-regexp overlay-limit)) + (is-empty (string= from ""))) + (when (and (not is-empty) (not anzu--query-defaults)) + (setq anzu--last-replaced-count anzu--total-matched)) + (if (and is-empty anzu--query-defaults) + (cons (query-replace-descr (caar anzu--query-defaults)) + (query-replace-compile-replacement + (query-replace-descr (cdar anzu--query-defaults)) use-regexp)) + (add-to-history query-replace-from-history-variable from nil t) + (when use-regexp + (unless (anzu--validate-regexp from) + (error "'%s' is invalid regexp." from)) + (anzu--query-validate-from-regexp from)) + from))) + +(defun anzu--compile-replace-text (str) + (let ((compiled (ignore-errors + (query-replace-compile-replacement str t)))) + (when compiled + (cond ((stringp compiled) compiled) + ((and (consp compiled) (functionp (car compiled))) + compiled) + ((and (consp compiled) (stringp (car compiled))) + (car compiled)))))) + +(defun anzu--evaluate-occurrence (ov to-regexp replacements fixed-case from-regexp) + (let ((from-string (overlay-get ov 'from-string)) + (compiled (anzu--compile-replace-text to-regexp))) + (if (not compiled) + "" + (with-temp-buffer + (insert from-string) + (goto-char (point-min)) + (when (re-search-forward from-regexp nil t) + (or (ignore-errors + (if (consp compiled) + (replace-match (funcall (car compiled) (cdr compiled) + replacements) fixed-case) + (replace-match compiled fixed-case)) + (buffer-substring (point-min) (point-max))) + "")))))) + +(defun anzu--overlay-sort (a b) + (< (overlay-start a) (overlay-start b))) + +(defsubst anzu--overlays-in-range (beg end) + (cl-loop for ov in (overlays-in beg end) + when (overlay-get ov 'anzu-replace) + collect ov into anzu-overlays + finally + return + (let ((sorted (sort anzu-overlays 'anzu--overlay-sort))) + (if anzu-replace-threshold + (cl-subseq sorted 0 (min (length sorted) anzu-replace-threshold)) + sorted)))) + +(defsubst anzu--propertize-to-string (str) + (let ((separator (or anzu-replace-to-string-separator ""))) + (propertize (concat separator str) 'face 'anzu-replace-to))) + +(defsubst anzu--replaced-literal-string (ov replaced from) + (let ((str (buffer-substring-no-properties + (overlay-start ov) (overlay-end ov)))) + (when (string-match (regexp-quote str) from) + (replace-match replaced (not case-fold-search) t str)))) + +(defun anzu--append-replaced-string (content buf beg end use-regexp overlay-limit from) + (let ((replacements 0)) + (unless (string= content anzu--last-replace-input) + (setq anzu--last-replace-input content) + (with-current-buffer buf + (let ((case-fold-search (anzu--case-fold-search from))) + (dolist (ov (anzu--overlays-in-range beg (min end overlay-limit))) + (let ((replace-evaled + (if (not use-regexp) + (anzu--replaced-literal-string ov content from) + (prog1 (anzu--evaluate-occurrence ov content replacements + (not case-fold-search) from) + (cl-incf replacements))))) + (overlay-put ov 'after-string (anzu--propertize-to-string replace-evaled))))))))) + +(defsubst anzu--outside-overlay-limit (orig-beg orig-limit) + (save-excursion + (goto-char (+ anzu--outside-point (- orig-limit orig-beg))) + (line-end-position))) + +(defun anzu--read-to-string (from prompt beg end use-regexp overlay-limit) + (let ((curbuf (current-buffer)) + (orig-beg beg) + (to-prompt (format "%s %s with: " prompt (query-replace-descr from))) + (history-add-new-input nil) + (blink-matching-paren nil) + timer is-input) + (setq anzu--last-replace-input "") + (when anzu--outside-point + (setq beg anzu--outside-point + overlay-limit (anzu--outside-overlay-limit orig-beg overlay-limit) + anzu--outside-point nil)) + (unwind-protect + (minibuffer-with-setup-hook + #'(lambda () + (setq timer (run-with-idle-timer + (max anzu-input-idle-delay 0.01) + 'repeat + (lambda () + (with-selected-window (or (active-minibuffer-window) + (minibuffer-window)) + (anzu--append-replaced-string + (minibuffer-contents) + curbuf beg end use-regexp overlay-limit from)))))) + (prog1 (read-from-minibuffer to-prompt + nil nil nil + query-replace-from-history-variable nil t) + (setq is-input t))) + (when timer + (cancel-timer timer) + (setq timer nil) + (unless is-input + (goto-char orig-beg)))))) + +(defun anzu--query-replace-read-to (from prompt beg end use-regexp overlay-limit) + (query-replace-compile-replacement + (let ((to (anzu--read-to-string from prompt beg end use-regexp overlay-limit))) + (add-to-history query-replace-to-history-variable to nil t) + (add-to-history 'anzu--query-defaults (cons from to) nil t) + to) + use-regexp)) + +(defun anzu--overlay-limit () + (save-excursion + (move-to-window-line -1) + (forward-line 1) + (point))) + +(defun anzu--query-from-at-cursor (buf beg end overlay-limit) + (let ((symbol (thing-at-point 'symbol))) + (unless symbol + (error "No symbol at cursor!!")) + (let ((symbol-regexp (concat "\\_<" (regexp-quote symbol) "\\_>"))) + (anzu--count-and-highlight-matched buf symbol-regexp beg end t overlay-limit t) + (setq anzu--total-matched anzu--cached-count) + (force-mode-line-update) + symbol-regexp))) + +(defun anzu--query-from-isearch-string (buf beg end use-regexp overlay-limit) + (anzu--count-and-highlight-matched buf isearch-string beg end use-regexp overlay-limit t) + (setq anzu--total-matched anzu--cached-count) + (force-mode-line-update) + (add-to-history query-replace-from-history-variable isearch-string nil t) + isearch-string) + +(defun anzu--thing-begin (thing) + (let ((bound (bounds-of-thing-at-point thing))) + (if bound + (car bound) + (let ((fallback-bound (bounds-of-thing-at-point 'symbol))) + (if fallback-bound + (car fallback-bound) + (point)))))) + +(defsubst anzu--thing-end (thing) + (let ((bound (bounds-of-thing-at-point thing))) + (if bound + (cdr bound) + (point-max)))) + +(defun anzu--region-begin (use-region thing backward) + (cond (use-region (region-beginning)) + (current-prefix-arg (line-beginning-position)) + (thing (anzu--thing-begin thing)) + (backward (point-min)) + (t (point)))) + +(defsubst anzu--line-end-position (num) + (save-excursion + (forward-line (1- num)) + (line-end-position))) + +(defun anzu--region-end (use-region thing) + (cond (use-region (region-end)) + (current-prefix-arg + (anzu--line-end-position (prefix-numeric-value current-prefix-arg))) + (thing (anzu--thing-end thing)) + (t (point-max)))) + +(defun anzu--begin-thing (at-cursor thing) + (cond ((and at-cursor thing) thing) + ((and at-cursor (not thing)) 'symbol) + (t nil))) + +(defun anzu--replace-backward-p (prefix) + ;; This variable is introduced at Emacs 24.4, I should fix this variable to + ;; version variable + (and (boundp 'list-matching-lines-prefix-face) + (and prefix (< prefix 0)))) + +(defun anzu--construct-perform-replace-arguments (from to delimited beg end backward query) + (if backward + (list from to query t delimited nil nil beg end backward) + (list from to query t delimited nil nil beg end))) + +(defun anzu--construct-query-replace-arguments (from to delimited beg end backward) + (if backward + (list from to delimited beg end backward) + (list from to delimited beg end))) + +(defsubst anzu--current-replaced-index (curpoint) + (cl-loop for m in anzu--replaced-markers + for i = 1 then (1+ i) + for pos = (marker-position m) + when (= pos curpoint) + return i)) + +(defadvice replace-highlight (before anzu-replace-highlight activate) + (when (and (eq anzu--state 'replace) anzu--replaced-markers) + (let ((index (anzu--current-replaced-index (ad-get-arg 0)))) + (when (or (not index) (/= index anzu--current-position)) + (force-mode-line-update) + (setq anzu--current-position (or index 1)))))) + +(defun anzu--set-replaced-markers (from beg end use-regexp) + (save-excursion + (goto-char beg) + (cl-loop with curbuf = (current-buffer) + with search-func = (if use-regexp #'re-search-forward #'search-forward) + while (funcall search-func from end t) + do + (progn + (anzu--set-marker (match-beginning 0) curbuf) + (when (= (match-beginning 0) (match-end 0)) + (if (eobp) + (cl-return nil) + (forward-char 1))) + (when (and end (> (point) end)) + (cl-return nil)))))) + +(cl-defun anzu--query-replace-common (use-regexp + &key at-cursor thing prefix-arg (query t) isearch-p) + (anzu--cons-mode-line 'replace-query) + (let* ((use-region (use-region-p)) + (orig-point (point)) + (backward (anzu--replace-backward-p prefix-arg)) + (overlay-limit (anzu--overlay-limit)) + (beg (anzu--region-begin use-region (anzu--begin-thing at-cursor thing) backward)) + (end (anzu--region-end use-region thing)) + (prompt (anzu--query-prompt use-region use-regexp at-cursor isearch-p)) + (delimited (and current-prefix-arg (not (eq current-prefix-arg '-)))) + (curbuf (current-buffer)) + (clear-overlay nil)) + (when (and anzu-deactivate-region use-region) + (deactivate-mark t)) + (unwind-protect + (let* ((from (cond ((and at-cursor beg) + (setq delimited nil) + (anzu--query-from-at-cursor curbuf beg end overlay-limit)) + (isearch-p + (anzu--query-from-isearch-string + curbuf beg end use-regexp overlay-limit)) + (t (anzu--query-from-string + prompt beg end use-regexp overlay-limit)))) + (to (cond ((consp from) + (prog1 (cdr from) + (setq from (car from) + anzu--total-matched anzu--last-replaced-count))) + ((string-match "\0" from) + (prog1 (substring-no-properties from (match-end 0)) + (setq from (substring-no-properties from 0 (match-beginning 0))))) + (t + (anzu--query-replace-read-to + from prompt beg end use-regexp overlay-limit))))) + (anzu--clear-overlays curbuf beg end) + (anzu--set-replaced-markers from beg end use-regexp) + (setq anzu--state 'replace anzu--current-position 0 + anzu--replaced-markers (reverse anzu--replaced-markers) + clear-overlay t) + (let ((case-fold-search (and case-fold-search (not at-cursor)))) + (if use-regexp + (apply #'perform-replace (anzu--construct-perform-replace-arguments + from to delimited beg end backward query)) + (apply #'query-replace (anzu--construct-query-replace-arguments + from to delimited beg end backward))))) + (progn + (unless clear-overlay + (anzu--clear-overlays curbuf beg end)) + (when (zerop anzu--current-position) + (goto-char orig-point)) + (anzu--cleanup-markers) + (anzu--reset-mode-line) + (force-mode-line-update))))) + +;;;###autoload +(defun anzu-query-replace-at-cursor () + (interactive) + (anzu--query-replace-common t :at-cursor t)) + +;;;###autoload +(defun anzu-query-replace-at-cursor-thing () + (interactive) + (anzu--query-replace-common t :at-cursor t :thing anzu-replace-at-cursor-thing)) + +;;;###autoload +(defun anzu-query-replace (arg) + (interactive "p") + (anzu--query-replace-common nil :prefix-arg arg)) + +;;;###autoload +(defun anzu-query-replace-regexp (arg) + (interactive "p") + (anzu--query-replace-common t :prefix-arg arg)) + +;;;###autoload +(defun anzu-replace-at-cursor-thing () + (interactive) + (let ((orig (point-marker))) + (anzu--query-replace-common t + :at-cursor t + :thing anzu-replace-at-cursor-thing + :query nil) + (goto-char (marker-position orig)) + (set-marker orig nil))) + +(defun anzu--isearch-query-replace-common (use-regexp arg) + (isearch-done nil t) + (isearch-clean-overlays) + (let ((isearch-recursive-edit nil) + (backward (< (prefix-numeric-value arg) 0))) + (when (and isearch-other-end + (if backward + (> isearch-other-end (point)) + (< isearch-other-end (point))) + (not (and transient-mark-mode mark-active + (if backward + (> (mark) (point)) + (< (mark) (point)))))) + (goto-char isearch-other-end)) + (anzu--query-replace-common use-regexp :prefix-arg arg :isearch-p t))) + +;;;###autoload +(defun anzu-isearch-query-replace (arg) + (interactive "p") + (anzu--isearch-query-replace-common nil arg)) + +;;;###autoload +(defun anzu-isearch-query-replace-regexp (arg) + (interactive "p") + (anzu--isearch-query-replace-common t arg)) + +(provide 'anzu) +;;; anzu.el ends here diff --git a/image/anzu-any-position.png b/image/anzu-any-position.png new file mode 100644 index 0000000..cf14612 Binary files /dev/null and b/image/anzu-any-position.png differ diff --git a/image/anzu-replace-demo-noquery.gif b/image/anzu-replace-demo-noquery.gif new file mode 100644 index 0000000..1da15a9 Binary files /dev/null and b/image/anzu-replace-demo-noquery.gif differ diff --git a/image/anzu-replace-demo.gif b/image/anzu-replace-demo.gif new file mode 100644 index 0000000..b1181da Binary files /dev/null and b/image/anzu-replace-demo.gif differ diff --git a/image/anzu-threshold.png b/image/anzu-threshold.png new file mode 100644 index 0000000..eeb4347 Binary files /dev/null and b/image/anzu-threshold.png differ diff --git a/image/anzu.gif b/image/anzu.gif new file mode 100644 index 0000000..d3b2458 Binary files /dev/null and b/image/anzu.gif differ -- cgit v1.2.3