美文网首页
读书笔记:《Learn Vimscript the Hard W

读书笔记:《Learn Vimscript the Hard W

作者: Whyn | 来源:发表于2020-01-17 17:52 被阅读0次

    [TOC]

    前言

    《Learn Vimscript the Hard Way》,中文名为:《笨办法学 Vimscript》,是一本学习 Vimscript 编程语言的书。

    消息回显(Echoing Messages)

    消息回显主要包含两个命令:echoechomechoerr
    运行以下命令均可看到命令行窗口显示出打印信息:

    :echo "Hello, world!"
    :echom "Hello again, world!"
    :echoerr "show error message!"
    

    echoechomechoerr都可以进行信息打印,区别在于:

    • echo:打印的信息是瞬态的,在脚本执行完成后就会消失,后续无法再进行查看。
    • echom:打印的信息是会被保存到消息列表的,后续可通过:messages命令进行查看。
    • echoerr:打印错误信息,文本背景以红色进行强调,且打印的信息是会被保存到消息列表的,后续可通过:messages命令进行查看。

    通常编写复杂脚本时,都会使用echom进行消息打印,作为日志调试信息。

    :清空messages消息,可使用:messages clear

    更多详细信息,请查看::help echo/:help echom/:help echoerr/:help messages

    注释(Comments)

    再 Vimscript 中,注释以"开头:

    " Make space more useful
    nnoremap <space> za
    

    但有时注释会不起作用(Vimscript 脚本语言深坑之一),比如:

    :map <space> viw " Select word 
    

    上述代码的本意是把空格键<space>映射为按键viw,但 Vimscript 在解析时,会严格按照map的格式:map {lhs} {rhs}进行解析,则此时就会将viw "Select word作为空格键的映射,屏蔽了注释。因此当我们按下空格键时,就等同于输入viw<space>"<space>Select<space>word

    设置选项

    Vim 内置了很多设置选项,主要可分为两大类:

    1. 布尔选项(Boolean Option): 值为onoff的选项。其格式为:
    • set <name>:表示打开该选项
    • set no<name>:表示关闭该选项
    • set <name>!:翻转该选项
    • set <name>&:重置该选项,恢复到默认值

    举个栗子:

    :set number
    :set nonumber
    :set number!
    :set number&
    
    1. 赋值选项:选项设置有多个不同值可取。其格式为:set <name>=<value>

    举个栗子:

    :set numberwidth=10 " 设置一个值
    :set number numberwidth=6 " 可同时设置多个选项
    

    如果想查看当前选项的设置状态,可使用 检测选项(Checking Options),其语法为::set <name>?

    举个栗子:

    :set number?
    :set numberwidth?
    

    按键映射(Key Mapping)

    Vim 提供了强大的按键映射功能,可让你自主设置一个或多个按键映射为某些操作。

    按键映射可大致分为如下几类:

    1. 基本映射(Basic Mapping):将一个键映射到某些操作。

    举个栗子:

    " 将按键 - 映射为操作 dd
    :map - dd 
    
    1. 模式映射(Modal Mapping)Vim 对于不同的模式提供了相应的按键映射,比如:
    • 正常模式按键映射:nmap
    • 可视模式按键映射:vmap
    • 插入模式按键映射:imap
      ······
      更多模式信息,请查看::help mode/:help map-listing
    1. 精确模式(Strict Mapping):当我们使用map/nmap/vmap/imap等按键映射时,这些映射可能会带来副作用。比如:
    :nmap - dd
    :nmap \ -
    

    当我们输入\时,Vim 会映射为按键-,而由于按键-又在同一模式下被映射为dd,因此最终\就被映射成dd

    *map除了上述那种间接映射副作用外,还有一个更令人烦恼的副作用:按键递归,比如:

    :nmap j jj
    

    我们本意是想按一下j,往下移动两行,但是当第一次按下键j时,Vim 检测到其映射到jj,就会再次按下j,此时第二个j还未按下,Vim 又会将这个刚按下的j映射成jj···,这样就陷入了无限递归,直至到达文章结尾才会停止,也即是说j最终的作用就变成了到达文章结尾位置。

    为了解决上述*map带来的问题,Vim 提供了*noremap非递归按键映射命令,用这些命令进行按键映射就可以安全地实现我们需要的功能。

    :nmap x dd
    :nnoremap \ x 
    

    此时我们按下\Vim 就会直接将其映射为按键x,而不会在做其他操作。

    :在真实开发中,强烈建议一律使用*noremap命令进行按键映射,避免因自身疏漏或第三方插件按键映射产生冲突。

    各个按键映射的命令与对应作用的模式如下所示:

    命令 模式
    :map /:noremap /:unmap Normal, Visual, Select, Operator-pending
    :nmap/:nnoremap/:nunmap Normal
    :vmap/:vnoremap/:vunmap Visual and Select
    :smap/:snoremap/:sunmap Select
    :xmap/:xnoremap/:xunmap Visual
    :omap/:onoremap/:ounmap Operator-pending
    :map!/:noremap!/:unmap! Insert and Command-line
    :imap/:inoremap/:iunmap Insert
    :lmap/:lnoremap/:lunmap Insert, Command-line, Lang-Arg
    :cmap/:cnoremap/:cunmap Command-line
    :tmap/:tnoremap/:tunmap Terminal

    可以看到,就比如map,它起作用的模式为:正常模式,可视模式,选择模式,操作符待决模式。

    更多详情,请查看::help map-overview

    Leader

    • Vim 提供的默认主键Leader为:\,通常我们会修改为自己习惯的按键:
    :let mapleader = ","
    

    主键Leader通过作为按键映射的前缀进行使用。

    • Vim 还提供了一个本地主键Local Leader:其只对特定类型的文件起作用,比如 Python 文件或 HTML 文件等:
    :let maplocalleader = "\\"
    

    更多详细情况,请查看::help mapleader/:help maplocalleader

    缩略语(Abbreviations)

    • 缩略语(Abbreviations):是 Vim 内置的一个特性,其与按键映射(Key-Mapping)功能类似,但主要使用于插入模式,替换模式和命令行模式。

    举个栗子:

    :iabbrev adn and
    

    上述命令意为在插入模式下,将按键adn整个词映射为and,其效果与下述按键映射作用几乎一致:

    :inoremap adn and
    

    一个大的区别就在于:缩略语作用于单词上,按键映射作用于字符序列。

    简单理解,插入模式下,输入下面字符串:

    testadntest
    

    在缩略语中,其作用于单词上,因此整个单词testadntest并未在缩略语中进行映射,不会有任何变化。
    而在按键映射中,由于单词testadntest中含有映射序列adn,按键映射会响应这个字符序列进行转换,因此最终的结果为:testandtest

    :缩略语一个很好的使用场景就是在插入模式下,进行一些输入映射,比如:

    :iabbrev @@    steve@stevelosh.com
    :iabbrev ccopy Copyright 2013 Steve Losh, all rights reserved.
    

    再比如,利用缩略语可以很方便地实现一些模板代码:

    :iabbrev pmain if __name__ == '__main__':<cr>pass
    

    插入模式下输入pmain,就会生成如下模板代码:

    if __name__ == '__main__':
        pass 
    

    :缩略语同按键映射一样,可自由指定其在具体模式下起作用,其格式为::{mode}ab[breviate],比如:

    :ia[bbrev] " insert mode
    :ca[bbrev] " command line mode
    

    更多模式,请查看::help map-listing

    • Vim 没有内置的默认缩略语,缩略语本身永不响应递归,比如你可以很安全地使用:ab f f-o-o,但是缩略语可以被按键映射,比如:
    :inoremap hi hello
    :iab h hi
    

    在插入模式下,输入h,就会经历h --> hi --> hello的转换,也即是缩略语被按键映射响应了,导致结果与我们预期不符。Vim 针对这一问题也提供了非递归版本的缩略语命令:{mode}norea[bbrev],比如:

    :inoremap hi hello
    :inorea h hi
    

    此时插入模式下输入h,就会转换为hi,而hi不会响应按键映射。

    最后,取消缩略语的语法与取消按键映射一致,其格式为:{mode}una[bbrev],比如:

    :iuna h
    :cuna test
    

    更多详细信息,请查看::help abbrev

    本地缓冲区选项和映射(Buffer-Local Options and Mappings)

    我们可以在一个 Vim 窗口中打开多个文件,每个文件都会对应有属于自己的缓冲区,称为 本地缓冲区

    我们可以单独地为每个本地缓冲区设置不同的选项和映射,设置方法如下:

    • 按键映射(Mappings):使用<buffer>指令:
    :nnoremap          <leader>d dd " 全局起作用
    :nnoremap <buffer> <leader>x dd " 只对本地文件起作用
    
    • 本地主键(Local Leader):全局主键(Leader)对全局起作用,而本地主键只对本地缓冲区起作用。
    :let maplocalleader = "\\"
    

    :由于 Leader 具备全局作用域,因此在自己编写脚本的时候,如果需要使用特定前缀按键,建议使用本地主键 Local Leader 进行设置,由于本地主键只会对本地缓冲区起作用,因此几乎不会意外造成主键按键冲突。

    • 设置(Settings):有些set命令会作用于整个 Vim 实例,而使用本地设置可只对本地缓冲区进行设置:
    :setlocal wrap
    
    • 覆盖(Shadowing):对于相同的设置,本地缓冲区会覆盖全局作用域:
    :nnoremap <buffer> Q x
    :nnoremap          Q dd
    

    由于同时对按键Q进行了映射,则第一个本地缓冲区的设置会覆盖第二个全局设置,因此,Q会转换为x

    更多详细信息,请查看::help local-options/:help setlocal/:help map-local

    自动命令(Autocommands)

    • 自动命令(Autocommands):用以监听某些事件发生进而执行相应指令。Vim 为我们提供了很多事件监听,比如,可以对读取文件,写入文件,进入缓冲区/窗口,退出缓冲区/窗口,退出 Vim 等事件进行监听,并自动运行相应的命令···

    自动命令的结构格式如下:

    autocmd
    1. 事件类型(event type):autocmd后面的第一个字段表示要监听的事件类型。

    Vim 内置的事件类型有很多,针对不同的场景,会提供相应的事件监听。

    Vim 的操作大概涉及的场景有:读取写入缓冲区选项启动和退出杂项

    各个场景对应的事件如下所示:

    • 读取(Reading):该场景事件类型如下表所示:
    事件名称 触发时间
    BufNewFile 编辑尚不存在的新文件时
    BufReadPre 读取文件前,开始编辑缓冲区(buffer)时
    BufRead 读取文件后,开始编辑缓冲区时
    BufReadPost 读取文件后,开始编辑缓冲区时
    BufReadCmd 开始编辑新缓冲区前 Cmd-event
    FileReadPre 执行:read命令读取文件前
    FileReadPost 执行:read命令读取文件后
    FileReadCmd 执行:read命令读取文件前 Cmd-event
    FilterReadPre 过滤读取文件前
    FilterReadPost 过滤读取文件后
    StdinReadPre 在从标准输入中读取文件到缓冲区前
    StdinReadPost 在从标准输入中读取文件到缓冲区后
    • 写入(Writing):该场景事件类型如下表所示:
    事件名称 触发时间
    BufWritePre 把缓冲区全部内容写入到文件前
    BufWrite 把缓冲区全部内容写入到文件
    BufWritePost 把缓冲区全部内容写入到文件后
    BufWriteCmd 把缓冲区全部内容写入到文件前 Cmd-event
    FileWritePre 开始把缓冲区部分内容写入到文件前
    FileWritePost 开始把部分缓冲区内容写入到文件后
    FileWriteCmd 开始把缓冲区部分内容写入到文件前 Cmd-event
    FileAppendPre 开始追加内容到文件前
    FileAppendPost 开始追加内容到文件后
    FileAppendCmd 开始追加内容到文件前 Cmd-event
    FilterWritePre 过滤文件 或 diff 开始写入文件前
    FilterWritePost 过滤文件 或 diff 开始写入文件后
    • 缓冲区(Buffers):该场景事件类型如下表所示:
    事件名称 触发时间
    BufAdd 将缓冲区添加到缓冲区列表后
    BufCreate 将缓冲区添加到缓冲区列表后(?)
    BufDelete 将缓冲区从缓冲区列表删除前
    BufWipeout 完全删除缓冲区前
    BufFilePre 改变当前缓冲区名称前
    BufFilePost 改变当前缓冲区名称后
    BufEnter 进入缓冲区后
    BufLeave 进入其他缓冲区前(即离开当前缓冲区时)
    BufWinEnter 窗口显示缓冲区内容前
    BufWinLeave 窗口删除缓冲区内容前
    BufUnload 移除缓冲区前
    BufHidden 刚隐藏缓冲区后
    BufNew 新创建缓冲区后
    SwapExists 检测到交换文件已存在时
    TermOpen 开启终端任务时
    TermEnter 进入终端模式时
    TermLeave 退出终端模式时
    TermClose 停止终端任务时
    ChanOpen 通道开启后
    ChanInfo 通道状态改变后
    • 选项(Options):该场景事件类型如下表所示:
    事件名称 触发时间
    FileType 设置了filetype选项时
    Syntax 设置了syntax选项时
    OptionSet 当设置了任何选项后
    • 启动和退出(Startup and exit):该场景事件类型如下表所示:
    事件名称 触发时间
    VimEnter 完成所有初始化操作后(即进入 Vim 时)
    UIEnter 进入 UI 后
    UILeave 退出 UI 后
    TermResponse 在接收到终端回复 t_RV 后
    QuitPre 当使用:quit,在决定退出前
    ExitPre 当使用可能使 Vim 退出的命令前
    VimLeavePre 在退出 Vim 前,在写入到 shada 文件前
    VimLeave 在退出 Vim 前,在写入到 shada 文件后
    VimResume Neovim 恢复后
    VimSuspend Neovim 暂停前
    • 杂项(Various):该场景事件类型如下表所示:
    事件名称 触发时间
    DiffUpdated 刷新比较结果后
    DirChanged 当前工作目录改变后
    FileChangedShell 在编辑文件后,Vim 检测到文件已被改变
    FileChangedShellPost 在对编辑文件更改处理完成后
    FileChangedRO 对只读文件第一次修改前
    ShellCmdPost 在执行 shell 命令后
    ShellFilterPost shell 命令执行完过滤后
    CmdUndefined 调用未定义的用户命令
    FuncUndefined 调用未定义的用户函数
    SpellFileMissing 无法找到拼写文件
    SourcePre 加载 Vim 脚本之前
    SourcePost 加载 Vim 脚本之后
    SourceCmd 加载 Vim 脚本之前 Cmd-event
    VimResized Vim 窗口大小改变后
    FocusGained Vim 获取到焦点
    FocusLost Vim 失去焦点
    CursorHold 光标静止,即用户一段时间内未按键
    CursorHoldI 用户在插入模式下一段时间内未按键
    CursorMoved 光标在正常模式下移动
    CursorMovedI 光标在插入模式下移动
    WinNew 创建新窗口后
    WinEnter 进入另一个窗口后
    WinLeave 离开窗口前
    TabEnter 进入另一个标签页面后
    TabLeave 离开标签页面时
    TabNew 创建新的标签页面时
    TabNewEntered 进入新的标签页面后
    TabClosed 关闭标签页面后
    CmdlineChanged 命令行文本改动后
    CmdlineEnter 进入命令行模式后
    CmdlineLeave 退出命令行模式前
    CmdwinEnter 进入命令行窗口后
    CmdwinLeave 离开命令行窗口前
    InsertEnter 进入插入模式
    InsertChange 在插入/替换模式时输入<Insert>
    InsertLeave 离开插入模式时
    InsertCharPre 在插入模式下输入每个字符前
    TextYankPost 复制/删除文本后
    TextChanged 正常模式下,文本更改时
    TextChangedI 插入模式且没有弹出菜单的文本更改
    TextChangedP 插入模式下且伴随弹出菜单的文本更改
    ColorSchemePre 加载色彩方案前
    ColorScheme 加载色彩方案后
    RemoteReply Vim 接收到服务器回复时
    QuickFixCmdPre 执行 quickfix 命令前
    QuickFixCmdPost 指定 quickfix 命令后
    SessionLoadPost 加载会话文件后
    MenuPopup 显示弹出菜单前
    CompleteChanged 补全弹出菜单被更改,弹出菜单隐藏时不会触发该事件
    CompleteDone 插入模式下,补全完成时
    User 结合:doautocmd进行使用
    Signal Neovim 接收到信号后

    :由于本人是在 Neovim 上查看文档,因此上述可能存在属于 Neovim 独有的内容。

    更多事件类型详细信息,请查看::help autocmd-events/:help autocmd-events-abc

    1. 模式匹配(pattern):模式匹配可让我们更加具体地指定自动命令执行的条件。

    举个栗子:

    :autocmd BufNewFile *.txt :write
    

    上述命令对BufNewFile创建新文件事件进行了监听,并且指定只有在 txt 文件时才会触发命令执行。

    1. 命令(commands):指定事件发生时自动运行的命令。
      :自动命令中不能使用特殊字符,比如:<cr>
    • 多事件监听(Multiple Events):自动命令不仅可对一个事件进行监听,它还同时支持对多事件进行监听。

    举个栗子:

    :autocmd BufWritePre,BufRead *.html :normal gg=G
    

    :一个常见的组合方式是同时对BufNewFileBufRead进行监听,这样在对某些文件执行自动命令时,就无须关心该文件是已存在还是新创建的。

    :autocmd BufNewFile,BufRead *.html setlocal nowrap
    

    :一个最常用的事件监听就是对文件类型FileType的监听,这个事件会在缓冲区设置filetype选项时被触发:

    :autocmd FileType javascript nnoremap <buffer> <localleader>c I//<esc>
    :autocmd FileType python     nnoremap <buffer> <localleader>c I#<esc>
    

    上述命令结合使用了自动命令和本地缓冲区按键映射,使得我们可以使用相同的按键在不同的文件中实现出不同的操作。

    本地缓冲区缩略语(Buffer-Local Abbreviations)

    • 对于缩略语,也可将其设置为只对本地缓冲区起作用:
    :iabbrev <buffer> --- &mdash;
    
    • 对于缩略语,可将其同自动命令和本地缓冲区结合到一起,使得对同样的事件,同样的按键,对不同的匹配会实现出不同的效果:
    :autocmd FileType python     :iabbrev <buffer> iff if:<left>
    :autocmd FileType javascript :iabbrev <buffer> iff if ()<left>
    

    自动命令组(Autocommand Groups)

    对于自动命令,Vim 默认的方式是追加设置,即设置了一摸一样的两条相同自动命令,则它们会同时起作用:

    :autocmd BufWrite * :echom "Writing buffer!"
    :autocmd BufWrite * :echom "Writing buffer!"
    

    上述命令设置完成后,执行:w操作,可以通过:messages命令查看得到两条Writing buffer!信息,说明上述两条一摸一样的自动命令同时起作用了。

    Vim 默认的自动命令同时生效操作可能会不经意间给我们带来一些副作用,比如如果我们的.vimrc设置了自动命令,那么我们手动source $MYVIMRC一下时,就相当于再执行了一次该自动命令,从而导致自动命令重复定义,这与我们的本意是相违背的。

    要解决上述问题,可采用 自动命令组 功能:augroup <group_name>,比如:

    :augroup testgroup
    :    autocmd BufWrite * :echom "Foo"
    :    autocmd BufWrite * :echom "Bar"
    :augroup END
    

    但这样做还不够,上述只是定义了一个自动命令组,并没有其他特别作用,我们此时手动再输入相同的一条自动命令,比如:

    :autocmd BufWrite * :echom "Bar"
    

    执行:w后,通过命令:messages可以查看到,此时的输出为:Foo Bar Bar,说明相同的命令没有覆盖。实际上,哪怕是重新输入相同的自动命令组,Vim 也不会进行覆盖,而是默认的追加。

    最终的解决办法就是在自动命令组内先将该自动命令组进行清除,然后再设置,这样就不会有重复定义的问题了。如下所示:

    :augroup testgroup
    :    autocmd! " 清除该自动命令组
    :    autocmd BufWrite * :echom "Cats"
    :augroup END
    

    操作符待决模式按键映射(Operator-Pending Mappings)

    • 操作符(operator):表示某些操作的命令(eg:d/y/c,具体查看::help operator),操作符后面会紧跟移动命令,共同完成某个操作。

    举个栗子:

    Keys   Operator   Movement
    ----   --------   -------------
    dw     Delete     to next word
    ci(    Change     inside parens
    yt,    Yank       until comma
    
    • 操作符待决模式(operator-pending mode):即在输入操作符(比如c/d/y)后,进入只接受动作命令的状态。比如在执行动作命令dw时,当按下操作符d时,该模式立即激活,此时 Vim 会记录d这个按键并等待后续命令动作,当按下w时,该模式立即结束,并执行dw操作。
    • 操作符待决模式按键映射(Operator-Pending Mappings):我们可以自定义操作符待决模式按键映射,自定义移动范围。

    举个栗子:

    :onoremap p i(
    

    上述代码设置了在操作符待决模式下,将按键p映射成了i(,比如我们对如下字符串进行操作:

    return person.get_pets(type="cat", fluffy_only=True)
    

    假设我们现在的光标位于括号内,此时我们按dp,就可以看到括号内的内容被清空了。
    原因在于当我们按下操作符d时,此时就进入了操作符待决模式,而该模式下按下p后,p后被映射为i(,所以就相当于执行了di(,因此清空了括号内容。

    :操作符待决模式的自定义选取范围的定义诀窍:借助可视模式高亮的范围即为选取范围。

    举个栗子:

    return person.get_pets(type="cat", fluffy_only=True)
    

    还是上述这个栗子,前面我们需要手动移动光标到括号内,才能选取括号内容:因为映射为i(,当光标在括号内时,vi(就高亮选取括号内全部内容,所以这就是我们设置的移动范围。

    因此,我们最终就是记录一系列移动,找到可用位置,然后高亮选择范围就可完成自定义操作符待决模式范围选取。

    比如,现在我们先增强上述例子的功能,要求无论当前光标处于该行字符串的哪个位置,均可实现选取括号内的内容。

    目的:最终能高亮选取括号内容。
    思路:先移动到括号内,然后就直接选取即可。
    代码如下:

    :onoremap in( :normal! 0f(vi(<cr>
    

    我们直接在命令行窗口输入:normal! 0f(vi(,看到高亮的内容就是我们的选取范围了。
    我们将该范围设置到操作符待决模式中,就可以响应操作符一起制定功能强大的文本对象操作。

    上述代码可以实现我们的需求,当还不够健壮,某些清空下可能会出现一些选取错误。比如现在 Vim 在可视模式下选取了部分内容,那么我们执行命令行命令的时候,就会被添加上当前选取范围,如下所示:

    :'<,'>
    

    这不经意间限定了我们的操作范围,因此最好将其去除(使用<c-u>),最终代码如下:

    :onoremap in( :<c-u>normal! 0f(vi(<cr>
    

    变量(Variables)

    Vim变量 定义使用关键字:let

    :let foo = "bar"
    :echo foo
    
    :let foo = 42
    :echo foo
    
    • 选项变量(Options as Variables)Vim 内置的选项也可以看作变量,可以通过符号引用&指向选项变量,对其进行修改:
    :let &textwidth = 80 " 设置选项变量
    :echo &textwidth " 读取选项变量:80
    

    当然,还可以采用 Vim 内置的关键字set对选项进行修改:

    :set textwidth=80 " 使用 set 关键字进行设置
    :set textwidth? " 使用 ? 符号查看选项:textwidth=80
    

    :使用关键字set只能对选项赋值一个字符串数值进行设置,而使用let可以将选项当作变量,使其具备类型信息,从而能更加充分地使用 Vimscript 的编程能力。

    :let &textwidth = &textwidth + 10
    :set textwidth?
    
    • 局部选项变量(Local Options):可以设置选项变量为局部变量,让其只对本地缓冲区起作用。选项前添加前缀l:表示将该选项变量设置为局部变量:
    :let &l:number = 1 " 本地缓冲区的 number 设置 1
    

    :Vimscript 中对于布尔值的实现是以整型进行表示:非零即为真(通常为:true1false0)。
    比如,对于布尔值类型选项变量,当设置为true时,其值为1false其值为0

    :set wrap " 设置为 true
    :echo &wrap " 1
    
    :set nowrap " 设置为 false
    :echo &wrap " 0
    
    • 寄存器变量(Registers as Variables):可以通过寄存器变量对寄存器进行读取与设置,其格式为:@{register}
    :let @a = "hello!" " 设置寄存器 a 的值
    :echo @a " 读取寄存器 a
    

    变量作用域(Variable Scoping)

    Vimscript 提供了多种命名空间(name space)对变量作用域进行限制,其格式为:{scope_char}:,即一个前缀后紧跟一个:作为作用域声明。

    Vimscript 中的命名空间有如下几种:

    变量归属 前缀 作用域
    buffer-variable b: 当前本地缓冲区局部变量
    window-variable w: 当前窗口局部变量
    tabpage-variable t: 当前标签页面局部变量
    global-variable g: 全局变量
    local-variable l: 函数内局部变量
    script-variable s: Vimscript 脚本局部变量(即.vim文件内的局部变量)
    function-argument a: 函数参数变量
    vim-variable v: Vim 内置的全局变量

    :在一个 Vimscript 脚本(即.vim)文件中:

    • 函数外定义的变量为全局作用域,可使用g:{name}引用:
    " test.vim
    let name = "Whyn"
    function! Test()
        echom g:name " Whyn
    endfunction
    
    :source test.vim
    :echo name " Whyn
    :echo g:name " Whyn
    :echo name is# g:name " 1,表示同一对象
    
    • 函数体内定义的变量为局部变量,可使用l:{name}引用:
    " test.vim
    function! Test()
        let name = "Whyn"
        echom name " Whyn
        echom l:name " Whyn
        echom name is# l:name " 1,表示为同一对象
    endfunction
    

    更多详细内容,请查看::help internal-variables

    条件语句(Conditionals)

    • if语句:其格式如下:
    " if
    if 1
        echom "1 is true"
    endif
    
    " if else
    if 0
        echom "0 is false"
    else
        echom "none 0 is true"
    endif
    
    " if elseif else
    if 0
        echom "false"
    elseif "somestrings"
        echom "strings are false"
    else
        echom "true"
    endif
    

    :Vimscript 中if语句后接一个布尔参数:

    • 当参数为数值时,非零即为真:
    if 2
        echom "2 is true"
    endif
    
    • 当参数不为数值时,则会自动进行转换,转换规则如下:
      对于字符串类型,如果是以数字开头,则转换为该数值,否则为 0:
    if "somestring"         " transform to 0
        echom "false"
    endif
    
    if "somestring10"       " transform to 0
        echom "false"
    endif
    
    if "10somestring"       "transform to 10
        echom "true"
    endif
    
    if "10"                 " transform to 10
        echom "true"
    endif
    

    比较(Comparisons)

    比较操作常见于 数值比较字符串比较,常用的比较操作有 大于>小于<等于==···

    • 数值比较:正常对数值进行比较。
    if 1 > 10 
        echom "false"
    endif
    
    • 字符串比较:比对字符串字典序。
      :字符串是否区分大小写由当前选项ignorecase决定。比如:
    :set ignorecase " 忽略大小写
    :echo "foo" == "FOO"    " 1
    
    :set noignorecase " 区分大小写
    :echo "foo" == "FOO"    " 0
    

    如上述代码所示:Vimscript 涉及比较操作的时候,竟然会受到用户设置的影响,按这样编写出来的脚本在不同的用户电脑上就会出现行为不一致的问题。因此,实际开发中,一般不会采用这种受到用户当前设置影响的操作符,而 Vimscript 其实也已提供了不受用户影响的等效操作符。

    比如,对于==操作,可用==?==#进行替换:

    • ==?:无论用户设置了任何比较操作符,字符串都不区分大小写:
    :set ignorecase " 忽略大小写
    :echo "foo" ==? "FOO"    " 1
    
    :set noignorecase " 区分大小写
    :echo "foo" ==? "FOO"    " 1
    
    • ==#:无论用户设置了任何比较操作符,字符串都区分大小写:
    :set ignorecase " 忽略大小写
    :echo "foo" ==# "FOO"    " 0
    
    :set noignorecase " 区分大小写
    :echo "foo" ==# "FOO"    " 0
    

    ==?==#等操作符也可用于数值类型,建议始终使用==?==#这类比较操作符进行比较操作,可以减少出错概率。

    以下是 Vimscript 提供的完整比较操作符:

    比较 操作符 操作符(区分大小写) 操作符(忽略大小写)
    相等 == ==# ==?
    不相等 != !=# !=?
    大于 > ># >?
    大于或等于 >= >=# >=?
    小于 < <# <?
    小于或等于 <= <=# <=?
    正则匹配 =~ =~# =~?
    正则不匹配 !~ !~# !~?
    相同实例对象 is is# is?
    不同实例对象 isnot isnot# isnot?

    更多比较操作符详细信息,请查看::help expr4

    函数(Functions)

    与大多数编程语言一样,Vimscript 也提供了函数功能。其格式如下:

    :fu[nction][!] {name}([arguments]) [range] [abort] [dict] [closure]
    

    Vimscript 中的函数定义有以下几点需要知晓:

    • 全局函数名称首字母大写:在 Vimscript 中,当定义一个全局函数时,要求首字母大写,避免与 Vimscript 内置函数冲突。
    function Test()
        echom "global function must start with a Captical letter!"
    endfunction
    
    • 函数调用:有两种方法可调用函数:
    1. 直接调用,忽略返回值:call Test()
    2. 表达式调用:表达式会调用函数,并获取函数返回结果传递给自身:
    function Test()
        return "somthing"
    endfunction
    
    echom Test() " 表达式调用
    
    • 隐式返回:当函数没有显示return时,其会隐式返回0
    function Test()
    endfunction
    
    echo Test() " 0
    
    • 函数覆盖:Vimscript 不能重复定义相同的函数,否则会出错,可使用function!关键字定义,强制覆盖同名函数,后面的函数会覆盖前面定义的同名函数。
    function! Test()
    endfunction
    

    :Vimscript 不支持函数重载。
    :普通函数定义在重新加载.vimrc的时候,会产生函数重复定义的错误,此时使用函数强制覆盖定义就是一个很好地解决方案。

    更多详细信息,请查看::help :call/:help E124/:help return

    函数参数(Function Arguments)

    • 基本使用:函数体访问函数参数时需要显示使用前缀a:进行引用:
    function DisplayName(name)
        echom "Hello!  My name is:"
        echom a:name
    endfunction
    
    call DisplayName("Whyn")
    
    • 可变参数(Varargs):Vimscript 函数支持可变参数传递,参数格式为:...
    function Varg(...)
        echom a:0   " 2
        echom a:1   " a
        echo a:000  " ['a','b']
    endfunction
    
    call Varg("a", "b")
    

    对于可变参数:

    • a:0:表示函数接收到的实际参数个数。

    • a:1:表示实际接收到的第一个参数。
      同理,a:n表示实际接收到的第 n 个参数。

    • a:000:当传递可变参数时,所有参数都会传递给a:000的列表中。

    • 赋值(Assignment):Vimscript 中函数参数不支持重新赋值:

    function Assign(foo)
        let a:foo = "Nope" " error,参数不能重新赋值
        echom a:foo
    endfunction
    

    数值(Numbers)

    Vimscript 内置两种数值类型:NumberFloat

    • Number:一个Number是一个 32 位有符号整型。
    :echom 100 " 十进制
    :echom 0xff " 十六进制
    :echom 010 " 八进制
    :echom 019 " 注:八进制不存在 019,此时 Vimscript 会将其作为十进制处理
    
    • Float:浮点数数值。
    :echo 100.1
    :echo 5.45e+3 "指数表示
    :echo 15.3e9  "+号可省略
    :echo 15.45e-2
    
    • 强制转换(Coercion):当NumberFloat混合进行数值计算,比较或其他操作时,Number类型会被强转为Float类型,操作结果也为Float类型。
    :echo 2 * 2.0 " 4.0
    
    • 除法(Division):Vimscript 中整型之间的除法/为整除操作,不会保留余数。
    :echo 3 / 2 " 1
    

    除法操作中,只要有一方为Float类型,就会进行浮点数除法:

    :echo 3 / 2.0 " 1.5
    

    更多详细信息,请参考::help Float/:help floating-point-precision

    字符串(String)

    VimScript 内置的字符串类型特性如下:

    • 基本使用:echo "String"

    • 拼接(Concatenation): Vimscript 中使用连结运算符.进行字符串拼接:

    :echo "Hello, " . "world"   " . 前后可以有任意空格
    :echo 10 . "foo"            "10foo,整型可以进行字符串拼接
    :echo 10.10 . "foo"         " error,浮点型不能进行字符串拼接
    

    :如果使用+进行拼接是不会成功的,比如::echo "Hello" + "World",结果为0,因为+只能作于数值类型上,因此当拼接两个字符串时,Vimscript 会默认先对字符串强转为数值类型,然后再进行数值相加。

    :Vimscript 对字符串的强转只会转换为整型Number,不会转为浮点型Float

    :echo +"Hello"    " 0
    :echo +"10Hello"  "10
    :echo +"10.10"    "10
    
    • 特殊字符(Special Characters):特殊字符需要进行转义:
    :echom "foo \"bar\""
    

    echo会输出转义完成的字符串,与预期是一致的。而echom会精确输出字符串的每个字符,因此有可能出现与预期不一致的显示效果:

    :echo "foo\nbar"   " 输出会换行
    :echom "foo\nbar"   " foo^@bar,^@在 Vim 里表示换行符
    
    • 字符串字面量(Literal Strings):字符串字面量不会对字符串进行转义,字符串会按原义/字面进行输出。字符串字面量使用单引号'进行表示。
    :echo 'foo\nbar' " foo\nbar
    

    :原则上字符串字面量不会对字符串进行转义,除了以下一种情况外:当两个单引号'连接到一起时,会被转义为一个单引号'

    :echom 'That''s enough.'  " That's enough
    

    更多字符串详细信息,请参考::help expr-quote/:help literal-string

    字符串函数(String Functions)

    Vimscript 内置了很多函数用于处理字符串,以下列举一些比较常用的字符串函数:

    • strlen/len: 获取字符串长度。
    :echo strlen('Hello') " 5
    :echo len('Hello') " 5
    
    • split:字符串切割。
    :echo split("one two three")          " ['one','two','three'],默认以空格进行切割
    :echo split("one,two,three", ",")     " ['one','two','three'],指定以 , 进行切割
    
    • join:字符串连接。
    :echo join(["foo", "bar"], "...")  " foo...bar
    

    splitjoin结合起来可以很优雅地取得很好的效果:

    :echo join(split("foo bar"), "-->") " foo-->bar
    
    • tolower/toupper:大小写转换。
    :echom tolower("Foo")  " foo
    
    :echom toupper("Foo")  " FOO
    

    更多详细信息,请查看::help split()/:help join()
    Vimscrpt 内置函数的文档::help functions

    命令

    Vimscript 内置了一些命令,以下介绍几种常用的命令:

    • execute:该命令可用于执行字符串脚本,相当于执行定义在 Vimscript 脚本中的命令。
    :execute "echom 'Hello, world!'"
    
    :execute "rightbelow vsplit " . bufname("#")
    

    execute命令的作用与 Javascript 的eval函数功能是差不多的,都是可以执行任意字符串定义的功能。

    更多详细信息,请查看::help execute/:help leftabove/:help rightbelow/:help :split/:help :vsplit

    • normal:该命令可在命令行模式下执行正常模式的命令。
    " 跳转到最底
    :normal G 
    

    normal命令执行的任何操作都与我们在正常模式下操作的效果一摸一样,包括按键映射也会生效:

    :nnoremap G dd
    :normal G      " dd
    

    上述代码中,由于按键G被映射为dd,因此最终会执行dd删除当前行。
    上述代码的效果是与我们预期相符的,但是对于插件来说,最好避免因用户环境的差异而导致本身代码的功能发生变化,即插件最好就是执行原生 Vimscript 的功能,要做到这一步,只需使用normal!命令即可:

    :nnoremap G dd
    :normal! G      " G
    

    normal!命令会把跟随的命令当作字符串字面量,也即normal!不会对识别特殊字符串,比如:

    :normal! /foo<cr>
    

    上述命令中,normal!不会识别字符串<cr>,因此也就不会执行回车操作。由于normal!将后续的字符串当作字符串字母量,因此上述代码的真正操作是搜索字符串foo<cr>

    要想让normal!命令识别特殊字符串,只需结合execute命令即可:

    :exe "normal! /foo\<cr>"
    

    exe[cute]命令后面接字符串字面量时,对于某些存在特定功能的特殊字符(比如<CR>),由于保持了原义,execute不进行解析,而normal!不会识别该特殊字符串,因此可能导致结果不正确,需要注意一下。

    :exe 'normal! /foo<cr>' " not working,相当于搜索 foo<cr>,但没按回车,所以没有任何效果
    :exe 'normal! /foo'."\<cr>" " ok, 对特殊字符进行单独设置
    

    :当进行 Vimscript 脚本开发时,建议始终使用normal!,避免使用normal

    更多详细信息,请查看::help normal

    正则表达式(Regular Expressions)

    Vim 是一个文本编辑器,这也意味着大量的 Vimscript 脚本将专注于处理文本,而正则表达式恰好就是文本处理的瑞士军刀。

    Vimscript 中的正则表达式与其他大多数编程语言的正则表达式稍有不同,在 Vimscript 中,提供了两种模式的正则匹配:

    1. magic:该模式下的正则匹配相当于设置了magic选项,依据magic选项的状态总共有如下两种情形:
    • 使能:即设置了set magic,开启magic搜索匹配,此时只有$.*^~具有特殊含义:比如$代表结尾。其他元字符必须转义才能具备正则含义。
      :可通过\m标识符显示以magic搜索,忽略当前set magic?选项状态。
    /\mworld$          " 搜索匹配以 world 结尾的字符串
    
    /\mworld\$         " 搜索匹配含 world$ 的字符串
    
    /\mhello (world)   " 搜索匹配含 hello (world) 的字符串
    
    /\mhello \(world\) " 搜索匹配含 hello world 的字符串,其组 1 为 world
    
    • 失能:即设置了set nomagic,开启nomagic搜索匹配,此时$^具备特殊含义,其余元字符串为字符串字面量,若想具备正则含义需转义。
      :可以看到,nomagic搜索基本默认为原义搜索,除了$^需要注意以外。
      :可通过\M标识符显示以nomagic搜索,忽略当前set magic?选项状态。
    1. very magic:该模式是 Vim 提供的更加强大的正则匹配,同样也包含两种搜索匹配模式:
    • 使能:表示开启very magic匹配,此时任何元字符都具备正则含义。
      :可通过\v标识符开启very magic搜索。
    /\vworld$          " 搜索匹配以 world 结尾的字符串
    
    /\vworld\$         " 搜索匹配含 world$ 的字符串
    
    /\vhello (world)   " 搜索匹配含 hello world 的字符串,其组 1 为 world
    
    /\vhello \(world\) " 搜索匹配含 hello (world) 的字符串
    
    • 失能:表示开启very nomagic匹配,此时只有反斜杠\和终止字符(一般为/?)具备正则含义,其他所有字符均为字面原义。
      :可通过\V标识符开启very nomagic搜索。

    Vim 缺省使用magic模式进行正则匹配,至于到底是magic还是nomagic,取决于用户当前的set magic?选项。

    以下是 Vim 中各模式匹配常用字符的比对效果:

    \v \m \M \V 匹配
    $ $ $ \$ 结尾
    . . \. \. 任意字符
    * * \* \* 任意数量
    ~ ~ \~ \~ 上一个子串
    () \(\) \(\) \(\)
    | \| \| \|
    \a \a \a \a 字母表(即[a-z,A-Z])
    \\ \\ \\ \\ 反斜杠\
    \. \. . . 字符.
    \{ { { { 字符{
    a a a a 字符a

    magic搜索模式对元字符的制定比较混乱(需要自己去区分元字符是否需要进行转义),难以记忆。而very magic正好弥补了这个缺点,比如使用\v搜索,则元字符全部具备正则含义,而使用\V搜索,则只需记住\///?等具备正则含义,其余字符均为字面原义即可。所以可大致认为:\v为正则匹配,\V为字面原义匹配。

    更多详细信息,请查看::help magic/:help pattern-overview

    集合(Aggregates )

    Vimscript 内置两种主要的集合类型:列表字典

    • 列表(List):Vimscript 列表是有序的,可同时容乃任意类型元素的集合。
      以下是 列表 的常用操作:
    " 创建
    :let mylist = [1,'two',3,"four"]  
    :let emptylist = []             
    
    " 查:使用索引
    :let first = mylist[0] " 1 
    :let last = mylist[-1] " four,-1 代表最后一个
    " 查:索引获取可能越界出错,get()函数当索引无效时返回 0 或指定的默认值
    :echo get(mylist,4)         " error: List out of range
    :echo get(mylist,4,"NONE")  " NONE
    " 自动拆包
    :let [item1,item2] = [1,2]        " item1=1,item2='two'
    :let [item1,item2,rest] = mylist  " item1=1,item2='two',rest=[3,'four']
    " 获取值对应的索引位置
    :echo index(mylist,'two') " 1,返回第一个找到的值对应的索引
    :echo index(mylist,2) " -1,找不到值返回 -1
    
    " 增
    :echo add([],1)                " [1]
    :let longlist = mylist + [5,6] " [1,'two',3,'four',5,6]
    :let mylist += [7,8]           " [1,'two',3,'four',7,8]
    :echo add([1,2],'three')       " [1,2,'three']
    :call insert([],'a')           " ['a']
    :call insert([1,2],'3',1)      " [1,'3',2],list[1]前插入
    :call extend([1,2],[3,4])      " [1,2,3,4]
    
    " 改
    :let mylist = [1,'two',3,"four"]  
    :let mylist[0] = 'one'           " ['one','two',3,'four']
    :let mylist[1:2] = [2,'three']   " ['one',2,'three','four']
    
    " 删
    :let mylist = [1,'two',3,"four"]  
    :echo remove(mylist,1) " 'two',移除索引 1,返回被移除的值
    :unlet mylist[0] " [3,"four"],移除索引 0
    
    " 切片(Slicing):效果于 Python 切片差不多
    :echo ['a', 'b', 'c', 'd', 'e'][0:2] " ['a','b','c']
    :echo ['a', 'b'][0:100000] " ['a','b'],无视索引越界
    :echo ['a', 'b', 'c', 'd', 'e'][-2:-1] " ['d','e'],倒数第二个-倒数第一个
    :echo ['a', 'b', 'c', 'd', 'e'][:1] " ['a','b'],上限缺省默认为 0
    :echo ['a', 'b', 'c', 'd', 'e'][3:] " ['d','e'],下限缺省默认为列表长度
    

    更多详细信息,请查看: :help List/:help add()/:help len()/:help get()/:help index()/:help join()/:help reverse(0)/:help functions

    • 字典(Dictionary):Vimscript 提供的字典类型与 Python 的dict,Ruby 的hashes和 Javascript 的object非常类似。
      以下是 字典 的常用操作:
    " 创建
    :echo {'a': 1, 100: 'foo'} " {'a': 1, '100': 'foo'},从输出可以看到,字典的 key 是字符串类型
    
    " 查
    " 使用字符串
    :echo {'a': 1, 100: 'foo',}['a']   " 1
    :echo {'a': 1, 100: 'foo',}[100]   " foo,Vimscript 会把 100 强转为 '100',在进行查找
    " 使用 .
    :echo {'a': 1, 100: 'foo',}.a      " 1
    :echo {'a': 1, 100: 'foo',}.100    " 100
    " 函数
    :echo get({'a': 100}, 'a', 'default') " 100
    :echo get({'a': 100}, 'b', 'default') " default
    :echo has_key({'a': 100}, 'a') " 1
    :echo has_key({'a': 100}, 'b') " 0
    :echo items({'a': 100, 'b': 200}) " [['a', 100], ['b', 200]]
    :keys({'a': 100,'b': 200}) " ['a','b'],获取所有键
    :values({'a': 100,'b': 200}) " [100,200],获取所有值
    
    " 增
    :let foo = {'a': 1}
    :let foo.b = 200 " {'a': 1,'b': 200,}
    
    " 改
    :let foo.a = 100 " {'a': 100,'b': 200}
    
    " 删
    :let foo = {'a': 1,'b': 200}
    :echo remove(foo,'a') " 1,foo = {'b': 200}
    :unlet foo.b  " foo = {}
    :unlet foo["asdf"] " error,unlet 不能移除不存在的 key
    

    更多详细信息,请查看::help Dictionary/:help get()/:help has_key()/:help items()/:help key()/:help values()

    循环(Looping)

    Vimscript 提供了太多的功能让我们操作文本(比如normal),以致于 循环 语句在 Vimscript 中显得不那么重要。
    即便如此,对于一门编程语言来说,在某些时候,一定是会需要使用 循环 操作的。

    下面介绍 Vimscript 中两种比较常用的循环语句:

    • for循环:Vimscript 提供的for循环语句挺优雅的:
    :for i in [1, 2, 3, 4]
    :  echom i   " 1 2 3 4
    :endfor
    
    • while循环:Vimscript 也提供了经典的while循环:
    :let c = 1
    :let total = 0
    
    :while c <= 4
    :  let total += c
    :  let c += 1
    :endwhile
    
    :echom total " 10
    

    更多详细信息,请查看::help for/:help while

    切换(Toggling)

    前文讲过,对于布尔值选项,我们可以通过set someoption!进行切换,这种做法对于按键映射是再适合不过了:

    :nnoremap <leader>N :setlocal number!<cr>
    

    上述代码,我们只需为选项设置一个映射,就可完成选项开/关的切换,无须定义两个映射分别进行控制,非常优雅。

    而对于非布尔值选项的切换,需要额外做些工作。

    Vimscript 中的非布尔值选项可以分为以下两种类型:

    • 状态选项:即选项的状态有有限多个值进行控制。比如,对于数值选项foldcolumn,其值范围为0 ~ 12,当为0时,表示折叠标识列关闭,当值为非零数值时,即为折叠标识列的宽度,也就是0表示关闭折叠标识列,非零表示打开。因此切换开关只需对数值进行判断即可:
    nnoremap <leader>f :call FoldColumnToggle()<cr>
    
    function! FoldColumnToggle()
        if &foldcolumn " 0 为 false
            setlocal foldcolumn=0
        else
            setlocal foldcolumn=4
        endif
    endfunction
    
    • 无状态选项:有些功能的开/光分别由两个或两个以上的命令进行控制,比如,对于quickfix窗口,其打开由命令copen控制,关闭由命令cclose控制,如果此时还想支持该种功能的实现,则需要我们人为进行状态维护,常见的做法就是使用一个全局变量进行状态记录,虽然不是很优雅,但是简单直接:
    nnoremap <leader>q :call QuickfixToggle()<cr>
    
    let g:quickfix_is_open = 0 " 全局变量,用于记录 quickfix 窗口开关状态
    
    function! QuickfixToggle()
        if g:quickfix_is_open
            cclose
            let g:quickfix_is_open = 0                        " 表示 quickfix 窗口已关闭
            execute g:quickfix_return_to_window . "wincmd w"  " 回到上一个窗口中
        else
            let g:quickfix_return_to_window = winnr()         " 记录当前窗口号
            copen
            let g:quickfix_is_open = 1                        " 表示 quickfix 窗口已关闭
        endif
    endfunction
    

    更多详细信息,请查看::help foldcolumn/:help winnr()/:help ctrl-w_w/:help wincmd

    函数式编程(Functional Programming)

    • 函数式编程:以下摘自 wiki

    a style of building the structure and elements of computer programs—that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data.
    一种构建计算机程序结构和元素的方式:将电脑运算视为函数的计算,同时避免更改状态和使用可变数据。

    函数式编程是一种编程范式,该函数指的不是编程语言中的function,而是数学的函数。参数作为自变量,经函数执行后映射为另一个数。

    在函数式编程中,函数式是一等公民,因此几乎可以在任何地方定义和使用函数。且由于其数学原理,一个函数的值只取决于参数的值,不依赖其他状态,因此,函数式编程不会依赖和更改外部状态和数据,没有副作用。

    在 Vimscript 中使用函数式编程,大概涉及如下三方面内容:

    • 不可变数据结构:Vimscript 并未提供不可修改的数据结构,但我们可以自己构造不可变数据结构:
    function! Sorted(l)
        let new_list = deepcopy(a:l) " 深拷贝
        call sort(new_list)
        return new_list
    endfunction
    

    通过对 Vimscript 内置数据结构进行深拷贝,构建一个副本,函数内所有的修改都针对副本进行,因此,对外部数据没有副作用。

    其他数据结构的不可变模式如上同理:

    function! Reversed(l)
        let new_list = deepcopy(a:l)
        call reverse(new_list)
        return new_list
    endfunction
    
    function! Append(l, val)
        let new_list = deepcopy(a:l)
        call add(new_list, a:val)
        return new_list
    endfunction
    
    function! Assoc(l, i, val)
        let new_list = deepcopy(a:l)
        let new_list[a:i] = a:val
        return new_list
    endfunction
    
    function! Pop(l, i)
        let new_list = deepcopy(a:l)
        call remove(new_list, a:i)
        return new_list
    endfunction
    
    • 函数变量:Vimscript 支持变量存储函数,但语法有点愚钝:
    :let Myfunc = function("Append")    " 执行 Append 函数
    :echo Myfunc([1, 2], 3)             " [1,2,3]
    

    :存储函数对象的变量名首字母需大写。

    函数同样可以存储到列表中,比如:

    :let funcs = [function("Append"), function("Pop")]
    :echo funcs[1](['a', 'b', 'c'], 1)  " [a,c]
    
    • 高阶函数:指能接收函数作为参数或返回函数的函数。
    function! Mapped(fn, l) " Mapped 为高阶函数,其第一个参数为函数对象
        let new_list = deepcopy(a:l)
        call map(new_list, string(a:fn) . '(v:val)') " map 对数据进行映射
        return new_list
    endfunction
    
    
    :let mylist = [[1, 2], [3, 4]]
    :echo Mapped(function("Reversed"), mylist) " [[2,1],[4,3]]
    

    map({expr1}[,{expr2}])用于对列表或字典进行转换,其中v:val表示{expr2}当前传递过来的条目。
    string({expr})用于将{expr}转换为字符串,比如,string(function("Reversed"))转化为"function('Reversed')"

    更多详细信息,请查看::help sort()/:help reverse()/:help copy()/:help deepcopy()/:help map()/:help function()

    路径(Paths)

    Vimscript 内置了一些 API 对路径处理非常有用:

    :echo expand('%')                           " test.vim,相对路径
    :echo expand('%:p')                         " ~/vimDir/test.vim,绝对路径
    :echo expand('%:p:h')                       " ~/vimDir,移除最后一个路径
    :echo expand('%:p:t')                       " test.vim,最后部分路径
    :echo expand('%:p:r')                       " ~/vimDir/test,移除扩展名
    :echo expand('%:p:e')                       " vim,只保留扩展名
    :echo expand('%:p:t:r')                     " test
    :echo fnamemodify('foo.txt', ':p')          " ~/vimDir/foo.txt,构建指定文件路径
    
    :echo globpath('.', '*')                    " 列举当前文件夹所有文件,不递归子文件夹
    :echo globpath('.', '**')                   " 列举当前文件夹及其子文件夹的所有文件
    :echo split(globpath('.', '*.md'), '\n')    " 列举当前文件夹所有 .md 文件,返回列表
    :echo split(globpath('.', '**/*.md'), '\n') " 列举当前文件夹及其子文件夹的所有 .md 文件,返回列表
    

    更多详细信息,请查看::help expand()/:help fnamemodify()/:help filename-modifiers()/:help simplify()/:help resolve()/:help globpath()/:help wildcards

    Vim 目录结构

    Vim 支持把插件分割成多个文件,在目录~/.vim下,可以看到很多不同的目录,它们分别有不同的作用。

    以下列举几个比较重要的 Vim 目录:

    • ~/.vim/colors/:用于存储主题(color schemes)文件。比如:color mycolorsVim 就会在该文件夹下找寻~/.vim/colors/mycolors并执行它。

    • ~/.vim/plugin/:用于存储插件,每次 Vim 启动时,都会加载该文件夹下的所有插件。

    • ~/.vim/ftdetect/ftdetect表示filetype detection,即 类型检测Vim 每次启动时都会加载该文件夹下的所有内容,检测打开的文件类型,设置其对应的filetype。因此该文件夹下的文件内容其实就是一些检测文件类型的自动命令和对打开文件设置filetype的功能,代码通常不超过两行。

    au BufNewFile,BufRead *.pn set filetype=potion
    

    ~/.vim/ftdetect/中的文件无须定义一个自动命令组,因为 Vim 自动将ftdetect/*.vim都归属到一个组中。

    • ~/.vim/ftplugin/:当 Vim 设置了filetype选项时,就会自动从该文件夹下寻找匹配该filetype的插件,因此,该文件夹下的文件命名很重要,必须与filetype名称一致,比如set filetype=drep时,Vim 就会找寻~/.vim/ftplugin/derp.vim,如果存在该文件,就会进行加载,如果存在同名目录,即~/.vim/ftplugin/derp/,则 Vim 会加载该同名目录下的所有.vim文件。每次缓冲区设置了filetype后,该文件夹都会被加载,因此filetype必须设置为buffer-local,避免因设置了全局选项而导致覆盖了其他所有缓冲区。
      :可通过在本地缓冲区设置setlocal filetype=xx触发 Vim 加载~/.vim/ftplugin/目录下的相应文件。

    • ~/.vim/indent/:该文件夹类似ftplugin,都会根据其名称进行加载。indent文件设置了相关文件类型的各自缩进配置,同样,这些匹配选项也必须设置为buffer-local

    • ~/.vim/compiler/:该文件夹工作机制与indent文件夹一摸一样,用于根据当前缓冲区的名称设置相关的编译选项。

    • ~/.vim/after/:每次 Vim 启动并加载完成~/.vim/plugin/后,就会加载该文件夹。在这里,你可以覆盖 Vim 的一些默认选项。

    • ~/.vim/autoload/autoload提供了一种 延迟加载插件 的功能,只有在用到插件功能时,才加载插件。

    • ~/.vim/doc/:放置插件文档的目录。

    自动加载(Autoloading)

    前面说过,对于~/.vim/autoload/Vim 会自动加载该目录下对应的文件,autoloadVim 提供的一种延迟加载的功能。

    如果要想使自己的插件支持自动加载,插件编写需要遵循以下要求:

    • 将要支持延迟加载的代码(一般为函数)放置到autoload中。

    • 命名:插件自动加载的命名方式为:插件文件名#函数,比如:

    " autoload/somefile.vim
    function! somefile#Hello()
    endfunction
    
    :call somefile#Hello() 
    

    上述代码表示执行autoload/somefile.vim文件内的Hello()函数,当 Vim 首次执行,没有找到somefile#Hello()函数的定义时,就会根据该函数名自动去autoload目录下找寻somefile.vim文件,如果存在该文件,就加载整个文件,并执行其Hello()函数。由于已经加载了somefile.vim,后续执行该文件内的任何函数就无须再进行加载了,而是直接执行。
    :可以使用多个#代表多级目录,比如::call myplugin#somefile#Hello(),表示执行autoload/myplugin/somefile.vim里面的Hello()函数。

    " autoload/myplugin/somefile.vim
    function! myplugin#somefile#Hello()
    endfunction
    

    更多详细信息,请查看::help autoload

    文档(Documentation)

    Vim 自带的文档非常优秀,非常全面且不过于啰嗦。许多优秀的开发者受 Vimscript 文档的影响也开始为自己的插件进行文档编写,这造就了 Vimscript 社区拥有优秀的文档文化。

    插件文档存放于~/.vim/doc目录中,文档的文件类型为set filetype=helpVim 会自动渲染文档格式文本,方便我们查阅。

    以下介绍几个常用的渲染指令:

    • ========:用于表示章节/区域划分
    • *<tag>**号用于创建一个标签。使用命令help <tag>就可以跳转到该标签帮助文档处。比如下例所示:
      ====================================================================
      INTRODUCTION                                 *myplugin-introduction*
      
      This is the introduction section of myplugin.
      
      表示插件myplugin的帮助文档简介INTRODUCTION章节,其对应的标签为myplugin-introduction,因此,在 Vim 中输入help myplugin-introduction就可以跳转到当前帮助文档位置。
    • |<tag>||号包裹表示引用标签内容。用户通过在当前位置键入<C-]>就可以跳转到引用的标签位置。
      :当章节正文内容要引用其他章节或某个函数时,可以为相关章节或函数创建相应的标签,然后在当前正文进行引用。

    插件文档没有严格的格式要求,但是建议包含如下几部分:

    • 帮助头部:通常会在插件帮助文档第一行创建插件文档标签,并进行简单介绍。比如,如果插件名为myplugin,那么其对应的文档存储在myplugin/doc/myplugin.txt,其第一行内容如下:
      *myplugin.txt* a short description of myplugin.
      
      后面我们输入help myplugin.txthelp myplugin就可以直接跳转到插件帮助文档头部,方便完整阅读文档。
    • 简介(Introduction)
    • 使用方法(Usage)
    • 按键映射(Mappings)
    • 配置选项(Configuration)
    • License
    • Bugs:描述当前插件存在的漏洞
    • Contributing:描述提交pull/requests的格式等
    • Changelog:更新日志
    • Credits:荣誉墙,列举插件开发者,受启发的项目···

    一个完整的帮助文档例子可以参考笔者开发的插件:enhanceOPM.txt

    局部命名空间(<SID>)

    当加载了多个 Vimscript 脚本时,有可能一个或多个脚本存在相同名字的映射或函数,这将导致错误。为了避免这个问题,可以将变量定义为私有/局部变量。

    • <SID>:该字符串可用于按键映射或菜单。其有如下几个作用:
    1. 自动命名:当作用于按键映射命令map时,Vim 会将<SID>替换为:一个特殊键码<SNR> + 一个全局唯一的序号 + _,比如:
    :map <SID>Add " 一个可能的转换为:<SNR>23_Add
    
    1. 自动追踪:我们可以使用s:前缀去定义一个 Vimscript 脚本私有函数,但是当有按键映射到这个私有函数,且在该脚本外触发该按键映射时,由于上下文不同,此时按键映射无法找到该私有函数的定义出处,因此会报错。比如:
    " a.vim
    function s:localFunc()
        echom 'call local functio nsuccessfully!!'
    endfunction
    nnoremap te :call s:localFunc()<cr>
    

    我们只加载该a.vim文件:vim -u a.vim -N,输入te,可以看到报错信息:

    E81: Using <SID> not in a script context
    

    报错信息是:在脚本环境外使用了<SID>

    出错的原因在于:一个私有函数只能在它被定义的脚本中被直接调用,而当按键映射时,则相当于在另一个脚本中调用该私有函数,上下文不同了,因此调用失败。

    一个简单的例子如下:

    " a.vim
    function s:localFunc()
        echom 'call local functio nsuccessfully!!'
    endfunction
    
    call s:localFunc() " OK
    

    直接加载a.vimvim -u a.vim -N,可以看到执行成功。

    " a.vim
    function s:localFunc()
        echom 'call local functio nsuccessfully!!'
    endfunction
    
    " b.vim
    source a.vim
    call s:localFunc() " error
    

    直接加载b.vimvim -u b.vim -N,可以看到执行失败,我这里的失败信息如下所示。

    E117: Unknown function: <SNR>1_localFunc
    

    从上述失败信息可以看出:Vimscript 中对私有函数的处理其实就是我们上面说的<SID>自动命名,所以私有函数最终会被编译成一个全局的唯一函数,因此其他脚本只需通过<SID>就可直接进行访问,如下所示:

    " a.vim
    function s:localFunc()
        echom 'call local functio nsuccessfully!!'
    endfunction
    nnoremap te :call <SID>localFunc()<cr>
    

    直接加载a.vim文件:vim -u a.vim -N,输入te,现在可以看到执行成功了。

    1. 上下文自动绑定:在自动命令或用户命令中,私有函数会自动绑定到它被定义的脚本的上下文中,因此直接调用是可行的。
    function s:localFunc()
        echom 'call local functio nsuccessfully!!'
    endfunction
    
    autocmd TextChanged,TextChangedI * :call s:localFunc()    " OK
    autocmd TextChanged,TextChangedI * :call <SID>localFunc() " OK
    

    :建议引用脚本私有变量时,一律采用<SID>进行引用。

    综上,<SID>其实就是script ID,表示当前脚本文件的IDVim 会在脚本内将<SID>转换为<SNR>xxx_xxx代表当前脚本的ID,对于不同的脚本文件,xxx不同,这样<SID>其实就是自动创建了一个全局唯一的命名空间,可以让不同的脚本享用同名变量,也即实现了脚本私有变量。在脚本被加载后,其内私有变量就会被自动映射到一个唯一的全局变量中。

    更多详细信息,请查看::help <SID>

    全局唯一按键映射(<Plug>)

    前面说过,<SID>是当前脚本的特有ID,被<SID>修饰的变量/函数能且只能在当前脚本被调用,外部脚本是无法直接显示调用其他脚本的<SID>作用域的。

    因此,脚本如果要提供变量/函数给到外部脚本调用,一个简单的方法就是通过定义全局变量/函数,但这种方式存在弊端,就是如果多个脚本存在相同的全局变量/函数,那么前面某些脚本定义的就被覆盖了,对于这些脚本来说,其内部再调用这些变量/函数就可能产生不可预期的结果。

    因此,Vim 提供了<Plug>机制,通过按键映射让我们能实现真正的全局唯一绑定,且对其他脚本无侵入(不会覆盖其他脚本变量/函数定义)。

    <Plug>代表了一个特殊的键码,用户普通的按键操作不可能产生这种键码,以此保证全局唯一。

    通常<Plug>会绑定到一个函数(也即某些操作),然后其他脚本使用该同名<Plug>来调用前面脚本定义的功能(通常是通过按键映射进行调用),比如:

    " a.vim
    nnoremap <Plug>GlobalMapping :echo 'a.vim => <Plug> is a global mapping'
    
    " b.vim
    source a.vim
    nmap ,g <Plug>GlobalMapping
    

    上述代码中脚本a.vim暴露了一个操作<Plug>GlobalMapping,此时外部脚本b.vim通过按键映射直接引用了<Plug>GlobalMapping

    而由于<Plug>是全局唯一的,因此,当其他脚本也使用了同名<Plug>时,则会覆盖前面的同名定义,比如:

    " b.vim
    source a.vim
    
    nnoremap <Plug>GlobalMapping :echo 'b.vim => <Plug> is a global mapping'
    nmap ,g <Plug>GlobalMapping
    

    由于b.vim覆盖了a.vim<Plug>GlobalMapping,因此按键,g此时响应的就是b.vim定义的操作。

    <Plug>通常的命名规范为<Plug>{scriptname}{mapname}<Plug>结合<SID>就可以将脚本的私有函数暴露给其他脚本使用,而且当其他脚本覆盖了同名<Plug>时,对于当前脚本的内部是没有任何影响的,因此内部使用的都是<SID>操作,如下:

    " a.vim
    function! s:sayHello()
        echom 'a.vim => hello world'
    endfunction
    
    " 如果 normal 模式下没有映射 <Plug>asayHello,则进行映射
    if !hasmapto('<Plug>asayHello', 'n')
        nnoremap <Plug>asayHello :call <SID>sayHello()<CR>
    endif
    

    用户自定义命令(command)

    Vim 提供了用户自定义 Ex 命令的功能,一个用户自定义的命令与 Vim 内置的命令基本一致(可以带范围,参数,参数可以为文件名或缓冲区名称等等),除了当用户自定义命令被执行时,会被转换为一个normal的 Ex 命令,然后再执行。

    用户自定义命令基本操作如下所示:

    • 基本使用:定义一个命令行命令,使用:command命令即可:
    :command DeleteFirst 1delete
    

    此时命令行执行::DeleteFirst,就可以看到第一行文本被删除了,相当于执行了:1delete
    :用户自定义命令名称首字母必须大写。

    其他基本操作还有:
    覆盖定义:使用!可以强制覆盖已定义的同名自定义命令:

    :command -nargs=+ Say :echo "<args>"
    :command! -nargs=+ Say :echo <q-args>
    

    查看用户自定义命令:方法如下:

    " 列举所有用户自定义命令
    :com[mand]
    
    " 列举以 {cmd} 开头的用户自定义ingle
    :com[mand] {cmd}
    
    " 展示命令 Say 的定义位置
    :verbose command Say
    

    删除命令:可删除单个命令delcommand或删除全部用户命令comclear

    " 删除自定义命令 Say
    :delcommand Say
    " 清空全部自定义命令,谨记:该操作无法撤销
    :comclear
    
    • 参数个数:用户自定义命令可接收参数,参数个数由选项-nargs指定:
    :command -nargs=0 DeleteFirst 1delete " DeleteFirst命令不接收参数
    

    :用户自定义命令参数个数缺省默认为 0。

    以下是-nargs的常用取值范围:

    指令 参数个数
    -nargs=0 不接收参数
    -nargs=1 接收一个参数
    -nargs=* 接收任意数目参数
    -nargs=? 0个或一个参数
    -nargs=+ 至少一个参数
    • 参数使用:在用户自定义命令定义内部,可以对参数进行使用,如下所示:

    可以通过关键字<args>来引用传递进来的参数:

    " 自定义命令
    :command -nargs=+ Say :echo "<args>"
    " 调用命令
    :Say Hello World " Hello World
    

    :如果传递进来的参数携带特殊字符,比如:

    :Say he said "hello"
    

    由于"hello"带双引号,因此上述命令执行会报错。\

    若参数携带特殊符号,需要使用关键字<q-args>进行接收:

    :command -nargs=+ Say :echo <q-args>
    
    :Say he said "hello" " hello "world"
    

    <q-args>会自动对参数特殊字符进行转义,因此上述代码会被转义为如下:

    :echo "he said \"hello\""
    

    对于函数接收参数时,使用关键字<f-args>

    :command -nargs=* DoIt :call AFunction(<f-args>)
    :DoIt a b c " 转换为:call AFunction("a","b","c")
    
    • 范围限定:因为自定义命令也属于命令行命令,因此它也同样支持指定范围执行。限制范围使用选项-range进行指定,其有如下几个值可选:
    范围 描述
    -range 使能范围指定,默认为当前行
    -range=% 使能范围指定,默认为整个文件
    -range={count} 使能范围指定,最后一行缺省则默认为{count}

    -range={count}的意思如下:

    :command -range=10 PrintLine :echom <line1> <line2>
    
    :1, PrintLine " 1 10,当只指定了范围起始位置,当未指定结束位置时,则结束位置默认则为 -range=10 指定的10
    
    • 其他选项command的其他一些选项和关键字如下所示:
    选项 描述
    -count={number} command命令可以带count参数,缺省值为{number}。使用<count>关键字可以访问该参数
    -bang 允许使用!。若!出现,<bang>会被扩展为!
    -register 可以指定寄存器(缺省为无名寄存器)
    -complete={type} 给出命令行补全方式,参见::command-completion
    -bar 该自定义命令后面使用` 运行另一个命令,或者使用"` 加一个注释
    -buffer 该自定义命令只对当前缓冲区有效
    <lt> 代表<字符,使用该关键字对上述涉及的<>进行转义

    更多详细信息,请参考::help 40.2/:help user-command

    其他

    • grepVim 内置了两种方法进行匹配查找:内部匹配查找外部匹配查找
    1. 内部匹配查找(Internal):内部匹配查找的优点是支持跨平台,并且使用 Vim 自带的强大的搜索匹配模式。缺点是其搜索速度相对较慢,因为它的处理方式是将要搜寻的所有文件都读进内存中。
      内部匹配查找的命令为::vim[grep][!]
    :vimgrep hello *.md
    
    1. 外部匹配查找(External)Vim 可以集成外部grepgrep-like程序。
      外部匹配搜索的命令为::gr[ep]
    :grep hello *.md
    

    :grep搜索完成后会自动跳转到第一个搜索位置,禁止自动跳转可以使用::grep!

    grep搜索的结果会存储于quickfix-window中,可通过命令copen(或如果使用的是:lgrep,则为:lopen)进行查看。

    更多详细信息,请查看::help grep/:help :grep/:help :make/:help quickfix-window

    • <cword>:获取当前光标所在的字符串:
    :echo expand("<cword>")
    

    同里还有:<cWORD>/<cfile>/<abuf>···

    更多详细信息,请查看::help cword

    • shellescape:传递给字符串给 shell 命令时,最好先将字符串转义一下,避免因某些特殊字符的存在而导致行为出错:
    :echo shellescape("that's")
    :echo shellescape("that is")
    
    • 禁止修改缓冲区:setlocal nomodifiable
      禁止保存缓冲区到文件:setlocal buftype=nofile

    相关文章

      网友评论

          本文标题:读书笔记:《Learn Vimscript the Hard W

          本文链接:https://www.haomeiwen.com/subject/vklbactx.html