做为一个 vimer,想必你也有有过这样的遭遇:在 normal 模式下输入 a 想进入插入模式,结果却发现当前输入法处于中文输入状态,于是进入了尴尬的组合输入状态:
停顿半秒之后你敲了下 delete 然后切回英文输入按下 a 进入插入模块,再切换回中文状态开始正常的中文输入。
难道说 vim 不能更聪明些吗?
当然可以,首先要介绍下 smartim 这个插件,它可以在离开插入状态记录同时切换到默认输入法,然后在下次进入插入模式后自动切换为上次插入状态使用的输入法。我们还可以做的更好一些:按键时判定输入状态,如果是 normal 模块并且处于输入法状态,则将按键值直接发送给 vim 同时阻止原来的输入法生效,这样我们就能做到保持中文输入法同时直接进入插入模式。
我使用了基于 electron 和 neovim 提供的 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.cindex 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!