自动切换 vim 中文输入法

2021-07-03 ⏳4.5分钟(1.8千字) 🕸️

在 vim 输入中文需要先切换到 insert 模式,再切换到中文输入法,然后才能开始输入内容。如果需要返回 normal 模式,则又需要将输入法切回英文,不然无法使用 normal 下的快捷键。这是中文 vim 用户最主要的问题。今天就给大家分享一下我个人的解决思路。

思路很简单,就是想办法让 vim 从 insert 模式切换到 normal 模式的时候自动切换输入法。

我用的是 mac 平台。怎么切换输入法呢?网上的教程大多基于xkbswitchsmartim这两个项目。前一个的最新提交是在2018年7月,后一个是在2017年5月,都比较老了,不一定适应最新的 mac 系统。另一方面,这两个工具都是用 objective-c 编写的,需要使用 xcode 编译,不是很方便。那能不能使用脚本实现输入法的切换呢?答案是可以的,我们需要用到 AppleScript。

思路也很简单,使用 AppleScript 模拟按键操作。比如,我的输入法切换快捷键是ctrl+空格,我就让 AppleScript 替我发送ctrl+空格就能实现输入法切换了,代码如下:

tell application "System Events"
	key code 49 using control down
end tell

在 AppleScript 中,空格键的编码是 49,其他编码可以参考这里

然后我们就可以测试了。点击右上角的三角形执行脚本。首次执行的时候系统会提示你授权, 一定要点 OK。如果不同意,后面操作有点麻烦。同意之后可以反复执行脚本,如果输入法正常切换说明没有问题。

下一步我们需要在命令行内运行 AppleScript。保存刚才的 AppleScript。我是保存在 /usr/local/opt/lv/ctrl+space.scpt。打开终端,执行:

osascript /usr/local/opt/lv/ctrl+space.scpt

这个时候系统又会提示授权,一定要同意。osascript 就是在命令行运行 AppleScript 的命令。

如果大家用 vim 打开 /usr/local/opt/lv/ctrl+space.scpt 会发现乱码,这是因为苹果的 Script Editor 会将 AppleScript 编译成二进制代码再保存。如果用同学只想用纯文本,可以这样做:

osascript \
	-e 'tell application "System Events"' \
	-e 'key code 49 using control down' \
	-e 'end tell'

但据我试验发现,二进行的版本速度会快一些。

有了切换输入法的工具,我还还需要想办法查询系统当前的输入是什么。如果本来就是英文输入法,那就不用切换了。我使用关键词”macos applescript get current input source” 搜索,找到了这个链接。核心思路如下:

#!/usr/bin/env bash

defaults read ~/Library/Preferences/com.apple.HIToolbox.plist \
AppleSelectedInputSources | \
grep '"KeyboardLayout Name" = ABC'

大意是从 ~/Library/Preferences/com.apple.HIToolbox.plist 读取当前输入法的信息,再通过 grep 判断是否为 ABC 键盘。我见过有一些同学没有添加 ABC 键盘,只有一种中文键盘,使用 CapsLock 或 Shift 来切换。如果是这样,就不能用我说的这种方法。不过还是我这种方法简单,建议大家都加一下 ABC 键盘。

把刚才那行代码保存到 /usr/local/bin/is_abc,并给予可执行权限,我们就能通过执行 is_abc 来检查当前是否使用英文输入法了。

到这里,所有准备工作已经就绪。我们开始写一点简单的 VimScript。

首先,我们需要判断 vim 在什么时候发生 normal 到 insert 或者 insert 到 normal 的切换。这个 vim 提供了 InsertEnter 和 InsertLeave 两个事件,我们只需要配置 autocmd 回调函数就好了:

autocmd InsertEnter * call AutoIM("enter")
autocmd InsertLeave * call AutoIM("leave")

Vim 从 normal 切换到 insert 模式的时候会触发 InsertEnter 事件,反之触发 InsertLeave 事件。在这里我们定义了 AutoIM 函数,并通过参数标明触发的事件。

在分析 AutoIM 的源码之前,我们需要解决一个小问题,即如何在 VimScript 中运行系统命令(也就是前面说的 osascript 和 is_abc)。

Vim 提供了两种方法。如果只是想执行命令而不想知道输出的内容,可以直接使用英文叹号!,比如执行前面的 AppleScript:

!osascript /usr/local/opt/lv/ctrl+space.scpt

通过这种方式执行命令,vim 会把结果直接输出到用户界面,不太好看。我们可以通过添加 silent 参数禁止 vim 打印命令输出的内容:

silent !osascript /usr/local/opt/lv/ctrl+space.scpt

如果希望获取命令输入的内容,则可以使用 system() 方法:

let out = system('is_abc')

is_abc 的输出结果会被赋值给变量 out

除此之外,我们还需要一个全局变量 g:lv_restore_last_im 来保存是否需要切换输入法。整个算法如下:

在 InsertLeave 事件回调中检查当前输入法是否为英文。如果不是,将全局变量 g:lv_restore_last_im 置一,表示后面切回插入模式时需要切换输入法,然后切换一次输入法回到英文状态;如果当前本来就是英文输入法,则需要清空 g:lv_restore_last_im

在 InsertEnter 事件回调中检查如果当前是英文输入法且 g:lv_restore_last_im 非空,则切换一次输入法。

整体的效果就是,进入插入模式,切换中文输入法,输入中文,切换到 normal 模式,自动切换回英文输入法,再换到 insert 模式时会自动切换到中文输入法。

如果你在 insert 模式中主动将输入法切换到英文,那后面切换模式的时候不会自动激活中文输入法。也就是说会记忆你在上一次 insert 模式所用的输入法。

AutoIM 的代码如下:

let g:lv_restore_last_im = 0

function! AutoIM(event)
	let is_abc = system('is_abc') != ''

	let need_switch_im = 0
	if a:event == 'leave'
		if !is_abc
			let g:lv_restore_last_im = 1
			let need_switch_im = 1
		else
			let g:lv_restore_last_im = 0
		end
	else " a:event == 'enter'
		if is_abc && g:lv_restore_last_im
			let need_switch_im = 1
		end
	end

	if need_switch_im 
		silent !osascript /path/to/ctrl+space.scpt
	end
endfunction

现总结一下思路。使用 AppleScript 模拟按键操作实现输入法切换(注意授权问题)。使用 defaults read 读取当前输入法信息。使用 VimScript 全局变量记录上次输入法状态。本文提供的方法虽然是针对 mac 平台,但同样也可以应用于 linux 等其他平台。不过大家需要自己想办法实现当前输入法状态查询功能,这个因平台而异(尤其是 linux 平台),需要具体问题具体分析。另外本文的方法要求一定要添加英文和中文两个键盘。如果没有英文键盘或者有多个键盘,也会出问题。但无论如何,它在我的系统上能跑。分享给大家,算是抛砖引玉。