update at 2026-07-15 11:04:39

This commit is contained in:
douboer
2026-07-15 11:04:39 +08:00
parent 995d672f4a
commit aeade0f813
7 changed files with 386 additions and 820 deletions

View File

@@ -1,9 +1,8 @@
" vim-im-switch-select.vim - 智能记忆输入法状态 (使用 im-select)
" vim-im-switch-select.vim - macOS Vim 输入法切换
" Author: Gavin Chan
" Modified by: codefalling, enhanced with smart IM state memory
" Version: 2.0.0
" Description: 记住退出 Insert 模式时的输入法状态,下次进入时自动恢复
" 使用 im-select 而不是 fcitx-remote 切换输入法
" Version: 2.3.1
" Description: Normal 模式固定英文Insert 模式恢复同进程内上次中/英状态
" ---------------------------------------------------------------------
" Load Once:
if exists('g:imselect_loaded')
@@ -21,19 +20,18 @@ endif
if exists('$SSH_TTY')
finish
endif
if !executable("im-select")
let s:hs_cmd = executable('hs') ? exepath('hs') : ''
let s:imselect_cmd = executable('im-select') ? exepath('im-select') : ''
if s:hs_cmd ==# '' && s:imselect_cmd ==# ''
finish
endif
let s:imselect_cmd = exepath('im-select')
let s:keepcpo = &cpo
let g:imselect_loaded = 1
set cpo&vim
" ---------------------------------------------------------------------
" 全局变量:记住上次 Insert 模式的输入法名称
let g:imselect_last_insert_im_name = ''
" 英文和中文输入法的 ID可以通过 im-select 获取)
" im-select fallback 使用输入源 IDHammerspoon 主路径使用 layout/method 名称。
if !exists('g:imselect_english_im')
let g:imselect_english_im = 'com.apple.keylayout.ABC'
endif
@@ -42,71 +40,167 @@ if !exists('g:imselect_chinese_im')
let g:imselect_chinese_im = 'com.apple.inputmethod.SCIM.ITABC'
endif
if !exists('g:imselect_hs_english_layout')
let g:imselect_hs_english_layout = 'ABC'
endif
if !exists('g:imselect_hs_chinese_method')
let g:imselect_hs_chinese_method = 'Pinyin Simplified'
endif
if !exists('g:imselect_restore_delay')
let g:imselect_restore_delay = 80
endif
if !exists('g:imselect_confirm_delay')
let g:imselect_confirm_delay = 200
endif
let s:restore_timer = -1
let s:confirm_timer = -1
let s:target = ''
" 只在当前 Vim 进程内记忆;退出 Vim 后会重新默认中文。
let s:last_insert_target = 'chinese'
" ---------------------------------------------------------------------
" Functions:
function! IMSelectCurrentIM() abort
if s:imselect_cmd ==# ''
return ''
endif
return substitute(system(shellescape(s:imselect_cmd) . ' 2>/dev/null'), '[\r\n]', '', 'g')
endfunction
function! s:CancelTimer(timer) abort
if a:timer != -1
silent! call timer_stop(a:timer)
endif
endfunction
function! s:CancelInsertTimers() abort
call s:CancelTimer(s:restore_timer)
call s:CancelTimer(s:confirm_timer)
let s:restore_timer = -1
let s:confirm_timer = -1
endfunction
function! s:RunHammerspoon(lua) abort
if s:hs_cmd ==# ''
return 0
endif
silent! call system(shellescape(s:hs_cmd) . ' -c ' . shellescape(a:lua) . ' >/dev/null 2>&1')
return v:shell_error == 0
endfunction
function! s:HammerspoonOutput(lua) abort
if s:hs_cmd ==# ''
return ''
endif
return substitute(system(shellescape(s:hs_cmd) . ' -c ' . shellescape(a:lua) . ' 2>/dev/null'), '[\r\n]', '', 'g')
endfunction
function! IMSelectSwitch(im) abort
if has('job')
silent! call job_start([s:imselect_cmd, a:im], {'out_io': 'null', 'err_io': 'null'})
if a:im ==# '' || s:imselect_cmd ==# ''
return
endif
silent! call system(shellescape(s:imselect_cmd) . ' ' . shellescape(a:im) . ' >/dev/null 2>&1')
endfunction
function! s:SwitchToEnglish() abort
" Hammerspoon 能直接设置 layout比只切 source ID 更贴近 macOS 实际输入状态。
if s:RunHammerspoon('hs.keycodes.setLayout(' . string(g:imselect_hs_english_layout) . ')')
return
endif
call IMSelectSwitch(g:imselect_english_im)
endfunction
function! s:SwitchToChinese() abort
" Apple 拼音可能出现 source ID 已切中文但 layout 仍为 ABC 的坏状态setMethod 可避免该问题。
if s:RunHammerspoon('hs.keycodes.setMethod(' . string(g:imselect_hs_chinese_method) . ')')
return
endif
call IMSelectSwitch(g:imselect_chinese_im)
endfunction
function! s:SwitchToTarget(target) abort
if a:target ==# 'english'
call s:SwitchToEnglish()
else
silent! call system(shellescape(s:imselect_cmd) . ' ' . shellescape(a:im) . ' >/dev/null 2>&1 &')
call s:SwitchToChinese()
endif
endfunction
function! s:DetectInsertTarget() abort
let layout = s:HammerspoonOutput('print(hs.keycodes.currentLayout() or "")')
let method = s:HammerspoonOutput('print(hs.keycodes.currentMethod() or "")')
if layout ==# g:imselect_hs_english_layout && method ==# ''
let s:last_insert_target = 'english'
elseif method ==# g:imselect_hs_chinese_method || layout ==# g:imselect_hs_chinese_method
let s:last_insert_target = 'chinese'
else
let current_im = IMSelectCurrentIM()
if current_im ==# g:imselect_english_im
let s:last_insert_target = 'english'
elseif current_im ==# g:imselect_chinese_im
let s:last_insert_target = 'chinese'
endif
endif
endfunction
function! IMSelectStart() abort
call IMSelectSwitch(g:imselect_english_im)
let s:target = 'english'
call s:CancelInsertTimers()
call s:SwitchToEnglish()
endfunction
function! IMSelectEnsureTarget(timer) abort
if g:imselect_last_insert_im_name != ''
call IMSelectSwitch(g:imselect_last_insert_im_name)
function! s:ConfirmTarget(timer) abort
let s:confirm_timer = -1
call s:SwitchToTarget(s:target)
endfunction
function! s:RestoreInsertTarget(timer) abort
let s:restore_timer = -1
" 防止快速 i<Esc> 后,延迟 timer 又把 Normal 模式切回中文。
if s:target !=# s:last_insert_target
return
endif
call s:SwitchToTarget(s:last_insert_target)
if g:imselect_confirm_delay > 0
let s:confirm_timer = timer_start(g:imselect_confirm_delay, function('s:ConfirmTarget'))
endif
endfunction
" 离开 Insert 模式前:保存当前输入法名称
" 必须在 InsertLeavePre 保存InsertLeave 中会立即切到英文。
function! IMSelectRemember() abort
let current_im = IMSelectCurrentIM()
if current_im != ''
let g:imselect_last_insert_im_name = current_im
endif
call s:DetectInsertTarget()
endfunction
" 离开 Insert 模式:切换到英文
function! IMSelect2en() abort
call IMSelectSwitch(g:imselect_english_im)
let s:target = 'english'
call s:CancelInsertTimers()
call s:SwitchToEnglish()
endfunction
" 恢复 Insert 模式输入法
function! IMSelectRestore(timer) abort
if g:imselect_last_insert_im_name == ''
let g:imselect_last_insert_im_name = g:imselect_chinese_im
endif
call IMSelectSwitch(g:imselect_last_insert_im_name)
call timer_start(200, function('IMSelectEnsureTarget'))
endfunction
" 进入 Insert 模式:延迟恢复保存的输入法
function! IMSelect2zh() abort
call timer_start(80, function('IMSelectRestore'))
let s:target = s:last_insert_target
call s:CancelInsertTimers()
if g:imselect_restore_delay > 0
let s:restore_timer = timer_start(g:imselect_restore_delay, function('s:RestoreInsertTarget'))
else
call s:RestoreInsertTarget(-1)
endif
endfunction
" ---------------------------------------------------------------------
" Autocmds:
" 绑定自动命令
function! BindIMSelectAu()
augroup IMSelect
autocmd!
" 离开 Insert 模式前:先保存输入法状态
autocmd InsertLeavePre * call IMSelectRemember()
" 离开 Insert 模式:切换到英文
autocmd InsertLeave * call IMSelect2en()
" 进入 Insert 模式:恢复上次的输入法状态
autocmd InsertEnter * call IMSelect2zh()
" Vim 启动后:切换到英文
autocmd VimEnter * call timer_start(100, {-> IMSelectStart()})
augroup END
endfunction
@@ -115,11 +209,11 @@ function! UnBindIMSelectAu()
augroup IMSelect
autocmd!
augroup END
call s:CancelInsertTimers()
endfunction
call BindIMSelectAu()
" 多光标支持:在选择多个光标前后的处理
function! IMSelect_multiple_cursors_before()
call UnBindIMSelectAu()
endfunction