站内链接:

What is vim?

发展历史

vim(vi[Improved]), 一种跨平台的文本文件编辑工具, 继承 UNIX 系统的 vi 编辑器, 在 Linux/Mac/Windows 系统上都支持. 关于 vim 的入门等文章, 网络上有非常之多, 例如: 简明 VIM 练级攻略.

vim 的发展历史脉络大概如下:ed-->ex-->vi-->vim,其发展历史反映了其作为开源项目的持续进化,以及一个活跃社区对它的支持和贡献。

  • ed: 文本编辑器, 一次仅仅能编辑一行
  • ex: 文本编辑器, 相比 ed, 增加了一些人性化的设置, 相当于 vim -E 启动
  • vi: 模式编辑器, visual(ex 命令: visual), 不同的模式下有不同的工作方式
  • vim: vi Improved
  • other: vi 的其他派生物, nvi(BSD 系统), vim, Elvis, Vigor.

其中 vim 自身也在不断地进化,从而赢得越来越多的用户,从 vim4.0 到现在的 vim9.0 前前后后总共结果了 20 多年的时光。

帮助文档

  1. 获取帮助信息或者使用 vimtutor 来练习基本的 vim 命令
  • 帮助信息: help tutor
  • 启动: vimtutor
  1. 中文文档,目前 vim 的中文文档据说已经完全托管到vimdoc上,安装步骤如下:
1
2
3
4
# 安装
git submodule add https://github.com/yianwillis/vimcdoc.git bundle/vimcdoc
# 之后重启vim即可
vim

Learning of vim

  1. 学习之道
  • 普通思路:发现问题-搜索并查找资料-找到更好的解决方式
  • 高级思路:在普通思路的基础上,了解细节和原理,从而更好的理解普通思路,并且提出相应的优化方法
  • 扩展思路:根据已有的技术和原理,找出相似问题的解决办法,参与技术和原理的制定
  1. 文档为重

一个官方文档抵得上多篇博客文章介绍, 不一定需要英文文档, 下载安装中文文档, 一旦有任何不懂的命令, 使用 help 查询即可.

文件

filetype

用于指示正在编辑的文件的类型,这是非常重要的,特别是对于自定义 vim 命令或者编写 vim 插件的时候,了解文件类型就能决定启用哪些语法高亮、缩进规则和其他特定于文件类型的插件或设置。例如:

  • 对于 python 文件,Vim 会启用 Python 的语法高亮和适当的缩进规则
  • 对于 html 文件,则启用 HTML 的语法高亮和相关设置

从上面就可以简单的看出 filetype 的作用,特别是结合 autocmd 可以根据具体的文件类型设置自定义的命令。那么,filetype 有哪几种设置方式呢?

  • 自动检测,此时设置filetype on即可开启,其主要启用文件类型检测, 当然如果希望关闭则可设置filetype off
  • 手动检测,在 ex 命令中输入:set filetype=python手动指定文件的具体类型

filetype 还是通过在配置中开启插件的自动加载、缩进的自动加载,他们的命令分别是:

  • filetype on/off:文件类型检测的开启和关闭
  • filetype plugin on/off:根据文件类型加载特定插件的开启和关闭
  • filetype indent on/off:根据文件类型进行自动缩进的开启和关闭

fileformat

决定了 Vim 如何识别和处理文件中的行结束符,根据不同的操作系统(unix/Dos/旧 mac)他们的换行标准是不一样的

  • dos: <CR><LF>
  • unix/linux: <LF>
  • 旧 mac 系统:<CR>

另外,可以通过 ex 命令手动设置文件的行结束符

1
2
set ffs         " 列出支持的文件格式
set ff " 获取当前文件格式

注意,line Endings不一致会导致文件编辑的时候出现乱码问题,文件打开出现^M问题,比如用 dos 去读取 unix 文件,即试图 从'^J'(LF)中识别'^M^J'(即 CRLF),所以发生错误,所以在日常开发或者多平台协作的时候需要设置统一的 fileformat

  • 项目文档开发中明确指出,现在一般都是 Unix/Linux 基准的 LF 标准
  • 通过项目目录下的.editorconfig 文件保证不同编辑器和 IDE 之间的统一
  • 通过 git 配置进行自动切换,另外在 CI/CD 时增加检查脚本,一旦检查不通过则拒绝 MR/PR(merge request, pull request)

其中通过 git 配置换行符的自动转换命令如下:

1
2
3
4
5
# Unix/Linux系统中, 设置 Git 检出时不转换,提交时转换为 LF:
git config --global core.autocrlf input

# windows系统中,设置 Git 检出时转换为 CRLF,提交时转换为 LF
git config --global core.autocrlf true

如果发生^M乱码问题之后,在 vim 中怎么处理或解决呢?

1
2
3
%s/\r//g
" 或者
%s/<C-v><C-m>//g

文件编码

fileencodingencoding 是两个与字符编码相关的设置,它们分别用于控制文件的编码方式和 Vim 内部处理字符的编码方式,理解他们的功能和区别对于处理多语言文本还是比较重要的。

  1. 文件编码

fileencoding设置指定了 Vim 保存文件(注意,其关注于保存)时使用的字符编码,一旦编辑并保存文件时,vim 会根据该值来决定如何将文件中的字符编码为相应的字节序列,例如 utf8、GBK 等。下面是文件编码的设置命令:

1
2
3
4
5
6
" 设置当前文件编码
:set fileencoding=utf-8
" 显示当前文件编码
set fileencoding
" 列出字符编码
set fileencodings

注意,如果未设置 fileencoding,默认使用 encoding 的值替代当前文件的字符编码。

  1. 内部编码

encoding 设置决定了 Vim 内部处理字符数据的方式,其影响 Vim 缓冲区中文本的表示、Vim 如何解释键盘输入和显示字符。通过设置encoding可以调整 vim 识别数据的编码方式,其命令为::set encoding=utf8

注意,fileencoding 和 encoding 两者的区别

  • fileencoding:用于当前缓冲区的字符编码,在保存的时候会基于该值作为文件的编码方式
  • encoding:影响的是文本的显示,不影响写入文件时的编码,这点非常重要

autocmd

定义和格式

在 filetype 章节,我们提到 fieltype 和 autocmd 的结合使用,下面就是一个简单的使用示例:

1
2
3
" 对html或者htmldjango类型进行额外的配置
autocmd Filetype html setlocal expandtab ts=2 sts=2 sw=2
autocmd Filetype htmldjango setlocal expandtab ts=2 sts=2 sw=2

那么,autocmd 是什么呢?autocmd 自动命令是 vim 中非常强大的功能,允许你根据不同的事件自动执行一系列命令,这些命令可以是文件类型的更改、读取或保存文件、进入或离开窗口。字如其意,autocmd 可以使 Vim 自动化执行特定任务。通过前端的场景解释,每一个 autocmd 都是一个事件回调函数,在某些动作触发事件之后就会自动执行(slient)对应的命令。

  1. 通用的基本 autocmd 命令格式
1
au[tocmd] [group] {event} {pattern} [nested] {command}

其中命令的说明如下:

  • group,可选项,自动命令组名,若是指定组名,则此 autocmd 自动加入该组,组让你更容易地管理和清除相关的自动命令
  • event,触发事件列表,用逗号隔开,表示当这些事件触发的时候执行此命令
  • pattern,文件模式列表,通常伴随通配符,目标文件类型,表示当文件名或类型与给定的模式匹配时 + 事件触发的时候执行该命令
  • nested,允许自动命令的嵌套使用
  • command,欲被执行的命令
  1. autocmd 删除命令格式

当然,有 autocmd 添加命令,自然伴随着 autocmd 删除命令,其命令格式如下:

1
2
" 删除所有和{event},{pat}关联的自动命令,之后加入新的{cmd}到{group}中
au[tocmd]! [group] {event} {pat} {nested} {cmd}
  1. 列出命令
1
2
3
4
" 列出关联的命令,不添加命令
au[tocmd] [group] {event} {pat}
au[tocmd] [group] * {pat}
au[tocmd] [group] {event}
  1. 嵌套命令

通过nested选项允许一个自动命令触发其他自动命令,当然,默认情况下是不支持嵌套的,其一般用于如下场景:

  • 自动命令需要执行某些可能会触发其他自动命令的操作时,例如加载或保存文件、改变缓冲区等
  • 自动命令链需要串联多个操作,并且后一个操作依赖前一个操作的结果时

例如,一个是在打开任何 .txt 文件时自动设置文本宽度为 80,另一个是在设置文本宽度时自动打开拼写检查,此时就需要设置 nested

1
2
3
" 当文本宽度被设置时,开启拼写检查
autocmd BufRead,BufNewFile *.txt set textwidth=80
autocmd OptionSet textwidth nested setlocal spell

匹配规则

那么,autocmd 的匹配规则是怎么样的呢?其有两种类型的匹配规则:基于路径的文件名匹配、文件类型匹配

  1. 文件名
  • *:匹配任意数量的任意字符,不包括目录分隔符,例如*.html
  • **:匹配任意数量的任意字符,包括目录分隔符,通常用于匹配子目录中的文件,例如:**/*.html表示当前目录下所有子孙目录的 html 文件
  • ?:匹配任意单个字符,不包括目录分隔符
1
2
3
4
5
6
7
8
autocdmd BufNewFile *.txt ex-write
" 这里使用normal命令,在保存文件的时候将光标移动到文件末尾
autocmd BufWritePre *.html ex-normal gg=G

" 删除当前缓冲区的局部于缓冲区的自动命令,见下文说明
au! * <buffer>
" 列出当前缓冲区的局部于缓冲区的自动命令
au * <buffer>
  1. 文件类型,这里就需要配合 filetype 来使用,例如
1
2
3
4
5
autocmd FileType python setlocal shiftwidth=4 tabstop=4 expandtab
" a. 打开.js文件,按,c会注释当前行
autocmd FileType javascript nnoremap <buffer> <localleader>c I//<esc>
" b. 打开.py文件,注释某一行
autocmd FileType python nnoremap <buffer> <localleader>c I#<esc>

当然,一般 autocmd 都是通过组合匹配来完成命令的触发,比如多个事件、多个文件模式都放在一个命令中,他们是的关系,例如:

1
2
" 在打开md或者markdown文件的时候
autocmd BufRead,BufWrite *.md,*.markdown SomeCommand

命令分组

前面已经讲解过 autocmd 可以通过 group 在逻辑上放在某个分组里面,实际上通过autogroup可以在定义的时候就物理意义上进行命令的分组,这样更加的清晰

1
2
3
4
5
6
7
8
" 命令
augroup cprograms
autocmd BufReadPost *.c,*.h set sw=4 sts=4
autocmd BufReadPost *.cpp set sw=3 sts=3
augroup END
" 等价于
autocmd cprograms BufReadPost *.c,*.h set sw=4 sts=4
autocmd cprograms BufReadPost *.cpp set sw=3 sts=3

忽略事件

有忽略命令自然就存在忽略事件的操作,实际上可以通过全局设置 eventignore 指定 Vim 应该忽略的事件列表,其命令说明如下:

1
2
3
4
5
6
" 事件列表
set eventignore=WinEnter,WinLeave
" 所有事件
set eventignore=all
" 恢复正常(清空事件忽略)
set eventinnore=

那么,其使用场景有哪些呢?

  • 编辑大型文件:编辑大型文件时,某些事件(如语法高亮更新)可能会导致性能下降,则可以临时的关闭语法高亮事件
  • 批处理或自动任务:不需要触发某些事件的自动命令,则可以通过 eventignore 来忽略
  • 调试

手动命令

doautocmd 用于手动触发一个或多个自动命令。它强制执行为特定事件定义的自动命令,而不需要等待事件自然发生。

1
2
3
4
5
6
7
8
doautocmd [nomodeline] [group] {event} [fname]
" fname 可选,默认为当前文件或者针对当前缓冲区,用于指定文件模式
" group 特定组或者所有组
" nomodeline 应用完自动命令后,该选项用于过滤否则自动命令中的设置

" 每个载入的缓冲区应用自动命令。用于执行类似于设置选项/修改高亮等任务的自动命令。
" arg 可选,提供给触发的自动命令的额外参数
doautoall [nomodeline] [group] event [arg]

例如,你的 vim 配置中已经存在一个打开 JavaScript 文件时设置特定的缩进规则,但是你现在随手打开一个 filetype 并非 javascript 的文件并进行类型更改,那么在不重新打开文件的前提下触发这条缩进规则,那么就应该执行如下操作:

1
2
:set filetype=javascript
:doautocmd FileType javascript

现场保存

保存方式

使用 session 和 viminfo,可以帮助用户保存和恢复编辑环境,保证当前环境,以便下次启动时进行现场恢复操作,类似 Mysql 的数据库迁移,记录各种命令。虽然这两者的目的类似,即在用户关闭 vim 后能够恢复到之前的工作状态,但它们保存信息和使用方式有一些不同。下面是这两者的使用场景:

  • session: 项目切换(每个项目保存一个 session)、工作状态保持(所有打开文件和他们的布局)、编辑环境恢复
  • viminfo:命令历史共享、寄存器内容持久化(即使重启 vim 也保持)、标记持久化、搜索历史维护

在场景的使用上,session 更加侧重于保存和恢复整个工作环境的状态(文件和布局),viminfo 用于保存和恢复 Vim 的历史记录和状态(命令历史、寄存器等会话间共享的数据),下面简单介绍下它们各自的使用方式和基本原理。

session

当保存一个 session 时,Vim 会将当前环境的状态写入到一个 Session 文件(例如 Session.vim)中,这个文件中保存的实际上一系列的 Vim 命令,当该文件被 vim 加载的时候会重放这些命令以重建当时的编辑环境,这点同binlog 主从复制原理的原理类似,通过重放 sql 命令来实现数据的复制和备份。

  1. 创建和保存会话
1
2
3
4
5
6
" a. 保存当前vim会话,默认保存到当前工作目录下Session.vim文件
:mksession
:mks

" b. 保存到特定的文件中
:mksession my_session.vim

最终生成的 Session.vim 是一个有几百行(安装的插件越多生效的插件越多,行数越多)命令的文件,其中包含了很多内置命令,文件的头部如下

1
2
3
4
5
6
7
8
9
10
11
let SessionLoad = 1
if &cp | set nocp | endif
let s:cpo_save=&cpo
set cpo&vim
inoremap <C-C>
inoremap <silent> <Plug>(ale_complete) :ALEComplete
=emmet#mergeLines()et-merge-lines) =emmet#util#closePopup()
=emmet#anchorizeURL(1)anchorize-summary) =emmet#util#closePopup()
=emmet#anchorizeURL(0)anchorize-url) =emmet#util#closePopup()
=emmet#removeTag()met-remove-tag) =emmet#util#closePopup()
...
  1. 加载会话

在推出或者离开工作编辑环境之后,需要重新加载之前的界面或窗口,则需要加载会话命令完成窗口的载入

1
2
3
4
5
6
7
8
" a. 通过ex命令加载会话文件
:source Session.vim

" b. 在打开vim的时候就加载
vim -S Session.vim

" c. 打开的时候加载
source Session.vim

此后,之前的编辑环境会自动加载,包括切割界面等等。

  1. 更新会话,通过:mksession!可以更新会话中的内容为最新的,确保每次离开工作环境之前都执行一遍该命令
  2. 设置会话保存选项,通过sessionoptions控制保存在会话中的内容,例如是否保存窗口布局等等,注意,这是白名单选项设置
1
2
3
4
5
6
" a. 当前目录和会话目录设置
" 若sessionoptions包含curdir,则保存会话时 Vim 会记录当前的工作目录,并在加载会话时尝试切换到这个目录
" 若sessionoptions包含sesdir,则加载会话时 Vim 会将工作目录切换到保存会话文件的目录
" 下面的配置实现功能:每当你保存或加载会话,Vim 将忽略原始的工作目录,而在加载会话时自动将工作目录切换到会话文件所在的目录
set sessionoptions-=curdir
set sessionoptions+=sesdir

viminfo

每次推出和重新打开一个 vim 窗口的时候都伴随着 viminfo(~/.viminfo)信息的重新写入和重新加载,这里保存的信息会在多个会话之间共享,包含命令历史、搜索历史、寄存器内容等,这个一般不做过多的了解即可。

折叠

folding 可以折叠和展开一部分代码和文本,从而能够聚焦于文件的某一部分,并从整体的视角查看代码或文本,折叠有两个术语或分类

  • foldlevel,决定了打开文件时折叠的最深层级(阈值),只有级别高于这个阈值的折叠才会被打开,而低于或等于这个阈值的折叠会被关闭
  • foldmethod,设置折叠的方式

其中 foldmethod 的可选值如下:

  • manual(手动):用户手动创建折叠。后续可使用zf命令创建折叠,zf20j表示创建从当前行开始向下 20 行的折叠
  • indent(缩进):根据缩进级别自动创建折叠。具有相同缩进级别的连续行被视为一个折叠区域,在 python 文件中经常使用
  • expr(表达式):使用 Vim 脚本表达式来定义折叠,允许基于复杂逻辑创建折叠。通过 foldexpr 来设置折叠的逻辑,例如:set foldexpr=getline(v:lnum)=~'^\\s*if'表示以 if 语句开始的折叠
  • syntax(语法):根据语法高亮来创建折叠,利用语法文件中定义的规则来识别折叠区域。
  • diff:在 diff 模式下,用于隐藏未更改的行
  • marker(标记):根据文件中的特定标记来创建折叠,用户可以在文件中插入特定的字符串来标记折叠的开始和结束,例如:set foldmarker=//{{,//}}定义开始和结束的标记,后续就可以使用`//