使用 nvim-cmp + rime-ls 配置 nvim 中文输入法

前言

在使用 nvim 的过程中,我们往往需要写一些文档性质的文件,而很多文档性质的文件需要中英文混合输入, 而如果我们使用系统的输入法进行操作会面临频繁切换中英文输入模式,或者需要频繁地使用回车或者上档键将 输入法中的英文字符上屏。

如果可以直接在 nvim 中使用输入法,那么将会大幅度提升我们的体验。在完成了相关的设置之后,当我使用 这套配置写下这篇文章的时候,总体的体验还是非常不错的。

本文将会介绍如何使用 nvim-cmp + rime-ls 完成 nvim 中实现中英文混合输入,以及过程中遇到的问题和解决 思路。

NOTE:目前我使用的这一套配置是我体验的很多配置中最好的,最符合日常输入法使用习惯的。

安装 rime 输入法

首先需要安装 rime 输入法,我使用的是 ubuntu ,因此我使用以下的命令进行安装:

sudo apt-get install -y ibus-rime

其他系统的安装方法可以参考 rime 的官方文档,这里不进行赘述。

由于我平时使用的是小鹤双拼输入法,因此我需要安装小鹤双拼的配置文件,可以使用以下的命令进行安装:

sudo apt-get install -y librime-data-double-pinyin

安装 rime-ls

rime-ls 是一个 rime 的后台服务,可以帮助我们在 nvim 中使用 rime 输入法。我们可以使用以下的命令进行安装:

sudo apt-get install -y clang librime-dev cargo
cd || exit 1
git clone https://github.com/wlh320/rime-ls.git
cd - || exit 1
cd ~/rime-ls || exit 1
cargo build --release || exit 1
cd - || exit 1

其中安装的 clang, librime-dev, cargo 是安装 rime-ls 所需要的依赖。上面的代码会将 rime-ls 安装 在家目录下,如果需要更改可以自行调整。

NOTE:因为使用的是 nvim-cmp + rime-ls 所以当然需要安装 nvim-cmp 了。除此之外,还需要 安装 lspconfig 用于配置 rime-ls。既然安装了 nvim-cmplspconfig 那么 cmp-nvim-lsp 也是需要 安装的。由于这几个是非常常用的插件,这里不详细介绍如何安装。

rime-ls 配置

如果你查看了 rime-ls 的官方文档,你会获得一份完成了大部分设置的配置。但是在使用官方文档中的配置的时候, 我遇到了许多困难,这里首先给出你能从官方文档中得到的代码:

local M = {}
-- 设置哪些文件开启 `rime-ls`
local rime_ls_filetypes = { 'markdown', 'vimwiki', 'copilot-chat' }

function M.setup_rime()
    vim.g.rime_enabled = true

    -- 修改 `lualine` 的样式,如果没有使用 `lualine` 可以不用这一段
    local function rime_status()
        if vim.g.rime_enabled then
            return 'ㄓ'
        else
            return ''
        end
    end
    require('lualine').setup({
        sections = {
            lualine_x = { rime_status, 'copilot', 'encoding', 'fileformat', 'filetype' },
        }
    })

    local lspconfig = require('lspconfig')
    local configs = require('lspconfig.configs')
    if not configs.rime_ls then
        configs.rime_ls = {
            default_config = {
                name = "rime_ls",
                -- 这里放置 `rime-ls` 的路径
                cmd = { vim.fn.expand'~/rime-ls/target/release/rime_ls' },
                filetypes = rime_ls_filetypes,
                single_file_support = true,
            },
            settings = {},
            docs = {
                description = [[
https://www.github.com/wlh320/rime-ls

A language server for librime
]],
            }
        }
    end

    local rime_on_attach = function(client, _)
        local toggle_rime = function()
            client.request('workspace/executeCommand',
                { command = "rime-ls.toggle-rime" },
                function(_, result, ctx, _)
                    if ctx.client_id == client.id then
                        vim.g.rime_enabled = result
                    end
                    -- 关闭或者开启的时候重新触发一次补全
                    if cmp.visible() then
                        if not vim.g.rime_enabled then
                            cmp.close()
                        end
                        cmp.complete()
                    end
                end
            )
        end
        -- 通过 `<c-space>` 来切换 `rime-ls` 的开关
        vim.keymap.set({ 'i', 'n' }, '<c-space>', toggle_rime)
    end

    local capabilities = vim.lsp.protocol.make_client_capabilities()
    -- `cmp-nvim-lsp` 支持额外的功能,如果没有这个插件,这一行可以删除。
    capabilities = require('cmp_nvim_lsp').default_capabilities(capabilities)

    lspconfig.rime_ls.setup {
        init_options = {
            enabled = vim.g.rime_enabled,
            -- `rime` 安装后方案所在目录
            shared_data_dir = "/usr/share/rime-data",
            -- `rime-ls` 的配置目录
            user_data_dir = vim.fn.expand"~/.local/share/rime-ls",
            log_dir = vim.fn.expand"~/.local/share/rime-ls",
            paging_characters = {"-", "="},
            trigger_characters = {},
            schema_trigger_character = "&"
        },
        on_attach = rime_on_attach,
        capabilities = capabilities,
    }
end
return M

上面的代码的使用方法很简单,只需要将内容拷贝到一个文件中,然后在你的 init.lua 中调用 require'filename'.setup_rime() 即可。

这里讲一下如何配置 rime-ls。在上面的代码中,存在一个 user_data_dir = vim.fn.expand"~/.local/share/rime-ls" 因此配置文件需要放置在这个目录下,而配置方法 与配置 rime 输入法是一致的,例如下面是我添加的一些简单配置:

如果需要切换输入方案只需要打开一个启动了 rime-ls 的文件,然后输入 & 并手动触发一次 cmp.complete() 即可 (默认的按键是 <c-n>) 。通过数字键选择对应的方案即可,选择后需要确认补全。

auto-pairs 与空格自动上屏

官方文档中还提供了设置空格自动选择第一个的方法,虽然可以使用但是我在使用的过程中遇到了一些问题: 由于我使用了 jiangmiao/auto-pairs 这一款插件,这款插件重新绑定了空格键,因此按照官方的方法设置空格键 的时候,在正常使用空格的时候,命令行中会一直出现 =AutoPairsSpace() 这段文字,这样会影响体验。 因此采用了以下的方式解决:

  1. 首先关闭 jiangmiao/auto-pairs 的空格键绑定,这个通过 vim.g.AutoPairsMapSpace = 0 可以关闭。
  2. 将一个一直不会使用的按键绑定到 AutoPairsSpace 上,这里我选择了 <f30> 这个按键。
  3. 将空格绑定到 <f30> 这个按键上,启用递归绑定。
  4. 使用 autocmd 为启用了 rime-ls 的文件重新绑定空格以实现空格上屏的操作,在不能上屏的时候通过触发 <f30> 来达到静默调用的目的。

那么我们可以写出以下的代码:

-- 这行代码需要在初始化 `jiangmiao/auto-pairs` 之前执行
vim.g.AutoPairsMapSpace = 0
map.set({ 'i' }, '<f30>', '<c-]><c-r>=AutoPairsSpace()<cr>', { remap = false, silent = true })
map.set({ 'i' }, '<space>', '<f30>', { remap = true, silent = true })
local cmp = require("cmp")
local function is_rime_entry(entry)
  return vim.tbl_get(entry, "source", "name") == "nvim_lsp"
    and vim.tbl_get(entry, "source", "source", "client", "name") == "rime_ls"
end
vim.api.nvim_create_autocmd('FileType', {
    pattern = rime_ls_filetypes,
    callback = function ()
        vim.keymap.set({ 'i', 's' }, '<space>', function()
            if not vim.g.rime_enabled then
                vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<f30>', true, true, true),
                    'm', false)
            else
                local entry = cmp.get_selected_entry()
                if entry ~= nil then
                    vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<f30>', true, true, true),
                        'm', false)
                    return
                end
                entry = cmp.core.view:get_first_entry()
                if is_rime_entry(entry) then
                    cmp.confirm({
                        behavior = cmp.ConfirmBehavior.Replace,
                        select = true,
                    })
                else
                    vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('<f30>', true, true, true),
                        'm', false)
                end
            end
        end, {
            noremap = true,
            silent = true,
            buffer = true
        })
    end
})

数字键自动上屏

rime-ls 的官方仓库中的 issues 部分提到了数字键不能自动上屏,在按了数字键之后还需要一次空格 (需要绑定空格自动上屏) 操作才能进行上屏的操作。这里参考了 issues 中的 解决方案 ,这个方案在输入的时候会有明显的卡顿,因为其每次输入一个字符都会去检查是否需要触发自动上屏,我对其方案 进行了一定的改进,通过自动命令只为数字键绑定了上屏操作,这样操作后使用基本不会感觉到卡顿:

local cmp = require("cmp")
local function is_rime_entry(entry)
  return entry ~= nil and vim.tbl_get(entry, "source", "name") == "nvim_lsp"
    and vim.tbl_get(entry, "source", "source", "client", "name") == "rime_ls"
end
local function auto_upload_rime()
    if not cmp.visible() then
        return
    end
    local entries = cmp.core.view:get_entries()
    if entries == nil or #entries == 0 then
        return
    end
    local first_entry = cmp.get_selected_entry()
    if first_entry == nil then
        first_entry = cmp.core.view:get_first_entry()
    end
    if first_entry ~= nil and is_rime_entry(first_entry) then
        local rime_ls_entry_occur = false
        for _, entry in ipairs(entries) do
            if is_rime_entry(entry) then
                if rime_ls_entry_occur then
                    return
                end
                rime_ls_entry_occur= true
            end
        end
        if rime_ls_entry_occur then
            cmp.confirm {
                behavior = cmp.ConfirmBehavior.Insert,
                select = true,
            }
        end
    end
end
vim.api.nvim_create_autocmd('FileType', {
    pattern = rime_ls_filetypes,
    callback = function ()
        for numkey = 1, 9 do
            local numkey_str = tostring(numkey)
            vim.keymap.set({ 'i', 's' }, numkey_str, function()
                local visible = cmp.visible()
                vim.fn.feedkeys(numkey_str, 'n')
                -- 使用 `vim.schedule` 可以保证 `auto_upload_rime` 在 `feedkeys` 之后执行
                if visible then
                    vim.schedule(auto_upload_rime)
                end
            end, {
                noremap = true,
                silent = true,
                buffer = true,
            })
        end
    end
})

中文标点

中文标点的使用一直是一个痛点,而如果使用 rime-ls 进行标点的频繁切换的话又会影响工作的效率,为了解决 这个问题,我想到了一个简单的方法。这个方法的灵感来源于我平常写英文文档的习惯。众所周知,在书写英文的时候 如果输入了一个标点符号后,我们是需要输入一个空格与下一个英文隔开的,因为英文的标点只占用一个宽度,如果 不隔开会导致非常的拥挤。正是因为这个习惯我想到了可以在 rime-ls 启动的时候通过标点加空格的形式插入 中文标点,而在输入法关闭的时候,标点加空格则是正常的添加英文标点与空格。我于是有了以下的代码 (如果使用这种方式的话,需要将 rime-ls 标点输入设置为英文标点,可以通过方案选择的方式设置):

local punc_en = { ',', '.', ':', ';', '?', '\\' }
local punc_zh = { ',', '。', ':', ';', '?', '、' }
vim.api.nvim_create_autocmd('FileType', {
    pattern = rime_ls_filetypes,
    callback = function ()
        for i = 1, #punc_en do
            local src = punc_en[i] .. '<space>'
            local dst = 'rime_enabled ? "' .. punc_zh[i] .. '" : "' .. punc_en[i] .. ' "'
            vim.keymap.set({ 'i', 's' }, src, dst, {
                noremap = true,
                silent = false,
                expr = true,
                buffer = true,
            })
        end
    end
})

如果需要更多的中文标点只需要更改 punc_enpunc_zh 的内容即可。

copilot-chatrime-ls

在我完成了所有配置后,我发现在 copilot-chat 的窗口中不能在触发 rime-ls 的补全,在我提了 issue 后我根据网友的回答得到了以下的代码:

vim.api.nvim_create_autocmd('FileType', {
    pattern = rime_ls_filetypes,
    callback = function (env)
        local rime_ls_client = vim.lsp.get_clients({ name = 'rime_ls' })
        -- 如果没有启动 `rime-ls` 就手动启动
        if #rime_ls_client == 0 then
            vim.cmd('LspStart rime_ls')
            rime_ls_client = vim.lsp.get_clients({ name = 'rime_ls' })
        end
        -- `attach` 到 `buffer`
        if #rime_ls_client > 0 then
            vim.lsp.buf_attach_client(env.buf, rime_ls_client[1].id)
        end
    end
})

我估计这个不能够自动启动的原因是 copilot-chat 窗口并不是一个有效的 buffer,而通过上面的方式启动后 会创建一个新的 buffer 这个 buffercopilot-chat 中的内容相同。

最后

我们将上面所有的配置整合起来便可以得到一个完整的文件: rime-ls.lua

NOTE:本文出现的所有代码没有全部放置在这个文件中。空格的重新绑定放置在 key_mapping.lua 中, auto-pairs 的变量设置放置在 autopairs_config.lua 中,而 require'rime-ls'.setup_rime() 则放置在了 nvimlspconfig_config.lua 中。




    Enjoy Reading This Article?

    Here are some more articles you might like to read next:

  • IEEE Xtreme 18.0 题解
  • 马拉松 4 小时挑战记录
  • ssh 端口转发简介
  • 服务器上创建 git 远程仓库
  • Spring Boot Test 自定义测试类顺序