-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathgit-grep-transient.el
More file actions
272 lines (232 loc) · 11.2 KB
/
git-grep-transient.el
File metadata and controls
272 lines (232 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
;;; git-grep-transient.el --- Search for text using git grep command -*- lexical-binding: t -*-
;;
;; Copyright (C) 2021-2021, Alain Delplanque
;; Maintainer: Alain Delplanque <alaindelplanque@mailoo.org>
;; Author: Alain Delplanque <alaindelplanque@mailoo.org>
;; URL: https://github.com/adelplanque/git-grep-transient
;; Keywords: git tools vc
;; Version: 0.1.1
;; Package-Requires: ((emacs "27.1") (magit "3.3.0") (transient "0.6.0") (symbol-overlay "4.2"))
;; This file is NOT part of GNU Emacs.
;;; Licence:
;; 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 3 of the License, 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. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; git-grep-transient.el allows pattern searching using the git grep command.
;; It works by using the transient package for selection of search options,
;; pattern, directory, file type and reference.
;; Usage:
;; (require 'git-grep-transient)
;; Then M-x git-grep-transient to start command
;;; Code:
(require 'compile)
(require 'magit)
(require 'symbol-overlay)
(require 'transient)
(defvar git-grep-transient--values-alist nil
"Alist of parameters by git repository.")
(defvar git-grep-transient--hit-face compilation-info-face
"Face name to use for grep hits.")
(defconst git-grep-transient--mode-filename-alist
'((python-mode . "*.py")
(emacs-lisp-mode . "*.el")))
(defun git-grep-transient--compilation-find-file
(orig-fun marker filename directory &rest formats)
"Wrap `compilation-find-file' to open file at a specific revision.
ORIG-FUN MARKER FILENAME DIRECTORY FORMATS are then arguments of the
`compilation-find-file' function."
(if (derived-mode-p 'git-grep-transient--mode)
(let* ((parts (split-string filename ":"))
(rev (car parts))
(filename (cadr parts)))
(if filename
(magit-find-file rev filename)
(apply orig-fun marker rev directory formats)))
(apply orig-fun marker filename directory formats)))
(defun git-grep-transient--next-error-function (&rest args)
"Wraps `compilation-next-error-function' with ARGS to setup advice."
(advice-add 'compilation-find-file :around #'git-grep-transient--compilation-find-file)
(unwind-protect
(apply 'compilation-next-error-function args)
(advice-remove 'compilation-find-file #'git-grep-transient--compilation-find-file)))
(define-compilation-mode git-grep-transient--mode "Git-Grep"
"Compilation mode for Git-Grep search results."
(let ((smbl 'git-grep-transient)
(pttrn '("^\\(\\(?:[^:\n]+?:\\)?[^:\n]+?\\):\\([0-9]+\\):\\([0-9]+\\):"
1 2 3)))
(setq-local truncate-lines t)
(setq-local compilation-disable-input t)
(setq-local compilation-error-regexp-alist (list smbl))
(setq-local compilation-error-regexp-alist-alist (list (cons smbl pttrn)))
(setq-local compilation-process-setup-function #'git-grep-transient--mode-setup)
(setq-local compilation-error-face git-grep-transient--hit-face)
(setq-local next-error-function #'git-grep-transient--next-error-function)))
(defun git-grep-transient--mode-setup ()
"Setup compilation variables and buffer for `git-grep-transient'.
Set up `compilation-exit-message-function'."
(set (make-local-variable 'compilation-exit-message-function)
(lambda (status code msg)
(if (eq status 'exit)
(cond ((and (zerop code) (buffer-modified-p))
'("finished (matches found)\n" . "matched"))
((not (buffer-modified-p))
'("finished with no matches found\n" . "no match"))
(t
(cons msg code)))
(cons msg code)))))
(defun git-grep-transient--default-for-read ()
"Determine the default value of the expression to search for.
This value is based on the position in the buffer."
(unless (git-grep-transient--use-region-p)
(let ((litteral (thing-at-point 'symbol)))
(set-text-properties 0 (length litteral) nil litteral)
litteral)))
(defun git-grep-transient--use-region-p ()
"When to use region as a search expression."
(or (and (fboundp 'use-region-p) (use-region-p))
(and transient-mark-mode mark-active
(> (region-end) (region-beginning)))))
(defclass git-grep-transient--values ()
((directory :initarg :directory :initform nil)
(filename :initarg :filename :initform nil)
(revision :initarg :revision :initform nil)
(expression :initarg :expression :initform nil))
"Parameters to call the git grep command.")
(defclass git-grep-transient--variable (transient-variable)
((unset-value :initarg :unset-value :initform "unset"))
"Subclass of function `transient-variable' that manages history.")
(cl-defmethod transient-init-value ((obj git-grep-transient--variable))
"Overriding the `transient-init-value' method.
The value of object OBJ is initialized from the last used value in the same git
repository."
(let ((values (oref transient--prefix value))
(var-name (oref obj variable)))
(oset obj value (eieio-oref values var-name))))
(cl-defmethod transient-format-value ((obj git-grep-transient--variable))
"Return the propertized vulue string for `git-grep-transient--variable' OBJ."
(let ((value (oref obj value)))
(if value
(propertize value 'face 'transient-value)
(propertize (oref obj unset-value) 'face 'transient-inactive-value))))
(cl-defmethod transient-infix-set ((obj git-grep-transient--variable) value)
"Set VALUE for `git-grep-transient--variable' OBJ."
(oset obj value value)
(let ((values (oref transient--prefix value))
(var-name (oref obj variable)))
(eieio-oset values var-name value)))
(cl-defmethod transient-infix-value ((obj transient-variable))
"Return the value of OBJ's `value' slot."
`(,(oref obj variable) . ,(oref obj value)))
(defun git-grep-transient--init-value (obj)
"Initialize value for `git-grep-transient--variable' OBJ."
(let ((toplevel (magit-toplevel)))
(if (not toplevel) (user-error "Not in a git repository"))
(if (not (assoc toplevel git-grep-transient--values-alist))
(let* ((values (git-grep-transient--values :directory toplevel)))
(push `(,toplevel . ,values) git-grep-transient--values-alist)))
(let ((values (cdr (assoc toplevel git-grep-transient--values-alist)))
(default (git-grep-transient--default-for-read)))
(if default (oset values expression default))
(if (not (oref values filename))
(oset values filename
(cdr (assoc (with-current-buffer (or (buffer-base-buffer)
(current-buffer)) major-mode)
git-grep-transient--mode-filename-alist))))
(oset obj value values))))
(defun git-grep-transient--read-expression (&rest _args)
"Ask the user for an expression to search for."
(let* ((default (git-grep-transient--default-for-read))
(prompt (if default (format "Search (%s): " default) "Search: "))
(pattern (read-string prompt nil)))
(if (string= "" pattern) default pattern)))
(defun git-grep-transient--read-directory (&rest _args)
"Ask user for a directory."
(let ((directory (read-directory-name "Root directory: "))
(toplevel (magit-toplevel)))
(if (string-prefix-p ".." (file-relative-name directory toplevel)) toplevel directory)))
(defun git-grep-transient--read-filename (&rest _args)
"Ask user for a filename or filename pattern."
(let* ((mode (with-current-buffer (or (buffer-base-buffer) (current-buffer)) major-mode))
(default (cdr (assoc mode git-grep-transient--mode-filename-alist)))
(prompt (if default (format "Filename pattern (%s): " default) "Filename pattern: "))
(pattern (read-from-minibuffer prompt)))
(if (string= "" pattern) default pattern)))
(defun git-grep-transient--read-revision (&rest _args)
"Ask user for a git revision."
(let ((pseudo-revs '("{cached}")))
(magit-completing-read "Search in revision: "
(append pseudo-revs
(magit-list-refnames nil t))
nil nil nil 'history
(or (magit-branch-or-commit-at-point)
(magit-get-current-branch)))))
(transient-define-infix git-grep-transient--expression-infix ()
:description "Search expression"
:class 'git-grep-transient--variable
:key "e"
:variable 'expression
:reader #'git-grep-transient--read-expression
:always-read t)
(transient-define-infix git-grep-transient--directory-infix ()
:description "Search in directory"
:class 'git-grep-transient--variable
:key "d"
:variable 'directory
:reader #'git-grep-transient--read-directory
:always-read t)
(transient-define-infix git-grep-transient--filename-infix ()
:description "Search in files"
:class 'git-grep-transient--variable
:key "f"
:variable 'filename
:reader #'git-grep-transient--read-filename)
(transient-define-infix git-grep-transient--revision-infix ()
:description "Search in revision"
:class 'git-grep-transient--variable
:key "r"
:variable 'revision
:reader #'git-grep-transient--read-revision
:unset-value "{worktree}")
(defun git-grep-transient--run (&rest args)
"Run.the git grep command.
ARGS are arguments provided by `git-grep-transient'."
(interactive (transient-args 'git-grep-transient))
(let-alist args
(when (not .expression) (user-error "Nothing to search"))
(message "eq index: %s" (eq .revision "{cached}"))
(let (cached revision)
(cond
((string= .revision "{cached}") (setq cached "--cached"))
(.revision (setq revision (shell-quote-argument .revision))))
(let* ((default-directory .directory)
(cmd (append (list "git" "--no-pager" "grep" "-n" "--column" cached
(shell-quote-argument .expression) revision)
(if .filename (list "--" (shell-quote-argument .filename)))))
(buf (compilation-start (string-join cmd " ") #'git-grep-transient--mode)))
(with-current-buffer buf
(symbol-overlay-remove-all)
(setq symbol-overlay-keywords-alist nil)
(symbol-overlay-put-all .expression nil))))))
;;;###autoload (autoload 'git-grep-transient "git-grep-transient" nil t)
(transient-define-prefix git-grep-transient ()
"Search text with git grep command."
:info-manual "Search text with git-grep-transient"
:init-value #'git-grep-transient--init-value
["Arguments"
(git-grep-transient--expression-infix)
(git-grep-transient--directory-infix)
(git-grep-transient--filename-infix)
(git-grep-transient--revision-infix)]
["Commands"
("c" "run" git-grep-transient--run)
("q" "Quit" transient-quit-one)])
(provide 'git-grep-transient)
;;; git-grep-transient.el ends here