:type '(repeat (choice (const :tag "Default" nil)
(string :tag "Directory"))))
+(defcustom grep-use-headings nil
+ "If non-nil, subdivide grep output into sections, one per file."
+ :type 'boolean
+ :version "30.1")
+
+(defface grep-heading `((t :inherit ,grep-hit-face))
+ "Face of headings when `grep-use-headings' is non-nil."
+ :version "30.1")
+
+(defvar grep-heading-regexp
+ (rx bol
+ (or
+ (group-n 2
+ (group-n 1 (+ (not (any 0 ?\n))))
+ 0)
+ (group-n 2
+ (group-n 1 (+? nonl))
+ (any ?: ?- ?=)))
+ (+ digit)
+ (any ?: ?- ?=))
+ "Regexp used to create headings from grep output lines.
+It should be anchored at beginning of line. The first capture
+group, if present, should match the heading associated to the
+line. The buffer range of the second capture, if present, is
+made invisible (presumably because displaying it would be
+redundant).")
+
(defvar grep-find-abbreviate-properties
(let ((ellipsis (if (char-displayable-p ?…) "[…]" "[...]"))
(map (make-sparse-keymap)))
(while (re-search-forward "\033\\[[0-9;]*[mK]" end 1)
(replace-match "" t t))))))
+(defvar grep--heading-format
+ (eval-when-compile
+ (let ((title (propertize "%s"
+ 'font-lock-face 'grep-heading
+ 'outline-level 1)))
+ (propertize (concat title "\n") 'compilation-annotation t)))
+ "Format string of grep headings.
+This is passed to `format' with one argument, the text of the
+first capture group of `grep-heading-regexp'.")
+
+(defvar-local grep--heading-state nil
+ "Variable to keep track of the `grep--heading-filter' state.")
+
+(defun grep--heading-filter ()
+ "Filter function to add headings to output of a grep process."
+ (unless grep--heading-state
+ (setq grep--heading-state (cons (point-min-marker) nil)))
+ (save-excursion
+ (let ((limit (car grep--heading-state)))
+ ;; Move point to the old limit and update limit marker.
+ (move-marker limit (prog1 (pos-bol) (goto-char limit)))
+ (while (re-search-forward grep-heading-regexp limit t)
+ (unless (get-text-property (point) 'compilation-annotation)
+ (let ((heading (match-string-no-properties 1))
+ (start (match-beginning 2))
+ (end (match-end 2)))
+ (when start
+ (put-text-property start end 'invisible t))
+ (when (and heading (not (equal heading (cdr grep--heading-state))))
+ (save-excursion
+ (goto-char (pos-bol))
+ (insert-before-markers (format grep--heading-format heading)))
+ (setf (cdr grep--heading-state) heading))))))))
+
(defun grep-probe (command args &optional func result)
(let (process-file-side-effects)
(equal (condition-case nil
(add-function :filter-return (local 'kill-transform-function)
(lambda (string)
(string-replace "\0" ":" string)))
+ (when grep-use-headings
+ (add-hook 'compilation-filter-hook #'grep--heading-filter 80 t)
+ (setq-local outline-search-function #'outline-search-level
+ outline-level (lambda () (get-text-property
+ (point) 'outline-level))))
(add-hook 'compilation-filter-hook #'grep-filter nil t))
(defun grep--save-buffers ()
(cl-letf (((symbol-function 'w32-shell-dos-semantics) #'ignore))
(grep-tests--check-rgrep-abbreviation))))
+(ert-deftest grep-tests--grep-heading-regexp-without-null ()
+ (dolist (sep '(?: ?- ?=))
+ (let ((string (format "filename%c123%ctext" sep sep)))
+ (should (string-match grep-heading-regexp string))
+ (should (equal (match-string 1 string) "filename"))
+ (should (equal (match-string 2 string) (format "filename%c" sep))))))
+
+(ert-deftest grep-tests--grep-heading-regexp-with-null ()
+ (dolist (sep '(?: ?- ?=))
+ (let ((string (format "funny:0:filename%c123%ctext" 0 sep)))
+ (should (string-match grep-heading-regexp string))
+ (should (equal (match-string 1 string) "funny:0:filename"))
+ (should (equal (match-string 2 string) "funny:0:filename\0")))))
+
;;; grep-tests.el ends here