我的 Vim 到 Emacs 到 Evil 之路

ceyes發表於2015-07-20

半個多月前,緣由 Vim 的一點小需求無法實現,我開始嘗試 Emacs。從初窺門徑到配置出完全滿足我的一切,中途曾一度不可自拔,工作之餘、入睡之前都在看 Emacs 的文件資料。發現我的控制慾特別強,不達目的不願罷休。好在 Emacs 的確是個強大的平臺,不負我望,在積累了一定的 elisp 基礎之後就很快突破瓶頸,輕鬆定製出自己的編輯器。折騰 Emacs 就是 “山重水複疑無路,柳暗花明又一村”,時而線索終端而疲憊不堪,時而找到突破而滿是成就感。總的來說 Emacs 的許多功能都無法 work out of the box,很多地方缺少面對新手的文件。只有熟悉了 Emacs 的理念,學習了 elisp 這門語言後再去 hack 他,才能為我所用。像 Gentoo 一樣,Emacs 非常適合以及需要折騰,因為他只是個 Platform,而非 Editor。

從 Vim 到 Emacs 到 Evil

Why

我使用 Linux 和 Vim 已有 5 年多,非常喜歡這種工作方式,大三的資料結構作業就是拿 Vim 編輯,用 gcc 編譯的。工作後繼續用 Vim 寫程式,其配置檔案也在同事 Linux 玩家的影響下越來越強大。我也 Vim 化了我的大多軟體,mutt, firefox, ranger。我很喜歡 hjkl 來移動游標,但有個地方不適用 —— Bash command line,雖然可以 set -o vi 把 bash 設定成 vi 方式的鍵繫結,但是這種方式來移動游標和操作命令很不方便,遠不及預設的 emacs 方式高效(其實叫Readline shortcuts, 很多鍵繫結和 emacs 是一樣的)。

Readline shortcuts 在行編輯的時候非常方便,比如 Ctrl-a 移動到行首,Ctrl-e 移動到行尾,Ctrl-p 上一條命令,Alt-b 後退一個單詞,Alt-d 向前刪除一個單詞……,這些對於需要長時間工作在CLI,敲大量命令的 Linuxer 或 Engineer 是非常方便和高效的。若是 Vi 方式則需要先 Esc 進入 Normal 模式,用 0,$,b,w,e,h,l 來移動游標,再 i,a,dw,x 來編輯或刪除。雖然雙手都不用離開主鍵盤區,但顯然 Vi 方式在這種行範圍內編輯修改的操作要複雜的多。 而且大多數系統或軟體都會在編輯內容的時候支援 Readline shortcuts,當發現配置 Cisco 裝置時也可以用這些 shortcuts,那是多麼的舒服和諧。所以雖然我之前完全沒有學過 Emacs,但在長時間的 CLI 磨鍊之下,早已熟練掌握其大量快捷鍵。

一很典型的場景是寫配對括號或引號時,我傾向於先寫配對的符號再退回來填內容。比如 int main() {} 或git commit a.sh -m “fix xxx” 用 Emacs 可以直接 Ctrl-b 和 Ctrl-f 來移動游標。而 Vi 則需要用方向鍵或不斷的模式切換來實現相同需求。所以我喜歡在編輯的時候使用 Emacs 方式,而在檔案瀏覽和游標選擇的時候使用 Vi 方式,各取其長,這就是我的最終需求。

Solutions

我最早的方案是給 Vim 新增鍵繫結,如下:

" emacs commands in insert mode

" ctrl-b/f
imap <C-b><Left>
imap <C-f><Right>" alt-b/f
" note the alt-b is generate by ctr-v then press alt-b ...
imap ^[b   <S-Left>
imap ^[f   <S-Right>

imap <C-a><Home>
imap <C-e><End>

imap <C-d><Del>
imap <C-h><BS>
imap ^[d   <c-o>de
imap <C-w><c-o>db
imap <C-u><c-o>d^
imap <C-k><c-o>d$

這些配置是可以工作的,但是有點小副作用,就是在 Insert 模式下按 Esc 進入 Normal 模式 想做些移動或刪除動作,若不幸用到 b/f/d 這幾個鍵會再次回到 Insert 模式,這是因為在大多終端下 Alt+x 和 先按 Esc 再按 x 的效果是一樣的,且 Vim 沒法區分他們。

之後漸漸地對 Emacs 這種無模式的編輯方式很感興趣,於是想嘗試下真正的 Emacs ,想體驗完全按 Emacs 的方式來工作,所謂 “得不到的永遠在騷動……”。完全是好奇心作祟,也當作挑戰吧,畢竟有時候能接觸到不一樣的強大的東西、能讓自己換種思維方式是蠻有趣的。所以這期間我看了不少 Emacs 的教程,練習他的命令、快捷鍵和操作。也試過 Emacs 下的 Viper 和 Evil,但我總覺得應該儘量先入鄉隨俗,不然怎能體會到其理念和樂趣。Org-mode 是最先牢牢吸引住我的一個功能,那時的想法是繼續用 Vim 作為主編輯器,把 Emacs 當作 Org-mode 工具和平時折騰的樂趣。

決定完全投入 Emacs 是在看到 Reddit 上的一個問答之後: “Switching from Vim. Should I use Emacs + Evil or just straight Emacs?” 。 1 樓的回答讓我頓悟——“Emacs is a platform. Its keybindings has nothing to do with its spirit.” 是的,Emacs 只是個強大的平臺,提供各種定製來滿足每個人的不同需求。所以 Thanks Evil, 我已把 Emacs 打造成了理想的 “Vim 化的 Emacs Editor” ,我可以縱情使用更方便的方式來工作。然後我還在 .bashrc 裡新增了alias vi='emacs -nw',我不要糾結他是 Vim, Emacs 還是 Evil,他只是我的編輯器。

Emacs 的定製性非常好,因為每個操作每個按鍵都是一條命令,加上 elisp 這門真正的語言,需求可以實現得很完美,尤其是 hook 非常強大。

一些意外收穫:

  • 寫中文更方便,避免了在編輯過程在需要不斷的模式切換+輸入法切換(雖然在 Vim 下有 fcitx.vim 可以緩解這個問題)
  • org-mode - 記筆記、記錄瑣事和管理task很方便。
  • elisp - 粗略學習了一門新語言,感受了下這個 lisp 的方言,後者很被《黑客與畫家》的作者推崇。
  • 系統地學習了 Emacs 的快捷鍵,發現了些之前沒意識到的規律和技巧。比如 alt-backspace 向後刪單詞更精確。

一些選擇:

  • 很多人建議互換CapsLock和Ctrl以避免"Emacs pinky"。我沒有換,因為我有 Evil 和 “壓掌大法”。
  • Emacs 在 23.1 之後支援以 daemon 執行以提高啟動速度,我不打算以他做為主要執行方式,一是他會造成很多不好解決的問題,比如 daemon 啟動在 terminal 下,然後 emacsclient 執行在 GUI 或 screen 會有些麻煩; 二是我覺得我的 Emacs 啟動速度還可以接受。
  • 很多人喜歡在 Emacs 裡 do anything, 比如上 IRC 和 發郵件,我沒有心動,因為我覺的 irssi 和 mutt 都很好。
  • 很多人喜歡藉助 Emacs daemon 來代替 screen 或 tmux,我依然堅持 screen,因為我習慣了用好多 screen 管理著不同的終端,不想折騰到這個層面。
  • 很多人很喜歡 Emacs 的分屏功能,把他當作視窗管理器來使用。我沒有這個念頭,因為我有強大的 Awesome WM。
  • ctrl-w, ctrl-u 這兩組快捷鍵在 bash 和 emacs 的功能完全不一樣,我沒有調整他們在 Emacs 裡對應的命令,而是選擇避免在 bash 裡用這兩組鍵,前者用 alt-b + alt-d 或 alt-backspace 來代替,後者用 ctrl-a + ctrl-k 來代替。因為我想用最簡單的方式相容。
  • 我主要是以 -nw 的方式啟動 Emacs,雖然他在終端下有些問題。

一些相關的調整:

  • Screen: ctrl-a 這麼重要的按鍵有衝突,不得不調整,我把它換成了 ctrl-j。因為 ctrl-j 在大多 mode 下的功能和回車是一樣的,除了在 Lisp Interaction mode 下的作用是執行當前lisp命令,這個小犧牲是可以的。(我的 commit ba2a73)
  • Awesome WM: 同樣為 Emacs 讓路,調整了些 alt 相關的快捷鍵。 (我的 commit 84060e)

Configurations

我的配置檔案在Github上:https://github.com/ceyes/dotfiles/tree/master/.emacs.d 一些重要的配置如下:

1、Evil, extensible vi layer for Emacs

預設配置完全模擬 Vim,除了用 Ctr-z 來切換模式。我調整成了在 Insert 模式下恢復 Emacs 鍵繫結,用 Esc 退到 Normal 模式。 參考了 https://gist.github.com/kidd/1828878 和 http://askubuntu.com/questions/99160/how-to-remap-emacs-evil-mode-toggle-key-from-ctrl-z

;; Enable evil
(setq evil-toggle-key "")   ; remove default evil-toggle-key C-z, manually setup later
(setq evil-want-C-i-jump nil)   ; don't bind [tab] to evil-jump-forward
(require 'evil)
(evil-mode 1)

;; remove all keybindings from insert-state keymap, use emacs-state when editing
(setcdr evil-insert-state-map nil)

;; ESC to switch back normal-state
(define-key evil-insert-state-map [escape] 'evil-normal-state)

;; TAB to indent in normal-state
(define-key evil-normal-state-map (kbd "TAB") 'indent-for-tab-command)

;; Use j/k to move one visual line insted of gj/gk
(define-key evil-normal-state-map (kbd "<remap> <evil-next-line>") 'evil-next-visual-line)
(define-key evil-normal-state-map (kbd "<remap> <evil-previous-line>") 'evil-previous-visual-line)
(define-key evil-motion-state-map (kbd "<remap> <evil-next-line>") 'evil-next-visual-line)
(define-key evil-motion-state-map (kbd "<remap> <evil-previous-line>") 'evil-previous-visual-line)

2、Solarized Colorscheme for Emacs,

Solarized 是我最喜歡的配色方案,終端工作和寫程式碼很舒服。但是發現在我的終端(rxvt-unicode-256color)下顯示不正常(issue #62), Workaround 是設定環境變數 TERM=xterm,所以我在 .bashrc 新增了些 alias:

alias emacs='TERM=xterm emacs'  # workaround for emacs-color-theme-solarized issue #62
alias emacsclient='TERM=xterm emacsclient'

還一問題是該配色在 emacsclient 下的顯示也不正常(issue #60), Workaround 如下:

;; solarized color theme
(add-to-list 'custom-theme-load-path "~/.emacs.d/emacs-color-theme-solarized/")
(load-theme 'solarized-dark t)

;; Workaround broken solarized colours in emacsclient. Issue #60
(if (daemonp)
    (add-hook 'after-make-frame-functions
          (lambda (frame)
        (select-frame frame)
        (load-theme 'solarized-dark t)))
      (load-theme 'solarized-dark t))

Update: 這兩個 issue 已經被修復,在 rxvt-unicode-256color 和 screen-256color 都表現的很好,所以我已去掉了上面的 workarounds。

3、Dynamic title

我喜歡讓 terminal 的 title 實時顯示 Emacs 正在編輯的檔案,emacswiki 的 FrameTitle 一文有介紹如何藉助 xterm-title.el 設定 Xterm 的 title。不過我採用的是直接向 terminal 傳送轉義碼的方案,支援 xterm/urxvt 和 screen。程式碼如下:

;; Automatically set screen title
;; ref http://vim.wikia.com/wiki/Automatically_set_screen_title
;; FIXME: emacsclient in xterm will have problem if emacs daemon start in screen
(defun update-title ()
  (interactive)
  (if (getenv "STY")    ; check whether in GNU screen
      (send-string-to-terminal (concat "/033k/033/134/033k" "Emacs("(buffer-name)")" "/033/134"))
    (send-string-to-terminal (concat "/033]2; " "Emacs("(buffer-name)")" "/007"))))
(add-hook 'post-command-hook 'update-title)

4、Use xsel to access the X clipboard

終端下的 Emacs 訪問系統剪下板,方便不同程式間的複製貼上,fromhttps://hugoheden.wordpress.com/2009/03/08/copypaste-with-emacs-in-terminal/

;; Use xsel to access the X clipboard
;; From https://hugoheden.wordpress.com/2009/03/08/copypaste-with-emacs-in-terminal/
(unless window-system
 (when (getenv "DISPLAY")
   ;; Callback for when user cuts
   (defun xsel-cut-function (text &optional push)
     ;; Insert text to temp-buffer, and "send" content to xsel stdin
     (with-temp-buffer
       (insert text)
       ;; Use primary the primary selection
       ;; mouse-select/middle-button-click
       (call-process-region (point-min) (point-max) "xsel" nil 0 nil "--primary" "--input")))
   ;; Call back for when user pastes
   (defun xsel-paste-function()
     ;; Find out what is current selection by xsel. If it is different
     ;; from the top of the kill-ring (car kill-ring), then return
     ;; it. Else, nil is returned, so whatever is in the top of the
     ;; kill-ring will be used.
     (let ((xsel-output (shell-command-to-string "xsel --primary --output")))
       (unless (string= (car kill-ring) xsel-output)
     xsel-output)))
   ;; Attach callbacks to hooks
   (setq interprogram-cut-function 'xsel-cut-function)
   (setq interprogram-paste-function 'xsel-paste-function)))

5、Paste mode for Emacs

模仿 Vim 的 :set paste, 這是我最喜歡的複製貼上方式——滑鼠選中即複製,滑鼠中鍵貼上。不過在終端下,Vim/Emacs 不識別滑鼠中鍵(沒有特別編譯和設定的情況),被複制的內容會被按照字元依次輸入的方式送入終端。然後 Vim/Emacs 會“智慧”地把傳入內容補全的亂七八糟、把格式縮排的錯亂不堪。Vim下開啟 “paste mode” 可以解決這個問題,但是 Emacs 沒有“paste mode”,所以得自己實現,Fromhttp://stackoverflow.com/questions/18691973/is-there-a-set-paste-option-in-emacs-to-paste-paste-from-external-clipboard

;; Mimic Vim's set paste
;; From http://stackoverflow.com/questions/18691973/is-there-a-set-paste-option-in-emacs-to-paste-paste-from-external-clipboard
(defvar ttypaste-mode nil)
(add-to-list 'minor-mode-alist '(ttypaste-mode " Paste"))
(defun ttypaste-mode ()
  (interactive)
  (let ((buf (current-buffer))
    (ttypaste-mode t))
    (with-temp-buffer
      (let ((stay t)
        (text (current-buffer)))
    (redisplay)
    (while stay
      (let ((char (let ((inhibit-redisplay t)) (read-event nil t 0.1))))
        (unless char
          (with-current-buffer buf (insert-buffer-substring text))
          (erase-buffer)
          (redisplay)
          (setq char (read-event nil t)))
        (cond
         ((not (characterp char)) (setq stay nil))
         ((eq char ?/r) (insert ?/n))
         ((eq char ?/e)
          (if (sit-for 0.1 'nodisp) (setq stay nil) (insert ?/e)))
         (t (insert char)))))
    (insert-buffer-substring text)))))

6、Setup smart-mode-line

用以定製漂亮的狀態列,Vim 下我用的是 powerline。Emacs 版的 powerline 無法用在終端下,所以找到了這個 smart-mode-line。

;; Smart mode-line
(setq sml/name-width        40
      sml/line-number-format    "%4l"
      sml/mode-width        'full
      sml/themea        'dark
      sml/no-confirm-load-theme t)
(require 'smart-mode-line)
(sml/setup)

;; Hidden minor-mode, by rich-minority
(setq rm-excluded-modes
      '(" Guide"            ;; guide-key mode
    " hc"               ;; hardcore mode
    " AC"               ;; auto-complete
    " vl"               ;; global visual line mode enabled
    " Wrap"             ;; shows up if visual-line-mode is enabled for that buffer
    " Omit"             ;; omit mode in dired
    " yas"              ;; yasnippet
    " drag"             ;; drag-stuff-mode
    " VHl"              ;; volatile highlights
    " ctagsU"           ;; ctags update
    " Undo-Tree"            ;; undo tree
    " wr"               ;; Wrap Region
    " SliNav"           ;; elisp-slime-nav
    " Fly"              ;; Flycheck
    " PgLn"             ;; page-line-break
    " GG"               ;; ggtags
    " ElDoc"            ;; eldoc
    " hl-highlight"         ;; hl-anything
    ))

7、emacs-vim-modeline

Read file’s vim modeline to set Emacs’s file local variable. 很讚的外掛

modeline 即在檔案中告訴編輯器來啟用/調整一些設定的內容,如檔案型別、tab 寬度等等。 比如我喜歡在 common 指令碼里加上 # vim: sts=4 sw=4 et 來保持程式碼風格一致。

Vim 把這些內容叫 modeline,不過這個詞在 Emacs 裡表示的是狀態列,所以 Emacs 稱之為 file ‘s local variable。兩者寫法截然不同,所以這個外掛很好的解決了這個問題——讀取 Vim 的 modeline 來設定 Emacs。因此我能非常平滑地切換到 Emacs 繼續編輯之前的程式碼。

Notes:

References:

相關文章