" Vim syntax file generator
" Language: Vim script
" Maintainer: Hirohito Higashi (h_east)
" URL: https://github.com/vim-jp/syntax-vim-ex
" Last Change: Feb 11, 2024
" Version: 2.0.0

let s:keepcpo= &cpo
set cpo&vim

language C

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\)\?.*')
			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
		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] : '~'
				call add(lcmd[key], list[1])
			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]))
            let spec=0
            if lcmd[key][my] ==# 'ex'
              let spec=1
              echo "cmd name:" lcmd[key][my]
            endif
						let matched = 0
						for pre in range(my - 1, 0, -1)
              if spec
                echo "pre:" pre ", my:" my
              endif
							if pre == my
                if spec
                  echo "continue"
                endif
								continue
							endif
							" for weird abbreviations for delete. (See :help :d)
							" And k{char} is used as mark. (See :help :k)
							if lcmd[key][my][:idx] ==# lcmd[key][pre][:idx] ||
							\	(key ==# 'd' &&
							\		lcmd[key][my][:idx] =~# '^d\%[elete][lp]$')
							\	|| (key ==# 'k' &&
							\		lcmd[key][my][:idx] =~# '^k[a-zA-Z]$')
								let matched = 1
								let omit_idx = idx + 1
                if spec
                  echo "match. break. omit_idx:" omit_idx
                endif
								break
							endif
						endfor
						if !matched
              if spec
                echo "not match. break"
              endif
							break
						endif
					endfor
				endif

				let item.name = 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:] . ']'
				else
					let item.omit_idx = -1
					let item.syn_str = item.name
				endif
				call add(a:cmd, copy(item))
			endfor
		endfor

		" Check exists in the help. (Usually it does not check...)
		let doc_dir = './vim/runtime/doc'
		if 0
			for vimcmd in a:cmd
				let find_ptn = '^|:' . vimcmd.name . '|\s\+'
				exec "silent! vimgrep /" . find_ptn . "/gj " . doc_dir . "/index.txt"
				let li = getqflist()
				if empty(li)
					call s:err_sanity(printf('Ex-cmd `:%s` is not found in doc/index.txt.', vimcmd.name))
				elseif len(li) > 1
					call s:err_sanity(printf('Ex-cmd `:%s` is duplicated in doc/index.txt.', vimcmd.name))
				else
					let doc_syn_str = substitute(li[0].text, find_ptn . '\(\S\+\)\s\+.*', '\1', '')
					if doc_syn_str ==# vimcmd.syn_str
						call s:err_sanity(printf('Ex-cmd `%s` short name differ in doc/index.txt. code: `%s`, document: `%s`', vimcmd.name, vimcmd.syn_str, doc_syn_str))
					endif
				endif

				if 1
				for i in range(2)
					if i || vimcmd.omit_idx >= 0
						if !i
							let base_ptn = vimcmd.name[:vimcmd.omit_idx]
						else
							let base_ptn = vimcmd.name
						endif
						let find_ptn = '\*:' . base_ptn . '\*'
						exec "silent! vimgrep /" . find_ptn . "/gj " . doc_dir . "/*.txt"
						let li = getqflist()
						if empty(li)
							call s:err_sanity(printf('Ex-cmd `:%s`%s is not found in the help tag.', base_ptn, !i ? ' (short name of `:' . vimcmd.name . '`)' : ''))
						elseif len(li) > 1
							call s:err_sanity(printf('Ex-cmd `:%s`%s is duplicated in the help tag.', base_ptn, !i ? ' (short name of `:' . vimcmd.name . '`)' : ''))
						endif
					endif
				endfor
			endif
			endfor
		endif

		" Add weird abbreviations for delete. (See :help :d)
		for i in ['l', 'p']
			let str = 'delete'
			let item.name = str . i
			let item.type = s:get_vim_command_type(item.name)
			let item.omit_idx = -1
			for x in range(strlen(str))
				let item.syn_str = str[:x] . i
				if item.syn_str !=# "del"
					call add(a:cmd, copy(item))
				endif
			endfor
		endfor

		" Required for original behavior
		let item.name = 'a'		" append
		let item.type = 0
		let item.omit_idx = -1
		let item.syn_str = item.name
		call add(a:cmd, copy(item))
		let item.name = 'i'		" insert
		let item.syn_str = item.name
		call add(a:cmd, copy(item))

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

function! s:get_vim_command_type(cmd_name)
	" Return value:
	"   0: normal
	"   1: (Reserved)
	"   2: abbrev (without un)
	"   3: menu
	"   4: map
	"   5: mapclear
	"   6: unmap
	"   99: (Exclude registration of "syn keyword")
	let menu_prefix = '^\%([acinosvx]\?\|tl\)'
	let map_prefix  = '^[acilnostvx]\?'
	let exclude_list = [
	\	'map',
	\	'substitute', 'smagic', 'snomagic',
	\	'setlocal', 'setglobal', 'set', 'var',
	\	'autocmd', 'doautocmd', 'doautoall',
	\	'echo', 'echohl', 'execute',
	\	'behave', 'augroup', 'normal', 'syntax',
	\	'append', 'insert',
	\	'Next', 'Print', 'X',
	\ ]
	" Required for original behavior
	" \	'global', 'vglobal'

	if index(exclude_list, a:cmd_name) != -1
		let ret = 99
	elseif a:cmd_name =~# '^\%(abbreviate\|noreabbrev\|\l\%(nore\)\?abbrev\)$'
		let ret = 2
	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
	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 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 '/^}\s*event_names\[\]\s*=\s*$/+1;/^};/-1yank'
		%delete _

		put
		g!/^\s*{\s*"\w\+"\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 '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\+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_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 ?)
		let item.name = 'LineNrAbove'
		let item.type = 'both'
		call add(a:li, copy(item))

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

		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 '/^}\s*command_complete\[\]\s*=\s*$/+1;/^};/-1yank'
		%delete _

		put
		g!/^\s*{.*"\w\+"\s*}\s*,.*$/d
		g/"custom\(list\)\?"/d

		for line in getline(1, line('$'))
			let list = matchlist(line, '^\s*{.*"\(\w\+\)"\s*}\s*,')
			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: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)
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 3)		" menu
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 4)		" map
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 5)		" mapclear
		let lnum = s:append_syn_vimcmd(lnum, str_info, li, 6)		" unmap

		" 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)

		" 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)

		" 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)

		" vimUserAttrbCmplt
		let li = a:vim_info.compl_name
		let lnum = s:search_and_check('vimUserAttrbCmplt', 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)
		" 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)

		update
		quit!

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

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

function! s:err_sanity(arg)
	call s:write_error(a:arg, '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.hlgroup = []
	let s:vim_info.compl_name = []

	set lazyredraw
	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_hlgroup(s:vim_info.hlgroup)
	silent call s:parse_vim_complete_name(s:vim_info.compl_name)

	call s:update_syntax_vim_file(s:vim_info)
	set nolazyredraw

finally
	quitall!
endtry

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