How to spell check function/variable in Emacs

to spell check function/variable in Emacs :en:emacs:flyspell:

CREATED: <2018-06-17 Sun>

UPDATED: <2018-07-06 Fri>

This article explains how developers can check typos of function/variable while programming in Emacs.

It uses options --run-together from GNU Aspell to check camel cased word.

But this solution is not perfect. It wrongly identifies two character interior word as typo. For example, "onChange" is regards as typo because the interior word "on". Another issue is namespace of function name. For example, "MS" from "MSToggleButton" is alias of "Microsoft". If "MS" is identified as typo, every word containing namespace "MS" is regarded as typo.

In this article,

  • I will explain how Emacs spell checker works
  • Then we study the algorithm of aspell (We can learn nothing from hunspell because hunspell can NOT check camel case word at all)
  • Finally, I will show you a complete solution which works with either aspell or hunspell

In Emacs, a built in plugin Fly Spell is in charge of spell check. It passes the options and plain text to command line tool aspell. Aspell sends back the typos of text into Fly Spell. Fly Spell then select certain typos to display. For example, when flyspell-prog-mode is on, only typos in comments and strings are visible.

So aspell doesn't understand syntax of any programming language. It scans plain text and report all typos to Fly Spell.

In aspell, there are two extra "run-together" word options:

  • --run-together-limit is "Maximum number of words can be strung together"
  • --run-together-min is "Minimal length of interior words"

Let's study the code of aspell to understand these two options. The "run-together" algorithm in implemented in function Working::check_word of file "modules/speller/default/suggest.cpp".

In order to help you understand this function, I documented the code line by line,

class Working : public Score {
  unsigned check_word(char * word, char * word_end, CheckInfo * ci, unsigned pos = 1);
};
unsigned Working::check_word(char * word, char * word_end,  CheckInfo * ci,
                             /* it WILL modify word */
                             unsigned pos)
{
  // check the whole word before go into run-together mode
  unsigned res = check_word_s(word, ci);
  // if `res` is true, it's a valid word, don't bother run-together
  if (res) return pos + 1;
  // it's typo because number of interior words is greater than "--run-together-limit"
  if (pos + 1 >= sp->run_together_limit_) return 0;

  // `i` is the `end` of interior word, the poition AFTER last character of interior word
  for (char * i = word + sp->run_together_min_; 
       // already checked the whole word; besides, any interior word whose size is less 
       // than "--run-together-min" is regarded as invalid
       i <= word_end - sp->run_together_min_;
       ++i)
  {
    char t = *i;

    // read the interior word by set the character at `end` position to '\0'
    *i = '\0';
    res = check_word_s(word, ci);
    // restore original character at `end` position
    *i = t;

    // Current interior word is invalid, we need append the character at current
    //  `end` position to creata new interior word.
    //  Inncrement `i` because `i` always points to the `end` of interior word
    if (!res) continue;

    // Current interior word is valid, strip it from the whole word to create a totally
    // new word for `check_word`, `check_word` is a recursive function
    res = check_word(i, word_end, ci + 1, pos + 1);
    if (res) return res;
  }
  memset(ci, 0, sizeof(CheckInfo));
  return 0;
}

Let's use "hisHelle" as demo how check_word runs:

  • "word" points to string "hisHelle" (in C/C++, string is character array. The last character of array is character '\0')
  • "sp->run_together_min_" is 3, so "i" initially points to the character "H", at the end of interior word "his"
  • "check_word_s" return "true" for interior word "his"
  • So we strip "his" from "hisHelle" and recursively call "check_word" to check new word "Helle"
  • In the new context of "check_word", we extract "Hel" from "Helle" initially
  • "Hel" is invalid. So we extract "Hell" from "Helle" and get new word "e" and recursively apply "check_word" on "e"
  • "e" is not valid and at the end of recursion. So "hisHelle" is a typo

Here is our conclusion after studying the code:

  • --run-together-limit could not be bigger if your computer got enough memory. It's default value is 8. I prefer 16.
  • --run-together-min can't be 2 because too many typos are combination of "correct" two character interior words ("hehe", "isme", …)
  • --run-together-min can't be greater than 3, or else, too many "correct" three character interior words are regarded as invalid ("his", "her", "one", "two")
  • --run-together-min should always be 3 which is its default value. Actually, it should never be tweak-able by user at the beginning

Since --run-together-min is 3. the word "onChange" is always regarded as typo because of two character interior word "on". Since there is nothing we can do at aspell side, we have to turn to Emacs to fix this problem.

When Emacs got potential typo on Emacs side, we can strip out all the two character interior word from original word and spell check new word again.

Please note hunspell can't check camel case word at all while aspell can check camel case word but with a little noise. So there is nothing we can study in hunspell.

We will use Emacs Lisp to solve this problem completely, using either aspell or hunspell.

We can attach a predicate into specific major-mode. The predicate return t if current word at cursor is typo,

(defun js-flyspell-verify ()
  (let* ((font-face (get-text-property (- (point) 1) 'face))
         (word (thing-at-point 'word)))
    (message "font-face=%s word=%s" font-face word)
    t))
(put 'js2-mode 'flyspell-mode-predicate 'js-flyspell-verify)

As you can see from above code, we have full control on what typos should be displayed in js-flyspell-verify. So predicate is actually the last chance to fix wrongly identified typos.

Here is complete setup you can paste into .emacs (I setup for js2-mode and rjsx-mode but code is generic enough).

Please note function split-camel-case split a camel case word into a list of sub-words. I just assume any sub-word whose length is less than three is not a typo.

(defun split-camel-case (word)
  "Split camel case WORD into a list of strings.
Ported from 'https://github.com/fatih/camelcase/blob/master/camelcase.go'."
  (let* ((case-fold-search nil)
         (len (length word))
         ;; ten sub-words is enough
         (runes [nil nil nil nil nil nil nil nil nil nil])
         (runes-length 0)
         (i 0)
         ch
         (last-class 0)
         (class 0)
         rlt)

    ;; split into fields based on class of character
    (while (< i len)
      (setq ch (elt word i))
      (cond
       ;; lower case
       ((and (>= ch ?a) (<= ch ?z))
        (setq class 1))
       ;; upper case
       ((and (>= ch ?A) (<= ch ?Z))
        (setq class 2))
       ((and (>= ch ?0) (<= ch ?9))
        (setq class 3))
       (t
        (setq class 4)))

      (cond
       ((= class last-class)
        (aset runes
              (1- runes-length)
              (concat (aref runes (1- runes-length)) (char-to-string ch))))
       (t
        (aset runes runes-length (char-to-string ch))
        (setq runes-length (1+ runes-length))))
      (setq last-class class)
      ;; end of while
      (setq i (1+ i)))

    ;; handle upper case -> lower case sequences, e.g.
    ;;     "PDFL", "oader" -> "PDF", "Loader"
    (setq i 0)
    (while (< i (1- runes-length))
      (let* ((ch-first (aref (aref runes i) 0))
             (ch-second (aref (aref runes (1+ i)) 0)))
        (when (and (and (>= ch-first ?A) (<= ch-first ?Z))
                   (and (>= ch-second ?a) (<= ch-second ?z)))
          (aset runes (1+ i) (concat (substring (aref runes i) -1) (aref runes (1+ i))))
          (aset runes i (substring (aref runes i) 0 -1))))
      (setq i (1+ i)))

    ;; construct final result
    (setq i 0)
    (while (< i runes-length)
      (when (> (length (aref runes i)) 0)
        (setq rlt (add-to-list 'rlt (aref runes i) t)))
      (setq i (1+ i)))
     rlt))

(defun flyspell-detect-ispell-args (&optional run-together)
  "If RUN-TOGETHER is true, spell check the CamelCase words.
Please note RUN-TOGETHER will make aspell less capable. So it should only be used in prog-mode-hook."
  ;; force the English dictionary, support Camel Case spelling check (tested with aspell 0.6)
  (let* ((args (list "--sug-mode=ultra" "--lang=en_US"))args)
    (if run-together
        (setq args (append args '("--run-together" "--run-together-limit=16"))))
    args))

;; {{ for aspell only, hunspell does not need setup `ispell-extra-args'
(setq ispell-program-name "aspell")
(setq-default ispell-extra-args (flyspell-detect-ispell-args t))
;; }}

;; ;; {{ hunspell setup, please note we use dictionary "en_US" here
;; (setq ispell-program-name "hunspell")
;; (setq ispell-local-dictionary "en_US")
;; (setq ispell-local-dictionary-alist
;;       '(("en_US" "[[:alpha:]]" "[^[:alpha:]]" "[']" nil ("-d" "en_US") nil utf-8)))
;; ;; }}

(defvar extra-flyspell-predicate '(lambda (word) t)
  "A callback to check WORD.  Return t if WORD is typo.")

(defun my-flyspell-predicate (word)
  "Use aspell to check WORD.  If it's typo return t."
  (let* ((cmd (cond
               ;; aspell: `echo "helle world" | aspell pipe`
               ((string-match-p "aspell$" ispell-program-name)
                (format "echo \"%s\" | %s pipe"
                        word
                        ispell-program-name))
               ;; hunspell: `echo "helle world" | hunspell -a -d en_US`
               (t
                (format "echo \"%s\" | %s -a -d en_US"
                        word
                        ispell-program-name))))
         (cmd-output (shell-command-to-string cmd))
         rlt)
    ;; (message "word=%s cmd=%s" word cmd)
    ;; (message "cmd-output=%s" cmd-output)
    (cond
     ((string-match-p "^&" cmd-output)
      ;; it's a typo because at least one sub-word is typo
      (setq rlt t))
     (t
      ;; not a typo
      (setq rlt nil)))
    rlt))

(defun js-flyspell-verify ()
  (let* ((case-fold-search nil)
         (font-matched (memq (get-text-property (- (point) 1) 'face)
                             '(js2-function-call
                               js2-function-param
                               js2-object-property
                               js2-object-property-access
                               font-lock-variable-name-face
                               font-lock-string-face
                               font-lock-function-name-face
                               font-lock-builtin-face
                               rjsx-text
                               rjsx-tag
                               rjsx-attr)))
         subwords
         word
         (rlt t))
    (cond
     ((not font-matched)
      (setq rlt nil))
     ;; ignore two character word
     ((< (length (setq word (thing-at-point 'word))) 2)
      (setq rlt nil))
     ;; handle camel case word
     ((and (setq subwords (split-camel-case word)) (> (length subwords) 1))
      (let* ((s (mapconcat (lambda (w)
                             (cond
                              ;; sub-word wholse length is less than three
                              ((< (length w) 3)
                               "")
                               ;; special characters
                              ((not (string-match-p "^[a-zA-Z]*$" w))
                               "")
                              (t
                               w))) subwords " ")))
        (setq rlt (my-flyspell-predicate s))))
     (t
      (setq rlt (funcall extra-flyspell-predicate word))))
    rlt))

(put 'js2-mode 'flyspell-mode-predicate 'js-flyspell-verify)
(put 'rjsx-mode 'flyspell-mode-predicate 'js-flyspell-verify)

Optionally, you could see https://github.com/redguardtoo/emacs.d/blob/master/lisp/init-spelling.el for my real world setup.

Use Imenu to list comments in current buffer

Imenu to list comments in current buffer :en:emacs:imenu:

evil-nerd-commenter v3.2.0 has a new function evilnc-imenu-create-index-function.

Imenu could use this function to list all comments in current file.

Usage:

(require 'counsel)
(defun counsel-imenu-comments ()
  "Imenu display comments."
  (interactive)
  (let* ((imenu-create-index-function 'evilnc-imenu-create-index-function))
    (counsel-imenu)))

Screen cast:

counsel-imenu-comments.gif

counsel-etags v1.3.1 is released

Counsel-etags is a complete solution for code navigation in Emacs.

It needs no setup. One command counsel-etags-find-tag-at-point is enough to start code navigation immediately.

It solves all problems using Ctags/Etags with Emacs.

Problem 1: Ctags takes a few seconds to update the tags file (the index file to lookup tags). The updating process blocks the user's further interaction. This problem is solved by the virtual updating function from counsel-etags. The setup is simple:

;; Don't ask before rereading the TAGS files if they have changed
(setq tags-revert-without-query t)
;; Don't warn when TAGS files are large
(setq large-file-warning-threshold nil)
;; Setup auto update now
(add-hook 'prog-mode-hook
  (lambda ()
    (add-hook 'after-save-hook
              'counsel-etags-virtual-update-tags 'append 'local)))
(add-hook 'after-save-hook 'counsel-etags-virtual-update-tags)

Problem 2: Tag lookup may fail if the latest code is not scanned yet. This problem is solved by running counsel-etags-grep automatically if counsel-etags-find-tag-at-point fails. So users always get results.

There are also other enhancements.

Enhancement 1: Levenshtein Distance algorithm is used to place the better matching candidates at the the top. For example, a function named renderTable could be defined all around in a ReactJS project. But it's very possible the user prefers the definition in same component or same folder where she triggers code navigation.

Enhancement 2: It's inefficient to search the same tag again and again. counsel-etags-recent-tag is used to jump to previous definitions.

Enhancement 3: Ivy-mode provides filter UI for counsel-etags. Its means all the functionalities from Ivy is also available. For example, users can input "!keyword1" to exclude candidates matching "keyword1".

Enhancement 4: counsel-etags-grep uses the fastest grep program ripgrep if it's installed. Or else it falls back to standard grep.

Please check https://github.com/redguardtoo/counsel-etags for more tips.

Auto complete everything in Emacs

complete everything in Emacs :en:emacs:

As a web developer using modern front end framework like React/Angular, I spend a lot of time on web components.

A component instance is like:

<GenericTable
  onSelectRow={ row => console.log(row) }
  numberOfPinnedColumns={2}
  withToolBar
  onClickCell={ cell => console.log(cell) }
>
  <PaginationButtons />
  <TotalSum />
  <ReportButtons />
</GenericTable>

Basically a component instance is a big chunk of html tags.

I created a new package EACL (Emacs auto complete lines) which could help me input components in unbelievable speed.

The idea is simple. If I've already used one component elsewhere in the project. It's unnecessary to re-type the similar code again.

All I need to do is to input the first characters of the component and run M-x eacl-complete-tag which will grep the project and input the remaining part of component.

Here is a demo to input component ButtonToolbar:

eacl-demo.gif

Please note EACL is generic and can be use in any programming language.

M-x eacl-complete-statement to complete below Javascript code:

import {
  Button,
  Row,
  Column
} from 'react-bootstrap';

M-x eacl-complete-snippet to complete below C code:

static int v9fs_drop_inode(struct inode *inode)
{
    struct v9fs_session_info *v9ses;
    v9ses = v9fs_inode2v9ses(inode);
    if (v9ses->cache == CACHE_LOOSE || v9ses->cache == CACHE_FSCACHE)
        return generic_drop_inode(inode);
    /*
     * in case of non cached mode always drop the
     * the inode because we want the inode attribute
     * to always match that on the server.
     */
    return 1;
}

You can also create your own commands based on API eacl-complete-multi-lines-internal.

For example, it is a piece of cake to support Lisp by creating comand my-complete-lisp:

(require 'eacl)
(defun my-complete-lisp ()
  (interactive)
  (eacl-complete-multi-lines-internal "[^)]*)"))

Split Emacs window with certain ratio

Emacs window with certain ratio :en:emacs:

The idea comes from yangdaweihit. Here is the implementation.

(defvar my-ratio-dict
  '((1 . 1.61803398875)
    (2 . 2)
    (3 . 3)
    (4 . 4)
    (5 . 0.61803398875))
  "The ratio dictionary.")

(defun my-split-window-horizontally (&optional ratio)
  "Split window horizontally and resize the new window.
Always focus bigger window."
  (interactive "P")
  (let* (ratio-val)
    (cond
     (ratio
      (setq ratio-val (cdr (assoc ratio my-ratio-dict)))
      (split-window-horizontally (floor (/ (window-body-width)
                                           (1+ ratio-val)))))
     (t
      (split-window-horizontally)))
    (set-window-buffer (next-window) (other-buffer))
    (if (or (not ratio-val)
            (>= ratio-val 1))
        (windmove-right))))

(defun my-split-window-vertically (&optional ratio)
  "Split window vertically and resize the new window.
Always focus bigger window."
  (interactive "P")
  (let* (ratio-val)
    (cond
     (ratio
      (setq ratio-val (cdr (assoc ratio my-ratio-dict)))
      (split-window-vertically (floor (/ (window-body-height)
                                         (1+ ratio-val)))))
     (t
      (split-window-vertically)))
    ;; open another window with other-buffer
    (set-window-buffer (next-window) (other-buffer))
    ;; move focus if new window bigger than current one
    (if (or (not ratio-val)
            (>= ratio-val 1))
        (windmove-down))))

(global-set-key (kbd "C-x 2") 'my-split-window-vertically)
(global-set-key (kbd "C-x 3") 'my-split-window-horizontally)

Usage is simple. For example, C-x 2 is similar to original split-winddow-vertically while C-u 1 C-x 2 split the window in golden ratio.

Enhance diff-mode with Ivy

diff-mode with Ivy :en:emacs:diff:ivy:

My current job requires me to review the freelancer's patches and apply them to our code branch under Perforce control. Due to my client's security policy, the freelancer can only work on isolated sandbox environment and can't access our code base directly.

I need two steps to finish the task:

  • Open the freelancer's patch in diff-mode
  • Run diff-apply-hunk to apply the hunks interactively

The problem is diff-mode always ask me to specify the file to be patched.

I read the code of diff-apply-hunk. The logic of diff-apply-hunk is simple. It tries different algorithms to guess the right file to patch. When the algorithms fail, it calls API read-file-name to ask me to provide the file path manually. If right file is found, the algorithms will work again and read-file-name will never be used for other hunks.

Here is my solution. I can find the file to patch in recent opened files because I store all of them by (setq recentf-max-saved-items 2048). I plan to use ivy-read from Ivy to locate the file at first. If this step fails , I can still fall back on original API read-file-name.

Here is the code


(defvar ffip-read-file-name-hijacked-p nil)
(defun ffip-diff-apply-hunk (&optional reverse)
  (interactive "P")
  (unless recentf-mode (recentf-mode 1))
  (setq ffip-read-file-name-hijacked-p t)
  (defadvice read-file-name (around ffip-read-file-name-hack activate)
    (cond
     (ffip-read-file-name-hijacked-p
      (let* ((args (ad-get-args 0))
             (file-name (file-name-nondirectory (nth 2 args)))
             (cands (remove nil (mapcar (lambda (s) (if (string-match-p (format "%s$" file-name) s) s))
                                        (mapcar #'substring-no-properties recentf-list))))
             (rlt (ivy-read "Recentf: " cands)))
        (if rlt (setq ad-return-value rlt) rlt ad-doit)))
     (t
      ad-do-it)))
  (diff-apply-hunk reverse)
  (setq ffip-read-file-name-hijacked-p nil))

Please note ffip-diff-apply-hunk can replace diff-apply-hunk.

BTW, I can edit the patch on the spot when applying hunks. Similar to the work flow of git add --patch.

The solution is added into https://github.com/technomancy/find-file-in-project.

Firefox and Emacs

and Emacs :en:emacs:firefox:ivy:keysnail:

For me, there is NO difference between Firefox and Emacs. They provide useful APIs, nothing more.

Three years ago, I wrote Use firefox in Emacs way to demo how to convert Firefox into Emacs by Keynsail.

A year ago I published Hello Ivy-mode, bye Helm to prove how powerful Ivy-mode is by using its API ivy-read.

Keysnail has similar javascript API prompt.selector and it's as powerful as ivy-read if not more powerful.

For example, you can insert below snippet into ~/.keysnail.js and press ",hh" or "C-c C-h" to query browse history:

function searchHistory(evt, arg) {
  function timeSince(now, date) {

    var seconds = Math.floor((now - date) / 1000);

    var interval = Math.floor(seconds / 31536000);

    if (interval > 1) {
      return interval + " years";
    }
    interval = Math.floor(seconds / 2592000);
    if (interval > 1) {
      return interval + " months";
    }
    interval = Math.floor(seconds / 86400);
    if (interval > 1) {
      return interval + " days";
    }
    interval = Math.floor(seconds / 3600);
    if (interval > 1) {
      return interval + " hours";
    }
    interval = Math.floor(seconds / 60);
    if (interval > 1) {
      return interval + " minutes";
    }
    return Math.floor(seconds) + " seconds";
  }

  function searchWithKeyword(q) {
    var collection = (function() {
      //search option
      var options = PlacesUtils.history.getNewQueryOptions();
      options.maxResults = 4096;
      options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
      //options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_FRECENCY_DESCENDING;
      options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
      options.includeHidden = true;

      //search query
      var query = PlacesUtils.history.getNewQuery();
      // read keyworld
      if(q && q !== '') {
        query.searchTerms  = q;
      }

      var result = PlacesUtils.history.executeQuery(query, options);
      var root = result.root;

      var collection = [];
      var now = new Date().getTime();
      var siteNode;
      root.containerOpen = true;
      for (var i = 0; i < root.childCount; i++) {
        // siteNode => nsINavHistoryResultNode
        siteNode = root.getChild(i);
        collection.push([siteNode.icon,siteNode.title,siteNode.uri, siteNode.time/1000]);
      }
      collection.sort(function(a, b) {
        return b[3]-a[3];
      });
      // reformat the time
      for (i = 0; i < collection.length; i++) {
        collection[i][3] = timeSince(now, collection[i][3]) + ' ago';
      }
      root.containerOpen = false;
      return collection;
    })();

    prompt.selector({
      message    : "Search history"+ (q && q !== ''? (' @'+q +':') : ':' ),
      collection : collection,
      flags      : [ICON | IGNORE, 0, 0, 0],
      header     : ["Title", "Url", "Last visited"],
      width      : [30, 60, 10],
      callback: function (i) {
        if (i >= 0) {
          openUILinkIn(collection[i][2], "tab");
        }
      },
      onFinish: function() {
        gBrowser.focus();
        _content.focus();
      }
    });
  }

  prompt.finish(true);
  prompt.read('Keyword to search history?', searchWithKeyword, null, null, null, 0, "history_search");
  // searchWithKeyword('test');

}
key.setViewKey([',', 'h', 'h'], searchHistory, "Search history");
key.setGlobalKey(['C-c', 'C-h'], searchHistory, "Search history");

Here is my complete .keysnail.js.

Use wgrep and evil to replace text efficiently

wgrep and evil to replace text efficiently :en:emacs:wgrep:

In my previous article Emacs is easy if you read code, I proved ivy and wgrep is easy if you read code. You can even create your own plugin based on their APIs. For example, I define my-grep and my-grep-occur in init-ivy.el in order to search/replace text in project root directory.

My wgrep-mode enabled buffer is in evil-mode. I prefer pressing vi key binding dd to remove lines in that buffer to tell wgrep skip them.

It turns out we need M-x C-c C-p or M-x wgrep-toggle-readonly-area before removing lines.

I'm too lazy to remember extra commands. So here is the workaround:

;; Press `dd' to delete lines in `wgrep-mode' in evil directly
(defadvice evil-delete (around evil-delete-hack activate)
  ;; make buffer writable
  (if (and (boundp 'wgrep-prepared) wgrep-prepared)
      (wgrep-toggle-readonly-area))
  ad-do-it
  ;; make buffer read-only
  (if (and (boundp 'wgrep-prepared) wgrep-prepared)
      (wgrep-toggle-readonly-area)))

Emacs is easy if you read code

is easy if you read code :en:emacs:ivy:counsel:

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)))))

Auto-complete word in Emacs mini-buffer when using Evil

word in Emacs mini-buffer when using Evil :en:emacs:evil:vim:

When using Evil I often input %s/old-keyword/new-keyword/g in Minibuffer.

The problem is auto completions of new-keyword using hippie-expand always fail.

It turns out that the character "/" is treated as Word constituent in minibuffer.

The solution is to re-define "/" as Punctuation characters:

(defun minibuffer-inactive-mode-hook-setup ()
  ;; make `try-expand-dabbrev' from `hippie-expand' work in mini-buffer
  ;; @see `he-dabbrev-beg', so we need re-define syntax for '/'
  (set-syntax-table (let* ((table (make-syntax-table)))
                      (modify-syntax-entry ?/ "." table)
                      table)))
(add-hook 'minibuffer-inactive-mode-hook 'minibuffer-inactive-mode-hook-setup)