Skip to content

Instantly share code, notes, and snippets.

@nikomatsakis
Created May 23, 2017 21:46
Show Gist options
  • Save nikomatsakis/e5fb99718db9ad2de50191d1a0fd5b34 to your computer and use it in GitHub Desktop.
Save nikomatsakis/e5fb99718db9ad2de50191d1a0fd5b34 to your computer and use it in GitHub Desktop.
;;; ripgrep.el --- from end for ripgrep
;;
;; Copyright (C) 2016 Nicholas Matsakis
;;
;; Adapted from ack-and-a-half.el, which is licensed as follows:
;;
;; Copyright (C) 2011 Jacob Helwig
;;
;; Author: Jacob Helwig <jacob+ack * technosorcery.net>
;; Version: 0.0.1
;; Homepage: http://technosorcery.net
;;
;; This file is NOT part of GNU Emacs.
;;
;; This program 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 2, or (at
;; your option) any later version.
;;
;; This program 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 this program ; see the file COPYING. If not, write to
;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
;; Boston, MA 02111-1307, USA.
;;
;;; Commentary:
;;
;; ripgrep.el provides a simple compilation mode for the tool ripgrep
;;
;; Add the following to your .emacs:
;;
;; (add-to-list 'load-path "/path/to/ripgrep")
;; (autoload 'ripgrep-same "ripgrep" nil t)
;; (autoload 'ripgrep "ripgrep" nil t)
;; (autoload 'ripgrep-find-file-samee "ripgrep" nil t)
;; (autoload 'ripgrep-find-file "ripgrep" nil t)
;;
;; Run `ripgrep' to search for all files and `ripgrep-same' to search for
;; files of the same type as the current buffer.
;;
;; `next-error' and `previous-error' can be used to jump to the
;; matches.
;;
;; `ripgrep-find-file' and `ripgrep-find-same-file' use ripgrep to
;; list the files in the current project. It's a convenient, though
;; slow, way of finding files.
;;
(eval-when-compile (require 'cl))
(require 'compile)
(require 'grep)
(add-to-list 'debug-ignored-errors
"^Moved \\(back before fir\\|past la\\)st match$")
(add-to-list 'debug-ignored-errors "^File .* not found$")
(define-compilation-mode ripgrep-mode "ripgrep"
"ripgrep results compilation mode."
(set (make-local-variable 'compilation-disable-input) t)
(set (make-local-variable 'compilation-error-face) grep-hit-face))
(defgroup ripgrep nil "Front end for ripgrep."
:group 'tools
:group 'matching)
(defcustom ripgrep-executable (executable-find "rg")
"*The location of the ripgrep executable"
:group 'ripgrep
:type 'file)
(defcustom ripgrep-arguments nil
"*Extra arguments to pass to ripgrep."
:group 'ripgrep
:type '(repeat (string)))
(defcustom ripgrep-mode-type-alist nil
"*File type(s) to search per major mode. (ripgrep-same)
This overrides values in `ripgrep-mode-type-default-alist'.
The car in each list element is a major mode, and the rest
is a list of strings passed to the --type flag of ripgrep when running
`ripgrep-same'."
:group 'ripgrep
:type '(repeat (cons (symbol :tag "Major mode")
(repeat (string :tag "rg --type")))))
(defcustom ripgrep-mode-extension-alist nil
"*File extensions to search per major mode. (ripgrep-same)
This overrides values in `ripgrep-mode-extension-default-alist'.
The car in each list element is a major mode, and the rest
is a list of file extensions to be searched in addition to
the type defined in `ripgrep-mode-type-alist' when
running `ripgrep-same'."
:group 'ripgrep
:type '(repeat (cons (symbol :tag "Major mode")
(repeat :tag "File extensions" (string)))))
(defcustom ripgrep-ignore-case 'smart
"*Ignore case when searching
The special value 'smart enables the ack option \"smart-case\"."
:group 'ripgrep
:type '(choice (const :tag "Case sensitive" nil)
(const :tag "Smart case" 'smart)
(const :tag "Case insensitive" t)))
(defcustom ripgrep-regexp-search t
"*Default to regular expression searching.
Giving a prefix argument to `ripgrep' toggles this option."
:group 'ripgrep
:type '(choice (const :tag "Literal searching" nil)
(const :tag "Regular expression searching" t)))
(defcustom ripgrep-use-environment t
"*Use .ackrc and ACK_OPTIONS when searching."
:group 'ripgrep
:type '(choice (const :tag "Ignore environment" nil)
(const :tag "Use environment" t)))
(defcustom ripgrep-root-directory-functions '(ripgrep-guess-project-root)
"*List of functions used to find the base directory to ack from.
These functions are called until one returns a directory. If successful,
`ripgrep' is run from that directory instead of from `default-directory'.
The directory is verified by the user depending on `ripgrep-prompt-for-directory'."
:group 'ripgrep
:type '(repeat function))
(defcustom ripgrep-project-root-file-patterns
'(".project\\'"
".xcodeproj\\'"
".sln\\'"
"\\`Project.ede\\'"
"\\`.git\\'"
"\\`.bzr\\'"
"\\`_darcs\\'"
"\\`.hg\\'")
"*List of file patterns for the project root (used by `ripgrep-guess-project-root'.
Each element is a regular expression. If a file matching any element is
found in a directory, then that directory is assumed to be the project
root by `ripgrep-guess-project-root'."
:group 'ripgrep
:type '(repeat (string :tag "Regular expression")))
(defcustom ripgrep-prompt-for-directory 'unless-guessed
"*Prompt for directory in which to run ack.
If this is 'unless-guessed, then the value determined by `ripgrep-root-directory-functions'
is used without confirmation. If it is nil, then the directory is never
confirmed. If t, then always prompt for the directory to use."
:group 'ripgrep
:type '(choice (const :tag "Don't prompt" nil)
(const :tag "Don't prompt when guessed" 'unless-guessed)
(const :tag "Always prompt" t)))
;;; Default setting lists ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defconst ripgrep-mode-type-default-alist
'((actionscript-mode "actionscript")
(LaTeX-mode "tex")
(TeX-mode "tex")
(asm-mode "asm")
(batch-file-mode "batch")
(c++-mode "cpp")
(c-mode "cc")
(cfmx-mode "cfmx")
(cperl-mode "perl")
(csharp-mode "csharp")
(css-mode "css")
(emacs-lisp-mode "elisp")
(erlang-mode "erlang")
(espresso-mode "java")
(fortran-mode "fortran")
(haskell-mode "haskell")
(hexl-mode "binary")
(html-mode "html")
(java-mode "java")
(javascript-mode "js")
(jde-mode "java")
(js2-mode "js")
(jsp-mode "jsp")
(latex-mode "tex")
(lisp-mode "lisp")
(lua-mode "lua")
(makefile-mode "make")
(mason-mode "mason")
(nxml-mode "xml")
(objc-mode "objc" "objcpp")
(ocaml-mode "ocaml")
(parrot-mode "parrot")
(perl-mode "perl")
(php-mode "php")
(plone-mode "plone")
(python-mode "python")
(ruby-mode "ruby")
(scheme-mode "scheme")
(shell-script-mode "shell")
(skipped-mode "skipped")
(smalltalk-mode "smalltalk")
(sql-mode "sql")
(tcl-mode "tcl")
(tex-mode "tex")
(tt-mode "tt")
(vb-mode "vb")
(vim-mode "vim")
(xml-mode "xml")
(yaml-mode "yaml"))
"Default values for `ripgrep-mode-type-alist'.")
(defconst ripgrep-mode-extension-default-alist
'((d-mode "d"))
"Default values for `ripgrep-mode-extension-alist'.")
(defun ripgrep-create-type (extensions)
(list "--type-set"
(concat "ripgrep-custom-type=" (mapconcat 'identity extensions ","))
"--type" "ripgrep-custom-type"))
(defun ripgrep-type-for-major-mode (mode)
"Return the --type and --type-set arguments to use with ack for major mode MODE."
(let ((types (cdr (or (assoc mode ripgrep-mode-type-alist)
(assoc mode ripgrep-mode-type-default-alist))))
(ext (cdr (or (assoc mode ripgrep-mode-extension-alist)
(assoc mode ripgrep-mode-extension-default-alist))))
result)
(dolist (type types)
(push type result)
(push "--type" result))
(if ext
(if types
`("--type-add" ,(concat (car types)
"=" (mapconcat 'identity ext ","))
. ,result)
(ripgrep-create-type ext))
result)))
;;; Project root ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defun ripgrep-guess-project-root ()
"Guess the project root directory.
This is intended to be used in `ripgrep-root-directory-functions'."
(catch 'root
(let ((dir (expand-file-name (if buffer-file-name
(file-name-directory buffer-file-name)
default-directory)))
(pattern (mapconcat 'identity ripgrep-project-root-file-patterns "\\|")))
(while (not (equal dir "/"))
(when (directory-files dir nil pattern t)
(throw 'root dir))
(setq dir (file-name-directory (directory-file-name dir)))))))
;;; Commands ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defvar ripgrep-directory-history nil
"Directories recently searched with `ripgrep'.")
(defvar ripgrep-literal-history nil
"Strings recently searched for with `ripgrep'.")
(defvar ripgrep-regexp-history nil
"Regular expressions recently searched for with `ripgrep'.")
(defsubst ripgrep-read (regexp)
(read-from-minibuffer (if regexp "ripgrep pattern: " "ripgrep literal search: ")
nil nil nil
(if regexp 'ripgrep-regexp-history 'ripgrep-literal-history)))
(defun ripgrep-read-dir ()
(let ((dir (run-hook-with-args-until-success 'ripgrep-root-directory-functions)))
(if ripgrep-prompt-for-directory
(if (and dir (eq ripgrep-prompt-for-directory 'unless-guessed))
dir
(read-directory-name "Directory: " dir dir t))
(or dir
(and buffer-file-name (file-name-and-directory buffer-file-name))
default-directory))))
(defsubst ripgrep-xor (a b)
(if a (not b) b))
(defun ripgrep-interactive ()
"Return the (interactive) arguments for `ripgrep' and `ripgrep-same'."
(let ((regexp (ripgrep-xor current-prefix-arg ripgrep-regexp-search)))
(list (ripgrep-read regexp)
regexp
(ripgrep-read-dir))))
(defun ripgrep-type ()
(or (ripgrep-type-for-major-mode major-mode)
(when buffer-file-name
(ripgrep-create-type (list (file-name-extension buffer-file-name))))))
(defun ripgrep-option (name enabled)
(format "--%s%s" (if enabled "" "no") name))
(defun ripgrep-arguments-from-options (regexp)
(let ((arguments (list "--no-heading" "--color" "never")))
;(ripgrep-option "env" ripgrep-use-environment)
(if (eq ripgrep-ignore-case 'smart-case)
(push "--smart-case" arguments))
(unless ripgrep-ignore-case
(push "-i" arguments))
(unless regexp
(push "--literal" arguments))
arguments))
(defun ripgrep-string-replace (from to string &optional re)
"Replace all occurrences of FROM with TO in STRING.
All arguments are strings.
When optional fourth argument is non-nil, treat the from as a regular expression."
(let ((pos 0)
(res "")
(from (if re from (regexp-quote from))))
(while (< pos (length string))
(if (setq beg (string-match from string pos))
(progn
(setq res (concat res
(substring string pos (match-beginning 0))
to))
(setq pos (match-end 0)))
(progn
(setq res (concat res (substring string pos (length string))))
(setq pos (length string)))))
res))
(defun ripgrep-shell-quote (string)
"Wrap in single quotes, and quote existing single quotes to make shell safe."
(concat "'" (ripgrep-string-replace "'" "'\\''" string) "'"))
(defun ripgrep-run (directory regexp &rest arguments)
"Run ack in DIRECTORY with ARGUMENTS."
(setq default-directory
(if directory
(file-name-as-directory (expand-file-name directory))
default-directory))
(setq arguments (append ripgrep-arguments
(nconc (ripgrep-arguments-from-options regexp)
arguments)))
(compilation-start (mapconcat 'identity (nconc (list ripgrep-executable) arguments) " ")
'ripgrep-mode))
(defun ripgrep-read-file (prompt choices)
(if ido-mode
(ido-completing-read prompt choices nil t)
(require 'iswitchb)
(with-no-warnings
(let ((iswitchb-make-buflist-hook
(lambda () (setq iswitchb-temp-buflist choices))))
(iswitchb-read-buffer prompt nil t)))))
(defun ripgrep-list-files (directory &rest arguments)
(with-temp-buffer
(let ((default-directory directory))
(when (eq 0 (apply 'call-process ripgrep-executable nil t nil "-f" "--print0"
arguments))
(goto-char (point-min))
(let ((beg (point-min))
files)
(while (re-search-forward "\0" nil t)
(push (buffer-substring beg (match-beginning 0)) files)
(setq beg (match-end 0)))
files)))))
(defun ripgrep-version-string ()
"Return the ack version string."
(with-temp-buffer
(call-process ack-executable nil t nil "--version")
(goto-char (point-min))
(re-search-forward " +")
(buffer-substring (point) (point-at-eol))))
;;; Public interface ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;###autoload
(defun ripgrep (pattern &optional regexp directory)
"Run ack.
PATTERN is interpreted as a regular expression, iff REGEXP is non-nil. If
called interactively, the value of REGEXP is determined by `ripgrep-regexp-search'.
A prefix arg toggles the behavior.
DIRECTORY is the root directory. If called interactively, it is determined by
`ripgrep-project-root-file-patterns'. The user is only prompted, if
`ripgrep-prompt-for-directory' is set."
(interactive (ripgrep-interactive))
(ripgrep-run directory regexp (ripgrep-shell-quote pattern)))
;;;###autoload
(defun ripgrep-same (pattern &optional regexp directory)
"Run ack with --type matching the current `major-mode'.
The types of files searched are determined by `ripgrep-mode-type-alist' and
`ripgrep-mode-extension-alist'. If no type is configured, the buffer's
file extension is used for the search.
PATTERN is interpreted as a regular expression, iff REGEXP is non-nil. If
called interactively, the value of REGEXP is determined by `ripgrep-regexp-search'.
A prefix arg toggles that value.
DIRECTORY is the directory in which to start searching. If called
interactively, it is determined by `ripgrep-project-root-file-patterns`.
The user is only prompted, if `ripgrep-prompt-for-directory' is set.`"
(interactive (ripgrep-interactive))
(let ((type (ack-type)))
(if type
(apply 'ripgrep-run directory regexp (append type (list (ripgrep-shell-quote pattern))))
(ripgrep pattern regexp directory))))
;;;###autoload
(defun ripgrep-find-file (&optional directory)
"Prompt to find a file found by ack in DIRECTORY."
(interactive (list (ripgrep-read-dir)))
(find-file (expand-file-name (ack-read-file "Find file: "
(ripgrep-list-files directory))
directory)))
;;;###autoload
(defun ripgrep-find-file-same (&optional directory)
"Prompt to find a file found by ack in DIRECTORY."
(interactive (list (ripgrep-read-dir)))
(find-file (expand-file-name
(ripgrep-read-file "Find file: "
(apply 'ripgrep-list-files directory (ripgrep-type)))
directory)))
;;; End ripgrep.el ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(provide 'ripgrep)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment