emacs-config/lisp/ripgrep.el
2026-04-20 06:30:35 -05:00

188 lines
8.6 KiB
EmacsLisp

;;; ripgrep.el --- -*- lexical-binding: t -*-
;; https://www.dyerdwelling.family/emacs/20260109094340-emacs--a-single-function-ripgrep-alternative-to-rgrep/
;;; Commentary:
;;; Code:
(defconst ripgrep-error-regexp
`((,(concat "^\\(?:"
;; Parse using NUL characters when `--null' is used.
;; Note that we must still assume no newlines in
;; filenames due to "foo: Is a directory." type
;; messages.
"\\(?1:[^\0\n]+\\)\\(?3:\0\\)\\(?2:[0-9]+\\):"
"\\|"
;; Fallback if `--null' is not used, use a tight regexp
;; to handle weird file names (with colons in them) as
;; well as possible. E.g., use [1-9][0-9]* rather than
;; [0-9]+ so as to accept ":034:" in file names.
"\\(?1:"
"\\(?:[a-zA-Z]:\\)?" ; Allow "C:..." for w32.
"[^\n:]+?[^\n/:]\\):[\t ]*\\(?2:[1-9][0-9]*\\)[\t ]*:\\(?:[1-9][0-9]*\\)[\t ]*:"
"\\)")
1 2
;; Calculate column positions (col . end-col) of first grep match on a line
(,(lambda ()
(when grep-highlight-matches
(let* ((beg (match-end 0))
(end (save-excursion (goto-char beg) (line-end-position)))
(mbeg
(text-property-any beg end 'font-lock-face grep-match-face)))
(when mbeg
(- mbeg beg)))))
.
,(lambda ()
(when grep-highlight-matches
(let* ((beg (match-end 0))
(end (save-excursion (goto-char beg) (line-end-position)))
(mbeg
(text-property-any beg end 'font-lock-face grep-match-face))
(mend
(and mbeg (next-single-property-change
mbeg 'font-lock-face nil end))))
(when mend
(- mend beg 1))))))
nil nil
(3 '(face nil display ":")))
("^Binary file \\(.+\\) matches" 1 nil nil 0 1)))
(defun ripgrep (search-term &optional directory glob)
"Run ripgrep (rg) with SEARCH-TERM and optionally DIRECTORY and GLOB.
If ripgrep is unavailable, fall back to Emacs's rgrep command. Highlights SEARCH-TERM in results.
By default, only the SEARCH-TERM needs to be provided. If called with a
universal argument, DIRECTORY and GLOB are prompted for as well."
(interactive
(let* ((univ-arg current-prefix-arg)
(default-search-term
(cond
((use-region-p)
(buffer-substring-no-properties (region-beginning) (region-end)))
((thing-at-point 'symbol t))
((thing-at-point 'word t))
(t ""))))
(list
(read-string (if (string-empty-p default-search-term)
"Search for: "
(format "Search for (default `%s`): " default-search-term))
nil nil default-search-term)
(when univ-arg (read-directory-name "Directory: "))
(when univ-arg (read-string "File pattern (glob, default: ): " nil nil "")))))
(let* ((directory (expand-file-name (or directory default-directory)))
(glob (or glob ""))
(buffer-name "*grep*"))
(if (executable-find "rg")
(let ((buffer (get-buffer-create buffer-name)))
(with-current-buffer buffer
(setq default-directory directory)
(let ((inhibit-read-only t))
(erase-buffer)
(insert (format "-*- mode: grep; default-directory: \"%s\" -*-\n\n" directory))
(if (not (string= "" glob))
(insert (format "[o] Glob: %s\n\n" glob)))
(insert "Searching...\n\n"))
(grep-mode)
(setq-local compilation-error-regexp-alist ripgrep-error-regexp)
(setq-local my/grep-search-term search-term)
(setq-local my/grep-directory directory)
(setq-local my/grep-glob glob))
(pop-to-buffer buffer)
(goto-char (point-min))
(make-process
:name "ripgrep"
:buffer buffer
:command `("rg" "--color=never" "--max-columns=500"
"--column" "--line-number" "--no-heading"
"--smart-case" "-e" ,search-term
"--glob" ,glob ,directory)
:filter `(lambda (proc string)
(when (buffer-live-p (process-buffer proc))
(with-current-buffer (process-buffer proc)
(let ((inhibit-read-only t)
(moving (= (point) (process-mark proc))))
(setq string (replace-regexp-in-string "[\r\0\x01-\x08\x0B-\x0C\x0E-\x1F]" "" string))
;; Replace full directory path with ./ in the incoming output
(setq string (replace-regexp-in-string
(concat "^" (regexp-quote ,directory))
"./"
string))
(save-excursion
(goto-char (process-mark proc))
(insert string)
(set-marker (process-mark proc) (point)))
(if moving (goto-char (process-mark proc)))))))
:sentinel
`(lambda (proc _event)
(when (memq (process-status proc) '(exit signal))
(with-current-buffer (process-buffer proc)
(let ((inhibit-read-only t))
;; Remove "Searching..." line
(goto-char (point-min))
(while (re-search-forward "Searching\\.\\.\\.\n\n" nil t)
(replace-match "" nil t))
;; Clean up the output - replace full paths with ./
(goto-char (point-min))
(forward-line 3)
(let ((start-pos (point)))
(while (re-search-forward (concat "^" (regexp-quote ,directory)) nil t)
(replace-match "./" t t))
;; Check if any results were found
(goto-char start-pos)
(when (= (point) (point-max))
(insert "No results found.\n")))
(goto-char (point-max))
(insert "\nRipgrep finished\n")
;; Highlight search terms using grep's match face
(goto-char (point-min))
(forward-line 3)
(save-excursion
(while (re-search-forward (regexp-quote ,search-term) nil t)
(put-text-property (match-beginning 0) (match-end 0)
'face 'match)
(put-text-property (match-beginning 0) (match-end 0)
'font-lock-face 'match))))
;; Set up keybindings
(local-set-key (kbd "D")
(lambda ()
(interactive)
(ripgrep my/grep-search-term
(read-directory-name "New search directory: ")
my/grep-glob)))
(local-set-key (kbd "S")
(lambda ()
(interactive)
(ripgrep (read-string "New search term: "
nil nil my/grep-search-term)
my/grep-directory
my/grep-glob)))
(local-set-key (kbd "o")
(lambda ()
(interactive)
(ripgrep my/grep-search-term
my/grep-directory
(read-string "New glob: "))))
(local-set-key (kbd "g")
(lambda ()
(interactive)
(ripgrep my/grep-search-term my/grep-directory my/grep-glob)))
(goto-char (point-min))
(message "ripgrep finished."))))
)
(message "ripgrep started..."))
;; Fallback to rgrep
(progn
(setq default-directory directory)
(message (format "%s : %s : %s" search-term glob directory))
(rgrep search-term (if (string= "" glob) "*" glob) directory)))))
(provide 'ripgrep)
;;; ripgrep.el ends here