" Vim syntax file generator
" Language:		 Vim script
" Maintainer:	 Hirohito Higashi (h_east)
" Last Change: 2026 Aug 02

let s:keepcpo= &cpo
set cpo&vim

language C
let s:log_write_dir = getcwd() . '/'

function s:parse_vim_option(opt, missing_opt, term_out_code)
	try
		let file_name = $VIM_SRCDIR . '/optiondefs.h'
		let item = {}

		new
		exec 'read ' . file_name
		norm! gg
		exec '/^.*\s*options\[\]\s*=\s*$/+1;/^\s*#\s*define\s*p_term(/-1yank a'
		exec '/^#define\s\+p_term(/+1;/^};$/-1yank b'
		%delete _

		put a
		" workaround for 'shortname'
		g/^#\s*ifdef\s*SHORT_FNAME\>/j
		g/^#/d
		g/^\s*{\s*"\w\+"\%(\s*,\s*[^,]*\)\{2}[^,]$/j
		g/^\s*{\s*"\w\+"\s*,.*$/j
		g!/^\s*{\s*"\w\+"\s*,.*$/d

		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*{\s*"\(\w\+\)"\s*,\s*\%("\(\w\+\)"\|NULL\)\s*,\s*\%([^,]*\(P_BOOL\)[^,]*\|[^,]*\)\s*,\s*\([^,]*NULL\)\?.*')
			if list[1] == 'completefuzzycollect'
				continue
			endif
			let item.name = list[1]
			let item.short_name = list[2]
			let item.is_bool = empty(list[3]) ? 0 : 1
			if empty(list[4])
				call add(a:opt, copy(item))
			else
				call add(a:missing_opt, copy(item))
			endif
		endfor
		if empty(a:opt)
			throw 'opt is empty'
		endif
		if empty(a:missing_opt)
			throw 'missing_opt is empty'
		endif

		%delete _
		put b
		g!/^\s*p_term(\s*"\w\+"\s*,.*$/d

		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*p_term(\s*"\(\w\+\)"\s*,')
			let item.name = list[1]
			call add(a:term_out_code, copy(item))
		endfor
		quit!
		if empty(a:term_out_code)
			throw 'term_out_code is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

function s:append_syn_vimopt(lnum, str_info, opt_list, prefix, bool_only)
	let ret_lnum = a:lnum
	let str = a:str_info.start

	for o in a:opt_list
		if !a:bool_only || o.is_bool
			if !empty(o.short_name)
				let str .= ' ' . a:prefix . o.short_name
			endif
			let str .= ' ' . a:prefix . o.name
			if len(str) > s:line_break_len
				if !empty(a:str_info.end)
					let str .= ' ' . a:str_info.end
				endif
				call append(ret_lnum, str)
				let str = a:str_info.start
				let ret_lnum += 1
			endif
		endif
	endfor
	if str !=# a:str_info.start
		if !empty(a:str_info.end)
			let str .= ' ' . a:str_info.end
		endif
		call append(ret_lnum, str)
		let ret_lnum += 1
	endif
	return ret_lnum
endfunc

" ------------------------------------------------------------------------------
function s:parse_vim_command(cmd)
	try
		let file_name = $VIM_SRCDIR . '/ex_cmds.h'
		let item = {}

		new
		exec 'read ' . file_name
		norm! gg
		exec '/^}\?\s*cmdnames\[\]\s*=\s*$/+1;/^};/-1yank'
		%delete _
		put
		/EXCMD(CMD_block,/s/\s\+\/\/.*//
		g/EXCMD(CMD_/,/),/j
		g!/^EXCMD(/d


		let lcmd = {}
		for key in range(char2nr('a'), char2nr('z'))
			let lcmd[nr2char(key)] = []
		endfor
		let lcmd['~'] = []

		for line in getline(1, line('$'))
			let list = matchlist(line, '^EXCMD(\w\+\s*,\s*"\(\a\w*\)"\s*,')
			if !empty(list)
				" Small ascii character or other.
				let key = (list[1][:0] =~# '\l') ? list[1][:0] : '~'
				let item.name = list[1]
				let item.flags = {}
				let item.flags.range = match(line, 'EX_RANGE') >= 0
				call add(lcmd[key], copy(item))
			endif
		endfor
		quit!

		for key in sort(keys(lcmd))
			for my in range(len(lcmd[key]))
				let omit_idx = 0
				if my > 0
					let omit_idx = (key =~# '\l') ? 1 : 0
					for idx in range(1, strlen(lcmd[key][my].name))
						let matched = 0
						for pre in range(my - 1, 0, -1)
							" Avoiding conflicts shortened command and special commands
							" - weird abbreviations for delete. (See :help :d)
							" - k{char} is used as mark. (See :help :k)
							" - :s commsnds repeat. (See :help :substitute-repeat)
							if lcmd[key][my].name[:idx] ==# lcmd[key][pre].name[:idx] ||
							\	(key ==# 'd' &&
							\		lcmd[key][my].name[:idx] =~# '^d\%[elete][lp]$')
							\	|| (key ==# 'k' &&
							\		lcmd[key][my].name[:idx] =~# '^k[a-zA-Z]$')
							\	|| (key ==# 's' &&
							\		lcmd[key][my].name[:idx] =~# '^s\%(c\%([^sr][^ip]\=\)\=$\|g\|i[^mlg]\=$\|I\|r[^e]\=$\)')
								let matched = 1
								let omit_idx = idx + 1
								break
							endif
						endfor
						if !matched
							break
						endif
					endfor
				endif

				let item = lcmd[key][my]
				let item.type = s:get_vim_command_type(item.name)
				if omit_idx + 1 < strlen(item.name)
					let item.omit_idx = omit_idx
					let item.syn_str = item.name[:omit_idx] . '[' . 
					\		item.name[omit_idx + 1:] . ']'
					let item.syn_pat = item.name[:omit_idx] . '\%[' .
					\		item.name[omit_idx + 1:] . ']'
				else
					let item.omit_idx = -1
					let item.syn_str = item.name
					let item.syn_pat = item.name
				endif
				call add(a:cmd, copy(item))
			endfor
		endfor

		let no_shorten_in_vim9 =<< trim EOL
			final
			def
			enddef
			class
			endclass
			enum
			endenum
			interface
			endinterface
			abstract
			public
			static
			this
			var
			type
		EOL

		call map(a:cmd, {_, v ->
			\ index(no_shorten_in_vim9, v.name) != -1 ?
			\		extend(copy(v), {'omit_idx': -1, 'syn_str': v.name}) :
			"\ ":fina" means ":finally" in legacy script, for backwards compatibility.
			"\ (From Vim source code find_ex_command() in ex_docmd.c)
			\ v.name ==# 'finally' ?
			\		extend(copy(v), {'omit_idx': 3, 'syn_str': 'fina[lly]'}) :
			"\ :ho must not be recognized as :horizontal.
			\ v.name ==# 'horizontal' ?
			\		extend(copy(v), {'omit_idx': 2, 'syn_str': 'hor[izontal]'}) :
			\ v
			\ })

		if empty(a:cmd)
			throw 'cmd is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

function s:memoize_cmd_modifiers()
	let modifiers = []
	function _() closure
		if empty(modifiers)
			try
				let file_name = $VIM_SRCDIR .. '/ex_docmd.c'

				new
				exec 'read ' .. file_name
				norm! gg
				exec ':/^static cmdmod_info_T cmdmod_info_tab\[] = {/+1;/^};/-1yank'
				%delete _

				put
				1delete _

				let list = []
				for line in getline(1, line('$'))
					let list = matchlist(line, '^\s*{"\(\w\+\)".*')
					" :browse and :confirm handled separately as lower priority matches
					" because they have same-named builtin functions
					if index(['browse', 'confirm'], list[1]) == -1
						call add(modifiers, copy(list[1]))
					endif
				endfor

				quit!

				if empty(modifiers)
					throw 'cmd modifiers list is empty'
				endif

			catch /.*/
				call s:err_gen('')
				throw 'exit'
			endtry
		endif
		return modifiers
	endfunction
	return function("_")
endfunction
let s:get_cmd_modifiers = s:memoize_cmd_modifiers()

function s:get_vim_command_type(cmd_name)
	" Return value:
	"   0: normal
	"   1: (Reserved)
	"   2: abbrev
	"   3: menu
	"   4: map
	"   5: mapclear
	"   6: unmap
	"   7: abclear
	"   8: modifiers
	"   9: cd
	"   99: (Exclude registration of "syn keyword")
	let ab_prefix   = '^[ci]\?'
	let menu_prefix = '^\%([acinostvx]\?\|tl\)'
	let map_prefix  = '^[acilnostvx]\?'
	let exclude_list =<< trim EOL
		2match
		3match
		Next
		Print
		X
		abstract
		argdo
		append
		augroup
		autocmd
		behave
		breakadd
		breakdel
		breaklist
		browse
		bufdo
		buffer
		call
		catch
		cdo
		cfdo
		change
		chdir
		class
		command
		confirm
		const
		copy
		debug
		debuggreedy
		def
		defer
		delcommand
		delete
		delfunction
		doautoall
		doautocmd
		echo
		echoconsole
		echoerr
		echohl
		echomsg
		echon
		echowindow
		else
		elseif
		endclass
		enddef
		endenum
		endfunction
		endif
		endinterface
		enum
		eval
		execute
		export
		filetype
		filter
		final
		folddoclosed
		folddoopen
		for
		function
		global
		grep
		grepadd
		help
		helpgrep
		highlight
		history
		if
		import
		insert
		interface
		join
		k
		language
		let
		ldo
		lfdo
		lgrep
		lgrepadd
		lhelpgrep
		lmake
		loadkeymap
		lockvar
		lua
		luado
		luafile
		lvimgrep
		lvimgrepadd
		make
		map
		mapclear
		mark
		match
		menutranslate
		mzfile
		mzscheme
		noremap
		normal
		number
		packadd
		perl
		perldo
		popup
		profdel
		profile
		promptfind
		promptrepl
		public
		py3
		py3do
		py3file
		pydo
		pyfile
		python
		python3
		pythonx
		pyx
		pyxdo
		pyxfile
		redir
		return
		ruby
		rubydo
		rubyfile
		runtime
		set
		setglobal
		setlocal
		sleep
		smagic
		snomagic
		sort
		split
		static
		substitute
		swapname
		syntax
		syntime
		t
		tabdo
		tcl
		tcldo
		tclfile
		terminal
		this
		throw
		type
		uniq
		unlet
		unlockvar
		unmap
		var
		vglobal
		vim9script
		vimgrep
		vimgrepadd
		while
		wincmd
		windo
		write
		z
	EOL

	if index(exclude_list, a:cmd_name) != -1
		let ret = 99
	elseif a:cmd_name =~# '^\%(\%(un\)\?abbreviate\|noreabbrev\|\l\%(nore\|un\)\?abbrev\)$'
		let ret = 2
	elseif a:cmd_name =~# ab_prefix . 'abclear$'
		let ret = 7
	elseif a:cmd_name =~# menu_prefix . '\%(nore\|un\)\?menu$'
		let ret = 3
	elseif a:cmd_name =~# map_prefix . '\%(nore\)\?map$'
		let ret = 4
	elseif a:cmd_name =~# map_prefix . 'mapclear$'
		let ret = 5
	elseif a:cmd_name =~# map_prefix . 'unmap$'
		let ret = 6
	elseif index(s:get_cmd_modifiers(), a:cmd_name) != -1
		let ret = 8
	" :chdir handled specially for command/function distinction
	elseif a:cmd_name =~# '^\%([lt]\?cd\|[lt]chdir\)$'
		let ret = 9
	else
		let ret = 0
	endif
	return ret
endfunc

function s:append_syn_vimcmd(lnum, str_info, cmd_list, type)
	let ret_lnum = a:lnum
	let str = a:str_info.start

	for o in a:cmd_list
		if o.type == a:type
			let str .= ' ' . o.syn_str
			if len(str) > s:line_break_len
				if !empty(a:str_info.end)
					let str .= ' ' . a:str_info.end
				endif
				call append(ret_lnum, str)
				let str = a:str_info.start
				let ret_lnum += 1
			endif
		endif
	endfor
	if str !=# a:str_info.start
		if !empty(a:str_info.end)
			let str .= ' ' . a:str_info.end
		endif
		call append(ret_lnum, str)
		let ret_lnum += 1
	endif
	return s:append_syn_vimcmd_after_range(ret_lnum, a:str_info, a:cmd_list, a:type)
endfunc

" append post-range :syn-match variants
function s:append_syn_vimcmd_after_range(lnum, str_info, cmd_list, type)
	let cmd_list = filter(
				\ copy(a:cmd_list),
				\ { _, v -> v.flags.range && v.type == a:type })
	let str_start = substitute(
				\ a:str_info.start,
				\ 'syn keyword vim\(\w\+\)\(.*\)',
				\ 'syn match vim\1_\2',
				\ '')
	let str_end = a:str_info.end
	let ret_lnum = a:lnum

	let by_head = {}
	for cmd in cmd_list
		let head = cmd.name[0]
		if !has_key(by_head, head)
			let by_head[head] = []
		endif
		call add(by_head[head], cmd)
	endfor

	for head in sort(keys(by_head))
		let str = str_start . ' "' . head
		let tails = by_head[head]->map({ _, v -> v.syn_pat[1:] })
		if len(tails) == 1
			let str .= tails[0]
		elseif len(tails) > 1
			let str .= '\%(' . tails->join('\|') . '\)'
		endif
		let str .= '\>"'
		if !empty(str_end)
			let str .= ' ' . str_end
		endif
		call append(ret_lnum, str)
		let ret_lnum += 1
	endfor
	return ret_lnum
endfunc

" ------------------------------------------------------------------------------
function s:parse_vim_event(li)
	try
		let file_name = $VIM_SRCDIR . '/autocmd.c'
		let item = {}

		new
		exec 'read ' . file_name
		norm! gg
		exec '/^static keyvalue_T event_tab\[NUM_EVENTS] = {$/+1;/^};$/-1yank'
		%delete _

		put
		g!/^\s*KEYVALUE_ENTRY(/d

		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*KEYVALUE_ENTRY(-\?EVENT_\w\+,\s*"\(\w\+\)"')
			let item.name = list[1]
			call add(a:li, copy(item))
		endfor

		" "User" requires a user defined argument event.
		" (Separately specified in vim.vim.base).
		call filter(a:li, {idx, val -> val.name !=# 'User'})

		quit!

		if empty(a:li)
			throw 'event is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:parse_vim_function(li)
	try
		let file_name = $VIM_SRCDIR . '/evalfunc.c'
		let item = {}

		new
		exec 'read ' . file_name
		norm! gg
		exec '/^static\s\+const\s\+funcentry_T\s\+global_functions\[\]\s*=\s*$/+1;/^};/-1yank'
		%delete _

		put
		g!/^\s*{\s*"\w\+"\s*,.*$/d
		g/^\s*{\s*"test"\s*,.*$/d
		g@//\s*obsolete@d
		g@/\*\s*obsolete\s*\*/@d

		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*{\s*"\(\w\+\)"\s*,')
			let item.name = list[1]
			call add(a:li, copy(item))
		endfor

		quit!

		if empty(a:li)
			throw 'function is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:parse_vim_group(li)
	try
		let file_name = $VIM_SRCDIR . '/../runtime/syntax/syncolor.vim'
		let item = {}

		new
		exec 'read ' . file_name
		g!/^\s*Syn\%(Color\|Link\)/d
		%s/^\s*Syn\%(Color\|Link\)\s\+\(\w\+\).*/\1/

		for group in getline(1, line('$'))->sort()->uniq()
			let item.name = group
			call add(a:li, copy(item))
		endfor

		quit!

		if empty(a:li)
			throw 'group is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:parse_vim_hlgroup(li)
	try
		let file_name = $VIM_SRCDIR . '/highlight.c'
		let item = {}

		new
		exec 'read ' . file_name
		call cursor(1, 1)
		exec '/^static\s\+char\s\+\*(highlight_init_both\[\])\s*=\%(\s*{\)\?$/+1;/^\s*};/-1yank a'
		exec '/^static\s\+char\s\+\*(highlight_init_light\[\])\s*=\%(\s*{\)\?$/+1;/^\s*};/-1yank b'
		exec '/^set_normal_colors(\%(void\)\?)$/+1;/^}$/-1yank d'
		%delete _
		put a
		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*\%(CENT(\)\?"\%(default\s\+link\s\+\)\?\(\a\+\).*",.*')
			if !empty(list)
				let item.name = list[1]
				let item.type = 'both'
				call add(a:li, copy(item))
			endif
		endfor

		%delete _
		put b
		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*\%(CENT(\)\?"\%(default\s\+link\s\+\)\?\(\a\+\).*",.*')
			if !empty(list)
				let item.name = list[1]
				let item.type = 'light'
				call add(a:li, copy(item))
			endif
		endfor

		%delete _
		put d
		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*if\s*(set_group_colors(.*"\(\a\+\)",')
			if !empty(list) && list[1] !=# 'Normal'
				let item.name = list[1]
				let item.type = 'gui'
				call add(a:li, copy(item))
			endif
		endfor

		let item.name = 'CursorIM'
		let item.type = 'gui'
		call add(a:li, copy(item))

		" The following highlight groups cannot be extracted from highlight.c
		" (TODO: extract from HIGHLIGHT_INIT ?)
		for group in ['ComplMatchIns', 'LineNrAbove', 'LineNrBelow', 'MsgArea', 'Terminal']
			let item.name = group
			let item.type = 'both'
			call add(a:li, copy(item))
		endfor

		for n in range(1, 9)
			let item.name = 'User' .. n
			let item.type = 'both'
			call add(a:li, copy(item))
		endfor

		" "Conceal" is a :syn option and cannot be used as keyword, so remove it.
		" (Separately specified as 'syn match' in vim.vim.base).
		call filter(a:li, {idx, val -> val.name !=# 'Conceal'})

		quit!

		if empty(a:li)
			throw 'hlgroup is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:parse_vim_complete_name(li)
	try
		let file_name = $VIM_SRCDIR . '/usercmd.c'
		let item = {}

		new
		exec 'read ' . file_name
		norm! gg
		exec '/^static keyvalue_T command_complete_tab\[] =$/+1;/^};$/-1yank'
		%delete _

		put
		g!/^\s*KEYVALUE_ENTRY(/d
		g/"custom\(list\)\?"/d

		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*KEYVALUE_ENTRY(EXPAND_\w\+,\s*"\(\w\+\)"')
			let item.name = list[1]
			call add(a:li, copy(item))
		endfor

		quit!

		if empty(a:li)
			throw 'complete_name is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:parse_vim_addr_name(li)
	try
		let file_name = $VIM_SRCDIR . '/usercmd.c'
		let item = {}

		new
		exec 'read ' . file_name
		norm! gg
		exec '/^static addrtype_T addr_type_complete_tab\[] =$/+1;/^};$/-1yank'
		%delete _

		put
		g!/^\s*ADDRTYPE_ENTRY(/d

		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*ADDRTYPE_ENTRY(ADDR_\w\+,\s*"\(\w\+\)",\s*"\(.*\)"')
			let item.name = list[1]
			call add(a:li, copy(item))
			let item.name = list[2]
			call add(a:li, copy(item))
		endfor

		" '?' is not in 'iskeyword' and cannot be used as keyword, so remove it.
		" (Separately specified as 'syn match' in vim.vim.base).
		call filter(a:li, {idx, val -> val.name !=# '?'})

		quit!

		if empty(a:li)
			throw 'addr_name is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:parse_vim_var(li)
	try
		let file_name = $VIM_SRCDIR . '/evalvars.c'
		let item = {}

		new
		exec 'read ' . file_name
		norm! gg
		exec '/^} vimvars\[VV_LEN] =\n{$/+1;/^};$/-1yank'
		%delete _

		put
		g!/^\s*{VV_NAME(/d

		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*{VV_NAME("\(\w\+\)"')
			let item.name = list[1]
			call add(a:li, copy(item))
		endfor

		quit!

		if empty(a:li)
			throw 'var is empty'
		endif
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:append_syn_any(lnum, str_info, li)
	let ret_lnum = a:lnum
	let str = a:str_info.start

	for o in a:li
		let str .= ' ' . o.name
		if len(str) > s:line_break_len
			if !empty(a:str_info.end)
				let str .= ' ' . a:str_info.end
			endif
			call append(ret_lnum, str)
			let str = a:str_info.start
			let ret_lnum += 1
		endif
	endfor
	if str !=# a:str_info.start
		if !empty(a:str_info.end)
			let str .= ' ' . a:str_info.end
		endif
		call append(ret_lnum, str)
		let ret_lnum += 1
	endif
	return ret_lnum
endfunc

" ------------------------------------------------------------------------------
function s:update_syntax_vim_file(vim_info)
	try
		function! s:search_and_check(kword, base_fname, str_info)
			let a:str_info.start = ''
			let a:str_info.end = ''

			let pattern = '^" GEN_SYN_VIM: ' . a:kword . '\s*,'
			let lnum = search(pattern)
			if lnum == 0
				throw 'Search pattern ''' . pattern . ''' not found in ' .
				\		a:base_fname
			endif
			let li = matchlist(getline(lnum), pattern . '\s*START_STR\s*=\s*''\(.\{-}\)''\s*,\s*END_STR\s*=\s*''\(.\{-}\)''')
			if empty(li)
				throw 'Bad str_info line:' . getline(lnum)
			endif
			let a:str_info.start = li[1]
			let a:str_info.end = li[2]
			return lnum
		endfunc

		let target_fname = 'vim.vim.rc'
		let base_fname = 'vim.vim.base'
		let str_info = {}
		let str_info.start = ''
		let str_info.end = ''

		new
		exec 'edit ' . target_fname
		%d _
		exec 'read ' . base_fname
		1delete _
		call cursor(1, 1)

		" vimCommand
		let li = a:vim_info.cmd
		" vimCommand - normal
		let lnum = s:search_and_check('vimCommand normal', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 0)

		" vimOption
		let kword = 'vimOption'
		let li = a:vim_info.opt
		" vimOption - normal
		let lnum = s:search_and_check(kword . ' normal', base_fname, str_info)
		let lnum = s:append_syn_vimopt(lnum, str_info, li, '', 0)
		" vimOption - turn-off
		let lnum = s:search_and_check(kword . ' turn-off', base_fname, str_info)
		let lnum = s:append_syn_vimopt(lnum, str_info, li, 'no', 1)
		" vimOption - invertible
		let lnum = s:search_and_check(kword . ' invertible', base_fname, str_info)
		let lnum = s:append_syn_vimopt(lnum, str_info, li, 'inv', 1)
		" vimOption - term output code
		let li = a:vim_info.term_out_code
		let lnum = s:search_and_check(kword . ' term output code', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" vimOption - normal variable
		let li = a:vim_info.opt
		let lnum = s:search_and_check(kword . ' normal variable', base_fname, str_info)
		let lnum = s:append_syn_vimopt(lnum, str_info, li, '', 0)
		" vimOption - term output code variable
		let li = a:vim_info.term_out_code
		let lnum = s:search_and_check(kword . ' term output code variable', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" Missing vimOption
		let li = a:vim_info.missing_opt
		let lnum = s:search_and_check('Missing vimOption', base_fname, str_info)
		let lnum = s:append_syn_vimopt(lnum, str_info, li, '', 0)
		let lnum = s:append_syn_vimopt(lnum, str_info, li, 'no', 1)
		let lnum = s:append_syn_vimopt(lnum, str_info, li, 'inv', 1)

		" vimAutoEvent
		let li = a:vim_info.event
		let lnum = s:search_and_check('vimAutoEvent', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" vimGroup
		let li = a:vim_info.group
		let lnum = s:search_and_check('vimGroup', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" vimHLGroup
		let li = a:vim_info.hlgroup
		let lnum = s:search_and_check('vimHLGroup', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" vimFuncName
		let li = a:vim_info.func
		let lnum = s:search_and_check('vimFuncName', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" vimVarName
		let li = a:vim_info.var
		let lnum = s:search_and_check('vimVarName', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" vimUserAttrComplete
		let li = a:vim_info.compl_name
		let lnum = s:search_and_check('vimUserCmdAttrComplete', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" vimUserAttrbAddr
		let li = a:vim_info.addr_name
		let lnum = s:search_and_check('vimUserCmdAttrAddr', base_fname, str_info)
		let lnum = s:append_syn_any(lnum, str_info, li)

		" vimCommand - abbrev
		let kword = 'vimCommand'
		let li = a:vim_info.cmd
		let lnum = s:search_and_check(kword . ' abbrev', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 2)
		let lnum = s:search_and_check(kword . ' abclear', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 7)
		" vimCommand - map
		let lnum = s:search_and_check(kword . ' map', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 4)
		let lnum = s:search_and_check(kword . ' mapclear', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 5)
		let lnum = s:search_and_check(kword . ' unmap', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 6)
		" vimCommand - menu
		let lnum = s:search_and_check(kword . ' menu', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 3)
		" vimCommand - modifier
		let lnum = s:search_and_check(kword . ' modifier', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 8)
		" vimCommand - cd
		let lnum = s:search_and_check(kword . ' cd', base_fname, str_info)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 9)

		update
		quit!

	catch /.*/
		call s:err_gen('')
		throw 'exit'
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:check_help_doc(vim_info)
	try
		new
		let cwd_save = getcwd()
		cd ../../../runtime/doc

		let exclude_cmd =<< trim END
			deletel
			deletep
			a
			i
		END

		let nocheck_shorten_excmd_list =<< trim END
			bufdo
			cfdo
			cstag
			debug
			defer
			eval
			intro
			lfdo
			luado
			luafile
			ownsyntax
			py3do
			pydo
			pyxdo
			pyxfile
			rundo
			smile
			syntime
			windo
			wundo
		END

		" Check the Ex-command is listed in index.txt
		split index.txt
		for vimcmd in a:vim_info.cmd
			if index(exclude_cmd, vimcmd.name) != -1
				continue
			endif
			norm! gg
			let find_ptn = '^|:' . vimcmd.name . '|\s\+'
			let lnum = search(find_ptn, 'eW')
			if lnum == 0
				call s:err_sanity($'Ex-cmd ":{vimcmd.name}" is not found in index.txt.')
			elseif search(find_ptn, 'eW') > 0
				call s:err_sanity($'Ex-cmd ":{vimcmd.name}" is duplicated in index.txt.')
			else
				let doc_syn_str = substitute(getline(lnum), find_ptn . ':\(\S\+\)\s\+.*', '\1', '')
				if doc_syn_str !=# vimcmd.syn_str
					call s:err_sanity($'Ex-cmd "{vimcmd.name}" short name differ in index.txt. expect: "{vimcmd.syn_str}", but: "{doc_syn_str}"')
				endif
			endif
		endfor
		quit!

		" Check the existence of the help tag for Ex-command.
		set wildignore=version*.txt,todo.txt,usr_*.txt
		for vimcmd in a:vim_info.cmd
			if index(exclude_cmd, vimcmd.name) != -1
				continue
			endif
			let find_ptn = '\s\*:' . vimcmd.name . '\*\_s'
			exec "silent! vimgrep /" . find_ptn . "/gj *.txt"
			let qfl = getqflist()
			if empty(qfl)
				call s:err_sanity($'Help tag for Ex-cmd ":{vimcmd.name}" not found.')
			elseif len(qfl) > 1
				call s:err_sanity($'Help tag for Ex-cmd ":{vimcmd.name}" is duplicated.')
			elseif index(nocheck_shorten_excmd_list, vimcmd.name) ==# -1
				" Check the existence of the shorten Ex-command notation.
				cc
				norm! 2k
				let end_lnum = qfl[0].lnum + 10
				let find_ptn = '^:.*\<' . vimcmd.syn_str->escape('[]')
				let lnum = search(find_ptn, 'W', end_lnum)
				if lnum == 0
					if vimcmd.omit_idx != -1
						" Check the existence of the shorten help tag for Ex-command.
						cc
						norm! k
						let end_lnum = qfl[0].lnum + 10
						let find_ptn = '\s\*:' . vimcmd.name[:vimcmd.omit_idx] . '\*\_s'
						let lnum = search(find_ptn, 'W', end_lnum)
					else
						let lnum = 1
					endif
					if lnum == 0
						call s:err_sanity($'Shorten help tag "{vimcmd.name[:vimcmd.omit_idx]}" for Ex-cmd "{vimcmd.name}" not found.')
					endif
				endif
			endif
		endfor
	catch /.*/
		call s:err_gen('')
		throw 'exit'
	finally
		call s:err_gen('Ex-cmd documentation consistency check completed.')
		exec 'cd ' . cwd_save
		set wildignore&
	endtry
endfunc

" ------------------------------------------------------------------------------
function s:err_gen(arg)
	call s:write_error(a:arg, s:log_write_dir .. 'generator.err')
endfunc

function s:err_sanity(arg)
	call s:write_error(a:arg, s:log_write_dir .. 'sanity_check.err')
endfunc

function s:write_error(arg, fname)
	let li = []
	if !empty(v:throwpoint)
		call add(li, v:throwpoint)
	endif
	if !empty(v:exception)
		call add(li, v:exception)
	endif
	if type(a:arg) == type([])
		call extend(li, a:arg)
	elseif type(a:arg) == type("")
		if !empty(a:arg)
			call add(li, a:arg)
		endif
	endif
	if !empty(li)
		call writefile(li, a:fname, 'a')
	else
		call writefile(['UNKNOWN'], a:fname, 'a')
	endif
endfunc

" ------------------------------------------------------------------------------
try
	let s:line_break_len = 768
	let s:vim_info = {}
	let s:vim_info.opt = []
	let s:vim_info.missing_opt = []
	let s:vim_info.term_out_code = []
	let s:vim_info.cmd = []
	let s:vim_info.event = []
	let s:vim_info.func = []
	let s:vim_info.group = []
	let s:vim_info.hlgroup = []
	let s:vim_info.compl_name = []
	let s:vim_info.addr_name = []
	let s:vim_info.var = []

	set lazyredraw
	if !$CHECK_HELP_DOC
		silent call s:parse_vim_option(s:vim_info.opt, s:vim_info.missing_opt,
		\						s:vim_info.term_out_code)
		silent call s:parse_vim_command(s:vim_info.cmd)
		silent call s:parse_vim_event(s:vim_info.event)
		silent call s:parse_vim_function(s:vim_info.func)
		silent call s:parse_vim_group(s:vim_info.group)
		silent call s:parse_vim_hlgroup(s:vim_info.hlgroup)
		silent call s:parse_vim_complete_name(s:vim_info.compl_name)
		silent call s:parse_vim_addr_name(s:vim_info.addr_name)
		silent call s:parse_vim_var(s:vim_info.var)

		call s:update_syntax_vim_file(s:vim_info)
	else
		silent call s:parse_vim_command(s:vim_info.cmd)
		silent call s:check_help_doc(s:vim_info)
	endif
	set nolazyredraw

finally
	quitall!
endtry

" ---------------------------------------------------------------------
let &cpo = s:keepcpo
unlet s:keepcpo
" vim:ts=2 sw=2
