xLV02-vimscript-10-折叠.md

本文主要是vimscript一些基础知识和操作的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
Windows windows11
Ubuntu Ubuntu16.04的64位版本
VMware® Workstation 16 Pro 16.2.3 build-19376536
点击查看本文参考资料
点击查看相关文件下载
--- ---

一、什么是折叠?

1. 折叠的概念

标题会是我看到这篇文章总标题想到的第一个问题,折叠就是叠起来嘛。那Vim文档中有两节来讲折叠:

VIM: usr_28 usr_28.txt
VIM: fold fold.txt

在一些IDE中都会有这样的折叠功能,其实就是这个样子的:

动画

Vim中折叠用于把缓冲区内某一范围内的文本行显示为屏幕上的一行。那些文本仍然在缓冲区内而没有改变。受到折叠影响的只是文本行显示的方式。 折叠的好处是,通过把多行的一节折叠成带有折叠提示的一行,会使我们更好地了解对文本的宏观结构。Vim支持六种不同折叠类型分别是ManualMarkerDiffExprIndent

manual手动定义折叠
indent更多的缩进表示更高级别的折叠
expr用表达式来定义折叠
syntax用语法高亮来定义折叠
diff对没有更改的文本进行折叠
marker对文中的标志折叠

下边就来详细了解下吧!不过之前需要创建一个测试文件test.vim

点击查看文件内容
1
2
3
4
5
6
7
8
9
let num = 1
let sum = 0

while num <= 10
let sum += num
let num += 1
endwhile

echo "sum="sum"\nnum="num

2. 一个例子

我们可以将光标置于while所在的第四行,然后按下zfap四个按键,如下图所示:

然后我们退出编辑器再打开看看,会发现这个折叠没有了。

zfap啥意思?
1
2
zf  是个操作符,代表创建折叠
ap 是一个文本对象,代表"一个段落"

二、基本语法

1. 折叠的选项

这里只列自己常用的几个,详细的可以使用:help fold来查看Vim中的手册,或者看这里:VIM: fold.txt-fold-options

1.1 foldcolumn

键值选项,设置在窗口的边上表示折叠的栏的宽度。当为 0 时,没有 折叠栏。值得范围最大为12,一般设置为4或者5,我一般设置为2

1
2
3
4
5
6
7
8
9
# 打开的折叠显示样式

|
|

# 关闭的折叠显示样式
+

# 折叠栏太窄不能显示所有折叠时,会显示一个数字来表示嵌套的级别

若是设置了mouse = a也就是启用鼠标选项,就可以直接点击+来打开折叠,点击-来关闭折叠。、

1.2 foldlevel

键值选项,数字越大则打开的折叠越多。为0时关闭所有折叠。

1.3 foldtext

键值选项,值是一个字符串。字符串可以是一个表达式,这个表达式被用来求得关闭折叠所显示的文字。

点击查看可以使用的变量
v:foldstart折叠首行的行号
v:foldend折叠末行的行号
v:folddashes一个含有连字符的字符串,用来表示折叠级别
v:foldlevel折叠级别

【注意】对 :set 命令作特殊处理的字符在其前面须加上反斜杠。如: 空格,反斜杠 和双引号。

键值选项,值为字符串,表示当前折叠方式。可以为manualindentexprmarkersyntax或者diff分别代表六种方式。

2. 折叠的命令

2.1 创建和删除

zf{motion}

{Visual}zf
创建折叠操作符。仅当 'foldmethod' 设为 "manual" 或 "marker" 时有效。用 "manual" 方式,新建的折叠会被关闭。同时 'foldenable' 会被设定。
zF对 [count] 行创建折叠。其余同 "zf" 。
:{range}fo[ld]对 {range} 内的行创建折叠。其余同 "zf" 。
zd删除 (delete) 在光标下的折叠。当光标在被折叠的行上,该折叠被删除。嵌套的折叠上移一级。在可视模式下所选区域 (部分) 涵盖的一层折叠被删除。仅当 'foldmethod' 设为 "manual" 或 "marker" 时有效。
zD循环删除 (Delete) 光标下的折叠。在可视模式下所选区域 (部分) 涵盖的折叠和嵌套的折叠都被删除。仅当 'foldmethod' 设为 "manual" 或 "marker" 时有效。
zE除去 (Eliminate) 窗口里所有的折叠。仅当 'foldmethod' 设为 "manual" 或 "marker" 时有效。

2.2 打开和关闭

zo 打开 (open) 在光标下的折叠。
zO 循环打开 (Open) 光标下的折叠。
zc 关闭 (close) 在光标下的折叠。
zC 循环关闭 (Close) 在光标下的所有折叠。
za 打开关闭的折叠,或关闭打开的折叠。
zA 当处在一关闭的折叠上时,循环地打开折叠。当处在一打开的折叠上时,循环地关闭折叠且设定 'foldenable'。
zv 查看 (view) 光标所在的行: 仅打开足够的折叠使光标所在的行不被折叠。
zx 重新应用 'foldlevel' 然后执行 "zv"。
zX 手工恢复被打开和关闭的折叠: 再次应用 'foldlevel'。
zm 折起更多 (more): 'foldlevel' 减 v:count1。
zM 关闭所有折叠: 'foldlevel' 设为 0。
zr 减少 (reduce) 折叠: 'foldlevel' 加 v:count1。
zR 打开所有的折叠。'foldlevel' 设为最高级别。
zn 不折叠 (none): 复位 'foldenable'。所有的折叠被打开。
zN 正常折叠 (normal): 设定 'foldenable'。所有的折叠都展现它们之前的样子。
zi 翻转 'foldenable' 的值。

2.3 存储和恢复

当我们关闭一个文件,开始编辑另一个文件时,被关闭的折叠状态就丢失了。如果稍后再重新打开关闭的文件,那么,所有手动打开和关闭的折叠,全都恢复到它们的默认状态了。如果折叠是用手动方式创建的,则所有的折叠都会消失。那这怎么解决呢?

:mkview 储存影响文件视图的设定及其它内容。
:loadview 可以重新载入前边保存的视图。

3. 手动折叠

使用命令来手动定义要折叠的范围。但是该种方式创建的折叠在关闭文件后就会消失,要想留存的话可以使用存储的命令,在退出前存储视图,而后再载入视图即可。例如,在mormal模式下键入zfap便可以为一个文本段创建折叠。

4. 按缩进折叠

由缩进行自动定义折叠。折叠级别由行的缩进除以shiftwidth(向下取整) 计算而得。连续的,有同样或更高的折叠级别的行,将会形成一个折叠。其中,有更高折叠级别的行形成嵌套的折叠。嵌套的级别数受 foldnestmax 选项限制。

某些行会被忽略并得到上一行或下一行的折叠级别 (取较小值)。符合条件的这样的行要么是空行,要么以 foldignore选项里包含的字符开始。在查找foldignore 里包含的字符时,空白字符会被忽略。对于 C语言,该选项使用 #来略过要预处理的那些行。

使用之前需要先设定:set foldmethod=indent

4.1 折叠原理

大概的原理就是下边这样:

  • 文件中的每行代码都有一个foldlevel,它不是为零就是一个正整数。
  • foldlevel0的行不会被折叠。
  • 有同等级的相邻行会被折叠到一起。
  • 如果一个等级n的折叠被关闭了,任何在里面的、foldlevel不小于n的行都会一起被折叠,直到有一行的等级小于n

看完之后,模棱两可,说实话,反正我是没看懂,不过可以试验一下。

4.2一个实例

我们修改test.vim内容如下:

1
2
3
4
5
6
7
a1
b1
b2
c1
c2
b3
a2

然后在命令模式中执行:

1
:set foldmethod=indent

可以观察到以下现象:

3

然后我们在命令模式下依次执行以下命令观察输出结果:

1
2
3
4
5
6
7
:echom foldlevel(1)
:echom foldlevel(2)
:echom foldlevel(3)
:echom foldlevel(4)
:echom foldlevel(5)
:echom foldlevel(6)
:echom foldlevel(7)

会依次看到有如下数字信息输出:

1
0 1 1 2 2 1 0

即有如下关系:

1
2
3
4
5
6
7
a1                    foldlevel=0
b1 foldlevel=1
b2 foldlevel=1
c1 foldlevel=2
c2 foldlevel=2
b3 foldlevel=1
a2 foldlevel=0

现在再去读上边的原理可能就更容易理解了。

5. 按标志折叠

5.1一般格式

标志用于指定一个折叠区的起点和终点。标志折叠可以精确地控制一个折叠究竟包含哪些行文本,缺点是需要改动文本。选项foldtext通常设 置为使折叠行显示折叠标志之前的文本,这样做可以为折叠命名。使用前需要设置选项:set foldmethod=marker

标志可以包含级别数,也可以使用匹配对。包含级别数较简单,我们可以无须添加结束标志,并可以避免标志不配对的问题。一般格式如下:

1
2
3
4
5
6
# 1.无结束标志
{{{n " n表示级别

# 2.有结束标志
{{{n
}}}n

(1)如果遇到级别相同的标志,上一个折叠结束,另一个有同样级别的折叠开始。

(2)如果遇到级别更高的标志,开始一个嵌套的折叠。

(3)如果遇到级别更低的标志,所有大于或等于当前级别的折叠结束,且指定级别的折叠开始。

(4)带数字的标志和没有带数字的标志可以混合使用。但是一般建议带上结束标志和数字,这样更安全些。

【注意】

(1)数字指定了折叠级别。不能使用 0 (忽略级别为 0 的标志)。

(2)对于一些编程语言源文件,这样做明显会报错,但是Vim是支持将其写在注释中的。

5.2一个实例

修改test.vim文件内容如下:

1
2
3
4
5
6
7
8
/* global variables {{{1 */
int varA, varB;
/* functions {{{1 */
/* funcA() {{{2 */
void funcA() {}

/* funcB() {{{2 */
void funcB() {}

然后在命令模式下执行:

1
:set foldmethod=marker

会看到如下现象:

4

6. 按语法折叠

Vim 为每一种不同的语言使用一个不同的语法文件。语法文件为文件中各种不同语法项定义颜色。如果我们正在用Vim在一个支持色彩的终端上阅读英文原版帮助文档,那么我们看见的色彩就是由语法文件help定制的。 在语法文件中,可以加入一些带有fold 参数的语法项来定义折叠,折叠级别由嵌套的折叠层数来定义。嵌套数由 foldnestmax限定。。这些语法项将定义折叠区。这要求写一个语法文件,把这些项目加入其中,然后可以像以上解释过的那样打开和关闭折叠,编辑文件时折叠会自动创建和删除。

7. 按是否改动折叠

对没有改动的文本或靠近改动的文本自动定义折叠。这个方法仅适用于当前窗口设定 diff选项来显示不同之处时才有效,否则,整个缓冲 区就是一个大的折叠。选项 diffopt 可以指定上下文,即折叠和不被折叠包括的改变之间相距的行数。例如,设定上下文为 8(默认值是 6

1
:set diffopt=filler,context:8

当设定了 scrollbind 选项时,Vim 会试图在其它比较窗口中打开相同的折叠,这样这些窗口就会显示同一处文本。不需要讨论这种折叠就可以,因为Vim会自动使用它。

8. 表达式折叠

表达式折叠类似于缩进折叠,但并非利用文本行的缩进,对每行,通过计算选项 foldexpr的值(表达式)来并得到它的折叠级别,当表达式变得相对复杂时,可以将其放入一个函数,然后设定 foldexpr来调用该函数。表达式方式的折叠也是由折叠级别自动定义的。

【说明】

(1)表达式必须没有副作用。在缓冲区里的文字,光标位置,查找模式,选项等等,不能被改动。

(2)表达式中有错误或者计算结果不能识别时,Vim 不会产生错误消息,而是将折叠级别设为 0。所以当需要调试时,可将 debug选项设为 msg,错误消息就可以被见到了。

(3)最好避免使用 =as 作为返回值,因为 Vim 不得不经常向后回溯以得到折叠级别,这会降低执行速度。

(4)foldlevel()计算相对于上一折叠级别的折叠级别。但要注意,如果该级别未知,foidlevel()将返回 -1。它返回的级别对应于该行开始的位置,尽管折叠本身可能在该行结束。

(5)折叠可能会没有及时更新。用zx或者zX可以强制折叠更新。

点击查看实例

对所有以制表符开始的连续的几行,创建折叠:

1
:set foldexpr=getline(v:lnum)[0]==\"\\t\"

调用函数来计算折叠级别:

1
:set foldexpr=MyFoldLevel(v:lnum)

用空白行分开的段落构成折叠:

1
2
:set foldexpr=getline(v:lnum)=~'^\\s*$'&&getline(v:lnum+1)=~'\\S'?'<1':1
:set foldexpr=getline(v:lnum-1)=~'^\\s*$'&&getline(v:lnum)=~'\\S'?'>1':1

【说明】变量 v:lnum 被定为该行行号

【注意】:set 要特殊处理的字符必须用反斜杠转义(空格,反斜杠,双引号等等)。

点击查看表达式计算结果说明
说明
0这行不折叠
1, 2, ..这行的折叠级别 1,2 等
-1折叠级别没有定义,使用这行之前或之后一行的级别值,取其中较小的一个。
"="使用上一行的折叠级别。
"a1", "a2", .. 上一行的折叠级别加 1,2,..,结果适用于当前行
"s1", "s2", .. 上一行的折叠级别减 1,2,..,结果适用于当前行
"<1", "<2", .. 此折叠级别在本行结束
">1", ">2", .. 此折叠级别在本行开始

【说明】不需要用>1 (<1) 标志折叠的开始 (结束),当这行折叠级别高于 (低于) 上一行的 级别时,折叠将开始 (结束)。

三、综合应用

1. 怎样的效果?

在之前,我们需要有想要折叠成的样子,然后才好动手,这个应用例子我想要达到的效果是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Hello(void)
{
int i = 0;
int count = 0;
printf("Hello World");
for(i = 0; i<5; i++)
{
count += i;
}
printf("count=%d\n", count)
}

void testHello(void)
{
Hello();
}
1
2
3
+--  11 lines: void Hello(void)

+-- 3 lines: void testHello(void)

2. 按表达式折叠

按表达式折叠的方式可以给我们最大限度的自由来自定义我们的折叠方式。修改~/test.vim文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
" 设置侧边显示折叠栏宽度
set foldcolumn = 2
" 告诉Vim使用expr折叠
setlocal foldmethod=expr
" 计算折叠级别
setlocal foldexpr=Get_C_fold(v:lnum)

" 对任意行均返回0的占位(dummy)函数
function! Get_C_fold(lnum)
return '0'
endfunction

然后我们打开test.c文件,在Vim命令模式中执行:

1
:source test.vim

我们会发现仅仅左侧出现了折叠栏,然而并未有任何的折叠产生。这是因为函数对任意行均返回0,所以Vim将不会进行任何折叠。

3. 特殊行折叠

自定义的表达式可以直接返回一个foldlevel,或者返回一个特殊字符串来告诉Vim如何折叠这一行。例如'-1'可以作为一种特殊字符串,它可以告诉Vim,这一行的foldlevelundefinedVim将把它理解为:该行的foldlevel等于其上一行或下一行的较小的那个foldlevel

Vim可以把undefined的行串在一起,所以假设有三个undefined的行和接下来的一个level1的行, 它将设置最后一行为1,接着是倒数第二行为1,然后是第一行为1

1
2
3
4
undefined    1
undefined 1
undefined 1
level 1 1

4. 空行处理

修改Get_C_fold函数,加入空行处理功能,修改函数如下:

1
2
3
4
5
6
7
function! Get_C_fold(lnum)
if getline(a:lnum) =~? '\v^\s*$'
return '-1'
endif

return '0'
endfunction

然后我们打开test.c文件,在Vim命令模式中执行:

1
:source test.vim

我们会发现仅仅左侧出现了折叠栏,然而还是未有任何的折叠产生。这是因为所有的行的foldlevel要不是为0,就是为undefined。 等级为0的行将影响undefined的行,最终导致所有的行的foldlevel都是0

【说明】

(1)\v^\s*$是正则表达式,将匹配”行的开头,任何空白字符,行的结尾”。

(2)比较用大小写不敏感的比较符=~?完成,也可以使用=~只匹配正则表达式。

(3)如果当前行包括一些非空白字符,它将不会匹配,将如返回'0'。如果当前行匹配正则表达式(比如它是空的或者只有空格),就返回字符串'-1'

5. 缩进等级

  • 函数一:获取各行缩进等级

接下来就是缩进等级的处理了,主要是针对非空行的处理,我们在test.vim中新增一个函数:

1
2
3
function! Indent_level(lnum)
return indent(a:lnum) / &shiftwidth
endfunction
点击查看 indent 函数
  • indent({lnum})

返回数值,表示第 {lnum} 行的缩进距离。缩进的计算以空格计,因此它与tabstop的值有关系。{lnum} 的使用方式和 getline() 相同。如果 {lnum} 非法,返回 -1,例如行号不存在的时候,就会返回-1

然后我们在test.c文件窗口的吗,命令模式执行:

1
:echom Indent_level(n)    

将会显示第n行的缩进等级。

  • 函数二:获取下一个非空行行号

为什么需要这样一个函数呢?主要是为了将折叠段头行到对应的缩进段中。在test.vim中增加如下函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function! Next_noneblank_line(lnum)
" 1.获取文件总行数
let numlines = line('$')
" 2.设置current为下一行行号
let current = a:lnum + 1
" 3.遍历当前行一直到文件最后一行,寻找首个非空行
while current <= numlines
" 匹配有一个非空白字符的行 \v\S表示非空白字符
if getline(current) =~? '\v\S'
return current
endif

let current += 1
endwhile

return -2
endfunction

若是找到了非空行,就返回非空行行号,要是没有非空行,那就会一直向下循环,直到循环结束,都还是空白行的话,那么就返回'-2',当遇到不存在的行的时候也是会返回'-2'

那为什么不是'-1'或者'0'作为返回值呢?'-1'(和'0')是特殊的Vim foldlevel选项字符串。看到'-1',会想到上一个空行处理的返回结果含义为undefined foldlevel'0'也类似,会有其他不同的含义。 这里用一个其他的数值可以突出它不是一个foldlevel,而是表示一个错误。不过当然也可以返回'1'或者'0',也不会有问题,区分清楚就可以啦。

6. 最后的折叠函数

前边对一些特殊情况和一般情况已经处理完毕了,接下来来就是最后的折叠函数,也就是表达式需要的折叠规则。在test.vim中修改函数Get_C_fold

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function! Get_C_fold(lnum)
" 1.空行处理
if getline(a:lnum) =~? '\v^\s*$'
return '-1'
endif
" 2获取各行的缩进等级
let this_indent = Indent_level(a:lnum)
" 3.获取当前行的下一行的缩进等级
let next_indent = Indent_level(Next_noneblank_line(a:lnum))
" 4.同级缩进判断
" 下一行缩进等级与当前行相等,则返回当前行缩进等级
if next_indent == this_indent
return this_indent
" 下一行缩进等级小于当前行,则返回当前行缩进等级
elseif next_indent < this_indent
return this_indent
" 下一行缩进等级大于当前行,则返回返回一个以 >开头和下一行的缩进等级构成的字符串
elseif next_indent > this_indent
return '>' . next_indent
endif
endfunction

我们可以先试一下这个函数获取的每行的缩进等级是怎样的,我们可以在test.c中执行:

1
:source test.vim

这条命令可以使脚本文件在此窗口中生效,然后依次执行:

1
:echo Get_C_fold(n)

但是好像行数有点多哈,那我们来做一个测试函数把,在test.vim中添加以下测试函数:

1
2
3
4
5
6
7
8
function! Test()
let numlines = line('$')
let i = 1
while i <= numlines
echo Get_C_fold(i)
let i += 1
endwhile
endfunction

然后在test.c中命令模式下执行:

1
2
:source test.vim
:call Test()

其实此时已经可以看到缩进效果了,不过这里主要还是分析缩进级别,这样便可以获取每一行的缩进级别,可以得到如下对应关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void Hello(void)                   /* 0  */
{ /* >1 */
int i = 0; /* 1 */
int count = 0; /* 1 */
printf("Hello World"); /* 1 */
for(i = 0; i<5; i++) /* 1 */
{ /* >2 */
count += i; /* 2 */
} /* 1 */
printf("count=%d\n", count) /* 1 */
} /* 0 */
/* -1 */
void testHello(void) /* 0 */
{ /* >1 */
Hello(); /* 1 */
} /* 0 */

上边在表达式折叠中有提到,>n就表示此折叠在本行开始,这就意味,这一行与下一行将折叠在一起。

最后一行?

其实一直以来都有一个问题哈,就是最后一行,我们并没有分析过,但是让人开心的是,它最后也得出了正确的折叠等级,但是究竟是为什么呢?

  • 情况一:
1
2
3
4
5
a1
a2
b1
c1
a3

处理a3的时候,得到它的this_indent = 0,文件到这行已经结束了,那下一行呢?此时我们调用Next_noneblank_line()函数来获取下一个非空行行号时,函数发现下一行不存在,所以直接返回'-2',在获取下一行缩进等级的时候做了一个除法操作,所以得到next_indent = 0,于是this_indent = next_indent 了,就返回了'0'

  • 情况二:
1
2
3
4
a1
a2
b1
c1

现在c1作为最后一行,得到它的this_indent = 2,而下一行已经不存在了,所以会得到next_indent = 0,于是就有了next_indent < this_indent,此时会返回'2'

然后,我们在test.c中命令模式下执行下边的命令使折叠生效:

1
:source test.vim

接着会发现,代码折叠成了这样:

1
2
3
4
5
6
7
void Hello(void)
+-- 9 lines: {-----------------------------------------------------------
}

void testHello(void)
+-- 2 lines: {-----------------------------------------------------------
}

额,勉勉强强吧😂,折叠的目的达到了,但是由于花括号的独占一行,就是目前的样子了,后边还可以继续优化嘛😁。