缩进::Vim进阶索引

http://blah.blogsome.com/2007/09/30/vim_tut_indent/


缩进::Vim进阶索引[8]

缩进可以使用文本结构更清晰易读。在Vi中,这通过是使用专用的外部程序(如:indent或c beautifier类的程序)实现的。Vim除保留了原有外部程序支持外更增加了一些内部的支持。包括了插入模式下的交互进行的缩进与'='指令的缩进操作。

1 基础知识

:h indent.txt
:h =
:h 'equalprg'
:h indentkeys

Vim中缩进有三种基本的使用方式。一是在普通(正常)模式下使用'='指令。可以圈选范围后使用可以在指令后加上移动的指令。使用的方式与其他编辑指令是一样的(比如'd')。'=='表示对当前行进行缩进。看下面的例子:

=ip
对当前段落缩进
=G
将由当前行至文章末尾的范围缩进
30==
缩进由当前行开始的30行

二是在编辑的过程(插入模式)使用某些键触发。比如,使用'autoindent'时,在插入模式中输入回车(即按回车键)时Vim自动对新行应用缩进规则。
三是粘贴文本时,使用']p'指令对粘贴文本强制运用缩进。详见::h ]p

此外,'gq'或ex命令':left'也能用来缩进文本。由于它们属于文本格式化的内容,这里不作讨论。

注意:当'equalprg'不为空时,'='/'gq'总是使用equalprg中设置的外部工具。除此之外其他的缩进操作不影响。

在equalprg中使用的外部程序通常是整理(过滤)文本的工具,很少是单独用于缩进的工具。此外,如果通过外部程序实现缩进,有一些缺点不可避免:

  • 使用上不方便。如果要实现交互方式的缩进(即边输入边根据输入实现缩进),要不断运行外部程序,运行效率低。通过cinkeys/indentkeys的设置Vim可以在输入时计算缩进。
  • 对大多数的一般应用而言,用户只需要最基本的缩进支持——如autoindent。而你却很难找到这样的程序。
  • 许多工具不跨平台。
  • 不够灵活。你其实不想为一些简单的缩进而写新的程序。

Vim应用缩进的过程如下:

  1. 依据设置使用缩进规则计算缩进宽度。

    在不同的缩进规则同时开启时只能有一个起作用。在所有开启的缩进项中只有优先级最高的起作用。它们的优先级排列如下:
    indentexpr > cindent > smartindent > ai

    缩进宽度:以一个半角字符的宽度为基本单位计算的总缩进的量。缩进时,Vim会在行首增加相应宽度的空格或制表符。举例而言,如果缩进宽度为4,则Vim在行首增加4个半角空格;如果缩进宽度为8,Vim在行首增加一个制表符。

  2. 删除目标行首的制表符与空格。
  3. 根据expandtab与tabstop的设置及缩进宽度添加相应的空格或制表符。

    制表符的宽度与'tabstop'的设置有关。默认值是8,所以8个半角空格(或其他字符)的宽度与一个制表符一样。如果将'tabstop'设为4,那么如果缩进宽度为9则Vim在行首增加2个制表符与1个半角空格。如果不想使用制表符可以:se noet

如果要实验各种缩进方式的话,建议定义如下的快捷键以便随时按<F9>查看缩进的设置:

map <F9> :se autoindent? smartindent? cindent? lisp? indentexpr? equalprg? paste? cpoptions?<CR>

有些选项,如'paste'会影响缩进,所以需要查看这个设置项的情况。各个设置项的情况可以见各自的文档。

2 预设规则

为了方便用户Vim提供了一些预置的缩进规则:自动缩进(autoindent)、智能缩进(smartindent)、c缩进(cindent)、lisp缩进(lisp)。

2.1 autoindent

autoindent的缩进规则是最简单的。它使用与上一行一样的缩进量。换言之如果你为当前行加了3个空格的缩进,则开始下一行时Vim会自动添加3个空格的缩进。写python脚本时,使用这种缩进就够了。

使用autoindent,只要开启相应的选项::se autoindent 或 :se ai

注意:indentexpr、lisp、cindent、smartindent中的任一项开启都会覆盖autoindent的设置。

2.2 smartindent,cindent

:h C-indenting
:h smartindent
:h cindent
:h cinkeys
:h cinwords
:h cinoptions
:h cinkeys-format

smartindent的缩进规则可应用于与c语法类似的语言如AWK、JavaScript等,当然也可以用在c语言。它的规则是将{}块内的语句缩进一定宽度。嵌套的{}块内的语句则相对于上一层语句缩进一定宽度。

cindent的缩进规则是专门用于c语言的缩进。与smartindent相比,cindent除了更严格地对应c语言的语法外,还增加了风格选项——为了适合不同的c语言风格,Vim提供了相当多的定置项改变cindent的缩进方式。设置项包括了:

cinkeys
这个选项定义了一组可以触发缩进的按键。在遇到这些按键是Vim会根据缩进规则重新计算当前行的缩进。定义按键的格式可以见:h cinkeys-format
cinwords
定义了一组让下一行相对对当前行增加缩进的关键字。在遇到定义在cinwords中的字时,Vim为接下来一行增加缩进。
cinoptions
缩进风格选项。参考::h cinoptions-value、

2.3 lisp

:h lisp
:h lispwords

根据lisp语法缩进,我懂得很少,所以——详见帮助。

3 进阶规则(indentexpr)

:h cpo
:h indentexpr
:h indentkeys

与折叠一样,缩进也支持使用表达式定义缩进。这个表达式可以是任意表示数值的表达式也可以是返回数值的自定义/内置函数,这个数值将做为缩进的宽度。也与折叠一样Vim使用v:lnum表示目标行的行号。其它常用的函数包括了indent()、getline()、prevnonblank()、nextnonblank()等等。与折叠不一样的是使用缩进表达式不用另外指定缩进方式,只要赋于indentexpr项一个值,就会覆盖autoindent或smartindent/cindent的设置。

Vim的缩进表达式要比折叠表达式直观得多。我们直接通过例子了解缩进表达式的使用。

3.1 简单缩进

先看几个简单的缩进表达式:

" 缩进宽度总为4
:se indentexpr=4
" 不使用表达式缩进
:se indentexpr=
	
" 将缩进宽度设为与&sw设置一致
:se inde=&shiftwidth
	
" 逐渐增加缩进
:se inde=v:lnum

这一组表达式还是比较容易理解的,都是直接将一某个数值(不需要什么计算)作为缩进量。 此外,三元条件表达式在折叠篇中也已经看了不少:

" 偶数行缩进4格
:se indentexpr=v:lnum%2?0:4
	
" 取消注释行的缩进
:se inde=getline(v:lnum)=~'^\\s*#'?0:indent(v:lnum)
	
" 行首缩进:
:se inde=(getline(v:lnum-1)=~'^\\s*$')?4:0
" 悬挂缩进:
:se inde=(getline(v:lnum-1)=~'\\S')?4:0
	
" 相对于上一行缩进行首带着-号的行
:se inde=getline(v:lnum)=~'^\\s*-'?indent(v:lnum-1)+4:0
	

这一小节的的最后一个例子是个常用到的缩进:根据编号缩进。

1. statement
1.1. substatement
2. statement
2.1. substatement
2.1.1. subsubstatement
2.2. substatement

在看需求文档时几乎每一行都是编号的。程序员从不同的渠道获得这些文档,可能是从某个需求管理系统,电子邮件或者SKYPE。它们有不一样的缩进,有一些甚至没缩进。自动缩进工具此时显得特别有用。

对于写需求文档的人来讲他们除了要能智能的缩进他们可能还需要一个可以自动编号(根据缩进或者行首的*字符的个数)的编辑器。当然Vim用户是不需要再花时间找这样的工具的!

要将这些编号可以用以下的脚本:

" 根据编号缩进
:se inde=len(split(substitute(getline(v:lnum),'^[\ \\t]*\\([0-9.]\\+\\).*','\\1',''),'\\.'))*&sw

将函数写成单行形式的最大挑战是要加上非常多的转义符。而且记住:使用单引号,不要用双引号。具体的原因See 附录的解释.

如果不想记转义规则可以用函数将它包装起来。当然,这样也就没办法在模式行中使用了:

func! GetIndent(lnum)
  let ind=len(split(substitute(
       \ getline(a:lnum),'^[ \t]*\([0-9.]\+\).*','\1',''),'\.'))
  return ind
endfunc
se inde=GetIndent(v:lnum)*&sw

3.2 indentkeys

在定义了缩进表达式后,我们可以在文本输入完成后使用'='或'=='进行缩进。如果要在编辑的过程中实时地缩进,我们需要定义合适的'indentkeys'。考虑下面的表达式:

" 将字串长小于20的句子右对齐
" 这条命令实际等价于:right 20
se inde=20-len(substitute(getline(v:lnum),'^[\\t\ ]\\+','',''))

因为默认的indentkeys中包含了o,O,所以在开启新行时,Vim就已经计算了缩进——但这时我们的输入还没完成,所以缩进宽度是错的。我们需要让Vim在句子输入完成后再计算缩进宽度。也就是在我们按下回车后先计算并应用缩进再插入换行符。同时还需要定义一个在插入模式中可以使用的缩进命令,以随时强制Vim计算缩进。就像所有Vim的其他功能一样,Vim也为这个功能提供了设置项,这次是indentkeys,

se indentkeys=*<CR>,!^F

*<CR>表示在插入模式下按回车键时,先重新计算缩进再加入换行符。如果只有<CR>则Vim会先加入换行符再计算缩进——这时新增行成了目标行。
!^F表示在插入模式下按Ctrl-F时,重新计算缩进但不插入字符。关于*和!在indentkeys(及cinkeys)中的意义可以见::h indentkeys-format

4 缩进进阶

在处理缩进时经常会遇到嵌套的格式文本,幸好它们都大同小异。考虑下面的文本嵌套结构:

[marker]
  block
  [marker]
    block
  [end marker]
[end marker]

这种类型的文件很常见xml(<xxx>block</xxx>),C代码({block}),opera书签文件(opera6.adr)等1

下面我们将一起为两个使用这种结构的文本的写缩进脚本。

4.1 例2

在一些情况下'marker'与'end marker'不那么明显。下面是一个文本目录树:

+ item1
- item2
  + subitem
  - subitem
    * subsubitem
  -
-
* item3

这里的marker是跟减号跟随文字,end marker则是一个减号(后面没有文字)。事实上将这个end marker改成一个空行,在处理上也不会有什么不同。2

但无论是哪种形式只是对marker的判别方式有一些区别,其结构并无本质区别。

状态及对应的处理方式;

  • 当前条目如果只有一个减号(^\s*-\s*$),则当前条目相对上一条目减少缩进量。
  • 上一条加号或星号开始(^\s*[+*]) 当前条目与上一条目的缩进一样
  • 上一条如果只有一个减号(^\s*-\s*$),则当前条目减少缩进量。
  • 上一条如果由一个减号开始(^\s*-\s*\S\+$),则当前条目增加缩进量。

这样脚本就很清楚了,

func! MyIndent(lnum)
  let lastline=getline(a:lnum-1)
  if a:lnum==1 | return 0 | endif
	
  if getline(a:lnum)=~'^\s*-\s*$'
    let diff=-1
  elseif lastline=~'^\s*[*+]'
    let diff=0
  elseif lastline=~'^\s*-\s*$'
    let diff=-1
  elseif lastline=~'^\s*-\s*\S'
    let diff=1
  endif 
	
  return indent(a:lnum-1)+diff*&sw
endfunc
	
se inde=MyIndent(v:lnum)

4.2 例2

最后是一个完整的例子仍是嵌套的文本块,看一下下面的文本,

[
outer block
[[
inner block
]
]
[
[inner block]
]
]

这个仍然是marker与end marker的格式。[是marker,]是end marker。我们要写一个使之能正确缩进的脚本,其中的关键在于判断嵌套的深度来决定缩进宽度。我们可以使用一个buffer变量保存嵌套深度,遇到[增加,遇到]减少深度。但因为=命令可以多次不连续地对不同文本块使用,所有变量的存在可能会导致不正常的结果。为此,仍像前面的例子一样我们将根据前一行的状态判断缩进深度。根据上一行的marker,计算当前行的缩进宽度。描述如下,

  • 如果上一行是[,当前行增加宽度
  • 如果上一行是],当前行减少宽度
  • 否则,保持上一行的宽度

还要考虑到一点,同一行可能数量不等的多个]或[——事实上这是这个例子与上一个例子唯一的不同之处。因为一对[]的缩进刚好可以抵消,我们可以通过它们的差决定缩进的宽度。另外,如果当前行有[或]还要相应增加或减少当前行的缩进。所以改进后的描述如下,

  • 将上一行[的数量减去]的数量,得到初始的缩进宽度
    • 结果为正,则为当前行增加相应数量的缩进
    • 否则,为当前行减少相应数量的缩进
  • 在前面计算的基础上计算当前行的[与]的差,得到缩进的增量。
    • 结果为正,则为当前行增加相应数量的缩进
    • 否则,为当前行减少相应数量的缩进

现在我们可以写脚本了,

" 其中,根据[]数量计算宽度这一段是重复的,
" 我们可以写成一个单独的函数
	
func! IndentSum(lnum,incre)
" 两个参数分别表示目标行行号与缩进的初始量
    let line=getline(a:lnum)
    " 通过'['与']'的数量计算缩进宽度
    " 每多一个[则增加一个单位的缩进
    " 每多一个]则减少一个单位的缩进
    " 没有]或[的行使用与上一行一样的缩进
    let in=len(split('x'.line.'x','['))-1
    let ou=len(split('x'.line.'x',']'))-1
    " [的数量减去]的数量
    return &sw*(in-ou)+a:incre
endfunc
	
func! BIndent(lnum)
    " 上一非空行的行号
    let llnum=prevnonblank(a:lnum-1)
    if llnum==0 | return 0 | endif
    " 由上一行得到初始的缩进宽度
    let ind=IndentSum(llnum,indent(llnum))
    " 计算当前行的的缩进增量
    let ind=IndentSum(a:lnum,ind)
    return ind
endfunc
	
se inde=BIndent(v:lnum)

现在,你已经可以写c缩进的脚本了(将上面脚本中的[]换成{} :) )。这种缩进的计算方式几乎是一个套路了。基于同样的模式,同样的工作流程的一个xml的缩进的例子可以见Vim安装目录中indent/xml.vim。

5 进阶提示

这一章是关于缩进的一些零散的内容。

5.1 去除缩进

:h g@
:h operatorfunc

现在你可以用'='进行缩进了你可能还需要一个可以去除缩进的指令。当然你可以用'<<',但这个命令一次只缩进一层。很遗憾你并不能使用'4<<'将文本向左移4次(这条命令将4行文本往左移一次),你只能一次一次来(或者按1次<<,再按3次.)。如果你确实需要一次去除许多缩进的话,可以使用下面的map宏:

:nnoremap <<< :left<CR>
:vnoremap <<< :left<CR>

这个宏有两个主要缺点,一是会使<<命令变慢,因为Vim要等等看后面是否还有一个<。这可以通过减小设置项'timeoutlen'的值,减少等待时间,但你的操作也要相应变快才行。或者另外定义一个按键序列,不使用<<<。二这条命令不支持对象选择。这不算是个很大的缺点,但如果支持的话显然会更方便一点。我们可以用g@包装上命令,使之具有对象选择的功能:

func! Deindent(dummy)
  exe 'normal! ' . "'[V']:left\<CR>"
endfunc
se opfunc=Deindent
	
nnoremap <<< g@
vnoremap <<< g@

现在试一下<<<G, <<<aB或<<<ip。g@的用法见Vim文档。

5.2 缩进与格式化选项

:h gq
:h formatoptions
:h formatprg

缩进与文格式化(gq)紧密相关,你可能会有兴趣看一下这一部分的内容。

5.3 缩进与折叠

在折叠篇我们知道可以以缩进作为折叠的规则。因此这实际给我们一种同时定义缩进与折叠的方式。在学了这一篇之后这个折叠的缩进规则就能派上用场了!

:se fdm=indent

Appendix A 表达式与沙箱


直接写表达式跟将表达式包装在一个函数中有一个最主要的不同是,前一种方式中的\及空格需要进行转义。所以在模式行中要使用很多的\。另外要注意的是"与'是不一样的。

:h expr-quote
:h expr-'

在写Vim脚本或命令时"的字串是允许使用转义字符的,而'的字串则不进行转义。如"\t"表示的是一个制表符而'\t'表示的是一个斜杠和一个字母t。'\t'等价的双字号字串是"\\t"。
在脚本篇我们就讲过了一个例子:

echo '|\t|'
echo "|\\t|'

但对于indentexpr及其它在沙箱中计算表达式的设置项来讲,数值表达式在进入沙箱中先进行了一次表达式的计算——只是计算字串值。例如,当你执行:se inde=len('abc\\t')时,先计算字串的“安全值”。即在计算函数的值之前,Vim会先计算字串表达式的值,所以函数现在成了,

"len('abc\\t')"

这个字串表达式的值,大家都知道是(通过:echo &inde可以观察字串表达式的值):

len('abc\t')

然后,再计算函数的值,结果是4(3个字母加一个制表符)。

在执行=命令时,Vim首先计算了字串表达式的值,再eval字串的值(即执行len('abc')并返回数值)。

注意,在写表达式时你并不需要在前后加上引号,Vim会自动为你加上双引号并进行计算字串的值。这个过程中Vim还进行了一些处理以确保值是“安全的”。其中包括移掉未转义的\。可以简单的记为这些表达式中不能有未转义的双引号",空格和斜杠。还是例子比较实在,

命令字串表达式字串值
:se inde=len('\\t')"len('\\t')"len('\t')
:se inde=len('\t')"len('\t')"len('t')未转义斜杠会被忽略掉
:se inde=len(' abc')空格未被转义,不合法表达式
:se inde=len('ab"c')"len('ab"c')"len('ab双引号"未被转义,所中间的"后的字串被省略
:se inde=len(\"\\t\")"len(\"\\t\")"len("\t")

正因为在计算len()之前已经先计算了字串值一次,所以本来,

func! GetIndent(lnum)
	return len(split(substitute(getline(v:lnum),'^[ \t]*\([0-9.]\+\).*','\1',''),'\.'))*&sw
endfunc
se inde=GetIndent(v:lnum)

不用转义的表达式,直接放到:se inde命令后,就成了:

:se inde=len(split(substitute(getline(v:lnum),'^[\ \\t]*\\([0-9.]\\+\\).*','\\1',''),'\\.'))*&sw

可以看到多了一堆的反斜杠('\')。不与这些转义规则打交道的方法是尽量将它们包装在独立的函数中(这样不用使用沙箱)。如果一定要用的话尽量少用空格与双引号。


Footnotes

[1] 事实上几乎所有的嵌套结构都是这样的

[2] 但有个'-'号会比空行直观一点。

Comments »

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值