1 ;;; ack.el --- Emacs interface to ack
3 ;; Copyright (C) 2012, 2013 Free Software Foundation, Inc.
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-buffer-name-function nil
61 "If non-nil, a function to compute the name of an ack buffer.
62 See `compilation-buffer-name-function' for details."
63 :type '(choice function (const nil))
66 (defcustom ack-vc-grep-commands
67 '((".git" . "git --no-pager grep --color -n -i")
68 (".hg" . "hg grep -n -i")
69 ;; Plugin bzr-grep required for bzr < 2.6
70 (".bzr" . "bzr grep --color=always -n -i"))
71 "An alist of vc grep commands for `ack-skel-vc-grep'.
72 Each element is of the form (VC_DIR . CMD)."
73 :type '(repeat (cons string string))
76 (defcustom ack-default-directory-function 'ack-default-directory
77 "A function to return the default directory for `ack'.
78 It is called with one arg, the prefix arg to `ack'."
82 (defcustom ack-project-root-patterns
83 (list (concat "\\`" (regexp-quote dir-locals-file) "\\'")
85 "\\.xcodeproj\\'" ; xcode
86 "\\`\\.ropeproject\\'" ; python rope
87 "\\`\\.\\(?:CVS\\|bzr\\|git\\|hg\\|svn\\)\\'")
88 "A list of regexps to match files in a project root.
89 Used by `ack-guess-project-root'."
90 :type '(repeat string)
93 ;;; ======== END of USER OPTIONS ========
95 (defvar ack-history nil "History list for ack.")
97 (defvar ack-first-column 0
98 "Value to use for `compilation-first-column' in ack buffers.")
100 (defvar ack-error-screen-columns nil
101 "Value to use for `compilation-error-screen-columns' in ack buffers.")
103 (defvar ack-error "ack match"
104 "Stem of message to print when no matches are found.")
107 "Handle match highlighting escape sequences inserted by the ack process.
108 This function is called from `compilation-filter-hook'."
110 (let ((ansi-color-apply-face-function
111 (lambda (beg end face)
113 (ansi-color-apply-overlay-face beg end face)
114 (put-text-property beg end 'ack-color t)))))
115 (ansi-color-apply-on-region compilation-filter-start (point)))))
117 (defvar ack-mode-font-lock-keywords
119 ;; Command output lines.
120 (": \\(.+\\): \\(?:Permission denied\\|No such \\(?:file or directory\\|device or address\\)\\)$"
121 1 'compilation-error)
122 ;; Remove match from ack-error-regexp-alist before fontifying
123 ("^Ack \\(?:started\\|finished\\) at.*"
124 (0 '(face nil compilation-message nil message nil help-echo nil mouse-face nil) t))
125 ("^Ack \\(exited abnormally\\|interrupt\\|killed\\|terminated\\)\\(?:.*with code \\([0-9]+\\)\\)?.*"
126 (0 '(face nil compilation-message nil message nil help-echo nil mouse-face nil) t)
127 (1 'compilation-error)
128 (2 'compilation-error nil t)))
129 "Additional things to highlight in ack output.
130 This gets tacked on the end of the generated expressions.")
132 (when (< emacs-major-version 24)
133 (defvar ack--column-start 'ack--column-start)
134 (defvar ack--column-end 'ack--column-end))
136 (defun ack--column-start ()
137 (or (let* ((beg (match-end 0))
140 (line-end-position)))
141 (mbeg (text-property-any beg end 'ack-color t)))
142 (when mbeg (- mbeg beg)))
143 ;; Use column number from `ack' itself if available
144 (when (match-string 4)
145 (1- (string-to-number (match-string 4))))))
147 (defun ack--column-end ()
148 (let* ((beg (match-end 0))
151 (line-end-position)))
152 (mbeg (text-property-any beg end 'ack-color t))
153 (mend (and mbeg (next-single-property-change
154 mbeg 'ack-color nil end))))
155 (when mend (- mend beg))))
162 (looking-at-p "^--$")))
163 (setq file (or (get-text-property (line-beginning-position) 'ack-file)
165 (put-text-property (line-beginning-position)
167 'font-lock-face compilation-info-face)
168 (buffer-substring-no-properties
169 (line-beginning-position) (line-end-position))))))
170 (put-text-property (line-beginning-position)
171 (min (1+ (line-end-position)) (point-max)) 'ack-file file)
175 (when (< emacs-major-version 24)
176 (defun ack--line (file col)
177 (if (string-match-p "\\`[1-9][0-9]*\\'" (car file))
178 (let ((has-ansi-color (overlays-at (match-beginning 1))))
179 ;; See `compilation-mode-font-lock-keywords' where there is
180 ;; overriding font-locking of FILE. Thus use the display
181 ;; property here to avoid being overridden.
183 (match-beginning 1) (match-end 1)
185 (propertize (match-string-no-properties 1)
186 'face (list (and (not has-ansi-color)
187 compilation-line-face)
188 :weight 'normal :inherit 'underline)))
189 (list nil (ack--file)
190 (string-to-number (match-string 1))
191 (1- (string-to-number (match-string 3)))))
192 (put-text-property (match-beginning 3)
194 'font-lock-face compilation-line-face)
196 (string-to-number (match-string 3))
197 (when (match-string 4)
198 (put-text-property (match-beginning 4)
200 'font-lock-face compilation-column-face)
201 (1- (string-to-number (match-string 4))))))))
203 ;;; In emacs-24 and above, `compilation-mode-font-lock-keywords' ->
204 ;;; `compilation--ensure-parse' -> `compilation--parse-region' ->
205 ;;; `compilation-parse-errors' -> `compilation-error-properties'.
206 ;;; `compilation-error-properties' returns nil if a previous pattern
207 ;;; in the regexp alist has already been applied in a region.
209 ;;; In emacs-23, `ack-regexp-alist' is a part of `font-lock-keywords'
210 ;;; after some transformation, so later entries can override earlier
213 ;;; The output of 'ack --group --column WHATEVER' matches both regexps
214 ;;; in `ack-regexp-alist' and this fails emacs-23 in finding the right
215 ;;; file. So ack--line is used to disambiguate this case.
217 (defconst ack-error-regexp-alist
218 `(;; grouping line (--group or --heading)
219 ("^\\([1-9][0-9]*\\)\\(:\\|-\\)\\(?:\\(?4:[1-9][0-9]*\\)\\2\\)?"
220 ack--file 1 (ack--column-start . ack--column-end)
221 nil nil (4 compilation-column-face nil t))
222 ;; none grouping line (--nogroup or --noheading)
223 ("^\\(.+?\\)\\(:\\|-\\)\\([1-9][0-9]*\\)\\2\\(?:\\(?4:[1-9][0-9]*\\)\\2\\)?"
224 ,@(if (>= emacs-major-version 24)
225 '(1 3 (ack--column-start . ack--column-end)
226 nil nil (4 compilation-column-face nil t))
228 ("^Binary file \\(.+\\) matches$" 1 nil nil 0 1))
229 "Ack version of `compilation-error-regexp-alist' (which see).")
231 (defvar ack--ansi-color-last-marker)
233 (defvar ack-process-setup-function 'ack-process-setup)
235 (defun ack-process-setup ()
236 ;; Handle `hg grep' output
237 (when (string-match-p "^[ \t]*hg[ \t]" (car compilation-arguments))
238 (setq compilation-error-regexp-alist
239 '(("^\\(.+?:[0-9]+:\\)\\(?:\\([0-9]+\\):\\)?" 1 2)))
240 (when (< emacs-major-version 24)
241 (setq font-lock-keywords (compilation-mode-font-lock-keywords)))
242 (make-local-variable 'compilation-parse-errors-filename-function)
243 (setq compilation-parse-errors-filename-function
246 (if (string-match "\\(.+\\):\\([0-9]+\\):" file)
247 (match-string 1 file)
249 ;; Handle `bzr grep' output
250 (when (string-match-p "^[ \t]*bzr[ \t]" (car compilation-arguments))
251 (make-local-variable 'compilation-parse-errors-filename-function)
252 (setq compilation-parse-errors-filename-function
255 ;; 'bzr grep -r' has files like `termcolor.py~147'
256 (if (string-match "\\(.+\\)~\\([0-9]+\\)" file)
257 (match-string 1 file)
260 (define-compilation-mode ack-mode "Ack"
261 "A compilation mode tailored for ack."
262 (set (make-local-variable 'compilation-disable-input) t)
263 (set (make-local-variable 'compilation-error-face)
265 (if (>= emacs-major-version 24)
266 (add-hook 'compilation-filter-hook 'ack-filter nil t)
267 (set (make-local-variable 'ack--ansi-color-last-marker)
269 (font-lock-add-keywords
270 nil '(((lambda (limit)
271 (let ((beg (marker-position ack--ansi-color-last-marker)))
272 (move-marker ack--ansi-color-last-marker limit)
273 (ansi-color-apply-on-region beg ack--ansi-color-last-marker))
276 (defun ack-update-minibuffer-prompt (prompt)
277 "Visually replace minibuffer prompt with PROMPT."
279 (let ((inhibit-read-only t))
281 (point-min) (minibuffer-prompt-end) 'display prompt))))
283 (defun ack-skel-file ()
284 "Insert a template for case-insensitive file name search."
286 (delete-minibuffer-contents)
287 (let ((ack (or (car (split-string ack-command nil t)) "ack")))
288 (skeleton-insert '(nil ack " -g '(?i:" _ ")'"))))
290 (defvar project-root) ; dynamically bound in `ack'
292 (defun ack-skel-vc-grep ()
293 "Insert a template for vc grep search."
295 (let* ((regexp (concat "\\`" (regexp-opt
296 (mapcar 'car ack-vc-grep-commands))
298 (root (or (ack-guess-project-root default-directory regexp)
299 (error "Cannot locate vc project root")))
300 (which (car (directory-files root nil regexp)))
301 (backend (downcase (substring which 1)))
302 (cmd (or (cdr (assoc which ack-vc-grep-commands))
303 (error "No command provided for `%s grep'" backend))))
304 (setq project-root root)
305 (ack-update-minibuffer-prompt
306 (format "Run %s grep in `%s': " backend
307 (file-name-nondirectory (directory-file-name project-root))))
308 (delete-minibuffer-contents)
309 (skeleton-insert '(nil cmd " '" _ "'"))))
311 (defun ack-yank-symbol-at-point ()
312 "Yank the symbol from the window before entering the minibuffer."
314 (let ((symbol (and (minibuffer-selected-window)
316 (window-buffer (minibuffer-selected-window))
317 (thing-at-point 'symbol)))))
318 (if symbol (insert symbol)
319 (minibuffer-message "No symbol found"))))
321 (defvar ack-minibuffer-local-map
322 (let ((map (make-sparse-keymap)))
323 (set-keymap-parent map minibuffer-local-map)
324 (define-key map "\t" (if (>= emacs-major-version 24)
327 (define-key map "\M-I" 'ack-skel-file)
328 (define-key map "\M-G" 'ack-skel-vc-grep)
329 (define-key map "\M-Y" 'ack-yank-symbol-at-point)
330 (define-key map "'" 'skeleton-pair-insert-maybe)
332 "Keymap used for reading `ack' command and args in minibuffer.")
334 (defun ack-guess-project-root (start-directory &optional regexp)
335 (let ((regexp (or regexp
336 (mapconcat 'identity ack-project-root-patterns "\\|")))
337 (parent (file-name-directory
338 (directory-file-name (expand-file-name start-directory)))))
339 (if (directory-files start-directory nil regexp)
341 (unless (equal parent start-directory)
342 (ack-guess-project-root parent regexp)))))
344 (defun ack-default-directory (arg)
345 "A function for `ack-default-directory-function'.
346 With no \\[universal-argument], return `default-directory';
347 With one \\[universal-argument], find the project root according to
348 `ack-project-root-patterns';
349 Otherwise, interactively choose a directory."
351 ((not arg) default-directory)
352 ((= (prefix-numeric-value arg) 4)
353 (or (ack-guess-project-root default-directory)
354 (ack-default-directory '(16))))
355 (t (read-directory-name "In directory: " nil nil t))))
358 (defun ack (command-args &optional directory)
359 "Run ack using COMMAND-ARGS and collect output in a buffer.
360 When called interactively, the value of DIRECTORY is provided by
361 `ack-default-directory-function'.
363 The following keys are available while reading from the
366 \\{ack-minibuffer-local-map}"
368 (let ((project-root (or (funcall ack-default-directory-function
371 ;; Disable completion cycling; see http://debbugs.gnu.org/12221
372 (completion-cycle-threshold nil))
373 (list (minibuffer-with-setup-hook (if (>= emacs-major-version 24)
374 'shell-completion-vars
375 'pcomplete-shell-setup)
376 (read-from-minibuffer
377 (format "Run ack in `%s': "
378 (file-name-nondirectory
379 (directory-file-name project-root)))
380 ack-command ack-minibuffer-local-map nil 'ack-history))
382 (let ((default-directory (expand-file-name
383 (or directory default-directory))))
384 ;; Change to the compilation buffer so that `ack-buffer-name-function' can
385 ;; make use of `compilation-arguments'.
386 (with-current-buffer (compilation-start command-args 'ack-mode)
387 (when ack-buffer-name-function
388 (rename-buffer (funcall ack-buffer-name-function "ack"))))))