;;; visual-regexp-steroids.el --- Extends visual-regexp to support other regexp engines ;; Copyright (C) 2013-2017 Marko Bencun ;; Author: Marko Bencun ;; URL: https://github.com/benma/visual-regexp-steroids.el/ ;; Version: 1.1 ;; Package-Requires: ((visual-regexp "1.1")) ;; Keywords: external, foreign, regexp, replace, python, visual, feedback ;; This file is part of visual-regexp-steroids ;; visual-regexp-steroids 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. ;; visual-regexp-steroids 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 visual-regexp-steroids. If not, see . ;;; WHAT'S NEW ;; 1.1: Add new engine: emacs-plain. ;; 1.0: Make compatible with visual-regexp 1.0. ;; 0.9: Fix warnings regarding free variables. ;; 0.8: Added support for pcre2el as a new engine. ;; 0.7: distinguish prompts in vr/replace, vr/query-replace, vr/mc-mark. ;; 0.6: new functions vr/select-replace, vr/select-query-replace, vr/select-mc-mark ;; 0.5: perform no case-conversion for non-emacs regexp engines. ;; 0.4: keep in sync with visual-regexp ;; 0.2: compatibility with visual-regexp 0.2 ;; 0.1: initial release ;;; Tip Jar ;; If you found this useful, please consider donating. ;; BTC: 1KtDEa5saBdJ2AFcFq93QZ3jz3sYpq2z2 ;;; Code: (require 'visual-regexp) ;;; variables (defvar vr--command-python-default (format "python %s" (expand-file-name "regexp.py" (file-name-directory load-file-name)))) (defcustom vr/command-python vr--command-python-default "External command used for the Python engine." :type 'string :group 'visual-regexp) (defcustom vr/command-custom "" "Custom external command used when the engine is set to custom." :type 'string :group 'visual-regexp) (defcustom vr/engine 'python "Which engine to use for searching/replacing. Use Emacs to use Emacs-style regular expressions. Use Python to use Python's regular expressions (see vr/command-python). Use pcre2el (https://github.com/joddie/pcre2el) to use PCRE regular expressions. Use Custom to use a custom external command (see vr/command-custom)." :type '(choice (const :tag "Emacs" emacs) (const :tag "pcre2el" pcre2el) (const :tag "Python" python) (const :tag "Custom" custom)) :group 'visual-regexp) (defcustom vr/default-regexp-modifiers '(:I nil :M t :S nil :U nil) "Modifiers that are applied by default. All modifiers are: '(I M S U). See also: http://docs.python.org/library/re.html#re.I" ;;:type '(choice (const 10) (const 5)) :type '(plist :key-type (choice (const :tag "Enable the IGNORECASE modifier by default" :I) (const :tag "Enable the MULTILINE modifier by default (^ and $ match on every line)" :M) (const :tag "Enable the DOTALL modifier by default (dot matches newline)" :S) (const :tag "Enable the UNICODE modifier by default" :U)) :value-type boolean) :group 'visual-regexp) ;;; private variables (defconst vr--engines '(emacs emacs-plain pcre2el python)) (defvar vr--use-expression nil "Use expression instead of string in replacement.") ;; modifiers IMSU (see http://docs.python.org/library/re.html#re.I) (defvar vr--regexp-modifiers '() "Modifiers in use.") (define-key vr/minibuffer-keymap (kbd "C-c i") (lambda () (interactive) (vr--toggle-regexp-modifier :I))) (define-key vr/minibuffer-keymap (kbd "C-c m") (lambda () (interactive) (vr--toggle-regexp-modifier :M))) (define-key vr/minibuffer-keymap (kbd "C-c s") (lambda () (interactive) (vr--toggle-regexp-modifier :S))) (define-key vr/minibuffer-keymap (kbd "C-c u") (lambda () (interactive) (vr--toggle-regexp-modifier :U))) (define-key vr/minibuffer-keymap (kbd "C-c C-c") (lambda () (interactive) (when (vr--in-replace) (setq vr--use-expression (not vr--use-expression)) (vr--update-minibuffer-prompt) (vr--do-replace-feedback)))) ;;; regexp modifiers (add-hook 'vr/initialize-hook (lambda () (setq vr--use-expression nil) (setq vr--regexp-modifiers (copy-sequence vr/default-regexp-modifiers)))) (defun vr--regexp-modifiers-enabled () (eq vr/engine 'python)) (defun vr--toggle-regexp-modifier (modifier) "modifier should be one of :I, :M, :S, :U." (when (and (vr--in-from) (vr--regexp-modifiers-enabled)) (plist-put vr--regexp-modifiers modifier (not (plist-get vr--regexp-modifiers modifier))) (vr--update-minibuffer-prompt) (vr--show-feedback))) (defun vr--get-regexp-modifiers-prefix () "Construct (?imsu) prefix based on selected modifiers." (if (vr--regexp-modifiers-enabled) (let ((s (mapconcat 'identity (delq nil (mapcar (lambda (m) (when (plist-get vr--regexp-modifiers m) (cond ((equal m :I) "i") ((equal m :M) "m") ((equal m :S) "s") ((equal m :U) "u") (t nil)))) (list :I :M :S :U))) ""))) (if (string= "" s) "" (format "(?%s)" s))) "")) (defadvice vr--get-replacement (around get-unmodified-replacement (replacement match-data i) activate) (cond ((member vr/engine '(emacs pcre2el)) ad-do-it) ((eq vr/engine 'emacs-plain) (let ((vr/plain t)) ad-do-it)) (t (setq ad-return-value replacement)))) (defadvice vr--get-regexp-string (around get-regexp-string (&optional for-display) activate) ad-do-it (let ((regexp ad-return-value)) (when (and (not for-display) (eq vr/engine 'pcre2el)) (condition-case err (setq regexp (pcre-to-elisp regexp)) (invalid-regexp (signal (car err) (cdr err))) ;; rethrow (error (signal (car err) (list "pcre2el error"))))) (setq ad-return-value (concat (vr--get-regexp-modifiers-prefix) regexp)))) ;;; shell command / parsing functions (defun vr--get-command () (cond ((eq vr/engine 'python) vr/command-python) ((eq vr/engine 'custom) vr/command-custom))) (defun vr--command (command) (let ((stdout-buffer (generate-new-buffer (generate-new-buffer-name " *pyregex stdout*"))) output exit-code) (with-current-buffer vr--target-buffer (setq exit-code (call-process-region vr--target-buffer-start vr--target-buffer-end shell-file-name nil ;; don't delete region stdout-buffer nil ;; don't redisplay buffer shell-command-switch command))) (with-current-buffer stdout-buffer (setq output (buffer-string)) (kill-buffer)) (list output exit-code))) (defun vr--run-command (args success) (cl-multiple-value-bind (output exit-code) (vr--command args) (cond ((equal exit-code 0) (funcall success output)) ((equal exit-code 1) (message "script failed:%s\n" output)) (t (error (format "External command failed with exit code %s" exit-code)))))) (defun vr--unescape (s) "Replacement/message strings returned by external script are base64 encoded." (decode-coding-string (base64-decode-string s) 'utf-8 t)) (defun vr--not-last-line () "Output of external script ends in one line of message and one empty line. Return t if current line is not the line with the message." (save-excursion (= 0 (forward-line 2)))) (defun vr--current-line () (buffer-substring-no-properties (line-beginning-position) (line-end-position))) (defun vr--parse-matches (s callback) "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), i being the match and j the group index and begin/end being the span of the match. The message line is returned. " (let (message-line) ;; store message line (last non-empty line of output) (with-temp-buffer (insert s) (goto-char (point-min)) (let ((offset vr--target-buffer-start)) (cl-loop while (and (vr--not-last-line) (/= (line-beginning-position) (line-end-position))) ;; loop until empty line is reached for i from 0 do (cl-loop while (re-search-forward "\\([0-9]+\\) \\([0-9]+\\)" (line-end-position) t) ;; loop integer pairs in line for j from 0 do (let ((begin (+ offset (string-to-number (match-string 1)))) (end (+ offset (string-to-number (match-string 2))))) (funcall callback i j begin end))) (forward-line 1))) (setq message-line (vr--unescape (vr--current-line)))) message-line)) (defun vr--parse-replace (s) "Parse string s with positions of matches and replacements as returned by external script. Returns a list, in reverse order, of (replacement (list begin end) i) (i = index of match = index of corresponding overlay) and the message line." (let ((replacements (list)) ;; store replacements (lines of output) in list message-line) ;; store message line (last non-empty line of output) (with-temp-buffer (insert s) (goto-char (point-min)) (cl-loop while (and (vr--not-last-line) (/= (line-beginning-position) (line-end-position))) ;; loop until empty line is reached for i from 0 do (re-search-forward "\\([0-9]+\\) \\([0-9]+\\) " (line-end-position) t) (let ((replacement (buffer-substring-no-properties (point) (line-end-position))) (begin (+ vr--target-buffer-start (string-to-number (match-string 1)))) (end (+ vr--target-buffer-start (string-to-number (match-string 2))))) (setq replacements (cons (list (vr--unescape replacement) (list begin end) i) replacements))) (forward-line 1)) (setq message-line (vr--unescape (vr--current-line)))) (list replacements message-line))) :;; prompt (defadvice vr--set-minibuffer-prompt (around prompt activate) (let ((prompt (cond ((equal vr--calling-func 'vr--calling-func-query-replace) "Query replace") ((equal vr--calling-func 'vr--calling-func-mc-mark) "Mark") (t "Replace")))) (when (vr--in-replace) (setq prompt (concat prompt (let ((flag-infos (mapconcat 'identity (delq nil (list (when vr--use-expression "using expression") (when vr--replace-preview "preview"))) ", "))) (when (not (string= "" flag-infos )) (format " (%s)" flag-infos)))))) (when (not (vr--in-from)) (setq prompt (concat prompt " " (vr--get-regexp-string t)))) (setq prompt (concat prompt (if (vr--in-from) ": " " with: "))) (when (and (vr--in-from) (vr--regexp-modifiers-enabled)) (setq prompt (concat prompt (vr--get-regexp-modifiers-prefix)))) (setq ad-return-value prompt))) (defadvice vr--minibuffer-help-text (around help activate) ad-do-it (let ((help ad-return-value)) (when (and (vr--in-from) (vr--regexp-modifiers-enabled)) (setq help (concat help ", C-c i: toggle case, C-c m: toggle multiline match of ^ and $, C-c s: toggle dot matches newline"))) (when (vr--in-replace) (setq help (concat help ", C-c C-c: toggle expression"))) (setq ad-return-value help))) ;; feedback / replace functions (defadvice vr--feedback-function (around feedback-around (regexp-string forward feedback-limit callback) activate) "Feedback function for search using an external command." (cond ((member vr/engine '(emacs pcre2el)) ad-do-it) ((eq vr/engine 'emacs-plain) (let ((vr/plain t)) ad-do-it)) (t (setq ad-return-value (vr--run-command (format "%s matches --regexp %s %s %s" (vr--get-command) (shell-quote-argument regexp-string) (when feedback-limit (format "--feedback-limit %s" feedback-limit)) (if forward "" "--backwards")) (lambda (output) (vr--parse-matches output callback))))))) (defadvice vr--get-replacements (around get-replacements-around (feedback feedback-limit) activate) "Get replacements using an external command." (cond ((member vr/engine '(emacs pcre2el)) ad-do-it) ((eq vr/engine 'emacs-plain) (let ((vr/plain t)) ad-do-it)) (t (setq ad-return-value (vr--run-command (format "%s replace %s %s %s --regexp %s --replace %s" (vr--get-command) (if feedback "--feedback" "") (if feedback-limit (format "--feedback-limit %s" feedback-limit) "") (if vr--use-expression "--eval" "") (shell-quote-argument (vr--get-regexp-string)) (shell-quote-argument (vr--get-replace-string))) 'vr--parse-replace))))) (defun vr--select-engine () (let ((default (symbol-name vr/engine)) (choices vr--engines)) ;; add custom engine if a custom command has been defined (unless (string= "" vr/command-custom) (setq choices (cons 'custom choices))) (intern (completing-read (format "Select engine (default: %s): " (symbol-name vr/engine)) (mapcar 'symbol-name choices) nil t nil nil default)))) ;;;###autoload (defun vr/select-replace () (interactive) (let ((vr/engine (vr--select-engine))) (call-interactively 'vr/replace))) ;;;###autoload (defun vr/select-query-replace () (interactive) (let ((vr/engine (vr--select-engine))) (call-interactively 'vr/query-replace))) ;;;###autoload (defun vr/select-mc-mark () (interactive) (let ((vr/engine (vr--select-engine))) (call-interactively 'vr/mc-mark))) ;; isearch starts here ;;;###autoload (defun vr/isearch-forward () "Like isearch-forward, but using Python (or custom) regular expressions." (interactive) (if (eq vr/engine 'emacs) (isearch-forward-regexp) (let ((isearch-search-fun-function 'vr--isearch-search-fun-function)) (isearch-forward-regexp)))) ;;;###autoload (defun vr/isearch-backward () "Like isearch-backward, but using Python (or custom) regular expressions." (interactive) (if (eq vr/engine 'emacs) (isearch-backward-regexp) (let ((isearch-search-fun-function 'vr--isearch-search-fun-function)) (isearch-backward-regexp)))) (defvar vr--isearch-cache-key nil) (defvar vr--isearch-cache-val nil) (defun vr--isearch-forward (string &optional bound noerror count) (vr--isearch t string bound noerror count)) (defun vr--isearch-backward (string &optional bound noerror count) (vr--isearch nil string bound noerror count)) (defun vr--isearch-find-match (forward matches start) (let ((i (vr--isearch-find-match-bsearch forward matches start 0 (- (length matches) 1)))) (unless (eq i -1) (aref matches i)))) (defun vr--isearch-find-match-bsearch (forward matches start left right) (if (= 0 (length matches)) -1 (let ((mid (/ (+ left right) 2)) (el (if forward 0 1)) ;; 0 => beginning of match; 1 => end of match (cmp (if forward '<= '>=))) (cond ((eq left right) (if (funcall cmp start (nth el (aref matches mid))) left -1) ) ((funcall cmp start (nth el (aref matches mid))) (vr--isearch-find-match-bsearch forward matches start left mid)) (t (vr--isearch-find-match-bsearch forward matches start (1+ mid) right)))))) (defun vr--isearch (forward string &optional bound noerror count) ;; This is be called from isearch. In the first call, bound will be nil to find the next match. ;; 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)). ;; 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. (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. matches-vec ;; stores matches from regexp.py message-line ;; message from regexp.py (regexp (if (eq vr/engine 'pcre2el) (pcre-to-elisp string) string)) (start (if forward (if is-called-from-lazy-highlighting (window-start (selected-window)) (point)) (if is-called-from-lazy-highlighting bound (point-min)))) (end (if forward (if is-called-from-lazy-highlighting bound (point-max)) (if is-called-from-lazy-highlighting (window-end (selected-window)) (point)))) (cache-key (list regexp start end))) (if (and is-called-from-lazy-highlighting (equal vr--isearch-cache-key cache-key)) (setq matches-vec vr--isearch-cache-val) ;; cache hit (progn ;; no cache hit, populate matches-vec (setq vr--target-buffer-start start vr--target-buffer-end end vr--target-buffer (current-buffer)) (let ((matches-list (list)) (number-of-matches 0)) (setq message-line (vr--feedback-function regexp forward (if count count ;; if no bound, the rest of the buffer is searched for the first match -> need only one match (if bound nil 1)) (lambda (i j begin end) (when (= j 0) (setq number-of-matches (1+ number-of-matches))) (setq matches-list (cons (list i j begin end) matches-list))))) ;; convert list to vector (setq matches-vec (make-vector number-of-matches nil)) (let ((cur-match (list))) (mapc (lambda (el) (cl-multiple-value-bind (i j begin end) el (when (and (= j 0) (> i 0)) (aset matches-vec (- i 1) (nreverse cur-match)) (setq cur-match (list))) (setq cur-match (cons end (cons begin cur-match))))) (nreverse matches-list)) (when cur-match (aset matches-vec (- (length matches-vec) 1) (nreverse cur-match))))) (when is-called-from-lazy-highlighting ;; store in cache (setq vr--isearch-cache-key cache-key vr--isearch-cache-val matches-vec)))) (let ((match (vr--isearch-find-match forward matches-vec (point)))) (if match (progn (set-match-data (mapcar 'copy-marker match)) ;; needed for isearch (if forward (goto-char (nth 1 match)) ;; move to end of match (goto-char (nth 0 match)) ;; move to beginning of match )) (progn (set-match-data (list 0 0)) (when (string= "Invalid:" (substring message-line 0 8)) (signal 'invalid-regexp (list message-line)))))))) (defun vr--isearch-search-fun-function () "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)`." ;; 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. (if isearch-regexp ;; let us handle regexp search (if isearch-forward 'vr--isearch-forward 'vr--isearch-backward) (let ((isearch-search-fun-function nil)) ;; fall back to the default implementation of isearch, which will handle regular search and word search. (isearch-search-fun)))) (add-hook 'isearch-mode-end-hook (lambda () (setq vr--isearch-cache-key nil vr--isearch-cache-val nil))) (provide 'visual-regexp-steroids) ;;; visual-regexp-steroids.el ends here