A plugin that makes Neovim more friendly to non-English input methods 🤝
- Translating all globally registered mappings;
- Translating local registered mappings for each buffer;
- Registering translated mappings for all built-in CTRL+ sequences;
- Provides utils for manual registration original and translated mapping with single function;
- Hacks built-in keymap's methods to translate all registered mappings (including mappings from lazy-loaded plugins);
- Real-time normal mode command processing variability depending on the input method.
- Neovim 0.8+
- CLI utility to determine the current input method (optional)
- Configured vim.opt.langmap for your input method;
- Set up
vim.g.mapleader
andmap.g.localleader
beforelangmapper.setup()
;
Examples of CLI utilities:
- im-select for Mac and Windows
- xkb-switch for Linux
With Lazy.nvim:
return {
'Wansmer/langmapper.nvim',
lazy = false,
priority = 1, -- High priority is needed if you will use `autoremap()`
config = function()
require('langmapper').setup({--[[ your config ]]})
end,
}
With Packer.nvim:
use({
'Wansmer/langmapper.nvim',
config = function()
require('langmapper').setup({--[[ your config ]]})
end,
})
After all the contents of your init.lua
(optional):
-- code
require('langmapper').automapping({ global = true, buffer = true })
-- end of init.lua
First, make sure you have a langmap
configured. Langmapper only handles key
mappings. All other movement commands depend on the langmap
.
Show example of `vim.opt.langmap`
local function escape(str)
-- You need to escape these characters to work correctly
local escape_chars = [[;,."|\]]
return vim.fn.escape(str, escape_chars)
end
-- Recommended to use lua template string
local en = [[`qwertyuiop[]asdfghjkl;'zxcvbnm]]
local ru = [[ёйцукенгшщзхъфывапролджэячсмить]]
local en_shift = [[~QWERTYUIOP{}ASDFGHJKL:"ZXCVBNM<>]]
local ru_shift = [[ËЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЭЯЧСМИТЬБЮ]]
vim.opt.langmap = vim.fn.join({
-- | `to` should be first | `from` should be second
escape(ru_shift) .. ';' .. escape(en_shift),
escape(ru) .. ';' .. escape(en),
}, ',')
Show default config
local default_config = {
---@type boolean Add mapping for every CTRL+ binding or not.
map_all_ctrl = true,
---@type string[] Modes to `map_all_ctrl`
---Here and below each mode must be specified, even if some of them extend others.
---E.g., 'v' includes 'x' and 's', but must be listed separate.
ctrl_map_modes = { 'n', 'o', 'i', 'c', 't', 'v' },
---@type boolean Wrap all keymap's functions (nvim_set_keymap etc)
hack_keymap = true,
---@type string[] Usually you don't want insert mode commands to be translated when hacking.
---This does not affect normal wrapper functions, such as `langmapper.map`
disable_hack_modes = { 'i' },
---@type table Modes whose mappings will be checked during automapping.
automapping_modes = { 'n', 'v', 'x', 's' },
---@type string Standart English layout (on Mac, It may be different in your case.)
default_layout = [[ABCDEFGHIJKLMNOPQRSTUVWXYZ<>:"{}~abcdefghijklmnopqrstuvwxyz,.;'[]`]],
---@type string[] Names of layouts. If empty, will handle all configured layouts.
use_layouts = {},
---@type table Fallback layouts
---Custom description builder:
--- old_desc - original description,
--- method - 'translate' (map translated lhs) or 'feedkeys' (call `nvim_feedkeys` with original lhs)
--- lhs - original left-hand side for translation
---should return new description as a string. If error is occurs or non-string is returned, original builder with `LM ()` prefix will use
---@type nil|function(old_desc, method, lhs): string
custom_desc = nil,
layouts = {
---@type table Fallback layout item. Name of key is a name of language
ru = {
---@type string Name of your second keyboard layout in system.
---It should be the same as result string of `get_current_layout_id()`
id = 'com.apple.keylayout.RussianWin',
---@type string Fallback layout to translate. Should be same length as default layout
layout = 'ФИСВУАПРШОЛДЬТЩЗЙКЫЕГМЦЧНЯБЮЖЭХЪËфисвуапршолдьтщзйкыегмцчнябюжэхъё',
---@type string if you need to specify default layout for this fallback layout
default_layout = nil,
},
},
os = {
-- Darwin - Mac OS, the result of `vim.loop.os_uname().sysname`
Darwin = {
---Function for getting current keyboard layout on your OS
---Should return string with id of layout
---@return string
get_current_layout_id = function()
local cmd = 'im-select'
if vim.fn.executable(cmd) then
local output = vim.split(vim.trim(vim.fn.system(cmd)), '\n')
return output[#output]
end
end,
},
},
}
Set up your layout
in config, set hack_keymap
to true
and load Langmapper
the first of the sheet of plugins, then call langmapper.setup(opts)
.
Under such conditions, all subsequent calls to vim.keymap.set
,
vim.keymap.del
, vim.api.nvim_(buf)_set_keymap
and
vim.api.nvim_(buf)_del_keymap
will be wrapped with a special function,
which will automatically translate mappings and register them.
This means that even in the case of lazy-loading, the mapping setup will still be processed and the translated mapping will be registered for it.
If you need to handle built-in and vim script mappings too, call the
langmapper.automapping({ buffer = false })
function at the very end of
your init.lua
. (buffer to false
, because nvim_buf_set_keymap
already hacked 😎)
Set up your layout
in config, set hack_keymap
to false,
and call langmapper.setup(opts)
.
-- this function completely repeats contract of vim.keymap.set
local map = require('langmapper').map
map('n', '<Leader>e', '<Cmd>Neotree toggle focus<Cr>')
-- Neo-tree config.
-- It will return a table with 'translated' keys and same values.
local map = require('langmapper.utils')
local window_mappings = mapper.trans_dict({
['o'] = 'open',
['sg'] = 'split_with_window_picker',
['<leader>d'] = 'copy',
})
Add langmapper.autoremap({ global = true, buffer = true })
to the end of your
init.lua
.
It will autotranslate all registered mappings from nvim_get_keymap()
and
nvim_buf_get_keymap()
.
But it cannot handle mappings of lazy loaded plugins.
NOTE: all keys, that you're using in
keys = {}
inlazy.nvim
also will be translated.
which-key
uses nvim_feedkeys
to execute the sequence entered by the user.
This imposes restrictions on the execution of commands related to operators,
text objects and movements, since nvim_feedkeys
does not handle the value
of your vim.opt.langmap
. Therefore, the entered sequence must be
translated back into English characters.
Here example how to integrate Langmapper to LazyNvim.
Configuration example:
return {
'folke/which-key.nvim',
enabled = true,
dependencies = { 'Wansmer/langmapper.nvim' },
config = function()
vim.o.timeout = true
vim.o.timeoutlen = 300
local lmu = require('langmapper.utils')
local view = require('which-key.view')
local execute = view.execute
-- wrap `execute()` and translate sequence back
view.execute = function(prefix_i, mode, buf)
-- Translate back to English characters
prefix_i = lmu.translate_keycode(prefix_i, 'default', 'ru')
execute(prefix_i, mode, buf)
end
-- If you want to see translated operators, text objects and motions in
-- which-key prompt
-- local presets = require('which-key.plugins.presets')
-- presets.operators = lmu.trans_dict(presets.operators)
-- presets.objects = lmu.trans_dict(presets.objects)
-- presets.motions = lmu.trans_dict(presets.motions)
-- etc
require('which-key').setup()
end,
}
Some other plugins that work with user input can also be hacked in this way. You can find some hacks or share your own it this discussion.
Usage:
local langmapper = require('langmapper')
langmapper.map(...)
langmapper.automapping(...)
-- etc
Gets the output of nvim_get_keymap
for all modes listed in the
automapping_modes
, and sets the translated mappings using nvim_feedkeys
.
Then sets event handlers { 'BufWinEnter', 'LspAttach' }
to do the same with
outputting nvim_buf_get_keymap
for each open buffer.
Must be called at the very end of init.lua
, after all plugins have been loaded
and all key bindings have been set.
This function also handles mappings made via vim script.
Does not handle mappings for lazy-loaded plugins. To avoid it, see
hack_keymap
.
NOTE: If you use
hack_keymap
, there are only one reason to use this function it is auto-handling built-in mappings (e.g., for netrw, like 'gx') and if you have mappings (or plugins with mappings) on vim script.
---@param opts {global=boolean|nil, buffer=boolean|nil}
function M.automapping(opts)
Wrappers of vim.keymap.set
\ vim.keymap.del
with same contract.
map()
- Sets the given lhs
, then translates it to the configured input
methods, and maps it with the same options.
E.g.:
map('i', 'jk', '<Esc>')
will execute vim.keymap.set('i', 'jk', '<Esc>)
and vim.keymap.set('i', 'ол', <Esc>)
.
map('n', '<leader>a', ':echo 123')
will execute vim.keymap.set('n', '<leader>a', ':echo 123')
and vim.keymap.set('n', '<leader>ф', ':echo 123')
.
lhs
with <Plug>
, <Sid>
and <Snr>
will not translate and will be mapped as is.
del()
works in the same way, but with mappings removing. Also, del()
is
wrapped with a safetely call (pcall
) to avoid errors on duplicate characters
(helpful when usingnvim-cmp
).
---@param mode string|table Same mode short names as |nvim_set_keymap()|
---@param lhs string Left-hand side |{lhs}| of the mapping.
---@param rhs string|function Right-hand side |{rhs}| of the mapping. Can also be a Lua function.
---@param opts table|nil A table of |:map-arguments|.
function M.map(mode, lhs, rhs, opts)
---@param mode string|table Same mode short names as |nvim_set_keymap()|
---@param lhs string Left-hand side |{lhs}| of the mapping.
---@param opts table|nil A table of optional arguments:
--- - buffer: (number or boolean) Remove a mapping from the given buffer.
--- When "true" or 0, use the current buffer.
function M.del(mode, lhs, opts)
Hack get_keymap
functions. See :h nvim_set_keymap()
and :h nvim_buf_set_keymap()
.
After this hack, nvim_set_keymap/nvim_buf_set_keymap
will return only
latin mappings (without translated mappings). Very useful for work with
nvim-cmp
(see #8)
Usage:
local langmapper = require("langmapper")
langmapper.setup()
langmapper.hack_get_keymap()
Original keymap's functions, that were wrapped with translation functions if
hack_keymap
is true
:
-- When you don't need some mapping to be translated. For example, I don't translate `jk`.
`original_set_keymap()` -- vim.api.nvim_set_keymap
`original_buf_set_keymap() -- vim.api.nvim_buf_set_keymap
`original_del_keymap()` -- vim.api.nvim_del_keymap
`original_buf_del_keymap()` -- vim.api.nvim_buf_del_keymap
`put_back_keymap()` -- Set original functions back
NOTE: No original
vim.keymap.set/del
becausenvim_set/del_keymap
is used inside
Another functions-wrappers with translates and same contracts:
`wrap_nvim_set_keymap()`
`wrap_nvim_del_keymap()`
`wrap_nvim_buf_set_keymap()`
`wrap_nvim_buf_del_keymap()`
Translate 'lhs' to 'to_lang' layout. If in 'to_lang' layout no specified
default_layout
, uses global default_layout
To translate back to English
characters, set 'to_lang' to default
and pass the name of the layout to
translate from as the third parameter.
---@param lhs string Left-hand side |{lhs}| of the mapping.
---@param to_lang string Name of layout or 'default' if need translating back to English layout
---@param from_lang? string Name of layout.
---@return string
function M.translate_keycode(lhs, to_lang, from_lang)
Example:
local utils = require('langmapper.utils')
local keycode = '<leader>gh'
local tr_keycode = utils.translate_keycode(keycode, 'ru') -- '<leader>пр'
Translates each key of table for all layouts in use_layouts
option
(recursive).
---@param dict table Dict-like table
---@return table
function M.trans_dict(dict)
Example:
local keycode_dict = { ['s'] = false, ['<leader>'] = { ['d'] = 'copy' }, ['<S-TAB>'] = 'prev_source' }
local result = utils.trans_dict(keycode_dict)
-- {
-- ['s'] = false,
-- ['ы'] = false,
-- ['<leader>'] = {
-- ['d'] = 'copy',
-- ['в'] = 'copy',
-- },
-- ['<S-TAB>'] = 'prev_source',
-- }
Translates each value of the list for all layouts in use_layouts
option.
Non-string value is ignored. Translated value will be added to the end.
---@param dict table Dict-like table
---@return table
function M.trans_list(dict)
Example:
local keycode_list = { '<leader>d', 'ab', '<S-Tab>' }
local translated = utils.trans_list(keycode_list)
-- { '<leader>d', 'ab', '<S-Tab>', '<leader>в', 'фи' }