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.

482 lines
21 KiB

  1. ;;; visual-regexp-steroids.el --- Extends visual-regexp to support other regexp engines
  2. ;; Copyright (C) 2013-2017 Marko Bencun
  3. ;; Author: Marko Bencun <mbencun@gmail.com>
  4. ;; URL: https://github.com/benma/visual-regexp-steroids.el/
  5. ;; Version: 1.1
  6. ;; Package-Requires: ((visual-regexp "1.1"))
  7. ;; Keywords: external, foreign, regexp, replace, python, visual, feedback
  8. ;; This file is part of visual-regexp-steroids
  9. ;; visual-regexp-steroids is free software: you can redistribute it and/or modify
  10. ;; it under the terms of the GNU General Public License as published by
  11. ;; the Free Software Foundation, either version 3 of the License, or
  12. ;; (at your option) any later version.
  13. ;; visual-regexp-steroids is distributed in the hope that it will be useful,
  14. ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. ;; GNU General Public License for more details.
  17. ;; You should have received a copy of the GNU General Public License
  18. ;; along with visual-regexp-steroids. If not, see <http://www.gnu.org/licenses/>.
  19. ;;; WHAT'S NEW
  20. ;; 1.1: Add new engine: emacs-plain.
  21. ;; 1.0: Make compatible with visual-regexp 1.0.
  22. ;; 0.9: Fix warnings regarding free variables.
  23. ;; 0.8: Added support for pcre2el as a new engine.
  24. ;; 0.7: distinguish prompts in vr/replace, vr/query-replace, vr/mc-mark.
  25. ;; 0.6: new functions vr/select-replace, vr/select-query-replace, vr/select-mc-mark
  26. ;; 0.5: perform no case-conversion for non-emacs regexp engines.
  27. ;; 0.4: keep in sync with visual-regexp
  28. ;; 0.2: compatibility with visual-regexp 0.2
  29. ;; 0.1: initial release
  30. ;;; Tip Jar
  31. ;; If you found this useful, please consider donating.
  32. ;; BTC: 1KtDEa5saBdJ2AFcFq93QZ3jz3sYpq2z2
  33. ;;; Code:
  34. (require 'visual-regexp)
  35. ;;; variables
  36. (defvar vr--command-python-default
  37. (format "python %s" (expand-file-name "regexp.py" (file-name-directory load-file-name))))
  38. (defcustom vr/command-python vr--command-python-default
  39. "External command used for the Python engine."
  40. :type 'string
  41. :group 'visual-regexp)
  42. (defcustom vr/command-custom ""
  43. "Custom external command used when the engine is set to custom."
  44. :type 'string
  45. :group 'visual-regexp)
  46. (defcustom vr/engine 'python
  47. "Which engine to use for searching/replacing.
  48. Use Emacs to use Emacs-style regular expressions.
  49. Use Python to use Python's regular expressions (see vr/command-python).
  50. Use pcre2el (https://github.com/joddie/pcre2el) to use PCRE regular expressions.
  51. Use Custom to use a custom external command (see vr/command-custom)."
  52. :type '(choice
  53. (const :tag "Emacs" emacs)
  54. (const :tag "pcre2el" pcre2el)
  55. (const :tag "Python" python)
  56. (const :tag "Custom" custom))
  57. :group 'visual-regexp)
  58. (defcustom vr/default-regexp-modifiers '(:I nil :M t :S nil :U nil)
  59. "Modifiers that are applied by default. All modifiers are: '(I M S U).
  60. See also: http://docs.python.org/library/re.html#re.I"
  61. ;;:type '(choice (const 10) (const 5))
  62. :type '(plist :key-type (choice
  63. (const :tag "Enable the IGNORECASE modifier by default" :I)
  64. (const :tag "Enable the MULTILINE modifier by default (^ and $ match on every line)" :M)
  65. (const :tag "Enable the DOTALL modifier by default (dot matches newline)" :S)
  66. (const :tag "Enable the UNICODE modifier by default" :U))
  67. :value-type boolean)
  68. :group 'visual-regexp)
  69. ;;; private variables
  70. (defconst vr--engines '(emacs emacs-plain pcre2el python))
  71. (defvar vr--use-expression nil
  72. "Use expression instead of string in replacement.")
  73. ;; modifiers IMSU (see http://docs.python.org/library/re.html#re.I)
  74. (defvar vr--regexp-modifiers '()
  75. "Modifiers in use.")
  76. (define-key vr/minibuffer-keymap (kbd "C-c i") (lambda () (interactive) (vr--toggle-regexp-modifier :I)))
  77. (define-key vr/minibuffer-keymap (kbd "C-c m") (lambda () (interactive) (vr--toggle-regexp-modifier :M)))
  78. (define-key vr/minibuffer-keymap (kbd "C-c s") (lambda () (interactive) (vr--toggle-regexp-modifier :S)))
  79. (define-key vr/minibuffer-keymap (kbd "C-c u") (lambda () (interactive) (vr--toggle-regexp-modifier :U)))
  80. (define-key vr/minibuffer-keymap (kbd "C-c C-c") (lambda () (interactive)
  81. (when (vr--in-replace)
  82. (setq vr--use-expression (not vr--use-expression))
  83. (vr--update-minibuffer-prompt)
  84. (vr--do-replace-feedback))))
  85. ;;; regexp modifiers
  86. (add-hook 'vr/initialize-hook (lambda ()
  87. (setq vr--use-expression nil)
  88. (setq vr--regexp-modifiers (copy-sequence vr/default-regexp-modifiers))))
  89. (defun vr--regexp-modifiers-enabled ()
  90. (eq vr/engine 'python))
  91. (defun vr--toggle-regexp-modifier (modifier)
  92. "modifier should be one of :I, :M, :S, :U."
  93. (when (and (vr--in-from) (vr--regexp-modifiers-enabled))
  94. (plist-put vr--regexp-modifiers modifier
  95. (not (plist-get vr--regexp-modifiers modifier)))
  96. (vr--update-minibuffer-prompt)
  97. (vr--show-feedback)))
  98. (defun vr--get-regexp-modifiers-prefix ()
  99. "Construct (?imsu) prefix based on selected modifiers."
  100. (if (vr--regexp-modifiers-enabled)
  101. (let ((s (mapconcat 'identity
  102. (delq nil (mapcar (lambda (m)
  103. (when (plist-get vr--regexp-modifiers m)
  104. (cond ((equal m :I) "i")
  105. ((equal m :M) "m")
  106. ((equal m :S) "s")
  107. ((equal m :U) "u")
  108. (t nil))))
  109. (list :I :M :S :U)))
  110. "")))
  111. (if (string= "" s) "" (format "(?%s)" s)))
  112. ""))
  113. (defadvice vr--get-replacement (around get-unmodified-replacement (replacement match-data i) activate)
  114. (cond ((member vr/engine '(emacs pcre2el))
  115. ad-do-it)
  116. ((eq vr/engine 'emacs-plain)
  117. (let ((vr/plain t)) ad-do-it))
  118. (t
  119. (setq ad-return-value replacement))))
  120. (defadvice vr--get-regexp-string (around get-regexp-string (&optional for-display) activate)
  121. ad-do-it
  122. (let ((regexp ad-return-value))
  123. (when (and (not for-display) (eq vr/engine 'pcre2el))
  124. (condition-case err
  125. (setq regexp (pcre-to-elisp regexp))
  126. (invalid-regexp (signal (car err) (cdr err))) ;; rethrow
  127. (error (signal (car err) (list "pcre2el error")))))
  128. (setq ad-return-value
  129. (concat (vr--get-regexp-modifiers-prefix)
  130. regexp))))
  131. ;;; shell command / parsing functions
  132. (defun vr--get-command ()
  133. (cond
  134. ((eq vr/engine 'python) vr/command-python)
  135. ((eq vr/engine 'custom) vr/command-custom)))
  136. (defun vr--command (command)
  137. (let ((stdout-buffer (generate-new-buffer (generate-new-buffer-name " *pyregex stdout*")))
  138. output
  139. exit-code)
  140. (with-current-buffer vr--target-buffer
  141. (setq exit-code (call-process-region
  142. vr--target-buffer-start
  143. vr--target-buffer-end
  144. shell-file-name
  145. nil ;; don't delete region
  146. stdout-buffer
  147. nil ;; don't redisplay buffer
  148. shell-command-switch
  149. command)))
  150. (with-current-buffer stdout-buffer
  151. (setq output (buffer-string))
  152. (kill-buffer))
  153. (list output exit-code)))
  154. (defun vr--run-command (args success)
  155. (cl-multiple-value-bind (output exit-code) (vr--command args)
  156. (cond ((equal exit-code 0)
  157. (funcall success output))
  158. ((equal exit-code 1)
  159. (message "script failed:%s\n" output))
  160. (t (error (format "External command failed with exit code %s" exit-code))))))
  161. (defun vr--unescape (s)
  162. "Replacement/message strings returned by external script are base64 encoded."
  163. (decode-coding-string (base64-decode-string s) 'utf-8 t))
  164. (defun vr--not-last-line ()
  165. "Output of external script ends in one line of message and one empty line.
  166. Return t if current line is not the line with the message."
  167. (save-excursion (= 0 (forward-line 2))))
  168. (defun vr--current-line ()
  169. (buffer-substring-no-properties (line-beginning-position) (line-end-position)))
  170. (defun vr--parse-matches (s callback)
  171. "Parse string s with positions of matches and groups as returned by external script. For each position, callback is called with arguments (i j begin end),
  172. i being the match and j the group index and begin/end being the span of the match.
  173. The message line is returned.
  174. "
  175. (let (message-line) ;; store message line (last non-empty line of output)
  176. (with-temp-buffer
  177. (insert s)
  178. (goto-char (point-min))
  179. (let ((offset vr--target-buffer-start))
  180. (cl-loop while (and (vr--not-last-line) (/= (line-beginning-position) (line-end-position))) ;; loop until empty line is reached
  181. for i from 0 do
  182. (cl-loop while (re-search-forward "\\([0-9]+\\) \\([0-9]+\\)" (line-end-position) t) ;; loop integer pairs in line
  183. for j from 0 do
  184. (let ((begin (+ offset (string-to-number (match-string 1))))
  185. (end (+ offset (string-to-number (match-string 2)))))
  186. (funcall callback i j begin end)))
  187. (forward-line 1)))
  188. (setq message-line (vr--unescape (vr--current-line))))
  189. message-line))
  190. (defun vr--parse-replace (s)
  191. "Parse string s with positions of matches and replacements as returned by external script.
  192. Returns a list, in reverse order, of (replacement (list begin end) i) (i = index of match = index of corresponding overlay)
  193. and the message line."
  194. (let ((replacements (list)) ;; store replacements (lines of output) in list
  195. message-line) ;; store message line (last non-empty line of output)
  196. (with-temp-buffer
  197. (insert s)
  198. (goto-char (point-min))
  199. (cl-loop while (and (vr--not-last-line) (/= (line-beginning-position) (line-end-position))) ;; loop until empty line is reached
  200. for i from 0 do
  201. (re-search-forward "\\([0-9]+\\) \\([0-9]+\\) " (line-end-position) t)
  202. (let ((replacement (buffer-substring-no-properties (point) (line-end-position)))
  203. (begin (+ vr--target-buffer-start (string-to-number (match-string 1))))
  204. (end (+ vr--target-buffer-start (string-to-number (match-string 2)))))
  205. (setq replacements (cons (list (vr--unescape replacement) (list begin end) i) replacements)))
  206. (forward-line 1))
  207. (setq message-line (vr--unescape (vr--current-line))))
  208. (list replacements message-line)))
  209. :;; prompt
  210. (defadvice vr--set-minibuffer-prompt (around prompt activate)
  211. (let ((prompt (cond ((equal vr--calling-func 'vr--calling-func-query-replace)
  212. "Query replace")
  213. ((equal vr--calling-func 'vr--calling-func-mc-mark)
  214. "Mark")
  215. (t
  216. "Replace"))))
  217. (when (vr--in-replace)
  218. (setq prompt (concat prompt
  219. (let ((flag-infos (mapconcat 'identity
  220. (delq nil (list (when vr--use-expression "using expression")
  221. (when vr--replace-preview "preview")))
  222. ", ")))
  223. (when (not (string= "" flag-infos ))
  224. (format " (%s)" flag-infos))))))
  225. (when (not (vr--in-from))
  226. (setq prompt (concat prompt " " (vr--get-regexp-string t))))
  227. (setq prompt (concat prompt (if (vr--in-from) ": " " with: ")))
  228. (when (and (vr--in-from) (vr--regexp-modifiers-enabled))
  229. (setq prompt (concat prompt (vr--get-regexp-modifiers-prefix))))
  230. (setq ad-return-value prompt)))
  231. (defadvice vr--minibuffer-help-text (around help activate)
  232. ad-do-it
  233. (let ((help ad-return-value))
  234. (when (and (vr--in-from) (vr--regexp-modifiers-enabled))
  235. (setq help (concat help ", C-c i: toggle case, C-c m: toggle multiline match of ^ and $, C-c s: toggle dot matches newline")))
  236. (when (vr--in-replace)
  237. (setq help (concat help ", C-c C-c: toggle expression")))
  238. (setq ad-return-value help)))
  239. ;; feedback / replace functions
  240. (defadvice vr--feedback-function (around feedback-around (regexp-string forward feedback-limit callback) activate)
  241. "Feedback function for search using an external command."
  242. (cond ((member vr/engine '(emacs pcre2el))
  243. ad-do-it)
  244. ((eq vr/engine 'emacs-plain)
  245. (let ((vr/plain t)) ad-do-it))
  246. (t
  247. (setq ad-return-value
  248. (vr--run-command
  249. (format "%s matches --regexp %s %s %s"
  250. (vr--get-command)
  251. (shell-quote-argument regexp-string)
  252. (when feedback-limit (format "--feedback-limit %s" feedback-limit))
  253. (if forward "" "--backwards"))
  254. (lambda (output)
  255. (vr--parse-matches
  256. output
  257. callback)))))))
  258. (defadvice vr--get-replacements (around get-replacements-around (feedback feedback-limit) activate)
  259. "Get replacements using an external command."
  260. (cond ((member vr/engine '(emacs pcre2el))
  261. ad-do-it)
  262. ((eq vr/engine 'emacs-plain)
  263. (let ((vr/plain t)) ad-do-it))
  264. (t
  265. (setq ad-return-value
  266. (vr--run-command
  267. (format "%s replace %s %s %s --regexp %s --replace %s"
  268. (vr--get-command)
  269. (if feedback "--feedback" "")
  270. (if feedback-limit
  271. (format "--feedback-limit %s" feedback-limit)
  272. "")
  273. (if vr--use-expression "--eval" "")
  274. (shell-quote-argument (vr--get-regexp-string))
  275. (shell-quote-argument (vr--get-replace-string)))
  276. 'vr--parse-replace)))))
  277. (defun vr--select-engine ()
  278. (let ((default (symbol-name vr/engine))
  279. (choices vr--engines))
  280. ;; add custom engine if a custom command has been defined
  281. (unless (string= "" vr/command-custom)
  282. (setq choices (cons 'custom choices)))
  283. (intern (completing-read (format "Select engine (default: %s): " (symbol-name vr/engine)) (mapcar 'symbol-name choices) nil t nil nil default))))
  284. ;;;###autoload
  285. (defun vr/select-replace ()
  286. (interactive)
  287. (let ((vr/engine (vr--select-engine)))
  288. (call-interactively 'vr/replace)))
  289. ;;;###autoload
  290. (defun vr/select-query-replace ()
  291. (interactive)
  292. (let ((vr/engine (vr--select-engine)))
  293. (call-interactively 'vr/query-replace)))
  294. ;;;###autoload
  295. (defun vr/select-mc-mark ()
  296. (interactive)
  297. (let ((vr/engine (vr--select-engine)))
  298. (call-interactively 'vr/mc-mark)))
  299. ;; isearch starts here
  300. ;;;###autoload
  301. (defun vr/isearch-forward ()
  302. "Like isearch-forward, but using Python (or custom) regular expressions."
  303. (interactive)
  304. (if (eq vr/engine 'emacs)
  305. (isearch-forward-regexp)
  306. (let ((isearch-search-fun-function 'vr--isearch-search-fun-function))
  307. (isearch-forward-regexp))))
  308. ;;;###autoload
  309. (defun vr/isearch-backward ()
  310. "Like isearch-backward, but using Python (or custom) regular expressions."
  311. (interactive)
  312. (if (eq vr/engine 'emacs)
  313. (isearch-backward-regexp)
  314. (let ((isearch-search-fun-function 'vr--isearch-search-fun-function))
  315. (isearch-backward-regexp))))
  316. (defvar vr--isearch-cache-key nil)
  317. (defvar vr--isearch-cache-val nil)
  318. (defun vr--isearch-forward (string &optional bound noerror count)
  319. (vr--isearch t string bound noerror count))
  320. (defun vr--isearch-backward (string &optional bound noerror count)
  321. (vr--isearch nil string bound noerror count))
  322. (defun vr--isearch-find-match (forward matches start)
  323. (let ((i (vr--isearch-find-match-bsearch forward matches start 0 (- (length matches) 1))))
  324. (unless (eq i -1)
  325. (aref matches i))))
  326. (defun vr--isearch-find-match-bsearch (forward matches start left right)
  327. (if (= 0 (length matches))
  328. -1
  329. (let ((mid (/ (+ left right) 2))
  330. (el (if forward 0 1)) ;; 0 => beginning of match; 1 => end of match
  331. (cmp (if forward '<= '>=)))
  332. (cond ((eq left right)
  333. (if (funcall cmp start (nth el (aref matches mid)))
  334. left
  335. -1)
  336. )
  337. ((funcall cmp start (nth el (aref matches mid)))
  338. (vr--isearch-find-match-bsearch forward matches start left mid))
  339. (t
  340. (vr--isearch-find-match-bsearch forward matches start (1+ mid) right))))))
  341. (defun vr--isearch (forward string &optional bound noerror count)
  342. ;; This is be called from isearch. In the first call, bound will be nil to find the next match.
  343. ;; Afterwards, lazy highlighting kicks in, which calls this function many times, for different values of (point), always with the same bound (window-end (selected-window)).
  344. ;; Calling a process repeatedly is noticeably slow. To speed the lazy highlighting up, we fetch all matches in the visible window at once and cache them for subsequent calls.
  345. (let* ((is-called-from-lazy-highlighting bound) ;; we assume only lazy highlighting sets a bound. isearch does not, and neither does our own vr/query-replace.
  346. matches-vec ;; stores matches from regexp.py
  347. message-line ;; message from regexp.py
  348. (regexp (if (eq vr/engine 'pcre2el) (pcre-to-elisp string) string))
  349. (start
  350. (if forward
  351. (if is-called-from-lazy-highlighting (window-start (selected-window)) (point))
  352. (if is-called-from-lazy-highlighting bound (point-min))))
  353. (end
  354. (if forward
  355. (if is-called-from-lazy-highlighting bound (point-max))
  356. (if is-called-from-lazy-highlighting (window-end (selected-window)) (point))))
  357. (cache-key (list regexp start end)))
  358. (if (and is-called-from-lazy-highlighting (equal vr--isearch-cache-key cache-key))
  359. (setq matches-vec vr--isearch-cache-val) ;; cache hit
  360. (progn ;; no cache hit, populate matches-vec
  361. (setq vr--target-buffer-start start
  362. vr--target-buffer-end end
  363. vr--target-buffer (current-buffer))
  364. (let ((matches-list (list))
  365. (number-of-matches 0))
  366. (setq message-line
  367. (vr--feedback-function
  368. regexp
  369. forward
  370. (if count
  371. count
  372. ;; if no bound, the rest of the buffer is searched for the first match -> need only one match
  373. (if bound nil 1))
  374. (lambda (i j begin end)
  375. (when (= j 0) (setq number-of-matches (1+ number-of-matches)))
  376. (setq matches-list (cons (list i j begin end) matches-list)))))
  377. ;; convert list to vector
  378. (setq matches-vec (make-vector number-of-matches nil))
  379. (let ((cur-match (list)))
  380. (mapc (lambda (el)
  381. (cl-multiple-value-bind (i j begin end) el
  382. (when (and (= j 0) (> i 0))
  383. (aset matches-vec (- i 1) (nreverse cur-match))
  384. (setq cur-match (list)))
  385. (setq cur-match (cons end (cons begin cur-match)))))
  386. (nreverse matches-list))
  387. (when cur-match
  388. (aset matches-vec (- (length matches-vec) 1) (nreverse cur-match)))))
  389. (when is-called-from-lazy-highlighting ;; store in cache
  390. (setq vr--isearch-cache-key cache-key
  391. vr--isearch-cache-val matches-vec))))
  392. (let ((match (vr--isearch-find-match forward matches-vec (point))))
  393. (if match
  394. (progn
  395. (set-match-data (mapcar 'copy-marker match)) ;; needed for isearch
  396. (if forward
  397. (goto-char (nth 1 match)) ;; move to end of match
  398. (goto-char (nth 0 match)) ;; move to beginning of match
  399. ))
  400. (progn
  401. (set-match-data (list 0 0))
  402. (when (string= "Invalid:" (substring message-line 0 8))
  403. (signal 'invalid-regexp (list message-line))))))))
  404. (defun vr--isearch-search-fun-function ()
  405. "To enable vr/isearch, set isearch-search-fun-function to vr--isearch-search-fun-function, i.e. `(setq isearch-search-fun-function 'vr--isearch-search-fun-function)`."
  406. ;; isearch-search-fun is a function that returns the function that does the search. It calls isearch-search-fun-function (if it exists) to do its job.
  407. (if isearch-regexp ;; let us handle regexp search
  408. (if isearch-forward 'vr--isearch-forward 'vr--isearch-backward)
  409. (let ((isearch-search-fun-function nil)) ;; fall back to the default implementation of isearch, which will handle regular search and word search.
  410. (isearch-search-fun))))
  411. (add-hook 'isearch-mode-end-hook (lambda ()
  412. (setq vr--isearch-cache-key nil
  413. vr--isearch-cache-val nil)))
  414. (provide 'visual-regexp-steroids)
  415. ;;; visual-regexp-steroids.el ends here