emacs 28.0 windows版原生支持输入法的中英文切换

因为我是用evil的,emacs要是能像在gvim里一样自动切换输入法,就不需要如pyim或者emacs-rime之类package的了。

在windows的gvim里,输入法是可以根据模式自动切换的,按esc进入normal mode就切换为英文;在insert mode中,如果之前是中文,进入insert mode后会保持中文状态。windows的gvim中,测试了rime和微软拼音,都正常切换。不过neovim是不行的。看来没有全盘copy gvim的代码。

实现上,emacs只要原生提供一个输入法切换的函数,在evil切换mode时hook住,切换输入法状态就可以了。

windows中和输入法相关的dll是imm32.dll,它提供了 =ImmSetOpenStatus= 函数进行输入法中英文模式切换。可以用 =LoadLibrary= 动态加载imm32.dll,然后用 =GetProcAddress= 获取到函数地址进行调用。

需要在emacs中实现一个系统级elisp函数进行调用。但是由于lisp代码是在Lisp Thread(mainThread)中被调用,而输入法(gui相关的)是在emacs的windowsThread中调用。而 =ImmSetOpenStatus()= 必须是windowsThread调用才行。

新增emacs自定义的window message WM_EMACS_IME_STATUS,Lisp thread中发WM_EMACS_IME_STATUS消息给window thread。

src/w32term.h

#define WM_EMACS_IME_STATUS            (WM_EMACS_START + 26)
#define WM_EMACS_END                   (WM_EMACS_START + 27)

src/w32fns.c

static HWND w32_main_hwnd;

/* emacs ime status change flag, invoked in mainThread and windowsThread */
static int w32_ime_status_changed = 0;
static int w32_emacs_ime_status = 0;

w32_msg_pump() {
...
            /* hwnd =NULL的消息必须在w32_msg_pump()中处理. */
            case WM_EMACS_IME_STATUS:
              if (!set_ime_open_status_fn) {
                  break;
              }
              else {
                  if (w32_ime_status_changed) {
                      w32_ime_status_changed = 0;
                      HIMC context;
                      context = get_ime_context_fn (w32_main_hwnd);
                      if (!context) {
                          break;
                      }

                      set_ime_open_status_fn (context, w32_emacs_ime_status);
                      release_ime_context_fn (w32_main_hwnd, context);
                  }
              }
              break;
              
...
}

static void
w32_createwindow (struct frame *f, int *coords)
{
...
  FRAME_W32_WINDOW (f) = hwnd
    = CreateWindow (EMACS_CLASS, f->namebuf, f->output_data.w32->dwStyle,
		    left, top, rect.right - rect.left, rect.bottom - rect.top,
		    parent_hwnd, NULL, hinst, NULL);
  /* 保存输入法相关window的handle*/
  w32_main_hwnd = hwnd;
...
}


DEFUN ("w32-set-ime-open-status",
       Fw32_set_ime_open_status, Sw32_set_ime_open_status,
       1, 1, 0,
       doc: /* Set emacs IME open status on Windows. */)
  (Lisp_Object status)
{
    w32_ime_status_changed = 1;
    if (NILP (status)) {
      w32_emacs_ime_status = 0;
    }
    else {
      w32_emacs_ime_status = 1;
    }

    /* 其实post IME_NOTIFY 就可以,里面是什么参数都无所谓的,自定义一个message都可以。 */
   PostThreadMessage(dwWindowsThreadId, WM_EMACS_IME_STATUS, 0, 0);

    return Qnil;
}

Lisp

(defun emacs-ime-disable ()
  (w32-set-ime-open-status nil))

(defun emacs-ime-enable ()
  (w32-set-ime-open-status t))

(add-hook 'evil-insert-state-entry-hook 'emacs-ime-enable)
(add-hook 'evil-insert-state-exit-hook 'emacs-ime-disable)
23 个赞

这个有点厉害,我都不敢想这样的实现方式。。

可以在windows上试试,挺好玩的。代码改了2个文件,好几个地方,大概的实现逻辑在上面。

emacs本身就用imm32.dll实现输入法调用后的编码转换。连头文件都不需要增加。

fcitx.el 可以在不同的模式自动切换中英文输入法, linux 下需要 fcitx , mac 下通过 fcitx-remote 模拟,支持多种输入法。

可以参考 GitHub - cute-jumper/fcitx-remote-for-windows: A fake fcitx-remote for Windows 。但是需要自己编译个exe,emacs子进程调用,不喜欢emacs搞太多需要自己编译的package。而且看上面的link说,fcitx-remote有时候调用反应有点慢。

既然vim都能原生支持输入法中英文模式切换,emacs怎么能落后。

2 个赞

我觉得这个方法可能比 emacs-rime 这样的方式还要复杂,当然 emacs-rime 的前提是要用 rime。

这种方法如果搞的好,可以集成进emacs官方发布,那可就造福大众了。

不过我现在用emac-rime 已经很满足了,Rime是我认为目前能够兼顾三大平台最好的输入法了,而且定制性高。特别适合喜欢折腾的emacs用户😄

为这个,给你点个赞

1 个赞

既然是 Windows 的话可以试下 DLL injection。

360 会跳出来阻止吗 [quote=“LdBeth, post:9, topic:12350, full:true”] 既然是 Windows 的话可以试下 DLL injection。 [/quote]

:laughing:

不会,因为 windows ime 就是靠这个实现的

最开始就考虑了用module的方式,但是仍然需要改一点源代码,如原来在src/w32fns.c中用createWindow创建window的时候没有保存window的handle,所以需要改改代码,增加一个变量保存window的handle。ImmGetContext函数需要window handle。

另外,在第一个帖子说了,elisp代码都是在lisp thread中执行,没法在window thread执行,调用输入法的ImmSetOpenStatus函数无效。没找到其他的方法。所以都在emacs的源代码里改了。

可以在windows上支持多个输入法。但是rime的效果不如微软拼音。

使用rime的时候,无论中英文模式,调用ImmGetOpenStatus函数,都返回1。稍微翻了一下小狼毫的代码,怀疑 ImmGetOpenStatus 这些接口的api实现得不完整。所以返回值和微软拼音不一样。这样就没法在lisp里判断输入法的中英文模式,更好的控制模式切换。

使用微软拼音的时候,中文模式,调用ImmGetOpenStatus函数,返回1。英文返回0。而且insert mode是英文模式的话,从insert mode切换回normal mode,再切换回insert mode,不会自动变成中文模式。不知道微软拼音是怎么做到的。

感觉 tumashu 在 pyim 集成了 liberime 之后掀起了一波输入法折腾大潮

librime有python绑定,不知道用python写一个动态模块,性能好不好

这是好事,这么多人折腾,才有可能折腾出好东西

一起折腾啊。给pyim写了一个sqlite的后端。用了一段时间,感觉不错,但是用pyim时,中英文混合输入,感觉英文输入不如windows上安装的不流畅。输入的有中文和英文时,按m-j转换的时候,没法选择我想要的字。

而且用pyim配合liberime时,需要编译liberime、librime,稍微有点麻烦。emacs 28在编译liberime的时候需要用28的emacs-module.h。

其实我就想在evil模式切换的时候,自动把中英文模式切换了。干脆自己折腾一个。反正现在在windows用native-comp分支,代码早就给我改了好多地方了,不差输入法这点代码。

我个人建议,能合并到 emacs master的代码,尽量早点尝试合并。。。。 不然

请问如何提交commit?

反正先自己爽了再说。不知道有没有其他更优雅的方式实现输入法中英文切换。

一般就是签署gpl纸质协议后。发补丁给emacs-devel, 审核通过后就会合并