我的 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$
這些配置是可以工作的,但是有點小副作用,就是在 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:
- 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”,否則找不到系統字型。
References:
- 入門最好的教材就是自帶的 Emacs 快速指南: c-h t
- 查詢某個鍵繫結可通過:c-h k
- Xah,李殺,一個 Emacs 狂熱分子,他的文章很有學習價值:http://ergoemacs.org/emacs/emacs.html
- EmacsWiki: http://www.emacswiki.org/
相關文章
- 回憶Emacs 和Vim 的學習之路 – v0.0.9Mac
- 從Delphi到Lazarus——我的程式設計之路程式設計
- 我的 .emacs(轉)Mac
- vim從入門到精通
- 從程式碼到產品,我的IT職業成長之路
- linux環境vim升級到vim74Linux
- 我知道到的HTTPHTTP
- 從美術生到程式設計師轉型之路【我的故事】程式設計師
- 從BST到LSM的進階之路
- vim 填坑之路
- 我是怎樣使用 Emacs 的Mac
- 3 個可以替代 Emacs 和 Vim 的文字編輯器Mac
- 從0到1,小白的前端摸索之路前端
- Servlet 到 Spring MVC 的簡化之路ServletSpringMVC
- 小白從零到AIoT之路(前言)AI
- vim從入門到棄坑:基礎指令的歸類
- vim複製文字到系統貼上板
- web前端從入門到精通的自學之路Web前端
- 我從Superfish事件中學到的事件
- 我感覺到的前端變化前端
- 我的第一個Emacs擴充套件Mac套件
- 從學生到遊戲開發者: 我學到的五件事遊戲開發
- Spring MVC 到 Spring Boot 的簡化之路MVCSpring Boot
- 單體到微服務架構的涅槃重生之路?微服務架構
- React 從入門到進階之路(七)React
- React 從入門到進階之路(八)React
- React 從入門到進階之路(九)React
- 從前端到全端:JavaScript逆襲之路前端JavaScript
- IDC銷售:從小白到高手的荊棘之路
- 不要再勸導菜鳥程式猿使用vim或者Emacs了Mac
- 我的css之路CSS
- 我的Oracle之路Oracle
- Flask框架從入門到精通之路由(三)Flask框架路由
- Python 從入門到進階之路(七)Python
- Python 從入門到進階之路(三)Python
- Python 從入門到進階之路(四)Python
- Python 從入門到進階之路(五)Python
- 從區塊鏈到數字貨幣之路區塊鏈