Skip to content

A plugin that makes Neovim more friendly to non-English input methods 🤝

License

Notifications You must be signed in to change notification settings

Wansmer/langmapper.nvim

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Langmapper

A plugin that makes Neovim more friendly to non-English input methods 🤝

TLDR

  • 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.

Requirements

  • 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 and map.g.localleader before langmapper.setup();

Examples of CLI utilities:

Installation

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

Settings

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,
    },
  },
}

Usage

Simple

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 😎)

Manually

Set up your layout in config, set hack_keymap to false, and call langmapper.setup(opts).

For regular mapping:

-- this function completely repeats contract of vim.keymap.set
local map = require('langmapper').map

map('n', '<Leader>e', '<Cmd>Neotree toggle focus<Cr>')

Mapping inside other plugin:

-- 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',
})

With automapping

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 = {} in lazy.nvim also will be translated.

How to use with X plugin

Check out the issues and discussions. There is probably already a recipe for the plugin you need.

API

Usage:

local langmapper = require('langmapper')

langmapper.map(...)
langmapper.automapping(...)
-- etc

automapping()

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)

map()/del()

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()

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()

Other

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 because nvim_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()`

Utils

translate_keycode()

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>пр'

trans_dict()

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',
-- }

trans_list()

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>в', 'фи' }