Como salvar e restaurar um mapeamento?

12

Estou desenvolvendo um plugin para o Vim e gostaria de definir um mapeamento que estaria disponível apenas durante a "execução do plugin".

Até agora, o fluxo de trabalho (simplificado) do plug-in é o seguinte:

  1. O usuário chama um comando do plug-in
  2. O comando chama a função de pré-tratamento:

    function! s:PreTreatmentFunction(function, ...)
        " Do some pretreatment stuff
    
        " Create a mapping to call the TearDown
        nnoremap <C-c> :call TeardDown()<CR>
    
        " Call a function depending on the parameter passed to this one
        if function == "foo"
            call Foo()
        else
            call Bar()
        endif
    endfunction
    
  3. É chamada outra função que altera o estado do buffer ( Foo()ou Bar()nas últimas linhas da função anterior)

  4. O usuário usa o mapeamento para chamar a função de desmontagem
  5. A função destacável remove o mapeamento criado:

    function! s:TearDown()
        " Do some tear down stuff
    
        " Remove the mapping
        unmap <C-c>
    endfunction
    

Não estou satisfeito com a maneira como manejo meu mapeamento: se o usuário já o mapeou para outra coisa, ele perderá seu mapeamento original.

Portanto, minha pergunta é: como posso salvar o que <C-c>está mapeado (se estiver mapeado) e restaurá-lo na minha função de desmontagem? Existe um recurso interno para fazer isso? Eu pensei sobre grepo resultado de, :nmap <C-c>mas isso não parece realmente "limpo".

Algumas notas laterais:

  • Eu sei que o LearnVimScriptTheHardWay tem uma seção sobre isso , mas eles dizem usar um ftplugin que não é possível aqui: o plug-in não depende de um tipo de arquivo
  • Eu poderia criar uma variável para permitir ao usuário escolher quais chaves usar: Provavelmente é o que farei, mas estou principalmente interessado em como salvar e restaurar.
  • Eu poderia usar um líder local, mas acho que é um pouco exagerado e ainda estou principalmente curioso sobre a coisa de salvar e restaurar.
statox
fonte

Respostas:

24

Você poderia usar a maparg()função

Para testar se o usuário mapeou algo <C-c>no modo normal, você deve escrever:

if !empty(maparg('<C-c>', 'n'))

Se o usuário mapeou algo, para armazenar o {rhs}em uma variável, você deve escrever:

let rhs_save = maparg('<C-c>', 'n')

Se você quiser obter mais informações sobre o mapeamento, como:

  • é silencioso ( <silent>argumento)?
  • é local para o buffer atual ( <buffer>argumento)?
  • é a {rhs}avaliação de uma expressão ( <expr>argumento)?
  • remapear o {rhs}( nnoremapvs nmap)?
  • se o usuário tiver outro mapeamento que comece com <C-c>, o Vim espera que mais caracteres sejam digitados ( <nowait>argumento)?
  • ...

Então, você poderia dar um terceiro e um quarto argumento: 0e 1.
0porque você está procurando um mapeamento e não uma abreviação e 1porque deseja um dicionário com o máximo de informações e não apenas o {rhs}valor:

let map_save = maparg('<C-c>', 'n', 0, 1)

Supondo que o usuário não tenha usado nenhum argumento especial em seu mapeamento e que não remapeie o {rhs}, para restaurá-lo, você pode simplesmente escrever:

let rhs_save = maparg('<C-c>', 'n')

" do some stuff which changes the mapping

exe 'nnoremap <C-c> ' . rhs_save

Ou, para ter certeza e restaurar todos os argumentos possíveis:

let map_save = maparg('<C-c>', 'n', 0, 1)

" do some stuff which changes the mapping

exe (map_save.noremap ? 'nnoremap' : 'nmap') .
     \ (map_save.buffer ? ' <buffer> ' : '') .
     \ (map_save.expr ? ' <expr> ' : '') .
     \ (map_save.nowait ? ' <nowait> ' : '') .
     \ (map_save.silent ? ' <silent> ' : '') .
     \ ' <C-c> ' .
     \ map_save.rhs

Edit: Desculpe, acabei de perceber que não funcionaria conforme o esperado se o usuário chamar uma função local de script no {rhs}mapeamento.

Suponha que o usuário tenha o seguinte mapeamento dentro dele vimrc:

nnoremap <C-c> :<C-U>call <SID>FuncA()<CR>

function! s:FuncA()
    echo 'hello world!'
endfunction

Quando ele bate <C-c>, ele exibe a mensagem hello world!.

E no seu plugin, você salva um dicionário com todas as informações e altera temporariamente o mapeamento dessa maneira:

let map_save = maparg('<C-c>', 'n', 0, 1)
nnoremap <C-c> :<C-U>call <SID>FuncB()<CR>

function! s:FuncB()
    echo 'bye all!'
endfunction

Agora, ele será exibido bye all!. Seu plug-in faz algum trabalho e, quando termina, tenta restaurar o mapeamento com o comando anterior.

Provavelmente falhará com uma mensagem assim:

E117: Unknown function: <SNR>61_FuncA

61é apenas o identificador do script no qual seu comando de mapeamento seria executado. Pode ser qualquer outro número. Se o seu plug-in for o 42º arquivo originado no sistema do usuário, será 42.

Dentro de um script, quando um comando de mapeamento é executado, o Vim converte automaticamente a notação <SID>no código de chave especial <SNR>, seguido por um número exclusivo para o script e um sublinhado. É necessário fazer isso, porque quando o usuário clicar <C-c>, o mapeamento será executado fora do script e, portanto, não saberá em qual script FuncA()está definido.

O problema é que o mapeamento original foi originado em um script diferente do seu plug-in, portanto, aqui a tradução automática está incorreta. Ele usa o identificador do seu script, enquanto deve usar o identificador do usuário vimrc.

Mas você poderia fazer a tradução manualmente. O dicionário map_savecontém uma chave chamada 'sid'cujo valor é o identificador correto.
Portanto, para tornar o comando de restauração anterior mais robusto, você pode substituir map_save.rhspor:

substitute(map_save.rhs, '<SID>', '<SNR>' . map_save.sid . '_', 'g')

Se o {rhs}mapeamento original contiver <SID>, ele deverá ser traduzido corretamente. Caso contrário, nada deve ser alterado.

E se você quiser reduzir um pouco o código, poderá substituir as 4 linhas que cuidam dos argumentos especiais por:

join(map(['buffer', 'expr', 'nowait', 'silent'], 'map_save[v:val] ? "<" . v:val . ">": ""'))

A map()função deve converter cada item da lista ['buffer', 'expr', 'nowait', 'silent']no argumento de mapeamento correspondente, mas apenas se sua chave dentro map_savefor diferente de zero. E join()deve juntar todos os itens em uma string.

Portanto, uma maneira mais robusta de salvar e restaurar o mapeamento pode ser:

let map_save = maparg('<C-c>', 'n', 0, 1)

" do some stuff which changes the mapping

exe (map_save.noremap ? 'nnoremap' : 'nmap') .
    \ join(map(['buffer', 'expr', 'nowait', 'silent'], 'map_save[v:val] ? "<" . v:val . ">": ""')) .
    \ map_save.lhs . ' ' .
    \ substitute(map_save.rhs, '<SID>', '<SNR>' . map_save.sid . '_', 'g')

Edit2:

Estou enfrentando o mesmo problema que você, como salvar e restaurar um mapeamento em um plug-in de desenho. E acho que encontrei dois problemas que a resposta inicial não viu no momento em que escrevi, desculpe por isso.

Primeiro problema, suponha que o usuário use <C-c>em um mapeamento global, mas também em um mapeamento local de buffer. Exemplo:

nnoremap          <C-c>    :echo 'global mapping'<CR>
nnoremap <buffer> <C-c>    :echo 'local  mapping'<CR>

Nesse caso, maparg()dará prioridade ao mapeamento local:

:echo maparg('<C-c>', 'n', 0, 1)

---> {'silent': 0, 'noremap': 1, 'lhs': '<C-C>', 'mode': 'n', 'nowait': 0, 'expr': 0, 'sid': 7, 'rhs': ':echo ''local  mapping''<CR>', 'buffer': 1}

O que é confirmado em :h maparg():

    The mappings local to the current buffer are checked first,
    then the global mappings.

Mas talvez você não esteja interessado no mapeamento local do buffer, talvez queira o global.
A única maneira de encontrar, de maneira confiável, as informações sobre o mapeamento global, é tentar remover temporariamente o mapeamento de um mapeamento potencial, sombreado e local do buffer usando a mesma chave.

Isso pode ser feito em 4 etapas:

  1. salve um mapeamento local de buffer (potencial) usando a chave <C-c>
  2. execute :silent! nunmap <buffer> <C-c>para excluir um mapeamento local de buffer (potencial)
  3. salve o mapeamento global ( maparg('<C-c>', 'n', 0, 1))
  4. restaurar o mapeamento local do buffer

A segunda questão é a seguinte. Suponha que o usuário não mapeou nada para <C-c>, então a saída de maparg()será um dicionário vazio. E, neste caso, o processo de restauração não consiste na instalação de um mapeamento ( :nnoremap), mas na destruição de um mapeamento ( :nunmap).

Para tentar resolver esses 2 novos problemas, você pode tentar esta função para salvar mapeamentos:

fu! Save_mappings(keys, mode, global) abort
    let mappings = {}

    if a:global
        for l:key in a:keys
            let buf_local_map = maparg(l:key, a:mode, 0, 1)

            sil! exe a:mode.'unmap <buffer> '.l:key

            let map_info        = maparg(l:key, a:mode, 0, 1)
            let mappings[l:key] = !empty(map_info)
                                \     ? map_info
                                \     : {
                                        \ 'unmapped' : 1,
                                        \ 'buffer'   : 0,
                                        \ 'lhs'      : l:key,
                                        \ 'mode'     : a:mode,
                                        \ }

            call Restore_mappings({l:key : buf_local_map})
        endfor

    else
        for l:key in a:keys
            let map_info        = maparg(l:key, a:mode, 0, 1)
            let mappings[l:key] = !empty(map_info)
                                \     ? map_info
                                \     : {
                                        \ 'unmapped' : 1,
                                        \ 'buffer'   : 1,
                                        \ 'lhs'      : l:key,
                                        \ 'mode'     : a:mode,
                                        \ }
        endfor
    endif

    return mappings
endfu

... e este para restaurá-los:

fu! Restore_mappings(mappings) abort

    for mapping in values(a:mappings)
        if !has_key(mapping, 'unmapped') && !empty(mapping)
            exe     mapping.mode
               \ . (mapping.noremap ? 'noremap   ' : 'map ')
               \ . (mapping.buffer  ? ' <buffer> ' : '')
               \ . (mapping.expr    ? ' <expr>   ' : '')
               \ . (mapping.nowait  ? ' <nowait> ' : '')
               \ . (mapping.silent  ? ' <silent> ' : '')
               \ .  mapping.lhs
               \ . ' '
               \ . substitute(mapping.rhs, '<SID>', '<SNR>'.mapping.sid.'_', 'g')

        elseif has_key(mapping, 'unmapped')
            sil! exe mapping.mode.'unmap '
                                \ .(mapping.buffer ? ' <buffer> ' : '')
                                \ . mapping.lhs
        endif
    endfor

endfu

A Save_mappings()função pode ser usada para salvar mapeamentos.
Espera 3 argumentos:

  1. uma lista de chaves; exemplo:['<C-a>', '<C-b>', '<C-c>']
  2. um modo; exemplo: npara o modo normal ou xvisual
  3. uma bandeira booleana; se for 1, significa que você está interessado em mapeamentos globais e, se for 0, em locais

Com ele, você pode salvar os mapeamentos globais usando as teclas C-a, C-be C-c, no modo normal, dentro de um dicionário:

let your_saved_mappings = Save_mappings(['<C-a>', '<C-b>', '<C-c>'], 'n', 1)

Depois, quando desejar restaurar os mapeamentos, você pode chamar Restore_mappings(), passando o dicionário que contém todas as informações como argumento:

call Restore_mappings(your_saved_mappings)

Pode haver um terceiro problema ao salvar / restaurar mapeamentos de buffer-local. Porque, entre o momento em que salvamos os mapeamentos e o momento em que tentamos restaurá-los, o buffer atual pode ter sido alterado.

Nesse caso, talvez a Save_mappings()função possa ser aprimorada salvando o número do buffer atual ( bufnr('%')).

E então, Restore_mappings()usaria essas informações para restaurar os mapeamentos locais do buffer no buffer direito. Provavelmente poderíamos usar o:bufdo comando, prefixar o último com uma contagem (correspondendo ao número do buffer salvo anteriormente) e sufixá-lo com o comando mapping.

Talvez algo como:

:{original buffer number}bufdo {mapping command}

Teríamos que verificar primeiro se o buffer ainda existe, usando a bufexists()função, porque poderia ter sido excluído nesse meio tempo.

user9433424
fonte
Incrível é exatamente o que eu precisava. Obrigado!
Statox
2

Nos meus plugins, quando eu tenho mapeamentos temporários, eles sempre são locais de buffer - eu realmente não me importo em salvar mapeamentos globais nem em nada complexo que os envolva. Daí a minha lh#on#exit().restore_buffer_mapping()função auxiliar - de lh-vim-lib .

No final, o que acontece é o seguinte:

" excerpt from autoload/lh/on.vim
function! s:restore_buffer_mapping(key, mode) dict abort " {{{4
  let keybinding = maparg(a:key, a:mode, 0, 1)
  if get(keybinding, 'buffer', 0)
    let self.actions += [ 'silent! call lh#mapping#define('.string(keybinding).')']
  else
    let self.actions += [ 'silent! '.a:mode.'unmap <buffer> '.a:key ]
  endif
  return self
endfunction

" The action will be executed later on with:
" # finalizer methods {{{2
function! s:finalize() dict " {{{4
  " This function shall not fail!
  for l:Action in self.actions
    try
      if type(l:Action) == type(function('has'))
        call l:Action()
      elseif !empty(l:Action)
        exe l:Action
      endif
    catch /.*/
      call lh#log#this('Error occured when running action (%1)', l:Action)
      call lh#log#exception()
    finally
      unlet l:Action
    endtry
  endfor
endfunction


" excerpt from autoload/lh/mapping.vim
" Function: lh#mapping#_build_command(mapping_definition) {{{2
" @param mapping_definition is a dictionary witch the same keys than the ones
" filled by maparg()
function! lh#mapping#_build_command(mapping_definition)
  let cmd = a:mapping_definition.mode
  if has_key(a:mapping_definition, 'noremap') && a:mapping_definition.noremap
    let cmd .= 'nore'
  endif
  let cmd .= 'map'
  let specifiers = ['silent', 'expr', 'buffer']
  for specifier in specifiers
    if has_key(a:mapping_definition, specifier) && a:mapping_definition[specifier]
      let cmd .= ' <'.specifier.'>'
    endif
  endfor
  let cmd .= ' '.(a:mapping_definition.lhs)
  let rhs = substitute(a:mapping_definition.rhs, '<SID>', "\<SNR>".(a:mapping_definition.sid).'_', 'g')
  let cmd .= ' '.rhs
  return cmd
endfunction
Luc Hermitte
fonte