¿Cómo guardar y restaurar un mapeo?


12

Estoy desarrollando un complemento para Vim y me gustaría definir un mapeo que estaría disponible solo durante la "ejecución del complemento".

Hasta ahora, el flujo de trabajo (simplificado) del complemento es el siguiente:

  1. El usuario llama a un comando del complemento
  2. El comando llama a la función de pretratamiento:

    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. Se llama a otra función que cambia el estado del búfer ( Foo()o Bar()en las últimas líneas de la función anterior)

  4. El usuario usa el mapeo para llamar a la función de desmontaje
  5. La función de eliminación elimina la asignación creada:

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

No estoy satisfecho con la forma en que manejo mi asignación: si el usuario ya la asignó a otra cosa, perderá su asignación original.

Entonces mi pregunta es: ¿cómo puedo guardar lo que <C-c>está asignado (si está asignado) y restaurarlo en mi función de desmontaje? ¿Hay una función incorporada para hacerlo? Pensé sobre grepel resultado de, :nmap <C-c>pero eso no se siente realmente "limpio".

Algunas notas al margen:

  • Sé que LearnVimScriptTheHardWay tiene una sección sobre eso , pero dicen que use un ftplugin que no es posible aquí: el complemento no depende de un tipo de archivo
  • Podría crear una variable para permitir al usuario elegir qué teclas usar: probablemente sea lo que haré, pero estoy principalmente interesado en cómo guardar y restaurar.
  • Podría usar un líder local, pero creo que es un poco exagerado y todavía tengo curiosidad sobre lo de guardar y restaurar.

Respuestas:


24

Podrías usar la maparg()función.

Para probar si el usuario asignó algo <C-c>en modo normal, escribiría:

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

Si el usuario asignó algo, para almacenarlo {rhs}en una variable, escribiría:

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

Si desea más información sobre el mapeo, como:

  • ¿Es silencioso ( <silent>argumento)?
  • ¿Es local para el búfer actual ( <buffer>argumento)?
  • ¿Es la {rhs}evaluación de una expresión ( <expr>argumento)?
  • ¿reasigna el {rhs}( nnoremapvs nmap)?
  • Si el usuario tiene otra asignación que comienza con <C-c>, ¿Vim espera a que se escriban más caracteres ( <nowait>argumento)?
  • ...

Entonces, podrías dar un tercer y cuarto argumento: 0y 1.
0porque está buscando un mapeo y no una abreviatura, y 1porque quiere un diccionario con un máximo de información y no solo el {rhs}valor:

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

Asumiendo que el usuario no utilizó ningún argumento especial en su mapeo, y que no reasigna el {rhs}, para restaurarlo, simplemente podría escribir:

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

" do some stuff which changes the mapping

exe 'nnoremap <C-c> ' . rhs_save

O para estar seguro y restaurar todos los argumentos posibles:

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

Editar: Lo siento, me acabo de dar cuenta de que no funcionaría como se esperaba si el usuario llama a una función de script local en {rhs}el mapeo.

Supongamos que el usuario tiene la siguiente asignación dentro de su vimrc:

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

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

Cuando golpea <C-c>, muestra el mensaje hello world!.

Y en su complemento, guarda un diccionario con toda la información, luego cambia temporalmente su mapeo de esta manera:

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

Ahora, se mostrará bye all!. Su complemento funciona y, cuando termina, intenta restaurar el mapeo con el comando anterior.

Probablemente fallará con un mensaje como este:

E117: Unknown function: <SNR>61_FuncA

61es solo el identificador del script en el que se ejecutaría su comando de mapeo. Podría ser cualquier otro número. Si su complemento es el archivo número 42 del sistema del usuario, lo será 42.

Dentro de un script, cuando se ejecuta un comando de mapeo, Vim traduce automáticamente la notación <SID>al código de clave especial <SNR>, seguido de un número único para el script y un guión bajo. Tiene que hacer esto, porque cuando el usuario golpee <C-c>, la asignación se ejecutará fuera del script y, por lo tanto, no sabrá en qué script FuncA()está definido.

El problema es que la asignación original se originó en un script diferente al de su complemento, por lo que aquí la traducción automática es incorrecta. Utiliza el identificador de su script, mientras que debe usar el identificador del usuario vimrc.

Pero podrías hacer la traducción manualmente. El diccionario map_savecontiene una clave llamada 'sid'cuyo valor es el identificador correcto.
Entonces, para hacer que el comando de restauración anterior sea más robusto, puede reemplazarlo map_save.rhscon:

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

Si el {rhs}mapeo original contiene <SID>, debe traducirse correctamente. De lo contrario, no se debe cambiar nada.

Y si desea acortar un poco el código, puede reemplazar las 4 líneas que se ocupan de los argumentos especiales con:

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

La map()función debe convertir cada elemento de la lista ['buffer', 'expr', 'nowait', 'silent']en el argumento de mapeo correspondiente, pero solo si su clave interna map_saveno es cero. Y join()debe unir todos los elementos en una cadena.

Entonces, una forma más robusta de guardar y restaurar el mapeo podría 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:

Estoy enfrentando el mismo problema que tú, cómo guardar y restaurar una asignación en un complemento de dibujo. Y creo que encontré 2 problemas que la respuesta inicial no vio en el momento en que lo escribí, lo siento.

Primer problema, suponga que el usuario lo utiliza <C-c>en una asignación global pero también en una asignación local de búfer. Ejemplo:

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

En este caso, maparg()dará prioridad al mapeo 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}

Lo cual se confirma en :h maparg():

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

Pero tal vez no esté interesado en el mapeo local del búfer, tal vez quiera el global.
La única forma en que encontré, de manera confiable, obtener la información sobre el mapeo global, es tratar de desasignar temporalmente un mapeo potencial, sombreado y local del búfer usando la misma clave.

Se puede hacer en 4 pasos:

  1. guardar una asignación (local) de búfer local utilizando la clave <C-c>
  2. ejecutar :silent! nunmap <buffer> <C-c>para eliminar una asignación local de búfer (potencial)
  3. guardar el mapeo global ( maparg('<C-c>', 'n', 0, 1))
  4. restaurar la asignación local de búfer

El segundo problema es el siguiente. Supongamos que el usuario no asignó nada <C-c>, entonces la salida de maparg()será un diccionario vacío. Y en este caso, el proceso de restauración no consiste en la instalación de un mapeo ( :nnoremap), sino en la destrucción de un mapeo ( :nunmap).

Para intentar resolver estos 2 nuevos problemas, puede probar esta función para guardar asignaciones:

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

... y este para restaurarlos:

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

La Save_mappings()función podría usarse para guardar asignaciones.
Espera 3 argumentos:

  1. una lista de llaves; ejemplo:['<C-a>', '<C-b>', '<C-c>']
  2. un modo; ejemplo: npara modo normal o xpara modo visual
  3. una bandera booleana; si es así 1, significa que está interesado en los mapeos globales, y si lo está 0, en los locales

Con él, usted podría ahorrar las asignaciones globales utilizando las teclas C-a, C-by C-c, en modo normal, dentro de un diccionario:

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

Luego, más tarde, cuando desee restaurar las asignaciones, puede llamar Restore_mappings()y pasar el diccionario que contiene toda la información como argumento:

call Restore_mappings(your_saved_mappings)

Podría haber un tercer problema al guardar / restaurar asignaciones locales de búfer. Porque, entre el momento en que guardamos las asignaciones y el momento en que intentamos restaurarlas, el búfer actual puede haber cambiado.

En este caso, tal vez la Save_mappings()función podría mejorarse guardando el número del búfer actual ( bufnr('%')).

Y luego, Restore_mappings()usaría esta información para restaurar las asignaciones de búfer local en el búfer correcto. Probablemente podríamos usar el :bufdocomando, prefijar el último con un recuento (que coincida con el número de búfer guardado previamente) y sufijarlo con el comando de mapeo.

Tal vez algo como:

:{original buffer number}bufdo {mapping command}

Tendríamos que verificar primero si el búfer todavía existe, usando la bufexists()función, porque podría haberse eliminado mientras tanto.


Increíble, eso es exactamente lo que necesitaba. ¡Gracias!
statox

2

En mis complementos, cuando tengo asignaciones temporales, siempre son búferes locales; realmente no me importa guardar asignaciones globales ni nada complejo que las involucre. De ahí mi lh#on#exit().restore_buffer_mapping()función auxiliar: de lh-vim-lib .

Al final, lo que sucede es lo siguiente:

" 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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.