1" Vim completion script
2" Language:	C
3" Maintainer:	Bram Moolenaar <[email protected]>
4" Last Change:	2020 Nov 14
5
6let s:cpo_save = &cpo
7set cpo&vim
8
9" This function is used for the 'omnifunc' option.
10func ccomplete#Complete(findstart, base)
11  if a:findstart
12    " Locate the start of the item, including ".", "->" and "[...]".
13    let line = getline('.')
14    let start = col('.') - 1
15    let lastword = -1
16    while start > 0
17      if line[start - 1] =~ '\w'
18	let start -= 1
19      elseif line[start - 1] =~ '\.'
20	if lastword == -1
21	  let lastword = start
22	endif
23	let start -= 1
24      elseif start > 1 && line[start - 2] == '-' && line[start - 1] == '>'
25	if lastword == -1
26	  let lastword = start
27	endif
28	let start -= 2
29      elseif line[start - 1] == ']'
30	" Skip over [...].
31	let n = 0
32	let start -= 1
33	while start > 0
34	  let start -= 1
35	  if line[start] == '['
36	    if n == 0
37	      break
38	    endif
39	    let n -= 1
40	  elseif line[start] == ']'  " nested []
41	    let n += 1
42	  endif
43	endwhile
44      else
45	break
46      endif
47    endwhile
48
49    " Return the column of the last word, which is going to be changed.
50    " Remember the text that comes before it in s:prepended.
51    if lastword == -1
52      let s:prepended = ''
53      return start
54    endif
55    let s:prepended = strpart(line, start, lastword - start)
56    return lastword
57  endif
58
59  " Return list of matches.
60
61  let base = s:prepended . a:base
62
63  " Don't do anything for an empty base, would result in all the tags in the
64  " tags file.
65  if base == ''
66    return []
67  endif
68
69  " init cache for vimgrep to empty
70  let s:grepCache = {}
71
72  " Split item in words, keep empty word after "." or "->".
73  " "aa" -> ['aa'], "aa." -> ['aa', ''], "aa.bb" -> ['aa', 'bb'], etc.
74  " We can't use split, because we need to skip nested [...].
75  " "aa[...]" -> ['aa', '[...]'], "aa.bb[...]" -> ['aa', 'bb', '[...]'], etc.
76  let items = []
77  let s = 0
78  let arrays = 0
79  while 1
80    let e = match(base, '\.\|->\|\[', s)
81    if e < 0
82      if s == 0 || base[s - 1] != ']'
83	call add(items, strpart(base, s))
84      endif
85      break
86    endif
87    if s == 0 || base[s - 1] != ']'
88      call add(items, strpart(base, s, e - s))
89    endif
90    if base[e] == '.'
91      let s = e + 1	" skip over '.'
92    elseif base[e] == '-'
93      let s = e + 2	" skip over '->'
94    else
95      " Skip over [...].
96      let n = 0
97      let s = e
98      let e += 1
99      while e < len(base)
100	if base[e] == ']'
101	  if n == 0
102	    break
103	  endif
104	  let n -= 1
105	elseif base[e] == '['  " nested [...]
106	  let n += 1
107	endif
108	let e += 1
109      endwhile
110      let e += 1
111      call add(items, strpart(base, s, e - s))
112      let arrays += 1
113      let s = e
114    endif
115  endwhile
116
117  " Find the variable items[0].
118  " 1. in current function (like with "gd")
119  " 2. in tags file(s) (like with ":tag")
120  " 3. in current file (like with "gD")
121  let res = []
122  if searchdecl(items[0], 0, 1) == 0
123    " Found, now figure out the type.
124    " TODO: join previous line if it makes sense
125    let line = getline('.')
126    let col = col('.')
127    if stridx(strpart(line, 0, col), ';') != -1
128      " Handle multiple declarations on the same line.
129      let col2 = col - 1
130      while line[col2] != ';'
131	let col2 -= 1
132      endwhile
133      let line = strpart(line, col2 + 1)
134      let col -= col2
135    endif
136    if stridx(strpart(line, 0, col), ',') != -1
137      " Handle multiple declarations on the same line in a function
138      " declaration.
139      let col2 = col - 1
140      while line[col2] != ','
141	let col2 -= 1
142      endwhile
143      if strpart(line, col2 + 1, col - col2 - 1) =~ ' *[^ ][^ ]*  *[^ ]'
144	let line = strpart(line, col2 + 1)
145	let col -= col2
146      endif
147    endif
148    if len(items) == 1
149      " Completing one word and it's a local variable: May add '[', '.' or
150      " '->'.
151      let match = items[0]
152      let kind = 'v'
153      if match(line, '\<' . match . '\s*\[') > 0
154	let match .= '['
155      else
156	let res = s:Nextitem(strpart(line, 0, col), [''], 0, 1)
157	if len(res) > 0
158	  " There are members, thus add "." or "->".
159	  if match(line, '\*[ \t(]*' . match . '\>') > 0
160	    let match .= '->'
161	  else
162	    let match .= '.'
163	  endif
164	endif
165      endif
166      let res = [{'match': match, 'tagline' : '', 'kind' : kind, 'info' : line}]
167    elseif len(items) == arrays + 1
168      " Completing one word and it's a local array variable: build tagline
169      " from declaration line
170      let match = items[0]
171      let kind = 'v'
172      let tagline = "\t/^" . line . '$/'
173      let res = [{'match': match, 'tagline' : tagline, 'kind' : kind, 'info' : line}]
174    else
175      " Completing "var.", "var.something", etc.
176      let res = s:Nextitem(strpart(line, 0, col), items[1:], 0, 1)
177    endif
178  endif
179
180  if len(items) == 1 || len(items) == arrays + 1
181    " Only one part, no "." or "->": complete from tags file.
182    if len(items) == 1
183      let tags = taglist('^' . base)
184    else
185      let tags = taglist('^' . items[0] . '$')
186    endif
187
188    " Remove members, these can't appear without something in front.
189    call filter(tags, 'has_key(v:val, "kind") ? v:val["kind"] != "m" : 1')
190
191    " Remove static matches in other files.
192    call filter(tags, '!has_key(v:val, "static") || !v:val["static"] || bufnr("%") == bufnr(v:val["filename"])')
193
194    call extend(res, map(tags, 's:Tag2item(v:val)'))
195  endif
196
197  if len(res) == 0
198    " Find the variable in the tags file(s)
199    let diclist = taglist('^' . items[0] . '$')
200
201    " Remove members, these can't appear without something in front.
202    call filter(diclist, 'has_key(v:val, "kind") ? v:val["kind"] != "m" : 1')
203
204    let res = []
205    for i in range(len(diclist))
206      " New ctags has the "typeref" field.  Patched version has "typename".
207      if has_key(diclist[i], 'typename')
208	call extend(res, s:StructMembers(diclist[i]['typename'], items[1:], 1))
209      elseif has_key(diclist[i], 'typeref')
210	call extend(res, s:StructMembers(diclist[i]['typeref'], items[1:], 1))
211      endif
212
213      " For a variable use the command, which must be a search pattern that
214      " shows the declaration of the variable.
215      if diclist[i]['kind'] == 'v'
216	let line = diclist[i]['cmd']
217	if line[0] == '/' && line[1] == '^'
218	  let col = match(line, '\<' . items[0] . '\>')
219	  call extend(res, s:Nextitem(strpart(line, 2, col - 2), items[1:], 0, 1))
220	endif
221      endif
222    endfor
223  endif
224
225  if len(res) == 0 && searchdecl(items[0], 1) == 0
226    " Found, now figure out the type.
227    " TODO: join previous line if it makes sense
228    let line = getline('.')
229    let col = col('.')
230    let res = s:Nextitem(strpart(line, 0, col), items[1:], 0, 1)
231  endif
232
233  " If the last item(s) are [...] they need to be added to the matches.
234  let last = len(items) - 1
235  let brackets = ''
236  while last >= 0
237    if items[last][0] != '['
238      break
239    endif
240    let brackets = items[last] . brackets
241    let last -= 1
242  endwhile
243
244  return map(res, 's:Tagline2item(v:val, brackets)')
245endfunc
246
247func s:GetAddition(line, match, memarg, bracket)
248  " Guess if the item is an array.
249  if a:bracket && match(a:line, a:match . '\s*\[') > 0
250    return '['
251  endif
252
253  " Check if the item has members.
254  if len(s:SearchMembers(a:memarg, [''], 0)) > 0
255    " If there is a '*' before the name use "->".
256    if match(a:line, '\*[ \t(]*' . a:match . '\>') > 0
257      return '->'
258    else
259      return '.'
260    endif
261  endif
262  return ''
263endfunc
264
265" Turn the tag info "val" into an item for completion.
266" "val" is is an item in the list returned by taglist().
267" If it is a variable we may add "." or "->".  Don't do it for other types,
268" such as a typedef, by not including the info that s:GetAddition() uses.
269func s:Tag2item(val)
270  let res = {'match': a:val['name']}
271
272  let res['extra'] = s:Tagcmd2extra(a:val['cmd'], a:val['name'], a:val['filename'])
273
274  let s = s:Dict2info(a:val)
275  if s != ''
276    let res['info'] = s
277  endif
278
279  let res['tagline'] = ''
280  if has_key(a:val, "kind")
281    let kind = a:val['kind']
282    let res['kind'] = kind
283    if kind == 'v'
284      let res['tagline'] = "\t" . a:val['cmd']
285      let res['dict'] = a:val
286    elseif kind == 'f'
287      let res['match'] = a:val['name'] . '('
288    endif
289  endif
290
291  return res
292endfunc
293
294" Use all the items in dictionary for the "info" entry.
295func s:Dict2info(dict)
296  let info = ''
297  for k in sort(keys(a:dict))
298    let info  .= k . repeat(' ', 10 - len(k))
299    if k == 'cmd'
300      let info .= substitute(matchstr(a:dict['cmd'], '/^\s*\zs.*\ze$/'), '\\\(.\)', '\1', 'g')
301    else
302      let info .= a:dict[k]
303    endif
304    let info .= "\n"
305  endfor
306  return info
307endfunc
308
309" Parse a tag line and return a dictionary with items like taglist()
310func s:ParseTagline(line)
311  let l = split(a:line, "\t")
312  let d = {}
313  if len(l) >= 3
314    let d['name'] = l[0]
315    let d['filename'] = l[1]
316    let d['cmd'] = l[2]
317    let n = 2
318    if l[2] =~ '^/'
319      " Find end of cmd, it may contain Tabs.
320      while n < len(l) && l[n] !~ '/;"$'
321	let n += 1
322	let d['cmd'] .= "  " . l[n]
323      endwhile
324    endif
325    for i in range(n + 1, len(l) - 1)
326      if l[i] == 'file:'
327	let d['static'] = 1
328      elseif l[i] !~ ':'
329	let d['kind'] = l[i]
330      else
331	let d[matchstr(l[i], '[^:]*')] = matchstr(l[i], ':\zs.*')
332      endif
333    endfor
334  endif
335
336  return d
337endfunc
338
339" Turn a match item "val" into an item for completion.
340" "val['match']" is the matching item.
341" "val['tagline']" is the tagline in which the last part was found.
342func s:Tagline2item(val, brackets)
343  let line = a:val['tagline']
344  let add = s:GetAddition(line, a:val['match'], [a:val], a:brackets == '')
345  let res = {'word': a:val['match'] . a:brackets . add }
346
347  if has_key(a:val, 'info')
348    " Use info from Tag2item().
349    let res['info'] = a:val['info']
350  else
351    " Parse the tag line and add each part to the "info" entry.
352    let s = s:Dict2info(s:ParseTagline(line))
353    if s != ''
354      let res['info'] = s
355    endif
356  endif
357
358  if has_key(a:val, 'kind')
359    let res['kind'] = a:val['kind']
360  elseif add == '('
361    let res['kind'] = 'f'
362  else
363    let s = matchstr(line, '\t\(kind:\)\=\zs\S\ze\(\t\|$\)')
364    if s != ''
365      let res['kind'] = s
366    endif
367  endif
368
369  if has_key(a:val, 'extra')
370    let res['menu'] = a:val['extra']
371    return res
372  endif
373
374  " Isolate the command after the tag and filename.
375  let s = matchstr(line, '[^\t]*\t[^\t]*\t\zs\(/^.*$/\|[^\t]*\)\ze\(;"\t\|\t\|$\)')
376  if s != ''
377    let res['menu'] = s:Tagcmd2extra(s, a:val['match'], matchstr(line, '[^\t]*\t\zs[^\t]*\ze\t'))
378  endif
379  return res
380endfunc
381
382" Turn a command from a tag line to something that is useful in the menu
383func s:Tagcmd2extra(cmd, name, fname)
384  if a:cmd =~ '^/^'
385    " The command is a search command, useful to see what it is.
386    let x = matchstr(a:cmd, '^/^\s*\zs.*\ze$/')
387    let x = substitute(x, '\<' . a:name . '\>', '@@', '')
388    let x = substitute(x, '\\\(.\)', '\1', 'g')
389    let x = x . ' - ' . a:fname
390  elseif a:cmd =~ '^\d*$'
391    " The command is a line number, the file name is more useful.
392    let x = a:fname . ' - ' . a:cmd
393  else
394    " Not recognized, use command and file name.
395    let x = a:cmd . ' - ' . a:fname
396  endif
397  return x
398endfunc
399
400" Find composing type in "lead" and match items[0] with it.
401" Repeat this recursively for items[1], if it's there.
402" When resolving typedefs "depth" is used to avoid infinite recursion.
403" Return the list of matches.
404func s:Nextitem(lead, items, depth, all)
405
406  " Use the text up to the variable name and split it in tokens.
407  let tokens = split(a:lead, '\s\+\|\<')
408
409  " Try to recognize the type of the variable.  This is rough guessing...
410  let res = []
411  for tidx in range(len(tokens))
412
413    " Skip tokens starting with a non-ID character.
414    if tokens[tidx] !~ '^\h'
415      continue
416    endif
417
418    " Recognize "struct foobar" and "union foobar".
419    " Also do "class foobar" when it's C++ after all (doesn't work very well
420    " though).
421    if (tokens[tidx] == 'struct' || tokens[tidx] == 'union' || tokens[tidx] == 'class') && tidx + 1 < len(tokens)
422      let res = s:StructMembers(tokens[tidx] . ':' . tokens[tidx + 1], a:items, a:all)
423      break
424    endif
425
426    " TODO: add more reserved words
427    if index(['int', 'short', 'char', 'float', 'double', 'static', 'unsigned', 'extern'], tokens[tidx]) >= 0
428      continue
429    endif
430
431    " Use the tags file to find out if this is a typedef.
432    let diclist = taglist('^' . tokens[tidx] . '$')
433    for tagidx in range(len(diclist))
434      let item = diclist[tagidx]
435
436      " New ctags has the "typeref" field.  Patched version has "typename".
437      if has_key(item, 'typeref')
438	call extend(res, s:StructMembers(item['typeref'], a:items, a:all))
439	continue
440      endif
441      if has_key(item, 'typename')
442	call extend(res, s:StructMembers(item['typename'], a:items, a:all))
443	continue
444      endif
445
446      " Only handle typedefs here.
447      if item['kind'] != 't'
448	continue
449      endif
450
451      " Skip matches local to another file.
452      if has_key(item, 'static') && item['static'] && bufnr('%') != bufnr(item['filename'])
453	continue
454      endif
455
456      " For old ctags we recognize "typedef struct aaa" and
457      " "typedef union bbb" in the tags file command.
458      let cmd = item['cmd']
459      let ei = matchend(cmd, 'typedef\s\+')
460      if ei > 1
461	let cmdtokens = split(strpart(cmd, ei), '\s\+\|\<')
462	if len(cmdtokens) > 1
463	  if cmdtokens[0] == 'struct' || cmdtokens[0] == 'union' || cmdtokens[0] == 'class'
464	    let name = ''
465	    " Use the first identifier after the "struct" or "union"
466	    for ti in range(len(cmdtokens) - 1)
467	      if cmdtokens[ti] =~ '^\w'
468		let name = cmdtokens[ti]
469		break
470	      endif
471	    endfor
472	    if name != ''
473	      call extend(res, s:StructMembers(cmdtokens[0] . ':' . name, a:items, a:all))
474	    endif
475	  elseif a:depth < 10
476	    " Could be "typedef other_T some_T".
477	    call extend(res, s:Nextitem(cmdtokens[0], a:items, a:depth + 1, a:all))
478	  endif
479	endif
480      endif
481    endfor
482    if len(res) > 0
483      break
484    endif
485  endfor
486
487  return res
488endfunc
489
490
491" Search for members of structure "typename" in tags files.
492" Return a list with resulting matches.
493" Each match is a dictionary with "match" and "tagline" entries.
494" When "all" is non-zero find all, otherwise just return 1 if there is any
495" member.
496func s:StructMembers(typename, items, all)
497  " Todo: What about local structures?
498  let fnames = join(map(tagfiles(), 'escape(v:val, " \\#%")'))
499  if fnames == ''
500    return []
501  endif
502
503  let typename = a:typename
504  let qflist = []
505  let cached = 0
506  if a:all == 0
507    let n = '1'	" stop at first found match
508    if has_key(s:grepCache, a:typename)
509      let qflist = s:grepCache[a:typename]
510      let cached = 1
511    endif
512  else
513    let n = ''
514  endif
515  if !cached
516    while 1
517      exe 'silent! keepj noautocmd ' . n . 'vimgrep /\t' . typename . '\(\t\|$\)/j ' . fnames
518
519      let qflist = getqflist()
520      if len(qflist) > 0 || match(typename, "::") < 0
521	break
522      endif
523      " No match for "struct:context::name", remove "context::" and try again.
524      let typename = substitute(typename, ':[^:]*::', ':', '')
525    endwhile
526
527    if a:all == 0
528      " Store the result to be able to use it again later.
529      let s:grepCache[a:typename] = qflist
530    endif
531  endif
532
533  " Skip over [...] items
534  let idx = 0
535  while 1
536    if idx >= len(a:items)
537      let target = ''		" No further items, matching all members
538      break
539    endif
540    if a:items[idx][0] != '['
541      let target = a:items[idx]
542      break
543    endif
544    let idx += 1
545  endwhile
546  " Put matching members in matches[].
547  let matches = []
548  for l in qflist
549    let memb = matchstr(l['text'], '[^\t]*')
550    if memb =~ '^' . target
551      " Skip matches local to another file.
552      if match(l['text'], "\tfile:") < 0 || bufnr('%') == bufnr(matchstr(l['text'], '\t\zs[^\t]*'))
553	let item = {'match': memb, 'tagline': l['text']}
554
555	" Add the kind of item.
556	let s = matchstr(l['text'], '\t\(kind:\)\=\zs\S\ze\(\t\|$\)')
557	if s != ''
558	  let item['kind'] = s
559	  if s == 'f'
560	    let item['match'] = memb . '('
561	  endif
562	endif
563
564	call add(matches, item)
565      endif
566    endif
567  endfor
568
569  if len(matches) > 0
570    " Skip over next [...] items
571    let idx += 1
572    while 1
573      if idx >= len(a:items)
574	return matches		" No further items, return the result.
575      endif
576      if a:items[idx][0] != '['
577	break
578      endif
579      let idx += 1
580    endwhile
581
582    " More items following.  For each of the possible members find the
583    " matching following members.
584    return s:SearchMembers(matches, a:items[idx :], a:all)
585  endif
586
587  " Failed to find anything.
588  return []
589endfunc
590
591" For matching members, find matches for following items.
592" When "all" is non-zero find all, otherwise just return 1 if there is any
593" member.
594func s:SearchMembers(matches, items, all)
595  let res = []
596  for i in range(len(a:matches))
597    let typename = ''
598    if has_key(a:matches[i], 'dict')
599      if has_key(a:matches[i].dict, 'typename')
600	let typename = a:matches[i].dict['typename']
601      elseif has_key(a:matches[i].dict, 'typeref')
602	let typename = a:matches[i].dict['typeref']
603      endif
604      let line = "\t" . a:matches[i].dict['cmd']
605    else
606      let line = a:matches[i]['tagline']
607      let e = matchend(line, '\ttypename:')
608      if e < 0
609	let e = matchend(line, '\ttyperef:')
610      endif
611      if e > 0
612	" Use typename field
613	let typename = matchstr(line, '[^\t]*', e)
614      endif
615    endif
616
617    if typename != ''
618      call extend(res, s:StructMembers(typename, a:items, a:all))
619    else
620      " Use the search command (the declaration itself).
621      let s = match(line, '\t\zs/^')
622      if s > 0
623	let e = match(line, '\<' . a:matches[i]['match'] . '\>', s)
624	if e > 0
625	  call extend(res, s:Nextitem(strpart(line, s, e - s), a:items, 0, a:all))
626	endif
627      endif
628    endif
629    if a:all == 0 && len(res) > 0
630      break
631    endif
632  endfor
633  return res
634endfunc
635
636let &cpo = s:cpo_save
637unlet s:cpo_save
638
639" vim: noet sw=2 sts=2
640