Use js2-mode to print json path

CREATED: <2015-10-29 Thu>

UPDATED: <2015-11-22 Sun>

These days most web applications are based on RESTful.

So javascript developers have to deal with JSON frequently.

When working on JSON, the most boring and error prone task is to get the full path of specific field.

For example, given the JSON response like:

{
  key1: {
    subkey1: {
      arr1: [{ key3: 'hello'}]
    }
  }
}

The developer need figure out the full path of "key3" which is "key1.subkey1.arr1[0].key3".

My solution is to take advantage of AST created by js2-mode.

Step 1, insert below code into ~/.emacs,

;; {{ print json path, will be removed when latest STABLE js2-mode released
(defun js2-get-element-index-from-array-node (elem array-node &optional hardcoded-array-index)
  "Get index of ELEM from ARRAY-NODE or 0 and return it as string."
  (let ((idx 0) elems (rlt hardcoded-array-index))
    (setq elems (js2-array-node-elems array-node))
    (if (and elem (not hardcoded-array-index))
        (setq rlt (catch 'nth-elt
                    (dolist (x elems)
                      ;; We know the ELEM does belong to ARRAY-NODE,
                      (if (eq elem x) (throw 'nth-elt idx))
                      (setq idx (1+ idx)))
                    0)))
    (format "[%s]" rlt)))

(defun js2-print-json-path (&optional hardcoded-array-index)
  "Print the path to the JSON value under point, and save it in the kill ring.
If HARDCODED-ARRAY-INDEX provided, array index in JSON path is replaced with it."
  (interactive "P")
  (let (previous-node current-node
        key-name
        rlt)

    ;; The `js2-node-at-point' starts scanning from AST root node.
    ;; So there is no way to optimize it.
    (setq current-node (js2-node-at-point))

    (while (not (js2-ast-root-p current-node))
      (cond
       ;; JSON property node
       ((js2-object-prop-node-p current-node)
        (setq key-name (js2-prop-node-name (js2-object-prop-node-left current-node)))
        (if rlt (setq rlt (concat "." key-name rlt))
          (setq rlt (concat "." key-name))))

       ;; Array node
       ((or (js2-array-node-p current-node))
        (setq rlt (concat (js2-get-element-index-from-array-node previous-node
                                                                 current-node
                                                                 hardcoded-array-index)
                          rlt)))

       ;; Other nodes are ignored
       (t))

      ;; current node is archived
      (setq previous-node current-node)
      ;; Get parent node and continue the loop
      (setq current-node (js2-node-parent current-node)))

    (cond
     (rlt
      ;; Clean the final result
      (setq rlt (replace-regexp-in-string "^\\." "" rlt))
      (kill-new rlt)
      (message "%s => kill-ring" rlt))
     (t
      (message "No JSON path found!")))

    rlt))

Step 2, move the cursor over the key/value of the field and `M-x js2-print-json-path`.

My code is merged into js2-mode. You need only install latest js2-mode.

(apply #'derived-mode-p '(org-mode web-mode))

Evil text object to select nearby file path

CREATED: <2015-08-08 Sat>

UPDATED: <2015-08-24 Mon>

Insert below code into ~/.emacs:

;; {{ nearby file path as text object,
;;      - "vif" to select only basename
;;      - "vaf" to select the full path
;;
;;  example: "/hello/world" "/test/back.exe"
;;               "C:hello\\hello\\world\\test.exe" "D:blah\\hello\\world\\base.exe"
;;
;; tweak evil-filepath-is-nonname to re-define a path
(defun evil-filepath-is-separator-char (ch)
  "Check ascii table"
  (let (rlt)
    (if (or (= ch 47)
            (= ch 92))
        (setq rlt t))
    rlt))

(defun evil-filepath-not-path-char (ch)
  "Check ascii table for charctater "
  (let (rlt)
    (if (or (and (<= 0 ch) (<= ch 32))
            (= ch 34) ; double quotes
            (= ch 39) ; single quote
            (= ch 40) ; (
            (= ch 41) ; )
            (= ch 60) ; <
            (= ch 62) ; >
            (= ch 91) ; [
            (= ch 93) ; ]
            (= ch 96) ; `
            (= ch 123) ; {
            (= ch 125) ; }
            (= 127 ch))
        (setq rlt t))
    rlt))

(defun evil-filepath-char-not-placed-at-end-of-path (ch)
  (or (= 44 ch) ; ,
      (= 46 ch) ; .
      ))

(defun evil-filepath-calculate-path (b e)
  (let (rlt f)
    (when (and b e)
      (setq b (+ 1 b))
      (when (save-excursion
                (goto-char e)
                (setq f (evil-filepath-search-forward-char 'evil-filepath-is-separator-char t))
                (and f (>= f b)))
        (setq rlt (list b (+ 1 f) (- e 1)))))
    rlt))

(defun evil-filepath-get-path-already-inside ()
  (let (b e)
    (save-excursion
      (setq b (evil-filepath-search-forward-char 'evil-filepath-not-path-char t)))
    (save-excursion
      (setq e (evil-filepath-search-forward-char 'evil-filepath-not-path-char))
      (when e
        (goto-char (- e 1))
        ;; example: hello/world,
        (if (evil-filepath-char-not-placed-at-end-of-path (following-char))
            (setq e (- e 1)))
        ))
    (evil-filepath-calculate-path b e)))

(defun evil-filepath-search-forward-char (fn &optional backward)
  (let (found rlt (limit (if backward (point-min) (point-max))) out)
    (save-excursion
      (while (not out)
        ;; for the char, exit
        (if (setq found (apply fn (list (following-char))))
            (setq out t)
          ;; reach the limit, exit
          (if (= (point) limit)
              (setq out t)
            ;; keep moving
            (if backward (backward-char) (forward-char)))))
      (if found (setq rlt (point))))
    rlt))

(defun evil-filepath-extract-region ()
  "Find the closest file path"
  (let (rlt b f1 f2)

    (if (and (not (evil-filepath-not-path-char (following-char)))
             (setq rlt (evil-filepath-get-path-already-inside)))
        ;; maybe (point) is in the middle of the path
        t
      ;; need search forward AND backward to find the right path
      (save-excursion
        ;; path in backward direction
        (when (setq b (evil-filepath-search-forward-char 'evil-filepath-is-separator-char t))
          (goto-char b)
          (setq f1 (evil-filepath-get-path-already-inside))))
      (save-excursion
        ;; path in forward direction
        (when (setq b (evil-filepath-search-forward-char 'evil-filepath-is-separator-char))
          (goto-char b)
          (setq f2 (evil-filepath-get-path-already-inside))))
      ;; pick one path as the final result
      (cond
       ((and f1 f2)
        (if (> (- (point) (nth 2 f1)) (- (nth 0 f2) (point)))
            (setq rlt f2)
          (setq rlt f1)))
       (f1
        (setq rlt f1))
       (f2
        (setq rlt f2))))

    rlt))

(evil-define-text-object evil-filepath-inner-text-object (&optional count begin end type)
  "File name of nearby path"
  (let ((selected-region (evil-filepath-extract-region)))
    (if selected-region
        (evil-range (nth 1 selected-region) (nth 2 selected-region) :expanded t))))

(evil-define-text-object evil-filepath-outer-text-object (&optional NUM begin end type)
  "Nearby path"
  (let ((selected-region (evil-filepath-extract-region)))
    (if selected-region
        (evil-range (car selected-region) (+ 1 (nth 2 selected-region)) type :expanded t))))

(define-key evil-inner-text-objects-map "f" 'evil-filepath-inner-text-object)
(define-key evil-outer-text-objects-map "f" 'evil-filepath-outer-text-object)
;; }}

柬埔寨名汤(Sgow Chrouk Soup)

视频教程: https://www.youtube.com/watch?v=Aclyh2c_BEs

备料

  • 两人份冷水
  • 米少许(半茶勺)
  • 香茅一根
  • 南姜(Galangal)一小块
  • 洋葱一个
  • 大蒜二瓣
  • 鸡精二茶勺
  • 糖(最好棕榈糖,因为有香味)半茶勺
  • 九层塔(薄荷)一把
  • 鲜红辣椒两到三根

Sgow-Chrouk-Soup-prepare.jpg

步骤

  • 鸡胸脯肉切成小块,盐过一下三四分钟,洗净刀和板
  • 烧开水
  • 香茅洗净,折成多段.南姜先去皮再洗净切成薄片.米洗净,大蒜拍扁,洋葱切好
  • 待水滚放入香茅,南姜,米,大蒜,洋葱(增加甜味)
  • 在水再次滚前,摘薄荷叶,放入大碗淘洗多次再沥干,切成细丝(让香味挥发).辣椒洗净切碎.柠檬切成长条.前述三种料分门别类放入一盘中.
  • 选部份柠檬挤汁入一小碟子中
  • 这时水已滚,加鸡精调味至满意,放入鸡肉(海鲜也可以):
  • 一分钟水滚后,尝一下鸡肉是否熟了.汤的口味,加入糖(略有甜味即可),如果口味淡了,可最后加入鸡精
  • 关火,撒入少量辣椒末,薄荷,葱

上菜

  • 准备大碗,放入薄荷,葱,适量柠檬汁,倒汤
  • 柬埔寨神秘香菜(可选)
  • 可加少量鱼露调咸味

Sgow-Chrouk-Soup-done.jpg

Emacs speed up 1000%

I'm still NOT satisfied with my Emacs performance after applying below tricks:

  • autoload packages
  • idle-load packages
  • compiling *.el to *.elc

After some research, I found I could make my Emacs 1000% fast in 1 minute.

Please note I'm talking about the general performance not just startup time.

The solution is really simple.

Since I'm a Linux guy and my computer got enough (24G) memory. I can place my setup into memory.

Step 1, insert below line into /etc/fstab and restart computer:

tmpfs       /tmp        tmpfs       nodev,nosuid,size=8G    0   0

Step 2, run the script "emacs2ram":

#!/bin/sh

if [ -z "$1" ];then
    echo "Usage:"
    echo "  emacs2ram start"
    echo "  emacs2ram restore"
    exit 1
fi

if [ "$1" == "start" ];then
    backup=emacs.d-backup
    link=.emacs.d
    volatile=/tmp/.emacs.d-$USER

    IFS=
    set -efu

    cd ~/

    if [ ! -r $volatile ]; then
        mkdir -m0700 $volatile
    fi

    # link -> volatie does not exist
    if [ "$(readlink $link)" != "$volatile" ]; then
        # backup project at first
        mv $link $backup
        # create the link
        ln -s $volatile $link
    fi

    if [ -e $link/.unpacked ]; then
        echo "Sync .emacs.d from memory to backup ..."
        rsync -avq --delete --exclude .unpacked ./$link/ ./$backup/
        echo "DONE!"
    else
        echo "Sync .emacs.d from disk to memory ..."
        rsync -avq ./$backup/ ./$link/
        touch $link/.unpacked
        echo "DONE!"
    fi
else
    echo "Moving .emacs.d back to disk ..."
    backup=$2-backup
    link=$2
    volatile=/tmp/$2-$USER
    cd ~/projs
    rm $link && mv $backup $link && rm -rf $volatile
    echo "DONE!"
fi

That's all! Please enjoy Emacs as usual.

The original script is from ArchLinux Wiki. I learned this technique eight years ago. I'm just wondering why I need eight years to apply it?

BTW, I've also moved all my projects into memory, using similar scripts.

UPDATE 1: I also publicize my project-managing script at gist. It's almost same as emacs2ram.

UPDATE 2: Now I use vmtouch which is easier to use and more light weight. Run vmtouch -vt ~/.emacs.d to place the directory into memory.

Unfortunately, vmtouch doesn't support Windows. You can convert my bash script to DOS batch script. Basically the script copies the directory into ram disk and create a link to the directory in memory. You can use ImDisk Toolkit to create ram disk.

Use git-timemachine with Evil

git-timemachine is simple.

After "M-x git-timemachine", a new read-only buffer is created and I need only use six key bindings: "n", "q", "p", "w", "W", "g".

The problem is I'm using Evil. How to make git-timemachine's key bindings override Evil's?

I consulted the issue with Frank Fischer.

The solution is simple. Insert below code into ~/.emacs:

;; @see https://bitbucket.org/lyro/evil/issue/511/let-certain-minor-modes-key-bindings
(with-eval-after-load 'git-timemachine
  (evil-make-overriding-map git-timemachine-mode-map 'normal)
  ;; force update evil keymaps after git-timemachine-mode loaded
  (add-hook 'git-timemachine-mode-hook #'evil-normalize-keymaps))

I also recommend reading his in-depth discussion on general handling of keybindings conflict between evil and other minor modes.

最佳作息时间表

7:00 起床时刻 需要一杯温开水
7:20-8:00 丰盛早餐 补充血糖要丰盛
8:30-9:00 避免运动 非运动最佳时间
9:00-10:30 困难工作 是工作最佳时间
10:30 眼睛休息 看窗外眼睛累了
11:00 吃点水果 血糖可能有下降
12:00-12:30 多吃豆类 豆类是很棒食物
13:00-14:00 小睡一会 会让你精力充沛
16:00 一杯酸奶 酸奶零负担零食
19:00 锻炼时间 快步走慢跑游泳
20:00 电视或书 工作太辛苦放松
22:00 洗热水澡 降温清洁利睡眠
22:30 上床睡觉 保证充足的睡眠

Code search in Emacs

After shutdown of Google Code Search, I turned to plain google search instead.

For example, if I want to search Emacs Lisp code. I google "keyword filetype:el".

"el" is the file extension of Emacs Lisp file.

Since I use Emacs for everything, it's natural to search code in Emacs.

So here is my solution.

Step 1, install w3m and its Emacs wrapper.

Step 2, insert below code into ~/.emacs:

(defun w3m-get-url-from-search-engine-alist (k l)
  (let (rlt)
    (if (listp l)
      (if (string= k (caar l))
          (setq rlt (nth 1 (car l)))
        (setq rlt (w3m-get-url-from-search-engine-alist k (cdr l)))))
    rlt))

(defun w3m-set-url-from-search-engine-alist (k l url)
    (if (listp l)
      (if (string= k (caar l))
          (setcdr (car l) (list url))
        (w3m-set-url-from-search-engine-alist k (cdr l) url))))

;; C-u S g RET <search term> RET in w3m
(setq w3m-search-engine-alist
      '(("g" "http://www.google.com.au/search?q=%s" utf-8)
        ;; stackoverflow search
        ("q" "http://www.google.com.au/search?q=%s+site:stackoverflow.com" utf-8)
        ;; elisp code search
        ("s" "http://www.google.com.au/search?q=%s+filetype:el"  utf-8)
        ;; wikipedia
        ("w" "http://en.wikipedia.org/wiki/Special:Search?search=%s" utf-8)
        ;; online dictionary
        ("d" "http://dictionary.reference.com/search?q=%s" utf-8)
        ;; javascript search on mozilla.org
        ("j" "http://www.google.com.au/search?q=%s+site:developer.mozilla.org" utf-8)))

(defun w3m-google-by-filetype ()
  (interactive)
  (unless (featurep 'w3m)
    (require 'w3m))
  (let ((thing (if (region-active-p)
                   (buffer-substring-no-properties (region-beginning) (region-end))
                 (thing-at-point 'symbol)))
        (old-url (w3m-get-url-from-search-engine-alist "s" w3m-search-engine-alist))
        new-url)
    (when buffer-file-name
      (setq new-url (replace-regexp-in-string
                     "filetype:.*"
                     (concat "filetype:" (file-name-extension buffer-file-name))
                     old-url))
      (w3m-set-url-from-search-engine-alist "s" w3m-search-engine-alist new-url))
    ;; change the url to search current file type
    (w3m-search "s" thing)
    ;; restore the default url
    (w3m-set-url-from-search-engine-alist "s" w3m-search-engine-alist old-url)))

Step 3, `M-x w3m-google-by-filetype`. Either the selected region or the symbol under cursor will be searched.

Use plain Vim to merge conflicts

I will explain how to resolve code merge conflicts with plain vim. The only third party plugin required is vim-unimpaired.

The reason to use vim is that I can't use vimdiff as a merge tool for certain VCS (CVS, Subversion, Perforce, …).

Problem

A typical file in conflict is like:

hello world
hello world
<<<<<<< .mine
This is fun stuff!
=======
This is a documentation file
>>>>>>> .r6
bye world
bye world

Let's call lines like ">>>>>>>", "<<<<<<<" and "=====" conflict markers.

Resolving conflicts is the operation of picking up either or both of the sections between markers.

Basic knowledge of Vim

  • "]n" move the focus to the next marker and "[n" to the previous marker.
  • "dd" delete current line
  • "d]n" delete from current line to the next marker (next marker exclusive)

Solution

Our workflow is actually simple:

  • Step 1, find the next marker by pressing "]n"
  • Step 2, "]nd]n[ndd[ndd" to pick up the section 1
  • Step 3, "d]ndd]ndd" to pick up the section 2, go to step 1

This workflow is compatible with any version control software.

Optionally you can set the shortcut for step 2 and step 3:

map <leader>dg1 ]nd]n[ndd[ndd
map <leader>dg2 d]ndd]ndd

Easy indentation setup in Emacs for web development

CREATED: <2015-04-02 Thu>

UPDATE: <2016-06-22 Wed>

This question has been asked so many times, so I answered it here once for all.

Solution

Insert below code into ~/.emacs:

(defun my-setup-indent (n)
  ;; java/c/c++
  (setq-local c-basic-offset n)
  ;; web development
  (setq-local coffee-tab-width n) ; coffeescript
  (setq-local javascript-indent-level n) ; javascript-mode
  (setq-local js-indent-level n) ; js-mode
  (setq-local js2-basic-offset n) ; js2-mode, in latest js2-mode, it's alias of js-indent-level
  (setq-local web-mode-markup-indent-offset n) ; web-mode, html tag in html file
  (setq-local web-mode-css-indent-offset n) ; web-mode, css in html file
  (setq-local web-mode-code-indent-offset n) ; web-mode, js code in html file
  (setq-local css-indent-offset n) ; css-mode
  )

(defun my-office-code-style ()
  (interactive)
  (message "Office code style!")
  ;; use tab instead of space
  (setq-local indent-tabs-mode t)
  ;; indent 4 spaces width
  (my-setup-indent 4))

(defun my-personal-code-style ()
  (interactive)
  (message "My personal code style!")
  ;; use space instead of tab
  (setq indent-tabs-mode nil)
  ;; indent 2 spaces width
  (my-setup-indent 2))

(defun my-setup-develop-environment ()
  (interactive)
  (let ((proj-dir (file-name-directory (buffer-file-name))))
    ;; if hobby project path contains string "hobby-proj1"
    (if (string-match-p "hobby-proj1" proj-dir)
        (my-personal-code-style))

    ;; if commericial project path contains string "commerical-proj"
    (if (string-match-p "commerical-proj" proj-dir)
        (my-office-code-style))))

;; prog-mode-hook requires emacs24+
(add-hook 'prog-mode-hook 'my-setup-develop-environment)
;; a few major-modes does NOT inherited from prog-mode
(add-hook 'lua-mode-hook 'my-setup-develop-environment)
(add-hook 'web-mode-hook 'my-setup-develop-environment)

You can use `prog-mode-hook` and `buffer-file-name` to do the per project setup easily.

For any new programming language not covered in this post, there are two steps:

  • Know your major-mode by pressing `ALT-X major-mode ENTER`. Say you got "org-mode".
  • Google "Emacs org-mode indent offset"

Tips (OPTIONAL)

  • Please note the first paramter of `string-match-p` is actually regular expression. So you can write regular expression like "\\(/proj1\\|work.org\\)"
  • Emacs gives you full freedom to setup project. If you prefer per computer setup instead of per project setup, you only need re-write `my-setup-develop-environment',
(defun my-setup-develop-environment ()
  (interactive)
  (let ((hostname (with-temp-buffer
                    (shell-command "hostname" t)
                    (goto-char (point-max))
                    (delete-char -1)
                    (buffer-string))))

  (if (string-match-p "home-pc" hostname)
      (my-personal-code-style))

  (if (string-match-p "office-pc" hostname)
      (my-office-code-style))))
  • Instead of press TAB manually to indent code line by line, you can select a region and press `Alt-x indent-region ENTER`
  • Because I use Evil, indenting the whole file is as simple as pressing `gg=G`

Effective code navigation for web development

UPDATED: <2020-03-27 Fri>

I use AngularJS as an example. The technique is actually generic and not limited to Angular Only.

Problem

In Angular application, a function is defined in a javascript file and called in a html file.

Javascript file:

<div ng-controller="MyController" ng-init="init()">
  <button ng-click="onClick()"></button>
</div>

html file:

angular.module['myapp'].controller('MyController', function ($scope, $http) {

  $scope.init = function () {
  };

  $scope.onClick= function() {
  };
});

As you can see, the keyword we are interested is just a string, like "MyController". The only way to extract it is regular expression.

In this article, I focus on how I combine Emacs and Ctags. If you use other text edit like Visual Studio Code, you still can use Ctags.

Solution

Ctags is good at parsing files with regular expression.

I use Ctags because:

  • New web frameworks keep popping up. I don't want to waste time&money on upgrading IDE from time to time
  • As a freelancer, I have to deal with in-house frameworks which IDE developers NEVER heard of
  • Setup of ctags and text editors is easy. Once the first project is set up, others are just five minutes of copy&paste jobs.

Old solution

Here is my ~/.ctags.

Please note on Windows, you can place ".ctags" anywhere. Then create an environment variable named "HOME" whose value is the parent directory of ".ctags".

Run ctags -e -R -f /app/path/TAGS at least once to generate tags file named "TAGS".

You can use builtin Emacs command find-tag to find tag.

If you prefer Helm UI, install it and M-x helm-etags-select instead.

Please install the latest helm because I enhanced helm-etags-select.

Latest solution (recommended)

I have developed some ctags packages on code completion and code navigation. No manual setup is needed. Everything just works out of box. My packages are very popular in the Emacs community right now,

Here are my packages,

Usage is simple,

  • Install Ctags and configure it with ~/.ctags
  • Install company-ctags which dependent on company-mode
  • Install counsel-etags and start use it immediately like M-x counsel-etags-find-tag-at-point
  • Done. The tags file is automatically created and updated from time time