vim中文件类型识别、语法高亮及缩进实现流程
一、文件类型
在使用vim编辑一个文件的时候,如果能够识别出文件的类型,加上对应的高亮规则,可以使文件的查看更加醒目,这个功能几乎是使用vim浏览文件的一个核心诉求。
另外,在进行文件编辑的时候,特别是使用vim写代码的时候(典型的场景是通过vim写C/C++代码),如果能够智能缩进,还可以减少敲代码。例如,在每行的开头自动添加缩进与前一行对齐;或者是当在输入注释时(前一行是//或者/*),通常默认的缩进也会在当前行前面加上注释(//或者*),这也是很多开源软件中看到/**/风格注释中每行开头都有一个*的原因。
前面说的高亮和缩进,需要先识别出文件类型,然后根据文件类型确定语法、高亮、缩进。所以如何识别出这个文件的类型就是整个便利性的基础。不过话说回来,这些功能对vim这种编辑器来说都不是必须的,它们都是为了让使用更加便捷,所以这些功能的实现很多是通过插件来完成。
二、vim的filetype命令
在vim执行filetype命令时,vim执行的函数为ex_filetype,其中比较关键的时,它会找到filetype.vim脚本文件并执行,这个filetype.vim文件名是在代码中写死的。
/*
* ":filetype [plugin] [indent] {on,off,detect}"
* on: Load the filetype.vim file to install autocommands for file types.
* off: Load the ftoff.vim file to remove all autocommands for file types.
* plugin on: load filetype.vim and ftplugin.vim
* plugin off: load ftplugof.vim
* indent on: load filetype.vim and indent.vim
* indent off: load indoff.vim
*/
static void
ex_filetype(exarg_T *eap)
{
……
if (STRCMP(arg, "on") == 0 || STRCMP(arg, "detect") == 0)
{
if (*arg == 'o' || !filetype_detect)
{
source_runtime((char_u *)FILETYPE_FILE, DIP_ALL);
filetype_detect = TRUE;
if (plugin)
{
source_runtime((char_u *)FTPLUGIN_FILE, DIP_ALL);
filetype_plugin = TRUE;
}
if (indent)
{
source_runtime((char_u *)INDENT_FILE, DIP_ALL);
filetype_indent = TRUE;
}
}
if (*arg == 'd')
{
(void)do_doautocmd((char_u *)"filetypedetect BufRead", TRUE, NULL);
do_modelines(0);
}
}
……
}
其中用到的一些宏
#ifndef FILETYPE_FILE
# define FILETYPE_FILE "filetype.vim"
#endif
#ifndef FTPLUGIN_FILE
# define FTPLUGIN_FILE "ftplugin.vim"
#endif
#ifndef INDENT_FILE
# define INDENT_FILE "indent.vim"
#endif
三、filetype.vim的主要内容
在该文件中,注册了对于打开文件,读取文件之类事件的自动处理函数。它们主要通过文件名、文件后缀之类的信息来猜测文件格式。
runtime\filetype.vim
" Shell scripts (sh, ksh, bash, bash2, csh); Allow .profile_foo etc.
" Gentoo ebuilds and Arch Linux PKGBUILDs are actually bash scripts
au BufNewFile,BufRead .bashrc*,bashrc,bash.bashrc,.bash[_-]profile*,.bash[_-]logout*,.bash[_-]aliases*,*.bash,*/{,.}bash[_-]completion{,.d,.sh}{,/*},*.ebuild,*.eclass,PKGBUILD* call dist#ft#SetFileTypeSH("bash")
au BufNewFile,BufRead .kshrc*,*.ksh call dist#ft#SetFileTypeSH("ksh")
au BufNewFile,BufRead */etc/profile,.profile*,*.sh,*.env call dist#ft#SetFileTypeSH(getline(1))
当然也有一部分是通过文件的前几行来判断/确认
" Shell script (Arch Linux) or PHP file (Drupal)
au BufNewFile,BufRead *.install
\ if getline(1) =~ '<?php' |
\ setf php |
\ else |
\ call dist#ft#SetFileTypeSH("bash") |
\ endif
还有一些脚本类型文件的判断,通过第一行中的"#!"来判断脚本类型的。这也意味着可以对于"#!"开头的文件,vim通常能很好的识别出来脚本类型。
在脚本类型scripts.vim文件检测中
let s:line1 = getline(1)
if s:line1 =~# "^#!"
" A script that starts with "#!".
" Check for a line like "#!/usr/bin/env VAR=val bash". Turn it into
" "#!/usr/bin/bash" to make matching easier.
if s:line1 =~# '^#!\s*\S*\<env\s'
let s:line1 = substitute(s:line1, '\S\+=\S\+', '', 'g')
let s:line1 = substitute(s:line1, '\<env\s\+', '', '')
endif
四、syntax on指令的执行
vim-8.1\src\syntax.c
static void
syn_cmd_onoff(exarg_T *eap, char *name)
{
char_u buf[100];
eap->nextcmd = check_nextcmd(eap->arg);
if (!eap->skip)
{
STRCPY(buf, "so ");
vim_snprintf((char *)buf + 3, sizeof(buf) - 3, SYNTAX_FNAME, name);
do_cmdline_cmd(buf);
}
}
其中高亮文件夹
#ifndef SYNTAX_FNAME
# define SYNTAX_FNAME "$VIMRUNTIME/syntax/%s.vim"
#endif
在syntax.vim脚本中,注册了FileType事件,也就是当设置了文件类型之后会执行的命令,这个命令会触发set syntax=XXX命令的执行,而该命令进而触发颜色的高亮。
vim-8.1\runtime\syntax\syntax.vim
" Load the Syntax autocommands and set the default methods for highlighting.
runtime syntax/synload.vim
……
" Set up the connection between FileType and Syntax autocommands.
" This makes the syntax automatically set when the file type is detected.
augroup syntaxset
au! FileType * exe "set syntax=" . expand("<amatch>")
augroup END
if (save_ei != NULL)
{
au_event_restore(save_ei);
apply_autocmds(EVENT_SYNTAX, curbuf->b_p_syn,
curbuf->b_fname, TRUE, curbuf);
}
五、缩进(indent)
当执行filetype indent on时,执行的命令为:
static void
ex_filetype(exarg_T *eap)
{
……
if (*arg == 'o' || !filetype_detect)
{
source_runtime((char_u *)FILETYPE_FILE, DIP_ALL);
filetype_detect = TRUE;
if (plugin)
{
source_runtime((char_u *)FTPLUGIN_FILE, DIP_ALL);
filetype_plugin = TRUE;
}
if (indent)
{
source_runtime((char_u *)INDENT_FILE, DIP_ALL);
filetype_indent = TRUE;
}
}
……
}
在indent.vim脚本中,也注册了对于文件类型设置的侦听。
runtime\indent.vim
augroup filetypeindent
au FileType * call s:LoadIndent()
func! s:LoadIndent()
六、通过set ft=XXXX触发的事件流
从这个流程上看,该动作执行之后,触发的事件为EVENT_FILETYPE自动事件,如果希望处理这个事件,可以注册对于该事件的自动命令。由于indent和syntax都注册了对这里抛出的FileType事件的侦听,所以当设置了文件类型,语法和缩进开启的情况下,它们会自动生效。
vim-8.1\src\option.c
static char_u *
did_set_string_option(
int opt_idx, /* index in options[] table */
char_u **varp, /* pointer to the option variable */
int new_value_alloced, /* new value was allocated */
char_u *oldval, /* previous value of the option */
char_u *errbuf, /* buffer for errors, or NULL */
int opt_flags) /* OPT_LOCAL and/or OPT_GLOBAL */
{
……
else if (varp == &(curbuf->b_p_ft))
{
/* 'filetype' is set, trigger the FileType autocommand.
* Skip this when called from a modeline and the filetype was
* already set to this value. */
if (!(opt_flags & OPT_MODELINE) || value_changed)
{
static int ft_recursive = 0;
++ft_recursive;
did_filetype = TRUE;
// Only pass TRUE for "force" when the value changed or not
// used recursively, to avoid endless recurrence.
apply_autocmds(EVENT_FILETYPE, curbuf->b_p_ft, curbuf->b_fname,
value_changed || ft_recursive == 1, curbuf);
--ft_recursive;
/* Just in case the old "curbuf" is now invalid. */
if (varp != &(curbuf->b_p_ft))
varp = NULL;
}
}
……
}