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