碎碎念

2007, September 30

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

Filed under: vim, linux, vi, 编辑器, 教程


Up: (dir)

缩进::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] 但有个'-'号会比空行直观一点。

2006, June 14

高亮::Vim进阶索引[5]

Filed under: vim, linux, vi, 编辑器

Vim进阶索引[5]::高亮

与以前的教程相比,这一篇做了一些小改变:使用了缩写而不是每次都给出完整的命令。提示使用文档时不同给出完整的命令而是给出“关键字”——你可以用“:h 关键字”的方式找到Vim文档中的相应内容。

hq00e

在语法高亮相信大家在使用Vim的文档时就已经见过了,Vim提供了一个测试用的脚本。输入下面的命令:

   :e $VIMRUNTIME/syntax/colortest.vim
   :so %

这个脚本除了让你知道Vim是可以显示许多颜色的外,它还是一个实时定义颜色的脚本实例。在本文的末尾我们会用同样的技巧生成一个Web色表。

Vim中与色彩有关的设定大致可以分为两部分:

  • Vim编辑环境配色。如Vim编辑区的前景背景颜色(文本的默认颜色)、状态栏颜色、错误提示颜色、光标、圈选区(可视区)、行号、折叠的颜色等。这些都属于编辑器的环境设定。
  • 文件使用的所使用的色彩。即所谓的语法高亮/语法着色,如Vim文档中索引标签使用粉红色,示例使用蓝色、链接使用青色。或是当你打开C语言文件时,注释蓝色、数据类型青色、字串粉红、关键字赭色。都是Vim根据文件类型启用相应的语法文件,对该应用颜色的部分进行识别并着色。

本文将通过对这些色彩进行设置的实例,让用户逐步了解与色彩相关的命令。


1 环境配色

Vim的环境配色决定了用户编辑环境的基本风格。语法高亮可以关闭但环境配色是始终存在的,下面我们将逐步地改造并定义属于自已的配色风格。


1.1 使用预置的配色风格

要改变Vim的编辑环境的配色很容易因为Vim提供了多种预置的配色风格(颜色主题)。比如要使用'evening'风格:在菜单中选择“编辑-调色板-evening”。或者在命令行中使用:colorscheme命令1

:colors evening

可以看到编辑环境包括编辑区的前景背景的颜色都发生了变化。命令行下Vim默认是黑底白字,图形下的Vim是白底黑字,在应用了上面的命令后都成了深灰的背景,银灰的前景。

提示:在菜单中选择“default”或使用命令:colors default,换回默认的配色。关键字::colorscheme


1.2 修改环境配色

如果你不满足于使用现成的颜色主题的话,那我们来看一下如何修改环境配色。首先要掌握的命令是:highlight。

" 使用默认的高亮(移除用户定义的高亮,即根据'ft'重新载入语法文件)
:hi clear 
	
" guifg表示图型介面(gui)下的前景色(ForeGround)
" guibg表示图型介面(gui)下的背景色(BackGround)
:hi {组} guifg={值} guibg={值}
	
" 进行颜色关联。
" 这条命令使{组1}使用与{组2}一样的颜色设置
:hi link {组1} {组2}

例如,要将“组”为“c_name”的组在图形介面下的颜色设置为背景黑色,前景灰色可用如下命令:

:hi c_name guifg=gray guibg=black

让组“c_blah”使用与“c_name”一样的颜色设置:

:hi link c_blah c_name

注意:目前我们只涉及图形介面下Vim(gVim)的色彩控制。所以本文接下来的例子,在我们讲到命令行下的颜色设置之前,所有例子都是在gVim环境中完成的。

忘了解释一下“组”(高亮组)是什么?有什么用?Vim中“组”被用来表示一组颜色设置(前景、背景、字体、风格)。当某个介面元素(或是编辑区中的文本)应用了特定的“组”后,它就根据“组”表示的颜色设置来显示。应用了同样“组”的介面元素或文本会有一样的颜色显示。有:hi为“组”分配颜色时,如果组已存在则覆盖原有的设置,否则定义新组。

现在我们知道通过赋于guifg和guibg颜色值为组分配颜色。哪到底可以使用哪些颜色呢?图形介面下的颜色有“名称”或“数值”两种表示方式。名称如上面所用的gray、black还有大家熟悉的red、white、yellow,更多可以使用的颜色名称见文档:gui-colors。颜色还可以用“数值”来表示,方法是用三个分别表示“红、黄、蓝”的十六进制数值表示。如red还可以表示为#ff0000。记得数值前要加上#号。没错,这与html文件中颜色的表示方法是一样的。用这种方法我们可以表示更多的颜色:

" 将前景改为淡紫色
:hi c_name guifg=#E6E6FA

提示:此外guifg/guibg还支持三个特殊的值:none、fg、bg,分别表示无颜色、编辑区一般文本的前景色、一般文本的背景色。

要改变Vim的配色我们需要知道都有哪些组可以改。下面表中是Vim环境配色中介面元素所应用的“组”(组名在前):

Cursor
光标
ErrorMsg
命令行中的错误提示
Folded
折叠行
LineNr
行号
NonText
非文本区(控制字符和一些特殊字符和编辑器空白区等)
Normal
编辑区一般文本的前景和背景色
Search
搜索
StatusLine
状态行
Visual
圈选区

提示:完整的列表见highlight-groups。非Windows用户还可以设置菜单、滚动条和提示框的颜色。见hl-menu。

现在做个实验,打开一个文档并依次输入下面的命令,观察变化:

:se ft= "关闭高亮
" 分别改变编辑区的前景色(guifg)和背景色(guibg)为灰色和红色
:hi Normal guifg=gray guibg=red
" 修改背景色为暗灰色,上面设置的前景色将被保留
:hi Normal guibg=#333333
	
"显示状态行
:se laststatus=2
" 设置状态行的颜色。
" 如果没“gui=none”会发现状态行的前/背景色颠倒了。
" 关于gui我们稍后再说
:hi Statusline guifg=green guibg=gray gui=None
" 状态栏的默认颜色
:hi statusline gui=bold,inverse guifg=fg guibg=bg
" 使错误提示使用与状态栏一样的颜色设置(默认是红色)
" 用hi link对已定义颜色的组重新定义颜色要加“!”
:hi! link ErrorMsg statusline
" 下面的命令会出错,错误提示成了绿色的
:hi link
" 清除颜色。
:hi! link ErrorMsg none
" 无颜色的错误提示
:echoerr "abc"
" 恢复默认的颜色
:hi clear

注:“:hi link {组} NONE”是:hi link的一种用法,用来清除组的颜色关联。


2 语法高亮

前面我们讲了如何更改Vim环境配色,如光标,状态行,错误提示的颜色。并没有涉及到如何根据编辑的文件来显示不同颜色,即语法高亮。与更改配色相比设置语法高亮要更复杂一些:配色中更改的组是确定的,因为编辑器中的介面元素是固定的,而语法高亮中所打开文件中那些需要高亮那些不需要,以及对不同类型的文件应用不同的语法高亮都要视具体的文件而定。很多时候我们还需要“创造”(自定义)出一些组来。但与配色一样在语法高亮中颜色的显示依然是由:hi命令控制。


2.1 修改当前的高亮设置

这一节我们将对当前文本中的语法高亮进行修改,你会发现这与修改环境配色相似——所不同的只是“组”名。这是一组实验:

:h syntax.txt "打开Vim文档
:hi helpHyperTextJump guifg=darkblue "改变文档中链接的颜色
:hi clear
"清除自定义颜色——包括上面的颜色
:hi! link helpHyperTextJump Identifier "恢复颜色

所以要定义颜色只要对相应的组名的颜色进行设置就行了。那你要问了,这个helpHyperTextJump是从哪来的我怎么会知道哪个的组名是哪个?如果我要定义自已的组名呢?
要查看当前的语法文件中定义了哪些组名可以用不带参数的:hi查看。此外,还可以用:

" 查看当前的文件类型(假设是texinfo文件)。
:se ft
texinfo
" 知道是texinfo文件后,使用以下命令打开相应的语法文件
" 在语法文件中,:syn命令后跟的就是组名。
:e $vimruntime/syntax/texinfo.vim

那用户该怎么定义自已的组呢?继续往下看吧……


2.2 定义新的高亮

在定义新的高亮组时我们要先回答这个问题:为什么要定义新的组呢?有很多可能的原因:其中之一是我们想在不修改原来的高亮的情况下增加一种色彩,为些我们需要定义新的高亮组,并分配适当的颜色。
现在我们要定义自已的组了,首先要为我们自定义的组取个名字,组的命名与变量一样只能由字符下划线和数字组成(虽然我们前面的例子中使用了不同的大小写,但组名是不区分大小写的)。下面的命令中我们定义了一个组名为“mygroup”的组2
:hi mygroup guifg=#ff9999
这条命令告诉Vim将mygroup组的字串颜色定义为淡红色(lightred)。但Vim现在还不知道哪些字串属于mygroup,所以我们得告诉Vim——方法是使用:match命令:
:match mygroup /xxx/
这条命令告诉Vim凡匹配式样的xxx的字串都属于mygroup。这样当前文件中所有匹配“xxx”的字串都会变成淡红色。定义自己的颜色是不是很容易呢?先用:hi命令定义组及其使用的颜色。再用:match告诉Vim编辑区文本中哪些部分是属于自定义组的。最后,Vim会根据:match设置的规则将当前编辑区文本分为许多不同的组(如果有定义多个组的话),并对不同的组应用:hi为其分配的颜色

下面我们要用一个更实际的例子来加深对语法高亮的印象。

看一下这个简单的表格:

王小明	数学	46
李阿月	数学	72
林小丽	数学	91

这是某个班主任手中的成绩单。他/她的班级有25个学生,这是其中的三条数据。这个班级经常有考试,这个老师希望考试的结果更直接明了一点:不及格(少于60分)的成绩显示为红色,90分以上的成绩有显示为青色,这样他/她就可以很快知道哪些学生该补课3,而哪些学生该表扬。当科目为数学时将科目显示为蓝色,这个班主任教数学的!学生名字显示为粉红色——看来这个班主任是女的。最后全班最高分的颜色反白显示。
我们先取几个不同的组名4:“u_student、u_subject、u_mark_fail、u_mark_a”分别表示“学生、科目、不及格、优秀”等。将这个表格另存为文本文件并用gVim打开,使用下面的命令:

:hi u_student guifg=#ff9999 guibg=white
:hi u_subject guifg=lightblue guibg=white
:hi u_mark_fail guifg=red guibg=white
:hi u_mark_a guifg=darkcyan guibg=white
" 上面的命令定义了不同的组及其对应的颜色
" 现在我们要用match告诉Vim怎么分辨不同的组,
" 我们要用到一些的正则表达式
	
"行头开始至第一个空白字符
:syn match u_student /^\S*/
:syn match u_subject /数学/
:syn match u_mark_fail /\s[1-5]\=.$/
:syn match u_mark_a /\s100\|\s9.$/

你可能发现了每条match命令前面都多了:syn。这是因为使用match命令时前一个match定义的组的颜色会丢失。用match命令你没办法同时显示多种颜色。在上面的match命令前加上syn就行了,就可以显示所有自定义的颜色了。

提示:其实:syntax match与:match是不同的命令,不过“目前为止”它们的语法是一样的,我们在下一篇会讲到:syntax命令。

现在我们为成绩单加上颜色了,但下一次呢?我们可不希望每次打开都手动设置,我们可以将之放进单独文件中。将上面的命令复制到单独的文件中,然后用:so命令运行就可以了。


2.3 写语法文件

我们在前面写的脚本,保存起来就成了一语法文件。我们使用:so命令就可以运行了,不过你也许还希望它像其他语法文件一样能自动加载。要做到这一点也很简单。

在我们继续之前我简单描述一下语法文件加载的机制。Vim读入/新建文件时根据后缀名判断文件类型(或者根据模式行中设置的'filetype'设置项判断文件类型),然后在$VIMRUNTIME/syntax/和$VIM/vimfiles/syntax/中查找以文件类型为文件名,.vim为后缀的文件。找到的话加载该文件。

所以语法文件我们已经有了,我们只需要再选择合适的文件类型名。假设我们使用的文件类型名是'u_mark',将上面的脚本命名为u_mark.vim放到这个目录中:$VIM/vimfiles/syntax/。然后要让Vim打开成绩单时知道自动应用语法文件。在学习autocmd和filetype的内容之前,在这里我们可以简单地使用模式行,来达到这个目的。在成绩单文件的末尾加入模式行:

   vim:ft=u_mark

现在打开成绩单文件时Vim就会自动加载语法文件。如果打开成绩单时还是没出现语法高亮请确定已经开启了语法高亮。使用:syn on开启高亮,必要的话将之放到.vimrc中。


3 :highlight命令详解

现在是对:hi进一步挖掘的时候了。

3.1 命令行下的颜色设置

在前面的内容中,我们讲:hi命令时一直都是以图形介面(gui)为例设置前景和背景色。由于命令终端对颜色显示的限制,Vim在命令行下可以使用的颜色相对gui要少得多,所以使用:hi命令时图形介面和命令行介面的颜色是分开设置的。对于黑白终端来说就无所谓颜色了,而彩色终端用cterm来表示,前景色就是“ctermfg”,而背景色是“ctermbg”。下面是一个表格:

终端类型    前景色      背景色      注释
term         -          -           黑白终端
cterm       ctermfg     ctermgb     彩色终端
gui         guifg       guibg       图形介面

在前面我们对编辑区文本的颜色进行了定义:
:hi Normal guifg=gray guibg=red
现在我们对其命令行下的颜色进行定义
:hi Normal ctermfg=gray ctermbg=red
我们可以简单地写成一行:
:hi Normal guifg=gray guibg=red ctermfg=gray ctermbg=red

有哪些颜色可以使用?见cterm-colors。


3.2 显示样式

:hi命令除控制颜色外还可以控制文字的显示样式。term、cterm和gui分别控制三种不同终端下的字体式样。这些字体样式包括了粗体、下划线、斜体、反显。使用多种样式时将样式用逗号隔开。详细样式见attr-list。

" 设置错误提示在不同终端下的显示样式
:hi ErrorMsg term=bold,reverse cterm=bold,reverse gui=reverse 
	
" 将某项的值设为NONE,可清除该项的样式设定
:hi ErrorMsg term=NONE

需要注意的是gui下不支持粗体的样式,但gui下多了一个字体的设置项font,用以指定字体::hi tung_poem font=……

另外因为Normal组是做为Vim的基准设定,所以对Normal进行的字体样式设定将被忽略。


3.3 关于link

在使用:hi link命令时有几个细节要注意一下。文档中都有(hi-link),这里简单提一下。仍是以命令“:hi link {组1} {组2}”为例:

  • 如果在关联之前{组1}组已经定义过了了,则要使用加!号的形式:hi! link否则提示错误。
  • 当{组1}关联到{组2}后,{组1}组使用与{组2}一样的颜色设置。如果此时再用:hi对{组1}定义颜色,则关联被取消。{组1}回复到设置关联前的颜色设置(如果有的话),再应用新定义的颜色。
  • default开关项。由于Vim有多个配置文件,又有语法文件定义颜色。所以一组颜色可能被多次定义,为了让某一组颜色只在未定义时关联到其他组。可以使用开关项default:
         :hi default link {组1} {组2}
    

    一般而言,后定义(关联)的颜色总是覆盖先定义(关联)的颜色。在使用了这个开关项时,设置了default开关的总是被覆盖。仅当其他地方未定义{组1}时,才使用该关联。这主要是用在语法文件中,语法文件的加载要晚于配置文件。当在配置文件中定义颜色时,由于语法文件较晚加载自定义的颜色总是被覆盖。如果在语法文件中使用了default,则配置文件中自定义的颜色就能被显示出来。

使用:hi link还能节省大量的时间,减少重复的劳动。完整的颜色定义通常较长,因为要兼顾各种终端的显示能力。这是Vim中对Comment组的颜色定义:

  :hi Comment	term=bold cterm=NONE ctermfg=Cyan ctermbg=NONE gui=NONE guifg=#80a0ff guibg=NONE

如果每定义一种颜色都要写这么长就太折腾人了。避免这种情况的最好方法是利用好预定义的颜色。这些是Vim预定义的组、所表示的意义及其使用的颜色与样式:

Comment     注释
Constant    常量
Identifier    变量名
Statement    语句
PreProc    预处理器
Type    数据类型
Special     特殊符号
Underlined    突出显示的文本
Ignore    无设置
Error    语法错误
Todo    待做事项

Vim的语法文件本身就大量地使用了:hi link命令,它们大部就是关联到上面所列的组。在上面成绩单的例子中我们定义了u_subject在gui下的颜色为蓝色,但使用下面这条命令我们除了达到同样的效果外,还定义在其他终端下的颜色:

:hi! link u_subject Comment

所以在定义新的颜色时,先在上面的列表中看一看有没有你需要的颜色,如果有的话又可以节省很多时间了。


4 综合

现在看一下与语法高亮相关的几个例子。

4.1 高亮tags

见文档:tag-highlight


4.2 生成web色索引

使用:hi命令和:syn match命令就可以让Vim显示出斑斓的色彩。现在我们要更进一步结合上面的两个命令与Vim脚本写出一个217色的Web安全色表。

" 生成Web色表
" 用法:so web_color_gen.vim
" 限制:只能在gui中使用
" 注意:这个脚本在Vim6.3/6.4中有时会出现颜色渲染错误的情况。
"       在Vim7中则没发现类似情况。
	
" 关闭搜索高亮
se nohls
	
" 定义数组
" 在Vim7中定义数组就不会这么累了
let c0="00"
let c1="33"
let c2="66"
let c3="99"
let c4="cc"
let c5="ff"
	
" 生成web色的数值表
let L1=0
while L1<6
  let L2=0
  while L2<6
    let L3=0
    while L3<6
       exec "norm o\<ESC>" . ':s/^/\=c{L1}.c{L2}.c{L3}." "/' . "\<CR>"
      let L3=L3+1
    endw
    let L2=L2+1
  endw
  let L1=L1+1
endw
	
g/./exec 'hi '.expand("<cword>").' guifg=grey guibg=#'.expand("<cword>") |
    \ exec 'syn match '.expand("<cword>").' /'.expand("<cword>").' /'
	

新建空文档,然后运行脚本就可以看到web色表了。

这里有几个地方我解释一下。
exec "norm o\<ESC>" . ':s/^/\=c{L1}.c{L2}.c{L3}." "/' . "\<CR>"
这条命令在文档中新起一行,并用:s命令插入颜色值。关于:s命令的rhs中使用\=在“寄存器”篇中我们已经讲过了(见:sub-replace-special)。当L1、L2、L3分别为1、2、3时,这条命令就成了:
exec "norm o\<ESC>" . ':s/^/\=c1.c2.c3." "/' . "\<CR>"
其结果就是在当前行下插入了“336699 ”。在这三组循环运行完后文档区将会有如下的web色数值表:

000000
000033
...
ffffff

还有就是expand()。这个函数的作用是将一些特殊的符号扩展为该符号所表示的字串。expand("<cword>")将返回当前光标所在位置的“词”。常见的用法还有expand("%")、expand("<sfile>")等。篇幅所限,关于这个函数的用法见*expand()*。

g/./exec 'hi '.expand("<cword>").' guifg=grey guibg=#'.expand("<cword>") ……

g/./表示对所有非空行执行命令。假设当前行在“336699 ”,对这行执行“exec 'hi…. .expand("<cword>")”命令时,expand("<cword>")将被扩展为“336699”,这样命令就成了:

hi 336699 guifg=grey guibg=336699

同样的后面的:syn命令就成了syn match 336699 /336699 /。在运行完这组命令后Vim就会刷新屏幕上的颜色了。

提示:这段代码只有在图形介面下能发挥功用,在脚本开头加入这段代码以检测运行环境:

if !has("gui")
    finish
endif

另外,在脚本末尾添加下面代码可以使用web色表更易读一点:

1d " 删除空行
" 格式化颜色表,每行六种颜色。
g/./norm 6gJ

这是运行结果(部分):

660000 660033 660066 660099 6600cc 6600ff
663300 663333 663366 663399 6633cc 6633ff
666600 666633 666666 666699 6666cc 6666ff
669900 669933 669966 669999 6699cc 6699ff
66cc00 66cc33 66cc66 66cc99 66cccc 66ccff


4.3 其他应用

Vim定义的高亮还可以用以生成彩色的html文档或打印彩色文档。

  • 要生成html文档,只要先开启高亮然后简单的输入:TOhtml命令就可以生成使用与当前颜色设置一样的html文档了。TOhtml其实是一个插件,除了简单的用法,它其实还支持许多高级的控制选项如编码,CSS等。这是非常值得用户花点时间了解的命令——关键字“:TOhtml”。
  • :hardcopy命令会根据当前的色彩设定打印文档。此外在Linux/Unix中这个命令还可以用来生成PostScript文档。关键字“:hardcopy”。


5 小结

至此对于给定的组我们已经能决定它颜色的显示了。然而,依赖:syn match加正则表达式的方式定义组仍有局限——它不能针对组与组之间的关系作出调整,注释的嵌套便是一例。这解决这些问题或者说要定义有更复杂规则的组我们需要对:syntax命令有进一步的了解。下一篇我们将深入高亮的另一重要命令:syntax并,定义更复杂的语法文件。




Footnotes

[1] 其实Vim中的颜色文件,配色文件,语法文件,配置文件和插件本质上都是脚本文件都可以用ru或so运行。colors命令可以认为是预设了目录的so命令。

[2] 这里用的命令格式与上面修改高亮时的命令格式是一样的,它们的唯一区别在于使用的是否是新的组名。如前所述,Vim并不知道它是修改已有组的颜色设置,还是定义了新的组。要检查特定的组是否已存在可以用:hi mygroup,如出现错误提示则说明在“当前应用的语法文件”中不存在组mygroup

[3] 在我读书的会儿老师会要求我们把错的题目抄800-1000遍

[4] 没错,你可以按自己的意愿选择组名

Get free blog up and running in minutes with Blogsome
Theme designed by Jay of onefinejay.com