"============================================================== " file: complete_parameter.vim " brief: " VIM Version: 8.0 " author: tenfyzhong " email: tenfy@tenfy.cn " created: 2017-06-07 20:29:10 "============================================================== let s:complete_parameter = {'index': 0, 'items': [], 'complete_pos': [], 'success': 0, 'echos': []} let g:complete_parameter_last_failed_insert = '' let s:log_index = 0 function! cmp#init() abort "{{{ runtime! cm_parser/*.vim " ultisnips will remove all smaps, this will without this plugin let g:UltiSnipsMappingsToIgnore = get(g:, 'UltiSnipsMappingsToIgnore', []) + ["complete_parameter"] " neosnippet will remove all smaps let g:neosnippet#disable_select_mode_mappings = 0 " 4 error " 2 error + debug " 1 erro + debug + trace let g:complete_parameter_log_level = get(g:, 'complete_parameter_log_level', 5) let g:complete_parameter_use_ultisnips_mappings = get(g:, 'complete_parameter_use_ultisnips_mappings', 0) endfunction "}}} function! cmp#default_echos(completed_item) return [] endfunction let s:ftfunc_prefix = 'cm_parser#' let s:ftfunc = {'ft': ''} function! cmp#new_ftfunc(filetype) abort "{{{ if empty(a:filetype) throw 'filetype is empty' endif let ftfunc = deepcopy(s:ftfunc) let ftfunc['ft'] = a:filetype try let ftfunc['parameters'] = function(s:ftfunc_prefix . a:filetype .'#parameters') let ftfunc['parameter_delim'] = function(s:ftfunc_prefix . a:filetype . '#parameter_delim') let ftfunc['parameter_begin'] = function(s:ftfunc_prefix. a:filetype . '#parameter_begin') let ftfunc['parameter_end'] = function(s:ftfunc_prefix . a:filetype . '#parameter_end') if exists('*'.s:ftfunc_prefix.a:filetype.'#echos') let ftfunc['echos'] = function(s:ftfunc_prefix.a:filetype.'#echos') else let ftfunc['echos'] = function('cmp#default_echos') endif catch /^E700/ throw 'the function should be defined: ' . v:exception endtry return ftfunc endfunction "}}} function! s:filetype_func_exist(filetype) abort "{{{ let filetype_func_prefix = s:ftfunc_prefix.a:filetype.'#' let parameters_func_name = filetype_func_prefix.'parameters' let parameter_delim_func_name = filetype_func_prefix.'parameter_delim' let parameter_begin_func_name = filetype_func_prefix.'parameter_begin' let parameter_end_func_name = filetype_func_prefix.'parameter_end' if !exists('*'.parameters_func_name) || \!exists('*'.parameter_delim_func_name) || \!exists('*'.parameter_begin_func_name) || \!exists('*'.parameter_end_func_name) return 0 endif return 1 endfunction "}}} function! cmp#filetype_func_check(ftfunc) abort "{{{ if !filetype_func_exist(a:ftfunc['ft']) return 0 endif " let parameters = a:ftfunc.parameters(v:completed_item) " if type(parameters) != 3 " return 0 " endif if !exists('*'.string(a:ftfunc.parameter_delim)) return 0 endif let delim = a:ftfunc.parameter_delim() if type(delim) != 1 || empty(delim) return 0 endif if !exists('*'.string(a:ftfunc.parameter_begin)) return 0 endif let begin = a:ftfunc.parameter_begin() if type(begin) != 1 || empty(begin) return 0 endif if !exists('*'.string(a:ftfunc.parameter_end)) return 0 endif let end = a:ftfunc.parameter_end() if type(end) != 1 || empty(end) return 0 endif return 1 endfunction "}}} " check v:completed_item is empty or not function! s:empty_completed_item() abort "{{{ let completed_item = v:completed_item if empty(completed_item) return 1 endif let menu = get(completed_item, 'menu', '') let info = get(completed_item, 'info', '') let kind = get(completed_item, 'kind', '') let abbr = get(completed_item, 'abbr', '') return empty(menu) && empty(info) && empty(kind) && empty(abbr) endfunction "}}} " select an item if need, and the check need to revert or not " else call the complete function function! cmp#pre_complete(failed_insert) abort "{{{ let s:log_index = timenow_ms() if !pumvisible() return failed_event(a:failed_insert) endif let completed_word = get(v:completed_item, 'word', '') if empty_completed_item() && pumvisible() let feed = printf("\=cmp#check_revert_select('%s', '%s')\", a:failed_insert, completed_word) call feedkeys(feed, 'n') return "\" else return cmp#complete(a:failed_insert) endif endfunction "}}} function! cmp#default_failed_insert(failed_insert) "{{{ if a:failed_insert =~# '()$' return "\" else return '' endif endfunction "}}} function! s:failed_event(failed_insert) abort "{{{ return the text to insert and toggle event let keys = '' if exists('*CompleteParameterFailed') let keys = CompleteParameterFailed(a:failed_insert) else let keys = cmp#default_failed_insert(a:failed_insert) endif let content = getline(line('.')) let parameter = a:failed_insert let pos = col('.') - 2 if pos > 0 let posEnd = pos + len(parameter) - 1 if content[pos : posEnd] !=# parameter && \content[pos] ==# parameter[0] let parameter = substitute(parameter, '\m.\(.*\)', '\1', '') endif endif let keys = parameter . keys call trace_log(keys) return keys endfunction "}}} " if the select item is not match with completed_word, the revert " else call the complete function function! cmp#check_revert_select(failed_insert, completed_word) abort "{{{ let select_complete_word = get(v:completed_item, 'word', '') call trace_log('s:completed_word: ' . a:completed_word) call trace_log('select_complete_word: ' . select_complete_word) redraw! if select_complete_word !=# a:completed_word return failed_event("\".a:failed_insert) else let keys = cmp#complete(a:failed_insert) return keys endif endfunction "}}} function! cmp#check_parameter_return(parameter, parameter_begin, parameter_end) abort "{{{ if len(a:parameter) < 2 return 0 endif " echom printf('mb, begin: %s, p[0]: %s, result: %d', a:parameter_begin, a:parameter[0], match(a:parameter_begin, a:parameter[0]) != -1) " echom printf('me, end: %s, p[-1]: %s, result: %d', a:parameter_end, a:parameter[-1], match(a:parameter_end, a:parameter[len(a:parameter)-1]) != -1) return match(a:parameter_begin, a:parameter[0]) != -1 && \match(a:parameter_end, a:parameter[len(a:parameter)-1]) != -1 endfunction "}}} function! cmp#complete(failed_insert) abort "{{{ call trace_log(string(v:completed_item)) if empty_completed_item() call debug_log('v:completed_item is empty') return failed_event(a:failed_insert) endif let filetype = &ft if empty(filetype) call debug_log('filetype is empty') return failed_event(a:failed_insert) endif try let ftfunc = cmp#new_ftfunc(filetype) catch call debug_log('new_ftfunc failed. '.string(v:exception)) return failed_event(a:failed_insert) endtry if !cmp#filetype_func_check(ftfunc) call error_log('ftfunc check failed') return failed_event(a:failed_insert) endif " example: the complete func like this`func Hello(param1 int, param2 string) int` " the parsed must be a list and the element of the list is a dictional, " the dictional must have the below keys " text: the text to be complete -> `(param1, param2)` " delim: the delim of parameters -> `,` " prefix: the begin of text -> `(` " suffix: the end of the text -> `)` let parseds = ftfunc.parameters(v:completed_item) call debug_log(string(parseds)) if type(parseds) != 3 call error_log('return type error') return failed_event(a:failed_insert) endif let parameter_begin = ftfunc.parameter_begin() let parameter_end = ftfunc.parameter_end() if empty(parseds) || len(parseds[0]) < 2 || !cmp#check_parameter_return(parseds[0], parameter_begin, parameter_end) call debug_log("parseds is empty") return failed_event(a:failed_insert) endif let s:complete_parameter['index'] = 0 let s:complete_parameter['items'] = parseds let s:complete_parameter['complete_pos'] = [line('.'), col('.')] let col = s:complete_parameter['complete_pos'][1] let s:complete_parameter['success'] = 1 if get(g:, 'complete_parameter_echo_signature', 1) let s:complete_parameter['echos'] = ftfunc.echos(v:completed_item) endif " if the first char of parameter was inserted, remove it from the parameter let content = getline(line('.')) let parameter = s:complete_parameter['items'][0] if col > 1 if content[col-2] ==# parameter[0] let parameter = substitute(parameter, '\m.\(.*\)', '\1', '') let s:complete_parameter['complete_pos'][1] = col - 1 endif endif " must be insert mode " the cursor is in the last char+1,col('$') " we need the pass the last char col, so col('.')-1 return cmp#goto_first_param(parameter, content, col('.')-1) endfunction "}}} function! cmp#goto_first_param(parameter, content, current_col) abort "{{{ let old_ei = &ei set ei=InsertLeave,InsertEnter,TextChanged if s:complete_parameter['success'] call trace_log(printf('content:[%s] current_col:%d, left:[%s], right:[%s]', a:content, a:current_col, a:content[:a:current_col-1], a:content[a:current_col:])) let content = a:content[:a:current_col-1] . a:parameter . a:content[a:current_col:] call trace_log("content: " . content) call trace_log("current_col: " . a:current_col) " the current_col is no in the `()` " show we need to add 1 let keys = cmp#goto_next_param_keys(1, content, a:current_col+1) let keys = printf("%s\%s", a:parameter, keys) call trace_log("keys: ". keys) let index = s:complete_parameter['index'] if len(s:complete_parameter['echos']) > index && s:complete_parameter['echos'][index] !=# '' echon s:complete_parameter['echos'][index] endif let &ei=old_ei return keys else let &ei=old_ei return a:parameter endif endfunction "}}} function! cmp#goto_next_param_keys(forward, content, current_col) abort "{{{ let filetype = &ft if empty(filetype) call debug_log('filetype is empty') return '' endif try let ftfunc = cmp#new_ftfunc(filetype) catch call debug_log('new ftfunc failed') return '' endtry if !cmp#filetype_func_check(ftfunc) return '' endif let step = a:forward ? 1 : -1 let delim = ftfunc.parameter_delim() let border_begin = a:forward ? ftfunc.parameter_begin() : ftfunc.parameter_end() let border_end = a:forward ? ftfunc.parameter_end() : ftfunc.parameter_begin() " if in the insert mode " go back to the first none space " this can select the parameter after cursor let pos = a:current_col-1 let scope_end = step > 0 ? -1 : len(a:content) if mode() ==# 'i' while pos != scope_end && \(a:content[pos-1] =~# '['.delim.border_begin.']' || \ a:content[pos-1] ==# ' ') let pos -= 1 if a:content[pos] =~# '['.delim.border_begin.']' break endif endwhile endif let [word_begin, word_end] = cmp#parameter_position(a:content, pos+1, delim, border_begin, border_end, step) call trace_log(printf('content:[%s],current_col:%d,word_begin:%d,word_end:%d', a:content, a:current_col, word_begin, word_end)) if word_begin == 0 && word_end == 0 call debug_log('word_begin and word_end is 0') return '' endif let word_len = word_end - word_begin call trace_log('word_len:'.word_len) let keys = printf("\0%dl", word_begin-2) if word_len == 0 if a:forward return keys . "a\" endif else let keys .= "lv" if &selection ==# 'exclusive' let right_len = word_len else let right_len = word_len - 1 endif if right_len > 0 let keys .= right_len let keys .= "l" endif let keys .= "\" return keys endif return '' endfunction "}}} function! cmp#goto_next_param(forward) abort "{{{ let s:log_index = timenow_ms() let filetype = &ft if empty(filetype) call debug_log('filetype is empty') return '' endif try let ftfunc = cmp#new_ftfunc(filetype) catch call debug_log('new ftfunc failed') return '' endtry if !cmp#filetype_func_check(ftfunc) return '' endif let step = a:forward ? 1 : -1 let border_end = a:forward ? ftfunc.parameter_end() : ftfunc.parameter_begin() let lnum = line('.') let content = getline(lnum) let current_col = col('.') let pos = current_col - 1 let parameter_delim = ftfunc.parameter_delim() if !a:forward && &selection==#'exclusive' && \(match(parameter_delim, content[pos])!=-1 || \ match(ftfunc.parameter_end(), content[pos])!=-1) let current_col -= 1 let pos -= 1 endif " if the selected is an object and the cursor char is an border_end " go back to border_begin and it can select the item in the object. if mode() == 'n' && match(border_end, content[pos]) != -1 normal % let current_col = col('.') endif let keys = cmp#goto_next_param_keys(a:forward, content, current_col) call feedkeys(keys, 'n') return '' endfunction "}}} " items: all overload complete function parameters " current_line: current line content " complete_pos: the pos where called complete " forward: down or up " [success, item, next_index, old_item_len] function! cmp#next_overload_content(items, current_index, current_line, complete_pos, forward) abort "{{{ if len(a:items) <= 1 || \a:current_index >= len(a:items) || \empty(a:current_line) || \len(a:current_line) < a:complete_pos[1] return [0] endif let current_overload_len = len(a:items[a:current_index]) let pos = a:complete_pos[1] - 1 let pos_end = pos+current_overload_len-1 let content = a:current_line[ pos : pos_end ] if content !=# a:items[a:current_index] return [0] endif let overload_len = len(a:items) if a:forward let next_index = (a:current_index + 1) % overload_len else let next_index = (a:current_index+overload_len-1)%overload_len endif return [1, a:items[next_index], next_index, len(a:items[a:current_index])] endfunction "}}} function! s:timenow_us() let t = reltime() return t[0] * 1000000 + t[1] endfunction function! s:timenow_ms() return timenow_us() endfunction function! cmp#overload_next(forward) abort "{{{ let s:log_index = timenow_ms() let overload_len = len(s:complete_parameter['items']) if overload_len <= 1 return endif let complete_pos = s:complete_parameter['complete_pos'] let current_line = line('.') let current_col = col('.') " if no in the complete content " then return if current_line != complete_pos[0] || current_col < complete_pos[1] call trace_log('no more overload') return endif let current_index = s:complete_parameter['index'] let current_line = getline(current_line) let result = cmp#next_overload_content( \s:complete_parameter['items'], \current_index, \current_line, \s:complete_parameter['complete_pos'], \a:forward) if result[0] == 0 call debug_log('get overload content failed') return endif let current_overload_len = result[3] call cursor(complete_pos[0], complete_pos[1]) call trace_log(printf('pos: %d %d', complete_pos[0], complete_pos[1])) exec 'normal! d'.current_overload_len.'l' let next_content = result[1] let s:complete_parameter['index'] = result[2] let s:complete_parameter['success'] = 1 let content = getline(line('.')) let current_col = col('.') let insert_method = 'a' if current_col != col('$')-1 " if no the last char " the cursor in the last complete char+1 " we need to -1 let current_col -= 1 let insert_method = 'i' endif let ret = insert_method.cmp#goto_first_param(next_content, content, current_col) call trace_log(ret) call feedkeys(ret, 'n') endfunction "}}} let s:stack = {'data':[]} function! s:new_stack() abort "{{{ return deepcopy(s:stack) endfunction "}}} function! s:stack.push(e) abort dict "{{{ call add(self.data, a:e) endfunction "}}} function! s:stack.len() abort dict "{{{ return len(self.data) endfunction "}}} function! s:stack.empty() abort dict "{{{ return self.len() == 0 endfunction "}}} function! s:stack.top() abort dict "{{{ if self.empty() throw "stack is empty" endif return self.data[-1] endfunction "}}} function! s:stack.pop() abort dict "{{{ if self.empty() throw "stack is empty" endif call remove(self.data, -1) endfunction "}}} function! s:stack.str() abort dict "{{{ let str = 'stack size:'.self.len() for d in self.data let str .= "\n" let str .= 'stack elem:'.d endfor return str endfunction "}}} function! s:in_scope(content, pos, border, step, end) abort "{{{ " echom printf('content: %s, pos: %d, border: %s, step: %d, end: %d', a:content, a:pos, a:border, a:step, a:end) let i = a:pos while i != a:end if a:content[i] =~# '\m['.a:border.']' return 1 endif let i += a:step endwhile return 0 endfunction "}}} function! cmp#jumpable(forward) abort "{{{ can jump to next parameter or not let filetype = &ft try let ftfunc = cmp#new_ftfunc(filetype) catch call debug_log('new ftfunc failed') return 0 endtry if !cmp#filetype_func_check(ftfunc) call debug_log('func check failed') return 0 endif let delim = ftfunc.parameter_delim() let border = a:forward > 0 ? ftfunc.parameter_begin() : ftfunc.parameter_end() let step = a:forward > 0 ? -1 : 1 let lnum = line('.') let content = getline(lnum) let current_pos = col('.') - 1 let end = a:forward > 0 ? -1 : len(content) return in_scope(content, current_pos, border, step, end) endfunction "}}} " content: string, the content to parse " current_col: int, current col " delim: string, split the paramter letter " return: [int, int] begin_col, end_col " " col: base 1 " pos: base 0 function! cmp#parameter_position(content, current_col, delim, border_begin, border_end, step) abort "{{{ "{{{2 if empty(a:content) || \a:current_col==0 || \empty(a:delim) || \empty(a:border_begin) || \empty(a:border_end) || \len(a:border_begin) != len(a:border_end) || \a:step==0 call debug_log('parameter_position param error') return [0, 0] endif "}}}2 let step = a:step > 0 ? 1 : -1 let current_pos = a:current_col - 1 let content_len = len(a:content) let end = a:step > 0 ? content_len : -1 if current_pos >= content_len let current_pos = content_len-1 endif " check current pos is in the scope or not let scope_end = step > 0 ? -1 : content_len if !in_scope(a:content, current_pos, a:border_begin, -step, scope_end) call trace_log(printf("no in scope, content: %s, current_pos: %d, a:border_begin: %s, step: %d, scope_end: %d", a:content, current_pos, a:border_begin, -step, scope_end)) retur [0, 0] endif let stack = new_stack() let pos = current_pos let border_matcher = {} let border_begin_chars = split(a:border_begin, '\zs') let border_end_chars = split(a:border_end, '\zs') let i = 0 while i < len(border_end_chars) let border_matcher[border_begin_chars[i]] = '\m['.a:delim.border_end_chars[i].']' let i += 1 endwhile " let border_matcher[a:border_begin] = '\m['.a:delim.a:border_end.']' let border_matcher[a:delim] = '\m['.a:delim.a:border_end.']' let border_matcher['"'] = '"' let border_matcher["'"] = "'" let border_matcher["`"] = "`" let begin_pos = 0 let end_pos = 0 " check has previous quote let quote_test_content_pos = pos if a:content[quote_test_content_pos] =~# '\m["''`]' let quote_test_content_pos -= step endif let quote_test_content = a:content[:quote_test_content_pos] let quote_test_content = substitute(quote_test_content, '\m\\.', '', 'g') let quote_test_content = substitute(quote_test_content, '\m[^"''`]', '', 'g') let quotes = split(quote_test_content, '\zs') for quote in quotes if stack.empty() call stack.push(quote) elseif border_matcher[stack.top()] ==# quote call stack.pop() endif endfor while pos != end "{{{2 if step < 0 if pos + step != end && a:content[pos+step] == '\' let pos += 2*step continue endif endif " if top of stack is quote and current letter is not a quote " the letter should be ignore if !stack.empty() && stack.top() =~# '\m["`'']' && a:content[pos] !~# '\m["`''\\]' let pos += step continue endif if a:content[pos] ==# '"' || a:content[pos] ==# "'" || a:content[pos] ==# '`' if stack.empty() || border_matcher[stack.top()] !=# a:content[pos] call stack.push(a:content[pos]) else call stack.pop() endif elseif a:content[pos] ==# '\' let pos += step elseif a:content[pos] ==# '=' if step > 0 if stack.len() > 1 " if stack more than 1, current maybe in the nest scope, ignore it let pos += step continue endif let pos += step let pos = find_first_not_space(a:content, pos, end, step) if pos == end break endif let begin_pos = pos if stack.len() == 0 " let = as a delim, it's the next begining call stack.push(a:delim[0]) endif continue else " backword " if stack is empty, we need to find the first begin or delim " if stack more than 1, current maybe in the nest scope, ignore it if stack.len() != 1 let pos += step continue else " if stack len is 1, and current pos must be want to select break endif endif elseif stridx(a:border_begin, a:content[pos]) != -1 if a:content[pos] ==# '>' && step < 0 " check if there are is a '<' or not let tmppos = pos + step while tmppos >= 0 && a:content[tmppos] !=# '<' let tmppos += step endwhile if tmppos < 0 let pos += step continue endif endif call stack.push(a:content[pos]) if stack.len() == 1 " begin let pos += step let pos = find_first_not_space(a:content, pos, end, step) if pos == end break endif let begin_pos = pos " no need to step forward " goto the beginning of the loop continue endif elseif a:content[pos] ==# a:delim if stack.empty() call stack.push(a:content[pos]) let pos += step let pos = find_first_not_space(a:content, pos, end, step) if pos == end break endif let begin_pos = pos " no need to step forward " goto the beginning of the loop continue elseif stack.len() == 1 && a:content[pos] =~# border_matcher[stack.top()] call stack.pop() if stack.empty() " match delim break endif endif elseif stridx(a:border_end, a:content[pos]) != -1 if a:content[pos] ==# '<' && step > 0 " check if there are is a '>' or not let tmppos = pos + step while tmppos < content_len && a:content[tmppos] !=# '>' let tmppos += step endwhile if tmppos >= content_len let pos += step continue endif endif if stack.empty() let begin_pos = pos let end_pos = pos else if a:content[pos] =~# border_matcher[stack.top()] " border match, then pop call stack.pop() if stack.empty() " match delim break endif endif endif endif let pos += step endwhile "}}}2 if pos == end if begin_pos != 0 && end_pos != 0 return [begin_pos+1,end_pos+1] else return [0, 0] endif endif if begin_pos != pos let pos -= step " find previous no space while pos != begin_pos && a:content[pos] =~# '\s' let pos -= step endwhile endif let end_pos = pos if begin_pos == end_pos && stridx(a:border_end, a:content[end_pos]) != -1 return [begin_pos+1, end_pos+1] endif if end_pos < begin_pos let [begin_pos, end_pos] = [end_pos, begin_pos] endif return [begin_pos+1, end_pos+2] endfunction "}}} function! s:find_first_not_space(content, pos, end, step) abort "{{{ let pos = a:pos if pos == -1 || \pos==len(a:content) return pos == a:end endif if a:step == 0 throw 'step is 0' endif while pos != a:end && a:content[pos] =~# '\s' let pos += a:step endwhile return pos endfunction "}}} function! s:log(level, msg) abort "{{{ echom printf("[CompleteParameter][%s][%s][%d] %s", strftime("%T"), a:level, s:log_index, a:msg) endfunction "}}} function! s:error_log(msg) abort "{{{ if g:complete_parameter_log_level <= 4 echohl ErrorMsg call log('ERROR', a:msg) echohl None endif endfunction "}}} function! s:debug_log(msg) abort "{{{ if g:complete_parameter_log_level <= 2 call log('DEBUG', a:msg) endif endfunction "}}} function! s:trace_log(msg) abort "{{{ if g:complete_parameter_log_level <= 1 call log('TRACE', a:msg) endif endfunction "}}}