从 Vim 到 Emacs 到 Evil

2015-07-20 11:13


半个多月前,缘由 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 向后删单词更精确。

一些选择:

  • 很多人建议互换CapsLockCtrl以避免"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: