Personal emacs config
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1037 lines
44 KiB

  1. ;;; visual-regexp.el --- A regexp/replace command for Emacs with interactive visual feedback
  2. ;; Copyright (C) 2013-2019 Marko Bencun
  3. ;; Author: Marko Bencun <mbencun@gmail.com>
  4. ;; URL: https://github.com/benma/visual-regexp.el/
  5. ;; Package-Version: 1.1.2
  6. ;; Package-Commit: 3e3ed81a3cbadef1f1f4cb16f9112a58641d70ca
  7. ;; Version: 1.1
  8. ;; Package-Requires: ((cl-lib "0.2"))
  9. ;; Keywords: regexp, replace, visual, feedback
  10. ;; This file is part of visual-regexp.
  11. ;; visual-regexp is free software: you can redistribute it and/or modify
  12. ;; it under the terms of the GNU General Public License as published by
  13. ;; the Free Software Foundation, either version 3 of the License, or
  14. ;; (at your option) any later version.
  15. ;; visual-regexp is distributed in the hope that it will be useful,
  16. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. ;; GNU General Public License for more details.
  19. ;; You should have received a copy of the GNU General Public License
  20. ;; along with visual-regexp. If not, see <http://www.gnu.org/licenses/>.
  21. ;;; WHAT'S NEW
  22. ;; 1.1: Add new customization: vr/plain
  23. ;; 1.0: Add support for one prompt for search/replace, using query-replace-from-to-separator
  24. ;; (query-replace history like in Emacs 25).
  25. ;; Breaking changes:
  26. ;; - vr/minibuffer-(regexp|replace)-keymap have been collapsed to vr/minibuffer-keymap
  27. ;; - vr/minibuffer-help-(regexp|replace) have been replaced by vr--minibuffer-help-text
  28. ;; 0.9: Fix warnings regarding free variables.
  29. ;; 0.8: Error handling for vr--get-regexp-string. Bug-fixes regarding error display.
  30. ;; 0.7: Customizable separator (arrow) string and face.
  31. ;; 0.6: distinguish prompts in vr/replace, vr/query-replace, vr/mc-mark.
  32. ;; 0.5: emulate case-conversion of replace-regexp.
  33. ;; 0.4: vr/mc-mark: interface to multiple-cursors.
  34. ;; 0.3: use the same history as the regular Emacs replace commands;
  35. ;; 0.2: support for lisp expressions in the replace string, same as in (query-)replace-regexp
  36. ;; 0.1: initial release
  37. ;;; Tip Jar
  38. ;; If you found this useful, please consider donating.
  39. ;; BTC: 1BxauiLGMQPb2pavkkQkuFe5CgrGMrUat2
  40. ;;; What's This?
  41. ;; visual-regexp for Emacs is like `replace-regexp`, but with live visual feedback directly in the buffer.
  42. ;; While constructing the regexp in the minibuffer, you get live visual feedback for the matches, including group matches.
  43. ;; While constructing the replacement in the minibuffer, you get live visual feedback for the replacements.
  44. ;; It can be used to replace all matches in one go (like `replace-regexp`), or a decision can be made on each match (like `query-replace-regexp`).
  45. ;; Thanks to Detlev Zundel for his re-builder.
  46. ;;; Where does visual-regexp come from?
  47. ;;
  48. ;; I was not happy with the way I used emacs' replace-regexp before. Constructing the regular expression is error prone and emacs' regular expressions are limited
  49. ;; (for example, no lookaheads, named groups, etc.).
  50. ;; Using re-builder to interactively build regular expressions was a step into the right direction, but manually copying over the regexp
  51. ;; to the minibuffer is cumbersome.
  52. ;; Using the idea of interactive feedback of re-builder, this package makes it possible to use just the minibuffer to construct (with live visual feedback) the regexp and replacement,
  53. ;; using Emacs style regular expressions, or optionally, regular expressions powered by other (mode modern) engines, for the replacement. For the latter part, see the package visual-regexp-steroids.
  54. ;;; Installation
  55. ;; If you are using Emacs 24, you can get visual-regexp from [melpa](https://melpa.org/) with the package manager.
  56. ;; Add the following code to your init file. Of course you can select your own key bindings.
  57. ;; ----------------------------------------------------------
  58. ;; (add-to-list 'load-path "folder-in-which-visual-regexp-files-are-in/") ;; if the files are not already in the load path
  59. ;; (require 'visual-regexp)
  60. ;; (define-key global-map (kbd "C-c r") 'vr/replace)
  61. ;; (define-key global-map (kbd "C-c q") 'vr/query-replace)
  62. ;; ;; if you use multiple-cursors, this is for you:
  63. ;; (define-key global-map (kbd "C-c m") 'vr/mc-mark)
  64. ;; ----------------------------------------------------------
  65. ;; To customize, use `M-x customize-group [RET] visual-regexp`.
  66. ;;; Code:
  67. (unless (fboundp 'make-overlay)
  68. (require 'overlay))
  69. ;; cl is used for the (loop ...) macro
  70. (require 'cl-lib)
  71. ;;; faces
  72. (defcustom vr/match-separator-use-custom-face nil
  73. "If activated, vr/match-separator-face is used to display the separator. Otherwise, use the same face as the current match."
  74. :type 'boolean
  75. :group 'visual-regexp)
  76. (defface vr/match-separator-face
  77. '((((class color))
  78. :foreground "red"
  79. :bold t)
  80. (t
  81. :inverse-video t))
  82. "Face for the arrow between match and replacement. To use this, you must activate vr/match-separator-use-custom-face"
  83. :group 'visual-regexp)
  84. ;; For Emacs < 25.0, this variable is not yet defined.
  85. ;; Copy pasted from Emacs 25.0 replace.el.
  86. (unless (boundp 'query-replace-from-to-separator)
  87. (defcustom query-replace-from-to-separator
  88. (propertize (if (char-displayable-p ?→) "" " -> ")
  89. 'face 'minibuffer-prompt)
  90. "String that separates FROM and TO in the history of replacement pairs."
  91. ;; Avoids error when attempt to autoload char-displayable-p fails
  92. ;; while preparing to dump, also stops customize-rogue listing this.
  93. :initialize 'custom-initialize-delay
  94. :type 'sexp))
  95. (defcustom vr/match-separator-string
  96. (progn
  97. (custom-reevaluate-setting 'query-replace-from-to-separator)
  98. (substring-no-properties query-replace-from-to-separator))
  99. "This string is used to separate a match from the replacement during feedback."
  100. :type 'sexp
  101. :initialize 'custom-initialize-delay
  102. :group 'visual-regexp)
  103. (defface vr/match-0
  104. '((((class color) (background light))
  105. :background "lightblue")
  106. (((class color) (background dark))
  107. :background "steelblue4")
  108. (t
  109. :inverse-video t))
  110. "First face for displaying a whole match."
  111. :group 'visual-regexp)
  112. (defface vr/match-1
  113. '((((class color) (background light))
  114. :background "pale turquoise")
  115. (((class color) (background dark))
  116. :background "dodgerblue4")
  117. (t
  118. :inverse-video t))
  119. "Second face for displaying a whole match."
  120. :group 'visual-regexp)
  121. (defface vr/group-0
  122. '((((class color) (background light))
  123. :background "aquamarine")
  124. (((class color) (background dark))
  125. :background "blue3")
  126. (t
  127. :inverse-video t))
  128. "First face for displaying a matching group."
  129. :group 'visual-regexp)
  130. (defface vr/group-1
  131. '((((class color) (background light))
  132. :background "springgreen")
  133. (((class color) (background dark))
  134. :background "chartreuse4")
  135. (t
  136. :inverse-video t))
  137. "Second face for displaying a matching group."
  138. :group 'visual-regexp)
  139. (defface vr/group-2
  140. '((((min-colors 88) (class color) (background light))
  141. :background "yellow1")
  142. (((class color) (background light))
  143. :background "yellow")
  144. (((class color) (background dark))
  145. :background "sienna4")
  146. (t
  147. :inverse-video t))
  148. "Third face for displaying a matching group."
  149. :group 'visual-regexp)
  150. ;;; variables
  151. (defcustom vr/auto-show-help t
  152. "Show help message automatically when the minibuffer is entered."
  153. :type 'boolean
  154. :group 'visual-regexp)
  155. (defcustom vr/default-feedback-limit 50
  156. "Limit number of matches shown in visual feedback.
  157. If nil, don't limit the number of matches shown in visual feedback."
  158. :type 'integer
  159. :group 'visual-regexp)
  160. (defcustom vr/default-replace-preview nil
  161. "Preview of replacement activated by default? If activated, the original is not shown alongside the replacement."
  162. :type 'boolean
  163. :group 'visual-regexp)
  164. (defcustom vr/query-replace-from-history-variable query-replace-from-history-variable
  165. "History list to use for the FROM argument. The default is to use the same history as Emacs' query-replace commands."
  166. :type 'symbol
  167. :group 'visual-regexp)
  168. (defcustom vr/query-replace-to-history-variable query-replace-to-history-variable
  169. "History list to use for the TO argument. The default is to use the same history as Emacs' query-replace commands."
  170. :type 'symbol
  171. :group 'visual-regexp)
  172. (setq vr--is-emacs24 (version< emacs-version "25"))
  173. (defvar vr--query-replace-defaults nil
  174. "Same as query-replace-defaults from Emacs 25, for compatibility with Emacs 24.")
  175. (defcustom vr/query-replace-defaults-variable
  176. (if vr--is-emacs24
  177. 'vr--query-replace-defaults
  178. 'query-replace-defaults)
  179. "History of search/replace pairs"
  180. :type 'symbol
  181. :group 'visual-regexp)
  182. (defcustom vr/plain nil
  183. "If non-nil, use plain search/replace instead of regexp search/replace."
  184. :type 'boolean
  185. :group 'visual-regexp)
  186. (defvar vr/initialize-hook nil
  187. "Hook called before vr/replace and vr/query-replace")
  188. ;;; private variables
  189. (defconst vr--match-faces '(vr/match-0 vr/match-1)
  190. "Faces in list for convenience")
  191. (defconst vr--group-faces '(vr/group-0 vr/group-1 vr/group-2)
  192. "Faces in list for convenience")
  193. (defconst vr--overlay-priority 1001
  194. "Starting priority of visual-regexp overlays.")
  195. (defvar vr--in-minibuffer nil
  196. "Is visual-regexp currently being used?")
  197. (defvar vr--calling-func nil
  198. "Which function invoked vr--interactive-get-args?")
  199. (defvar vr--last-minibuffer-contents nil
  200. "Keeping track of minibuffer changes")
  201. (defvar vr--target-buffer-start nil
  202. "Starting position in target buffer.")
  203. (defvar vr--target-buffer-end nil
  204. "Ending position in target buffer.")
  205. (defvar vr--limit-reached)
  206. (defvar vr--regexp-string nil
  207. "Entered regexp.")
  208. (defvar vr--replace-string nil
  209. "Entered replacement.")
  210. (defvar vr--feedback-limit nil
  211. "Feedback limit currently in use.")
  212. (defvar vr--replace-preview nil
  213. "Preview of replacement activated?")
  214. (defvar vr--target-buffer nil
  215. "Buffer to which visual-regexp is applied to.")
  216. (defvar vr--overlays (make-hash-table :test 'equal)
  217. "Overlays used in target buffer.")
  218. (defvar vr--visible-overlays (list)
  219. "Overlays currently visible.")
  220. (defvar vr--minibuffer-message-overlay nil)
  221. ;;; keymap
  222. (defvar vr/minibuffer-keymap
  223. (let ((map (copy-keymap minibuffer-local-map)))
  224. (define-key map (kbd "C-c ?") 'vr--minibuffer-help)
  225. (define-key map (kbd "C-c a") 'vr--shortcut-toggle-limit)
  226. (define-key map (kbd "C-c p") 'vr--shortcut-toggle-preview)
  227. map)
  228. "Keymap used while using visual-regexp,")
  229. ;;; helper functions
  230. (defun vr--shortcut-toggle-preview ()
  231. (interactive)
  232. (when (vr--in-replace)
  233. (setq vr--replace-preview (not vr--replace-preview))
  234. (vr--update-minibuffer-prompt)
  235. (vr--do-replace-feedback)))
  236. (defun vr--shortcut-toggle-limit ()
  237. "Toggle the limit of overlays shown (default limit / no limit)"
  238. (interactive)
  239. (if vr--feedback-limit
  240. (setq vr--feedback-limit nil)
  241. (setq vr--feedback-limit vr/default-feedback-limit))
  242. (vr--show-feedback))
  243. (defun vr--get-regexp-string-full ()
  244. (if (equal vr--in-minibuffer 'vr--minibuffer-regexp)
  245. (minibuffer-contents)
  246. vr--regexp-string))
  247. (defun vr--query-replace--split-string (string)
  248. "Copy/paste of query-replace--split-string, removing the assertion."
  249. (let* ((length (length string))
  250. (split-pos (text-property-any 0 length 'separator t string)))
  251. (if (not split-pos)
  252. (substring-no-properties string)
  253. (cons (substring-no-properties string 0 split-pos)
  254. (substring-no-properties string (1+ split-pos) length)))))
  255. (defun vr--in-from ()
  256. "Returns t if the we are in the regexp prompt. Returns nil if we are in the replace prompt. Call only if (and vr--in-minibuffer (minibufferp))"
  257. (equal vr--in-minibuffer 'vr--minibuffer-regexp))
  258. (defun vr--in-replace ()
  259. "Returns t if we are either in the replace prompt, or in the regexp prompt containing a replacement (separated by vr/match-separator-string)"
  260. (or (not (vr--in-from))
  261. (consp (vr--query-replace--split-string (vr--get-regexp-string-full)))))
  262. (defun vr--get-regexp-string (&optional for-display)
  263. (let ((split (vr--query-replace--split-string (vr--get-regexp-string-full))))
  264. (if (consp split) (car split) split)))
  265. (defun vr--get-replace-string ()
  266. (if (equal vr--in-minibuffer 'vr--minibuffer-replace)
  267. (minibuffer-contents-no-properties)
  268. (let ((split (vr--query-replace--split-string (vr--get-regexp-string-full))))
  269. (if (consp split) (cdr split) vr--replace-string))))
  270. (defun vr--format-error (err)
  271. (if (eq (car err) 'error)
  272. (car (cdr err))
  273. (format "%s" err)))
  274. ;;; minibuffer functions
  275. (defun vr--set-minibuffer-prompt ()
  276. (let ((prompt (cond ((equal vr--calling-func 'vr--calling-func-query-replace)
  277. "Query replace")
  278. ((equal vr--calling-func 'vr--calling-func-mc-mark)
  279. "Mark")
  280. (t
  281. "Replace"))))
  282. (when (and (vr--in-replace) vr--replace-preview)
  283. (setq prompt (concat prompt " (preview)")))
  284. (when (not (vr--in-from))
  285. (setq prompt (concat prompt " " (vr--get-regexp-string t))))
  286. (setq prompt (concat prompt (if (vr--in-from) ": " " with: ")))
  287. prompt))
  288. (defun vr--update-minibuffer-prompt ()
  289. (when (and vr--in-minibuffer (minibufferp))
  290. (let ((inhibit-read-only t)
  291. (prompt (vr--set-minibuffer-prompt)))
  292. (put-text-property (point-min) (minibuffer-prompt-end) 'display prompt))))
  293. (defun vr--minibuffer-message (message &rest args)
  294. "Adaptation of minibuffer-message that does not use sit-for
  295. to make the message disappear. The problem with this was that during sit-for,
  296. the cursor was shown at the beginning of the message regardless of whether
  297. the point was actually there or not. Workaround: we let the message stay
  298. visible all the time in the minibuffer."
  299. (if (not (and vr--in-minibuffer (minibufferp (current-buffer))))
  300. ;; fallback
  301. (apply 'minibuffer-message message args)
  302. ;; Clear out any old echo-area message to make way for our new thing.
  303. (message nil)
  304. (setq message (concat " [" message "]"))
  305. (when args (setq message (apply 'format message args)))
  306. (unless (zerop (length message))
  307. ;; The current C cursor code doesn't know to use the overlay's
  308. ;; marker's stickiness to figure out whether to place the cursor
  309. ;; before or after the string, so let's spoon-feed it the pos.
  310. (put-text-property 0 1 'cursor t message))
  311. (unless (overlayp vr--minibuffer-message-overlay)
  312. (setq vr--minibuffer-message-overlay (make-overlay (point-max) (point-max) nil t t)))
  313. (move-overlay vr--minibuffer-message-overlay (point-max) (point-max))
  314. (overlay-put vr--minibuffer-message-overlay 'after-string message)))
  315. (defun vr--minibuffer-help-text ()
  316. (let ((help ""))
  317. (setq help (concat help (substitute-command-keys "\\<vr/minibuffer-keymap>\\[vr--minibuffer-help]: help, \\[vr--shortcut-toggle-limit]: toggle show all, \\[previous-history-element]: previous")))
  318. (when (vr--in-replace)
  319. (setq help (concat help (substitute-command-keys ", \\[vr--shortcut-toggle-preview]: toggle preview"))))
  320. help
  321. ))
  322. (defun vr--minibuffer-help ()
  323. (interactive)
  324. (vr--minibuffer-message (vr--minibuffer-help-text)))
  325. ;;; overlay functions
  326. (defun vr--get-overlay (i j)
  327. "i: match index, j: submatch index"
  328. (let (overlay)
  329. (setq overlay (gethash (list i j) vr--overlays))
  330. (unless overlay ;; create new one if overlay does not exist yet
  331. (setq overlay (make-overlay 0 0))
  332. (if (= 0 j)
  333. (overlay-put overlay 'face (nth (mod i (length vr--match-faces)) vr--match-faces))
  334. (overlay-put overlay 'face (nth (mod j (length vr--group-faces)) vr--group-faces)))
  335. (overlay-put overlay 'priority (+ vr--overlay-priority (if (= j 0) 0 1)))
  336. (overlay-put overlay 'vr-ij (list i j))
  337. (puthash (list i j) overlay vr--overlays))
  338. overlay))
  339. (defun vr--delete-overlays ()
  340. "Delete all visible overlays."
  341. (mapc (lambda (overlay)
  342. (delete-overlay overlay))
  343. vr--visible-overlays)
  344. (setq vr--visible-overlays (list)))
  345. (defun vr--delete-overlay-display (overlay)
  346. (overlay-put overlay 'display nil)
  347. (overlay-put overlay 'after-string nil)
  348. (overlay-put overlay 'priority vr--overlay-priority))
  349. (defun vr--delete-overlay-displays ()
  350. "Delete the display of all visible overlays. Call before vr--delete-overlays."
  351. (mapc (lambda (overlay)
  352. (cl-multiple-value-bind (i j) (overlay-get overlay 'vr-ij)
  353. (when (= 0 j)
  354. (vr--delete-overlay-display overlay))))
  355. vr--visible-overlays))
  356. ;;; hooks
  357. (defun vr--show-feedback ()
  358. (if (vr--in-replace)
  359. (vr--do-replace-feedback)
  360. (vr--feedback)))
  361. (defun vr--after-change (beg end len)
  362. (when (and vr--in-minibuffer (minibufferp))
  363. ;; minibuffer-up temporarily deletes minibuffer contents before inserting new one.
  364. ;; don't do anything then as the messages shown by visual-regexp are irritating while browsing the history.
  365. (unless (and (string= "" (minibuffer-contents-no-properties))
  366. (equal last-command 'previous-history-element))
  367. ;; do something when minibuffer contents changes
  368. (unless (string= vr--last-minibuffer-contents (minibuffer-contents-no-properties))
  369. (setq vr--last-minibuffer-contents (minibuffer-contents-no-properties))
  370. ;; minibuffer contents has changed, update visual feedback.
  371. ;; not using after-change-hook because this hook applies to the whole minibuffer, including minibuffer-messages
  372. ;; that disappear after a while.
  373. (vr--update-minibuffer-prompt)
  374. (vr--show-feedback)))))
  375. (defun vr--minibuffer-setup ()
  376. "Setup prompt and help when entering minibuffer."
  377. (when vr--in-minibuffer
  378. (progn
  379. (vr--update-minibuffer-prompt)
  380. (when vr/auto-show-help (vr--minibuffer-help)))))
  381. ;;; helper functions
  382. (defun vr--target-window ()
  383. (if vr--target-buffer
  384. (get-buffer-window vr--target-buffer)
  385. nil))
  386. (defun vr--compose-messages (&rest msgs)
  387. (mapconcat 'identity (delq nil (mapcar (lambda (msg) (if (or (not msg) (string= "" msg)) nil msg)) msgs)) " - "))
  388. ;;; show feedback functions
  389. (defun vr--feedback-function (regexp-string forward feedback-limit callback)
  390. "Feedback function for emacs-style regexp search"
  391. (let ((message-line "")
  392. (err))
  393. (with-current-buffer vr--target-buffer
  394. (save-excursion
  395. (goto-char (if forward vr--target-buffer-start vr--target-buffer-end))
  396. (let ((i 0)
  397. (looping t))
  398. (while (and looping
  399. (condition-case err
  400. (if forward
  401. (funcall (if vr/plain 'search-forward 're-search-forward) regexp-string vr--target-buffer-end t)
  402. (funcall (if vr/plain 'search-backward 're-search-backward) regexp-string vr--target-buffer-start t))
  403. (invalid-regexp (progn (setq message-line (car (cdr err))) nil))))
  404. (when (or (not feedback-limit) (< i feedback-limit)) ;; let outer loop finish so we can get the matches count
  405. (cl-loop for (start end) on (match-data) by 'cddr
  406. for j from 0
  407. when (and start end)
  408. do
  409. (funcall callback i j start end)))
  410. (when (= (match-beginning 0) (match-end 0))
  411. (cond ;; don't get stuck on zero-width matches
  412. ((and forward (> vr--target-buffer-end (point))) (forward-char))
  413. ((and (not forward) (< vr--target-buffer-start (point))) (backward-char))
  414. (t (setq looping nil))))
  415. (setq i (1+ i)))
  416. (if (string= "" message-line)
  417. (setq message-line (format "%s matches" i))))))
  418. message-line))
  419. (defun vr--feedback-match-callback (i j begin end)
  420. (with-current-buffer vr--target-buffer
  421. (save-excursion
  422. (when (= 0 i) ;; make first match visible
  423. (with-selected-window (vr--target-window)
  424. (goto-char end)))
  425. (let ((overlay (vr--get-overlay i j)))
  426. (move-overlay overlay begin end vr--target-buffer)
  427. (if (and (= 0 j) (= begin end)) ;; empty match; indicate by a pipe
  428. (overlay-put overlay 'after-string (propertize "|" 'face (nth (mod i (length vr--match-faces)) vr--match-faces) 'help-echo "empty match"))
  429. (overlay-put overlay 'after-string nil))
  430. (setq vr--visible-overlays (cons overlay vr--visible-overlays)))
  431. ;; mark if we have reached the specified feedback limit
  432. (when (and vr--feedback-limit (= vr--feedback-limit (1+ i)) )
  433. (setq vr--limit-reached t)))))
  434. (defun vr--feedback (&optional inhibit-message)
  435. "Show visual feedback for matches."
  436. (vr--delete-overlays)
  437. (setq vr--limit-reached nil)
  438. (let (message-line)
  439. (setq message-line
  440. (condition-case err
  441. (progn
  442. (vr--feedback-function (vr--get-regexp-string) t vr--feedback-limit 'vr--feedback-match-callback))
  443. (error (vr--format-error err))))
  444. (unless inhibit-message
  445. (let ((msg (vr--compose-messages message-line (when vr--limit-reached (format "%s matches shown, hit C-c a to show all" vr--feedback-limit)))))
  446. (unless (string= "" msg)
  447. (vr--minibuffer-message msg))))))
  448. (defun vr--get-replacement (replacement match-data i)
  449. (with-current-buffer vr--target-buffer
  450. (let*
  451. ;; emulate case-conversion of (perform-replace)
  452. ((case-fold-search (if (and case-fold-search search-upper-case)
  453. (ignore-errors (isearch-no-upper-case-p (vr--get-regexp-string) t))
  454. case-fold-search))
  455. (nocasify (not (and case-replace case-fold-search))))
  456. ;; we need to set the match data again, s.t. match-substitute-replacement works correctly.
  457. ;; (match-data) could have been modified in the meantime, e.g. by vr--get-regexp-string->pcre-to-elisp.
  458. (set-match-data match-data)
  459. (if (stringp replacement)
  460. (match-substitute-replacement replacement nocasify vr/plain)
  461. (match-substitute-replacement (funcall (car replacement) (cdr replacement) i) nocasify vr/plain)))))
  462. (defun vr--do-replace-feedback-match-callback (replacement match-data i)
  463. (let ((begin (cl-first match-data))
  464. (end (cl-second match-data))
  465. (replacement (vr--get-replacement replacement match-data i)))
  466. (let* ((overlay (vr--get-overlay i 0))
  467. (empty-match (= begin end)))
  468. (move-overlay overlay begin end vr--target-buffer)
  469. (vr--delete-overlay-display overlay)
  470. (let ((current-face (nth (mod i (length vr--match-faces)) vr--match-faces)))
  471. (if (or empty-match vr--replace-preview)
  472. (progn
  473. (overlay-put overlay (if empty-match 'after-string 'display) (propertize replacement 'face current-face))
  474. (overlay-put overlay 'priority (+ vr--overlay-priority 2)))
  475. (progn
  476. (overlay-put overlay 'after-string
  477. (concat (propertize vr/match-separator-string 'face
  478. (if vr/match-separator-use-custom-face
  479. 'vr/match-separator-face
  480. current-face))
  481. (propertize replacement 'face current-face)))
  482. (overlay-put overlay 'priority (+ vr--overlay-priority 0))))))))
  483. (defun vr--mapcar-nonnil (rep list)
  484. (mapcar (lambda (it) (when it (funcall rep it))) list))
  485. (defun vr--get-replacements (feedback feedback-limit)
  486. "Get replacements using emacs-style regexp."
  487. (setq vr--limit-reached nil)
  488. (let ((regexp-string)
  489. (replace-string (vr--get-replace-string))
  490. (message-line "")
  491. (i 0)
  492. (replacements (list))
  493. (err)
  494. (buffer-contents (with-current-buffer vr--target-buffer
  495. (buffer-substring-no-properties (point-min) (point-max)))))
  496. (condition-case err
  497. (progn
  498. ;; can signal invalid-regexp
  499. (setq regexp-string (vr--get-regexp-string))
  500. (with-current-buffer vr--target-buffer
  501. (goto-char vr--target-buffer-start)
  502. (let ((looping t))
  503. (while (and
  504. looping
  505. (condition-case err
  506. (funcall (if vr/plain 'search-forward 're-search-forward) regexp-string vr--target-buffer-end t)
  507. ('invalid-regexp (progn (setq message-line (car (cdr err))) nil))))
  508. (condition-case err
  509. (progn
  510. (if (or (not feedback) (not feedback-limit) (< i feedback-limit))
  511. (setq replacements (cons
  512. (let ((match-data (vr--mapcar-nonnil 'marker-position (match-data))))
  513. (list (query-replace-compile-replacement replace-string t) match-data i))
  514. replacements))
  515. (setq vr--limit-reached t))
  516. (when (= (match-beginning 0) (match-end 0))
  517. (if (> vr--target-buffer-end (point))
  518. (forward-char) ;; don't get stuck on zero-width matches
  519. (setq looping nil)))
  520. (setq i (1+ i)))
  521. ('error (progn
  522. (setq message-line (vr--format-error err))
  523. (setq replacements (list))
  524. (setq looping nil))))))))
  525. (invalid-regexp (setq message-line (car (cdr err))))
  526. (error (setq message-line (vr--format-error err))))
  527. (if feedback
  528. (if (string= "" message-line)
  529. (setq message-line (vr--compose-messages (format "%s matches" i) (when vr--limit-reached (format "%s matches shown, hit C-c a to show all" feedback-limit)))))
  530. (setq message-line (format "replaced %d matches" i)))
  531. (list replacements message-line)))
  532. (defun vr--do-replace-feedback ()
  533. "Show visual feedback for replacements."
  534. (vr--feedback t) ;; only really needed when regexp has not been changed from default (=> no overlays have been created)
  535. (cl-multiple-value-bind (replacements message-line) (vr--get-replacements t vr--feedback-limit)
  536. ;; visual feedback for matches
  537. (condition-case err
  538. (mapc (lambda (replacement-info) (apply 'vr--do-replace-feedback-match-callback replacement-info)) replacements)
  539. ('error (setq message-line (vr--format-error err))))
  540. (unless (string= "" message-line)
  541. (vr--minibuffer-message message-line))))
  542. ;;; vr/replace
  543. (defun vr--do-replace (&optional silent)
  544. "Replace matches."
  545. (vr--delete-overlay-displays)
  546. (vr--delete-overlays)
  547. (cl-multiple-value-bind (replacements message-line) (vr--get-replacements nil nil)
  548. (let ((replace-count 0)
  549. (cumulative-offset 0)
  550. last-match-data)
  551. (cl-loop for replacement-info in replacements
  552. for counter from 0 do
  553. (setq replace-count (1+ replace-count))
  554. (cl-multiple-value-bind (replacement match-data i) replacement-info
  555. ;; replace match
  556. (let* ((replacement (vr--get-replacement replacement match-data i))
  557. (begin (cl-first match-data))
  558. (end (cl-second match-data)))
  559. (with-current-buffer vr--target-buffer
  560. (save-excursion
  561. ;; first insert, then delete
  562. ;; this ensures that if we had an active region before, the replaced match is still part of the region
  563. (goto-char begin)
  564. (insert replacement)
  565. (setq cumulative-offset (+ cumulative-offset (- (point) end)))
  566. (delete-char (- end begin))))
  567. (when (= 0 counter)
  568. (setq last-match-data match-data))
  569. )))
  570. (unless (or silent (string= "" message-line))
  571. (vr--minibuffer-message message-line))
  572. ;; needed to correctly position the mark after query replace (finished with 'automatic ('!'))
  573. (set-match-data (vr--mapcar-nonnil (lambda (el) (+ cumulative-offset el)) last-match-data))
  574. replace-count)))
  575. (defun vr--set-target-buffer-start-end ()
  576. (setq vr--target-buffer-start (if (region-active-p)
  577. (region-beginning)
  578. (point)))
  579. (setq vr--target-buffer-end (if (region-active-p)
  580. (region-end)
  581. (point-max))))
  582. (defun vr--set-regexp-string ()
  583. (save-excursion
  584. ;; deactivate mark so that we can see our faces instead of region-face.
  585. (deactivate-mark)
  586. (setq vr--in-minibuffer 'vr--minibuffer-regexp)
  587. (setq vr--last-minibuffer-contents "")
  588. (custom-reevaluate-setting 'vr/match-separator-string)
  589. (let* ((minibuffer-allow-text-properties t)
  590. (history-add-new-input nil)
  591. (text-property-default-nonsticky
  592. (cons '(separator . t) text-property-default-nonsticky))
  593. ;; seperator and query-replace-from-to-history copy/pasted from replace.el
  594. (separator
  595. (when vr/match-separator-string
  596. (propertize "\0"
  597. 'display vr/match-separator-string
  598. 'separator t)))
  599. (query-replace-from-to-history
  600. (append
  601. (when separator
  602. (mapcar (lambda (from-to)
  603. (concat (query-replace-descr (car from-to))
  604. separator
  605. (query-replace-descr (cdr from-to))))
  606. (symbol-value vr/query-replace-defaults-variable)))
  607. (symbol-value vr/query-replace-from-history-variable))))
  608. (setq vr--regexp-string
  609. (read-from-minibuffer
  610. " " ;; prompt will be set in vr--minibuffer-setup
  611. nil vr/minibuffer-keymap
  612. nil 'query-replace-from-to-history))
  613. (let ((split (vr--query-replace--split-string vr--regexp-string)))
  614. (if (not (consp split))
  615. (add-to-history vr/query-replace-from-history-variable vr--regexp-string nil t)
  616. (add-to-history vr/query-replace-from-history-variable (car split) nil t)
  617. (add-to-history vr/query-replace-to-history-variable (cdr split) nil t)
  618. (add-to-history vr/query-replace-defaults-variable split nil t))))))
  619. (defun vr--set-replace-string ()
  620. (save-excursion
  621. ;; deactivate mark so that we can see our faces instead of region-face.
  622. (deactivate-mark)
  623. (let ((split (vr--query-replace--split-string vr--regexp-string)))
  624. (unless (consp split)
  625. (setq vr--in-minibuffer 'vr--minibuffer-replace)
  626. (setq vr--last-minibuffer-contents "")
  627. (let ((history-add-new-input nil))
  628. (setq vr--replace-string
  629. (read-from-minibuffer
  630. " " ;; prompt will be set in vr--minibuffer-setup
  631. nil vr/minibuffer-keymap
  632. nil vr/query-replace-to-history-variable))
  633. (add-to-history vr/query-replace-to-history-variable vr--replace-string nil t)
  634. (add-to-history vr/query-replace-defaults-variable (cons vr--regexp-string vr--replace-string)))))))
  635. (defun vr--interactive-get-args (mode calling-func)
  636. "Get interactive args for the vr/replace and vr/query-replace functions."
  637. (unwind-protect
  638. (progn
  639. (let ((buffer-read-only t)) ;; make target buffer
  640. (when vr--in-minibuffer (error "visual-regexp already in use."))
  641. (add-hook 'after-change-functions 'vr--after-change)
  642. (add-hook 'minibuffer-setup-hook 'vr--minibuffer-setup)
  643. (setq vr--calling-func calling-func)
  644. (setq vr--target-buffer (current-buffer))
  645. (vr--set-target-buffer-start-end)
  646. (run-hooks 'vr/initialize-hook)
  647. (setq vr--feedback-limit vr/default-feedback-limit)
  648. (setq vr--replace-preview vr/default-replace-preview)
  649. (vr--set-regexp-string)
  650. (when (equal mode 'vr--mode-regexp-replace)
  651. (vr--set-replace-string))
  652. ;; Successfully got the args, deactivate mark now. If the command was aborted (C-g), the mark (region) would remain active.
  653. (deactivate-mark)
  654. (cond ((equal mode 'vr--mode-regexp-replace)
  655. (list vr--regexp-string
  656. vr--replace-string
  657. vr--target-buffer-start
  658. vr--target-buffer-end))
  659. ((equal mode 'vr--mode-regexp)
  660. (list vr--regexp-string
  661. vr--target-buffer-start
  662. vr--target-buffer-end)))))
  663. (progn ;; execute on finish
  664. (setq vr--in-minibuffer nil)
  665. (remove-hook 'after-change-functions 'vr--after-change)
  666. (remove-hook 'minibuffer-setup-hook 'vr--minibuffer-setup)
  667. (setq vr--calling-func nil)
  668. (unless (overlayp vr--minibuffer-message-overlay)
  669. (delete-overlay vr--minibuffer-message-overlay))
  670. (vr--delete-overlay-displays)
  671. (vr--delete-overlays))))
  672. (add-hook 'multiple-cursors-mode-enabled-hook
  673. ;; run vr/mc-mark once per cursor by default (do not ask the user)
  674. (lambda ()
  675. (when (boundp 'mc--default-cmds-to-run-once)
  676. (add-to-list 'mc--default-cmds-to-run-once 'vr/mc-mark))))
  677. ;;;###autoload
  678. (defun vr/mc-mark (regexp start end)
  679. "Convert regexp selection to multiple cursors."
  680. (require 'multiple-cursors)
  681. (interactive
  682. (vr--interactive-get-args 'vr--mode-regexp 'vr--calling-func-mc-mark))
  683. (with-current-buffer vr--target-buffer
  684. (mc/remove-fake-cursors)
  685. (activate-mark)
  686. (let (;; disable deactivating of mark after buffer-editing commands
  687. ;; (which happens for example in visual-regexp-steroids/vr--parse-matches
  688. ;; during the callback).
  689. (deactivate-mark nil)
  690. (first-fake-cursor nil))
  691. (vr--feedback-function (vr--get-regexp-string) t nil (lambda (i j begin end)
  692. (when (zerop j)
  693. (with-current-buffer vr--target-buffer
  694. (goto-char end)
  695. (push-mark begin)
  696. ;; temporarily enable transient mark mode
  697. (activate-mark)
  698. (let ((fc (mc/create-fake-cursor-at-point)))
  699. (unless first-fake-cursor
  700. (setq first-fake-cursor fc)))))))
  701. ;; one fake cursor too many, replace first one with
  702. ;; the regular cursor.
  703. (when first-fake-cursor
  704. (mc/pop-state-from-overlay first-fake-cursor)))
  705. (mc/maybe-multiple-cursors-mode)))
  706. ;;;###autoload
  707. (defun vr/replace (regexp replace start end)
  708. "Regexp-replace with live visual feedback."
  709. (interactive
  710. (vr--interactive-get-args 'vr--mode-regexp-replace 'vr--calling-func-replace))
  711. (unwind-protect
  712. (progn
  713. (when vr--in-minibuffer (error "visual-regexp already in use."))
  714. (setq vr--target-buffer (current-buffer)
  715. vr--target-buffer-start start
  716. vr--target-buffer-end end
  717. vr--regexp-string regexp
  718. vr--replace-string replace)
  719. ;; do replacement
  720. (vr--do-replace))
  721. ;; execute on finish
  722. (setq vr--in-minibuffer nil)))
  723. ;; query-replace-regexp starts here
  724. (defvar vr--query-replacements nil)
  725. ;; we redefine the help text from replace.el to remove the commands we don't support.
  726. (defconst vr--query-replace-help
  727. "Type Space or `y' to replace one match, Delete or `n' to skip to next,
  728. RET or `q' to exit, Period to replace one match and exit,
  729. Comma to replace but not move point immediately,
  730. p to preview the replacement (like 'C-c p' during construction of the regexp),
  731. C-r [not supported in visual-regexp],
  732. C-w [not supported in visual-regexp],
  733. C-l to clear the screen, redisplay, and offer same replacement again,
  734. ! to replace all remaining matches with no more questions,
  735. ^ [not supported in visual-regexp],
  736. E [not supported in visual-regexp]"
  737. "Help message while in `vr/query-replace'.")
  738. (defvar vr--query-replace-map
  739. (let ((map (make-sparse-keymap)))
  740. (set-keymap-parent map query-replace-map)
  741. ;; the following replace.el commands are not supported by visual-regexp.
  742. (define-key map "e" nil)
  743. (define-key map "E" nil)
  744. (define-key map "\C-r" nil)
  745. (define-key map "\C-w" nil)
  746. (define-key map "^" nil)
  747. (define-key map "p" 'toggle-preview)
  748. map
  749. ))
  750. ;;;###autoload
  751. (defun vr/query-replace (regexp replace start end)
  752. "Use vr/query-replace like you would use query-replace-regexp."
  753. (interactive
  754. (vr--interactive-get-args 'vr--mode-regexp-replace 'vr--calling-func-query-replace))
  755. (unwind-protect
  756. (progn
  757. (when vr--in-minibuffer (error "visual-regexp already in use."))
  758. (setq vr--target-buffer (current-buffer)
  759. vr--target-buffer-start start
  760. vr--target-buffer-end end
  761. vr--regexp-string regexp
  762. vr--replace-string replace)
  763. (vr--perform-query-replace))
  764. ;; execute on finish
  765. (setq vr--in-minibuffer nil)))
  766. (defun vr--perform-query-replace ()
  767. ;; This function is a heavily modified version of (perform-replace) from replace.el.
  768. ;; The original plan was to use the original perform-replace, but various issues stood in the way.
  769. (and minibuffer-auto-raise
  770. (raise-frame (window-frame (minibuffer-window))))
  771. (let* ((from-string (vr--get-regexp-string))
  772. (map vr--query-replace-map)
  773. (vr--query-replacements (nreverse (car (vr--get-replacements nil nil))))
  774. (next-replacement nil) ;; replacement string for current match
  775. (keep-going t)
  776. (replace-count 0)
  777. ;; a match can be replaced by a longer/shorter replacement. cumulate the difference
  778. (cumulative-offset 0)
  779. (recenter-last-op nil) ; Start cycling order with initial position.
  780. (message
  781. (concat
  782. (propertize "Replacing " 'read-only t)
  783. (propertize "%s" 'read-only t 'face 'font-lock-keyword-face)
  784. (propertize " with " 'read-only t)
  785. (propertize "%s" 'read-only t 'face 'font-lock-keyword-face)
  786. (propertize (substitute-command-keys
  787. " (\\<vr--query-replace-map>\\[help] for help) ")
  788. 'read-only t))))
  789. ;; show visual feedback for all matches
  790. (mapc (lambda (replacement-info)
  791. (cl-multiple-value-bind (replacement match-data i) replacement-info
  792. (vr--feedback-match-callback i 0 (cl-first match-data) (cl-second match-data))))
  793. vr--query-replacements)
  794. (goto-char vr--target-buffer-start)
  795. (push-mark)
  796. (undo-boundary)
  797. (unwind-protect
  798. ;; Loop finding occurrences that perhaps should be replaced.
  799. (while (and keep-going vr--query-replacements)
  800. ;; Advance replacement list
  801. (cl-multiple-value-bind (replacement match-data i) (car vr--query-replacements)
  802. (setq match-data (vr--mapcar-nonnil (lambda (el) (+ cumulative-offset el)) match-data))
  803. (let ((begin (cl-first match-data))
  804. (end (cl-second match-data))
  805. (next-replacement-orig replacement))
  806. (setq next-replacement (vr--get-replacement replacement match-data replace-count))
  807. (goto-char begin)
  808. (setq vr--query-replacements (cdr vr--query-replacements))
  809. ;; default for new occurrence: no preview
  810. (setq vr--replace-preview nil)
  811. (undo-boundary)
  812. (let (done replaced key def)
  813. ;; Loop reading commands until one of them sets done,
  814. ;; which means it has finished handling this
  815. ;; occurrence.
  816. (while (not done)
  817. ;; show replacement feedback for current occurrence
  818. (unless replaced
  819. (vr--do-replace-feedback-match-callback next-replacement-orig match-data i))
  820. ;; Bind message-log-max so we don't fill up the message log
  821. ;; with a bunch of identical messages.
  822. (let ((message-log-max nil))
  823. (message message from-string next-replacement))
  824. (setq key (read-event))
  825. (setq key (vector key))
  826. (setq def (lookup-key map key))
  827. ;; can use replace-match afterwards
  828. (set-match-data match-data)
  829. ;; Restore the match data while we process the command.
  830. (cond ((eq def 'help)
  831. (with-output-to-temp-buffer "*Help*"
  832. (princ
  833. (concat "Query replacing visual-regexp "
  834. from-string " with "
  835. next-replacement ".\n\n"
  836. (substitute-command-keys
  837. vr--query-replace-help)))
  838. (with-current-buffer standard-output
  839. (help-mode))))
  840. ((eq def 'exit)
  841. (setq keep-going nil
  842. done t))
  843. ((eq def 'act)
  844. (unless replaced
  845. (replace-match next-replacement t t)
  846. (setq replace-count (1+ replace-count)))
  847. (setq done t
  848. replaced t))
  849. ((eq def 'act-and-exit)
  850. (unless replaced
  851. (replace-match next-replacement t t)
  852. (setq replace-count (1+ replace-count)))
  853. (setq keep-going nil
  854. done t
  855. replaced t))
  856. ((eq def 'act-and-show)
  857. (unless replaced
  858. (replace-match next-replacement t t)
  859. (setq replace-count (1+ replace-count))
  860. (setq replaced t)))
  861. ((eq def 'toggle-preview)
  862. (setq vr--replace-preview (not vr--replace-preview)))
  863. ((eq def 'automatic)
  864. (setq vr--target-buffer-start (match-beginning 0)
  865. vr--target-buffer-end (+ cumulative-offset vr--target-buffer-end))
  866. (setq replace-count (+ replace-count (vr--do-replace t)))
  867. (setq done t
  868. replaced t
  869. keep-going nil))
  870. ((eq def 'skip)
  871. (setq done t))
  872. ((eq def 'recenter)
  873. ;; `this-command' has the value `query-replace',
  874. ;; so we need to bind it to `recenter-top-bottom'
  875. ;; to allow it to detect a sequence of `C-l'.
  876. (let ((this-command 'recenter-top-bottom)
  877. (last-command 'recenter-top-bottom))
  878. (recenter-top-bottom)))
  879. (t
  880. (setq this-command 'mode-exited)
  881. (setq keep-going nil)
  882. (setq unread-command-events
  883. (append (listify-key-sequence key)
  884. unread-command-events))
  885. (setq done t)))
  886. (when replaced
  887. (setq cumulative-offset (+ cumulative-offset (- (length next-replacement) (- end begin)))))
  888. (unless (eq def 'recenter)
  889. ;; Reset recenter cycling order to initial position.
  890. (setq recenter-last-op nil))
  891. ;; in case of 'act-and-show: delete overlay display or it will still be
  892. ;; visible even though the replacement has been made
  893. (when replaced (vr--delete-overlay-display (vr--get-overlay i 0)))))
  894. ;; occurrence has been handled
  895. ;; delete feedback overlay
  896. (delete-overlay (vr--get-overlay i 0)))))
  897. ;; unwind
  898. (progn
  899. (vr--delete-overlay-displays)
  900. (vr--delete-overlays)
  901. ;; (replace-dehighlight)
  902. ))
  903. (unless unread-command-events
  904. ;; point is set to the end of the last occurrence.
  905. (goto-char (match-end 0))
  906. (message "Replaced %d occurrence%s"
  907. replace-count
  908. (if (= replace-count 1) "" "s")))))
  909. (provide 'visual-regexp)
  910. ;;; visual-regexp.el ends here