分享关于rg.el 的心得。

doc https://rgel.readthedocs.io

  1. 支持类似helm 的多个关键词按空格分开的过滤方式,支持在 rg buffer 中 增量添加关键词,重新用新关键词刷新 rg,(绑定在/ 与z 上)
  2. 支持在 rg buffer 中 增量排除匹配的文件或目录,缩小匹配的范围(绑定在x)
  3. 支持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)))

更多配置可以参见 vmacs/conf-rg.el at master · jixiuf/vmacs · GitHub

3 个赞

关于删除行(类似 narrow 机制),这里有一个现成的包

2 个赞

能简单介绍一下rg.elcounsel-rg比有什么明显的优势吗?简单尝试一下感觉缺了interactive,多了类似于occur的功能。

我试着写过类似的单纯buffer 内的 hide matched line 和hide unmatched line ,后来发现有些rg 命令显示的结果过多,耗时太长,,后来才实现了这种rg 命令层面的过滤,直接让返回的结果少一些。

我的感觉是你如果用counsel-rg 的话 就没有必要换到rg.el 上。 我是大部分功能都换成icomplete-mode 了, 不再想用ivy/counsel 了 。

这些功能在 color-rg.el 里面早就实现了,可以考虑 color-rg.el ,哈哈哈。

哈有空看一下