neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

ccomplete.vim (19493B)


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