From b85c61a58c6025916f0cae4dded98d2852a6ef73 Mon Sep 17 00:00:00 2001 From: lymslive <403708621@qq.com> Date: Thu, 30 Nov 2023 18:04:54 +0800 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=E9=87=8D=E7=BB=84=E7=AB=A0?= =?UTF-8?q?=E8=8A=82=E8=87=B3=20content?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitmodules | 3 + config.toml | 19 + content/_index.md | 7 + content/ch00-prefcace/_index.md | 74 ++ content/ch01-viml-feature/_index.md | 7 + content/ch01-viml-feature/sn1-hello-world.md | 202 ++++++ .../ch01-viml-feature/sn2-from-ex-command.md | 271 ++++++++ .../sn3-week-type-strong-scope.md | 202 ++++++ .../ch01-viml-feature/sn4-autoload-schema.md | 87 +++ content/ch02-viml-grammar/_index.md | 6 + .../ch02-viml-grammar/sn1-variable-type.md | 197 ++++++ .../sn2-comapare-condition.md | 250 +++++++ content/ch02-viml-grammar/sn3-loop-iterate.md | 233 +++++++ .../ch02-viml-grammar/sn4-function-call.md | 358 ++++++++++ .../ch02-viml-grammar/sn5-exception-error.md | 50 ++ content/ch03-viml-command/_index.md | 29 + content/ch03-viml-command/sn1-option-set.md | 187 +++++ content/ch03-viml-command/sn2-key-remap.md | 648 ++++++++++++++++++ .../ch03-viml-command/sn3-custom-command.md | 421 ++++++++++++ .../ch03-viml-command/sn4-execute-normal.md | 230 +++++++ .../ch03-viml-command/sn5-autocmd-event.md | 332 +++++++++ .../ch03-viml-command/sn6-debug-command.md | 134 ++++ content/ch04-viml-datastruct/_index.md | 11 + .../ch04-viml-datastruct/sn1-list-string.md | 300 ++++++++ .../ch04-viml-datastruct/sn2-dictionary.md | 97 +++ .../ch04-viml-datastruct/sn3-nest-compose.md | 214 ++++++ .../ch04-viml-datastruct/sn4-regex-apply.md | 267 ++++++++ content/ch05-viml-function/_index.md | 10 + .../sn1-variable-argument.md | 196 ++++++ .../ch05-viml-function/sn2-function-refer.md | 251 +++++++ .../ch05-viml-function/sn3-dict-function.md | 425 ++++++++++++ .../ch05-viml-function/sn4-closure-lambda.md | 374 ++++++++++ .../sn5-autoload-function.md | 290 ++++++++ content/ch06-builtin-function/_index.md | 23 + .../sn1-operate-datatype.md | 363 ++++++++++ .../sn2-operate-edit-object.md | 512 ++++++++++++++ .../sn3-operate-filesystem.md | 271 ++++++++ .../sn4-other-utility.md | 158 +++++ content/ch07-object-program/_index.md | 11 + .../ch07-object-program/sn1-object-intro.md | 119 ++++ .../ch07-object-program/sn2-dict-object.md | 343 +++++++++ .../sn3-object-organize.md | 474 +++++++++++++ content/ch08-viml-asynchronous/_index.md | 7 + .../sn1-asynchronous-intro.md | 134 ++++ .../sn2-asynchronous-job.md | 190 +++++ .../ch08-viml-asynchronous/sn3-channle-job.md | 255 +++++++ .../sn4-internal-terminal.md | 291 ++++++++ content/ch09-viml-mix-program/_index.md | 6 + .../sn1-extern-filter.md | 153 +++++ .../sn2-extern-interface.md | 348 ++++++++++ .../sn3-perl-interface.md | 251 +++++++ content/ch10-viml-plugin-develop/_index.md | 6 + .../sn1-plugin-directory.md | 358 ++++++++++ .../sn2-plugin-manager.md | 230 +++++++ .../sn3-plugin-devflow.md | 518 ++++++++++++++ content/chA1-postfcace/_index.md | 9 + movezola.sh | 560 +++++++++++++++ 57 files changed, 11972 insertions(+) create mode 100644 .gitmodules create mode 100644 config.toml create mode 100644 content/_index.md create mode 100644 content/ch00-prefcace/_index.md create mode 100644 content/ch01-viml-feature/_index.md create mode 100644 content/ch01-viml-feature/sn1-hello-world.md create mode 100644 content/ch01-viml-feature/sn2-from-ex-command.md create mode 100644 content/ch01-viml-feature/sn3-week-type-strong-scope.md create mode 100644 content/ch01-viml-feature/sn4-autoload-schema.md create mode 100644 content/ch02-viml-grammar/_index.md create mode 100644 content/ch02-viml-grammar/sn1-variable-type.md create mode 100644 content/ch02-viml-grammar/sn2-comapare-condition.md create mode 100644 content/ch02-viml-grammar/sn3-loop-iterate.md create mode 100644 content/ch02-viml-grammar/sn4-function-call.md create mode 100644 content/ch02-viml-grammar/sn5-exception-error.md create mode 100644 content/ch03-viml-command/_index.md create mode 100644 content/ch03-viml-command/sn1-option-set.md create mode 100644 content/ch03-viml-command/sn2-key-remap.md create mode 100644 content/ch03-viml-command/sn3-custom-command.md create mode 100644 content/ch03-viml-command/sn4-execute-normal.md create mode 100644 content/ch03-viml-command/sn5-autocmd-event.md create mode 100644 content/ch03-viml-command/sn6-debug-command.md create mode 100644 content/ch04-viml-datastruct/_index.md create mode 100644 content/ch04-viml-datastruct/sn1-list-string.md create mode 100644 content/ch04-viml-datastruct/sn2-dictionary.md create mode 100644 content/ch04-viml-datastruct/sn3-nest-compose.md create mode 100644 content/ch04-viml-datastruct/sn4-regex-apply.md create mode 100644 content/ch05-viml-function/_index.md create mode 100644 content/ch05-viml-function/sn1-variable-argument.md create mode 100644 content/ch05-viml-function/sn2-function-refer.md create mode 100644 content/ch05-viml-function/sn3-dict-function.md create mode 100644 content/ch05-viml-function/sn4-closure-lambda.md create mode 100644 content/ch05-viml-function/sn5-autoload-function.md create mode 100644 content/ch06-builtin-function/_index.md create mode 100644 content/ch06-builtin-function/sn1-operate-datatype.md create mode 100644 content/ch06-builtin-function/sn2-operate-edit-object.md create mode 100644 content/ch06-builtin-function/sn3-operate-filesystem.md create mode 100644 content/ch06-builtin-function/sn4-other-utility.md create mode 100644 content/ch07-object-program/_index.md create mode 100644 content/ch07-object-program/sn1-object-intro.md create mode 100644 content/ch07-object-program/sn2-dict-object.md create mode 100644 content/ch07-object-program/sn3-object-organize.md create mode 100644 content/ch08-viml-asynchronous/_index.md create mode 100644 content/ch08-viml-asynchronous/sn1-asynchronous-intro.md create mode 100644 content/ch08-viml-asynchronous/sn2-asynchronous-job.md create mode 100644 content/ch08-viml-asynchronous/sn3-channle-job.md create mode 100644 content/ch08-viml-asynchronous/sn4-internal-terminal.md create mode 100644 content/ch09-viml-mix-program/_index.md create mode 100644 content/ch09-viml-mix-program/sn1-extern-filter.md create mode 100644 content/ch09-viml-mix-program/sn2-extern-interface.md create mode 100644 content/ch09-viml-mix-program/sn3-perl-interface.md create mode 100644 content/ch10-viml-plugin-develop/_index.md create mode 100644 content/ch10-viml-plugin-develop/sn1-plugin-directory.md create mode 100644 content/ch10-viml-plugin-develop/sn2-plugin-manager.md create mode 100644 content/ch10-viml-plugin-develop/sn3-plugin-devflow.md create mode 100644 content/chA1-postfcace/_index.md create mode 100755 movezola.sh diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..032099e --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "book"] + path = themes/book + url = git@github.com:getzola/book.git diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..e64a9a3 --- /dev/null +++ b/config.toml @@ -0,0 +1,19 @@ +# The URL the site will be built for +base_url = "https://example.com" + +# Whether to automatically compile all Sass files in the sass directory +compile_sass = true + +# Whether to build a search index to be used later on by a JavaScript library +build_search_index = true + +theme = "book" + +[markdown] +# Whether to do syntax highlighting +# Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola +highlight_code = true + +[extra] +# Put all your custom variables here +book_number_chapters = false diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..c5b7405 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,7 @@ ++++ +title = "VimL 语言编程指北" +weight = 0 +sort_by = "weight" +redirect_to = "ch00-prefcace" ++++ + diff --git a/content/ch00-prefcace/_index.md b/content/ch00-prefcace/_index.md new file mode 100644 index 0000000..372fe8c --- /dev/null +++ b/content/ch00-prefcace/_index.md @@ -0,0 +1,74 @@ ++++ +title = "前言" +weight = 0 +sort_by = "weight" ++++ + + +这是一篇有关 Vim 脚本语言的入门与进阶教程。是“指北”,不是“指南”,所以如果不慎 +指错了路,切勿见怪。不过要相信地球是圆的,绕了一圈之后,希望还是能找对目标的。 + +初学者如果第一章看不懂,建议直接看第二章;如果第二章看不懂,建议直接看第三章; +如果第三章也看不懂,建议直接放弃治疗,汝须先培养对 vim 的信仰习惯。 + +以下……开始严肃话题。 + +## 正名约定 + +Vim 是上古神器之一,且能历久弥新,与时俱进。随着 Vim 的发展,Vim 的脚本也逐渐 +发展壮大,支持的功能与特性越来越丰富,俨然成为一种新的或老的实用脚本语言。然而 +这语言的名字,网上的称谓似乎还有点五花八门。 + +为了行文统一与方便,在这里我采用“VimL”(Vim Language 缩写)来表示该脚本语言,用“ +Vim” 表示编辑器;而小写的 “vim” 则是指系统中可执行的编辑器程序,若从“VimL”角度 +看,也算是它的解释器程序;然后“vim script”就是存放“VimL”代码且可用“vim”解释运 +行它的文本文件。 + +## 目标假设 + +本教程针对的目标群体,假定是有使用 Vim 的基础及一定的编程基础。尽管我尽量从基 +本概念讲起,但一些最基础的东西怕无从再细致了。然后最重要的是要热爱 Vim ,并且 +有折腾的精神来打造或调教自己的 Vim。 + +其实,不管是使用 Vim 还是 VimL,最好的资源都是 Vim 的内置帮助文档(`:help`)。 +外部教程都不免有所侧重,较适于学习阶段的引领者。 + +本教程依据的 Vim 版本是 `8.1`,系统环境 Linux。但除了一些新特性,应该也适用 +Vim7 以下版本。同时由于 Vim 本身是跨平台的,VimL 自然也与操作系统无关。虽然无 +法一一验证,但在一些重要的差异处,尽量在文中指出。 + +最新的 Vim 版本是 9,主要是大幅提升性能,同时也改变了之前版本的部分用法,整体 +向现代的通用脚本语言靠近。本书后续计划加入 Vim 9 的新语法,但因本书讲的更多是 +通用的基础且 Vim 9 还在不断发展,待时机成熟后再更新也不迟。 + +## VimL 的优缺点 + +作为一种语言,首先指出 VimL 的缺点一是只能在 Vim 环境下运行,二是运行速度有点 +慢。但是,对于热衷 Vim 的程序猿,每天的编码工作都在 Vim 环境下,VimL 的编程方 +式与 Vim 的操作方式无间密合,应该算是个优势。 + +另外,程序的运行速度都是相对的。所有的动态脚本语言,相对静态的编译语言,都很慢 +。但这不要紧,只要完成大部分工作能足够快,脚本的简单便捷性就能体现出来了。VimL +同样具有脚本语言这个共性。 + +用 Vim 编写 VimL 代码,另有个天然的优势,就是编辑器,解释器,与文档手册一体化 +,同时仍然保持了 Vim 的小巧,不像静态语言的IDE那么笨重。 + +编程思想基本是独立于语言的,大多数语言都是相通的。现代的高级脚本语言更是几乎都 +能提供差不多的功能。(而且,据说只要是“图灵完备”的语言,理论上都能做任何事)。 +所以,经常使用 Vim 的程序猿,如果想多学一门脚本语言,那 VimL 是个不坏的选择。 + +## 文本约定 + +本教程拟用 `.md` 文件书写,章用一级标题,节用二级标题,每节至少一个文件。初稿 +不一定严格按目录大纲的顺序书写,并且在此过程中或有增删调整。 + +带星号 `*` 的章节,表示略有艰深晦涩的内容,可以选择性略过。 + +关于示例代码块,`:` 开始的行表示 Vim 的命令行(也叫 ex 命令),`$` 开始的行表 +示从 shell 运行的命令行。较短的示例代码,可以直接输入或粘贴入 vim 的命令行,较 +长的示例代码,建议保存 `.vim` 文件,然后 `:source`。 + +本书正文共十章,可粗略分为三部分。第 1-3 章为基础篇,第 4-7 章为中级篇,第 +8-10 为高级篇。在行文组织上尽量循序渐进,建议按顺序阅读。文中经常用提示用 +`:help` 命令查阅相关帮助主题,此后忘记细节时可随时查询。 diff --git a/content/ch01-viml-feature/_index.md b/content/ch01-viml-feature/_index.md new file mode 100644 index 0000000..0afc5fd --- /dev/null +++ b/content/ch01-viml-feature/_index.md @@ -0,0 +1,7 @@ ++++ +title = "第一章 VimL 语言主要特点" +weight = 1 +sort_by = "weight" ++++ + +基础篇包括第一章至第三章。 diff --git a/content/ch01-viml-feature/sn1-hello-world.md b/content/ch01-viml-feature/sn1-hello-world.md new file mode 100644 index 0000000..107e4be --- /dev/null +++ b/content/ch01-viml-feature/sn1-hello-world.md @@ -0,0 +1,202 @@ ++++ +title = "1.1 Hello World 的四种写法" +weight = 1 ++++ + + + +按惯例,我们讨论一门语言,首先看下如何写最简单的 “Hello World” (程序)。 +由于 Vim 是高度自由的,VimL 也有多种不同的方式玩转 “Hello World”。 + +### 速观派:直接操起命令行 + +最快速的办法是在 Vim 命令行下用 `:echo` 命令输出 “Hello World”: +```vim +: echo 'Hello World!' +``` +唯一需要注意的是,得把 “Hello World” 用引号括起来,单引号或双引号都可以。 +这样再按下回车就能在 Vim 的消息区显示出 “Hello World” 这行字符串了。 + +由于这条消息字符串很简短,一行就能显示完整,Vim 将其直接显示在命令行的位置,并 +且运行完直接返回 Vim 普通模式。如果字符串很长或多行字符串,则消息区将向上滚动 +,以显示完整的消息,用户需要额外按个回车才回普通模式。 + +试试在命令行输入这条命令,看看有啥不同反应: +```vim +: echo "Hello World! \n Hello World! \n Hello World!" +``` + +好了,你已经学会了如何用 VimL 输出 “Hello World” 了。这也算编程吗?别逗了!其 +实,别把编程想得那么严肃,那这就算编程! + +### 正规派:建立脚本文件 + +把刚才在命令行输入的那条命令保存在一个 `.vim` 后缀的文本文件中,那就是一个 +`vim script` 了,这是用 VimL 编程比较正常的用法。 + +为了方便,建议在本地建个目录,用于保存本教程的示例代码。比如: +```bash +$ cd ~/.vim +$ mkdir vimllearn +$ vim vimllearn/hello1.vim +``` + +这将在 `~/.vim` 目录下新建一个 `vimllearn` 目录,并用 vim 开始编辑一个名为 +`hello1.vim` 的文件。vim 会为该文件新建一个缓冲区 `buffer`,在该 `buffer` 中输 +入以下文本,然后输入命令 `:w` 保存: +```vim +" 文件:hello1.vim +" 用途:VimL hello world 示例 +" 作者:lymslive +" 时间:2017-08 + +echo 'Hello World!' + +finish + +脚本结束了,可以随便浪~~ +不管写些什么乱七八糟的都木有关系。 +``` + +你其实可以只在该文件中写入 `echo 'Hello World!'` 这一行就够了。几点说明: +1. 前面以一个双引号 `"` 开始的行是注释,注释也可以写在行尾。 +2. 在脚本文件中,`echo` 命令前不必加冒号 `:`,但是加上冒号也是允许的。 +3. `finish` 表示直接结束脚本,在之后的语句都不再被 vim 解析;这是可选的,没有 + 遇到 `finish` 就会执行到文件最后一行。 + +当有了 `*.vim` 脚本文件,就可以在 vim 环境中用 `:source` 命令加载运行了: +```vim +: source ~/.vim/vimllearn/hello1.vim +``` +需要将脚本文件的路径写在 `source` 命令之后作为参数。如果当前 vim 正常编辑 +`hello1.vim` 这个文件,则可用 `%` 表示当前文件的路径: +```vim +: source % +``` + +折腾并解释了这许久,终于可以通过 `source` 一个 vim 脚本打印输出 “Hello World” +了,与此前的效果是一样一样的。当然了,用 VimL 写脚本肯定不能只满足于写 “Hello +World” 吧,所以这才是标准用法。 + +此外 Vim 的命令是可以简写的,`source` 可简写为 `so`。当你在写一个 vim 脚本时想 +快速验证执行该脚本时,可以只输入: +```vim +: so % +``` + +如果还想更省键,就定义一个快捷键映射吧,比如: +```vim +: nnoremap :update:source % +``` +可以将这行定义加入你的 `vimrc` 中,不过最好是放在 `~/.vim/ftplugin/vim.vim` 中 +,并加上局部参数,让它只影响 `*.vim` 文件: +```vim +: nnoremap :update:source % +``` + +### 测试派:进入 Ex 模式 + +直接在命令行用 `:echo` 查看一些东西其实很有用的,可以快速验证一些记不清楚的细 +节。比如你想确认下在 VimL 中字符 `'0'` 是不是与数字 `0` 相等,可以这样: +```vim +: echo '0' == 0 +``` + +但如果要连续输入多条命令并查看结果,每次都要(从普通模式)先输入个冒号,不免有 +些麻烦。这时,`Ex` 模式就有用了。默认情况下(若未在 vimrc 被改键映射),在普通 +模式下用 `Q` 键进入 `Ex` 模式。例如,在 `Ex` 模式下尝试各种输出 “Hell World” +的写法,看看不同引号对结果的影响: +```vim +Entering Ex mode. Type "visual" to go to Normal mode. +: echo 'Hello World!' +: echo "Hello World!" +: echo 'Hello \t World! \n Hello \t World!' +: echo "Hello \t World! \n Hello \t World!" +: vi +``` + +最后,按提示用 `visual` 或简写 `vi` 命令回到普通模式。 + +Vim 的 `Ex` 模式有点像 VimL 的交互式的解释器,不过语法完全一样(有些脚本语言的 +交互式解释器与执行脚本有些不同的优化),仍然要用 `echo` 显示变量的值。 + +### \*索隐派:从 shell 直接运行 + +如果只为了运行一个 vim script 脚本,也不一定要先启动 vim 再 `source`,直接在启 +动 vim 时指定特定参数也能办到。`-e` 参数表示以 `Ex` 模式启动 vim,`-S` 参数启 +动后立即 source 一个脚本。因此,也可以用如下的命令来输出 “Hello World”: +```vim +$ cd ~/.vim/vimllearn +$ vim -eS hello1.vim +``` + +这就相当于使用 `vim` 解释器来运行 `hello.vim` 这个脚本,并且停留在交互式界面 +上。此时可以用 `:q` 命令退出,或 `:vi` 以正常方式继续使用 Vim。 + +vim 本身的命令行启动参数其实还支持很多功能,请查阅 `:help starting`。其中还有 +个特殊的参数是 `-s`,如果与 `-e` 联用,就启动静默的批处理模式,试试这个: +```vim +$ vim -eS hello1.vim -s +``` +没有任何反应输出?因为 `-s` 使普通的 `echo` 提示无效,看不到任何提示!赶紧输入 +`q` 回车退出 vim 回到 shell。因为如果不小心按了其他键,vim 可能就将其当作命令 +来处理了,而且不会有任何错误提示,这就会让大部分人陷入不知如何退出 vim 的恐慌 +。 + +虽然 `vim -e -s` 不适合来输出 “Hello World”,但如果你的脚本不是用来做这种无聊 +的任务,这种模式还是有用的。比如批处理,在完全不必启动 vim 可视编辑的情况下, +批量地对一个文件或多个文件执行编辑任务。可达到类似 `sed` 的效果。而且,在 vim +脚本写好的情况下,不仅可以按批处理模式执行,也可以在正常 Vim 可视编辑某个文件 +时,遇到类似需求时,也可以再手动 `:source` 脚本处理。 + +### 小结 + +运行 vim 脚本的常规方法用 `:source` 命令,而且有很多情况下并不需要手动输入 +`:source` 命令,在满足一定条件下,vim 会自动帮你 source 一些脚本。vim 的启动参 +数 `-S` 其实也是执行 `:source`。 + +Vim 的命令行可以随时手动输入一些简短命令以验证某些语法功能,进入 `Ex` 模式则可 +以连续手动输入命令并执行。`Ex` 模式虽然比较少用,但不该反感排斥,这对学用 VimL +还是大有裨益的,以后会讲到,VimL 的 debug 功能也是在 `Ex` 模式中的。 + +静默批处理 `vim -e -s` 本质上也是 `Ex` 模式,不过禁用或避免了交互的中断。属于 +黑科技,一般的 vim 用户可先不必深究。 + +### \*拓展阅读:Vim 与可视化 + +“可视化”是个相对的概念。现在说到可视化,似乎是指功能丰富的 IDE 那种,有很多辅 +助窗口展示各方面的信息,甚至有图形化来表示类层次关系与函数调用关系。还有传说中 +的唯一的中文编程语言“易语言”还支持图文框拖拖拽拽就能编写代码的东东……而 vim 这 +种古董,只有编辑纯文本,似乎就该归属于“不可视”。 + +然而,让我们回顾洪荒时代,体验一下什么叫真正的“不可视”编辑。 + +在 Vi 都还没诞生的时代,有一个叫 `ed` 的行编辑器,它只能通过命令以行为单位去操 +作或编辑文本文件。它完全没有界面,无从知道当前编辑的是哪个文件,在哪行,当前行 +是什么内容,用户只能“记住”,或用命令查询。比如用 `p` 命令打印显示当前行(不过 +也可以在前面带上行地址打印多行,至今 `vim` 的大部分命令都可以带地址参数)。要 +编辑当前行,请用 `a` `i` 或 `c` 命令(vimer 有点眼熟吧),不过编辑完后也无从知 +晓结果是否正确,可能还需要再用 `p` 命令打印查看确证。 + +之后,有个 `ex` 编辑器,不过是对 `ed` 的命令进行了扩展,本质上仍是行编辑器。直 +到 `vi` 横空出世,那才叫“屏幕编辑器”。意思是可以全屏显示文件的许多行,移动光标 +实时修改某一可见行,修改结果即时显示……这才像我们现在可认知的编辑器了。 + +然后是 `vim` 对 `vi` 的扩展增强。事实上,`vim` 还不如 `vi` 的划时代意义,它的 +增强与此前 `ex` 对 `ed` 的增强是差不多的程度,基本上是平行扩展。 + +可视化程度不是越高越好。 `vim` 与 `vi` 都保留与继承了 `ex` 的命令,因为 `ex` +命令确实高效,当你能确知一个命令的运行结果,就没必要关注中间过程了。比如最平凡 +无奇但常用的 `ex` 命令就是 `:s` 全局替换命令。 + +VimL 语言就是基于 `ex` 命令的,再加上一些流程控制,就成了一种完整的脚本语言。 +如果说 `vim` 对 `vi` 有什么压倒性的里程碑意义,我觉得应是丰富完善了 VimL 语言 +,使得 `vim` 有了无穷的扩展与定制。利用 VimL 写的插件,既可以走增加可视化的方 +向,也可以不增加可视化而偏向自动化。依每人的性格习惯不同,可能会在 Vim 的可视 +化与自动化之间找到适合自己的不同的平衡点。 + + diff --git a/content/ch01-viml-feature/sn2-from-ex-command.md b/content/ch01-viml-feature/sn2-from-ex-command.md new file mode 100644 index 0000000..ecde35d --- /dev/null +++ b/content/ch01-viml-feature/sn2-from-ex-command.md @@ -0,0 +1,271 @@ ++++ +title = "1.2 同源 ex 命令行" +weight = 2 ++++ + + + +那么,VimL 到底是种什么样的语言。这里先说结论吧,VimL 就是富有程序流程控制的 +`ex` 命令。用个式子来表示就是: +``` +VimL = ex 命令 + 流程控制 +``` + +VimL 源于 ex ,基于 ex,即使它后来加了很多功能,也始终兼容 ex 命令。 + +然则什么是 ex 命令,这不好准确定义,形象地说,就是可以在 Vim 底部命令行输入并 +执行的语句。什么是流程控制,这也不好定义呢,类比地说,就是像其他大多语言支持的 +选择、循环分支,还有函数,因为函数调用也是种流程跳转。 + +下面,还是用些例子来阐述。 + +### 第一个脚本:vimrc + +为了说明 `vimrc` 先假设你没有 `vimrc` 。这可以通过以下参数启动 vim: +```bash +$ cd ~/.vim/vimllearn/ +$ vim -u NONE +``` +这样启动的 vim 不会加载任何配置文件,可以假装自己是个只会用裸装 vim 的萌新。同 +时也保证以下示例中所遇命令没有被重映射,始终能产生相同的结果。 + +Vim 主要功能是要用来编辑一些东西的,所以我们需要一些语料文本。这也可以用 vim +的普通命令生成,请在普通模式下依次输入以下按键(命令): +```vim +20aHello World! +yy +99p +: w helloworld.txt +``` +其中输入的按键不包括换行符,上面分几行显示,只为方便分清楚几个步骤的命令。 + +首先是个 `a` 命令,进入插入模式,输入字符串“Hello World!”,然后按 `` 键 +返回普通模式(这里``表示那个众所周知的特殊键,不是五个字符啦)。`a` 之前 +的 `20` 是还在普通模式下输入的数字参数,(它不会显示在光标所在的当前行,而是临时 +显示在右下角,然而一般不必关注)这表示后来的命令重复执行多少次。所以结果是在当 +前行插入了 20 个 “Hello World!”,也就是新文件的第一行。 + +接着命令 `yy` 是复制当前行,`99p` 是再粘贴 99 行。于是总共得到 100 行 “Hello +World!” ——满屏尽是 Hello World!,应该相当壮观。 + +最后的命令是用冒号 `:` 进入 ex 命令行,保存文件,`` 表示回车,ex 命令需要 +回车确认执行。 + +现在,我们已经在用 vim 编辑一个名为 `helloworld.txt` 的文件了。看着有点素是不 +是?可以用下面的 ex 命令设置行号选项: +```vim +: set number +``` +如此就会在文本窗口左则增加几列特殊列,为文件中的每行编号,确认一下是不是恰好 +100 行,用 `G` 普通命令翻到最后一行。 + +还有,是不是觉得每一行太长了,超过了窗口右边界。(如果你用的是超大显示屏,Vim +的窗口足够大还没超过,那么在一开始的示例中,把数字参数 `20` 调大吧)如果想让 +vim 折行显示,则用如下命令设置选项: +```vim +: set wrap +``` +可以看到,长行都折行显示了,但是行编号并没有改变。也就是说文件中仍是只有 100 +行,只有太长的行,vim 自动分几行显示在屏幕窗口上了。 + +你可以继续输入些设置命令让 vim 的外观更好看些,或让其操作方式更贴心些。但是等 +等,这样通过冒号一行行输入实在是太低效,应该把它保存到一个 vim 脚本文件中。 + +按冒号进入命令行后,再按 `` 将打开一个命令行窗口,里面记录着刚才输入的 +ex 历史命令。这个命令窗口的设计用意是为了便于重复执行一条历史命令,或在某条历 +史命令的基础上小修后再执行。不过现在我们要做的是将刚才输入的两条命令保存到一个 +文件中,比如就叫 `vimrc.vim`,整个按键序列是: +```vim +: +Vk +: '<, '> w vimrc.vim +: q +``` + +解释一下:进入命令窗口后光标自动在最后一行,`V` 表示进入行选择模式,`k` 上移一 +行,即选择了最后两行。在选择模式下按 `:` 进入命令行,会自动添加 `'<, '>`,这是 +特殊的行地址标记法,表示选区,然后用 `:w` 命令将这两行写入 `vimrc.vim` 文件( +注意当前目录应在 `~/.vim/vimllearn` 中)。最后的 `:q` 命令只是退出命令窗口,但 +vim 仍处于编辑 `helloworld.txt` 状态中。 + +你需要再输入一个 `:q` 退出 vim,然后用刚才保存的脚本当作启动配置文件重新打开文 +件,看看效果: +```vim +:q +$ vim -u vimrc.vim helloworld.txt +``` +可见,重新打开 `helloworld.txt` 文件后也自动设置了行号与折行。你可以换这个参 +数启动 vim 对比下效果,确认是 `vimrc.vim` 的功效: +```bash +$ vim -u NONE helloworld.txt +``` + +可以手动编辑 `vimrc.vim` 增加更多配置命令: +```vim +: e vimrc.vim +``` +这样就切换到编辑 'vimrc.vim' 状态了,里面已经有了两行,用普通命令 `Go` 在末尾 +打开新行进入插入模式,加入如下两行(还可自行添加一些注释): +```vim +: nnoremap j gj +: nnoremap k gk +``` + +按 `` 回普通模式再用 `:w` 保存文件。可以退出 vim 后重新用 +`$ vim -u vimrc.vim helloworld.txt` 参数启动 vim 打开文件观察效果。也可以在当 +前的 vim 环境中重新加载 `vimrc.vim` 令其生效: +```vim +: source % +: e # +``` +其中,`:e #` 或快捷键 `` 表示切换到最近编辑的另一个文件,这里就是 +`helloworld.txt` 文件啦,在这个文件上移动 `j` `k` 键,看看是否有什么不同体验了 +。 + +不过,表演到此为止吧。这段演示示例主要想说明几点: + +1. VimL 语言没什么神秘,把一些 ex 命令保存到文件中就是 vim 脚本了。 +2. vimrc 配置文件是 vim 启动时执行的第一个脚本,也应是大多数 Vim 初学者编写的 + 第一个实用脚本。 + +关于 vim “命令” 这个名词,还有一点要区分。普通模式下的按键也叫“命令”,可称之为 +“普通命令”,但由于普通模式是 Vim 的主模式,所以“普通命令”也往往简称为“命令”了 +。通过冒号开始输入而用回车结束输入的,叫 "ex 命令",vim 脚本文件不外是记录 +“ex 命令”集。(注:宏大多是记录普通命令) + +### 默认的 vimrc 位置 + +正常使用 vim 时不会带 `-u` 启动参数,它会从默认的位置去找配置文件。这可以在 +shell 中执行这个命令来查看: +```bash +$ vim --version +``` +或者在任一已启动的 vim 中用这个 ex 命令 `:version` 也是一样的输出。在中间一段 +应该有类似这样几行: +``` +系统 vimrc 文件: "$VIM/vimrc" +用户 vimrc 文件: "$HOME/.vimrc" +第二用户 vimrc 文件: "~/.vim/vimrc" +用户 exrc 文件: "$HOME/.exrc" +defaults file: "$VIMRUNTIME/defaults.vim" +``` + +它告诉了我们 vim 搜索 vimrc 的位置与顺序。主要是这两个地方,`~/.vimrc`, +`~/.vim/vimrc`。用户可用其中一个做为自定义配置,强烈建议用第二个 `~/.vim/vimrc`。 +因为配置可能渐渐变得很复杂,将所有配置放在一个目录下管理会更方便。不过有些低版 +本的 vim 可能不支持 `~/.vim/vimrc` 配置文件,在 unix/linux 系统下可将其软链接 +为 `~/.vimrc` 即可。 + +需要注意的是,`vimrc` 是个特殊的脚本,习惯上没有 `.vim` 后缀。 + +如何配置 vimrc 属于使用 Vim 的知识(或经验)范畴,不是本 VimL 教程的重点。不过 +为了说明 VimL 的特点,也给出一个简单的示例框架如下: + +```vim +" File: ~/.vim/vimrc + +let $VIMHOME = $HOME . '/.vim' +if has('win32') || has ('win64') + let $VIMHOME = $VIM . '/vimfiles' +endif + +source $VIMHOME/setting.vim +source $VIMHOME/remap.vim +source $VIMHOME/plug.vim + +if has('gui') + " source ... +endif + +finish +let $USER = 'vimer' +echo 'Hello ' . $USER '! Working on: ' . strftime("%Y-%m-%d %T") +``` + +一般地,一份 vimrc 配置包括选项设置,快捷键映射,插件加载等几部分,每部分都可 +能变得复杂起来,为方便管理,可以分别写在不同的 vim 脚本中,然后在主 vimrc 脚本 +中用 `:source` 命令调用。这就涉及脚本路径全名问题了,若期望能跨平台,就可创建 +一个变量,根据运行平台设置不同路径,这就用到了 `:if` 分支命令了。 + +最后两行打印个欢迎词。你可以将自己的大名赋给变量 `$USER`。如果,你觉得这很傻很 +天真,可以移到 finish 之后就不生效了。 + +在 vimrc 中,选择分支可能很常见,根据不同环境加载合适的配置嘛。但循环就很少见 +了。因为 vim 向来还有个追求是小巧,启动快,那么你在启动脚本中写个循环是几个意 +思啊,万一写个死循环BUG还启不起来了。 + +### 流程控制语句也是 ex 命令 + +在 VimL 中,每一行都是 `ex 命令`。作为一门脚本语言,最常见的,创建变量要用 +`:let` 命令,调用函数要用 `:call` 命令。初学者最易犯与迷惑的错误,就是忘了 +`let` 或 'call',裸用变量,裸调函数,比如: +```vim +i = -1 +abs(-1) +``` +用过其他语言的可能会觉得这很自然,但在 VimL 中是个错误,因为它要求第一个词是钦 +定的 `ex 命令`!正确的写法是: +```vim +let i = -1 +call abs(-1) +``` + +从这个意义上讲,VimL 与 shell 脚本很类似的,把命令行语句保存到文件中就成了脚本 +。每一行都以可执行命令开始,后面的都算作该命令的参数。 + +在 VimL 中,`:if` `:for` `:while` 也当作扩展的 `ex 命令` 处理,在 vim 脚本中, +这些“关键词”前面,可以像 `:set` 一样加个可选的冒号。同时,也可以像其他 ex 命令 +一样在无歧义时任意简写。比如: + +* `:endif` 可简写为 `en` 或 `end` 或 `endi` 或 `endif` +* `:endfor` 可简写为 `endfo` +* `:endfunction` 可简写为 `endf`,后面补上 `unction` 任意前几个字符也可以。 + +这套缩写规则,就与替换命令 `:substitute` 简写为 `:s`,设置命令 `:set` 简写为 +`:se` 一样一样的。但是,在写脚本时,强烈建议都写命令全称。命令简写只为在命令行 +中快速输入,而在脚本中只要输入一次,一劳永逸,就应以可读性为重了。 + +当有了这个意识,VimL 的一些奇怪语法约定,也就显得容易理解多了。比如: + +* ex 命令以回车结束,所以 VimL 语句也按行结束,不要在末尾加分号,加了反而是语 + 法错误,在 Vim 中每个符号都往往有奇葩意义。 +* VimL 的续行符 `\` 写在下一行的开始,其他一些语言是把 `\` 写在上一行结束,表 + 示转义掉换行符,合为一行。但在 VimL 中,每一行都需要一个命令,你可以把 `\` + 想象为一个特殊命令,意思是“合并到上一行”。 +* 在 VimL 中,不推荐在一行写多个语句,要写也可以,把反斜杠 `\` 扶正为竖线 `|` + 表示语句分隔吧。这在 vim 下临时手输单行命令时可能较为常见,减少额外按回车与 + 冒号。在很多键盘布局中,`|`(与`\`)恰好在回车键上面。 + +### 关键命令列表 + +Vim 的 `ex 命令`集是个很大的集合,比绝大多数的语言的关键字都多一个数量级。幸运 +的是,我们写 VimL 语言的脚本,并不需要掌握或记住这所有的命令,只要记住一些主要 +的关键命令就可以完成大部分需求了。 + +我从 VimL 语言的角度,按常用度与重要度并结合功能性将那些主要的命令分类如下, +仅供参考: + +1. let call (unlet) +2. if for while function try (endif, endfor, end...) +3. break continue return finish +4. echo echomsg echoerr +5. execute normal source +6. set map command +7. augroup autocmd +8. wincmd tabnext +9. 其他着重于编辑功能的命令 + +其中,第 5 类恰好是个分界线,之上的是形成 VimL 语言的关键命令,之下是作为 Vim +编辑器的重要命令。没有后面的编辑器命令,纯 VimL 语言也可以写脚本,作为一种(没 +有什么优势的)通用脚本而已;只有利用后面的编辑器命令,才可以调控 Vim。 + +后面的编辑器命令也可以单独使用,所以 Vim 高手也未必一定需要会 VimL。不过有了 +VimL 语言命令的助力,那些编辑器命令可变得更高效与灵活。不过最后一大类纯编辑命 +令,可能较少出现在 VimL 语言脚本中。因为按 Vim 可视化编辑的理念,是需要使用者 +对这些编辑结果作出及时反馈的。同时,很多编辑命令也有相应的函数,在 VimL 中调用 +函数,可能更显得像脚本语言。所以,VimL 中不仅有“库函数”的概念,还有“库命令”呢 +。 + +总之,语句与命令,是联结 VimL 与 Vim 的重要纽带。这是 VimL 语言的重要特点,也 +是初学者的一大疑难点。尤其是对有其他语言编程经验的,可能还需要一定的思维转换过 +程吧。 diff --git a/content/ch01-viml-feature/sn3-week-type-strong-scope.md b/content/ch01-viml-feature/sn3-week-type-strong-scope.md new file mode 100644 index 0000000..954b28b --- /dev/null +++ b/content/ch01-viml-feature/sn3-week-type-strong-scope.md @@ -0,0 +1,202 @@ ++++ +title = "1.3 弱类型强作用域" +weight = 3 ++++ + + + +“弱类型”不是 VimL 的特点,是几乎所有脚本语言的特点。准确地说是变量无类型,但值 +有类型。创建变量时不必定义类型,直接赋值就行,也可以认为是变量临时获得了值的类 +型。关于 VimL 的变量与类型,将在下一章的基础语法中详解。 + +变量作用域是编程的另一个重要概念,也几乎每个语言都要管理的任务。这里说 VimL 具 +有“强作用域”的特点,是指它提供了一种简明的语法,让用户强调变量的作用域范围。 + +### VimL 语言级的作用域 g: l: s: a: + +变量作用域的意义是指该变量在什么范围内可见,可被操作(读值或赋值)。在 VimL 中 +,每个变量都可以加上一个冒号前缀,表示该变量的作用域。不过另有两条规则: + +1. 在一些上下文环境中,可以省略作用域前缀,等效于加上了默认的作用域前缀 +2. 有一些作用域前缀只在特定的上下文环境中使用。 + +从 VimL 语言角色看,主要有以下几种作用域: + +1. `g:` 全局作用域。全局变量就是在当前 vim 会话环境中,在任何脚本,任何 ex 命 + 令行中都可以引用的变量。所有在函数之外的命令语句,都默认是全局变量。 +2. `l:` 局部作用域。只可在当前执行的函数体内使用的变量,在函数体内的变量默认为 + 局部变量,`l:`局部变量也只能在函数体内使用。 +3. `s:` 脚本作用域。只有当前脚本内可引用的变量,包括该脚本的函数体内。 +4. `a:` 参数作用域。特指函数的参数,在函数体内,要引用传入的实参,就得加上 + `a:` 前缀,但定义函数时的形参,不能加 `a:` 前缀。`a:` 还隐含一个限定是只读 + 性,即不能在函数体内修改参数。 + +这几种作用域前缀所对应的英文单词,可认为是 `global`, `local`, `script` 与 +`argument`。不过 `s:` 也可认为是 `static`,因为在 C 语言中,`static` 也表示只 +在当前文件中有效的意思。 + +变量作用域的应用原则: + +1. 尽量少用全局变量,因为容易混乱使用,难于管理。不过在 ex 命令行或 Ex 模式下 + 只为临时测试的语句,为了方便省略前缀,是全局变量,当然在此命令中也只能是全 + 局变量。在写 vim 脚本文件时,若要使用全局变量,不要省略 `g:` 前缀。同时全局 + 变量名尽量取得特殊点,比如全是大写,或带个插件名的长变量名,以减少被冲突的 + 概率。 +2. 局部变量的前缀 `l:` 一般可省略。但我建议也始终加上,虽然多敲了两个字符,但 + 编程的效率来源于思路清晰,不在于少那几个字符。同时在 VimL 编程时,坚持习 + 惯了作用域前缀,就能在头脑中无形地加强这种意识,然后对作用域的把握也更加精 + 准。另外,显然地,在函数体内要引用全局就是必须加上 `g:` 前缀。 +3. 在写 vim 脚本时,函数外的代码,能用 `s:` 变量就尽量用 `s:` 变量。对于比较大 + 的脚本变量(如字典),想对外分享,也宁可先定义为 `s:` 变量,再定义一个全局 + 可访问的函数来返回这个脚本变量。 +4. 参数变量,`a:`是语法强制要求,漏写了 `a:` 往往是个错误,(如果它没报错,恰 + 好与同名局部变量冲突了,那是更糟糕与难以觉察的错误)也是初写 VimL 函数最容 + 易犯的语法错误。 + +### Vim 实体作用域 b: w: t: + +Vim 作为一个可视化的编辑器,给用户呈现的,能让用户交互地操作的实体对象主要有 +`buffer`(缓冲文件),`window`(窗口),`tabpage`(标签页)。可以把它们想象为 +互有关系的容器: + +* 缓冲对应着一个正在编辑中的文件,在不细究的情况下可认为与文件等同。(不过不一 + 定对应着硬盘上的一个文件,比如新建的尚未保存的文件,以及一些特殊缓冲文件)缓 + 冲也可认为是容纳着文件中所有文本行的容器,就像是简单的字符串列表了。 +* 窗口是用于展示缓冲文件的一部分在屏幕上的容器。Vim 可编辑的文件很大,极有可能 + 在一个屏幕窗口中无法显示文件的所有内容,所以窗口对应于缓冲文件还有个可视范 + 围。一个窗口在一个时刻只能容纳一个缓冲文件,但在不同时刻可以对应不同的缓冲文 + 件。 +* 标签页是可以同时容纳不同的窗口的另一层更大的容器。原始的 `Vi` 没有标签页,标 + 签页是 `Vim` 的扩展功能。标签页极大增强 `Vim` 的可视范围,可认为窗口是平面的 + ,再叠上标签页就是(伪)立体的了。 +* 一个缓冲文件可以展示在不同的窗口或(与)标签页中。所有已展示在某个窗口(包括 + 在其他标签页的窗口)的缓冲文件都是“已加载”状态,其他曾经被编辑过但当前不可见 + 的缓冲文件则是“未加载”状态,不过 Vim 仍然记录着所有这些缓冲文件的列表。 + +然后,Vim 还有个“当前位置”的概念。也就是光标所在的位置,决定了哪个是“当前缓冲 +文件”,“当前窗口”与“当前标签页”。 + +有了这些概念,对 VimL 中的另外三个作用域前缀 `b:` `w:` `t:` 就容易理解了。其意 +即指一个变量与特定的缓冲文件、窗口或标签页相关联的,以 `b:` 举例说明。 + +* `b:varname` 表示在当前缓冲文件(实体对象)中存在一个名为 "varname" 的变量。 +* VimL 语句在执行过程中,只能直接引用当前缓冲文件的 `b:` 变量,如果要引用其他 + 缓冲文件的变量,要么先用其他命令将目标缓冲文件切换为当前编辑的缓冲文件,或者 + 调用其他的内置函数来访问。 +* 如果一个缓冲文件“消失”了,那么与之关联的所有 `b:` 变量也消失了。 +* 窗口与标签页的“消失”能比较形象与容易地理解,关闭了就算消失了。但 Vim 内部对 + 缓冲的管理比较复杂,未必是从窗口上不见了就代表“消失”了。 +* 不过在一般 VimL 编程中,可暂不必深究缓冲文件什么时候“消失”。只要记着一个 + `b:` 变量必定与一个缓冲文件关联着,不同的缓冲文件使用相同的 `b:` 变量是安全的, + 它们互不影响。 + +### 作用域前缀其实是个字典 + +以上介绍的各种作用域前缀,不仅是种语法约定的标记,它们本身也是个变量,是可以容纳 +保存其他变量的字典类型变量。关于字典,在后续章节再详述。这里只能介绍几个演示示 +例来体会一下这种特性。 + +为了操作环境一致,也假设按上节的“裸装” Vim 启动:(不过其实不太影响,也不必太 +拘泥) +```bash +$ cd ~/.vim/vimllearn +$ vim -u NONE helloworld.txt +``` + +现在已用 vim 打开了一个 `helloworld.txt` 文件。在命令行输入以下 ex 命令: +```vim +: let x = 1 +: echo x +: echo g:x +: echo g:.x +: echo g:['x'] +: echo g: +``` +你可以每次按冒号进入命令行逐行输入,也可以先进入 `Ex` 模式,连续输入这几行,效 +果是一样的(以后不再注明)。 + +首先用 `:let` 命令定义了一个 `x` 变量,命令行语句默认是全局变量。然后用 +`:echo` 命令使用几种不同写法来引用读取这个变量的值,这几种写法都是等效的。最后 +将 `g:` 当作一个整体变量打印显示。它就是个全局字典,而里面包含了 `x` 键,值就 +是 `1`。(如果按正常的 vim 启动,你的 vimrc 以及各种插件可能会提供很多全局变量 +,那么 `echo g:` 的内容可能很多,不只 `x` 哟) + +然后我们再写个脚本观察下 `s:` 变量。`:e hello2.vim`,输入以下内容并保存: +```vim +" File: ~/.vim/vimllearn/hello2.vim +let s:hello = 1 +let s:world = 2 +let s:hello_world = s:hello + s:world +echo s: +``` + +脚本写完了,在 ex 命令行输入以下几条测试下: +```vim +: source % +: echo s: +: echo s:hello +``` + +可见,`: source %` 命令能正常执行,脚本内的 `:echo s:` 打印出了该脚本内定义的 +所有 `s:` 脚本变量。但在命令行直接试图访问 `s:` 变量则报错了。 + +在脚本中也可以访问全局变量。可以自行尝试在 `hello2.vim` 中加入对刚才在命令行定 +义的 `g:x` 变量的访问。不过在实际的编程中,可千万别在脚本中依赖在命令行建立的 +全局变量。 + +然后再测试下 `b:` 变量,直接在命令行执行以下语句吧: +```vim +: let b:x = 2 +: echo b:x +: echo x +: echo g:x +: e # +: echo b:x +: echo b: +: echo x +: e # +: echo b:x +: echo b: +: echo x +``` + +这里,`:e #` 表示切换编辑另一个文件。在实际工作中,或者用快捷键 `` 更 +方便,不过在本教程中,为说明方便,采用 ex 命令 `:e #` 。在本例中,vim 启动时 +打开的文件是 `helloworld.txt`,后来又编辑了 `hello2.vim`;此时用 `:e #` 命令就 +切回编辑 `helloworld.txt`了,再执行 `:e #` 就再次回到 `hello2.vim` 中,这是轮换 +的效果。 + +这个示例结果表明,在编辑 `hello2.vim` 时定义了一个 `b:x` 变量,这与全局的 `x` +变量是互不冲突的。但是在换到编辑 `helloworld.txt` 时,`b:x` 变量就不存在了,因 +为并未在该缓冲文件中定义 `b:x` 变量呀。重新回到编辑 `hello2.vim` 文件时,`b:x` +变量又能访问了。这也说明当缓冲文件“不可见”时,vim 内部管理它的对象实体其实并未 +“消失”呢。而全局变量 `g:x` 或 `x` 是始终能访问的。 + +最后要指出的是,局部作用域 `l:` 与参数作用域 `a:` 不能像 `s:` 或 `b:` 这样当作 +整体的字典变量,是两个例外。VimL 这样处理的原因,可能一是没必要,二是没效率。 +函数体内的局部作用域与参数作用域,太窄,没必要将局部变量另外保存一个字典;而且 +有效时间太短,函数在栈上反复重建销毁,额外维护一个字典没有明显好处就不浪费了。 +另外若要表示所有函数参数另有一个语法变量 `a:000` 可实现其功能。 + +### 其他特殊变量前缀 $ v: & + +这几个符号其实并不是作用域标记。不过既然也是变量前缀,也就一道说明一下,也好加 +以区分。 + +含 `$` 前缀的变量是环境变量。除了 vim 在启动时会从父进程(如 shell)自动继承一 +些环境变量,这些变量在使用上与全局变量没什么区别。不过要谨慎使用,一般建议只读 +,不要随便修改,没必要的话也不要随便创建多余的环境变量。(实际上环境变量与全局 +变量的最大区别是环境变量在 `ex` 命令中会自动展开为当前的具体值,比如可直接使用 +`:e $MYVIMRC` 编辑启动加载的 `vimrc` 文件。但在 VimL 脚本中将环境变量当作全局 +变量使用完全没问题) + +含 `v:` 前缀的变量是 vim 内部提供的预定义常量或变量。用户不能增删这类特殊变量 +,也不能修改其类型与含义。比如 `v:true` 与 `v:false` 分别用于表示逻辑值“真”与“ +假”。Vim 所支持的这类 `v:` 变量往往随着版本功能的增加而增加。从与时俱进的角度 +讲,vim 脚本中鼓励使用这类变量增加程序的可读性,但若想兼容低版本,还是考虑慎用 +。要检查当前 vim 版本是否支持某个 `v:` 变量,只要用 `:help` 命令查阅一下即可。 +而且 `v:` 本身也是个字典集合变量,可用 `:echo v:` 命令查看所有这类变量。 + +含 `&` 前缀的变量表示选项的值,相当于把选项变量化,以便于在 VimL 中编程。所支持 +的选项集,也是由 Vim 版本决定的,用户当然无法定义与使用不存在的选项。这部分内 +容在后面讲选项设置时再行讨论。 diff --git a/content/ch01-viml-feature/sn4-autoload-schema.md b/content/ch01-viml-feature/sn4-autoload-schema.md new file mode 100644 index 0000000..d9c0acf --- /dev/null +++ b/content/ch01-viml-feature/sn4-autoload-schema.md @@ -0,0 +1,87 @@ ++++ +title = "1.4 自动加载脚本机制" +weight = 4 ++++ + + + +前文已提及,vim 脚本主要用 `:source` 命令加载,然而很多情况下又不需要手动执行 +该命令。只要将脚本放在特定的目录下,vim 就有个机制能自动搜寻并加载。 + +### Vim 插件搜索目录 + +首先要知道有 `&runtimepath` (常简写为 `&rtp`)这个选项。它与系统的环境变量 +`$PATH` 有点类似,就是一组有序的目录名称,用于 Vim 在许多不同情况下搜寻 `*.vim` +脚本文件的。你可以在命令行输入 `:echo &rtp` 查看当前运行的 vim 有哪些“运行时目 +录”,一般都会包含 `~/.vim` 这个目录。 + +* 除了 vim 启动时的第一个配置文件 `vimrc`,运行时需要加载的脚本,一般都是从 + `&rtp` 目录列表中搜索的。 +* vim 启动时,会在所有 `&rtp` 目录下的 `plugin/` 搜索 `*.vim` 文件,并加载所有 + 找到的脚本文件。需要注意的是在 `plugin/` 子目录下的所有脚本也会自动加载。除 + 非你先在 vimrc 中用选项禁用加载插件这个行为。 +* 当一个文件类型 `&filetype` 被识别时,Vim 会从所有 `&rtp` 目录下的 `ftplugin/` + 子目录中搜索以文件类型开始的脚本文件,然后加载执行。比如编辑一个 `cpp` 文件 + 时,`ftplugin/` 目录下的 `cpp.vim` `cpp_*.vim` `cpp/*.vim` 都会被加载。 + +所以,我们自己写的脚本,如果想让它在 vim 启动时自动生效,就扔到 +`~/.vim/plugin/` 目录下,想只针对某种文件类型生效,就扔到 `~/.vim/ftplugin/` +目录下。 + +目前主流的第三方插件,也会遵循这种子目录规范,然后安装时一般会将整个目录添加到 +`&rpt` 中,以便让 Vim 找到对应的脚本。 + +### VimL 的自动加载函数(延时加载) + +Vim 一直有个追求的目标是启动快。当插件越来越多时,vim 启动时要解析大量的脚本文 +件,就会被拖慢了。这时就出现了一个 `autoload` 自动加载函数的机制,这个巧妙的方 +法可算是 VimL 发展的一个里程碑吧。而在这之前,须由用户在 `plugin/*.vim` 的复杂 +脚本中用极具巧妙的编程技巧,才好实现延时加载。 + +虽然还没有讲到 VimL 的函数,但也可以在这里解释自动加载函数的原理与过程,毕竟这 +不需要涉及到函数的具体实现。 + +例如,有一个 `~/.vim/autoload/foo.vim` 脚本(或在其他任一个 `&rtp` 目录下的 +`autoload/` 子目录也行),该脚本内定义一个函数 `foo#bar()`,其中 `#` 之前的部 +分必须与脚本文件名 `foo.vim` 相同。将有以下故事发生: + +* 在 vim 启动时,完全不会读取 `foo.vim` 文件,也不知道它里面可能定义了什么复杂 + 的脚本内容。 +* 当 `foo#bar()` 第一次被调用时,比如从命令行中执行 `:call foo#bar()`,vim 发 + 现 `foo#bar` 这个函数未定义,就会试图从这个函数名分析出它可能定义于 `foo.vim` + 文件中。然后就从 `&rtp` 目录列表中,依次寻找其中 `autoload/` 子目录的 + `foo.vim` 文件。将所找到的第一个 `foo.vim` 脚本加载,并停止继续寻找。如果在 + 所有 `&rtp` 目录下都找不到,那就是个错误了。 +* 加载(即 `:source`)完 `foo.vim` ,再次响应 `:call foo#bar()` 的函数调用,就 + 能正常执行了。 +* 如果 `foo.vim` 文件中其实并没有定义 `foo#bar()` 这个函数,比如手误把函数名写 + 错了,写成了 `foo#Bar()`,则 vim 在二次尝试执行 `:call foo#bar()` 时依然报错 + 说“函数未定义”。 +* 如果此后再次调用 `:call foo#bar()`,由于文件已加载,该函数是已定义的了,vim + 就不需要再次寻找 `foo.vim` 文件了,直接执行就是。 +* 如果 `foo.vim` 文件中还定义了一个 `foo#bar2()` 函数,由于之前是加载整个文件 + ,`foo#bar2()` 也是个已定义函数,也就可以直接调用到 `:call foo#bar2()`。 +* 如果尝试调用一个 `foo.vim` 文件中根本不存在函数,如 `:call foo#nobar()`。即 + 使之前已经加载过 `foo.vim` 一次,由于这个 `foo#nobar` 函数未定义,vim 会再次 + 从 `&rtp` 目录找到这个 `foo.vim` 文件再加载一次,然后再尝试 `:call + foo#nobar()` 依然出错报错。 + +各种细节过程可能很复杂,但总体思想还是很简单,就是延时加载,只有在必要时才额外 +加载脚本。从用户使用角度,只要注意几点: + +* 函数名 `foo#bar()` 必须与文件名 `foo.vim` 完全一致(大小写也最好一致)。如果 + 脚本是在 `autoload` 的深层子目录下,那函数名也必须是相对于 `autoload` 的路径 + 名,把路径分隔符 `/` 替换为 `#` 就是。即在 `autoload/path/to/foo.vim` 文件中 + 定义的函数名应该是 `path#to#foo#bar()`。 +* 从使用便利性上,一般是会定义快捷键或命令来调用 `#` 函数,并在首次使用时触发 + 相关脚本的加载。 +* `#` 函数是全局作用域的,也可以认为各层 `#` 是完整的命名空间,当然从任何地方 + 访问时都须使用路径全名,即使从相同的脚本内访问也须用全名。 +* 全局变量也可以用 `#` 命名,如 `g:path#to#foo#varname` 也能触发相应脚本文件的 + 自动(延时)加载,不过一般没有函数应用那么广泛。 +* 尽量将复杂业务逻辑代码写在 `#` 自动加载函数中,有时要注意不同 `&rtp` 目录下 + 同名文件的屏蔽效应。 + +利用 VimL 的这个自动加载机制,还有效地避免了全局变量(函数)名的冲突问题,因为 +函数名包含了路径名,而一般文件系统下是不会有重名文件的。唯一的问题是,这个函数 +名有点长。 diff --git a/content/ch02-viml-grammar/_index.md b/content/ch02-viml-grammar/_index.md new file mode 100644 index 0000000..211cc40 --- /dev/null +++ b/content/ch02-viml-grammar/_index.md @@ -0,0 +1,6 @@ ++++ +title = "第二章 VimL 语言基本语法" +weight = 2 +sort_by = "weight" ++++ + diff --git a/content/ch02-viml-grammar/sn1-variable-type.md b/content/ch02-viml-grammar/sn1-variable-type.md new file mode 100644 index 0000000..7688aff --- /dev/null +++ b/content/ch02-viml-grammar/sn1-variable-type.md @@ -0,0 +1,197 @@ ++++ +title = "2.1 变量与类型" +weight = 1 ++++ + + + +VimL 语言的变量规则与其他大多数语言一样,可以(只允许)由字母、数字与下划线组 +成,且不能以数字开头。特殊之处在于还可以在变量名之前添加可选的作用域前缀,如 +`g: l: s: b: w: t:`(`a:`又有点特殊,在定义函数参数时不要前缀,而在使用参数时 +需要前缀),这在第一章有专门讨论,此不再叙说。 + +VimL 所支持的变量(值)类型可由帮助(`:help type()`)查看。其中最主要最常用的 +有数字(number)、字符串(string)、列表(list)与字典(dictionary)四种,或者 +可以再进一步归纳为三种,因为前两种(数字与字符串)在绝大数情况下自动转换,在使 +用时几乎不必考虑其类型差别,只须知道它表示“一个值”,所以也称作标量。而列表与字 +典变量则是“多个值”的集合,所不同在于获取其中某个值的索引方式不同。 + +### 标量:数字与字符串 + +数字是(number)直译,其实就是其他大多数语言所称的整数(int)。数字是有符号的 +,包括正负数与 0 ,其取值范围与 Vim 的编译版本有关。经笔者的测试,vim8.0 支持 +8 字节的有符号整数,低版本只支持 4 字节的有符号整数。数字经常(或常规功能)是 +用于命令的地址参数表示行号,或命令的重复次数,一般情况下不必考虑数字溢界的问题 +。当你需要用到 VimL 来表达很大的整数时,才要小心这个潜在的问题。 + +字符串也简单,用单引号或双引号括起来即可,它们的语义是完全一致的。不过有以下使 +用建议原则: + +* 一般使用单引号表示字符串,如 `'string'`,毕竟双引号还可用于行末注释,尽量避 + 免混淆。 +* 如果需要使用转义如 `\n` `\t`,则使用双引号,单引号不支持转义。 +* 如果字符串包含一种引号,则使用另一种引号括起整个字符串。 +* 如果有包含一层引用,则内外层用不同的引号。 + +数字变量支持常见的数学运算(加减乘除模,`+-*/%`),字符串只支持连接运算符,用 +点号(`.`)表示。此外,一些内建函数与命令也会要求其参数是数字或字符串。也就是 +数字与字符串有不同的使用环境,VimL 便能依据上下文环境将数字或字符串进行自动转 +换,规则如下: + +* 数字转字符串表示是显而易见的,就是十进制数的10个数字字符表示法; +* 字符串转数字时,只截前面像数字的子串,若不以数字字符开头,则转为数字 0 。 + +请测试: +```vim +: echo 'sring' . 123 +: echo '123' +: echo '123' + 1 +: echo '123string' + 1 +: echo '1.23string' + 1 +: echo 'string123' + 1 +``` +需要特别注意的是字符串 `'1.23'` 只会自动转为字数 `1` ,而不是浮点数 `1.23`。 + +在 VimL 中输入数字常量时,也支持按二进制、八进制、十六进制的表示法,不过自动转 +字符串时只按十进制转换。请自行观察以下结果,如果不懂其他进制可无视。 +```vim +: echo 0xff +: echo 020 +: echo 0b10 +: echo 0b10 + 3 +: echo 'string' . 0xff +``` + +### 列表:有序集合 + +列表,在其他语言中也有的叫数组,就是许多值的有序集合。VimL 的列表有以下要点: + +* 创建列表语法, 中括号整体,逗号分隔元素: `:let list = [0, 1, 2, 3]` +* 用中括号加数字索引访问元素:`:echo list[1]` +* 索引从 0 开始,支持负索引,-1 表示最后一个元素,访问不存在索引时报错。 +* 索引也可以用整数变量。 +* 不限长度,在需要时会自动扩展容量。 + +在列表创建后,用内建函数 `add()` 与 `remove()` 动态增删元素,用 `len()` 函数取 +得列表长度(元素个数)。例如: +```vim +: let list = [0, 1, 2, 3] +: echo list +: call add(list, 4) +: call add(list, 5) +: echo list +: call remove(list, -1) | echo list +: call remove(list, 1) | echo list +``` + +### 字典:无序集合 + +字典,在其他语言中可能叫 Hash 表或散列表,就是许多“键-值”对的集合。与列表最大 +的不同在于,它不是用数字索引来访问其内的元素,而是用字符串索引(键)来访问元素 +。字典在内部存储方式是无序的,但通过键访问元素的速度极快。 + +定义与使用字典的语法示例如下: +```vim +: let dict = {'x': 1, 'y': 2, 'z': 3,} +: echo dict +: echo dict['x'] +: echo dict.y +: let var = 'z' +: echo dict[var] +: let dict['u'] = 4 +: let dict.v = 5 +: echo dict +``` + +语法要点: + +* 字典用大括号 `{}` 括号整体。 +* 每个键值对用逗号分隔,键与值用冒号分隔,键一般有引号表字符串。 +* 大括号内的空白是可选的,最后一个逗号也是可选的。 +* 访问字典内某个元素时,仍是中括号 `[]` 索引,键放在中括号中。 +* 在创建字典或访问元素时,键既可用引号引起的常量字符串,也可用字符串变量,数字 + 变量自动转换为字符串。 +* 当一个键是普通常量字符串(可用作变量名的字符串)时,可不用中括号加引号索引, + 而简洁地用点号索引,二者等价。 +* 不能访问不存在的键,否则报错。 +* 能直接对不存在的键赋值,表示对字典增加一个键值对元素。 + +### 删除变量 + +创建(或叫定义)变量用 `:let` 命令,相应的也就有 `:unlet` 命令用于删除一个变量 +。一般情况下没必要删除一个标量,因为它也占不了多少内存,需要重定义时也可以重新 +赋值。但对于列表与字典,有时比较在意其集合意义,可以用 `:unlet` 删除其中一个值 +,加上对应的索引即可,如果不加索引,则表示删除整个列表或字典。 + +例如:假设 `list` 与 'dict' 变量已如上定义: +```vim +: unlet list[1] | echo list +: unlet list[-1] | echo list +: unlet dict['u'] | echo dict +: unlet dict.v | echo dict +``` + +如果要删除的变量或字典(列表)不存在的索引,`:unlet` 会报错。如果想绕过该错误 +检测,则可用 `:unlet!` 命令。 + +在 vim8.0 版本之前,标量、列表、字典三者是不互通的。如果 `list` 已被定义成了一 +个列表变量,那么它就不能用 `:let` 重赋值为一个字典或字符串或其他什么,但允许重 +赋值为另一个列表变量。如果一定要改变 `list` 的变量类型,只能先 `:unlet` 它,再 +重新 `:let` 它为其他任意变量。 + +在 vim8.0 版本之后,不再有这个限制,不会再报诸如“类型不匹配”的错误了,更好地体 +现了动态弱类型的特点。然而,良好的命名规范要求变量名望文生义,在同一个范围的同 +一个变量名,前后用之于表达完全不同类型的变量,并不是个好习惯。 + +### 浮点数 + +虽然浮点数在 VimL 中用的比较少,但毕竟还是支持的。 + +* 浮点数也叫小数,支持科学记数法。 +* 数字(整数)可自动转为浮点数。 +* 浮点不能自动转为整数,也不能自动转为字符串。 +* 整数运算结果仍是整数,浮点数运算结果仍是浮点数。 +* 浮点数取整后仍是浮点数,不是整数。 + +请看以下示例: +```vim +: echo 1.23e3 +: let int = 123 | let float = 1.23 | let str = 'string' +: echo str . int +: echo str . float |"错误 +: echo str . 5 / 3 +: echo str . 5 % 3 +: echo str . 5 / 3.0 |"错误 +: echo str . 5 % 3.0 |"错误 +: echo round(5/3.0) +: echo round(5/3.0) == 2 +: echo round(5/3.0) . str |"错误 +``` + +最后一行语句说明,虽然一个浮点数取整后看似与一个整数相等,但它仍然不是整数,所 +以不能与字符串自动连接。 + +要将一个字符串“显式”转换为整数,可以与 `0` 相加;同理,要将整数“显式”转换为字 +符串,可与空串 `""` 相连接。VimL 还提供了另一个内建函数 `string()` 将任意其他 +类型转换为可打印字符串。于是,想将一个浮点数转换为“真正”的整数,可用如下操作: +```vim +: echo 0 + string(round(5/3.0)) +: echo type(round(5/3.0)) +: echo type(0 + string(round(5/3.0))) +``` + +注:行末注释可用一个双引号 `"` 开始,但建议用 `|"` 更有适用性。`|` 表示分隔语 +句,只是后面一个语句是只有注释的空语句。 + +### 类型判断 + +从 Vim8.0 开始,有一系列 vim 变量专门地用来表示各种变量(值)类型。比如 +`v:t_list` 表示列表类型。如果要判断一个变量是否为列表类型,可用以下三种写法中任 +何一种(但之前的低版本 Vim 只能用后两种): +```vim +if type(var) == v:t_list +if type(var) == 3 +if type(var) == type([]) +``` + +关于具有选择分支功能的 `:if` 语句,在下一节继续讲解。 diff --git a/content/ch02-viml-grammar/sn2-comapare-condition.md b/content/ch02-viml-grammar/sn2-comapare-condition.md new file mode 100644 index 0000000..4e7147c --- /dev/null +++ b/content/ch02-viml-grammar/sn2-comapare-condition.md @@ -0,0 +1,250 @@ ++++ +title = "2.2 选择与比较" +weight = 2 ++++ + + + +vim 在执行脚本时,一般是按顺序逐条语句执行的。但如果只能像流水帐地顺序执行,未 +免太无趣了,功能也弱爆了。本节介绍顺序结构之外的最普遍的选择分支结构,它可以根 +据某种条件有选择地执行或不执行语句块(一条或多条语句)。 + +在 VimL 中通过 `:if` 命令来表示条件选择,其基本语法结构是: +```vim +: if {expr} +: " todo +: endif +``` + +如果满足表达式 `{expr}` ,或说其值为“真”,则执行其后至 `:endif` 之间的语句。貌 +似突然迸进了许多新概念,得先理一理。 + +### 表达式与语句 + +什么叫表达式?这可难说了。我只能先描叙下在 VimL 中什么是与什么不是表达式: + +* 单独的变量就是表达式,常量也是表达式,选项值(`&option`)也是,但选项本身不是; +* 函数调用是表达式; +* 表达式有值,表达式之间的合法运算的结果也还是表达式。 +* 但表达式不是可执行语句,它只是语句的一部分。 + +至于语句,在第一章也讲过。VimL 语句就是 Vim 的 ex 命令行。笼统地说,有时说到 +`ex 命令` 是指整个命令行,不过狭义地说,是指它第一个单词所指代的关键命令。于是 +,VimL 语言的大部分语句,可认为遵循以下范式: +``` +VimL 语句 = ex 命令 + 表达式 +``` + +为什么说大部分呢?因为我们已经很熟悉的赋值语句如 `:let i=1` 就不完全适合。在这 +里,`:let` 是个命令,`1` 是个表达式。但 `=` 只是依附于 `:let` 命令的特殊语义符 +号,它不是个表达式,也不是个运算符。变量 `i` 在被创建之前,也还算不上表达式。 +而 `i=1` 写在一起,或为了增加可读性加些空白 `i = 1`,它也不是表达式,因为它没 +有值,(并不能像 C 语言那样使用连等号赋值),下面这两个语句是非法的: +```vim +: let i = j = 1 +: let i = (j = 1) +``` + +在 VimL 中的常用语句中,除了这个基础得有点平淡无奇的赋值语句,其他大多是 +`命令 + 表达式` 范式的。比如已经大量使用的 `:echo` 语句,以及上节介绍过的给列 +表添加一个元素的函数调用语句 `:call add(list, item)` 。 + +然而,其实也不必太拘泥于这些概念名词。理解就好。我们归纳出概念也不外是为了更好地 +理解。 + +### 逻辑值与条件表达式 + +`:if` 命令(以及下一节要介绍的 `:while`命令)后面的表达式,就是一个条件表达式 +。它期望这个表达式的值的类型是逻辑值,即 `type()` 的结果是 `v:t_bool(=6)` 的值 +。如果值的类型不是逻辑值,则会自动将其他值转换为一个逻辑值。逻辑类型只有唯二的 +两个值,`v:true` 表示真,`v:false` 表示假。 + +所以关键在于 VimL 如何判定其他值是否有真假,什么是真,什么是假?其转换规则如何? +这直接写代码测试一下吧: + +```vim +: if 1 | echo v:true | endif +: if 0 | echo v:true | endif +: if -1 | echo v:true | endif + +: if '0' | echo v:true | endif +: if '1' | echo v:true | endif +: if '' | echo v:true | endif +: if 'a' | echo v:true | endif + +: if 1.23 | echo v:true | endif |" 非法用法 +: if 0.23 | echo v:true | endif |" 非法用法 +: if '0.23' | echo v:true | endif +: if '1.23' | echo v:true | endif + +" 以下四条也都是非法用法 +: if [1, 2, 3] | echo v:true | endif +: if [] | echo v:true | endif +: if {'x':1, 'y':2} | echo v:true | endif +: if {} | echo v:true | endif +``` + +注:由于语句比较简单,就将 `:if` 与 `:endif` 直接写在一行了,用 `|` 分隔子语句 +。正常代码建议写在不同行上且缩进布局。 + +结果归纳于下: + +* 数字 `0` 为假,其他正数或负数为真; +* 字符串先自动转为数字,转为 `0` 的话认为假,能转为其他数字认为真; +* 浮点数不能转为逻辑值,无法判断真假; +* 列表与字典也不能直接判断真假。 + +其实可进一步归纳为一句话,在 VimL 中,只能对整数值判断真假,`0` 是假的,其他都 +是真的,字符串先自动转为数字再判断真假。其他类型的值不能直接判断真假。(至 +vim8.0 版本是此规则,后面是否会改就不得而知了) + +然而,我们还是经常需要判断其他类型的值的某种状态。这时可以利用一个内建函数 +`empty()` 来帮忙。它可以接收任何类型的一个参数,如果它是“空”的,就返回真( +`v:true`),否则返回假('v:false')。在很大程度上,它可以代替直接使用 `:if` 的 +条件表达式,只不过在值上恰好是逻辑取反;优点则是写法统一,适用于所有类型。 + +在上面这个例子中,可以都再次尝试把 `:if` 后面的表达式作为 `empty()` 的参数执 +行看看结果,或用 `!empty()` 取反判断,如: +```vim +: if empty('0.23') | echo v:true | endif +: if !empty('a') | echo v:true | endif +``` + +综合建议:用 `:if !empty({expr})` 代替 `:if {expr}`,避免逻辑烧脑,并且大部分 +情况下应该是你想要的。 + +### 比较运算符 + +两个整数进行相等性的比较,或大小性的比较,结果返回一个或真或假的逻辑值。整数支持 +的比较运算符包括:`==`, `!=`,`>`,`>=`,`<`,`<=`。 + +浮点数支持与整数相同的比较运算,但由于浮点误差,不建议用相等性判断。 + +字符串也支持与整数相同的那六个比较运算。虽然整数在直接 `:if` 命令中自动转为数 +字处理,但在比较运算中表现良好,就是按正常的编码序逐字符比较。不过有一点特别要 +注意的是,字符串比较结果受选项 `&ignorecase` 的影响,即有可能按忽略大小写的方 +式来比较字符串。比如,观察一下如下结果吧: +```vim +: set ignorecase +: echo 'abc' == 'ABC' +: set noignorecase +: echo 'abc' == 'ABC' +``` + +因此,为了使比较结果不受用户个人的 `vimrc` 配置 `&ignorecase` 的影响,VimL 另 +外提供两套限定大小写规则的比较运算符。在以上比较运算符之后再加 `#` 符号就表示 +强制按大小写敏感方式比较,后面加上 `?` 符号就表示强制按大小写不敏感的方式比较 +。比如: +```vim +: echo 'abc' ==# 'ABC' +: echo 'abc' ==? 'ABC' +``` + +所以,强烈建议在进行字符串比较时,只用 `==#` 或 `==?` 系的比较运算符。当然由于 +弱类型,字符串变量与数字变量其实是不可分的,所以将 `==#` 或 `==?` 之类的运用于 +数字上比较,也是完全没有关系的。 + +此外,字符串除了相等性比较,还有匹配性比较,即用 `=~` `!~` 运算符判断一个字符 +串是否匹配另一个作为正则表达式的字符串。正则表达式是另一个高级话题,这里不再展 +开。当然,匹配运算符也有限定大小写是否敏感的衍生运算符,而且一般建议用 `=~#` +与 `!~#` 匹配,毕竟正则表达本身有表达大小写的能力。 + +对于列表与字典变量,可进行相等性比较,但不能进行大小性比较。如果两个列表或字典 +的对应元素都相等,则认为它们相等。此外,列表与字典还另外有个同例性比较运算符, +`is` 或 `isnot`。注意,这两个是类似 `==` 的运算符号,不是关键词,虽然它们用英 +文单词来表示。同样地,也有 `is#` 与 `isnot?` 的衍生运算,不过这主要为了语法的 +统一整齐,其实 `is#` `is?` 与 `is` 的结果是一致的。同例性比较的具体含义涉及实 +例引用的概念,这留待后面的章节继续展开。 + +### 逻辑运算符 + +在 VimL 中的逻辑值所支持的或、且、非运算并无意外,分别用符号 `||` `&&` `!` 表 +示就是,而且也支持短路计算特性。 + +* 或 `expr1 || expr2 `, 只要 `expr1` 或 `expr2` 其中一个是真,整个表达式就是真 + ,两个都是假才是假。如果 `expr1` 已经是真的,`expr2` 不必计算就直接获得真的 + 结果。 +* 且 `expr1 && expr2`,只有两个表达式都是真,结果才是真。如果 `expr1` 是假,则 + 不必计算 `expr2` 就返回结果假。 +* 非 `!expr` ,对表达式真假取反。 + +### if 分支流程 + +在了解这些逻辑值判断之后,理解 `:if` 的选择分支语句就容易多了,其完整语法结构 +如下: + +```vim +: if {expr} +: {block_if} +: elseif {expr} +: {block_elseif} +: elseif {expr} +: {block_elseif} +: ...... +: else +: {block_else} +: endif +``` + +* 首先执行的是 `:if` 后面的 `{expr}` 表达式,它可能只是个简单表达式,也可能是 + 多个逻辑值的复合运算,或者是很多表达式运算后得到的一个数字结果或逻辑值。只要 + 它最终能被解释为真,就执行其后的 `{block_if}` 语句块。 +* 如果 `:if` 的表达式为假,则依次寻找下一个表达式为真的 `:elseif` 语句块。 +* 最后如果没有真的 `:if` 与 `:elseif` 条件满足,就执行 `:else` 语句块。 +* 只有 `:if` 与 `:endif` 关键命令是必须的,`:elseif` 与 `:else` 及其语句块是可 + 选的。 +* 在任一条件下,最多只有一个语句块被执行,然后流程跳转到 `:endif` 之后,结束整 + 个选择分支流程。 +* 如果没有 `:else` 语句块,则在没有任何一个条件满足时,就不会执行任何一个语句块 + 。在有 `:else` 时,则至少会执行一个语句块。 + +注意:`elseif` 是直接将 `else` 与 `if` 这两个单词拼在一起的,中间没有空格,也 +没有缩写。在许多不同的语言中,`else if` 的写法可能是变化最多的。 + +在 VimL 中,目前也没有 `switch case` 的类似语句,如果要实现多分支,只能叠加 +`:elseif`。 + +在非常简单的 `if else endif` 语句中,也可以用条件表达式 `expr1 : expr2 ? expr3`, +这类似于: +```vim +: if expr1 +: expr2 +: else +: expr3 +: endif +``` +整个表达式的值是 `expr2` 或 `expr3` 的值。至于条件表达式是否可以嵌套,这个我也 +不知道,反正我不用,也不建议用。就是条件表达式本身,也只推荐在一些有限的场合用 +,不推荐大量使用。因为一开始以为简单的逻辑判断,也可能以后会被修改的复杂起来, +仍然是用 `:if` 清晰一些。 + +然后推荐 `:if` 的一个特殊技法。VimL 并没有块注释,但是可以把多行语句嵌套放在 +`:if 0 ... :endif` 之间,然后其内的语句就完全不会被执行了,甚至有不合 VimL 语 +法的行也没事。然而仍然只建议这样“注释”合法的语句行,因为 `:if 0` 的潜意识是在 +某个时刻可能需要将其改为 `:if 1` 以重新激活语句。这主要是用于更方便地切换测试 +某块语句的运行效果。 + +```vim +: if 0 +: 这里被注释了 +: endif +: echo 'done' +``` + +### \*运算符优先级 + +本节为讲叙选择分支语句,也引申讲了不少有关语句、表达式、运算符的相关问题。落到 +实处就是各种运算符的使用了,这就需要特别注意运算符的优先级问题。在此并不打算罗 +列 VimL 的运算符优先级表,因为到这里可能还有些内容未覆盖到。而且运算符优先级的 +问题太过琐碎,只看一遍教程并无多大助益,需要经常查文档,并自行验证。可以通过这 +个命令 `:help expression-syntax` 查看表达式语法表,其中也基本是按运算符优先级 +从低到高排列的,请经常查阅。 + +虽然由于运算符优先级会引起一些自己意想不到的问题,但回避这类问题的办法也是很简 +单的,这里是一些建议: + +* 首先按自己的理解去使用运算符,要相信大部分语言的设计都是人性化的,不会故意设 + 些奇怪的违反常理的规则。 +* 对于自己不确定优先级,或者发现运算结果不符合自己所想时,添加小括号组合,使表 + 达式运算的次序明确化。 +* 拆分复杂表达式,借助中间变量,写成多行语句,不要写过长的语句。 diff --git a/content/ch02-viml-grammar/sn3-loop-iterate.md b/content/ch02-viml-grammar/sn3-loop-iterate.md new file mode 100644 index 0000000..46599b5 --- /dev/null +++ b/content/ch02-viml-grammar/sn3-loop-iterate.md @@ -0,0 +1,233 @@ ++++ +title = "2.3 循环与迭代" +weight = 3 ++++ + + + +程序比人体强大的另一个特性就是可以任劳任怨地重复地做些单调无聊(或有聊)的 +工作。本节介绍在 VimL 语言中,如何控制程序,命令其循环地按规则干活。 + +### 遍历集合变量 + +首先介绍的是如何依次访问列表如字典内的所有元素,毕竟在 2.1 节介绍的索引方法只 +适于偶尔访问查看某个具体的元素。这里要用到的是`for ... in` 语法。 + +例如遍历列表: +```vim +: let list = [0, 1, 2, 3, 4,] +: for item in list +: echo item +: endfor +``` +在这个例子中,变量 `item` 每次获取 `list` 列表中的一个元素,直到取完所有元素。 +相当于在循环中,依次执行 `:let item=list[0]` `:let item=list[1]` ... 等语句。 +这个变量也可以叫做“循环变量”。遍历列表保证是有序的。 + +对于字典的 `for ... in` 语法略有不同,因为在字典内的每个元素是个键值对,不仅仅 +是值而已。其用法如下: +```vim +: let dict = {'x':1, 'y':2, 'z':3, 'u':4, 'v':5, 'w':6,} +: for [key,val] in items(dict) +: echo key . '=>' . val +: endfor +``` +注意:字典内的元素是无序的。 + +可以单独遍历键,利用内建函数 `keys()` 从字典变量中构造出一个列表: +```vim +: for key in keys(dict) +: echo key . '=>' . dict[key] +: endfor +``` +这里的输出结果应该与上例完全一致。 + +遍历字典键时,如有需要,也可以先对键排个序: +```vim +: for key in sort(keys(dict)) +: echo key . '=>' . dict[key] +: endfor +``` + +遍历字典还有个只遍历值的方式,不过这种方式用途应该不多: +```vim +: for val in values(dict) +: echo val +: endfor +``` + +总之,对于 `:for var in list` 语句结构,`var` 变量每次获取列表 `list` 内的一个值。 +字典不是列表,所以要利用函数 `items()` `keys()` `values()` 等先从中构造出一个 +临时数组。 + +### 固定次数循环 + +如果要循环执行某个语句至某个固定次数,依然可利用 `for ... in` 语法。只不过要利 +用 `range()` 函数构造一个计次列表。例如,以下语句输出 `Hello World!` 5 次: +```vim +: for _ in range(5) +: echo 'Hello World!' +: endfor +``` +这里,我们用一个极简的合变量,单下划线 `_` 来作为循环变量,因为我们在循环体中 +根本用不着这个变量。不过这种用法并不常见,这里只说明可用 `range()` 实现计次循 +环。 + +那么,`range()` 函数到底产生了怎样的一个列表呢,这可用如下的示例来测试: +```vim +: for i in range(5) +: echo i +: endfor +``` +可见,`range(n)` 产出一个含 `n` 个元素的列表,元素内容即是数字从 `0` 开始直到 `n`, +但不包含 `n`,用数学术语就叫做左闭右开。 + +其实,`range()` 函数不仅可以接收一个参数,还可以接收额外参数,不同个数的参数使 +得其产出意义相当不一样,可用以下示例来理解一下: +```vim +: echo range(10) |" => [0, 10) +: echo range(1, 10) |" => [1, 10] +: echo range(1, 10, 2) |" => 从 1 开始步长为 2 的序列,不能超过 10 +: echo range(0, 10, 2) |" => 从 0 开始步长为 2 的序列,恰好能包含 10 +``` + +利用 `range()` 函数的这个性质,也就可以写出不同需求的计次 `for ... in` 循环。 + +注:VimL 没有类似 C 语言的三段式循环 `for(初化;条件;更新)`。只有这个 +`for ... in` 循环,在某些语言中也叫 `foreach` 循环。 + +### 不定次数循环 + +不定循环用 `:while` 语句实现,当条件满足时,一直循环,基本结构如: +```vim +: let i = 0 +: while i < 5 +: echo i +: let i += 1 +: endwhile +``` +用 `:while` 循环一个重要的注意点是须在循环前定义循环变量,并记得在循环体内更新 +循环变量。否则容易出现死循环,如果出现死循环,vim 没响应,一般可用 `Ctrl-C` 中 +断脚本或命令执行。 + +如果 `:while` 条件在一开始就不满足,则 `:while` 循环一次也不执行。在 `:for ... +in` 循环中,空列表也是允许的,那就也不执行循环体。 + +在某些情况下,死循环是设计需求,那就可用 `:while 1` 或 `:while v:true` 来实现 +,而 `for` 循环无法实现,因为构建一个无限大的列表是不现实的。 + +### 循环内控制 + +循环除了正常结束,还另外有两个命令改变循环的执行流程: + +* `:break` 结束整个循环,流程跳转到 `:endfor` 或 `:endwhile` 之后。 +* `:continue` 提前结束本次循环,开始下次循环,流程跳转到循环开始,对于 `:for` + 循环来说,循环变量将获取下一个值,对于 `:while` 循环来说,会再次执行条件判断 + 。 +* 这两个命令一般要结合 `:if` 条件语句使用,在特定条件下才改变流程,否则没有太 + 多实际意义。 + +举些例子: +```vim +: for i in range(10) +: if i >= 5 +: break +: endif +: echo i +: endfor +: echo 'done' +``` +这里只打印了前 5 个数,因为当 `i` 变量到达 5 时,直接 `break` 了。 + +```vim +: for i in range(10) +: if i % 2 +: continue +: endif +: echo i +: endfor +: echo 'done' +``` +在这里,`i % 2` 是求模运算,如果是奇数,余数为 1 ,`:if` 条件满足后由于 +`:continue` 直接开始下一次循环,`:echo i` 就被跳过,所以只会打印偶数。 + +在用 `:while` 循环时,要慎重用 `:continue`,例如以下示例: +```vim +: let i = 0 +: while i < 10 +: if i % 2 +: continue +: endif +: echo i +: let i += 1 +: endwhile +: echo 'done' +``` +这原意是将上个打印偶数的 `:for` 循环改为 `:while` 循环,但是好像陷入了死循环, +先 `` 中止再来分析原因。那原因就是 `:continue` 语句跳过了 `:let i+=1` +的循环变量更新语句,使它陷在同一个循环中再也出不来了。 + +所以,如果你的 `:while` 是需要更新循环变量的,而且还用了 `:continue`,最好将更 +新语句放在所有 `:continue` 之前。不过就这个例子而言,若作些修改后,还要同时修 +改一些判断逻辑,才能实现原有意图。 + +### \*循环变量作用域与生存期 + +对于 `:while` 循环,循环变量是在循环体之外定义的,它的作用域无可厚非应与循环结 +构本身同级。但对于 `:for` 循环,其循环变量是在循环头语句定义的,(可见 `:let` +并不是唯一定义或创建变量的命令,`:for`也可以呢),那么在整个 `:for` 结构结束之 +后,循环变量是否还存在,值是什么呢? + +```vim +: unlet! i +: for i in range(10) +: echo i +: endfor +: echo 'done: ' . i +``` +在这个例子中,为避免之前创建的变量 `i` 的影响,先调用 `:unlet` 删了它,然后执 +行一个循环,在循环结束查看这个变量的值。可见在循环结束后,循环变量仍然存在,且 +其值是 `:for` 列表中的最后一个元素。 + +那么空循环又会怎样呢? +```vim +: unlet! i +: for i in [] +: echo i +: endfor +: echo 'done: ' . i +``` +这个示例执行到最后会报错,提示变量不存在。所以循环变量 `i` 并未创建。因此准确 +地说,循环变量是在第一次进入循环时被赋值而创建的,而空循环就没能执行到这步。 + +再看一下示例: +```vim +: unlet! i +: for i in range(10) +: echo i +: unlet i +: endfor +: echo 'done: ' . i +``` +在这个例子中,只在循环体最后多加了一个语句,`:unlet i` 将循环变量删除了。这种 +写法在 Vim7 以前版本中很常见。因为列表中是可以保存不同类型的其他变量的,甚至包 +括另一个列表或字典。因此在后续循环中,循环变量将可能被重新赋与完全不同类型的值, +这在 Vim7 是一个“类型不匹配”的错误。所以在每次循环后将循环变量删除,能避免这个 +错误,使之适用性更广。在 Vim8 之后,这种情况不再视为错误,所以这个 `:unlet` 语 +句不是必要。只是在这里故意加回去,讨论一下循环变量作用域与生存期的问题。 + +运行这个示例,可见在循环打印了 10 个数字后,最后那条语句报错,变量 `i` 不存在 +。这也是可理解的,因为这个变量在每次循环中反复删除重建。在第 10 次循环结束后, +删除了 `i` ,但循环无法再进入第 11 次循环,也就 `i` 没有再重建,所以之后 `i` +就不存在了。 + +这里想说明的问题是,如果从安全性考虑,或对变量的作用域有洁癖的话,可以在循环体 +内 `:unlet` 删除循环变量。这样可避免循环变量在循环结束后的误用,尤其是循环中有 +`:break` 时,退出循环时那个循环变量的最后的值是很不直观的,你最好不要依赖它去 +做什么事情(除非是有意设计并考虑清楚了)。不过这有个显然的代价是反复删除重建变 +量会消耗一些性能(别说 VimL 反正慢就不注重性能了,性能都是相对的)。 + +### 小结 + +VimL 只有两种循环,`:for ... in` 与 `:while`。语义语法简单明了,没有其他太多变 +种需要记忆负担,掌握起来其实应该不难。 diff --git a/content/ch02-viml-grammar/sn4-function-call.md b/content/ch02-viml-grammar/sn4-function-call.md new file mode 100644 index 0000000..e85b005 --- /dev/null +++ b/content/ch02-viml-grammar/sn4-function-call.md @@ -0,0 +1,358 @@ ++++ +title = "2.4 函数定义与使用" +weight = 4 ++++ + + + +函数是可重复调用的一段程序单元。在用程序解决一个比较大的功能时,知道如何拆分多 +个小功能,尤其是多次用到的辅助小功能,并将它们独立为一个个函数,是编程的基本素 +养吧。 + +### VimL 函数语法 + +在 VimL 中定义函数的语法结构如下:(另参考 `:help :function`) +```vim +function[!] 函数名(参数列表) 附加属性 + 函数体 +endfunction +``` + +在其他地方调用函数时一般用 `:call` 命令,这能触发目标函数的函数体开始执行,以 +产生它所设计的功效。如果要接收函数的返回值,则不宜用 `:call` 命令,可用 `:echo` +观察函数的返回结果,或者用 `:let` 定义一个变量保存函数的返回结果。实际上,函数 +调用是一个表达式,任何需要表达式的地方,都可植入函数调用。例如: + +```vim +call 函数名(参数) +echo 函数名(参数) +let 返回值 = 函数名(参数) +``` + +注:这里为了阐述方便,除了关键命令,直接用中文名字描述了。因而不是有效代码,在 +每行的前面也就不加 `:` 了。 + +### 函数名 + +函数名的命令规则,除了要遵循普通变量的命令规则外,还有条特殊规定。如果函数是在 +全局作用域,则只能以大写字母开头。 + +因为 vim 内建的命令与函数都以小写字母开始,而且随着版本提升,增加新命令与函数 +也是司空见惯的事。所以为了方便避免用户自定义命令与函数的冲突,它规定了用户定义 +命令与函数时必须以大写字母开头。从可操作 Vim 的角度,函数与命令在很大程度上是 +有些相似功能的。当然,如果将 VimL 视为一种纯粹的脚本语言,那函数也可以做些与 +Vim 无关的事情。 + +习惯上,脚本中全局变量时会加 `g:` 前缀,但全局函数一般不加 `g:` 前缀。全局函数 +是期望用户可以直接从命令行用 `:call` 命令调用的,因而省略 `g:` 前缀是有意义的 +。当然更常见的是将函数调用再重映射为自定义命令或快捷键。 + +除了接口需要定义在全局作用域的函数外,其他一些辅助与实现函数更适合定义为脚本作 +用域的函数,即以 `s:` 前缀的函数,此时函数名不强制要求以大写字母开头。毕竟脚 +本作用域的函数,不可能与全局作用域的内建函数冲突了。 + +### 函数返回值 + +函数体内可以用 `:return` 返回一个值,如果没有 `:return` 语句,在函数结束后默认 +返回 `0`。请看以下示例: +```vim +: function! Foo() +: echo 'I am in Foo()' +: endfunction +: +: let ret = Foo() +: echo ret +``` + +你可以将这段代码保存在一个 `.vim` 脚本文件中,然后用 `:source` 加载执行它。如 +果你也正在用 vim 读该文档,可以用 `V` 选择所有代码行再按 `y` 复制,然后在命令 +行执行 `:@"`,这是 Vim 的寄存器用法,这里不准备展开详述。如果你在用其他工具读 +文档,原则上也可以将代码复制粘贴至 vim 的命令行中执行,但从外部程序复制内容至 +vim 有时会有点麻烦,可能还涉及你的 `vimrc` 配置。因此还是复制保存为 `.vim` 文 +件再 `:source` 比较通用。 + +这段示例代码执行后,会显示两行,第一行输出表示它进到了函数 `Foo()` 内执行了, +第二行输出表明它的默认返回值是 `0`。这个默认返回值的设定,可以想像为错误码,当 +函数正常结束时,返回 `0` 是很正常的事。 + +当然,根据函数的设计需求,可以显式地返回任何表达式或值。例如: +```vim +: function! Foo() +: return range(10) +: endfunction +: +: let ret = Foo() +: echo ret +``` +执行此例将打印出一个列表,这个列表是由函数 `Foo()` 生成并返回的。 + +注意一个细节,这里的 `:function!` 命令必须加 `!` 符号,因为它正在重定义原来存 +在的 `Foo()` 函数。如果没有 `!` ,vim 会阻止你重定义覆盖原有的函数,这也是一种 +保护机制吧。用户加上 `!` 后,就认为用户明白自己的行为就是期望重定义同名函数。 + +一般在写脚本时,在脚本内定义的函数,建议始终加上 `!` 强制符号。因为你在调试时 +可能经常要改一点代码后重新加载脚本,若没有 `!` 覆盖指令,则会出错。然后在脚本 +调试完毕后,函数定义已定稿的情况下,假使由于什么原因也重新加载了脚本,也不外是 +将函数重定义为与原来一样的函数而已,大部分情况下这不是问题。(最好是在正常使用 +脚本时,能避免脚本的重新加载,这需要一些技巧) + +不过这需要注意的是,避免不同脚本定义相同的全局函数名。 + +### 函数参数 + +在函数定义时可以在参数表中加入若干参数,然后在调用时也须使用相同数量的参数: + +```vmi +: function! Sum(x, y) +: return a:x + a:y +: endfunction + +: let x = 2 +: let y = 3 +: let ret = Sum(x, y) +: echo ret +``` +在本例中定义了一个简单的求和函数,接收两个参数;然后调用者也传入两个参数,运行 +结果毫无惊喜地得到了结果 `5` 。 + +这里必须要指出的是,在函数体内使用参数 `x` 时,必须加上参数作用域前缀 `a:`,即 +用 `a:x` 才是参数中的 `x` 形参变量。`a:x` 与函数之外的 `x` 变量(实则是 `g:x` +)毫无关系,如果在函数内也创建了个 `x` 变量(实则是 `l:x`),`a:x`与之也无关系 +,他们三者是互不冲突相扰的变量。 + +参数还有个特性,就是在函数体内是只读的,不能被重新赋值。其实由于函数传参是按值 +传递的。比如在上例中,调用 `Sum(x, y)` 时,是把 `g:x` 与 `g:y` 的值分别拷贝给 +参数 `a:x` 与 `a:y` ,你即使能对 `a:x` `a:y` 作修改,也不会影响外面的 `g:x` +`g:y`,函数调用结束后,这种修改毫无影响。然而,VimL 从语法上保证了参数不被修改 +,使形参始终保存着当前调用时实参的值,那是更加安全的做法。 + +为了更好地理解参数作用域,改写上面的代码如下: +```vmi +: function! Sum(x, y) +: let x = 'not used x' +: let y = 'not used y' +: +: echo 'g:x = ' . g:x +: echo 'l:x = ' . l:x +: echo 'a:x = ' . a:x +: echo 'x = ' . x +: +: let l:sum = a:x + a:y +: return l:sum +: endfunction + +: let x = 2 +: let y = 3 +: let ret = Sum(-2, -3) +: echo ret +``` +在这个例子中,调用函数 `Sum()` 时,不再传入全局作用域的 `x` `y` 了,另外传入两 +个常量,然后在函数体内查看各个作用域的 `x` 变量值。 + +结果表明,在函数体内,直接使用 `x` 代表的是 `l:x`,如果在函数内没定义局部变量 +`x`,则使用 `x` 是个错误,它也不会扩展到全局作用域去取 `g:x` 的值。如果要在函 +数内使用全局变量,必须指定 `g:` 前缀,同样要使用参数也必须使用 `a:` 前缀。 + +虽然在函数体内默认的变量作用域就是 `l:` ,但我还是建议在定义局部变量时显式地 +写上 `l:`,就如定义 `l:sum` 这般。虽然略显麻烦,但语义更清晰,更像 VimL 的风格 +。函数定义一般写在脚本文件,只用输入一次,多写两字符不多的。 + +至于脚本作用域变量,读者可自行将示例保存在文件中,然后也创建 `s:x` `s:y` 变量 +试试。当然了,在正常的编程脚本中,请不要故意在不同作用域创建同名变量,以避免不 +必要的麻烦。(除非在某些特定情境下,按设计意图有必要用同名变量,那也始终注意加 +上作用域前缀加以区分) + +### 函数属性:abort + +VimL 在定义函数时,在参数表括号之后,还可以选择指定几个属性。虽然在帮助文档 +`:help :function` 中也称之为 `argument`,不过这与在调用时要传入的参数是完全不 +同的东西。所以在这我称之为函数属性。文档中称之为 `argument` 是指它作为 +`:function` 这个 `ex 命令` 的参数,就像我们要定义的函数名、参数表也是这个命令 +的 “参数”。 + +至 Vim8.0 ,函数支持以下几个特殊属性: + +* `abort`,中断性,在函数体执行时,一旦发现错误,立即中断运行。 +* `range`,范围性,函数可隐式地接收两个行地址参数。 +* `dict`, 字典性,该函数必须通过字典键来调用。 +* `closure`,闭包性,内嵌函数可作为闭包。 + +其中后面两个函数属性涉及相对高深的话题,留待第五章的函数进阶继续讨论。这里先只 +讨论前两个属性。 + +为理解 `abort` 属性,我们先来看一下,vim 在执行命令时,遇到错误会怎么办? +```vim +: echomsg 'before error' +: echomsg error +: echomsg 'after error' +``` +在这个例子中,第二行是个错误,因为 `echo` 要求表达式参数,但 `error` 这个词是 +未定义变量。这里用 `echomsg` 代替 `echo` 是因为 `echomsg` 命令的输出会保存在 +vim 的消息区,此后可以用 `:message` 命令重新查看;而 `echo` 只是临时查看。 + +将这几行语句写入一个临时脚本,比较 `~/.vim/vimllearn/cmd.vim` ,然后用命令加载 +`:source ~/.vim/vimllearn/cmd.vim` 。结果表明,虽然第二行报错了,但第三行仍然 +执行了。 + +不过,如果在 vim 下查看该文档,将这几行复制到寄存器中,再用 `:@"` 运行,第三行 +语句就似乎不能被执行到了。然而这不是主流用法,可先不管这个差异。 + +然后,我们将错误语句放在一个函数中,看看怎样? + +```vim +: function! Foo() +: echomsg 'before error' +: echomsg error +: echomsg 'after error' +: endfunction +: +: echomsg 'before call Foo()' +: call Foo() +: echomsg 'after call Foo()' +``` +将这个示例保存在 `~/.vim/vimllearn/t_abort1.vim`,然后 `:source` 运行。结果错 +误之后的语句也都将继续执行。 + +在函数定义行末加上 `abort` 参数,改为: +```vim +: function! Foo() abort +``` +重新 `:source` 执行。结果表明,在函数体内错误之后的语句不再执行,但是调用这个 +出错函数之后的语句仍然执行。 + +现在你应该明白 `abort` 这个函数属性的意义了。一个良好的编程习惯是,始终在定义函数时 +加上这个属性。因为一个函数我们期望它执行一件相对完整独立的工作,如果中间出错了 +,为何还有必要继续执行下去。立即终止这个函数,一方面便于跟踪调试,另一方面避免 +在错误的状态下继续执行可能造成的数据损失。 + +那为什么 vim 的默认行为是容忍错误呢?想想你的 `vimrc` ,如果中间某行不慎出错了 +,如果直接终止运行脚本,那你的初始配置可能加载很不全了。Vim 在最初提供函数功能 +,可能也只是作为简单的命令包装重用,所以延续了这种默认行为。但是当 VimL 的函数 +功能可以写得越来越复杂时,为了安全性与调试,立即终止的 `abort` 行为就很有必要 +了。 + +如果你写的某个函数,确实有必要利用容忍错误这个默认特性,当然你可以选择不加 +`abort` 这个属性。不过最好还是重新想想你的函数设计,如果真有这需求,是否直接写 +在脚本中而不要写在函数中更合适些。 + +### \*函数属性:range + +函数的 `range` 属性,表明它很好地继承了 Vim 风格,因为很多命令之前都支持带行地 +址(或数字)参数的。不过 `range` 只影响一些特定功能的函数与函数使用方式,而在 +其他情况下,有没有 `range` 属性影响似乎都不大。 + +首先,只有在用 `:call Fun()` 调用函数时,在 `:call` 之前有行地址(也叫行范围) +参数时,`Fun()` 函数的 `range` 属性才有可能影响。 + +那么,什么又是行地址参数呢。举个例子,你在 Vim 普通模式下按 `V` 进入选择模式, +选了几行之后,按冒号 `:`,然后输入 `call Fun()`。你会发现,在选择模式下按冒号 +进入 ex 命令行时,vim 会自动在命令行加上 `'<,'>`。所以你实际将要运行的命令是 +`:'<,'>call Fun()`。`'<` 与 `'>` 是两个特殊的 `mark` 位置,分别表示最近选区的 +第一行与最后一行。你也可以手动输入地址参数,比如 `1,5call Fun()` 或 `1,$call +Fun()`,其中 `$` 是个特殊地址,表示最后一行,当前行用 `.` 表示,还支持 `+` 与 +`-` 表示相对当前行的相对地址。 + +总之,当用带行地址参数的 `:{range}call` 命令调用函数时,其含义是要在这些行范围 +内调用一个函数。如果该函数恰好指定了 `range` 属性,那么就会隐式地额外传两个参数 +给这个函数,`a:firstline` 表示第一行,`a:lastline` 表示最后一行。 + +比如若用 `:1,5call Fun()` 调用已指定 `range` 属性的函数 `Fun()` ,那么在 +`Fun()` 函数体内就能直接使用 `a:firstline` 与 `a:lastline` 这两个参数了,其值 +分别为 `1` 与 `5`。如果用 `:'<,'>call Fun()` 调用,vim 也会自动从标记中计算出 +实际数字地址来传给 `a:firstline` 与 `a:lastline` 参数。函数调用结束后,光标回 +到指定范围的第 1 行,也就是 `a:firstline` 那行。 + +如果用 `:1,5call Fun()` 调用时,`Fun()` 却没指定 `range` 属性时。那又该怎办, +`Fun()` 函数内没有 `a:firstline` 与 `a:lastline` 参数来接收地址啊?此时,vim +会采用另一种策略,在指定的行范围内的每一行调一次目标函数。按这个实例,vim 会调 +用 5 次 `Fun()` 函数,每次调用时分别将当前光标置于 1 至 5 行,如此在 `Fun()` +函数内就可直接操作 “当前行” 了。整个调用结束后,光标停留在范围内的最后一行。 + +函数的 `range` 属性的工作原理就是这样,然则它有什么用呢?如果函数在操作 vim 中 +的当前 buffer 是极有用的。举个例子: +```vim +" File: ~/.vim/vimllearn/frange.vim + +function! NumberLine() abort + let l:sLine = getline('.') + let l:sLine = line('.') . ' ' . l:sLine + call setline('.', l:sLine) +endfunction + +function! NumberLine2() abort range + for l:line in range(a:firstline, a:lastline) + let l:sLine = getline(l:line) + let l:sLine = l:line . ' ' . l:sLine + call setline(l:line, l:sLine) + endfor +endfunction + +finish + +测试行 +测试行 +测试行 +测试行 +测试行 +``` +在这个脚本中,定义了一个 `NumberLine()` 不带 `range` 属性的函数,与一个带 +`range` 属性的 `NumberLine2()` 函数。它们的功能差不多,就是给当前 buffer 内的 +行编号,类似 `set number` 效果,只不过把行号写在文本行之前。 + +这里用到的几个内建函数稍作解释下,`getline()` 与 `setline()` 分别表示获取与设定 +文本行,它们的第一个参数都是行号,当前行号用 `'.'`表示。 `line('.')` 也表示获取 +当前行号。 + +如果你正用 vim 编辑这个脚本,直接用 `:source %` 加载脚本,然后将光标移到 +`finish` 之后,选定几行,按冒号进入命令行,调用 `:'<,'>call NumberLine()` 或 +`:'<,'>call NumberLine2()` 看看效果。可用 `u` 撤销修改。然后可将光标移到其他地 +方,手动输入数字行号代替自动添加的 `'<,'>` 试试看。 + +最后,关于使用 `range` 属性的几点建议: + +* 如果函数实现的功能,不涉及读取或修改当前 buffer 的文本行,完全不用管 `range` + 属性。但在调用函数时,也请避免在 `:call` 之前加行地址参数,那样既无意义,还 + 导致重复调用函数,影响效率。 +* 如果函数功能就是要操作当前 buffer 的文本行,则根据自己的需求决定是否添加 + `range` 属性。有这属性时,函数只调用一次,效率高些,但要自己编码控制行号,略 + 复杂些。 +* 综合建议就是,如果你懂 `range` 就用,不懂就不用。 + +### \*函数命令 + +`:function` 命令不仅可用来(在脚本中)定义函数,也可以用来(在命令行中)查看函 +数,这个特性就如 `:command` `:map` 一样的设计。 + +* `:function` 不带参数,列出所有当前 vim 会话已定义的函数(包括参数)。 +* `:function {name}` 带一个函数名参数,必须是已定义的函数全名,则打印出该函数 + 的定义。由此可见,vim 似乎通过函数名保存了一份函数定义代码的拷贝。 +* `:function /{pattern}` 不需要全名,按正则表达式搜索函数,因为不带参数的 + `:function` 可能列出太多的函数,如此可用这个命令过滤一下,但是也只会打印函数 + 头,不包括函数体的实现代码,即使只匹配了一个函数。 +* `:function {name}()` 请不要在命令行中使用这种方式,在函数名之后再加小括号, + 因为这就是定义一个函数的语法! + +### \*函数定义 snip + +在实际写 vim 脚本中,函数应该是最常用的结构单元了。然后函数定义的细节还挺多, +`endfunction` 这词也有点长(脚本中不建议缩写)。如果你用过 `ultisnips` 或其他 +类似的 snip 插件,则可考虑将常用函数定义的写法归纳为一个 snip。 + +作为参考示例,我将 `fs` 定义为写 `s:函数` 的代码片断模板: + +```vim +snippet fs "script local function" b +" $1: +function! s:${1:function_name}(${2}) abort "{{{ + ${3:" code} +endfunction "}}} +endsnippet +``` + +关于 ultisnips 这插件的用法,请参考:https://github.com/SirVer/ultisnips + +### 小结 + +函数是构建复杂程序的基本单元,请一定要掌握。函数必须先定义,再调用,通过参数与 +返回值与调用者交互。本节只讲了 VimL 函数的基础部分,函数的进阶用法后面另有章节 +专门讨论。 diff --git a/content/ch02-viml-grammar/sn5-exception-error.md b/content/ch02-viml-grammar/sn5-exception-error.md new file mode 100644 index 0000000..91c8ac9 --- /dev/null +++ b/content/ch02-viml-grammar/sn5-exception-error.md @@ -0,0 +1,50 @@ ++++ +title = "2.5 异常处理" +weight = 5 ++++ + + + +异常是编程中相对高级的话题,也是比较有争议的话题。本教程旨在 VimL ,不可能 +展开去讨论异常机制。所以如果你不了解异常,也不用异常,那就可完全跳过这节了。 +如果你了解异常,并且不反对用异常,那么这里只是告诉你,VimL 也提供了语法支持, +可以让你在脚本中使用异常,其基本语法结构如下: + +```vim +try + 尝试语句块 +catch /正则1/ + 异常处理1 +catch /正则2/ + 异常处理2 +... +finally + 收尾语句块 +endtry +``` + +大致流程是这样的:先执行 `try` 下面的尝试语句块,如果这过程中不出现错误, +那就没 `catch` 什么事了,但是如果有 `finally`,其后的收尾语句块也会执行。麻烦 +在于如果尝试语句块中有错误发生,就会抛出一个错误。错误用字符串消息的形式,所以 +`catch` 用正则表达式捕获。由于错误消息可能有本地化翻译,所以匹配错误号比较通用 +。如果 `catch` 没有参数,则捕获所有错误。一旦错误被某个 `catch` 正确匹配了,就 +执行其后的异常处理语句块,然后如果有 `finally` 的话,收尾语句块也会执行。 + +如果在 `try` 中出现了错误,既没有 `catch` 捕获,也没有 `finally` 善后,那它就 +向上层继续抛出这个错误。直到有地方处理了这个错误,如果一直没能处理该错误,就终 +止脚本运行。 + +除了 vim 执行脚本中自动检测错误抛出外,也有个命令 `:throw` 可手动抛出。比较常 +见的在 `catch` 的异常处理块中,只处理了部分工作后,用 `:throw` 重新抛出错误让 +后续机制继续处理。`:throw` 不带参数时重新抛出最近相同的错误,否则可带上参数抛 +出指定错误。 + +虽然 VimL 也提供了这个一套完整的异常处理机制,但一般情况下用得不多。大约有以下 +原因: + +* 使用 VimL 希望简单,用上异常就似乎很复杂了。 +* vim 脚本本身就很安全,只能在 vim 环境下运行,似乎干不了什么坏事。而且 vim 早 + 就有备份相关的配置,对编辑保存的文件都可以备份的。 + +所以,除非要写个比较大与复杂的插件,用异常可能在代码组织上更为简洁,提供更良好 +的用户接口。 diff --git a/content/ch03-viml-command/_index.md b/content/ch03-viml-command/_index.md new file mode 100644 index 0000000..09c9ba6 --- /dev/null +++ b/content/ch03-viml-command/_index.md @@ -0,0 +1,29 @@ ++++ +title = "第三章 Vim 常用命令" +weight = 3 +sort_by = "weight" ++++ + +在第二章已经介绍了 VimL 语言的基本语法,理论上来说,就可以据此写出让 vim 解释 +执行的合法脚本了。然而,能写什么脚本呢?除了打印“Hello World!”,以及高级点的 +用循环计算诸如“1+2+...+100”这样人家好像也能心算的题目外,还能干嘛呢? + +所以,如果要让 vim 脚本真正有实用价值,还得掌握 vim 提供的内置命令,用以控制 +Vim 或定制 Vim。本章就来介绍一些主要的、常用用的命令。 + +Vim 是个极高自由度的文本编辑软件,它在以下几个层级上给用户提供了自由度: + +1. `option` 选项。预设了一个很庞大的选项集,用户可以按自己的喜好设置每个选项的 +值(当然很多选项也可以接受默认值而假装当它们不存在),这可能改变 Vim 的很多 +基础表现与行为。 +2. `map` (快捷键)映射。一个非常简单但非常强大的机制。用户可以根据自己的习惯 +来重新映射各种模式下不同按键(及按键序列)的解释意义。初入门的 Vimer 很容易 +沉迷于折腾各种快捷键。 +3. `command` 自定义命令。Vim 是基于 ex 命令的,然后允许又你自定义 Ex 命令。可 +见这是比简单映射更灵活强大的利器,当然它的使用要求也比映射要高一些。 +4. `VimL` 脚本。进一步将命令升级为脚本语言,据此开发插件,使得 Vim 的扩展性具 +有无限可能。在 Vim 社区已经涌现了很多优秀插件,大多可以直接拿来用。当自己掌 +握了 VimL 语言后,也就可以自己写些插件来满足自己的特殊需求或癖好。 + +本教程虽是旨在 VimL 脚本语言,但还是有必要从简单的选项说起吧。 + diff --git a/content/ch03-viml-command/sn1-option-set.md b/content/ch03-viml-command/sn1-option-set.md new file mode 100644 index 0000000..bdf9576 --- /dev/null +++ b/content/ch03-viml-command/sn1-option-set.md @@ -0,0 +1,187 @@ ++++ +title = "3.1 选项设置" +weight = 1 ++++ + + + +### 选项分类与设置命令 + +设置选项的命令是 `set`。根据选项值的不同情况,可以将选项分为以下三类: + +1. 不需要值的选项,或者说是 bool 型的开关切换状态的选项。这种选项有两个相对立 + 的选项名,分别用命令 `:set option` 表示开启选项,`:set nooption` 表示关闭选 + 项。例如 `:set number` 是设置显示行号,`:set nonumber` 是设置不显示行号。 +2. 选项有一个值。用命令 `:set option=value` 设定该类选项的值。选项值可以是数字 + 或字符串,但字符串的值也不能加引号,就按字面字符串理解。也就是说,`:set` 后 + 面的参数,不是 VimL 的表达式,与 `:let` 命令有根本的不同。这个命令更像是 + `shell` 设置变量的语法,`=` 前后也最好不要用空格。 +3. 选项允许有多个值,值之间用逗号分隔。设置命令形如 `:set option=val1,val2`。 + 此外还支持 `+=` 增量与 `-=` 减量语法,如 `:set option+=val3` 或 `:set + option-=val2`,表示在原来的“值集合”的基础上增加某个值或移除某个值。 + +### 选项值变量 + +在选项名前面加个 `&` 符号,就将一个选项变成了相应的选项值变量。例如,以下两条 +命令是等效的: +```vim +: set option=value +: let &option = value +``` + +与普通变量赋值一样,`=` 前后的空格是可选的,这里的空格只是一种编程习惯,为增加 +可读性。另外有以下几点要注意: + +1. 第一类选项,在用 `:set` 命令时不需要等号,但是用 `:let &` 命令时也要用等号将 + 其值赋为 1 或 0,分别表示开启选项与关闭选项。同时 `&` 只允许作用在没有 `no` + 前缀的选项之前。比如 `:let &nonumber = 1` 是非法的,只能用 `:let &number = 0` + 表示相同意图。 +2. 第二类选项,如果值是字符串,用 `:let &` 命令时要将值用引号括起来,也就像普 + 通变量赋值一样,要求等号后面是合法的表达式。 +3. 第三类选项,它的值也是一个由逗号分隔的(长)字符串,比如 `:echo &rtp`。并不 + 能由于这类选项支持多个值就将 VimL 的列表赋给它,不过很容易通过 `split()` 函 + 数从这类选项值中分隔出一个列表。 + +备注:选项设置 `:set` 应是历史渊源最早的命令之一吧。而 `:let` 是后来 VimL 语言 +发展丰富起来提供的命令。两者有不一样的语法,所以又提供了这种等价转换方法。 + +### vimrc 配置全局选项 + +严格地说,`:set` 是设置全局选项的命令。既是影响全局的选项,一般是要第一时间在 +`vimrc` 中配置的。最重要的是以下两条配置: +```vim +: set nocompatible +: filetype plugin indent on +``` + +第一条配置是说不要兼容 `vi`,否则可能有很多 `vim` 的高级功能用不了。第二条配置 +(虽然不是 `set` 选项)是用 Vim 编写程序源代码必要的,意思是自动检测文件类型, +加载插件,自动缩进的意思。除非在很老旧的机器上,或为了研究需要,一般都没理由不 +加上这两条至关重要的配置。 + +下面再介绍一些比较重要的几类配置选项,当然这远远不够全面。查看选项的帮助命令是 +`:help options`,查看某一个选项的帮助是在用单引号括起选项名作为帮助参数,例如 +`:help 'option'`。查看某个选项的当前值是命令 `:set option?` 或 `:echo &option` +。 + ++ 编码相关:encoding fileencodings fileencoding + - encoding 是 Vim 内部使用的编码。建议 `:set encoding=utf-8`。 + - fileencodings 是打开文件时,Vim 用于猜测检测文件编码的一个编码列表。对中文 + 用户,建议 `:set fileencodings=ucs-bom,utf-8,gb18030,cp936,latin1`。 + - fileencoding (局部选项),当前文件的编码,如果与 encoding 不同,在写入时 + 自动转码。用户一般不必手动设这个选项,除非你想用另外一种编码保存文件。 + ++ 外观相关:number/relativenumber wrap statusline/tabline + - number 是在窗口左侧加一列区域显示行号,relativenumber 显示相对行号,即相对 + 光标所在的行的行号,当前行是 0,上面的行是负数,下面的行是正数。 + - wrap 是指很长的文本行折行显示。一般良好风格的程序源文件不应出现长行,但 + Vim 作为通用文件编辑器,不一定只用于编辑程序。 + - statusline 是定制状态栏,格式比较复杂,建议查看文档,也有些插件提供了很炫 + 酷的状态栏。tabline 的定制格式与状态栏一样,在开多个标签页时才生效。 + - laststatus 什么时候显示状态栏,建议用值 `2` 表示始终显示状态栏。 + - cmdheight 命令行的高度,默认只有 1 行太少,当命令行有输出时可能经常要多按 + 一个回车才回到普通模式。建议 2 行,更多就浪费空间了。 + - wildmenu 这是在编辑命令行时,按补全键后,会临时在状态栏位置显示补全提示。 + ++ GUI外观:只在 gVim 或有 GUI 版本的 Vim 有效 + - guioptions 设置 GUI 各部件(菜单工具栏滚动条等)是否显示。 + - clipboard 设置剪切板与 Vim 的哪个寄存器关联。 + ++ 颜色主题: + - colorscheme 这是个单独的命令,不是 `set` 选项。选择一个颜色主题。颜色主题 + 是放在运行时各路径的 `colors/` 子目录的 `*.vim` 文件。 + - background 背景是深色 `dark` 或浅色 `light`。有的 colorscheme 只适于深色或 + 浅色背景,有的则分别为不同背景色定义不同的颜色主题。 + - term 与 t\_Co 有的颜色主题可能还与终端与终端色数量有关。 + - cursorline 与 cursorcolumn 用不同格式高亮当前行与当前列,具体高亮格式由颜色 + 主题定义。个人建议只高亮 cursorline 。 + - hlsearch 高亮搜索结果。 + ++ 格式控制: + - formatoptions 控制自动格式化文本的许多选项,建议看文档。 + - textwidth 文本行宽度,超过该宽度(默认78)时自动加回车换回。在编辑程序源文 + 件时可用上个 formatoptions 选项控制只在注释中自动换行但代码行不自动换行。 + - autoindent smartindent 插入模式下回车自动缩进。 + - shiftwidth 缩进宽度。 + - tabstop softtabstop 制表符宽度,软制表符是行首按制符缩进的宽度。一般建议 + 硬制表符宽度 tabstop 保持 8 不变,用 shiftwidth softtabstop 表示缩进。 + - expandtab 插入制表符自动转为合适数量的空格。 + - paste 将 Vim 的插入模式置于“粘贴”模式,从外部复制文本进 Vim 开启该选项可避 + 免一些副作用。但只建议临时开启该选项。 + ++ 路径相关: + - runtimepath 运行时路径,简称(rtp)。vim 在运行时搜索脚本的一组路径。一般 + 不手动设置该值,如果有插件管理器管理插件的话。插件必须放在某个 `&rtp` 路径 + 下,现在流行的是将插件工程主目录添加至 Vim 的 `&rtp` 中。 + - packpath (Vim8开始才支持)动态加载插件的搜索路径,默认是 `~/.vim/pack`。 + 插件主目录可置于 `{packpath}/{packname}/opt/{plugin}`。然后用 `:packadd` + 启用插件。 + - path 这是 vim 在寻找编辑文件,如 `gf` `:find` 等命令时所要搜索的一组目录。 + - tags 这是 vim 按标签跳转 `Ctrl-]` 或 `:tag` 等命令所依据的标签文件,默认是 + `./tags,tags`(相对路径)。一般不建议修改默认值,但可以默认值基础上添加更 + 多的标签文件,比如编辑一个工程时,将工程主目录下的 tags 文件也加进来。 + - autochdir 将当前路径自动切换到当前编辑的文件所在的目录。当你依赖一些管理工 + 程类的插件时,可能要求当前路径锁定在工程主目录,不宜开启该选项。但是个人喜 + 欢开启这选项,这样在用 `:e` 命令打开同目录下的其他文件时很方便。 + +Vim 所支持的选项实在是太多了。初学者建议参考前人经验成熟的配置,用 `:help` 查 +看每个选项的具体含义,然后决定这种选项是否适合自己。另外注意有些选项可能要配合 +起来才能发挥更好的效果。 + +### VimL 控制局部选项 + +局部选项是只影响当前缓冲文件或窗口(buffer/window)的选项。严格来说是局部选项 +值,而不是有另外一类选项。默认情况下每个新文件或窗口都继承选项的全局值,但对于 +一些选项,可以为该文件或窗口设定一个不同与全局的局部值。然而并不是所有选项都有 +局部值意义,在每个选项的帮助文档中,会指明该选项是全局(global)或局部的( +local to buffer 或 local to window)。 + +设置局部选项(值)用 `:setlocal` 命令。如果目标选项没有局部值,则等效 `:set` +命令设置全局值。但是最好不要混用,避免误解。局部选项值变量用 `&l:option` 表示 +。比如 `number` 行号就是个局部选项: + +```vim +: set nonumber +: setlocal number +: echo &number +: echo &l:number +: echo &g:number +``` + +你可以将 vim 分裂出两个窗口(`:split` 或 `:vsplit`),在其中一个窗口上执行以上 +语句,试试看结果。需要注意的是,虽然局部选项值借用了变量的局部作用域前缀 `l:` +,但它的默认规则又有点不同。看这里的 `&number` 是默认的 `&l:number` 而不是 +`&g:number`。事实上,普通的局部变量 `l:var` 根本不能在函数外的命令行使用。 + +当用 VimL 写脚本时,如果要改变选项设置,且该选项支持局部值,最好用 `:setlocal` +只改变局部值。这也是编程的一大原则,尽量将影响局部化。下面介绍一些比较重要的局 +部选项设置: + ++ 文件类型:filetype + - 大部分情况下,这个选项不用手动设置,也不用脚本显式设置,打开自动检测就可以自 + 动根据后缀名设置相应的文件类型。不过在创建新的文件类型时,可能需要自己设置 + 这个选项。 + - 文件类型插件,如 `~/.vim/ftplugin/*.vim` 脚本内若涉及选项更改,也尽量用 + `:setlocal` 只设局部选项。 ++ 缓冲类型:buftype + - "buffer type" 与 "file type" 是两个不同的概念。缓冲类型更加抽象,是 vim 内 + 部用于管理缓冲的一些控制属性,而文件类型是着眼于文件内容性质的。 + - `buftype` 的两个重要的选项值是 `nofile` 与 `nowrite`,表示特殊的不用写文件 + 的 buffer,而这两者又还有细微差别,具体请读文档。 ++ 其他 buffer 属性: + - buflisted 是否将当前缓冲记录在缓冲列表中。 + - bufhidden 当缓冲不再任一窗口展示,如何处理该缓冲,有几种不同的选项值。 + - modifiable 当前缓冲是否可修改,包括更改编码与换行符格式也算种修改。 + +由于在 Vim 中,最主要的可见(可编辑)对象就只是 buffer,所以在一些复杂而细致的 +插件中,经常会开辟一个辅助窗口,仅为展示辅助内容,这就往往要设置一个特殊的 +`buftype` 及其他一些 buffer 属性。 + +此外,在脚本中,可能有需求只临时改变某个选项值,处理完毕后再恢复原选项设置,这 +就要借且选项值变量了。处理流程大致如下: +```vim +: let l:save_option = &l:option +: let &l:option = ? |" 或者 setlocal option = ? +: " do something +: let &l:option = l:save_option +``` diff --git a/content/ch03-viml-command/sn2-key-remap.md b/content/ch03-viml-command/sn2-key-remap.md new file mode 100644 index 0000000..641fa92 --- /dev/null +++ b/content/ch03-viml-command/sn2-key-remap.md @@ -0,0 +1,648 @@ ++++ +title = "3.2 快捷键重映射" +weight = 2 ++++ + + + +几乎每个初窥门径的 vimer 都曾为它的键映射欣喜若狂吧,因为它定制起来实在是太简 +洁了,却又似能搞出无尽的花样。 + +快捷键,或称映射,在 Vim 文档中的术语叫 "map",它的基本用法如下: +```vim +map {lhs} {rhs} +map 快捷键 相当于按下的键序列 +``` + +其中快捷键 `{lhs}` 不一定是单键,也可能是一个(较短的)按键序列,然后 vim 将其 +解释为另一个(可能较长较复杂的)的按键序列 `{rhs}`。为方便叙述,我们将 `{lhs}` +称为“左参数”,而将 `{rhs}` 称为“右参数”。左参数是源序列,也可叫被映射键,右参 +数是目标序列,也可叫映射键。 + +例如,在 vim 的默认解释下,普通模式下大写的 `Y` 与两个小写的 `yy` 是完全相同的 +功能,就是复制当前行。如果你觉得这浪费了快捷键资源,可将 `Y` 重定义为复制当前 +行从当前光标列到列尾的部分,用下面这个映射命令就能实现: +```vim +: map Y y$ +``` + +然而,映射虽然初看起来简单,其中涉及的门道还是很曲折的。让我们先回顾一下 Vim +的模式。 + +### Vim 的主要模式 + +模式是 Vim 与其他大多数编辑器的一个显著区别。在不同的模式下,vim 对用户按键的 +响应意义有根本的差别。Vim 支持很多种模式,但最主要的模式是以下几种: + +* 普通模式,这是 Vim 的默认模式,在其他大多模式下按 `` 键都将回到普通模式 + 。在该模式下按键被解释为普通命令,用以完成快速移动、查找、复制粘贴等操作。 +* 插入模式,类似其他“正常”编辑的模式,键盘上的字母、数字、标点等可见符号当作直 + 接的字符插入到当前缓冲文件中。从普通模式进入插件模式的命令有:`aAiIoO` + - `a` 在当前光标后面开始插入, + - `i` 在当前光标之前开始插入, + - `A` 在当前行末尾开始插入, + - `I` 在当前行行首开始插入, + - `o` 在当前行下面打开新的一行开始插入, + - `o` 在当前行上面打开新的一行开始插入。 +* 可视模式(visual),非正式场合下也可称之为“选择”模式。在该模式下原来的移动命 + 令变成改变选区。选区文本往往有不同的高亮模式,使用户更清楚地看到后续命令将要 + 操作的目标文本区域。从普通模式下,有三个键分别进入三种不同的可视模式: + - `v` (小写 v)字符可视模式,可以按字符选择文本, + - `V` (大写 V)行可视模式,按行选择文本(jk有效,hl无效), + - `Ctrl-v` 列块可视模式,可选择不同行的相同一列或几列。 + (Vim 还另有一种 "select" 模式,与可视模式的选择意义不同,按键输入直接覆盖替 + 换所选择的文本) +* 命令行模式。就是在普通模式时按冒号 `:` 进入的模式,此时 Vim 窗口最后一行将变 + 成可编辑输入的命令行(独立于当前所编辑的缓冲文件),按回车执行该命令行后回到 + 普通模式。 + 本教程所说的 VimL 语言其实不外也是可以在命令行中输入的语句。此外还有一种“Ex + 模式”,与命令行模式类似,不过在回车执行完后仍停留在该模式,可继续输入执行命 + 令,不必每次再输入冒号。在“Ex模式”下用 `:vi` 命令才回到普通模式。 + +大部分初、中级 Vim 用户只要掌握这四种模式就可以了。对应不同模式,就有不同的映 +射命令,表示所定义的快捷键只能用于相应的模式下: + +* 普通模式:nmap +* 插入模式:imap +* 可视模式:vmap (三种不同可视模式并不区分,也包括选择模式) +* 命令模式:cmap + +如果不指定模式,直接的 `map` 命令则同时可作用于普通模式与可视选择模式以及命令 +后缀模式(Operator-pending,后文单独讲)。而 `map!` 则同时作用于插入模式与命令 +行模式,即相当于 `imap` 与 `cmap` 的综合体。其实 `vmap` 也是 `xmap`(可视模式 +)与 `smap` (选择模式)的综合体,只是 `smap` 用得很少,`vmap` 更便于记忆(`v` +命令进入可视模式),因此我在定义可视选择模式下的快捷键时倾向于用 `vmap`。 + +在其他情况下,建议用对应模式的映射命令,也就是将模式简名作为 `map` 的限定前缀。 +而不建议用太过宽泛的 `map` 或 `map!` 命令。 + +### 特殊键表示 + +在 `map` 系列命令中,`{lhs}` 与 `{rhs}` 部分可直接表示一般字符,但若要映射(或 +被映射)的是不可打印字符,则需要特殊的标记(`<>`尖括号内不分大小写): + +* 空格:`` 。映射命令之后的各个参数要用空格分开,所以若正是要重定义空格 + 键意义,就得用 `` 表示。同时映射命令尽量避免尾部空格,因为有些映射会 + 把尾部空格当作最后一个参数的一部分。始终用 `` 是安全可靠的。 +* 竖线:``。`|` 在命令行中一般用于分隔多条语句,因此要重定义这个键要用 + `` 表示。 +* 叹号:``。`!` 可用于很多命令之后,用以修饰该命令,使之做一些相关但不同 + 的工作,相当于特殊的额外参数。映射中要用到这个符号最好也以 `` 表示。 +* 制表符:``,回车:`` +* 退格:``,删除键: ``,插入键: `` +* 方向键:`` `` `` `` +* 功能键:`` `` 等 +* Ctrl 修饰键:`` (这表示同时按下 Ctrl 键与 x 键) +* Shift 修饰键:``,对于一般字母,直接用大写字母表示即可,如 `A` 即可,不 + 必有``。一般对特殊键可双修饰键时才用到,如 ``。 +* Alt `` 或 Meta `` 修饰键。在 term 中运行的 vim 可能不方便映射这个修 + 饰键。 +* 小于号:``,大于号 `` +* 直接用字符编码表示:``,后面可接十进制或十六进制或八进制数字。如 + `` 表示编码为 `127` 那个字符。这种方法虽然统一,但如有可能,优先 + 使用上述意义明确方便识记的特殊键名表示法。 + +此外,还有几个特殊标记并不是特指哪个可从键盘输入的按键: +* `` 代表 `mapleader` 这个变量的值,一般叫做快捷键前缀,默认是 `\`。同 + 时还有个 ``,它取的是 `maplocalleader` 的变量值,常用于局部映射 + 。 +* `` 当映射命令用于脚本文件中(应该经常是这种情况),`` 用于指代当前 + 脚本作用域的函数,故一般用于 `{rhs}` 部分。当 vim 执行映射命令时,实际会把 + `` 替换为 `dd_` 样式,其中 `dd` 表示当前脚本编号,可用 + `:scriptnames` 查看所有已加载的脚本,同时也列出每个脚本的编号。 +* `` 一种特殊标记,可以避免与用户能从键盘输入的任何按键冲突。常用于插件 + 中,表示该映射来自某插件。与 `` 关联某一特定脚本不同,`` 并不关联 + 特定插件的脚本文件。它的意义请继续看下一节。 + +### 键映射链的用途与陷阱 + +键映射是可传递的,例如若有以下映射命令: +```vim +: map x y +: map y z +``` +当用户按下 `x`,vim 首先将其解释为相当于按下 `y`,然后发现 `y` 也被映射了,于 +是最终解释为相当于按下 `z`。 + +这就是键映射的传递链特性。那这有什么用呢,为什么不直接定义为 `:map x z` 呢?假 +如 `z` 是个很复杂的按键命令,比如 `LongZZZZZZZ`,那么就可先为它定义一个简短的 +映射名,如 `y`: +```vim +: map y LongZZZZZZZ +: map x1 y +: map x2 y +``` +然后再可以将其他多个键如 `x1` 与 `x2` 都映射为 `y`,不必重复多次写 +`LongZZZZZZZ` 了。然而,这似乎仍然很无趣,真正有意义的是用于 ``。 + +假设在某个插件文件中有如下映射命令: +```vim +: map (do_some_funny_thing) :call ActualFunction() +: map x (do_some_funny_thing) +: map (do_some_funny_thing) +: map x (do_some_funny_thing) +``` + +在第一个映射命令中,其 `{lhs}` 部分是 `(do_some_funny_thing)`,这也是一 +个“按键序列”,不过第一键是 ``(其实不可能从键盘输入的键),然后接一个左 +括号,接着是一串普通字符按键,最后还是个右括号。其中左右括号不是必须的,甚至 +可以不必配对,中间也不一定只能普通字符,加一些任意特殊字符也是允许的。不过当前许 +多优秀的插件作者都自觉遵守这个范式:`(mapping_name)`。 + +该命令的 `{rhs}` 部分是 `:call ActualFunction()`,表示调用当前脚本中 +定义的一个函数,用以完成实际的工作。然而 `...` 是不可能由用户按出来的键 +序列,所以需要再定义一个映射 `:map x ...`,让一个可以方便按出的键 `x` 来 +触发这个特殊键序列 `...`,并最终调用函数工作。当然了,在普通模式下的几乎 +每个普通字母 vim 都有特殊意义(不一定是 `x`,而`x`表示删除一个字符),你可能不 +应该重定义这个字母按键,可加上 `` 前缀修饰或其他修饰键。 + +那么为何不直接定义 `:map x :call ActualFunction()` 呢?一是为了封装隐 +藏实现,二是可为映射取个易记的映射名如 `(mapping_name)`。这样,插件作者 +只将 `(mapping_name)` 暴露给用户,用户也可以自己按需要喜好重定义触发键映 +射,如 `:map y (mapping_name)`。 + +因此,`` 不过是某个普通按键序列的特殊前缀而已,特殊得让它不可能从键盘输 +入,主要只用于映射传递,同时该中间序列还可取个意义明确好记的名字。一些插件作者 +为了进一步避免这个中间序列被冲突的可能性,还在序列中加入插件名,比如改长为: +`(plug_name_mapping_name)`。 + +不过,映射传递链可能会引起另一个麻烦。例如请看如下这个映射: +```vim +: map j gj +: map k gk +``` +在打开具有长文本行的文件时,如果开启了折行显示选项(`&wrap`),则 `gj` 或 `gk` +命令表示按屏幕行移动,这可能比按文件行的 `j` `k` 移动更方便。所以这两个键的重 +映射是有意义的,可惜残酷的事实是这并没有达到想要的效果。作了这两个映射命令之后 +,若试图按 `j` 或 `k` 时,vim 会报错,指出循环定义链太长了。因为 vim 试图作以 +下解释: +``` +j --> gj --> ggj --> gggj --> ... +``` +无尽循环了,当达到一些深度限制后,vim 就不干了。 + +为了避免这个问题, vim 提供了另一套命令,在 `map` 命令之前加上 `nore` 前缀改为 +`noremap` 即可,表示不要对该命令的 `{rhs}` 部分再次解析映射了。 +```vim +: noremap j gj +: noremap k gk +``` + +当然,前面还提到,良好的映射命令习惯是显式限定模式,模式前缀还应在 `nore` 前缀 +之前,如下表示只在普通模式下作此映射命令: +```vim +: nnoremap j gj +: nnoremap k gk +``` + +结论就是:除了有意设计的 `` 映射必须用 `:map` 命令外,其他映射尽量习惯用 +`:noremap` 命令,以避免可能的循环映射的麻烦。例如对本节开始提出的示例规范改写 +如下: +```vim +: nnoremap (do_some_funny_thing) :call ActualFunction() +: nmap x (do_some_funny_thing) +: nmap (do_some_funny_thing) +: nmap x (do_some_funny_thing) +``` + +其中,`:` 并不是什么特殊语法,只不过表示当按下冒号刚进入命令行时先按个 ``, +用以先清空当前命令行,确保在执行后面那个命令时不会被其他可能的命令行字符干扰。 +(比如若不用 `nnoremap` 而用 `noremap` 时,在可视模式选了一部分文本后,按冒号 +就会自己加成 `:'<,'>`,此时在命令行中先按 `` 就能把前面的地址标记清除。在 +很小心地用了 `nnoremap` 时,还会不会有特殊情况导致干扰字符呢,也不好说,反正加 +上 `` 没坏处。但若你的函数本就设计为允许接收行地址参数,则最好额外定义 +`:vnoremap`,不用 `` 的版本。) + +### 各种映射命令 + +前面讲了最基础的 `:map` 命令,还有更安全的 `:noremap` 命令,以及各种模式前缀限 +定的命令 `:nnoremap` `:inoremap` 等。这已经能组合出一大群映射命令了,不过它们 +仍只算是一类映射命令,就是定义映射的命令。此外,vim 还提供了其他几个映射相关的 +命令。 + +* 退化的映射定义命令用于列表查询。不带参数的 `:map` 裸命令会列出当前已重定义的 + 所有映射。带一个参数的 `:map {lhs}` 会列出以 `{lhs}` 开头的映射。同样支持模 + 式前缀缩小查询范围,但由于只为查询,没有 `nore` 中缀的必要。定义映射的命令, + 至少含 `{lhs}` 与 `{rhs}` 两个参数。 +* 删除指定映射的命令 `:unmap {lhs}`,需要带一个完全匹配的左参数(不像查询命令 + 只要求匹配开头,毕竟删除命令比较危险)。可以限定模式前缀,如 `nunmap {lhs}` + 只删除普通模式下的映射 `{lhs}`。注意,模式前缀始终是在最前面,如果你把 `un` + 也视为 `map` 命令的中缀的话。 +* 清除所有映射的命令 `:mapclear`。因为清除所有,所以不需要参数了。当然也可限定 + 模式前缀,如 `:nmapclear`,表示只清除普通模式下的映射。另外还可以有个 + `` 参数,表示只清除当前 buffer 内的局部映射。这类特殊参数在下节继续 + 讲解。 + +### 特殊映射参数 + +映射命令支持许多特殊参数,也用 `<>` 括起来。但它们不同于特殊键标记,并不是左 +参数或右参数序列的一部分。同时必须紧跟映射命令之后,左参数 `{lhs}` 之前,并用 +空格分隔参数。 + +* `` 表示只影响当前 buffer 的映射,`:map` `:unmap` 与 `:mapclear` 都可 + 接收这个局部参数。 +* `` 字面意思是不再等待。较短的局部映射将掩盖较长的全局映射。 + +`` 这个参数很少用到。但其中涉及到的一个映射机制有必要了解。假设有如下 +两个映射定义: +```vim +* nnoremap x1 something +* nnoremap x2 another-thing +``` +因为定义的是两个按键的序列,当用户按下 `x` 键时,vim 会等待一小段时间,以判断 +用户是否想用 `x1` 或 `x2` 快捷键,然后触发相应的映射定义。如果超过一定时间后用 +户没有按任何键,就按默认的 `x` 键意义处理了。当然如果后面接着的按键不匹配任何 +映射,也是按实际按键解释其意义。 + +因此,若还定义单键 `x` 的映射: +```vim +: nnoremap x simple-thing +``` +当用户想通过按 `x` 键来触发该映射时,由于 `x1` 与 `x2` 的存在,仍然需要等待一 +小段时间才能确定用户确实是想用 `x` 键来触发 `simple-thing` 这件事。这样的迟滞 +效应可不是个好体验。 + +于是就提出 `` 参数,与 `` 参数联用,可避免等待: +```vim +: nnoremap x local-thing +``` +这样,在当前 buffer 中按下 `x` 键时就能直接做 `local-thing` 这件事了。 + +尽管有这个效用,但 `` 在实践中还是用得很少。用户在自行设定快捷键时,最 +好还是遵循“相同前缀等长快捷键”的原则。也就说当定义 `x1` 或 `x2` 快捷键后,就最好 +不要再定义 `x` 或 `x123` 这样的变长快捷键了。规划整齐点,体验会好很多。当然, +如实在想为某个功能定义更方便的快捷键快,可定义为重复按键 `xx`,因为重复按键 +的效率会比按不同键快一点。(想想 vim 内置的 `dd` 与 `yy` 命令) +```vim +: nnoremap xx most-used-thing +``` + +另一方面,局部映射参数 `` 却是非常常用,鼓励多用。局部映射会覆盖相同的 +全局映射,而且当 `` 存在时,会进一步隐藏全局中更长的映射。 + +* `` 在默认情况下,当按下某个映射的 `{lhs}` 序列键中,vim 下面的命令行 + 会显示 `{rhs}` 序列键。加上这个 `` 参数时,就不会回显了。我的建议是 + 一般没必要加这个参数禁用这个特性。当映射键正常工作时,你不必去理会它的回显, + 但是当映射键没按预想的工作时,你就可在回显中看到它实际映射成什么 `{rhs}` 了 + ,这可帮助你判断是由于映射被覆盖了还是映射本身哪里写错了。 + +* `` 这是相对过时的参数了,它指示当前这个映射命令中接受 `<>` 标记特殊 + 键。在默认不兼容 vi 的设置下,不必加这个参数也能直接用 `<>` 表示特殊键。 +* `