1 ;;; ack.el --- Emacs interface to ack
3 ;; Copyright (C) 2012 Leo Liu
5 ;; Author: Leo Liu <sdl.web@gmail.com>
7 ;; Keywords: tools, processes, convenience
9 ;; URL: https://github.com/leoliu/ack-el
11 ;; This program is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation, either version 3 of the License, or
14 ;; (at your option) any later version.
16 ;; This program is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
26 ;; ack is a tool like grep, designed for programmers with large trees
27 ;; of heterogeneous source code - http://betterthangrep.com/.
33 (when (>= emacs-major-version 24)
34 (autoload 'shell-completion-vars "shell"))
37 "Run `ack' and display the results."
41 ;; Used implicitly by `define-compilation-mode'
42 (defcustom ack-scroll-output nil
43 "Similar to `compilation-scroll-output' but for the *Ack* buffer."
47 (defcustom ack-command
48 ;; Note: on GNU/Linux ack may be renamed to ack-grep
49 (concat (file-name-nondirectory (or (executable-find "ack-grep")
50 (executable-find "ack")
52 "The default ack command for \\[ack].
54 Note also options to ack can be specified in ACK_OPTIONS
55 environment variable and ~/.ackrc, which you can disable by the
60 (defcustom ack-vc-grep-commands
61 '((".git" . "git --no-pager grep --color -n -i")
62 (".hg" . "hg grep -n -i")
63 ;; Plugin bzr-grep required for bzr < 2.6
64 (".bzr" . "bzr grep --color=always -n -i"))
65 "An alist of vc grep commands for `ack-skel-vc-grep'.
66 Each element is of the form (VC_DIR . CMD)."
67 :type '(repeat (cons string string))
70 (defcustom ack-default-directory-function 'ack-default-directory
71 "A function to return the default directory for `ack'.
72 It is called with one arg, the prefix arg to `ack'."
76 (defcustom ack-project-root-patterns
77 (list (concat "\\`" (regexp-quote dir-locals-file) "\\'")
79 "\\.xcodeproj\\'" ; xcode
80 "\\`\\.ropeproject\\'" ; python rope
81 "\\`\\.\\(?:CVS\\|bzr\\|git\\|hg\\|svn\\)\\'")
82 "A list of regexps to match files in a project root.
83 Used by `ack-guess-project-root'."
84 :type '(repeat string)
87 ;;; ======== END of USER OPTIONS ========
89 (defvar ack-history nil "History list for ack.")
91 (defvar ack-first-column 0
92 "Value to use for `compilation-first-column' in ack buffers.")
94 (defvar ack-error-screen-columns nil
95 "Value to use for `compilation-error-screen-columns' in ack buffers.")
97 (defvar ack-error "ack match"
98 "Stem of message to print when no matches are found.")
101 "Handle match highlighting escape sequences inserted by the ack process.
102 This function is called from `compilation-filter-hook'."
104 (let ((ansi-color-apply-face-function
105 (lambda (beg end face)
107 (ansi-color-apply-overlay-face beg end face)
108 (put-text-property beg end 'ack-color t)))))
109 (ansi-color-apply-on-region compilation-filter-start (point)))))
111 (defvar ack-mode-font-lock-keywords
113 ;; Command output lines.
114 (": \\(.+\\): \\(?:Permission denied\\|No such \\(?:file or directory\\|device or address\\)\\)$"
115 1 'compilation-error)
116 ;; Remove match from ack-error-regexp-alist before fontifying
117 ("^Ack \\(?:started\\|finished\\) at.*"
118 (0 '(face nil compilation-message nil message nil help-echo nil mouse-face nil) t))
119 ("^Ack \\(exited abnormally\\|interrupt\\|killed\\|terminated\\)\\(?:.*with code \\([0-9]+\\)\\)?.*"
120 (0 '(face nil compilation-message nil message nil help-echo nil mouse-face nil) t)
121 (1 'compilation-error)
122 (2 'compilation-error nil t)))
123 "Additional things to highlight in ack output.
124 This gets tacked on the end of the generated expressions.")
126 (when (< emacs-major-version 24)
127 (defvar ack--column-start 'ack--column-start)
128 (defvar ack--column-end 'ack--column-end))
130 (defun ack--column-start ()
131 (or (let* ((beg (match-end 0))
134 (line-end-position)))
135 (mbeg (text-property-any beg end 'ack-color t)))
136 (when mbeg (- mbeg beg)))
137 ;; Use column number from `ack' itself if available
138 (when (match-string 4)
139 (1- (string-to-number (match-string 4))))))
141 (defun ack--column-end ()
142 (let* ((beg (match-end 0))
145 (line-end-position)))
146 (mbeg (text-property-any beg end 'ack-color t))
147 (mend (and mbeg (next-single-property-change
148 mbeg 'ack-color nil end))))
149 (when mend (- mend beg))))
156 (looking-at-p "^--$")))
157 (setq file (or (get-text-property (line-beginning-position) 'ack-file)
159 (put-text-property (line-beginning-position)
161 'font-lock-face compilation-info-face)
162 (buffer-substring-no-properties
163 (line-beginning-position) (line-end-position))))))
164 (put-text-property (line-beginning-position)
165 (min (1+ (line-end-position)) (point-max)) 'ack-file file)
169 (when (< emacs-major-version 24)
170 (defun ack--line (file col)
171 (if (string-match-p "\\`[1-9][0-9]*\\'" (car file))
172 (let ((has-ansi-color (overlays-at (match-beginning 1))))
173 ;; See `compilation-mode-font-lock-keywords' where there is
174 ;; overriding font-locking of FILE. Thus use the display
175 ;; property here to avoid being overridden.
177 (match-beginning 1) (match-end 1)
179 (propertize (match-string-no-properties 1)
180 'face (list (and (not has-ansi-color)
181 compilation-line-face)
182 :weight 'normal :inherit 'underline)))
183 (list nil (ack--file)
184 (string-to-number (match-string 1))
185 (1- (string-to-number (match-string 3)))))
186 (put-text-property (match-beginning 3)
188 'font-lock-face compilation-line-face)
190 (string-to-number (match-string 3))
191 (when (match-string 4)
192 (put-text-property (match-beginning 4)
194 'font-lock-face compilation-column-face)
195 (1- (string-to-number (match-string 4))))))))
197 ;;; In emacs-24 and above, `compilation-mode-font-lock-keywords' ->
198 ;;; `compilation--ensure-parse' -> `compilation--parse-region' ->
199 ;;; `compilation-parse-errors' -> `compilation-error-properties'.
200 ;;; `compilation-error-properties' returns nil if a previous pattern
201 ;;; in the regexp alist has already been applied in a region.
203 ;;; In emacs-23, `ack-regexp-alist' is a part of `font-lock-keywords'
204 ;;; after some transformation, so later entries can override earlier
207 ;;; The output of 'ack --group --column WHATEVER' matches both regexps
208 ;;; in `ack-regexp-alist' and this fails emacs-23 in finding the right
209 ;;; file. So ack--line is used to disambiguate this case.
211 (defconst ack-error-regexp-alist
212 `(;; grouping line (--group or --heading)
213 ("^\\([1-9][0-9]*\\)\\(:\\|-\\)\\(?:\\(?4:[1-9][0-9]*\\)\\2\\)?"
214 ack--file 1 (ack--column-start . ack--column-end)
215 nil nil (4 compilation-column-face nil t))
216 ;; none grouping line (--nogroup or --noheading)
217 ("^\\(.+?\\)\\(:\\|-\\)\\([1-9][0-9]*\\)\\2\\(?:\\(?4:[1-9][0-9]*\\)\\2\\)?"
218 ,@(if (>= emacs-major-version 24)
219 '(1 3 (ack--column-start . ack--column-end)
220 nil nil (4 compilation-column-face nil t))
222 ("^Binary file \\(.+\\) matches$" 1 nil nil 0 1))
223 "Ack version of `compilation-error-regexp-alist' (which see).")
225 (defvar ack--ansi-color-last-marker)
227 (defvar ack-process-setup-function 'ack-process-setup)
229 (defun ack-process-setup ()
230 ;; Handle `hg grep' output
231 (when (string-match-p "^[ \t]*hg[ \t]" (car compilation-arguments))
232 (setq compilation-error-regexp-alist
233 '(("^\\(.+?:[0-9]+:\\)\\(?:\\([0-9]+\\):\\)?" 1 2)))
234 (when (< emacs-major-version 24)
235 (setq font-lock-keywords (compilation-mode-font-lock-keywords)))
236 (make-local-variable 'compilation-parse-errors-filename-function)
237 (setq compilation-parse-errors-filename-function
240 (if (string-match "\\(.+\\):\\([0-9]+\\):" file)
241 (match-string 1 file)
243 ;; Handle `bzr grep' output
244 (when (string-match-p "^[ \t]*bzr[ \t]" (car compilation-arguments))
245 (make-local-variable 'compilation-parse-errors-filename-function)
246 (setq compilation-parse-errors-filename-function
249 ;; 'bzr grep -r' has files like `termcolor.py~147'
250 (if (string-match "\\(.+\\)~\\([0-9]+\\)" file)
251 (match-string 1 file)
254 (define-compilation-mode ack-mode "Ack"
255 "A compilation mode tailored for ack."
256 (set (make-local-variable 'compilation-disable-input) t)
257 (set (make-local-variable 'compilation-error-face)
259 (if (>= emacs-major-version 24)
260 (add-hook 'compilation-filter-hook 'ack-filter nil t)
261 (set (make-local-variable 'ack--ansi-color-last-marker)
263 (font-lock-add-keywords
264 nil '(((lambda (limit)
265 (let ((beg (marker-position ack--ansi-color-last-marker)))
266 (move-marker ack--ansi-color-last-marker limit)
267 (ansi-color-apply-on-region beg ack--ansi-color-last-marker))
270 (defun ack-skel-file ()
271 "Insert a template for case-insensitive file name search."
273 (delete-minibuffer-contents)
274 (let ((ack (or (car (split-string ack-command nil t)) "ack")))
275 (skeleton-insert '(nil ack " -g '(?i:" _ ")'"))))
277 (defvar project-root) ; dynamically bound in `ack'
279 (defun ack-skel-vc-grep ()
280 "Insert a template for vc grep search."
282 (let* ((regexp (concat "\\`" (regexp-opt
283 (mapcar 'car ack-vc-grep-commands))
285 (root (or (ack-guess-project-root default-directory regexp)
286 (error "Cannot locate vc project root")))
287 (which (car (directory-files root nil regexp)))
288 (cmd (or (cdr (assoc which ack-vc-grep-commands))
289 (error "No command provided for `%s grep'"
290 (substring which 1)))))
291 (setq project-root root)
292 (delete-minibuffer-contents)
293 (skeleton-insert '(nil cmd " '" _ "'"))))
295 (defvar ack-minibuffer-local-map
296 (let ((map (make-sparse-keymap)))
297 (set-keymap-parent map minibuffer-local-map)
298 (define-key map "\t" (if (>= emacs-major-version 24)
301 (define-key map "\M-I" 'ack-skel-file)
302 (define-key map "\M-G" 'ack-skel-vc-grep)
303 (define-key map "'" 'skeleton-pair-insert-maybe)
305 "Keymap used for reading `ack' command and args in minibuffer.")
307 (defun ack-guess-project-root (start-directory &optional regexp)
308 (let ((regexp (or regexp
309 (mapconcat 'identity ack-project-root-patterns "\\|")))
310 (parent (file-name-directory
311 (directory-file-name (expand-file-name start-directory)))))
312 (if (directory-files start-directory nil regexp)
314 (unless (equal parent start-directory)
315 (ack-guess-project-root parent regexp)))))
317 (defun ack-default-directory (arg)
318 "A function for `ack-default-directory-function'.
319 With no \\[universal-argument], return `default-directory';
320 With one \\[universal-argument], find the project root according to
321 `ack-project-root-patterns';
322 Otherwise, interactively choose a directory."
324 ((not arg) default-directory)
325 ((= (prefix-numeric-value arg) 4)
326 (or (ack-guess-project-root default-directory)
327 (ack-default-directory '(16))))
328 (t (read-directory-name "In directory: " nil nil t))))
331 (defun ack (command-args &optional directory)
332 "Run ack using COMMAND-ARGS and collect output in a buffer.
333 When called interactively, the value of DIRECTORY is provided by
334 `ack-default-directory-function'.
336 The following keys are available while reading from the
339 \\{ack-minibuffer-local-map}"
341 (let ((project-root (funcall ack-default-directory-function
343 ;; Disable completion cycling; see http://debbugs.gnu.org/12221
344 (completion-cycle-threshold nil))
345 (list (minibuffer-with-setup-hook (if (>= emacs-major-version 24)
346 'shell-completion-vars
347 'pcomplete-shell-setup)
348 (read-from-minibuffer "Run ack (like this): "
349 ack-command ack-minibuffer-local-map
352 (let ((default-directory (expand-file-name
353 (or directory default-directory))))
354 (compilation-start command-args 'ack-mode)))