Vim
首发于Vim
vim 中文输入解决方案

vim 中文输入解决方案

做为一个 vimer,想必你也有有过这样的遭遇:在 normal 模式下输入 a 想进入插入模式,结果却发现当前输入法处于中文输入状态,于是进入了尴尬的组合输入状态:

停顿半秒之后你敲了下 delete 然后切回英文输入按下 a 进入插入模块,再切换回中文状态开始正常的中文输入。

难道说 vim 不能更聪明些吗?

当然可以,首先要介绍下 smartim 这个插件,它可以在离开插入状态记录同时切换到默认输入法,然后在下次进入插入模式后自动切换为上次插入状态使用的输入法。我们还可以做的更好一些:按键时判定输入状态,如果是 normal 模块并且处于输入法状态,则将按键值直接发送给 vim 同时阻止原来的输入法生效,这样我们就能做到保持中文输入法同时直接进入插入模式

我使用了基于 electronneovim 提供的 RPC 调用实现的 neoclide-client 这个模块来实现这个功能。

监控当前系统输入法

进行系统调用获取输入法是有时间消耗的,如果每次 normal 模式按键都去获取则必然导致输入的延迟,首先想到的做法是监听 input 元素的 compositionstart 和 compositionend 来判定输入法状态,然而这种办法并不可行,因为 compositionstart 事件实在 keyDown 之后才会触发,此时输入法已经开始起作用了,而我们必须在 keyDown 时获取到当前的输入法状态。所以需要系统提供对应的接口,keyboard-layout 这个模块为我们提供了一个监听接口,只需要简单调用就可以做到同步输入法:

let keyboardLayout = ''

KeyboardLayout.observeCurrentKeyboardLayout(layout => {
  keyboardLayout = layout
  const ev = new CustomEvent('layoutChange', {
    detail: layout
  })
  window.dispatchEvent(ev) // 便于其它模块监听
})

export function imeRunning() {
  return keyboardLayout && keyboardLayout !== 'com.apple.keylayout.US'
}

仅做了针对 Mac 的处理

设置系统输入法

因为没找到可用的 node 模块,所以我做了 imselect 这个使用了一点 Object-C 的 node 原生模块。

export function defaultIM() {
  if (keyboardLayout && keyboardLayout !== 'com.apple.keylayout.US') {
    imselect.selectMethod()
    return true
  }
  return false
}

在 onKeyDown 事件使用的代码:

    if (mode == 'normal' && imeRunning() &&
      !ctrlKey &&
      !metaKey &&
      !altKey) {
      event.preventDefault()
      if (['a', 'A', 'i', 'I', 'o', 'O'].indexOf(event.key) === -1) {
          setImmediate(() => defaultIM())
      }
      this.inputToNeovim(event.key, event)
      return
    }

监听 vim 模式变化

我们希望 vim 在 normal 模式下总是自动切换到系统的输入法,然而 vim 仅提供了 InsertLeave,并没有的 CmdlineLeave 事件让我们监听,譬如说我们使用 / 搜索中文,回到 normal 模式还会是中文输入法状态。neovim 提供的 RPC 接口也不会给我们返回 cmdline 这个状态,所以我暂时只能对 neovim 的源码做一点修改:

diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c
index 56b41f1..9178538 100644
--- a/src/nvim/api/ui.c
+++ b/src/nvim/api/ui.c
@@ -271,6 +271,8 @@ static void remote_ui_mode_change(UI *ui, int mode)
     ADD(args, STRING_OBJ(cstr_to_string("insert")));
   } else if (mode == REPLACE) {
     ADD(args, STRING_OBJ(cstr_to_string("replace")));
+  } else if (mode == CMDLINE) {
+    ADD(args, STRING_OBJ(cstr_to_string("cmdline")));
   } else {
     assert(mode == NORMAL);
     ADD(args, STRING_OBJ(cstr_to_string("normal")));
d然iff --git a/src/nvim/ui.c b/src/nvim/ui.c
index eb50041..3e31b90 100644
--- a/src/nvim/ui.c
+++ b/src/nvim/ui.c
@@ -537,6 +537,8 @@ static void ui_mode_change(void)
     mode = REPLACE;
   else if (State & INSERT)
     mode = INSERT;
+  else if (State & CMDLINE)
+    mode = CMDLINE;
   else
     mode = NORMAL;
   UI_CALL(mode_change, mode);

然后监听 RPC 传来的 mode_change 事件即可。

更新: 这部分代码已经合并到 neovim 的 master 分支上了

Focus 事件监听

Mac 提供了针对应用的输入法记录功能,可以自动还原 app 之前的输入法状态,建议开启。

记录搜索模式输入法

通过在 onKeyDown 中我们判定 event.key (这是个比较新的 API, 很多浏览器并不支持) 为 ‘?’ 或者 ‘/’ 同时模式为 normal 可以判定 vim 即将进入搜索模式,监听 mode_change 可在模式变为其它模式时保存当前输入法,下次进入后自动复原。

    p.on('mode_change', mode => {
      const {searching} = proxy
      const curMode = proxy.mode
      if (mode != 'cmdline' && searching) {
        util.saveCommandIm()
        store.dispatch(A.toggleSearch(false))
      }
      if (curMode != 'insert' && mode == 'normal') {
        // works with smartim
        util.defaultIM()
      }
      if (mode == 'cmdline' && searching) {
        util.selectCommandIm()
      }
      store.dispatch(A.changeMode(mode))
    })

醒目颜色提醒

我们还需要更加便捷的知道当前所处的输入法状态,所以我使用了贴心的黄色鼠标背景:

neoclide-client 这个模块尽管已经完成,但它实际只是为了neoclide 提供编辑的模块,更多的主要功能还在开发中 :p

Happy vimming!

文章被以下专栏收录

    最前沿的Vim资讯,实用的Vim配置技巧,操作技巧,插件推荐.