如何保存和还原映射?


12

我正在为Vim开发一个插件,我想定义一个仅在“执行插件”时可用的映射。

到目前为止,插件的(简化)工作流程如下:

  1. 用户调用插件命令
  2. 该命令调用预处理功能:

    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. 调用另一个函数来更改缓冲区的状态(Foo()Bar()在上一个函数的最后一行中)

  4. 用户使用映射来调用拆解函数
  5. 拆卸功能删除创建的映射:

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

我对处理映射的方式不满意:如果用户已经将其映射到其他对象,他将失去其原始映射。

所以我的问题是:如何保存<C-c>映射到的内容(如果已映射)并将其恢复到我的拆卸功能中? 有内置功能吗?我虽然想了解grep的结果,:nmap <C-c>但是感觉并不“干净”。

一些注意事项:

  • 我知道LearnVimScriptTheHardWay中有关于这一部分的内容,但他们说要使用ftplugin,这在这里是不可能的:插件不依赖于文件类型
  • 我可以创建一个变量,让用户选择要使用的键:这可能是我要做的,但是我主要对如何执行保存和还原感兴趣。
  • 我可以聘用当地领导人,但我认为这有点过分,我仍然主要对保存和恢复事情感到好奇。

Answers:


24

您可以使用该maparg()功能。

要测试用户是否<C-c>在正常模式下映射了某些内容,您可以编写:

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

如果用户映射了一些内容,以将{rhs}变量存储在变量中,则应编写:

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

如果您想了解有关映射的更多信息,例如:

  • 沉默(<silent>争论)吗?
  • 它在当前缓冲区(<buffer>参数)本地吗?
  • {rhs}一个表达式(评价<expr>参数)?
  • 它会重新映射{rhs}nnoremapvs nmap)吗?
  • 如果用户有另一个以开头的映射<C-c>,Vim是否等待输入更多字符(自<nowait>变量)?
  • ...

然后,您可以给出第三个和第四个参数:01
0因为您要查找的是映射而不是缩写,并且1因为您想要的是具有最大信息量而不只是{rhs}值的字典:

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

假设用户在映射中没有使用任何特殊参数,并且没有重新映射{rhs}来还原它,则可以简单地编写:

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

" do some stuff which changes the mapping

exe 'nnoremap <C-c> ' . rhs_save

或者可以确定并恢复所有可能的参数:

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

编辑:对不起,我只是意识到,如果用户{rhs}在映射的中调用脚本本地函数,它将无法按预期工作。

假设用户内部具有以下映射vimrc

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

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

当他点击时<C-c>,它会显示消息hello world!

然后在您的插件中,保存包含所有信息的字典,然后像这样临时更改其映射:

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

现在,它将显示bye all!。您的插件可以完成一些工作,结束后,它会尝试使用上一个命令恢复映射。

它可能会失败,并显示以下消息:

E117: Unknown function: <SNR>61_FuncA

61只是将在其中执行映射命令的脚本的标识符。它可以是任何其他数字。如果您的插件是来自用户系统的第42个文件,则它将为42

在脚本内部,执行映射命令时,Vim会自动将表示法<SID>转换为特殊的键代码<SNR>,后跟脚本唯一的数字和下划线。它必须这样做,因为当用户点击时<C-c>,映射将在脚本之外执行,因此它将不知道在哪个脚本FuncA()中定义。

问题在于原始映射是从与您的插件不同的脚本中获取的,因此这里的自动翻译是错误的。它使用脚本的标识符,而应使用用户的标识符vimrc

但是您可以手动进行翻译。词典map_save包含一个名为'sid'的键,其值是正确的标识符。
因此,为使先前的恢复命令更健壮,可以替换map_save.rhs为:

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

如果{rhs}包含原始映射的<SID>,则应正确翻译。否则,应保持不变。

而且,如果您想稍微缩短代码,则可以将照顾特殊参数的4行替换为:

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

map()函数应将列表中的每个项目都转换['buffer', 'expr', 'nowait', 'silent']为相应的映射参数,但前提是其内部的键map_save非零。并且join()应将所有项目连接成一个字符串。

因此,保存和还原映射的更可靠的方法可能是:

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

编辑2:

我面临与您相同的问题,即如何在图形插件中保存和还原映射。我想我发现了2个问题,这些问题在我写这篇文章时都没有看到,对此感到抱歉。

第一个问题,假设用户<C-c>在全局映射中使用,也在缓冲区局部映射中使用。例:

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

在这种情况下,maparg()将优先进行本地映射:

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

这也是证实:h maparg()

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

但是,也许您对缓冲区局部映射不感兴趣,也许您想要全局映射。
我发现可靠地获取有关全局映射的信息的唯一方法是尝试使用相同的键临时取消映射潜在的,带有阴影的缓冲区局部映射。

可以通过4个步骤完成:

  1. 使用键保存(潜在的)本地缓冲区映射 <C-c>
  2. 执行:silent! nunmap <buffer> <C-c>以删除(潜在的)缓冲区本地映射
  3. 保存全局映射(maparg('<C-c>', 'n', 0, 1)
  4. 恢复本地缓冲区映射

第二个问题如下。假设用户没有将任何内容映射到<C-c>,则的输出maparg()将是一个空字典。在这种情况下,还原过程不在于安装映射(:nnoremap),而在于销毁映射(:nunmap)。

要尝试解决这两个新问题,您可以尝试使用以下功能保存映射:

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

...和恢复它们的这个:

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

Save_mappings()功能可用于保存映射。
它需要3个参数:

  1. 键列表;例:['<C-a>', '<C-b>', '<C-c>']
  2. 模式 示例:n用于普通模式或x可视模式
  3. 布尔标志;如果是1,则表示您对全局映射感兴趣,如果是0,则对本地映射感兴趣

有了它,您可以在正常模式下使用C-aC-b和键C-c在字典中保存全局映射:

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

然后,稍后,当您要还原映射时,可以调用Restore_mappings(),将包含所有信息的字典作为参数传递:

call Restore_mappings(your_saved_mappings)

保存/恢复本地缓冲区映射时,可能会出现第三个问题。因为在我们保存映射的那一刻到尝试还原它们的那一刻之间,当前缓冲区可能已更改。

在这种情况下,也许Save_mappings()可以通过节省当前缓冲区(bufnr('%'))的数量来改善功能。

然后,Restore_mappings()将使用此信息在正确的缓冲区中还原缓冲区本地映射。我们可能可以使用该:bufdo命令,为后者加上一个计数(与先前保存的缓冲区号匹配),并在其后加上mapping命令。

也许像这样:

:{original buffer number}bufdo {mapping command}

我们必须首先使用该bufexists()函数检查缓冲区是否仍然存在,因为与此同时它可能已被删除。


太神奇了,这正是我所需要的。谢谢!
statox

2

在我的插件中,当我有临时映射时,它们总是在本地缓冲-我真的不在乎保存全局映射,也不在乎涉及它们的复杂事物。因此,我的lh#on#exit().restore_buffer_mapping()助手功能-来自lh-vim-lib

最后,将发生以下情况:

" 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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.