以下目标按重要性粗略排序。
注意 许多目标彼此间并不统一,这并非是设计疏漏。我们需要在各项目标之间作出取
舍,寻找平衡。
VIM 坚 守 … 兼 容 VI design-compatible
首先,Vim 必须要能无缝替代传统 Vi。用户可以选择开启兼容模式,Vim 会尽量复制原
版 Vi 的行为,几乎完全无差别。
例外:
- Vim 不会刻意重现 Vi 已知的明显错误。
- Vi 存在多个版本。Vim 以 Vi 3.7 (1985-06-07) 版为参考基准。尽可能兼容其他版
本,但 POSIX 中的 Vi 标准并非绝对依据。
- Vim 会新增指令,不能因为某个命令在 Vi 中不存在,所以就必须假定会报错。
- Vim 具备许多 Vi 没有的新特性。从 Vim 切回 Vi 会有一些无法避免的适配门槛。
- 极少使用的特性 (如 open 模式、崩溃自动发送邮件等),除非有合理原因且实现成本
不高,否则不予加入。
- 部分兼容性存在争议的场景,会通过专属选项标志位控制是否 Vi 兼容。
VIM 定 位 … 增 强 版 VI design-improved
Vim 的 IMproved (改良),是指在保留 Vi 精髓的前提下做得更好,而不是一款完全不同
的编辑器。
- 尽量依托键盘,鼠标需要额外用手操作,而我们没有第三只手。很多终端也不支持鼠
标。
- 一旦开始鼠标操作,尽量不切换回键盘,避免键鼠混合操作。
- 新增命令与选项保持风格统一。便于查找和记忆。同时为后续扩容预留空间。
- 无人知晓的功能等同于无用功能。不添加晦涩特性,或至少在文档中给出明确提示。
- 尽量少用 CTRL 等组合修饰符,这类按键不易输入。
- 兼顾新手与生手。降低上手门槛,支持循序渐进学习。
- 功能扩展没有硬性上限。新增功能取决于: (1) 用户需求 (2) 实现成本 (3) 是否有人
愿意实现。
VIM 特 性 … 多 平 台 适 配 design-multi-platform
Vim 尽可能覆盖更多平台,服务更多用户。
- 支持多种终端。最低要求仅需光标定位和清屏能力。仅用多数键盘具有的按键实现内建
命令。但支持全键盘按键映射。
- 支持多种平台。必要条件是有专人负责该平台开发,且新代码不会破坏主干代码结构。
- 支持多种编译器和库。并非所有人都能随意更换编译器或 GUI 库。
- 用户常会跨平台,在终端版本到图形界面版本之间切换。核心功能应尽量全平台统一,
至少也要在合理成本下做到尽量多平台覆盖。避免用户为了高效工作被迫更换平台。
- 允许部分功能仅在若干平台甚至单个平台上可用。[本条与上条刻意存在冲突,需要权
衡取舍。]
VIM 标 准 … 完 善 文 档 体 系 design-documented
- 没有文档的功能等同于无用功能。新特性补丁必须附带相应文档。
- 文档力求全面易懂。配合实例讲解更佳。
- 行文应尽量简洁,精简文档更便于检索查阅。
VIM 追 求 … 高 性 能 、 体 积 精 简 design-speed-size
Vim 不能过度占用系统资源,保持轻量化、高响应。
- 硬件性能及容量逐年提升。Vim 也可适度扩容,但增速不能超过硬件变化速度。同时应
保证老旧设备依然可用。
- 许多用户会频繁从外壳启动 Vim。启动时间必须尽量短。
- 内置命令执行要高效。尽量缩减耗时。实用而复杂的功能可适当放宽。
- 不少用户仍在慢速网络连接上使用 Vim,应尽量减少通信开销。
- 对体积占用大、使用率低的功能,应设计为编译时可关闭的可选特性。
- Vim 定位为能和其他组件配合使用的工具组件,不应做成臃肿巨型应用,要能和其他程
序良好协作。
VIM 要 求 … 代 码 方 便 维 护 design-maintain
- 源码结构整洁,逻辑可靠,避免混乱堆砌。
- 所有源码文件统一风格,提升可读性 coding-style 。
- 注释要有实际意义! 不要 重复罗列函数名和参数名。要说明功能用途。
- 降低跨平台移植成本,尽量不用大幅修改平台独立代码。
- 遵循面向对象思想: 数据与相关代码封装在同一模块,减少代码模块间的过度依赖。
VIM 理 念 … 高 度 灵 活 可 定 制 design-flexible
Vim 应适配用户个人的使用习惯,而不强求用户使用固定不变的工作方式。无论是影响整
体逻辑的宏观配置 (如 'compatible' 选项),还是细节微调,都应支持自定义。
缺省值经过精心挑选,使大多数用户可以开箱即用。但可用命令和选项调整配置,适配用
户个人习惯与运行环境。
VIM 边 界 … 不 过 度 膨 胀 design-not
- Vim 不是外壳,也不是操作系统。它确实内置终端窗口,其中可运行外壳或调试噐,方
便 ssh 远程应用。但如果主要用途不是文本编辑,这些功能就超出了设计范围 (请改
用 screen 或 tmux 等专用工具)。
调侃的说法: "Vim 不像 Emacs 那样,包揽除了厨房水槽外的所有任务,但有些人说顶
多可以用它洗洗水槽。;-)"
关于 Vim 配合 gdb 调试,可见 terminal-debugger 。其他 (老旧) 工具可见:
http://clewn.sf.net。
- Vim 不刻意追求华丽 GUI 界面,不会为了视觉美观牺牲全平台一致性。但依然欢迎实
用 GUI 功能。
折叠
同一缓冲区可支持以多种折叠方式显示。例如,一个窗口将函数体折叠,另一个窗口则显
示函数体内容。
折叠只是文本的显示方式。它不会修改文本本身。所以,折叠的实现方式是在缓冲区实际
存储的文本 (缓冲区行) 与窗口显示的文本 (逻辑行) 之间,增加一层过滤机制。
窗口命名
"窗口" 一词常被混用: 屏幕窗口、终端窗口、Vim 内部查看缓冲区的窗口。
为避免歧义,原本也可称为窗口的不同概念采用了不同的命名。相关术语一览:
screen 屏幕,整个显示区域。GUI 下对应 1024x768 像素或其他分辨率。Vim
外壳可使用整个屏幕或只占部分区域 (xterm 或 GUI)。
shell 外壳,Vim 应用程序本身。可占据整个屏幕 (如控制台运行时) 或只占
部分区域 (xterm 或 GUI)。
window 窗口,缓冲区视图。Vim 中可存在多个窗口,窗口与命令行、菜单栏、
工具栏等共同分享外壳空间。
拼写检查 develop-spell
在为 Vim 添加拼写检查功能前,已对现有拼写检查库和程序做过全面调研。遗憾的是,
没有一个能够满足 Vim 拼写检查引擎要求,相关原因如下:
- 缺乏多字节编码支持。至少必须支持 UTF-8,以便同一文件能使用多种语言。
实时编码转换未必总是可行 (需要 iconv 支持)。
- 对于外部程序和库: 直接使用需要与 Vim 分别安装。虽非不可行,但有明显缺陷。
- 性能: 测试表明,实时拼写检查 (重绘时执行) 理论上可像语法高亮一样高效,但其他
代码实际使用的机制要慢得多。如基于哈希表的 Myspell。多数拼写检查程序使用的词
缀压缩 (affix compression) 也会降低速度。
- 使用外部程序 (如 aspell) 需要建立通信机制。跨平台实现复杂 (只考虑 Unix 系统
会相对简单,但覆盖率不够)。性能也可能不佳 (涉及大量进程切换)。
- 不支持包含非单词字符的词汇,如 "Etten-Leur"、"et al." 等。只能分段标识每个部
分为好词,从而降低了检测可靠性。
- 缺乏区域或方言支持。例如,难于做到接受所有英语单词,并单独高亮出非加拿大英语
单词。
- 缺乏生僻词支持。许多拼写正确但极少使用的单词,往往是常用词的错误拼写形式。
- 拼写建议功能对性能要求较低,可接受额外安装外部程序或库。但单词列表可能与检查
器使用的不一致,导致建议词可能同时是标识的坏词。
拼写建议 develop-spell-suggestions
拼写建议的生成有两种基础机制:
1. 对坏词小幅修改,查找好词的匹配。或者遍历好词列表,小幅修改后,查找坏词的匹
配。小幅修改指: 删除一个字符,插入一个字符,交换两个字符等。
2. 对坏词和好词列表同时进行发音折叠。再查找匹配,可搭配少量第一种方法提到的小
幅修改。
第一种方法适合查找打字错误。通过实验对比哈希表和其他拼写检查程序采用的方案后,
结论是: 字典树 (trie,一种树结构) 是理想方案。既节省内存,又能高效尝试合理修
改。例如插入字符时,只尝试能拼成好词的字符。哈希表则需要在单词的所有位置尝试所
有可能字符。而且,哈希表需要单独识别单词边界,而字典树无需如此,实现简单很多。
第二种方法适合发音正确但拼写错误的情况。例如 "dictionary" 可能被拼成
"daktonerie"。采用第一种方法时,即使多次尝试也很难匹配正确单词。发音折叠后,变
为 "tkxnry" 和 "tktnr",仅有两个字符之差,匹配难度大幅降低。
要实现按发音折叠 (发音相似) 匹配,需要生成发音折叠相同的单词列表。通过以下方案
的对比实验:
1. 寻找拼写建议时,实时进行发音折叠。遍历由好词构成的字典树,将每个单词进行发
音折叠,然后与坏词比较。这种方式内存占用少,但速度慢。实测即使在快速机器
上,英文也需要数秒,对交互会话还勉强可用。但德语、加泰罗尼亚语等语言都需时
十余秒,明显过慢。对于批量处理 (自动更正),则对所有语言都无法接受。
2. 为发音折叠词单独建字典树。查找简单,和非发音折叠的查找方法完全一致。需要为
每个发音折叠词保存对应的好词列表。优点是查找极快,但缺点是占用大量内存 (达
到 1MB 到 10MB 级别)。部分语言的内存占用甚至超过原单词列表。
3. 和第二种方案类似,但通过词缀压缩 (affix compression),只保存发音折叠词的词
根,减少内存开销。Aspell 就采用这种方式。缺点是需要先对坏词剥离词缀才能进行
折叠比较,词首或词尾出现的错误会使此法失效。好坏词差异较大时速度也会变慢。
最终选择是采用方案二,并使用独立文件存储。这样,内存充足的用户可获取最佳建议,
而内存紧张或只需拼写检查而无需拼写建议的用户,则不会占用过多内存。
单词频率
对拼写建议进行排序时,了解单词的使用频率很有帮助。理论上可以在词典中为每个单词
附带保存频率。但这需要为每个单词单独保存计数,会严重降低单词树的压缩率。而且为
所有语种维护一套词频数据的工作量也非常繁重。另外,排序时最好优先选用当前文本中
已出现过的单词,使建议列表适配当前文本的风格。
实际采用的实现方案是统计文档渲染过程中出现过的单词。通过哈希表来快速记录单词出
现次数。词频初始值则从词缀文件的 COMMON 条目列出的单词预加载。这样,新建文件时
此功能也能生效。
这套方案并非完美,因为 Vim 运行时间越久,单词计数会持续累积。但实际应用中,这
仍然比不做词频加权的排序效果有明显改进。
以下小节定义了所有 Vim 代码和编译工具必须遵循的可移植性和兼容性约束。
MAKEFILES assumptions-makefiles
POSIX.1-2001
Vim 的主 Makefile 以最高可移植性为目标,仅依赖 POSIX.1-20001 标准中定义的
make 特性,不使用后续 POSIX 标准或 GNU/BSD 扩展特性。具体应避免的规则是:
- % 模式规则
- POSIX.1-2001 之外的现代赋值语法 ( := 、 ::= )
- 特殊目标 ( .ONESHELL 、 .NOTPARALLEL 、 .SILENT 、…)
- 唯顺序前提 (order-only prerequisites) ( | ) 或自动创建目录功能
- GNU/BSD 条件语句 ( ifdef 、 ifndef 、 .for / .endfor 、…)
因为 POSIX.1-2001 只支持传统后缀规则,所有在独立目录里编译的目标文件必须编写显
式规则。例如:
objects/evalbuffer.o: evalbuffer.c
$(CCC) -o $@ evalbuffer.c
这种冗余写法确保同一份 Makefile 不作修改就可在 Linux、*BSD、macOS、Solaris、
AIX、HP-UX 和几乎所有类 Unix 的操作系统上,通过系统缺省 make 成功构造 Vim。
部分平台专用的 Makefile (如 Windows、NSIS 或 Cygwin) 在无需兼容基础 make 时。
允许使用更高级的特性。
C 编 译 器 assumptions-C-compiler
ANSI-C C89 C90 C95 C99
Vim 致力于最高可移植性 (见 design-multi-platform ),必须仍能在 OpenVMS VAX
V7.3 系统上的 Compaq C V6.4-005 编译器上编译。
因此,Vim 遵循的最新 ISO C 标准是:
C95 (ISO/IEC 9899:1990/AMD1:1995)
但显式允许使用以下 C99 特性:
- // 风格的注释,因为 style-comments 强制要求;
- 代码块内声明和语句混合书写;
- 可变参数宏 `(..., __VA_ARGS__)`;
- enum 列表的额外拖尾逗号;
- _Bool 类型 (用于 bool 、 true 和 false );
- __func__ 预定义标识符;
- inline 函数 (为可移植性,请用 `static inline`);
- 复合常量 `(type){ initializer-list }`;
- 源码逻辑行最长支持 4095 个字符。
平台特定代码则允许使用该平台支持的更新的任意编译器特性。
变 量 大 小 assumptions-variables
Vim 遵循 POSIX.1-2001 (SUSv3) 规定的类型大小规格,实际含义为:
char_u 8 位无符号整数
int 32 位或更大的有符号整数
unsigned 32 位或更大的无符号整数
函 数 原 型 assumptions-prototypes
对绝大多数内部函数原型,Vim 不使用常规头文件 ( .h )。当前架构是在 src/proto/
目录下,为每个 .c 文件使用一个单独的 .pro 文件。
和传统自包含头文件不同,这些 .pro 文件不包含 API 文档,也不包含 struct 和
enum 定义以及其他声明;只包含函数原型。
src/Makefile 里的 `make proto` 目标通过 Python 脚本 proto/gen_prototypes.py
自动更新大部分 .pro 文件,该脚本依赖 python3-clang 模块。注意 少数 proto 文
件仍需手工编辑。
以下是修改 Vim 源代码时必须遵循的准则。为了保持源代码的可读性和可维护性,请严
格遵守这些准则。
这份列表并非完整,更多示例请直接查看源码。
代码库中包含一份 editorconfig 文件,配合发布的 editorconfig 插件
editorconfig-install ,可确保代码自动遵循推荐风格。
修 改 代 码 style-changes
代码修改的基本步骤为:
1. 从 github 获取代码。方便修改版本随时和主代码库保持同步 (修改可能需要一段时
间才会被合并)。
2. 首先修改文档,这有助于直观感受到本改动对用户产生的影响。
3. 修改源代码。
4. 检查 ../doc/todo.txt,确认所作改动是否会影响已列出的任何项目。
5. 在 src/testdir 中添加测试用例,以验证新行为并确保未来不会出现功能退化。
6. 使用 "git diff" 生成补丁。
7. 记录修改内容,最好能说明相关问题及解决方案。向 vim-dev 邮件组发送邮件,附
上说明与 diff 本身。
除非是很小的改动,务必在 github 上创建拉取请求 (PR),因为这样会自动触发测试套
件。
一个理想的 PR 应该只包含一个提交,包含一个逻辑变动。不过,如果要想将多个逻辑独
立的原子化改动归组,也可在单个 PR 里汇总多个提交,这能使更长的 PR 方便评审。请
务必在每个提交信息中描述修改的目的,这对评审过程非常有帮助。当每个提交处理不同
的逻辑变动时,它们在 Vim 仓库里会被作为独立补丁来应用。
style-clang-format
sound.c 和 sign.c 可根据已发布的 .clang-format 文件,通过 clang-format 工具
进行 (半) 自动格式化。其他源文件目前尚未与 .clang-format 文件匹配。未来可能会
有所改变,其他文件也可能会被统一重新格式化。
注 释 style-comments
尽量避免在函数内部使用多行注释: 如果函数复杂到需要逐段分别注释,通常应该考虑重
构拆分函数结构。
文件头部和函数整体描述,应用多行注释:
/*
* 描述
*/
其他普通注释,应用单行注释:
// 注释
缩 进 style-indentation
代码缩进固定使用 4 个空格。用 Vim 自身编辑源代码时无需自动设置,文件自带
modeline 会自动生效。
代码库根目录提供了 .editorconfig ,可使其他编辑器自动识别缩进规则。
源代码 sign.c 和 sound.c 以及所有新建源码文件只能使用空格缩进,禁止使用制
表。此外,所有新建文件必须加入含 `set et` 的模式行,才能通过缩进规范检测。
变 量 声 明 style-declarations
建议在 for 循环内直接声明循环变量:
正确:
for (int i = 0; i < len; ++i)
错误:
int i;
for (i = 0; i < len; ++i)
所有变量声明时必须提供缺省值:
正确:
int n = 0;
int *ptr = NULL;
错误:
int n;
int *ptr;
花 括 号 style-braces
所有花括号必须单独起一行:
正确:
if (cond)
{
cmd;
cmd;
}
else
{
cmd;
cmd;
}
错误:
if (cond) {
cmd;
cmd;
} else {
cmd;
cmd;
}
正确:
while (cond)
{
cmd;
cmd;
}
错误:
while (cond) {
cmd;
cmd;
}
正确:
do
{
cmd;
cmd;
} while (cond);
或
do
{
cmd;
cmd;
}
while (cond);
错误:
do {
cmd;
cmd;
} while (cond);
自 定 义 类 型 style-types
建议使用语义清晰的自定义类型名。在 src/vim.h、src/structs.h 等头文件里定义。
注意 所有的自定义类型必须使用 "_T" 后缀。
示例:
linenr_T
buf_T
pos_T
空 格 和 标 点 style-spaces
函数名和括号间不留空格:
正确: func(arg);
错误: func (arg);
if,while,switch 等关键字后必须加一个空格:
正确: if (arg) for (;;)
错误: if(arg) for(;;)
逗号或分号后必须加一个空格:
正确: func(arg1, arg2); for (i = 0; i < 2; ++i)
错误: func(arg1,arg2); for (i = 0;i < 2;++i)
在 '=','+','/' 等运算符前后必须各加一个空格:
正确: var = a * 5;
错误: var=a*5;
应使用空行将包括关联操作的代码段分组。
正确:
msg_puts_title(_("\n--- Signs ---"));
msg_putchar('\n');
if (rbuf == NULL)
buf = firstbuf;
else
buf = rbuf;
while (buf != NULL && !got_int)
错误:
msg_puts_title(_("\n--- Signs ---"));
msg_putchar('\n');
if (rbuf == NULL)
buf = firstbuf;
else
buf = rbuf;
while (buf != NULL && !got_int)
函 数 style-functions
函数返回类型单独占一行,和函数声明的其余部分使用相同缩进。
正确:
int
function_name(int arg1, int arg2)
{
}
错误:
int function_name(int arg1, int arg2)
{
}
函数参数的命名应有实际含义。
使 用 通 用 函 数 style-common-functions
许多常用的通用函数,Vim 都提供了专用版本。请优先使用 Vim 版本,因为它们的引入
是有明确原因的。
通用函数名 VIM 函数名 VIM 版本的区别
free() vim_free() 检查释放 NULL 的情况
malloc() alloc() 检查内存不足的情况
malloc() lalloc() 与 alloc() 类似,但参数为长整型
strcpy() STRCPY() char_u* 类型参数强制转换为 (char*) 类型
strchr() vim_strchr() 接受特殊字符
strrchr() vim_strrchr() 接受特殊字符
isspace() vim_isspace() 可处理代码值 > 128 的字符
iswhite() vim_iswhite() 仅对 Tab 和空格返回 TRUE
memcpy() mch_memmove() 能处理内存重叠的复制
bcopy() mch_memmove() 能处理内存重叠的复制
memset() vim_memset() 对所有系统通用
命 名 style-names
函数名不能超过 31 个字符 (因为 VMS 系统限制)。
不要使用 "delete" 或 "this" 作为变量名称,C++ 不兼容。
为了使 Vim 能在尽可能多的平台上运行,必须避免使用系统已定义的名称。下面是已知
会引发冲突的名称列表 (以正则表达式表示)。
is.*() POSIX,ctype.h
to.*() POSIX,ctype.h
d_.* POSIX,dirent.h
l_.* POSIX,fcntl.h
gr_.* POSIX,grp.h
pw_.* POSIX,pwd.h
sa_.* POSIX,signal.h
mem.* POSIX,string.h
str.* POSIX,string.h
wcs.* POSIX,string.h
st_.* POSIX,stat.h
tms_.* POSIX,times.h
tm_.* POSIX,time.h
c_.* POSIX,termios.h
MAX.* POSIX,limits.h
__.* POSIX,system
_[A-Z].* POSIX,system
E[A-Z0-9]* POSIX,errno.h
.*_t POSIX,用于 typedefs。请用 .*_T 代替。
wait 不要用作函数参数,和 types.h 冲突
index 会屏蔽全局的同名声明
time 会屏蔽全局的同名声明
new C++ 保留关键字
clear Mac curses.h
echo Mac curses.h
instr Mac curses.h
meta Mac curses.h
newwin Mac curses.h
nl Mac curses.h
overwrite Mac curses.h
refresh Mac curses.h
scroll Mac curses.h
typeahead Mac curses.h
basename() GNU 字符串函数
dirname() GNU 字符串函数
get_env_value() Linux 系统函数
杂 项 style-various
宏 (define) 定义名称必须全部大写:
#define SOME_THING
功能特性 (feature) 必须以 "FEAT_" 开头:
#define FEAT_FOO
不要使用 '\"',部分编译器无法处理。直接使用 '"' 即可。
不要使用:
#if HAVE_SOME
因为部分编译器无法处理,会报告提示 "HAVE_SOME" 未定义。
请改用
#ifdef HAVE_SOME
或者
#if defined(HAVE_SOME)
风 格 style-examples
一行只写一条语句。
错误: if (cond) a = 1;
正确: if (cond)
a = 1;
错误: while (cond);
正确: while (cond)
;
错误: do a = 1; while (cond);
正确: do
a = 1;
while (cond);
次要版本 (如 9.2.0) 或主要版本 (如 10.0) 发布间相隔的时间称为一个开发周期。
在同一个开发周期里,对 C 核心代码的每一次变动,都会分配一个递增的人类可读补丁
号,用于唯一标识特定补丁版本。一个典型的开发周期往往持续数年,累积大约
1500 - 2500 个补丁号。
正式发布前,会宣布进入稳定期。在此期间,只接受纯粹的漏洞修正、安全补丁,文档
改动、翻译刷新以及运行时文件更新 (只要不引入后向不兼容的改动),工作重心只为打
磨即将到来的发布。
新特性只在开发阶段内接纳,稳定阶段不再新增功能。开发周期中可以持续开发、迭代调
整新特性,但在周期结束前必须完成特性定型。
次要版本一旦发布,其中包含的特性不再接受后向不兼容的改动。后续补丁必须保持 Vim
C 核心代码的兼容性。运行时文件的处理则稍宽松,给维护者留出更多调整旧行为的余
地。
在开发周期内,可以标记部分特性为已废弃,废弃特性可通过编译选项在编译时禁用。等
到下一个开发周期,废弃特性可被彻底移除。
vim:tw=78:ts=8:noet:ft=help:norl: