Emacs is easy if you read code

  |   Source

If you regard a package as a collection of APIs and read its code, Emacs is easy to master.

For example, here is a useful tip on using counsel-ag and wgrep to edit multiple files I recently learned.

To understand this black magic, you only need know counsel-ag-occur from counsel.el (v0.9.1):

(defun counsel-ag-occur ()
  "Generate a custom occur buffer for `counsel-ag'."
  (unless (eq major-mode 'ivy-occur-grep-mode)
    (ivy-occur-grep-mode))
  (setq default-directory counsel--git-grep-dir)
  (let* ((regex (counsel-unquote-regex-parens
                 (setq ivy--old-re
                       (ivy--regex
                        (progn (string-match "\"\\(.*\\)\"" (buffer-name))
                               (match-string 1 (buffer-name)))))))
         (cands (split-string
                 (shell-command-to-string
                  (format counsel-ag-base-command (shell-quote-argument regex)))
                 "\n"
                 t)))
    ;; Need precise number of header lines for `wgrep' to work.
    (insert (format "-*- mode:grep; default-directory: %S -*-\n\n\n"
                    default-directory))
    (insert (format "%d candidates:\n" (length cands)))
    (ivy--occur-insert-lines
     (mapcar
      (lambda (cand) (concat "./" cand))
      cands))))
(ivy-set-occur 'counsel-ag 'counsel-ag-occur)
(ivy-set-display-transformer 'counsel-ag 'counsel-git-grep-transformer)

Inside counsel-ag-occur:

  • The variable regex is the regular expression built from the filter string you input. Please note that regex is unquoted by counsel-unquote-regex-parens so it can be used in shell. If you use regex in Emacs Lisp, you don't need unquote it
  • The variable cands is the candidate lines created by running ag with regex as parameters in shell
  • Then a wgrep-friendly buffer is created

After spending 5 minutes to understand the internals, you can easily implement similar features.

Now let's develop our own black magic by enhancing the wgrep-friendly buffer.

My project uses Perforce as VCS. So I need check out files and make them writable before using wgrep.

Read code of wgrep.el (v2.1.10),

(defun wgrep-prepare-context ()
  (save-restriction
    (let ((start (wgrep-goto-first-found))
          (end (wgrep-goto-end-of-found)))
      (narrow-to-region start end)
      (goto-char (point-min))
      (funcall wgrep-results-parser))))

wgrep-results-parser is actually alias of wgrep-parse-command-results whose code is too much to paste here. You can M-x find-function wgrep-parse-command-results to read its code.

By combining wgrep-prepare-context and wgrep-parse-command-results I got my own access-files-in-wgrep-buffer:

(defun access-files-in-wgrep-buffer()
  (interactive)
  (save-restriction
    (let* ((start (wgrep-goto-first-found))
           (end (wgrep-goto-end-of-found))
           fn-accessed)
      (narrow-to-region start end)
      (goto-char (point-min))
      (unless (featurep 'wgrep) (require 'featurep))
      (while (not (eobp))
        (if (looking-at wgrep-line-file-regexp)
            (let* ((fn (match-string-no-properties 1)))
              (unless (string= fn fn-accessed)
                (setq fn-accessed fn)
                (message "File relative path=%s" fn))))
        (forward-line 1)))))

You can replace the line (message "File relative path=%s" fn) to (shell-command (format "any-shell-cli %s" fn)) to do anything on the files.

You can insert definition of access-files-in-wgrep-buffer into your .emacs and run M-x access-files-in-wgrep-buffer in wgrep buffer to have a test.

For example, I modified access-files-in-wgrep-buffer to p4edit-in-grep-buffer to checkout files under Perforce control,

(defun p4edit-in-wgrep-buffer()
  "'p4 edit' files in wgrep buffer.
Turn off `read-only-mode' of opened files."
  (interactive)
  (save-restriction
    (let* ((start (wgrep-goto-first-found))
           (end (wgrep-goto-end-of-found))
           fn-accessed)
      (narrow-to-region start end)
      (goto-char (point-min))
      (unless (featurep 'wgrep) (require 'featurep))
      (while (not (eobp))
        (if (looking-at wgrep-line-file-regexp)
            (let* ((filename (match-string-no-properties 1)) buf)
              (unless (string= filename fn-accessed)
                (setq fn-accessed filename)
                (shell-command (format "p4 edit %s" filename))
                (if (setq buf (get-file-buffer filename))
                    (with-current-buffer buf
                      ;; turn off read-only since we've already `p4 edit'
                      (read-only-mode -1))))))
        (forward-line 1)))))
Comments powered by Disqus