1

I know there are questions/answers to execute commands with a single shortcut, but in my case I can't think how to do multiple maps with a single shortcut in Insert Mode. In other words, what I want:

  • Suppose I'm in Insert Mode now
  • Press, for example, <C-s> to do multiple maps, like
    • a;o ---> año
    • e;e ---> eñe
    • and more than 30 maps
  • Press <C-s> again to undo those maps (so a;o is a;o now)

I just know how to do maps (one by one on .vimrc) and keep them forever (obviously, deactivate manually is not an option).

Can you help me with the structure or the vim features I have to learn? It could be useful to type Spanish words with US keyboard and without changing entire keyboard layout each time.

1
  • The default digraphs for accented characters aren't awful (:help digraphs, :help i_CTRL-K). I could write about those, but if you'd rather have the toggle mapping I have an idea or two as well.
    – D. Ben Knoble
    Oct 12 '20 at 22:19
3

This looks like a textbook example of the sort of situation for which the 'keymap' option exists.

To use this option, first create a file ~/.vim/keymap/spanish.vim with the contents:

" Uncomment the following line to set a "short" name for the keymap.
" This will be shown in the statusline instead of the full keymap name.
"let b:keymap_name = "sp"

loadkeymap
a;o    año
e;e    eñe
" etc

Then add the line set keymap=spanish to your .vimrc (or run the same :set command manually).

You can then toggle the entire set of mappings in insert mode or command-line mode by pressing Ctrl-^. If you want to use a different shortcut for this, you can of course set up an imap to do so.

When re-entering insert mode, Vim will remember whether the keymap was in use the last time you left insert mode: it stores this info in the 'iminsert' option.

See :help mbyte-keymap for more details, including how to change the colour of the cursor in GUI Vim when the keymap is in use.

3

I have an alternative recommendation, and a flexible script you can adapt.


Use Digraphs

Vim has wonderful support for a large swath of characters that are hard to enter on many ISO/American Keyboards (among others). Without even looking, I could tell that the digraph for ñ is n~ (and the same goes for most accented characters). Ironically, this isn't documented; the official digraph is n?. There are patterns, some arbitrary, in the digraphs table to help you remember (e.g., all greek characters end with *).

To enter a digraph, you press Ctrl-K. If you already have a character, you can use ga to see what the digraph would be. And :help digraph-table in a pinch.

Some computer systems even have general facilities for entering characters. For example, on mac, Alt-n adds a tilde to the next character typed, where applicable.


Data-driven mappings

This won't be entirely different from user938271's answer, but the presentation is a little different (perhaps a little less robust, but certainly easier to read).

I've had this autoload script that implements a version of "deadkeys" in my Dotfiles for a while; the plugin part includes a command to toggle the mappings, but you could make it mapping instead by using a variable to remember which state you're in, and then flip it.

(At one point, I even had an insert-mode mapping to turn on the mappings; they were deleted automatically when I left insert-mode! To be honest, I rarely use this now anyways, though it helps when writing, e.g., a lot of French.)

The basic idea is to create a data-structure of mappings

let s:digraphs = [
      \ { 'letter': 'a', 'accent': '`' },
      \ { 'letter': 'e', 'accent': '`' },
      \ { 'letter': 'i', 'accent': '`' },
      \ { 'letter': 'o', 'accent': '`' },
      \ { 'letter': 'u', 'accent': '`' },
      \ { 'letter': 'A', 'accent': '`' },
      \ ...,
      \ ]

And then create the maps:

let s:map_template = 'inoremap <buffer> <silent> %s%s <C-k>%s%s'
let s:unmap_template = 'iunmap <buffer> %s%s'

function! s:make_maps(letter, accent) abort
  let l:cmd_1 = printf(s:map_template, a:letter, a:accent, a:letter, a:accent)
  let l:cmd_2 = printf(s:map_template, a:accent, a:letter, a:letter, a:accent)
  for l:cmd in [l:cmd_1, l:cmd_2]
    execute l:cmd
  endfor
endfunction

function! bk#digraphs#map(letter, accent) abort
  call s:make_maps(a:letter, a:accent)
endfunction

function! bk#digraphs#activate() abort
  for digraph_dict in s:digraphs
    call bk#digraphs#map(digraph_dict.letter, digraph_dict.accent)
  endfor
endfunction

function! bk#digraphs#unmap(letter, accent) abort
  execute printf(s:unmap_template, a:letter, a:accent)
  execute printf(s:unmap_template, a:accent, a:letter)
endfunction

function! bk#digraphs#deactivate() abort
  for digraph_dict in s:digraphs
    silent! call bk#digraphs#unmap(digraph_dict.letter, digraph_dict.accent)
  endfor
endfunction

If you need to save and restore other mappings, you'll need something more robust.

You'd also need to adapt this to your specific a;o case, as I was more concerned with turning n~ into ñ automatically; but you could have generic left-hand-side and right-hand-side.

3

As a naive attempt, try this:

inoremap a;o año
inoremap e;e eñe

const s:LHS = ['a;o', 'e;e']

let s:MAPSAVE = []
for s:key in s:LHS
    let s:MAPSAVE += [maparg(s:key, 'i', v:false, v:true)]
endfor
lockvar! s:MAPSAVE
unlet! s:key
let s:mappingsEnabled = v:true

inoremap <expr> <c-s> <sid>ToggleMappings()
fu s:ToggleMappings()
    let i = 0
    for key in s:LHS
        if s:mappingsEnabled
            exe 'silent! iunmap ' .. key
        else
            call mapset('i', v:false, s:MAPSAVE[i])
        endif
        let i += 1
    endfor
    let s:mappingsEnabled = !s:mappingsEnabled
    return "\<Ignore>"
endfu

Make sure to include all the left-hand-sides of your mappings in the list constant s:LHS:

const s:LHS = ['a;o', 'e;e']
               ^----------^

You'll need at least the patch 8.2.0807 to be able to invoke the mapset() function which can restore a mapping based on the info given by maparg().

You might also need the patches 8.2.0852 and 8.2.0856 to be able to map something to C-s:

For more info, see:


For something more reliable, try to create the directory ~/.vim/import. Inside the latter, create the file map.vim. Inside the latter, write this code:

vim9script

export def MapSave(argkeys: any, mode = '', wantlocal = false): list<dict<any>>
    if type(argkeys) != v:t_list && type(argkeys) != v:t_string
        return []
    endif
    var keys: list<string> = type(argkeys) == v:t_list ? argkeys : [argkeys]
    var save: list<dict<any>> = []
    for key in keys
        for m in mode == '' ? [''] : split(mode, '\zs')
            var maparg: dict<any> = Maparg(key, m, wantlocal)
            save += [maparg]
        endfor
    endfor
    return save
enddef

export def MapRestore(save: list<dict<any>>)
    if empty(save)
        return
    endif
    for maparg in save
        if NotInRightBuffer(maparg) | continue | endif
        if has_key(maparg, 'unmapped')
            var cmd: string = GetMappingCmd(maparg)
            exe 'sil! ' .. cmd .. ' ' .. (maparg.buffer ? ' <buffer> ' : '') .. maparg.lhs
        else
            for mode in split(maparg.mode, '\zs')
                maparg->deepcopy()->extend({mode: mode})->Reinstall()
            endfor
        endif
    endfor
enddef

def Maparg(name: string, mode: string, wantlocal: bool): dict<any>
    var maparg: dict<any> = maparg(name, mode, false, true)
    if empty(maparg) || wantlocal && !Islocal(maparg)
        maparg = {
            unmapped: true,
            lhs: name,
            mode: mode == '' ? ' ' : mode,
            buffer: wantlocal,
            }
    elseif !wantlocal && Islocal(maparg)
        exe mode .. 'unmap <buffer> ' .. name
        var local_maparg: dict<any> = deepcopy(maparg)->extend({bufnr: bufnr('%')})
        maparg = Maparg(name, mode, false)
        MapRestore([local_maparg])
    else
        extend(maparg, {
            lhs: name,
            rhs: maparg(name, mode)->escape('|'),
            })
    endif
    if Islocal(maparg)
        extend(maparg, {bufnr: bufnr('%')})
    endif
    return maparg
enddef

def Reinstall(maparg: dict<any>)
    var cmd: string = GetMappingCmd(maparg)
    exe cmd
        .. ' '
        .. (maparg.buffer  ? ' <buffer> ' : '')
        .. (maparg.expr    ? ' <expr>   ' : '')
        .. (maparg.nowait  ? ' <nowait> ' : '')
        .. (maparg.silent  ? ' <silent> ' : '')
        .. (maparg.script  ? ' <script> ' : '')
        .. maparg.lhs
        .. ' '
        .. maparg.rhs
enddef

def Islocal(maparg: dict<any>): bool
    return get(maparg, 'buffer', false)
enddef

def NotInRightBuffer(maparg: dict<any>): bool
    return Islocal(maparg) && bufnr('%') != get(maparg, 'bufnr', false)
enddef

def GetMappingCmd(maparg: dict<any>): string
    var cmd: string
    if has_key(maparg, 'unmapped')
        if maparg.mode == '!'
            cmd = 'unmap!'
        else
            cmd = maparg.mode .. 'unmap'
        endif
    else
        if maparg.mode == '!'
            cmd = maparg.noremap ? 'noremap!' : 'map!'
        else
            cmd = maparg.mode
            cmd ..= maparg.noremap ? 'noremap' : 'map'
        endif
    endif
    return cmd
enddef

This will provide 2 functions: MapSave() and MapRestore(). As their name suggests, they can save and restore a list of mappings. They can be imported from anywhere; a legacy Vim script, or a Vim9 script. In particular, they can be imported from your vimrc, regardless of how it's written:

import {MapSave, MapRestore} from 'map.vim'

If your vimrc is written in legacy Vim script, you can use these functions like this for a list of global mappings:

" save the mappings
let s:my_global_mappings = s:MapSave(['key1', 'key2', ...], 'i')
                                                             ^
                                                             insert mode

" do something which removes or alters the mappings
" ...

" restore the mappings
call s:MapRestore(s:my_global_mappings)

And for a list of local mappings:

" save the mappings
let s:my_local_mappings = s:MapSave(['key1', 'key2', ...], 'n', v:true)
                                                            │   │
                                                            │   └ save local mappings
                                                            └ normal mode

" do something which removes or alters the mappings
" ...

" restore the mappings
call s:MapRestore(s:my_local_mappings)

If your vimrc is written in Vim9 script, and you want to save and restore global mappings:

# save the mappings
var my_global_mappings = MapSave(['key1', 'key2', ...], 'i')
                                                         ^
                                                         insert mode

# do something which removes or alters the mappings
# ...

# restore the mappings
MapRestore(my_global_mappings)

For local mappings:

# save the mappings
var my_local_mappings = MapSave(['key1', 'key2', ...], 'n', true)
                                                        │   │
                                                        │   └ save local mappings
                                                        └ normal mode

# do something which removes or alters the mappings
# ...

# restore the mappings
MapRestore(my_local_mappings)

Assuming your vimrc is written in Vim script legacy, here's how you could use these functions for your particular issue :

import {MapSave, MapRestore} from 'map.vim'

inoremap a;o año
inoremap e;e eñe

const s:LHS = ['a;o', 'e;e']

const s:SAVE = s:MapSave(s:LHS, 'i')
let s:mappingsEnabled = v:true

inoremap <expr> <c-s> <sid>ToggleMappings()
fu s:ToggleMappings()
    if s:mappingsEnabled
        for key in s:LHS
            exe 'silent! iunmap ' .. key
        endfor
    else
        call s:MapRestore(s:SAVE)
    endif
    let s:mappingsEnabled = !s:mappingsEnabled
    return "\<Ignore>"
endfu

And if your vimrc is written in Vim9 script:

import {MapSave, MapRestore} from 'map.vim'

inoremap a;o año
inoremap e;e eñe

const LHS = ['a;o', 'e;e']

const SAVE = MapSave(LHS, 'i')
var mappingsEnabled = true

inoremap <expr> <c-s> <sid>ToggleMappings()
def ToggleMappings(): string
    if mappingsEnabled
        for key in LHS
            exe 'silent! iunmap ' .. key
        endfor
    else
        MapRestore(SAVE)
    endif
    mappingsEnabled = !mappingsEnabled
    return "\<Ignore>"
enddef

This will require a recent Vim version; at least 8.2.2015. It works on 8.2.2159.

For more info, see:

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.