Enhance diff-mode with 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

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

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

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

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)

Indent JSX in Emacs

I find rjsx-mode annoying when it indents closed html tag with extra spaces.

It's based on js2-mode which is actually just calling Emacs v25 API js-jsx-indent-line.

So the solution is as simple as advice js-jsx-indent-line:

(defadvice js-jsx-indent-line (after js-jsx-indent-line-after-hack activate)
  "Workaround sgml-mode and follow airbnb component style."
  (let* ((cur-line (buffer-substring-no-properties
                    (line-beginning-position)
                    (line-end-position))))
    (if (string-match "^\\( +\\)\/?> *$" cur-line)
      (let* ((empty-spaces (match-string 1 cur-line)))
        (replace-regexp empty-spaces
                        (make-string (- (length empty-spaces) sgml-basic-offset) 32)
                        nil
                        (line-beginning-position) (line-end-position))))))

Find the buffer with Chinese name

We can find the buffer with Chinese name efficiently by using the first character of pinyin.

Here is the code (ivy and pinyinlib is required):

(defun ivy-switch-buffer-matcher-pinyin (regexp candidates)
  (unless (featurep 'pinyinlib) (require 'pinyinlib))
  (let* ((pys (split-string regexp "[ \t]+"))
         (regexp (format ".*%s.*"
                         (mapconcat 'pinyinlib-build-regexp-string pys ".*"))))
    (ivy--switch-buffer-matcher regexp candidates)))

(defun ivy-switch-buffer-by-pinyin ()
  "Switch to another buffer."
  (interactive)
  (unless (featurep 'ivy) (require 'ivy))
  (let ((this-command 'ivy-switch-buffer))
    (ivy-read "Switch to buffer: " 'internal-complete-buffer
              :matcher #'ivy-switch-buffer-matcher-pinyin
              :preselect (buffer-name (other-buffer (current-buffer)))
              :action #'ivy--switch-buffer-action
              :keymap ivy-switch-buffer-map
              :caller 'ivy-switch-buffer)))

You can M-x ivy-switch-buffer-by-pinyin to switch buffer.

The algorithm of pinyinlib is simple. We build a big lookup table to convert the a plain English string into a regular expression containing Chinese characters.

You can apply the same algorithm to other non-English languages.

Use Perforce inside Emacs

CREATED: <2016-12-05>

UPDATED: <2017-08-14 Mon>

Perforce is a proprietary VCS. It's less powerful than Git so need extra effort to be use with Emacs.

For example, git log -p file-path displays the detailed history of a single file. There is no way you can do this in perforce. Even ClI like p4 changes file-path | awk '{print $2}' | xargs -i p4 describe -du {} can't do it. I have to use Emacs Lisp to clean the output of p4.

Perforce workflow

  • p4 set to set up
  • p4 login to login
  • p4 client creates a new work space to tell server the directories/files to check out or to ignore
  • p4 sync //depot/url/... to checkout files
  • Files are read-only by default. You need p4 edit file to make files writable before editing
  • p4 add files to add new files. p4 revert to revert edited file to it original status and lose the local changes
  • p4 change to create a pending change. Then p4 submit -c #changelist to actually submit code to main branch. Pending change gives you a chance to tweak the change before submit
  • Or p4 submit -d"description" file to submit a single file directly

My solution

Perforce Cygwin portable is not recommended.

I suggest using executable from Perforce Windows native version which works on both Cygwin and Windows.

Perforce server assigns unique URL for each physical file. If I only feed p4 that URL, the operation is always successful.

Emacs

I developed vc-msg which shows "commit message of current line in Emacs". It supports Perforce out of box (you still need p4 login at first.)

My patched emacs-git-gutter show Perforce gutters. You only need setup git-gutter:exp-to-create-diff. Here is a sample,

(setq-local git-gutter:exp-to-create-diff
            (shell-command-to-string (format "p4 diff -du -db %s"
                                             (file-relative-name buffer-file-name))))

I also provided commands like p4edit, p4revert, p4submit, p4diff, and p4history,

;; {{ perforce utilities
(defvar p4-file-to-url '("" "")
  "(car p4-file-to-url) is the original file prefix
(cadr p4-file-to-url) is the url prefix")

(defun p4-current-file-url ()
  (replace-regexp-in-string (car p4-file-to-url)
                            (cadr p4-file-to-url)
                            buffer-file-name))

(defun p4-generate-cmd (opts)
  (format "p4 %s %s" opts (p4-current-file-url)))

(defun p4edit ()
  "p4 edit current file."
  (interactive)
  (shell-command (p4-generate-cmd "edit"))
  (read-only-mode -1))

(defun p4submit (&optional file-opened)
  "p4 submit current file.
If FILE-OPENED, current file is still opened."
  (interactive "P")
  (let* ((msg (read-string "Say (ENTER to abort):"))
         (open-opts (if file-opened "-f leaveunchanged+reopen -r" ""))
         (full-opts (format "submit -d '%s' %s" msg open-opts)))
    ;; (message "(p4-generate-cmd full-opts)=%s" (p4-generate-cmd full-opts))
    (if (string= "" msg)
        (message "Abort submit.")
      (shell-command (p4-generate-cmd full-opts))
      (unless file-opened (read-only-mode 1))
      (message (format "%s submitted."
                       (file-name-nondirectory buffer-file-name))))))

(defun p4revert ()
  "p4 revert current file."
  (interactive)
  (shell-command (p4-generate-cmd "revert"))
  (read-only-mode 1))

(defun p4-show-changelist-patch (line)
  (let* ((chg (nth 1 (split-string line "[\t ]+")))
         (url (p4-current-file-url))
         (pattern "^==== //.*====$")
         sep
         seps
         (start 0)
         (original (if chg (shell-command-to-string (format "p4 describe -du %s" chg)) ""))
         rlt)

    (while (setq sep (string-match pattern original start))
      (let* ((str (match-string 0 original)))
        (setq start (+ sep (length str)))
        (add-to-list 'seps (list sep str) t)))
    (setq rlt (substring original 0 (car (nth 0 seps))))
    (let* ((i 0) found)
      (while (and (not found)
                  (< i (length seps)))
        (when (string-match url (cadr (nth i seps)))
          (setq rlt (concat rlt (substring original
                                           (car (nth i seps))
                                           (if (= i (- (length seps) 1))
                                               (length original)
                                             (car (nth (+ 1 i) seps))))))
          ;; out of loop now since current file patch found
          (setq found t))
        (setq i (+ 1 i))))

    ;; remove p4 verbose bullshit
    (setq rlt (replace-regexp-in-string "^\\(Affected\\|Moved\\) files \.\.\.[\r\n]+\\(\.\.\. .*[\r\n]+\\)+"
                                        ""
                                        rlt))
    (setq rlt (replace-regexp-in-string "Differences \.\.\.[\r\n]+" "" rlt))
    ;; one line short description of change list
    (setq rlt (replace-regexp-in-string "Change \\([0-9]+\\) by \\([^ @]+\\)@[^ @]+ on \\([^ \r\n]*\\).*[\r\n \t]+\\([^ \t].*\\)" "\\1 by \\2@\\3 \\4" rlt))
    rlt))

(defun p4--create-buffer (buf-name content &optional enable-imenu)
  (let* (rlt-buf)
    (if (get-buffer buf-name)
        (kill-buffer buf-name))
    (setq rlt-buf (get-buffer-create buf-name))
    (save-current-buffer
      (switch-to-buffer-other-window rlt-buf)
      (set-buffer rlt-buf)
      (erase-buffer)
      (insert content)
      (diff-mode)
      (goto-char (point-min))
      ;; nice imenu output
      (if enable-imenu
          (setq imenu-create-index-function
                (lambda ()
                  (save-excursion
                    (imenu--generic-function '((nil "^[0-9]+ by .*" 0)))))))
      ;; quit easily in evil-mode
      (evil-local-set-key 'normal "q" (lambda () (interactive) (quit-window t))))))

(defun p4diff ()
  "Show diff of current file like `git diff'."
  (interactive)
  (let* ((content (shell-command-to-string (p4-generate-cmd "diff -du -db"))))
    (p4--create-buffer "*p4diff*" content)))

(defun p4history ()
  "Show history of current file like `git log -p'."
  (interactive)
  (let* ((changes (split-string (shell-command-to-string (p4-generate-cmd "changes")) "\n"))
         (content (mapconcat 'p4-show-changelist-patch
                             changes
                             "\n\n")))
    (p4--create-buffer "*p4log*" content t)))
;; }}

As a bonus tip, if you use find-file-in-project, insert below code into prog-mode-hook to view any perforce change inside Emacs,

(setq-local ffip-diff-backends
            '((ivy-read "p4 change to show:"
                        (split-string (shell-command-to-string "p4 changes //depot/development/DIR/PROJ1/...")
                                      "\n")
                        :action (lambda (i)
                                  (if (string-match "^ Change \\([0-9]*\\)" i)
                                      (shell-command-to-string (format "p4 describe -du -db %s"
                                                                       (match-string 1 i))))))
              "p4 diff -du -db //depot/development/DIR/PROJ1/..."))

You can also check my emacs.d to get latest code.

Bash Shell

Other operations are finished in Bash Shell,

# {{ Perforce, I hope I will never use it
if [ "$OS_NAME" = "CYGWIN" ]; then
    function p4() {
        export PWD=`cygpath -wa .`
        /cygdrive/c/Program\ Files/Perforce/p4.exe $@
    }
fi

# p4 workflow:
#
#   # basic setup
#   p4 set P4CLIENT=clientname  # set your default client
#   p4 set P4PORT=SERVER:1666
#   p4 set P4USER=username
#   p4 client # create/edit client, client views selected files
#
#   # checkout code
#   p4 sync [-f] //depot/project-name/path/...
#   p4 edit file[s]
#   ... do some editing ...
#
#   # submit code
#   either `p4 submit -d"say hi" file` or `p4 change`
#
#   I recommend `p4 change` because you can edit files list before submit happens.
#   After `p4 change`,  `p4 submit -c changelist#` to actually submit change.
#
alias p4clr='p4 diff -sr | p4 -x - revert' # like `git reset HEAD`
alias p4blame='p4 annotate -c -db ' # could add -a see deleted lines
alias p4cr='p4 submit -f leaveunchanged+reopen -r'
alias reviewcl='ccollab addchangelist new'
alias p4pending='p4 changes -s pending' # add ... for current directory
alias p4untrack='find . -type f| p4 -x - fstat >/dev/null'
alias p4v='p4 resolve' # after `p4 sync ...`, maybe resolve
alias p4r='p4 revert' # discard changes
alias p4e='p4 edit'
alias p4s='p4 submit'
alias p4sr='p4 submit -f submitunchanged+reopen' #submit&reopen
alias p4up='p4 sync ...' # synchronize from current directory
alias p4o='p4 opened' # list opened files
alias p4c='p4 changes' # create a new pending change
alias p4chg='p4 change' # create a pending change
alias p4d='p4 diff -du -db'
alias p4ds='p4 diff -du -db | lsdiff' # diff summary, patchutils required
alias p4i='p4 integrate'
alias p4unsh='p4 unshelve -s' # Usage: p4unsh changelist#, like `git stash apply`
alias p4h='p4 changes -m 1 ...' # show the head change

function p4mypending {
    local P4USERNAME="`p4 user -o | grep '^User:\s' | sed 's/User:\s\([a-bA-B0-9]*\)/\1/g'`"
    p4 changes -s pending -u $P4USERNAME
}

function p4shelved {
    local P4USERNAME="`p4 user -o | grep '^User:\s' | sed 's/User:\s\([a-bA-B0-9]*\)/\1/g'`"
    p4 changes -s shelved -u $P4USERNAME # add ... for current directory
}

function p4cmp {
    if [ -z "$1" ]; then
        echo "Usage: p4cmp changelist-number changelist-number"
    else
        p4 diff2 -dub -q -u ...@$1 ...@$2
    fi
}

function p4dl {
    # git diff
    p4 diff -du -db $@ | vim -c "set syntax=diff" -R -
}
function p4sh(){
    # show specific change or the latest change
    if [ -z "$1" ]; then
        p4 changes | python ~/bin/percol.py | awk '{print $2}' | xargs -i p4 describe -du {} | vim -c "set syntax=diff" -R -
    else
        p4 describe -du -db $@ | vim -c "set syntax=diff" -R -
    fi
}

function p4lp {
    #like `git log -p`
    p4 changes $@ | awk '{print $2}' | xargs -i p4 describe -du {} | less -F
}

function p4mlp {
    #like `git log -p`
    p4 changes -u $P4USERNAME $@ | awk '{print $2}' | xargs -i p4 describe -du {} | less -F
}

function p4adddir(){
    if [ -z "$1" ]; then
        echo "Usage: p4adddir directory"
    else
        find $1 -type f -print | p4 -x - add
    fi
}

# p4's suggestion,http://kb.perforce.com/article/27/creating-release-notes
# @google "assing variable from bash to perl in a bash script"
function p4l(){
    # p4 log
    if [ -z "$1" ]; then
        # show the full log
        p4 changes -l ... | less
    else
        # p4log since-changelist-number
        p4 changes -l ...@$1,#head|perl -pe "if(\$_=~/$1/){ last;};"
    fi
}

function p4ml(){
    # my p4 log
    if [ -z "$1" ]; then
        # show the full log
        p4 changes -l -u $P4USERNAME ... | less
    else
        # p4log since-changelist-number
        p4 changes -l -u $P4USERNAME ...@$1,#head|perl -pe "if(\$_=~/$1/){ last;};"
    fi
}
# }}

purify quora.com with vanilla javascript

"Smart" http://quora.com always recommends the stories I hate to see.

So here is my way to toggle the stories display on Chrome and Firefox.

Step 1, create a new bookmark with below link,

javascript:a=Array.from(document.getElementsByClassName("AnswerStoryToggleModal"));a.forEach(function(e){e.style.display=a[a.length-1].style.display==='none'?'block':'none';});

Step 2, DONE! You only need click the bookmark to hide or show the stories when visiting http://quora.com.

Here is the original vanilla javascript,

var a = Array.from(document.getElementsByClassName("AnswerStoryToggleModal"));
a.forEach(function (e) {
    // check 'display' of the last item in story feed before toggling
    e.style.display = a[a.length - 1].style.display === 'none' ? 'block' : 'none';
});

Screenshot:

purify-quora-nq8.png

山东韭菜猪肉虾仁水餃

1 原料

  • 韭菜一斤(8把)
  • 虾仁适量
  • 八角一个
  • 猪肉适量
  • 饺子皮90片

2 流程

  • 韭菜切碎
  • 虾仁切成小块
  • 猪肉冷冻后切丁
  • 葱姜切末
  • 八角磨成粉(或十三香或五香粉),可以用捣蒜的工具捣尽量碎
  • 以上材料混合,加鲜味酱油,加菜油(馅不干且有香味),加盐适量,可再加适量麻油
  • 包饺子时面皮涂点水,对折用两个大拇指用力压扁饺子边
  • 饺子底部粘生粉防止粘一起
  • 熟后可用蒜末和醋调味

馅:

dumpling-inside.jpg

开吃:

dumpling.jpg