Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: include the ModeChanged event for more flexibility #19

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

Zhniing
Copy link

@Zhniing Zhniing commented Nov 21, 2023

I have been using this plugin for a while now, and enjoying the convenience of automatically switching IM, thanks for your work.

However, some specific needs cannot be met:

  1. Restoring the default IM on CmdlineLeave is valuable, because we may search for some text in the command line. But, in most cases, we just want to execute some commands (e.g. :w), and only the default IM will be used. So, we may not expect the default IM to override the previous IM.
  2. There is a wired (I'm a Vim newbie) behavior in Neovim: the CmdlineEnter (pattern i:c) and CmdlineLeave (pattern c:i) events will be triggered when I type the Enter key in the insert mode, which may cause unexpected IM state switching if CmdlineLeave is included in set_default_events.

Workaround:

The ModeChanged event supports the pattern match to identify events precisely:

  1. Saving the IM state only on the limited pattern (e.g. i:n, R:n), which could be configured in save_state_patterns.
  2. Don't switch the IM state on the specified pattern (e.g. i:c, c:i), which could be configured in exclude_patterns.

这个插件非常棒,非常方便,感谢作者的积极维护。

用一段时间后,发现了一些问题:

  1. 退出命令行模式时回到英文输入法是很有必要的,因为偶尔搜索文本会切换输入法,但是,大多数时候只是进入命令行模式保存文件,这就不会切换输入法。所以,我不希望退出命令行的时候覆盖上一次保存的输入法。
  2. 我的Neovim有一个奇怪的行为(也可是我不懂):在插入模式下敲回车换行会触发CmdlineEnteri:c模式)和CmdlineLeavec:i模式)事件,如果在set_default_events中配置了CmdlineLeave就会自动切换为英文输入法。

解决方案:

ModeChanged支持使用模式匹配来精确识别事件:

  1. 可以限定几个需要保存输入法的事件模式,其他事件模式都不保存输入法;在save_state_patterns里面配置
  2. 可以指定一些事件模式不要切换输入法;在exclude_patterns里面配置

@keaising
Copy link
Owner

keaising commented Nov 21, 2023

感谢你的 pr,我有两个点想详细了解一下:

  1. 第一个问题我没有看明白场景是什么,可以详细描述一下你的操作步骤和每个步骤之后的输入法状态吗?
  2. 第二个问题看起来可能是你的配置或者某个插件导致的,你可以做一个最小的可重现配置出来吗?可以参考 Lazy.nvim 里复现问题所需要的 Repro:https://github.com/folke/lazy.nvim/issues/new?assignees=&labels=bug&projects=&template=bug_report.yml&title=bug%3A+

还有一点我需要提前说一下,我觉得这个 pr 里的内容有点过于复杂和精细了,就我之前调研过的用户反馈来看,大部分用户可能完全不会需要这些精细的设置,他们也多半不了解 ModeChanged 这个参数(了解 VimEnterFocusGained 之类的其实都不多 ),所以后面我可能会跟你一起做一些修改,比如将新增的 ModeChangedset_default_events 剥离出去,成为完全独立的设置,最好是跟 set_default_events 互相不影响。这样做的目的是保持在默认设置的情况下,现有用户的配置完全不需要改动,而对 ModeChanged 有需求的用户(比如你),可以通过自定义 ModeChanged 之类的配置实现自己的需求,我想这样对你和其他用户来说都会比较方便

@Zhniing
Copy link
Author

Zhniing commented Nov 21, 2023

感谢您的回复

第一个问题的具体步骤如下

(为了方便描述,我们用符号P代表插件当前保存的上一个输入法状态)

  1. 打开Vim(自动切换到:英文)
  2. 进入Insert模式,使用中文输入法(自动切换到:中文,不会更新P
  3. ESC切换到Normal模式(自动切换到:英文,P更新为:中文)
  4. 输入:w保存文件(不会切换,不会更新P
  5. 回到Normal模式(自动切换到:英文,P更新为:英文
  6. 再次进入Insert模式,继续输入中文(无法自动切换到中文,因为P在第5步变成了英文)

那么,只要不在命令行模式下保存输入法,就能解决我的问题,于是有了save_state_patterns

关于第二个问题

我使用packer.nvim来管理插件,通过逐个禁用插件,发现问题来自coc.nvim插件配置,我把回车键<cr>映射为了一条命令。感谢您提供的思路。

那么,exclude_patterns是否还有存在的必要?它让我们用通配符*匹配ModeChanged的同时,还能指定一些例外,而不是把所有情况全部写在配置中。如果您觉得不需要,我们可以把它去掉。

关于兼容性

大部分用户可能完全不会需要这些精细的设置,他们也多半不了解ModeChanged这个参数

set_default_eventsset_previous_events能够完全兼容之前的配置,原来的事件写法和ModeChanged写法可以同时存在:

set_default_events = { "VimEnter", "FocusGained", ModeChanged = {"i:*", "c:*", "R:*"} },

也就是说,可以让默认配置与之前保持一致:

set_default_events = { "VimEnter", "FocusGained", "InsertLeave", "CmdlineLeave" },
set_previous_events = { "InsertEnter" },

这样就能确保在默认设置的情况下,现有用户的配置完全不需要改动

然后,需要ModeChanged的配置默认为空:

save_state_patterns = {},  -- 为空时,与以前的行为一致,还需要修改一下代码
exclude_patterns = {},

(通过注释或其他形式)告知用户ModeChanged的配置写法,让有需求的用户自行配置

如果需要,我可以更新一下pr,把默认配置改回原来的样子

@keaising
Copy link
Owner

我理解你的意思了,你只是需要在某些事件触发的时候,切换到默认输入法,但是不更新当前输入法的状态。

具体来说就是:在 CmdlineLeave 事件发生时,在 restore_default_im 函数里,只调用

if current ~= C.default_method_selected then
change_im_select(C.default_command, C.default_method_selected)
end
切换到默认输入法,不调用
local current = get_current_select(C.default_command)
vim.api.nvim_set_var("im_select_saved_state", current)
将缓存里保存的 im_select_saved_state 值改为默认输入法

那我们这样改可以吗?

增加一个参数 keep_state_when_set_default_events = { } ,默认是空的,你可以使用下面的配置

        config = function()
            require("im_select").setup({
                set_previous_events = { "InsertEnter" },
                set_default_events = { "VimEnter", "InsertLeave", "CmdlineLeave" },
                keep_state_when_set_default_events = { "CmdlineLeave" },
            })
        end,

最终实现的效果就是在 "VimEnter", "InsertLeave", "CmdlineLeave" 三个事件发生时会还原成默认输入法,但是 CmdlineLeave 里还原成默认输入法时并不是把默认输入法当作一个状态存到缓存里,下次还原上一次保存的输入法状态时就不会是默认输入法了

你可以试试这个分支:https://github.com/keaising/im-select.nvim/tree/keep_state_event

我觉得这样简单点,不用引入 ModeChanged ,使用的参数和工具(依然是 vim 里的各种事件)跟现有的保持一致

1. Remove `exclude_patterns`
2. Make the configuration compatible with the previous ones
3. Separate the `save_im_state` from `restore_default_im`
@Zhniing
Copy link
Author

Zhniing commented Nov 29, 2023

感谢您提供的方案,但在使用过程中发现一个小问题:补全的时候,会出现模式转换,触发restore_default_im函数,导致中文输入法丢失

具体复现步骤如下(已禁用除了im-select之外的所有插件和设置):

  1. 打开一个文件,输入如下内容:
初级
初
  1. 在第二行的“初”打出来时,会弹出一个补全提示框
  2. 使用Ctrl+n选择补全这时,会触发一系列模式转换(ModeChanged)事件,依次为:i:c c:i ic:i i:ix ix:i(用上下方向键选择补全只会触发i:cc:i事件)
  3. i:c事件和c:i事件之间,会触发restore_default_im函数(应该是因为i:c属于InsertLeave事件)
  4. 之后并不会触发restore_previous_im函数

modechanged

~/.config/nvim/init.lua中加入如下代码,可以监测到模式转换:

vim.api.nvim_create_autocmd("ModeChanged", {
  pattern = {"*:*"},
  callback = function (args)
    os.execute("notify-send " .. args.match)
  end,
})

当然,只输入中文几乎不会用到补全,但如果在中文穿插数字或字母,就有可能用到补全

所以,我还是建议引入ModeChanged,然后对之前的pr做了一些改动:

  1. 移除exclude_patterns选项
  2. 完全兼容原来的配置:保留以前的autocmd绑定不变,为ModeChanged绑定新的autocmd,代码实现在这里
  3. 将保存输入法状态的部分从restore_default_im函数中独立出来,由save_state_events触发(默认与set_default_events一致,以前的用户不用修改配置),这样就让用户能够自由配置

您看这样是否满足了兼容性要求?

另外,我目前使用的配置如下:

set_default_events = { "VimEnter", ModeChanged = { "[iRc]*:[nvV\x16sS]*" } },
save_state_events = { ModeChanged = { "[iR]*:[nvV\x16sS]*" } },
set_previous_events = { ModeChanged = { "[nvV\x16sS]*:[iR]" } },  -- \x16 means Ctrl-V

save_state_events中少写一个c就能实现我的需求

@keaising
Copy link
Owner

不好意思,前几天新冠了一直没上线

你可以给我一个最小的可复现的配置吗?我试了一下没有复现出来你说的“在补全的时候会触发 InsertLeave”的场景,按理说补全的时候摁 Ctrl+N 是不会脱离 Insert mode 的,也就不会触发 restore_default_im 函数

@Zhniing
Copy link
Author

Zhniing commented Dec 29, 2023

抱歉之前没看到消息

不好意思,之前是我测试做得有问题。
按照Lazy.nvim的Repro试了一下,确实不会脱离Insert模式,但是会触发CmdlineLeave事件,导致切换回默认的英文输入法。
我之前测试时只是把其他插件(包括coc.nvim)注释掉了,没有注意到coc.nvim还在生效。
执行:CocDisable禁用coc.nvim后,补全时就不再触发InsertLeave和CmdlineLeave事件,一切正常。

最小的复现配置repro.lua如下:

-- DO NOT change the paths and don't remove the colorscheme
local root = vim.fn.fnamemodify("./.repro", ":p")

-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
  vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end

-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", lazypath, })
end
vim.opt.runtimepath:prepend(lazypath)

-- install plugins
local plugins = {
  "folke/tokyonight.nvim",
  -- add any other plugins here

  {
    "keaising/im-select.nvim",
    branch = "keep_state_event",
    opts = {
      set_previous_events = { "InsertEnter" },
      set_default_events = {
        "VimEnter",
        "InsertLeave",
        "CmdlineLeave"  -- 注释这行就能防止补全时自动切回默认的英文输入法
      },
      keep_state_when_set_default_events = { "CmdlineLeave" },
    },
  },
  {
    'neoclide/coc.nvim',
    branch = 'release',
  }
}
require("lazy").setup(plugins, {
  root = root .. "/plugins",
})

vim.cmd.colorscheme("tokyonight")
-- add anything else here

vim.api.nvim_create_autocmd("ModeChanged", {
  pattern = {"*:*"},
  callback = function (args)
    os.execute("notify-send " .. args.match)
  end,
})
vim.api.nvim_create_autocmd("CmdlineLeave", {
  callback = function ()
    os.execute("notify-send CmdlineLeave")
  end,
})
vim.api.nvim_create_autocmd("InsertLeave", {
  callback = function ()
    os.execute("notify-send InsertLeave")
  end,
})

通过nvim -u repro.lua启动,然后复现所需的具体操作步骤和之前一样。

不知道怎么配置coc.nvim才能让它不触发CmdlineLeave(看了下coc.nvim的文档,感觉不大可能),目前还不打算放弃使用coc.nvim
可能还是只有ModeChanged才能实现我的需求
如果您觉得没有必要引入ModeChanged,那这个pr就算了吧,我先用着自己的fork

非常感谢您的积极维护

@keaising
Copy link
Owner

keaising commented Jan 3, 2024

上面的 notify-send 是哪个应用?我尝试执行 nvim -u repro.lua repro.lua 的时候会弹出很多提示

sh: notify-send: command not found

@Zhniing
Copy link
Author

Zhniing commented Jan 3, 2024

notify-send是一个发送桌面通知的程序,这里用来监听模式切换的事件。

我这里好像是Linux自带的。

在Ubuntu上可以使用命令安装:

apt-get install libnotify-bin

其他平台的安装可以参考这里

@keaising
Copy link
Owner

keaising commented Jan 3, 2024

嗯嗯,我在 macOS 上用的这个 https://formulae.brew.sh/formula/terminal-notifier ,代码改成下面这样

我能复现你说的在 coc 里用 ctrl+p/n 选择补全的时候会触发 CmdlineLeave 的问题,我感觉有点奇怪,我再试试我自己的配置

-- DO NOT change the paths and don't remove the colorscheme
local root = vim.fn.fnamemodify("./.repro", ":p")

-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
  vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end

-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", lazypath, })
end
vim.opt.runtimepath:prepend(lazypath)

-- install plugins
local plugins = {
  {
    "keaising/im-select.nvim",
    branch = "keep_state_event",
    opts = {
      set_previous_events = { "InsertEnter" },
      set_default_events = {
        "VimEnter",
        "InsertLeave",
        "CmdlineLeave"  -- 注释这行就能防止补全时自动切回默认的英文输入法
      },
      keep_state_when_set_default_events = { "CmdlineLeave" },
    },
  },
  {
    'neoclide/coc.nvim',
    branch = 'release',
  }
}

local cmd_count = 0
local insert_count = 0
require("lazy").setup(plugins, {
  root = root .. "/plugins",
})

-- vim.api.nvim_create_autocmd("ModeChanged", {
--   pattern = {"*:*"},
--   callback = function (args)
--     os.execute("terminal-notifier -title ModeChanged -message " .. args.match)
--   end,
-- })
vim.api.nvim_create_autocmd("CmdlineLeave", {
  callback = function ()
    os.execute("terminal-notifier -title Cmd -message CmdlineLeave" .. cmd_count)
    cmd_count = cmd_count + 1
  end,
})
vim.api.nvim_create_autocmd("InsertLeave", {
  callback = function ()
    os.execute("terminal-notifier -title Cmd -message InsertLeave" .. insert_count)
    insert_count = insert_count + 1
  end,
})

@keaising
Copy link
Owner

keaising commented Jan 3, 2024

好像还真是 Coc 的一个问题,之前也有人提到过类似的问题:neoclide/coc.nvim#3290 (comment) ,在他的注释里提到了一句:

  " Make <S-CR> auto-select the first completion item and notify coc.nvim to
  " format on shift-enter, <S-CR> could be remapped by other vim plugin
  " because it will trigger CmdLineLeave to affect the fcitx-remote, so I
  " only use it when I type codes

@Zhniing
Copy link
Author

Zhniing commented Jan 3, 2024

嗯嗯,我试过按照他README给的配置,改用TAB来选择补全,也会触发CmdlineLeave;另外,如果按照这个配置使用<CR>来确认补全的话,正常按回车键都会触发CmdlineLeave

repro.lua如下:

-- DO NOT change the paths and don't remove the colorscheme
local root = vim.fn.fnamemodify("./.repro", ":p")

-- set stdpaths to use .repro
for _, name in ipairs({ "config", "data", "state", "cache" }) do
  vim.env[("XDG_%s_HOME"):format(name:upper())] = root .. "/" .. name
end

-- bootstrap lazy
local lazypath = root .. "/plugins/lazy.nvim"
if not vim.loop.fs_stat(lazypath) then
  vim.fn.system({ "git", "clone", "--filter=blob:none", "https://github.com/folke/lazy.nvim.git", lazypath, })
end
vim.opt.runtimepath:prepend(lazypath)

-- install plugins
local plugins = {
  "folke/tokyonight.nvim",
  -- add any other plugins here

  {
    "keaising/im-select.nvim",
    branch = "keep_state_event",
    opts = {
      set_previous_events = { "InsertEnter" },
      set_default_events = {
        "VimEnter",
        "InsertLeave",
        "CmdlineLeave"  -- 注释这行就能防止补全时自动切回默认的英文输入法
      },
      keep_state_when_set_default_events = { "CmdlineLeave" },
    },
  },
  {
    'neoclide/coc.nvim',
    branch = 'release',
    config = function ()
      -- Some servers have issues with backup files, see #649
      vim.opt.backup = false
      vim.opt.writebackup = false

      -- Having longer updatetime (default is 4000 ms = 4s) leads to noticeable
      -- delays and poor user experience
      vim.opt.updatetime = 300

      -- Always show the signcolumn, otherwise it would shift the text each time
      -- diagnostics appeared/became resolved
      vim.opt.signcolumn = "yes"

      local keyset = vim.keymap.set
      -- Autocomplete
      function _G.check_back_space()
          local col = vim.fn.col('.') - 1
          return col == 0 or vim.fn.getline('.'):sub(col, col):match('%s') ~= nil
      end

      -- Use Tab for trigger completion with characters ahead and navigate
      -- NOTE: There's always a completion item selected by default, you may want to enable
      -- no select by setting `"suggest.noselect": true` in your configuration file
      -- NOTE: Use command ':verbose imap <tab>' to make sure Tab is not mapped by
      -- other plugins before putting this into your config
      local opts = {silent = true, noremap = true, expr = true, replace_keycodes = false}
      keyset("i", "<TAB>", 'coc#pum#visible() ? coc#pum#next(1) : v:lua.check_back_space() ? "<TAB>" : coc#refresh()', opts)
      keyset("i", "<S-TAB>", [[coc#pum#visible() ? coc#pum#prev(1) : "\<C-h>"]], opts)

      -- Make <CR> to accept selected completion item or notify coc.nvim to format
      -- <C-g>u breaks current undo, please make your own choice
      keyset("i", "<cr>", [[coc#pum#visible() ? coc#pum#confirm() : "\<C-g>u\<CR>\<c-r>=coc#on_enter()\<CR>"]], opts)
    end
  }
}
require("lazy").setup(plugins, {
  root = root .. "/plugins",
})

vim.cmd.colorscheme("tokyonight")
-- add anything else here

vim.api.nvim_create_autocmd("ModeChanged", {
  pattern = {"*:*"},
  callback = function (args)
    os.execute("notify-send " .. args.match)
  end,
})
vim.api.nvim_create_autocmd("CmdlineLeave", {
  callback = function ()
    os.execute("notify-send CmdlineLeave")
  end,
})
vim.api.nvim_create_autocmd("InsertLeave", {
  callback = function ()
    os.execute("notify-send InsertLeave")
  end,
})

@keaising
Copy link
Owner

keaising commented Jan 3, 2024

我也去翻了一下 Coc 的文档,好像没有提这个的,具体为什么会在补全的时候触发 CmdlineLeave 就不得而知了,可能得读一下 Coc 的代码才知道能不能改,估计不太好弄

说实话我一直不想引入 ModeChanged 的原因是我觉得这玩意太复杂了,我读了好久才弄明白它是干什么的以及该如何用,至于用起来还是直接抄的你的代码,我想尽量让这个插件简单点,让新来的用户一眼就能看明白里面的逻辑然后在有需求的时候自己修改它(就像你现在做的一样)。所以我不打算合并这个 PR(并不是说你的 PR 有问题,只是单纯我对功能复杂性的偏好导致的)

不过你发现的的确是个兼容性问题,稍后我会把我们的对话和找到的问题翻译一下放在后面,然后在 README 里提示一下如果用 Coc 的话需要把 CmdlineLeave 排除掉

@Zhniing
Copy link
Author

Zhniing commented Jan 3, 2024

嗯嗯,可以。这个插件的代码确实写得不错,我改代码的时候也觉得读起来很舒服,写这个PR的过程中也学到了很多,还是非常感谢您

@keaising
Copy link
Owner

keaising commented Jan 5, 2024

总结

结论: Coc 的 bug/feature 导致在 insert mode 里每次用 Coc 补全时切换了候选项,都会导致输入法被切换到默认输入法

通过后面附上的脚本配置可以复现,在使用 Coc 补全的时候(不管快捷键是使用Ctrl+n/p 还是 Tab,应该是更底层的命令触发的),每一次切换补全项都会触发一次 CmdlineEnter 和 CmdlineLeave,这就导致在下列两种情况下会遇到这个问题:

  1. 使用了 im-select.nvim 的默认配置,
  2. 你的 option 里的 set_default_events 里有 CmdlineLeave set_previous_events 里没有 CmdlineEnter

这是一个不兼容的情况,但是目前本插件无法解决,一个可用的解决方案是使用 https://github.com/Zhniing/im-select.nvim ,这个 fork 使用了 https://neovim.io/doc/user/autocmd.html#ModeChanged 来精确匹配切换模式,可以避开 Coc 的这个问题,配置需要修改为

set_default_events = { "VimEnter", ModeChanged = { "[iRc]*:[nvV\x16sS]*" } },
save_state_events = { ModeChanged = { "[iR]*:[nvV\x16sS]*" } },
set_previous_events = { ModeChanged = { "[nvV\x16sS]*:[iR]" } },  -- \x16 means Ctrl-V

复现 Coc 切换问题的 repro.lua

Ubuntu: #19 (comment)
macOS: #19 (comment)


Summary

Conclusion: A bug/feature in Coc causes the input method to be switched to the default input method every time a candidate is switched when using Coc Completion in insert mode.

This can be reproduced by the following script configuration, when using Coc complement (regardless of whether the shortcut is Ctrl+n/p or Tab, it should be triggered by a lower level command), every time you switch between the complement items, it triggers CmdlineEnter and CmdlineLeave once, which results in the problem in the following two scenarios:

  1. the default configuration of im-select.nvim is used.
  2. you have CmdlineLeave in set_default_events but not CmdlineEnter in set_previous_events.

This is an incompatibility, but currently this plugin can't solve it. An available solution is to use https://github.com/Zhniing/im-select.nvim, this fork uses https://neovim.io/doc/user/autocmd.html#ModeChanged to accurately match the switching mode, you can bypass this problem in Coc, the configuration needs to be changed to

set_default_events = { "VimEnter", ModeChanged = { "[iRc]*:[nvV\x16sS]*" } },
save_state_events = { ModeChanged = { "[iR]*:[nvV\x16sS]*" } },
set_previous_events = { ModeChanged = { "[nvV\x16sS]*:[iR]" } },  -- \x16 means Ctrl-V

You can reproduce this issue by

Ubuntu: #19 (comment)
macOS: #19 (comment)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants