view runtime/autoload/ccomplete.vim @ 34359:0447bf3a88a5 v9.1.0110

patch 9.1.0110: filetype: add 'Config.in' filetype detection Commit: https://github.com/vim/vim/commit/5f20f050efed3431beaf85739f0113e9ef0abd8e Author: Brandon Maier <brandon.maier@collins.com> Date: Wed Feb 14 22:30:06 2024 +0100 patch 9.1.0110: filetype: add 'Config.in' filetype detection The 'Config.in' file type is for Buildroot configuration files. Buildroot Config.in files use the same Kconfig backend as the Linux kernel's Kconfig files. Buildroot also has other filename variants that follow "Config.in.*", they are used to distinguish multiple Config.in files in the same directory. See https://buildroot.org/downloads/manual/manual.html#_literal_config_in_literal_file closes: #14038 Signed-off-by: Brandon Maier <brandon.maier@collins.com> Signed-off-by: Christian Brabandt <cb@256bit.org>
author Christian Brabandt <cb@256bit.org>
date Wed, 14 Feb 2024 22:45:02 +0100
parents 4027cefc2aab
children 9dfa17769328
line wrap: on
line source

vim9script noclear

# Vim completion script
# Language:	C
# Maintainer:	The Vim Project <https://github.com/vim/vim>
# Last Change:	2023 Aug 10
#		Rewritten in Vim9 script by github user lacygoill
# Former Maintainer:   Bram Moolenaar <Bram@vim.org>

var prepended: string
var grepCache: dict<list<dict<any>>>

# 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<string>
  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<dict<any>>
  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<dict<any>>
    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<any>): bool =>
                v->has_key('kind') ? v.kind != 'm' : true)
      # Remove static matches in other files.
      ->filter((_, v: dict<any>): bool =>
                 !v->has_key('static')
              || !v['static']
              || bufnr('%') == bufnr(v['filename']))

    res = res->extend(tags->map((_, v: dict<any>) => Tag2item(v)))
  endif

  if len(res) == 0
    # Find the variable in the tags file(s)
    var diclist: list<dict<any>> = taglist('^' .. items[0] .. '$')
      # Remove members, these can't appear without something in front.
      ->filter((_, v: dict<string>): 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<any>): dict<string> => Tagline2item(v, brackets))
enddef

def GetAddition( # {{{1
    line: string,
    match: string,
    memarg: list<dict<any>>,
    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<any>): dict<any> # {{{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<any> = {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<any>): 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<any> # {{{1
# Parse a tag line and return a dictionary with items like taglist()
  var l: list<string> = split(line, "\t")
  var d: dict<any>
  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<any>, brackets: string): dict<string> # {{{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<string> = {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<string>,
    depth: number,
    all: bool): list<dict<string>>
# 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<string> = split(lead, '\s\+\|\<')

  # Try to recognize the type of the variable.  This is rough guessing...
  var res: list<dict<string>>
  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<dict<any>> = taglist('^' .. tokens[tidx] .. '$')
    for tagidx: number in len(diclist)->range()
      var item: dict<any> = 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<string> = 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<string>,
    all: bool): list<dict<string>>

# 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<dict<any>>
  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<dict<string>>
  for l: dict<any> 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<string> = {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<dict<any>>,
    items: list<string>,
    all: bool): list<dict<string>>

# 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<dict<string>>
  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