從 Vim 到 Emacs 到 Evil
半個多月前,緣由 Vim 的一點小需求無法實現,我開始嘗試 Emacs。從初窺門徑到配置出完全滿足我的一切,中途曾一度不可自拔,工作之余、入睡之前都在看 Emacs 的文檔資料。發現我的控制欲特別強,不達目的不愿罷休。好在 Emacs 的確是個強大的平臺,不負我望,在積累了一定的 elisp 基礎之后就很快突破瓶頸,輕松定制出自己的編輯器。折騰 Emacs 就是 “山重水復疑無路,柳暗花明又一村”,時而線索終端而疲憊不堪,時而找到突破而滿是成就感。總的來說 Emacs 的許多功能都無法 work out of the box,很多地方缺少面對新手的文檔。只有熟悉了 Emacs 的理念,學習了 elisp 這門語言后再去 hack 他,才能為我所用。像 Gentoo 一樣,Emacs 非常適合以及需要折騰,因為他只是個 Platform,而非 Editor。
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$</pre>
這些配置是可以工作的,但是有點小副作用,就是在 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 向后刪單詞更精確。 </ul>
- 很多人建議互換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,雖然他在終端下有些問題。 </ul>
- Screen: ctrl-a 這么重要的按鍵有沖突,不得不調整,我把它換成了 ctrl-j。因為 ctrl-j 在大多 mode 下的功能和回車是一樣的,除了在 Lisp Interaction mode 下的作用是運行當前lisp命令,這個小犧牲是可以的。(我的 commit ba2a73)
- Awesome WM: 同樣為 Emacs 讓路,調整了些 alt 相關的快捷鍵。 (我的 commit 84060e) </ul>
一些選擇:
一些相關的調整:
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)</pre>
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))</pre>
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 ))</pre>
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:
- Tab 的功能和配置往往讓人迷惑,可以參考 Understanding GNU Emacs and Tabs 和http://ergoemacs.org/emacs/emacs_tabs_space_indentation_setup.html
- 很多按鍵在終端下是無法被正確識別的,比如 M-<up>, C-RET, S-RET。 這在終端下使用 org-mode 和 markdown-mode 會有不少麻煩。詳見 http://orgmode.org/manual/TTY-keys.html#TTY-keys 和http://stackoverflow.com/questions/3528713/how-does-one-send-s-ret-to-emacs-in-a-terminal
- Emacs 的 keybind 有許多復雜的機制,自己配置的時候難免踩到坑,這幾篇文檔很有幫助:
http://ergoemacs.org/emacs/emacs_key_notation_return_vs_RET.html
http://ergoemacs.org/emacs/keyboard_shortcuts_examples.html
http://ergoemacs.org/emacs/keyboard_shortcuts.html - 我遇到不少功能都在終端下無法正常工作,所以配置的時候最好用 CLI 和 GUI 一起 debug。
- Gentoo 編譯帶 X 的 Emacs 需要添加 USE "gsettings xft",否則找不到系統字體。 </ul>
- 入門最好的教材就是自帶的 Emacs 快速指南: c-h t
- 查詢某個鍵綁定可通過:c-h k
- Xah,李殺,一個 Emacs 狂熱分子,他的文章很有學習價值:http://ergoemacs.org/emacs/emacs.html
- EmacsWiki: http://www.emacswiki.org/ </ul> 來源: http://ceyes.github.io/2015-01/from-Vim-to-Emacs/