mirror of https://github.com/Chizi123/.emacs.d.git

Chizi123
2018-11-18 76bbd07de7add0f9d13c6914f158d19630fe2f62
commit | author | age
5cb5f7 1 ;;; helm-grep.el --- Helm Incremental Grep. -*- lexical-binding: t -*-
C 2
3 ;; Copyright (C) 2012 ~ 2018 Thierry Volpiatto <thierry.volpiatto@gmail.com>
4
5 ;; This program is free software; you can redistribute it and/or modify
6 ;; it under the terms of the GNU General Public License as published by
7 ;; the Free Software Foundation, either version 3 of the License, or
8 ;; (at your option) any later version.
9
10 ;; This program is distributed in the hope that it will be useful,
11 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
12 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 ;; GNU General Public License for more details.
14
15 ;; You should have received a copy of the GNU General Public License
16 ;; along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
18 ;;; Code:
19 (require 'cl-lib)
20 (require 'format-spec)
21 (require 'helm)
22 (require 'helm-help)
23 (require 'helm-regexp)
24
25 ;;; load wgrep proxy if it's available
26 (require 'wgrep-helm nil t)
27
28 (declare-function helm-buffer-list "helm-buffers")
29 (declare-function View-quit "view")
30 (declare-function doc-view-goto-page "doc-view" (page))
31 (declare-function pdf-view-goto-page "pdf-view" (page &optional window))
32 (declare-function helm-mm-split-pattern "helm-multi-match")
33 (declare-function helm--ansi-color-apply "helm-lib")
34 (defvar helm--ansi-color-regexp)
35
36
37 (defgroup helm-grep nil
38   "Grep related Applications and libraries for Helm."
39   :group 'helm)
40
41 (defcustom helm-grep-default-command
42   "grep --color=always -a -d skip %e -n%cH -e %p %f"
43   "Default grep format command for `helm-do-grep-1'.
44 Where:
45 '%e' format spec is for --exclude or --include grep options or
46      ack-grep --type option.               (Not mandatory)
47
48 '%c' format spec is for case-fold-search,
49      whether to use the -i option of grep. (Not mandatory)
50      When you specify this spec, helm grep will use smartcase
51      that is when a upcase character is found in pattern case will
52      be respected and no '-i' option will be used, otherwise, when
53      no upcase character is found in pattern always use '-i'.
54      If you don't want this behavior, don't use this spec and
55      specify or not the '-i' option.
56      Note that with ack-grep this is not needed, just specify
57      the '--smart-case' option.
58
59 '%p' format spec is for pattern.           (Mandatory)
60
61 '%f' format spec is for filenames.         (Mandatory)
62
63 If your grep version doesn't support the --exclude/include args
64 don't specify the '%e' format spec.
65
66 Helm also support ack-grep and git-grep ,
67 here a default command example for ack-grep:
68
69 \(setq helm-grep-default-command \"ack-grep -Hn --color --smart-case --no-group %e %p %f\"
70        helm-grep-default-recurse-command \"ack-grep -H --color --smart-case --no-group %e %p %f\")
71
72 You can ommit the %e spec if you don't want to be prompted for types.
73
74 NOTE: Helm for ack-grep support ANSI sequences, so you can remove
75 the \"--no-color\" option safely (recommended)
76 However you should specify --color to enable multi matches highlighting
77 because ack disable it when output is piped.
78
79 Same for grep you can use safely the option \"--color=always\" (default).
80 You can customize the color of matches using GREP_COLORS env var.
81 e.g: \(setenv \"GREP_COLORS\" \"ms=30;43:mc=30;43:sl=01;37:cx=:fn=35:ln=32:bn=32:se=36\")
82
83 To enable ANSI color in git-grep just add \"--color=always\".
84 To customize the ANSI color in git-grep, GREP_COLORS have no effect,
85 you will have to setup this in your .gitconfig:
86
87     [color \"grep\"]
88         match = black yellow
89
90 where \"black\" is the foreground and \"yellow\" the background.
91 See the git documentation for more infos.
92
93 `helm-grep-default-command' and `helm-grep-default-recurse-command'are
94 independents, so you can enable `helm-grep-default-command' with ack-grep
95 and `helm-grep-default-recurse-command' with grep if you want to be faster
96 on recursive grep.
97
98 NOTE: Remote grepping is not available with ack-grep,
99       and badly supported with grep because tramp handle badly
100       repeated remote processes in a short delay (< to 5s)."
101   :group 'helm-grep
102   :type  'string)
103
104 (defcustom helm-grep-default-recurse-command
105   "grep --color=always -a -d recurse %e -n%cH -e %p %f"
106   "Default recursive grep format command for `helm-do-grep-1'.
107 See `helm-grep-default-command' for format specs and infos about ack-grep."
108   :group 'helm-grep
109   :type  'string)
110
111 (defcustom helm-default-zgrep-command
112   "zgrep --color=always -a -n%cH -e %p %f"
113   "Default command for Zgrep.
114 See `helm-grep-default-command' for infos on format specs.
115 Option --color=always is supported and can be used safely
116 to replace the helm internal match highlighting,
117 see `helm-grep-default-command' for more infos."
118   :group 'helm-grep
119   :type  'string)
120
121 (defcustom helm-pdfgrep-default-command
122   "pdfgrep --color always -niH %s %s"
123   "Default command for pdfgrep.
124 Option \"--color always\" is supported starting helm version 1.7.8,
125 when used matchs will be highlighted according to GREP_COLORS env var."
126   :group 'helm-grep
127   :type  'string)
128
129 (defcustom helm-pdfgrep-default-recurse-command
130   "pdfgrep --color always -rniH %s %s"
131   "Default recurse command for pdfgrep.
132 Option \"--color always\" is supported starting helm version 1.7.8,
133 when used matchs will be highlighted according to GREP_COLORS env var."
134   :group 'helm-grep
135   :type  'string)
136
137 (defcustom helm-grep-use-ioccur-style-keys t
138   "Use Arrow keys to jump to occurences."
139   :group 'helm-grep
140   :type  'boolean)
141
142 (defcustom helm-pdfgrep-default-read-command nil
143   "Default command to read pdf files from pdfgrep.
144 Where '%f' format spec is filename and '%p' is page number.
145 e.g In Ubuntu you can set it to:
146
147     \"evince --page-label=%p '%f'\"
148
149 If set to nil either `doc-view-mode' or `pdf-view-mode' will be used
150 instead of an external command."
151   :group 'helm-grep
152   :type  'string)
153
154 (defcustom helm-grep-max-length-history 100
155   "Max number of elements to save in `helm-grep-history'."
156   :group 'helm-grep
157   :type 'integer)
158
159 (defcustom helm-zgrep-file-extension-regexp
160   ".*\\(\\.gz\\|\\.bz\\|\\.xz\\|\\.lzma\\)$"
161   "Default file extensions zgrep will search in."
162   :group 'helm-grep
163   :type 'string)
164
165 (defcustom helm-grep-preferred-ext nil
166   "This file extension will be preselected for grep."
167   :group 'helm-grep
168   :type  'string)
169
170 (defcustom helm-grep-save-buffer-name-no-confirm nil
171   "when *hgrep* already exists,auto append suffix."
172   :group 'helm-grep
173   :type 'boolean)
174
175 (defcustom helm-grep-ignored-files
176   (cons ".#*" (delq nil (mapcar (lambda (s)
177                                   (unless (string-match-p "/\\'" s)
178                                     (concat "*" s)))
179                                 completion-ignored-extensions)))
180   "List of file names which `helm-grep' shall exclude."
181   :group 'helm-grep
182   :type '(repeat string))
183
184 (defcustom helm-grep-ignored-directories
185   helm-walk-ignore-directories
186   "List of names of sub-directories which `helm-grep' shall not recurse into."
187   :group 'helm-grep
188   :type '(repeat string))
189
190 (defcustom helm-grep-truncate-lines t
191   "When nil the grep line that appears will not be truncated."
192   :group 'helm-grep
193   :type 'boolean)
194
195 (defcustom helm-grep-file-path-style 'basename
196   "File path display style when grep results are displayed.
197 Possible value are:
198     basename: displays only the filename, none of the directory path
199     absolute: displays absolute path
200     relative: displays relative path from root grep directory."
201   :group 'helm-grep
202   :type '(choice (const :tag "Basename" basename)
203                  (const :tag "Absolute" absolute)
204                  (const :tag "Relative" relative)))
205
206 (defcustom helm-grep-actions
207   (helm-make-actions
208    "Find File" 'helm-grep-action
209    "Find file other frame" 'helm-grep-other-frame
210    "Save results in grep buffer" 'helm-grep-save-results
211    "Find file other window (C-u vertically)" 'helm-grep-other-window)
212   "Actions for helm grep."
213   :group 'helm-grep
214   :type '(alist :key-type string :value-type function))
215
216 (defcustom helm-grep-pipe-cmd-switches nil
217   "A list of additional parameters to pass to grep pipe command.
218 This will be used for pipe command for multiple pattern matching
219 for grep, zgrep ack-grep and git-grep backends.
220 If you add extra args for ack-grep, use ack-grep options,
221 for others (grep, zgrep and git-grep) use grep options.
222 Here are the commands where you may want to add switches:
223
224     grep --color=always
225     ack-grep --smart-case --color
226
227 You probably don't need to use this unless you know what you are doing."
228   :group 'helm-grep
229   :type 'string)
230
231 (defcustom helm-grep-ag-pipe-cmd-switches nil
232   "A list of additional parameters to pass to grep-ag pipe command.
233 Use parameters compatibles with the backend you are using
234 \(i.e AG for AG, PT for PT or RG for RG)
235
236 You probably don't need to use this unless you know what you are doing."
237   :group 'helm-grep
238   :type 'string)
239
240
241 ;;; Faces
242 ;;
243 ;;
244 (defgroup helm-grep-faces nil
245   "Customize the appearance of helm-grep."
246   :prefix "helm-"
247   :group 'helm-grep
248   :group 'helm-faces)
249
250 (defface helm-grep-match
251   '((((background light)) :foreground "#b00000")
252     (((background dark))  :foreground "gold1"))
253   "Face used to highlight grep matches.
254 Have no effect when grep backend use \"--color=\"."
255   :group 'helm-grep-faces)
256
257 (defface helm-grep-file
258     '((t (:foreground "BlueViolet"
259           :underline t)))
260   "Face used to highlight grep results filenames."
261   :group 'helm-grep-faces)
262
263 (defface helm-grep-lineno
264     '((t (:foreground "Darkorange1")))
265   "Face used to highlight grep number lines."
266   :group 'helm-grep-faces)
267
268 (defface helm-grep-finish
269     '((t (:foreground "Green")))
270   "Face used in mode line when grep is finish."
271   :group 'helm-grep-faces)
272
273 (defface helm-grep-cmd-line
274     '((t (:inherit font-lock-type-face)))
275   "Face used to highlight grep command line when no results."
276   :group 'helm-grep-faces)
277
278
279 ;;; Keymaps
280 ;;
281 ;;
282 (defvar helm-grep-map
283   (let ((map (make-sparse-keymap)))
284     (set-keymap-parent map helm-map)
285     (define-key map (kbd "M-<down>") 'helm-goto-next-file)
286     (define-key map (kbd "M-<up>")   'helm-goto-precedent-file)
287     (define-key map (kbd "C-c o")    'helm-grep-run-other-window-action)
288     (define-key map (kbd "C-c C-o")  'helm-grep-run-other-frame-action)
289     (define-key map (kbd "C-x C-s")  'helm-grep-run-save-buffer)
290     (define-key map (kbd "DEL")      'helm-delete-backward-no-update)
291     (when helm-grep-use-ioccur-style-keys
292       (define-key map (kbd "<right>")  'helm-execute-persistent-action)
293       (define-key map (kbd "<left>")  'helm-grep-run-default-action))
294     (delq nil map))
295   "Keymap used in Grep sources.")
296
297 (defvar helm-pdfgrep-map
298   (let ((map (make-sparse-keymap)))
299     (set-keymap-parent map helm-map)
300     (define-key map (kbd "M-<down>") 'helm-goto-next-file)
301     (define-key map (kbd "M-<up>")   'helm-goto-precedent-file)
302     (define-key map (kbd "DEL")      'helm-delete-backward-no-update)
303     map)
304   "Keymap used in pdfgrep.")
305
306 (defvar helm-grep-mode-map
307   (let ((map (make-sparse-keymap)))
308     (define-key map (kbd "RET")      'helm-grep-mode-jump)
309     (define-key map (kbd "C-o")      'helm-grep-mode-jump-other-window)
310     (define-key map (kbd "<C-down>") 'helm-grep-mode-jump-other-window-forward)
311     (define-key map (kbd "<C-up>")   'helm-grep-mode-jump-other-window-backward)
312     (define-key map (kbd "<M-down>") 'helm-gm-next-file)
313     (define-key map (kbd "<M-up>")   'helm-gm-precedent-file)
314     (define-key map (kbd "M-n")      'helm-grep-mode-jump-other-window-forward)
315     (define-key map (kbd "M-p")      'helm-grep-mode-jump-other-window-backward)
316     (define-key map (kbd "M-N")      'helm-gm-next-file)
317     (define-key map (kbd "M-P")      'helm-gm-precedent-file)
318     map))
319
320
321 ;;; Internals vars
322 ;;
323 ;;
324 (defvar helm-rzgrep-cache (make-hash-table :test 'equal))
325 (defvar helm-grep-default-function 'helm-grep-init)
326 (defvar helm-zgrep-recurse-flag nil)
327 (defvar helm-grep-history nil)
328 (defvar helm-grep-ag-history nil)
329 (defvar helm-grep-last-targets nil)
330 (defvar helm-grep-include-files nil)
331 (defvar helm-grep-in-recurse nil)
332 (defvar helm-grep-use-zgrep nil)
333 (defvar helm-grep-default-directory-fn nil
334   "A function that should return a directory to expand candidate to.
335 It is intended to use as a let-bound variable, DON'T set this globaly.")
336 (defvar helm-pdfgrep-targets nil)
337 (defvar helm-grep-last-cmd-line nil)
338 (defvar helm-grep-split-line-regexp "^\\([[:lower:][:upper:]]?:?.*?\\):\\([0-9]+\\):\\(.*\\)")
339
340
341 ;;; Init
342 ;;
343 ;;
344 (defun helm-grep-prepare-candidates (candidates in-directory)
345   "Prepare filenames and directories CANDIDATES for grep command line."
346   ;; If one or more candidate is a directory, search in all files
347   ;; of this candidate (e.g /home/user/directory/*).
348   ;; If r option is enabled search also in subdidrectories.
349   ;; We need here to expand wildcards to support crap windows filenames
350   ;; as grep doesn't accept quoted wildcards (e.g "dir/*.el").
351   (if helm-zgrep-recurse-flag
352       (mapconcat 'shell-quote-argument candidates " ")
353       ;; When candidate is a directory, search in all its files.
354       ;; NOTE that `file-expand-wildcards' will return also
355       ;; directories, they will be ignored by grep but not
356       ;; by ack-grep that will grep all files of this directory
357       ;; without recursing in their subdirs though, see that as a one
358       ;; level recursion with ack-grep.
359       ;; So I leave it as it is, considering it is a feature. [1]
360       (cl-loop for i in candidates append
361                (cond ((string-match "^git" helm-grep-default-command)
362                       (list i))
363                      ;; Candidate is a directory and we use recursion or ack.
364                      ((and (file-directory-p i)
365                            (or helm-grep-in-recurse
366                                ;; ack-grep accept directory [1].
367                                (helm-grep-use-ack-p)))
368                       (list (expand-file-name i)))
369                      ;; Grep doesn't support directory only when not in recurse.
370                      ((file-directory-p i)
371                       (file-expand-wildcards
372                        (concat (file-name-as-directory (expand-file-name i)) "*") t))
373                      ;; Candidate is a file or wildcard and we use recursion, use the
374                      ;; current directory instead of candidate.
375                      ((and (or (file-exists-p i) (string-match "[*]" i))
376                            helm-grep-in-recurse)
377                       (list (expand-file-name
378                              (directory-file-name ; Needed for windoze.
379                               (file-name-directory (directory-file-name i))))))
380                      ;; Else should be one or more file/directory
381                      ;; possibly marked.
382                      ;; When real is a normal filename without wildcard
383                      ;; file-expand-wildcards returns a list of one file.
384                      ;; wildcards should have been already handled by
385                      ;; helm-read-file-name or helm-find-files but do it from
386                      ;; here too in case we are called from elsewhere.
387                      (t (file-expand-wildcards i t))) into all-files ; [1]
388                finally return
389                (let ((files (if (file-remote-p in-directory)
390                                 ;; Grep don't understand tramp filenames
391                                 ;; use the local name.
392                                 (mapcar (lambda (x)
393                                           (file-remote-p x 'localname))
394                                         all-files)
395                                 all-files)))
396                  ;; When user mark files and use recursion with grep
397                  ;; backend enabled, the loop collect on each marked
398                  ;; candidate its `file-name-directory' and we endup with
399                  ;; duplicates (Issue #1714). FIXME: For now as a quick fix
400                  ;; I just remove dups here but I should handle this inside
401                  ;; the cond above.
402                  (setq files (helm-fast-remove-dups files :test 'equal))
403                  (if (string-match "^git" helm-grep-default-command)
404                      (mapconcat 'identity files " ")
405                      (mapconcat 'shell-quote-argument files " "))))))
406
407 (defun helm-grep-command (&optional recursive grep)
408   (let* ((com (if recursive
409                   helm-grep-default-recurse-command
410                   helm-grep-default-command))
411          (exe (if grep
412                   (symbol-name grep)
413                   (and com (car (split-string com " "))))))
414     (if (and exe (string= exe "git")) "git-grep" exe)))
415
416 (cl-defun helm-grep-use-ack-p (&key where)
417   (let* ((rec-com (helm-grep-command t))
418          (norm-com (helm-grep-command))
419          (norm-com-ack-p (string-match "\\`ack" norm-com))
420          (rec-com-ack-p (and rec-com (string-match "\\`ack" rec-com))))
421     (cl-case where
422       (default   (and norm-com norm-com-ack-p))
423       (recursive (and rec-com rec-com-ack-p))
424       (strict    (and norm-com rec-com rec-com-ack-p norm-com-ack-p))
425       (t         (and (not (and norm-com (string= norm-com "git-grep")))
426                       (or (and norm-com norm-com-ack-p)
427                           (and rec-com rec-com-ack-p)))))))
428
429 (defun helm-grep--pipe-command-for-grep-command (smartcase pipe-switches &optional grep-cmd)
430   (pcase (or grep-cmd (helm-grep-command))
431     ;; Use grep for GNU regexp based tools.
432     ((or "grep" "zgrep" "git-grep")
433      (format "grep --color=always%s %s"
434              (if smartcase " -i" "")
435              pipe-switches))
436     ;; Use ack-grep for PCRE based tools.
437     ;; Sometimes ack-grep cmd is ack only.
438     ((and (pred (string-match-p "ack")) ack)
439      (format "%s --smart-case --color %s" ack pipe-switches))))
440
441 (defun helm-grep--prepare-cmd-line (only-files &optional include zgrep)
442   (let* ((default-directory (or helm-ff-default-directory
443                                 (helm-default-directory)
444                                 default-directory))
445          (fnargs            (helm-grep-prepare-candidates
446                              only-files default-directory))
447          (ignored-files     (unless (helm-grep-use-ack-p)
448                               (mapconcat
449                                (lambda (x)
450                                    (concat "--exclude="
451                                            (shell-quote-argument x)))
452                                helm-grep-ignored-files " ")))
453          (ignored-dirs      (unless (helm-grep-use-ack-p)
454                               (mapconcat
455                                ;; Need grep version >=2.5.4
456                                ;; of Gnuwin32 on windoze.
457                                (lambda (x)
458                                    (concat "--exclude-dir="
459                                            (shell-quote-argument x)))
460                                helm-grep-ignored-directories " ")))
461          (exclude           (unless (helm-grep-use-ack-p)
462                               (if helm-grep-in-recurse
463                                   (concat (or include ignored-files)
464                                           " " ignored-dirs)
465                                 ignored-files)))
466          (types             (and (helm-grep-use-ack-p)
467                                  ;; When %e format spec is not specified
468                                  ;; in `helm-grep-default-command'
469                                  ;; we need to pass an empty string
470                                  ;; to types to avoid error.
471                                  (or include "")))
472          (smartcase         (if (helm-grep-use-ack-p)
473                                 ""
474                                 (unless (let ((case-fold-search nil))
475                                           (string-match-p
476                                            "[[:upper:]]" helm-pattern))
477                                   "i")))
478          (helm-grep-default-command
479           (concat helm-grep-default-command " %m")) ; `%m' like multi.
480          (patterns (helm-mm-split-pattern helm-pattern t))
481          (pipe-switches (mapconcat 'identity helm-grep-pipe-cmd-switches " "))
482          (pipes
483           (helm-aif (cdr patterns)
484               (cl-loop with pipcom = (helm-grep--pipe-command-for-grep-command
485                                       smartcase pipe-switches)
486                        for p in it concat
487                        (format " | %s %s" pipcom (shell-quote-argument p)))
488             "")))
489     (format-spec
490      helm-grep-default-command
491      (delq nil
492            (list (unless zgrep
493                    (if types
494                        (cons ?e types)
495                      (cons ?e exclude)))
496                  (cons ?c (or smartcase ""))
497                  (cons ?p (shell-quote-argument (car patterns)))
498                  (cons ?f fnargs)
499                  (cons ?m pipes))))))
500
501 (defun helm-grep-init (cmd-line)
502   "Start an asynchronous grep process with CMD-LINE using ZGREP if non--nil."
503   (let* ((default-directory (or helm-ff-default-directory
504                                 (helm-default-directory)
505                                 default-directory))
506          (zgrep (string-match "\\`zgrep" cmd-line))
507          ;; Use pipe only with grep, zgrep or git-grep.
508          (process-connection-type (and (not zgrep) (helm-grep-use-ack-p)))
509          (tramp-verbose helm-tramp-verbose)
510          (start-time (float-time))
511          (proc-name (if helm-grep-use-zgrep
512                         "Zgrep"
513                         (capitalize
514                          (if helm-grep-in-recurse
515                              (helm-grep-command t)
516                              (helm-grep-command)))))
517          non-essential)
518     ;; Start grep process.
519     (helm-log "Starting Grep process in directory `%s'" default-directory)
520     (helm-log "Command line used was:\n\n%s"
521               (concat ">>> " (propertize cmd-line 'face 'helm-grep-cmd-line) "\n\n"))
522     (prog1            ; This function should return the process first.
523         (start-file-process-shell-command
524          proc-name helm-buffer cmd-line)
525       ;; Init sentinel.
526       (set-process-sentinel
527        (get-buffer-process helm-buffer)
528        (lambda (process event)
529            (let* ((err      (process-exit-status process))
530                   (noresult (= err 1)))
531              (unless (and err (> err 0))
532                (helm-process-deferred-sentinel-hook
533                 process event (helm-default-directory)))
534              (cond ((and noresult
535                          ;; This is a workaround for zgrep
536                          ;; that exit with code 1
537                          ;; after a certain amount of results.
538                          (with-helm-buffer (helm-empty-buffer-p)))
539                     (with-helm-buffer
540                       (insert (concat "* Exit with code 1, no result found,"
541                                       " command line was:\n\n "
542                                       (propertize helm-grep-last-cmd-line
543                                                   'face 'helm-grep-cmd-line)))
544                       (setq mode-line-format
545                             `(" " mode-line-buffer-identification " "
546                               (:eval (format "L%s" (helm-candidate-number-at-point))) " "
547                               (:eval (propertize
548                                       (format
549                                        "[%s process finished - (no results)] "
550                                        ,proc-name)
551                                       'face 'helm-grep-finish))))))
552                    ((or (string= event "finished\n")
553                         (and noresult
554                              ;; This is a workaround for zgrep
555                              ;; that exit with code 1
556                              ;; after a certain amount of results.
557                              (with-helm-buffer (not (helm-empty-buffer-p)))))
558                     (helm-log "%s process finished with %s results in %fs"
559                               proc-name
560                               (helm-get-candidate-number)
561                               (- (float-time) start-time))
562                     (helm-maybe-show-help-echo)
563                     (with-helm-window
564                       (setq mode-line-format
565                             `(" " mode-line-buffer-identification " "
566                               (:eval (format "L%s" (helm-candidate-number-at-point))) " "
567                               (:eval (propertize
568                                       (format
569                                        "[%s process finished in %.2fs - (%s results)] "
570                                        ,proc-name
571                                        ,(- (float-time) start-time)
572                                        (helm-get-candidate-number))
573                                       'face 'helm-grep-finish))))
574                       (force-mode-line-update)
575                       (when helm-allow-mouse
576                         (helm--bind-mouse-for-selection helm-selection-point))))
577                    ;; Catch error output in log.
578                    (t (helm-log
579                        "Error: %s %s"
580                        proc-name
581                        (replace-regexp-in-string "\n" "" event))))))))))
582
583 (defun helm-grep-collect-candidates ()
584   (let ((cmd-line (helm-grep--prepare-cmd-line
585                    helm-grep-last-targets
586                    helm-grep-include-files
587                    helm-grep-use-zgrep)))
588     (set (make-local-variable 'helm-grep-last-cmd-line) cmd-line)
589     (funcall helm-grep-default-function cmd-line)))
590
591
592 ;;; Actions
593 ;;
594 ;;
595 (defun helm-grep-action (candidate &optional where)
596   "Define a default action for `helm-do-grep-1' on CANDIDATE.
597 WHERE can be one of other-window, other-frame."
598   (let* ((split        (helm-grep-split-line candidate))
599          (split-pat    (helm-mm-split-pattern helm-input))
600          (lineno       (string-to-number (nth 1 split)))
601          (loc-fname        (or (with-current-buffer
602                                    (if (eq major-mode 'helm-grep-mode)
603                                        (current-buffer)
604                                        helm-buffer)
605                                  (get-text-property (point-at-bol) 'helm-grep-fname))
606                                (car split)))
607          (tramp-method (file-remote-p (or helm-ff-default-directory
608                                           default-directory) 'method))
609          (tramp-host   (file-remote-p (or helm-ff-default-directory
610                                           default-directory) 'host))
611          (tramp-prefix (concat "/" tramp-method ":" tramp-host ":"))
612          (fname        (if tramp-host
613                            (concat tramp-prefix loc-fname) loc-fname)))
614     (cl-case where
615       (other-window (helm-window-show-buffers
616                      (list (find-file-noselect fname)) t))
617       (other-frame  (find-file-other-frame fname))
618       (grep         (helm-grep-save-results-1))
619       (pdf          (if helm-pdfgrep-default-read-command
620                         (helm-pdfgrep-action-1 split lineno (car split))
621                       (find-file (car split)) (if (derived-mode-p 'pdf-view-mode)
622                                                   (pdf-view-goto-page lineno)
623                                                 (doc-view-goto-page lineno))))
624       (t            (find-file fname)))
625     (unless (or (eq where 'grep) (eq where 'pdf))
626       (helm-goto-line lineno))
627     ;; Move point to the nearest matching regexp from bol.
628     (cl-loop for reg in split-pat
629              when (save-excursion
630                     (condition-case _err
631                         (if helm-migemo-mode
632                             (helm-mm-migemo-forward reg (point-at-eol) t)
633                           (re-search-forward reg (point-at-eol) t))
634                       (invalid-regexp nil)))
635              collect (match-beginning 0) into pos-ls
636              finally (when pos-ls (goto-char (apply #'min pos-ls))))
637     ;; Save history
638     (unless (or helm-in-persistent-action
639                 (eq major-mode 'helm-grep-mode)
640                 (string= helm-pattern ""))
641       (setq helm-grep-history
642             (cons helm-pattern
643                   (delete helm-pattern helm-grep-history)))
644       (when (> (length helm-grep-history)
645                helm-grep-max-length-history)
646         (setq helm-grep-history
647               (delete (car (last helm-grep-history))
648                       helm-grep-history))))))
649
650 (defun helm-grep-persistent-action (candidate)
651   "Persistent action for `helm-do-grep-1'.
652 With a prefix arg record CANDIDATE in `mark-ring'."
653   (helm-grep-action candidate)
654   (helm-highlight-current-line))
655
656 (defun helm-grep-other-window (candidate)
657   "Jump to result in other window from helm grep."
658   (helm-grep-action candidate 'other-window))
659
660 (defun helm-grep-other-frame (candidate)
661   "Jump to result in other frame from helm grep."
662   (helm-grep-action candidate 'other-frame))
663
664 (defun helm-goto-next-or-prec-file (n)
665   "Go to next or precedent candidate file in helm grep/etags buffers.
666 If N is positive go forward otherwise go backward."
667   (let* ((allow-mode (or (eq major-mode 'helm-grep-mode)
668                          (eq major-mode 'helm-moccur-mode)))
669          (sel (if allow-mode
670                   (buffer-substring (point-at-bol) (point-at-eol))
671                 (helm-get-selection nil t)))
672          (current-line-list  (helm-grep-split-line sel))
673          (current-fname      (nth 0 current-line-list))
674          (bob-or-eof         (if (eq n 1) 'eobp 'bobp))
675          (mark-maybe (lambda ()
676                          (if allow-mode
677                              (ignore)
678                            (helm-mark-current-line)))))
679     (catch 'break
680       (while (not (funcall bob-or-eof))
681         (forward-line n) ; Go forward or backward depending of n value.
682         ;; Exit when current-fname is not matched or in `helm-grep-mode'
683         ;; the line is not a grep line i.e 'fname:num:tag'.
684         (setq sel (buffer-substring (point-at-bol) (point-at-eol)))
685         (when helm-allow-mouse
686           (helm--mouse-reset-selection-help-echo))
687         (unless (or (string= current-fname
688                              (car (helm-grep-split-line sel)))
689                     (and (eq major-mode 'helm-grep-mode)
690                          (not (get-text-property (point-at-bol) 'helm-grep-fname))))
691           (funcall mark-maybe)
692           (throw 'break nil))))
693     (cond ((and (> n 0) (eobp))
694            (re-search-backward ".")
695            (forward-line 0)
696            (funcall mark-maybe))
697           ((and (< n 0) (bobp))
698            (helm-aif (next-single-property-change (point-at-bol) 'helm-grep-fname)
699                (goto-char it)
700              (forward-line 1))
701            (funcall mark-maybe)))
702     (unless allow-mode
703       (helm-follow-execute-persistent-action-maybe)
704       (helm-log-run-hook 'helm-move-selection-after-hook))))
705
706 ;;;###autoload
707 (defun helm-goto-precedent-file ()
708   "Go to precedent file in helm grep/etags buffers."
709   (interactive)
710   (with-helm-alive-p
711     (with-helm-window
712       (helm-goto-next-or-prec-file -1))))
713 (put 'helm-goto-precedent-file 'helm-only t)
714
715 ;;;###autoload
716 (defun helm-goto-next-file ()
717   "Go to precedent file in helm grep/etags buffers."
718   (interactive)
719   (with-helm-window
720     (helm-goto-next-or-prec-file 1)))
721
722 (defun helm-grep-run-default-action ()
723   "Run grep default action from `helm-do-grep-1'."
724   (interactive)
725   (with-helm-alive-p
726     (helm-exit-and-execute-action 'helm-grep-action)))
727 (put 'helm-grep-run-default-action 'helm-only t)
728
729 (defun helm-grep-run-other-window-action ()
730   "Run grep goto other window action from `helm-do-grep-1'."
731   (interactive)
732   (with-helm-alive-p
733     (helm-exit-and-execute-action 'helm-grep-other-window)))
734 (put 'helm-grep-run-other-window-action 'helm-only t)
735
736 (defun helm-grep-run-other-frame-action ()
737   "Run grep goto other frame action from `helm-do-grep-1'."
738   (interactive)
739   (with-helm-alive-p
740     (helm-exit-and-execute-action 'helm-grep-other-frame)))
741 (put 'helm-grep-run-other-frame-action 'helm-only t)
742
743 (defun helm-grep-run-save-buffer ()
744   "Run grep save results action from `helm-do-grep-1'."
745   (interactive)
746   (with-helm-alive-p
747     (helm-exit-and-execute-action 'helm-grep-save-results)))
748 (put 'helm-grep-run-save-buffer 'helm-only t)
749
750
751 ;;; helm-grep-mode
752 ;;
753 ;;
754 (defun helm-grep-save-results (candidate)
755   (helm-grep-action candidate 'grep))
756
757 (defun helm-grep-save-results-1 ()
758   "Save helm grep result in a `helm-grep-mode' buffer."
759   (let ((buf "*hgrep*")
760         new-buf
761         (pattern (with-helm-buffer helm-input-local))
762         (src-name (assoc-default 'name (helm-get-current-source))))
763     (when (get-buffer buf)
764       (if helm-grep-save-buffer-name-no-confirm
765           (setq new-buf  (format "*hgrep|%s|-%s" pattern
766                                  (format-time-string "%H-%M-%S*")))
767           (setq new-buf (helm-read-string "GrepBufferName: " buf))
768           (cl-loop for b in (helm-buffer-list)
769                    when (and (string= new-buf b)
770                              (not (y-or-n-p
771                                    (format "Buffer `%s' already exists overwrite? "
772                                            new-buf))))
773                    do (setq new-buf (helm-read-string "GrepBufferName: " "*hgrep "))))
774       (setq buf new-buf))
775     (with-current-buffer (get-buffer-create buf)
776       (setq default-directory (or helm-ff-default-directory
777                                   (helm-default-directory)
778                                   default-directory))
779       (setq buffer-read-only t)
780       (let ((inhibit-read-only t)
781             (map (make-sparse-keymap)))
782         (erase-buffer)
783         (insert "-*- mode: helm-grep -*-\n\n"
784                 (format "%s Results for `%s':\n\n" src-name pattern))
785         (save-excursion
786           (insert (with-current-buffer helm-buffer
787                     (goto-char (point-min)) (forward-line 1)
788                     (buffer-substring (point) (point-max)))))
789         (save-excursion
790           (while (not (eobp))
791             (add-text-properties (point-at-bol) (point-at-eol)
792                                  `(keymap ,map
793                                           help-echo ,(concat
794                                                       (get-text-property
795                                                        (point) 'helm-grep-fname)
796                                                       "\nmouse-1: set point\nmouse-2: jump to selection")
797                                           mouse-face highlight))
798             (define-key map [mouse-1] 'mouse-set-point)
799             (define-key map [mouse-2] 'helm-grep-mode-mouse-jump)
800             (define-key map [mouse-3] 'ignore)
801             (forward-line 1))))
802       (helm-grep-mode))
803     (pop-to-buffer buf)
804     (message "Helm %s Results saved in `%s' buffer" src-name buf)))
805
806 (defun helm-grep-mode-mouse-jump (event)
807   (interactive "e")
808   (let* ((window (posn-window (event-end event)))
809          (pos    (posn-point (event-end event))))
810     (with-selected-window window
811       (when (eq major-mode 'helm-grep-mode)
812         (goto-char pos)
813         (helm-grep-mode-jump)))))
814 (put 'helm-grep-mode-mouse-jump 'helm-only t)
815
816 (define-derived-mode helm-grep-mode
817     special-mode "helm-grep"
818     "Major mode to provide actions in helm grep saved buffer.
819
820 Special commands:
821 \\{helm-grep-mode-map}"
822     (set (make-local-variable 'helm-grep-last-cmd-line)
823          (with-helm-buffer helm-grep-last-cmd-line))
824     (set (make-local-variable 'revert-buffer-function)
825          #'helm-grep-mode--revert-buffer-function))
826 (put 'helm-grep-mode 'helm-only t)
827
828 (defun helm-grep-mode--revert-buffer-function (&optional _ignore-auto _noconfirm)
829   (goto-char (point-min))
830   (when (re-search-forward helm-grep-split-line-regexp nil t) (forward-line 0))
831   (let ((inhibit-read-only t))
832     (delete-region (point) (point-max)))
833   (message "Reverting buffer...")
834   (let ((process-connection-type
835          ;; Git needs a nil value otherwise it tries to use a pager.
836          (null (string-match-p "\\`git" helm-grep-last-cmd-line))))
837     (set-process-sentinel
838      (start-file-process-shell-command
839       "hgrep"  (generate-new-buffer "*hgrep revert*") helm-grep-last-cmd-line)
840      'helm-grep-mode--sentinel)))
841
842 (defun helm-grep-mode--sentinel (process event)
843   (when (string= event "finished\n")
844     (with-current-buffer (current-buffer)
845       (let ((inhibit-read-only t))
846         (save-excursion
847           (cl-loop for l in (with-current-buffer (process-buffer process)
848                               (prog1 (split-string (buffer-string) "\n")
849                                 (kill-buffer)))
850                    for line = (if (string-match-p helm--ansi-color-regexp l)
851                                   (helm--ansi-color-apply l) l)
852                    when (string-match helm-grep-split-line-regexp line)
853                    do (insert (propertize
854                                (car (helm-grep-filter-one-by-one line))
855                                ;; needed for wgrep.
856                                'helm-realvalue line)
857                               "\n"))))
858       (message "Reverting buffer done"))))
859
860 (defun helm-gm-next-file ()
861   (interactive)
862   (helm-goto-next-or-prec-file 1))
863
864 (defun helm-gm-precedent-file ()
865   (interactive)
866   (helm-goto-next-or-prec-file -1))
867
868 (defun helm-grep-mode-jump ()
869   (interactive)
870   (helm-grep-action
871    (buffer-substring (point-at-bol) (point-at-eol)))
872   (helm-match-line-cleanup-pulse))
873
874 (defun helm-grep-mode-jump-other-window-1 (arg)
875   (let ((candidate (buffer-substring (point-at-bol) (point-at-eol))))
876     (condition-case nil
877         (progn
878           (save-selected-window
879             (helm-grep-action candidate 'other-window)
880             (helm-match-line-cleanup-pulse)
881             (recenter))
882           (forward-line arg))
883       (error nil))))
884
885 (defun helm-grep-mode-jump-other-window-forward ()
886   (interactive)
887   (helm-grep-mode-jump-other-window-1 1))
888
889 (defun helm-grep-mode-jump-other-window-backward ()
890   (interactive)
891   (helm-grep-mode-jump-other-window-1 -1))
892
893 (defun helm-grep-mode-jump-other-window ()
894   (interactive)
895   (let ((candidate (buffer-substring (point-at-bol) (point-at-eol))))
896     (condition-case nil
897         (progn (helm-grep-action candidate 'other-window)
898                (helm-match-line-cleanup-pulse))
899       (error nil))))
900
901
902 ;;; ack-grep types
903 ;;
904 ;;
905 (defun helm-grep-hack-types ()
906   "Return a list of known ack-grep types."
907   (with-temp-buffer
908     ;; "--help-types" works with both 1.96 and 2.1+, while
909     ;; "--help types" works only with 1.96 Issue #422.
910     ;; `helm-grep-command' should return the ack executable
911     ;; when this function is used in the right context
912     ;; i.e After checking is we are using ack-grep with
913     ;; `helm-grep-use-ack-p'.
914     (call-process (helm-grep-command t) nil t nil "--help-types")
915     (goto-char (point-min))
916     (cl-loop while (re-search-forward
917                     "^ *--\\(\\[no\\]\\)\\([^. ]+\\) *\\(.*\\)" nil t)
918              collect (cons (concat (match-string 2)
919                                    " [" (match-string 3) "]")
920                            (match-string 2))
921              collect (cons (concat "no" (match-string 2)
922                                    " [" (match-string 3) "]")
923                            (concat "no" (match-string 2))))))
924
925 (defun helm-grep-ack-types-transformer (candidates _source)
926   (cl-loop for i in candidates
927         if (stringp i)
928         collect (rassoc i helm-grep-ack-types-cache)
929         else
930         collect i))
931
932 (defvar helm-grep-ack-types-cache nil)
933 (defun helm-grep-read-ack-type ()
934   "Select types for the '--type' argument of ack-grep."
935   (require 'helm-mode)
936   (require 'helm-adaptive)
937   (setq helm-grep-ack-types-cache (helm-grep-hack-types))
938   (let ((types (helm-comp-read
939                 "Types: " helm-grep-ack-types-cache
940                 :name "*Ack-grep types*"
941                 :marked-candidates t
942                 :must-match t
943                 :fc-transformer '(helm-adaptive-sort
944                                   helm-grep-ack-types-transformer)
945                 :buffer "*helm ack-types*")))
946     (mapconcat (lambda (type) (concat "--type=" type)) types " ")))
947
948
949 ;;; grep extensions
950 ;;
951 ;;
952 (defun helm-grep-guess-extensions (files)
953   "Try to guess file extensions in FILES list when using grep recurse.
954 These extensions will be added to command line with --include arg of grep."
955   (cl-loop with ext-list = (list helm-grep-preferred-ext "*")
956         with lst = (if (file-directory-p (car files))
957                        (directory-files
958                         (car files) nil
959                         directory-files-no-dot-files-regexp)
960                      files)
961         for i in lst
962         for ext = (file-name-extension i 'dot)
963         for glob = (and ext (not (string= ext ""))
964                         (concat "*" ext))
965         unless (or (not glob)
966                    (and glob-list (member glob glob-list))
967                    (and glob-list (member glob ext-list))
968                    (and glob-list (member glob helm-grep-ignored-files)))
969         collect glob into glob-list
970         finally return (delq nil (append ext-list glob-list))))
971
972 (defun helm-grep-get-file-extensions (files)
973   "Try to return a list of file extensions to pass to '--include' arg of grep."
974   (require 'helm-adaptive)
975   (let* ((all-exts (helm-grep-guess-extensions
976                     (mapcar 'expand-file-name files)))
977          (extensions (helm-comp-read "Search Only in: " all-exts
978                                      :marked-candidates t
979                                      :fc-transformer 'helm-adaptive-sort
980                                      :buffer "*helm grep exts*"
981                                      :name "*helm grep extensions*")))
982     (when (listp extensions) ; Otherwise it is empty string returned by C-RET.
983       ;; If extensions is a list of one string containing spaces,
984       ;; assume user entered more than one glob separated by space(s) and
985       ;; split this string to pass it later to mapconcat.
986       ;; e.g '("*.el *.py")
987       (cl-loop for i in extensions
988                append (split-string-and-unquote i " ")))))
989
990
991 ;;; Set up source
992 ;;
993 ;;
994 (defvar helm-grep-before-init-hook nil
995   "Hook that runs before initialization of the helm buffer.")
996
997 (defvar helm-grep-after-init-hook nil
998   "Hook that runs after initialization of the helm buffer.")
999
1000 (defclass helm-grep-class (helm-source-async)
1001   ((candidates-process :initform 'helm-grep-collect-candidates)
1002    (filter-one-by-one :initform 'helm-grep-filter-one-by-one)
1003    (keymap :initform helm-grep-map)
1004    (pcre :initarg :pcre :initform nil
1005          :documentation
1006          "  Backend is using pcre regexp engine when non--nil.")
1007    (nohighlight :initform t)
1008    (nomark :initform t)
1009    (backend :initarg :backend
1010             :initform nil
1011             :documentation
1012             "  The grep backend that will be used.
1013   It is actually used only as an internal flag
1014   and don't set the backend by itself.
1015   You probably don't want to modify this.")
1016    (candidate-number-limit :initform 9999)
1017    (help-message :initform 'helm-grep-help-message)
1018    (history :initform 'helm-grep-history)
1019    (action :initform 'helm-grep-actions)
1020    (persistent-action :initform 'helm-grep-persistent-action)
1021    (persistent-help :initform "Jump to line (`C-u' Record in mark ring)")
1022    (requires-pattern :initform 2)
1023    (before-init-hook :initform 'helm-grep-before-init-hook)
1024    (after-init-hook :initform 'helm-grep-after-init-hook)
1025    (group :initform 'helm-grep)))
1026
1027 (defvar helm-source-grep nil)
1028
1029 (defmethod helm--setup-source ((source helm-grep-class))
1030   (call-next-method)
1031   (helm-aif (and helm-follow-mode-persistent
1032                  (if (eq (slot-value source 'backend) 'git)
1033                      helm-source-grep-git
1034                      helm-source-grep))
1035       (setf (slot-value source 'follow)
1036             (assoc-default 'follow it))))
1037
1038 (cl-defun helm-do-grep-1 (targets &optional recurse backend exts
1039                                   default-input input (source 'helm-source-grep))
1040   "Launch helm using backend BACKEND on a list of TARGETS files.
1041
1042 When RECURSE is given and BACKEND is 'grep' use -r option of
1043 BACKEND and prompt user for EXTS to set the --include args of BACKEND.
1044 Interactively you can give more than one arg separated by space at prompt.
1045 e.g
1046     $Pattern: *.el *.py *.tex
1047
1048 From lisp use the EXTS argument as a list of extensions as above.
1049 If you are using ack-grep, you will be prompted for --type
1050 instead and EXTS will be ignored.
1051 If prompt is empty `helm-grep-ignored-files' are added to --exclude.
1052
1053 Argument DEFAULT-INPUT is use as `default' arg of `helm' and INPUT
1054 is used as `input' arg of `helm', See `helm' docstring.
1055
1056 Arg BACKEND when non--nil specify which backend to use
1057 It is used actually to specify 'zgrep' or 'git'.
1058 When BACKEND 'zgrep' is used don't prompt for a choice
1059 in recurse, and ignore EXTS, search being made recursively on files matching
1060 `helm-zgrep-file-extension-regexp' only."
1061   (let* (non-essential
1062          (ack-rec-p (helm-grep-use-ack-p :where 'recursive))
1063          (exts (and recurse
1064                     ;; [FIXME] I could handle this from helm-walk-directory.
1065                     (not (eq backend 'zgrep)) ; zgrep doesn't handle -r opt.
1066                     (not ack-rec-p)
1067                     (or exts (helm-grep-get-file-extensions targets))))
1068          (include-files
1069           (and exts
1070                (mapconcat (lambda (x)
1071                             (concat "--include="
1072                                     (shell-quote-argument x)))
1073                           (if (> (length exts) 1)
1074                               (remove "*" exts)
1075                               exts) " ")))
1076          (types (and (not include-files)
1077                      (not (eq backend 'zgrep))
1078                      recurse
1079                      ack-rec-p
1080                      ;; When %e format spec is not specified
1081                      ;; ignore types and do not prompt for choice.
1082                      (string-match "%e" helm-grep-default-command)
1083                      (helm-grep-read-ack-type)))
1084          (src-name (capitalize (helm-grep-command recurse backend)))
1085          (com (cond ((eq backend 'zgrep) helm-default-zgrep-command)
1086                     ((eq backend 'git) helm-grep-git-grep-command)
1087                     (recurse helm-grep-default-recurse-command)
1088                     ;; When resuming, the local value of
1089                     ;; `helm-grep-default-command' is used, only git-grep
1090                     ;; should need this.
1091                     (t helm-grep-default-command))))
1092     ;; When called as action from an other source e.g *-find-files
1093     ;; we have to kill action buffer.
1094     (when (get-buffer helm-action-buffer)
1095       (kill-buffer helm-action-buffer))
1096     ;; If `helm-find-files' haven't already started,
1097     ;; give a default value to `helm-ff-default-directory'
1098     ;; and set locally `default-directory' to this value . See below [1].
1099     (unless helm-ff-default-directory
1100       (setq helm-ff-default-directory default-directory))
1101     ;; We need to store these vars locally
1102     ;; to pass infos later to `helm-resume'.
1103     (helm-set-local-variable
1104      'helm-zgrep-recurse-flag (and recurse (eq backend 'zgrep))
1105      'helm-grep-last-targets targets
1106      'helm-grep-include-files (or include-files types)
1107      'helm-grep-in-recurse recurse
1108      'helm-grep-use-zgrep (eq backend 'zgrep)
1109      'helm-grep-default-command com
1110      'default-directory helm-ff-default-directory) ;; [1]
1111     ;; Setup the source.
1112     (set source (helm-make-source src-name 'helm-grep-class
1113                   :backend backend
1114                   :pcre (string-match-p "\\`ack" com)))
1115     (helm
1116      :sources source
1117      :buffer (format "*helm %s*" (helm-grep-command recurse backend))
1118      :default default-input
1119      :input input
1120      :keymap helm-grep-map
1121      :history 'helm-grep-history
1122      :truncate-lines helm-grep-truncate-lines)))
1123
1124
1125 ;;; zgrep
1126 ;;
1127 ;;
1128 (defun helm-ff-zgrep-1 (flist recursive)
1129   (unwind-protect
1130        (let* ((def-dir (or helm-ff-default-directory
1131                            default-directory))
1132               (only    (if recursive
1133                            (or (gethash def-dir helm-rzgrep-cache)
1134                                (puthash
1135                                 def-dir
1136                                 (helm-walk-directory
1137                                  def-dir
1138                                  :directories nil
1139                                  :path 'full
1140                                  :match helm-zgrep-file-extension-regexp)
1141                                 helm-rzgrep-cache))
1142                          flist)))
1143          (helm-do-grep-1 only recursive 'zgrep))
1144     (setq helm-zgrep-recurse-flag nil)))
1145
1146
1147 ;;; transformers
1148 ;;
1149 ;;
1150 (defun helm-grep-split-line (line)
1151   "Split a grep output line."
1152   ;; The output of grep may send a truncated line in this chunk,
1153   ;; so don't split until grep line is valid, that is
1154   ;; once the second part of the line comes with next chunk
1155   ;; send by process.
1156   (when (string-match helm-grep-split-line-regexp line)
1157     ;; Don't use split-string because buffer/file name or string
1158     ;; may contain a ":".
1159     (cl-loop for n from 1 to 3 collect (match-string n line))))
1160
1161 (defun helm-grep--filter-candidate-1 (candidate &optional dir)
1162   (let* ((root   (or dir (and helm-grep-default-directory-fn
1163                               (funcall helm-grep-default-directory-fn))))
1164          (ansi-p (string-match-p helm--ansi-color-regexp candidate))
1165          (line   (if ansi-p (helm--ansi-color-apply candidate) candidate))
1166          (split  (helm-grep-split-line line))
1167          (fname  (if (and root split)
1168                      ;; Filename should always be provided as a local
1169                      ;; path, if the root directory is remote, the
1170                      ;; tramp prefix will be added before executing
1171                      ;; action, see `helm-grep-action' and issue #2032.
1172                      (expand-file-name (car split)
1173                                        (or (file-remote-p root 'localname)
1174                                            root))
1175                    (car-safe split)))
1176          (lineno (nth 1 split))
1177          (str    (nth 2 split))
1178          (display-fname (cl-ecase helm-grep-file-path-style
1179                           (basename (and fname (file-name-nondirectory fname)))
1180                           (absolute fname)
1181                           (relative (and fname root
1182                                          (file-relative-name fname root))))))
1183     (if (and display-fname lineno str)
1184         (cons (concat (propertize display-fname
1185                                   'face 'helm-grep-file
1186                                   'help-echo (abbreviate-file-name fname)
1187                                   'helm-grep-fname fname)
1188                       ":"
1189                       (propertize lineno 'face 'helm-grep-lineno)
1190                       ":"
1191                       (if ansi-p str (helm-grep-highlight-match str t)))
1192               line)
1193         "")))
1194
1195 (defun helm-grep-filter-one-by-one (candidate)
1196   "`filter-one-by-one' transformer function for `helm-do-grep-1'."
1197   (let ((helm-grep-default-directory-fn
1198          (or helm-grep-default-directory-fn
1199              (lambda () (or helm-ff-default-directory
1200                             (and (null (eq major-mode 'helm-grep-mode))
1201                                  (helm-default-directory))
1202                             default-directory)))))
1203     (if (consp candidate)
1204         ;; Already computed do nothing (default as input).
1205         candidate
1206         (and (stringp candidate)
1207              (helm-grep--filter-candidate-1 candidate)))))
1208
1209 (defun helm-grep-highlight-match (str &optional multi-match)
1210   "Highlight in string STR all occurences matching `helm-pattern'."
1211   (let (beg end)
1212     (condition-case-unless-debug nil
1213         (with-temp-buffer
1214           (insert (propertize str 'read-only nil)) ; Fix (#1176)
1215           (goto-char (point-min))
1216           (cl-loop for reg in
1217                    (if multi-match
1218                        ;; (m)occur.
1219                        (cl-loop for r in (helm-mm-split-pattern
1220                                           helm-pattern)
1221                                 unless (string-match "\\`!" r)
1222                                 collect
1223                                 (helm-aif (and helm-migemo-mode
1224                                                (assoc r helm-mm--previous-migemo-info))
1225                                     (cdr it) r))
1226                        ;; async sources (grep, gid etc...)
1227                        (list helm-input))
1228                    do
1229                    (while (and (re-search-forward reg nil t)
1230                                (> (- (setq end (match-end 0))
1231                                      (setq beg (match-beginning 0))) 0))
1232                      (helm-add-face-text-properties beg end 'helm-grep-match))
1233                    do (goto-char (point-min))) 
1234           (buffer-string))
1235       (error nil))))
1236
1237
1238 ;;; Grep from buffer list
1239 ;;
1240 ;;
1241 (defun helm-grep-buffers-1 (candidate &optional zgrep)
1242   "Run grep on all file--buffers or CANDIDATE if it is a file--buffer.
1243 If one of selected buffers is not a file--buffer,
1244 it is ignored and grep will run on all others file--buffers.
1245 If only one candidate is selected and it is not a file--buffer,
1246 switch to this buffer and run `helm-occur'.
1247 If a prefix arg is given run grep on all buffers ignoring non--file-buffers."
1248   (let* ((prefarg (or current-prefix-arg helm-current-prefix-arg))
1249          (helm-ff-default-directory
1250           (if (and helm-ff-default-directory
1251                    (file-remote-p helm-ff-default-directory))
1252               default-directory
1253             helm-ff-default-directory))
1254          (cands (if prefarg
1255                     (buffer-list)
1256                   (helm-marked-candidates)))
1257          (win-conf (current-window-configuration))
1258          ;; Non--fname and remote buffers are ignored.
1259          (bufs (cl-loop for buf in cands
1260                      for fname = (buffer-file-name (get-buffer buf))
1261                      when (and fname (not (file-remote-p fname)))
1262                      collect (expand-file-name fname))))
1263     (if bufs
1264         (if zgrep
1265             (helm-do-grep-1 bufs nil 'zgrep)
1266           (helm-do-grep-1 bufs))
1267       ;; bufs is empty, thats mean we have only CANDIDATE
1268       ;; and it is not a buffer-filename, fallback to occur.
1269       (switch-to-buffer candidate)
1270       (when (get-buffer helm-action-buffer)
1271         (kill-buffer helm-action-buffer))
1272       (helm-occur)
1273       (when (eq helm-exit-status 1)
1274         (set-window-configuration win-conf)))))
1275
1276 (defun helm-grep-buffers (candidate)
1277   "Action to grep buffers."
1278   (helm-grep-buffers-1 candidate))
1279
1280 (defun helm-zgrep-buffers (candidate)
1281   "Action to zgrep buffers."
1282   (helm-grep-buffers-1 candidate 'zgrep))
1283
1284
1285 ;;; Helm interface for pdfgrep
1286 ;;  pdfgrep program <http://pdfgrep.sourceforge.net/>
1287 ;;  and a pdf-reader (e.g xpdf) are needed.
1288 ;;
1289 (defvar helm-pdfgrep-default-function 'helm-pdfgrep-init)
1290 (defun helm-pdfgrep-init (only-files &optional recurse)
1291   "Start an asynchronous pdfgrep process in ONLY-FILES list."
1292   (let* ((default-directory (or helm-ff-default-directory
1293                                 default-directory))
1294          (fnargs   (helm-grep-prepare-candidates
1295                     (if (file-remote-p default-directory)
1296                         (mapcar (lambda (x)
1297                                     (file-remote-p x 'localname))
1298                                 only-files)
1299                       only-files)
1300                     default-directory))
1301          (cmd-line (format (if recurse
1302                                helm-pdfgrep-default-recurse-command
1303                                helm-pdfgrep-default-command)
1304                            helm-pattern
1305                            fnargs))
1306          process-connection-type)
1307     ;; Start pdf grep process.
1308     (helm-log "Starting Pdf Grep process in directory `%s'" default-directory)
1309     (helm-log "Command line used was:\n\n%s"
1310               (concat ">>> " (propertize cmd-line 'face 'helm-grep-cmd-line) "\n\n"))
1311     (prog1
1312         (start-file-process-shell-command
1313          "pdfgrep" helm-buffer cmd-line)
1314       (message nil)
1315       (set-process-sentinel
1316        (get-buffer-process helm-buffer)
1317        (lambda (_process event)
1318            (if (string= event "finished\n")
1319                (with-helm-window
1320                  (setq mode-line-format
1321                        '(" " mode-line-buffer-identification " "
1322                          (:eval (format "L%s" (helm-candidate-number-at-point))) " "
1323                          (:eval (propertize
1324                                  (format "[Pdfgrep Process Finish - %s result(s)] "
1325                                          (max (1- (count-lines
1326                                                    (point-min) (point-max))) 0))
1327                                  'face 'helm-grep-finish))))
1328                  (force-mode-line-update)
1329                  (when helm-allow-mouse
1330                    (helm--bind-mouse-for-selection helm-selection-point)))
1331              (helm-log "Error: Pdf grep %s"
1332                        (replace-regexp-in-string "\n" "" event))))))))
1333
1334 (defun helm-do-pdfgrep-1 (only &optional recurse)
1335   "Launch pdfgrep with a list of ONLY files."
1336   (unless (executable-find "pdfgrep")
1337     (error "Error: No such program `pdfgrep'."))
1338   (let (helm-grep-in-recurse) ; recursion is implemented differently in *pdfgrep.
1339     ;; When called as action from an other source e.g *-find-files
1340     ;; we have to kill action buffer.
1341     (when (get-buffer helm-action-buffer)
1342       (kill-buffer helm-action-buffer))
1343     (setq helm-pdfgrep-targets only)
1344     (helm
1345      :sources (helm-build-async-source "PdfGrep"
1346                 :init (lambda ()
1347                         ;; If `helm-find-files' haven't already started,
1348                         ;; give a default value to `helm-ff-default-directory'.
1349                         (setq helm-ff-default-directory (or helm-ff-default-directory
1350                                                             default-directory)))
1351                 :candidates-process (lambda ()
1352                                       (funcall helm-pdfgrep-default-function
1353                                                helm-pdfgrep-targets recurse))
1354                 :nohighlight t
1355                 :nomark t
1356                 :filter-one-by-one #'helm-grep-filter-one-by-one
1357                 :candidate-number-limit 9999
1358                 :history 'helm-grep-history
1359                 :keymap helm-pdfgrep-map
1360                 :help-message 'helm-pdfgrep-help-message
1361                 :action #'helm-pdfgrep-action
1362                 :persistent-help "Jump to PDF Page"
1363                 :requires-pattern 2)
1364      :buffer "*helm pdfgrep*"
1365      :history 'helm-grep-history)))
1366
1367 (defun helm-pdfgrep-action (candidate)
1368   (helm-grep-action candidate 'pdf))
1369
1370 (defun helm-pdfgrep-action-1 (_split pageno fname)
1371   (save-selected-window
1372     (start-file-process-shell-command
1373      "pdf-reader" nil
1374      (format-spec helm-pdfgrep-default-read-command
1375                   (list (cons ?f fname) (cons ?p pageno))))))
1376
1377 ;;; AG - PT - RG
1378 ;;
1379 ;;  https://github.com/ggreer/the_silver_searcher
1380 ;;  https://github.com/monochromegane/the_platinum_searcher
1381 ;;  https://github.com/BurntSushi/ripgrep
1382
1383 (defcustom helm-grep-ag-command
1384   "ag --line-numbers -S --hidden --color --nogroup %s %s %s"
1385   "The default command for AG, PT or RG.
1386
1387 Takes three format specs, the first for type(s), the second for pattern
1388 and the third for directory.
1389
1390 You can use safely \"--color\" (used by default) with AG RG and PT.
1391
1392 For ripgrep here is the command line to use:
1393
1394     rg --color=always --smart-case --no-heading --line-number %s %s %s
1395
1396 NOTE: Old versions of ripgrep was not supporting colors in emacs and a
1397 workaround had to be used (i.e prefixing command line with
1398 \"TERM=eterm-color\"), this is no more needed.
1399 See issue <https://github.com/BurntSushi/ripgrep/issues/182> for more infos.
1400
1401 You must use an output format that fit with helm grep, that is:
1402
1403     \"filename:line-number:string\"
1404
1405 The option \"--nogroup\" allow this.
1406 The option \"--line-numbers\" is also mandatory except with PT (not supported).
1407 For RG the options \"--no-heading\" and \"--line-number\" are the ones to use.
1408
1409 When modifying the default colors of matches with e.g \"--color-match\" option of AG
1410 you may want to modify as well `helm-grep-ag-pipe-cmd-switches' to have all matches
1411 colorized with same color in multi match."
1412   :group 'helm-grep
1413   :type 'string)
1414
1415 (defun helm-grep--ag-command ()
1416   (car (helm-remove-if-match
1417         "\\`[A-Z]*=" (split-string helm-grep-ag-command))))
1418
1419 (defun helm-grep-ag-get-types ()
1420   "Returns a list of AG types if available with AG version.
1421 See AG option \"--list-file-types\"
1422 Ripgrep (rg) types are also supported if this backend is used."
1423   (with-temp-buffer
1424     (let* ((com (helm-grep--ag-command))
1425            (ripgrep (string= com "rg"))
1426            (regex (if ripgrep "^\\(.*\\):" "^ *\\(--[a-z]*\\)"))
1427            (prefix (if ripgrep "-t" "")))
1428       (when (equal (call-process com
1429                                  nil t nil
1430                                  (if ripgrep
1431                                      "--type-list" "--list-file-types")) 0)
1432         (goto-char (point-min))
1433         (cl-loop while (re-search-forward regex nil t)
1434                  for type = (match-string 1)
1435                  collect (cons type (concat prefix type)))))))
1436
1437 (defun helm-grep-ag-prepare-cmd-line (pattern directory &optional type)
1438   "Prepare AG command line to search PATTERN in DIRECTORY.
1439 When TYPE is specified it is one of what returns `helm-grep-ag-get-types'
1440 if available with current AG version."
1441   (let* ((patterns (helm-mm-split-pattern pattern t))
1442          (pipe-switches (mapconcat 'identity helm-grep-ag-pipe-cmd-switches " "))
1443          (pipe-cmd (helm-acase (helm-grep--ag-command)
1444                      (("ag" "pt")
1445                       (format "%s -S --color%s" it (concat " " pipe-switches)))
1446                      ("rg" (format "rg -N -S --color=always%s"
1447                                     (concat " " pipe-switches)))))
1448          (cmd (format helm-grep-ag-command
1449                       (mapconcat 'identity type " ")
1450                       (shell-quote-argument (car patterns))
1451                       (shell-quote-argument directory))))
1452     (helm-aif (cdr patterns)
1453         (concat cmd (cl-loop for p in it concat
1454                              (format " | %s %s"
1455                                      pipe-cmd (shell-quote-argument p))))
1456       cmd)))
1457
1458 (defun helm-grep-ag-init (directory &optional type)
1459   "Start AG process in DIRECTORY maybe searching only files of type TYPE."
1460   (let ((default-directory (or helm-ff-default-directory
1461                                (helm-default-directory)
1462                                default-directory))
1463         (cmd-line (helm-grep-ag-prepare-cmd-line
1464                    helm-pattern (or (file-remote-p directory 'localname)
1465                                     directory)
1466                    type))
1467         (start-time (float-time))
1468         (proc-name (helm-grep--ag-command)))
1469     (set (make-local-variable 'helm-grep-last-cmd-line) cmd-line)
1470     (helm-log "Starting %s process in directory `%s'"
1471               proc-name directory)
1472     (helm-log "Command line used was:\n\n%s"
1473               (concat ">>> " cmd-line "\n\n"))
1474     (prog1
1475         (start-file-process-shell-command
1476          proc-name helm-buffer cmd-line)
1477       (set-process-sentinel
1478        (get-buffer-process helm-buffer)
1479        (lambda (process event)
1480          (let* ((err      (process-exit-status process))
1481                 (noresult (= err 1)))
1482            (cond (noresult
1483                   (with-helm-buffer
1484                     (insert (concat "* Exit with code 1, no result found,"
1485                                     " command line was:\n\n "
1486                                     (propertize helm-grep-last-cmd-line
1487                                                 'face 'helm-grep-cmd-line)))
1488                     (setq mode-line-format
1489                           `(" " mode-line-buffer-identification " "
1490                             (:eval (format "L%s" (helm-candidate-number-at-point))) " "
1491                             (:eval (propertize
1492                                     (format
1493                                      "[%s process finished - (no results)] "
1494                                      ,(upcase proc-name))
1495                                     'face 'helm-grep-finish))))))
1496                  ((string= event "finished\n")
1497                   (helm-log "%s process finished with %s results in %fs"
1498                               proc-name
1499                               (helm-get-candidate-number)
1500                               (- (float-time) start-time))
1501                   (helm-maybe-show-help-echo)
1502                   (with-helm-window
1503                     (setq mode-line-format
1504                           `(" " mode-line-buffer-identification " "
1505                             (:eval (format "L%s" (helm-candidate-number-at-point))) " "
1506                             (:eval (propertize
1507                                     (format
1508                                      "[%s process finished in %.2fs - (%s results)] "
1509                                      ,(upcase proc-name)
1510                                      ,(- (float-time) start-time)
1511                                      (helm-get-candidate-number))
1512                                     'face 'helm-grep-finish))))
1513                     (force-mode-line-update)
1514                     (when helm-allow-mouse
1515                       (helm--bind-mouse-for-selection helm-selection-point))))
1516                  (t (helm-log
1517                      "Error: %s %s"
1518                      proc-name
1519                      (replace-regexp-in-string "\n" "" event))))))))))
1520
1521 (defclass helm-grep-ag-class (helm-source-async)
1522   ((nohighlight :initform t)
1523    (pcre :initarg :pcre :initform t
1524          :documentation
1525          "  Backend is using pcre regexp engine when non--nil.")
1526    (keymap :initform helm-grep-map)
1527    (history :initform 'helm-grep-ag-history)
1528    (help-message :initform 'helm-grep-help-message)
1529    (filter-one-by-one :initform 'helm-grep-filter-one-by-one)
1530    (persistent-action :initform 'helm-grep-persistent-action)
1531    (persistent-help :initform "Jump to line (`C-u' Record in mark ring)")
1532    (candidate-number-limit :initform 99999)
1533    (requires-pattern :initform 2)
1534    (nomark :initform t)
1535    (action :initform 'helm-grep-actions)
1536    (group :initform 'helm-grep)))
1537
1538 (defvar helm-source-grep-ag nil)
1539
1540 (defmethod helm--setup-source ((source helm-grep-ag-class))
1541   (call-next-method)
1542   (helm-aif (and helm-follow-mode-persistent
1543                  helm-source-grep-ag
1544                  (assoc-default 'follow helm-source-grep-ag))
1545       (setf (slot-value source 'follow) it)))
1546
1547 (defun helm-grep-ag-1 (directory &optional type)
1548   "Start helm ag in DIRECTORY maybe searching in files of type TYPE."
1549   (setq helm-source-grep-ag
1550         (helm-make-source (upcase (helm-grep--ag-command)) 'helm-grep-ag-class
1551           :header-name (lambda (name)
1552                          (format "%s [%s]"
1553                                  name (abbreviate-file-name directory)))
1554           :candidates-process
1555           (lambda () (helm-grep-ag-init directory type))))
1556   (helm :sources 'helm-source-grep-ag
1557         :keymap helm-grep-map
1558         :history 'helm-grep-ag-history
1559         :truncate-lines helm-grep-truncate-lines
1560         :buffer (format "*helm %s*" (helm-grep--ag-command))))
1561
1562 (defun helm-grep-ag (directory with-types)
1563   "Start grep AG in DIRECTORY.
1564 When WITH-TYPES is non-nil provide completion on AG types."
1565   (require 'helm-adaptive)
1566   (helm-grep-ag-1 directory
1567                   (helm-aif (and with-types
1568                                  (helm-grep-ag-get-types))
1569                       (helm-comp-read
1570                        "Ag type: " it
1571                        :must-match t
1572                        :marked-candidates t
1573                        :fc-transformer 'helm-adaptive-sort
1574                        :buffer "*helm ag types*"))))
1575
1576 ;;; Git grep
1577 ;;
1578 ;;
1579 (defvar helm-source-grep-git nil)
1580
1581 (defcustom helm-grep-git-grep-command
1582   "git --no-pager grep -n%cH --color=always --full-name -e %p -- %f"
1583   "The git grep default command line.
1584 The option \"--color=always\" can be used safely.
1585 The color of matched items can be customized in your .gitconfig
1586 See `helm-grep-default-command' for more infos.
1587
1588 The \"--exclude-standard\" and \"--no-index\" switches allow
1589 skipping unwanted files specified in ~/.gitignore_global
1590 and searching files not already staged (not enabled by default).
1591
1592 You have also to enable this in global \".gitconfig\" with
1593     \"git config --global core.excludesfile ~/.gitignore_global\"."
1594   :group 'helm-grep
1595   :type 'string)
1596
1597 (defun helm-grep-git-1 (directory &optional all default input)
1598   "Run git-grep on DIRECTORY.
1599 If DIRECTORY is not inside or part of a git repo exit with error.
1600 If optional arg ALL is non-nil grep the whole repo otherwise start
1601 at DIRECTORY.
1602 Arg DEFAULT is what you will have with `next-history-element',
1603 arg INPUT is what you will have by default at prompt on startup."
1604   (require 'vc)
1605   (let* (helm-grep-default-recurse-command
1606          ;; Expand filename of each candidate with the git root dir.
1607          ;; The filename will be in the helm-grep-fname prop.
1608          (helm-grep-default-directory-fn (lambda ()
1609                                            (vc-find-root directory ".git")))
1610          (helm-ff-default-directory (funcall helm-grep-default-directory-fn)))
1611     (cl-assert helm-ff-default-directory nil "Not inside a Git repository")
1612     (helm-do-grep-1 (if all '("") `(,(expand-file-name directory)))
1613                     nil 'git nil default input 'helm-source-grep-git)))
1614
1615
1616 ;;;###autoload
1617 (defun helm-do-grep-ag (arg)
1618   "Preconfigured helm for grepping with AG in `default-directory'.
1619 With prefix-arg prompt for type if available with your AG version."
1620   (interactive "P")
1621   (require 'helm-files)
1622   (helm-grep-ag (expand-file-name default-directory) arg))
1623
1624 ;;;###autoload
1625 (defun helm-grep-do-git-grep (arg)
1626   "Preconfigured helm for git-grepping `default-directory'.
1627 With a prefix arg ARG git-grep the whole repository."
1628   (interactive "P")
1629   (require 'helm-files)
1630   (helm-grep-git-1 default-directory arg))
1631
1632
1633 (provide 'helm-grep)
1634
1635 ;; Local Variables:
1636 ;; byte-compile-warnings: (not obsolete)
1637 ;; coding: utf-8
1638 ;; indent-tabs-mode: nil
1639 ;; End:
1640
1641 ;;; helm-grep.el ends here