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

;;; visual-regexp-steroids.el --- Extends visual-regexp to support other regexp engines
;; Copyright (C) 2013-2017 Marko Bencun
;; Author: Marko Bencun <mbencun@gmail.com>
;; 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 <http://www.gnu.org/licenses/>.
;;; 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