vim9script noclear # Vim completion script # Language: C # Maintainer: The Vim Project # Last Change: 2023 Aug 10 # Rewritten in Vim9 script by github user lacygoill # Former Maintainer: Bram Moolenaar var prepended: string var grepCache: dict>> # This function is used for the 'omnifunc' option. export def Complete(findstart: bool, abase: string): any # {{{1 if findstart # Locate the start of the item, including ".", "->" and "[...]". var line: string = getline('.') var start: number = charcol('.') - 1 var lastword: number = -1 while start > 0 if line[start - 1] =~ '\w' --start elseif line[start - 1] =~ '\.' if lastword == -1 lastword = start endif --start elseif start > 1 && line[start - 2] == '-' && line[start - 1] == '>' if lastword == -1 lastword = start endif start -= 2 elseif line[start - 1] == ']' # Skip over [...]. var n: number = 0 --start while start > 0 --start if line[start] == '[' if n == 0 break endif --n elseif line[start] == ']' # nested [] ++n endif endwhile else break endif endwhile # Return the column of the last word, which is going to be changed. # Remember the text that comes before it in prepended. if lastword == -1 prepended = '' return byteidx(line, start) endif prepended = line[start : lastword - 1] return byteidx(line, lastword) endif # Return list of matches. var base: string = prepended .. abase # Don't do anything for an empty base, would result in all the tags in the # tags file. if base == '' return [] endif # init cache for vimgrep to empty grepCache = {} # Split item in words, keep empty word after "." or "->". # "aa" -> ['aa'], "aa." -> ['aa', ''], "aa.bb" -> ['aa', 'bb'], etc. # We can't use split, because we need to skip nested [...]. # "aa[...]" -> ['aa', '[...]'], "aa.bb[...]" -> ['aa', 'bb', '[...]'], etc. var items: list var s: number = 0 var arrays: number = 0 while 1 var e: number = base->charidx(match(base, '\.\|->\|\[', s)) if e < 0 if s == 0 || base[s - 1] != ']' items->add(base[s :]) endif break endif if s == 0 || base[s - 1] != ']' items->add(base[s : e - 1]) endif if base[e] == '.' # skip over '.' s = e + 1 elseif base[e] == '-' # skip over '->' s = e + 2 else # Skip over [...]. var n: number = 0 s = e ++e while e < strcharlen(base) if base[e] == ']' if n == 0 break endif --n elseif base[e] == '[' # nested [...] ++n endif ++e endwhile ++e items->add(base[s : e - 1]) ++arrays s = e endif endwhile # Find the variable items[0]. # 1. in current function (like with "gd") # 2. in tags file(s) (like with ":tag") # 3. in current file (like with "gD") var res: list> if items[0]->searchdecl(false, true) == 0 # Found, now figure out the type. # TODO: join previous line if it makes sense var line: string = getline('.') var col: number = charcol('.') if line[: col - 1]->stridx(';') >= 0 # Handle multiple declarations on the same line. var col2: number = col - 1 while line[col2] != ';' --col2 endwhile line = line[col2 + 1 :] col -= col2 endif if line[: col - 1]->stridx(',') >= 0 # Handle multiple declarations on the same line in a function # declaration. var col2: number = col - 1 while line[col2] != ',' --col2 endwhile if line[col2 + 1 : col - 1] =~ ' *[^ ][^ ]* *[^ ]' line = line[col2 + 1 :] col -= col2 endif endif if len(items) == 1 # Completing one word and it's a local variable: May add '[', '.' or # '->'. var match: string = items[0] var kind: string = 'v' if match(line, '\<' .. match .. '\s*\[') > 0 match ..= '[' else res = line[: col - 1]->Nextitem([''], 0, true) if len(res) > 0 # There are members, thus add "." or "->". if match(line, '\*[ \t(]*' .. match .. '\>') > 0 match ..= '->' else match ..= '.' endif endif endif res = [{match: match, tagline: '', kind: kind, info: line}] elseif len(items) == arrays + 1 # Completing one word and it's a local array variable: build tagline # from declaration line var match: string = items[0] var kind: string = 'v' var tagline: string = "\t/^" .. line .. '$/' res = [{match: match, tagline: tagline, kind: kind, info: line}] else # Completing "var.", "var.something", etc. res = line[: col - 1]->Nextitem(items[1 :], 0, true) endif endif if len(items) == 1 || len(items) == arrays + 1 # Only one part, no "." or "->": complete from tags file. var tags: list> if len(items) == 1 tags = taglist('^' .. base) else tags = taglist('^' .. items[0] .. '$') endif tags # Remove members, these can't appear without something in front. ->filter((_, v: dict): bool => v->has_key('kind') ? v.kind != 'm' : true) # Remove static matches in other files. ->filter((_, v: dict): bool => !v->has_key('static') || !v['static'] || bufnr('%') == bufnr(v['filename'])) res = res->extend(tags->map((_, v: dict) => Tag2item(v))) endif if len(res) == 0 # Find the variable in the tags file(s) var diclist: list> = taglist('^' .. items[0] .. '$') # Remove members, these can't appear without something in front. ->filter((_, v: dict): bool => v->has_key('kind') ? v.kind != 'm' : true) res = [] for i: number in len(diclist)->range() # New ctags has the "typeref" field. Patched version has "typename". if diclist[i]->has_key('typename') res = res->extend(diclist[i]['typename']->StructMembers(items[1 :], true)) elseif diclist[i]->has_key('typeref') res = res->extend(diclist[i]['typeref']->StructMembers(items[1 :], true)) endif # For a variable use the command, which must be a search pattern that # shows the declaration of the variable. if diclist[i]['kind'] == 'v' var line: string = diclist[i]['cmd'] if line[: 1] == '/^' var col: number = line->charidx(match(line, '\<' .. items[0] .. '\>')) res = res->extend(line[2 : col - 1]->Nextitem(items[1 :], 0, true)) endif endif endfor endif if len(res) == 0 && items[0]->searchdecl(true) == 0 # Found, now figure out the type. # TODO: join previous line if it makes sense var line: string = getline('.') var col: number = charcol('.') res = line[: col - 1]->Nextitem(items[1 :], 0, true) endif # If the last item(s) are [...] they need to be added to the matches. var last: number = len(items) - 1 var brackets: string = '' while last >= 0 if items[last][0] != '[' break endif brackets = items[last] .. brackets --last endwhile return res->map((_, v: dict): dict => Tagline2item(v, brackets)) enddef def GetAddition( # {{{1 line: string, match: string, memarg: list>, bracket: bool): string # Guess if the item is an array. if bracket && match(line, match .. '\s*\[') > 0 return '[' endif # Check if the item has members. if SearchMembers(memarg, [''], false)->len() > 0 # If there is a '*' before the name use "->". if match(line, '\*[ \t(]*' .. match .. '\>') > 0 return '->' else return '.' endif endif return '' enddef def Tag2item(val: dict): dict # {{{1 # Turn the tag info "val" into an item for completion. # "val" is is an item in the list returned by taglist(). # If it is a variable we may add "." or "->". Don't do it for other types, # such as a typedef, by not including the info that GetAddition() uses. var res: dict = {match: val['name']} res['extra'] = Tagcmd2extra(val['cmd'], val['name'], val['filename']) var s: string = Dict2info(val) if s != '' res['info'] = s endif res['tagline'] = '' if val->has_key('kind') var kind: string = val['kind'] res['kind'] = kind if kind == 'v' res['tagline'] = "\t" .. val['cmd'] res['dict'] = val elseif kind == 'f' res['match'] = val['name'] .. '(' endif endif return res enddef def Dict2info(dict: dict): string # {{{1 # Use all the items in dictionary for the "info" entry. var info: string = '' for k: string in dict->keys()->sort() info ..= k .. repeat(' ', 10 - strlen(k)) if k == 'cmd' info ..= dict['cmd'] ->matchstr('/^\s*\zs.*\ze$/') ->substitute('\\\(.\)', '\1', 'g') else var dictk: any = dict[k] if typename(dictk) != 'string' info ..= dictk->string() else info ..= dictk endif endif info ..= "\n" endfor return info enddef def ParseTagline(line: string): dict # {{{1 # Parse a tag line and return a dictionary with items like taglist() var l: list = split(line, "\t") var d: dict if len(l) >= 3 d['name'] = l[0] d['filename'] = l[1] d['cmd'] = l[2] var n: number = 2 if l[2] =~ '^/' # Find end of cmd, it may contain Tabs. while n < len(l) && l[n] !~ '/;"$' ++n d['cmd'] ..= ' ' .. l[n] endwhile endif for i: number in range(n + 1, len(l) - 1) if l[i] == 'file:' d['static'] = 1 elseif l[i] !~ ':' d['kind'] = l[i] else d[l[i]->matchstr('[^:]*')] = l[i]->matchstr(':\zs.*') endif endfor endif return d enddef def Tagline2item(val: dict, brackets: string): dict # {{{1 # Turn a match item "val" into an item for completion. # "val['match']" is the matching item. # "val['tagline']" is the tagline in which the last part was found. var line: string = val['tagline'] var add: string = GetAddition(line, val['match'], [val], brackets == '') var res: dict = {word: val['match'] .. brackets .. add} if val->has_key('info') # Use info from Tag2item(). res['info'] = val['info'] else # Parse the tag line and add each part to the "info" entry. var s: string = ParseTagline(line)->Dict2info() if s != '' res['info'] = s endif endif if val->has_key('kind') res['kind'] = val['kind'] elseif add == '(' res['kind'] = 'f' else var s: string = line->matchstr('\t\(kind:\)\=\zs\S\ze\(\t\|$\)') if s != '' res['kind'] = s endif endif if val->has_key('extra') res['menu'] = val['extra'] return res endif # Isolate the command after the tag and filename. var s: string = line->matchstr('[^\t]*\t[^\t]*\t\zs\(/^.*$/\|[^\t]*\)\ze\(;"\t\|\t\|$\)') if s != '' res['menu'] = s->Tagcmd2extra(val['match'], line->matchstr('[^\t]*\t\zs[^\t]*\ze\t')) endif return res enddef def Tagcmd2extra( # {{{1 cmd: string, name: string, fname: string): string # Turn a command from a tag line to something that is useful in the menu var x: string if cmd =~ '^/^' # The command is a search command, useful to see what it is. x = cmd ->matchstr('^/^\s*\zs.*\ze$/') ->substitute('\<' .. name .. '\>', '@@', '') ->substitute('\\\(.\)', '\1', 'g') .. ' - ' .. fname elseif cmd =~ '^\d*$' # The command is a line number, the file name is more useful. x = fname .. ' - ' .. cmd else # Not recognized, use command and file name. x = cmd .. ' - ' .. fname endif return x enddef def Nextitem( # {{{1 lead: string, items: list, depth: number, all: bool): list> # Find composing type in "lead" and match items[0] with it. # Repeat this recursively for items[1], if it's there. # When resolving typedefs "depth" is used to avoid infinite recursion. # Return the list of matches. # Use the text up to the variable name and split it in tokens. var tokens: list = split(lead, '\s\+\|\<') # Try to recognize the type of the variable. This is rough guessing... var res: list> for tidx: number in len(tokens)->range() # Skip tokens starting with a non-ID character. if tokens[tidx] !~ '^\h' continue endif # Recognize "struct foobar" and "union foobar". # Also do "class foobar" when it's C++ after all (doesn't work very well # though). if (tokens[tidx] == 'struct' || tokens[tidx] == 'union' || tokens[tidx] == 'class') && tidx + 1 < len(tokens) res = StructMembers(tokens[tidx] .. ':' .. tokens[tidx + 1], items, all) break endif # TODO: add more reserved words if ['int', 'short', 'char', 'float', 'double', 'static', 'unsigned', 'extern']->index(tokens[tidx]) >= 0 continue endif # Use the tags file to find out if this is a typedef. var diclist: list> = taglist('^' .. tokens[tidx] .. '$') for tagidx: number in len(diclist)->range() var item: dict = diclist[tagidx] # New ctags has the "typeref" field. Patched version has "typename". if item->has_key('typeref') res = res->extend(item['typeref']->StructMembers(items, all)) continue endif if item->has_key('typename') res = res->extend(item['typename']->StructMembers(items, all)) continue endif # Only handle typedefs here. if item['kind'] != 't' continue endif # Skip matches local to another file. if item->has_key('static') && item['static'] && bufnr('%') != bufnr(item['filename']) continue endif # For old ctags we recognize "typedef struct aaa" and # "typedef union bbb" in the tags file command. var cmd: string = item['cmd'] var ei: number = cmd->charidx(matchend(cmd, 'typedef\s\+')) if ei > 1 var cmdtokens: list = cmd[ei :]->split('\s\+\|\<') if len(cmdtokens) > 1 if cmdtokens[0] == 'struct' || cmdtokens[0] == 'union' || cmdtokens[0] == 'class' var name: string = '' # Use the first identifier after the "struct" or "union" for ti: number in (len(cmdtokens) - 1)->range() if cmdtokens[ti] =~ '^\w' name = cmdtokens[ti] break endif endfor if name != '' res = res->extend(StructMembers(cmdtokens[0] .. ':' .. name, items, all)) endif elseif depth < 10 # Could be "typedef other_T some_T". res = res->extend(cmdtokens[0]->Nextitem(items, depth + 1, all)) endif endif endif endfor if len(res) > 0 break endif endfor return res enddef def StructMembers( # {{{1 atypename: string, items: list, all: bool): list> # Search for members of structure "typename" in tags files. # Return a list with resulting matches. # Each match is a dictionary with "match" and "tagline" entries. # When "all" is true find all, otherwise just return 1 if there is any member. # Todo: What about local structures? var fnames: string = tagfiles() ->map((_, v: string) => escape(v, ' \#%')) ->join() if fnames == '' return [] endif var typename: string = atypename var qflist: list> var cached: number = 0 var n: string if !all n = '1' # stop at first found match if grepCache->has_key(typename) qflist = grepCache[typename] cached = 1 endif else n = '' endif if !cached while 1 execute 'silent! keepjumps noautocmd ' .. n .. 'vimgrep ' .. '/\t' .. typename .. '\(\t\|$\)/j ' .. fnames qflist = getqflist() if len(qflist) > 0 || match(typename, '::') < 0 break endif # No match for "struct:context::name", remove "context::" and try again. typename = typename->substitute(':[^:]*::', ':', '') endwhile if !all # Store the result to be able to use it again later. grepCache[typename] = qflist endif endif # Skip over [...] items var idx: number = 0 var target: string while 1 if idx >= len(items) target = '' # No further items, matching all members break endif if items[idx][0] != '[' target = items[idx] break endif ++idx endwhile # Put matching members in matches[]. var matches: list> for l: dict in qflist var memb: string = l['text']->matchstr('[^\t]*') if memb =~ '^' .. target # Skip matches local to another file. if match(l['text'], "\tfile:") < 0 || bufnr('%') == l['text']->matchstr('\t\zs[^\t]*')->bufnr() var item: dict = {match: memb, tagline: l['text']} # Add the kind of item. var s: string = l['text']->matchstr('\t\(kind:\)\=\zs\S\ze\(\t\|$\)') if s != '' item['kind'] = s if s == 'f' item['match'] = memb .. '(' endif endif matches->add(item) endif endif endfor if len(matches) > 0 # Skip over next [...] items ++idx while 1 if idx >= len(items) return matches # No further items, return the result. endif if items[idx][0] != '[' break endif ++idx endwhile # More items following. For each of the possible members find the # matching following members. return SearchMembers(matches, items[idx :], all) endif # Failed to find anything. return [] enddef def SearchMembers( # {{{1 matches: list>, items: list, all: bool): list> # For matching members, find matches for following items. # When "all" is true find all, otherwise just return 1 if there is any member. var res: list> for i: number in len(matches)->range() var typename: string = '' var line: string if matches[i]->has_key('dict') if matches[i]['dict']->has_key('typename') typename = matches[i]['dict']['typename'] elseif matches[i]['dict']->has_key('typeref') typename = matches[i]['dict']['typeref'] endif line = "\t" .. matches[i]['dict']['cmd'] else line = matches[i]['tagline'] var eb: number = matchend(line, '\ttypename:') var e: number = charidx(line, eb) if e < 0 eb = matchend(line, '\ttyperef:') e = charidx(line, eb) endif if e > 0 # Use typename field typename = line->matchstr('[^\t]*', eb) endif endif if typename != '' res = res->extend(StructMembers(typename, items, all)) else # Use the search command (the declaration itself). var sb: number = line->match('\t\zs/^') var s: number = charidx(line, sb) if s > 0 var e: number = line ->charidx(match(line, '\<' .. matches[i]['match'] .. '\>', sb)) if e > 0 res = res->extend(line[s : e - 1]->Nextitem(items, 0, all)) endif endif endif if !all && len(res) > 0 break endif endfor return res enddef #}}}1 # vim: noet sw=2 sts=2