doc https://rgel.readthedocs.io
- 支持类似helm 的多个关键词按空格分开的过滤方式,支持在 rg buffer 中 增量添加关键词,重新用新关键词刷新 rg,(绑定在/ 与z 上)
- 支持在 rg buffer 中 增量排除匹配的文件或目录,缩小匹配的范围(绑定在x)
- 支持symbol 边界,直观点说就是当光标下的单词是"define-key" 时,我用它进行搜索时, 不希望 “evil-define-key” 也显示在结果中, rg 支持"\b" 表示单词边界,但是它不把"-" 与"_" 当作单词。
即 “hello world”. 匹配一行中同时包含 hello 与world 两个关键词,不分先后顺序。
且 hello !world
匹配包含hello ,但不包含 world 的行。
其底层机制是使用正则的0宽断言,可以参考这篇文章的介绍 https://leongfeng.github.io/2017/03/10/regex-java-assertions/
其缺点是无法正常高亮显示匹配的关键字。
rg 需要启用 -prce2 选项才能支持0宽断言,用法如下:
rg --pcre2 '^(?=.*hello)^(?=.*world)' #include hello and world
rg --pcre2 '^(?!.*hello)^(?=.*world)' # include world ,exclude hello
(setq rg-command-line-flags '("-z" "--pcre2"))
(define-key rg-global-map "g" #'vmacs-rg-word-current-dir) ; 在当前目录下搜索 ,关键词支持按空格分隔
(define-key rg-global-map "t" #'vmacs-rg-word-root-dir) ; 在项目根目录上进行搜索 ,关键词支持按空格分隔
; 查找光标下的symbol
(define-key rg-global-map (kbd ".") #'vmacs-rg-dwim-current-dir)
(define-key rg-global-map (kbd ",") #'vmacs-rg-dwim-project-dir)
(defun vmacs-rg-hook()
(setq-local compilation-scroll-output 'first-error) ; 自动跳转到首个匹配行
(setq-local compilation-always-kill t) ;有进程在运行时,kill buffer 时 不提示直接kill
;允许在 *rg* buffer 中更改关键词, 如当前关键词是hello ,发现匹配的行太多,
; 可直接按“/" 添加新的关键词 重新用新的关键词过滤
;按z 则是方便添加带! 的否定关键词,
(define-key rg-mode-map (kbd "/") #'vmacs-rg-rerun-change-regex)
(define-key rg-mode-map (kbd "z") #'vmacs-rg-rerun-hide-matched)
;x 则用于过滤文件名或目录名,直接排除匹配的文件或目录,
;用法如 -g '!filename' , 如 -g '!*.el' -g '!name*' ,使用时直接输入关键词即可,会自动在关键词前后添加 "*"
(define-key rg-mode-map (kbd "x") #'vvmacs-rg-rerun-filter-by-file)
;(evil-define-key 'normal 'local "x" 'vmacs-rg-rerun-filter-by-file) ; 如果用evil
; toggle 单词边界
(define-key rg-mode-map (kbd "c") #'vmacs-rg-rerun-toggle-surround)
; (evil-define-key 'normal 'local "c" 'vmacs-rg-rerun-toggle-surround)
)
(add-hook 'rg-mode-hook #'vmacs-rg-hook)
具体的实现如下:
(rg-define-search vmacs-rg-word-current-dir
:query (vmacs-rg-query (vmacs-rg-read-regex "Regexp at . : " (thing-at-point 'symbol) )
(= 4 (prefix-numeric-value current-prefix-arg)))
:format regexp
:flags ("--type=all")
:files current :dir current)
(rg-define-search vmacs-rg-word-root-dir
:query (vmacs-rg-query (vmacs-rg-read-regex "Regexp at project root: " (thing-at-point 'symbol) )
(= 4 (prefix-numeric-value current-prefix-arg)))
:format regexp
:flags ("--type=all")
:files current :dir project)
(defconst rg-symbol-prefix "(?<![a-zA-Z0-9_-])" )
(defconst rg-symbol-suffix "(?![a-zA-Z0-9-_])" )
;; 0宽断言
;; https://leongfeng.github.io/2017/03/10/regex-java-assertions/
;; '(?<![a-zA-Z0-9_-])(world)(?![a-zA-Z0-9-_])' ;
;; 搜索world ,只是它前后不能包含a-z等字符, 基本等价于\bworld\b
;; 但是\b 对于_- 也当作单词边界,并不符合我的预期
;;
;; 利用0宽断言作多关键字匹配,
;; rg --pcre2 '^(?=.*hello)^(?=.*world)' #include hello and world
;; rg --pcre2 '^(?!.*hello)^(?=.*world)' # include world ,exclude hello
;; hello world
;; world hello
;; world foo
;; hello !world 包含hello 不包含world
(defun vmacs-rg-query(&optional val surround-as-symbol)
(unless val (setq val (thing-at-point 'symbol)))
(let ((tokens (split-string val " " t))
(regex ""))
(dolist (token tokens)
(if (char-equal ?! (car (string-to-list token)))
(setq regex (format "%s^(?!.*%s)" regex (substring token 1 (length token))))
(setq regex (format "%s^(?=.*%s)" regex token ))))
(when (= 1 (length tokens)) (setq regex val))
(if surround-as-symbol
(format "%s%s%s" rg-symbol-prefix regex rg-symbol-suffix)
regex)))
(defun vmacs-rg-unquote (val )
(when (string-prefix-p rg-symbol-prefix val)
(setq val (substring val (length rg-symbol-prefix))))
(when (string-suffix-p rg-symbol-suffix val)
(setq val (substring val 0 (- (length val) (length rg-symbol-suffix)))))
(while (string-match "\\(\\^(\\?!\\.\\*\\(.+?\\))\\)" val)
(setq val (replace-match (format "!%s " (match-string 2 val)) t t val 1)))
(while (string-match "\\(\\^(\\?=\\.\\*\\(.+?\\))\\)" val)
(setq val (replace-match (format "%s " (match-string 2 val)) t t val 1)))
(string-trim val))
(defun vmacs-rg-read-regex (&optional prompt pattern suffix)
(let ((pattern (or pattern (rg-search-pattern rg-cur-search)))
(read-from-minibuffer-orig (symbol-function 'read-from-minibuffer)))
(setq pattern (vmacs-rg-unquote pattern))
(setq pattern (concat pattern (or suffix "")))
(cl-letf (((symbol-function #'read-from-minibuffer)
(lambda (prompt &optional _ keymap read hist _ &rest args)
(apply read-from-minibuffer-orig prompt pattern keymap read hist nil args))))
(read-regexp (or prompt "regexp: " ) nil 'rg-pattern-history))))
(defun vmacs-rg-rerun-change-regex (&optional suffix)
"Rerun last search but prompt for new regexp."
(interactive)
(let ((pattern (vmacs-rg-read-regex nil nil (or suffix " "))))
(setf (rg-search-pattern rg-cur-search)
(vmacs-rg-query pattern nil))
(rg-rerun)))
(defun vmacs-rg-rerun-hide-matched (&optional suffix)
"Rerun last search but prompt for new regexp."
(interactive)
(vmacs-rg-rerun-change-regex " !"))
(defun vmacs-rg-rerun-toggle-surround ()
"Rerun last search but prompt for new search pattern.
IF LITERAL is non nil this will trigger a literal search, otherwise a regexp search."
(interactive)
(let ((pattern (rg-search-pattern rg-cur-search)))
;; Override read-from-minibuffer in order to insert the original
;; pattern in the input area.
(if (not (string-prefix-p rg-symbol-prefix pattern))
(setq pattern (format "%s%s%s" rg-symbol-prefix pattern rg-symbol-suffix))
(setq pattern (substring pattern (length rg-symbol-prefix)))
(when (string-suffix-p rg-symbol-suffix pattern)
(setq pattern (substring pattern 0 (- (length pattern) (length rg-symbol-suffix))))))
(setf (rg-search-pattern rg-cur-search) pattern)
(setf (rg-search-literal rg-cur-search) nil)
(rg-rerun)))
(defun vmacs-rg-rerun-filter-by-file ()
"Rerun last search but exclude selected filename or diredctory with flag: --glob='!*name*'"
(interactive)
(let ((flags (rg-search-flags rg-cur-search))
(dir (read-string (format "%s(file or dir): " (if current-prefix-arg "include" "exclude")))))
(setq flags (append flags (list (format "--glob='%s*%s*'" (if current-prefix-arg "" "!") dir))))
(setf (rg-search-flags rg-cur-search) flags)
(rg-rerun)))