neovim

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

commit 8c81ed86786ad155b262cfa04bd56a5794fe97ec
parent 65170e8dad908b15d14b5d68be3ee1a26642ad8b
Author: Christian Clason <c.clason@uni-graz.at>
Date:   Sun, 20 Apr 2025 11:53:49 +0200

feat(runtime): revert cfilter, ccomplete to legacy Vim

Problem: Transpiled Lua code from vim9script is not amenable to static
analysis, requiring manual cleanup or ignoring parts of our codebase.

Solution: Revert to pre-rewrite version of (low-impact) legacy plugins
and remove the vim9jit shim.

Diffstat:
M.luarc.json | 1-
Druntime/autoload/ccomplete.lua | 859-------------------------------------------------------------------------------
Mruntime/autoload/ccomplete.vim | 647++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mruntime/doc/faq.txt | 9+++++----
Druntime/lua/_vim9script.lua | 650-------------------------------------------------------------------------------
Druntime/pack/dist/opt/cfilter/plugin/cfilter.lua | 115-------------------------------------------------------------------------------
Aruntime/pack/dist/opt/cfilter/plugin/cfilter.vim | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dtest/functional/plugin/ccomplete_spec.lua | 63---------------------------------------------------------------
8 files changed, 706 insertions(+), 1700 deletions(-)

diff --git a/.luarc.json b/.luarc.json @@ -10,7 +10,6 @@ ], "ignoreDir": [ "test", - "_vim9script.lua" ], "checkThirdParty": "Disable" }, diff --git a/runtime/autoload/ccomplete.lua b/runtime/autoload/ccomplete.lua @@ -1,859 +0,0 @@ ----------------------------------------- --- This file is generated via github.com/tjdevries/vim9jit --- For any bugs, please first consider reporting there. ----------------------------------------- - --- Ignore "value assigned to a local variable is unused" because --- we can't guarantee that local variables will be used by plugins --- luacheck: ignore ---- @diagnostic disable - -local vim9 = require('_vim9script') -local M = {} -local prepended = nil -local grepCache = nil -local Complete = nil -local GetAddition = nil -local Tag2item = nil -local Dict2info = nil -local ParseTagline = nil -local Tagline2item = nil -local Tagcmd2extra = nil -local Nextitem = nil -local StructMembers = nil -local SearchMembers = nil --- vim9script - --- # 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> - -prepended = '' -grepCache = vim.empty_dict() - --- # This function is used for the 'omnifunc' option. - -Complete = function(findstart, abase) - findstart = vim9.bool(findstart) - if vim9.bool(findstart) then - -- # Locate the start of the item, including ".", "->" and "[...]". - local line = vim9.fn.getline('.') - local start = vim9.fn.charcol('.') - 1 - local lastword = -1 - while start > 0 do - if vim9.ops.RegexpMatches(vim9.index(line, vim9.ops.Minus(start, 1)), '\\w') then - start = start - 1 - elseif - vim9.bool(vim9.ops.RegexpMatches(vim9.index(line, vim9.ops.Minus(start, 1)), '\\.')) - then - if lastword == -1 then - lastword = start - end - start = start - 1 - elseif - vim9.bool( - start > 1 - and vim9.index(line, vim9.ops.Minus(start, 2)) == '-' - and vim9.index(line, vim9.ops.Minus(start, 1)) == '>' - ) - then - if lastword == -1 then - lastword = start - end - start = vim9.ops.Minus(start, 2) - elseif vim9.bool(vim9.index(line, vim9.ops.Minus(start, 1)) == ']') then - -- # Skip over [...]. - local n = 0 - start = start - 1 - while start > 0 do - start = start - 1 - if vim9.index(line, start) == '[' then - if n == 0 then - break - end - n = n - 1 - elseif vim9.bool(vim9.index(line, start) == ']') then - n = n + 1 - end - end - else - break - end - end - - -- # 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 then - prepended = '' - return vim9.fn.byteidx(line, start) - end - prepended = vim9.slice(line, start, vim9.ops.Minus(lastword, 1)) - return vim9.fn.byteidx(line, lastword) - end - - -- # Return list of matches. - - local base = prepended .. abase - - -- # Don't do anything for an empty base, would result in all the tags in the - -- # tags file. - if base == '' then - return {} - end - - -- # 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. - local items = {} - local s = 0 - local arrays = 0 - while 1 do - local e = vim9.fn.charidx(base, vim9.fn.match(base, '\\.\\|->\\|\\[', s)) - if e < 0 then - if s == 0 or vim9.index(base, vim9.ops.Minus(s, 1)) ~= ']' then - vim9.fn.add(items, vim9.slice(base, s, nil)) - end - break - end - if s == 0 or vim9.index(base, vim9.ops.Minus(s, 1)) ~= ']' then - vim9.fn.add(items, vim9.slice(base, s, vim9.ops.Minus(e, 1))) - end - if vim9.index(base, e) == '.' then - -- # skip over '.' - s = vim9.ops.Plus(e, 1) - elseif vim9.bool(vim9.index(base, e) == '-') then - -- # skip over '->' - s = vim9.ops.Plus(e, 2) - else - -- # Skip over [...]. - local n = 0 - s = e - e = e + 1 - while e < vim9.fn.strcharlen(base) do - if vim9.index(base, e) == ']' then - if n == 0 then - break - end - n = n - 1 - elseif vim9.bool(vim9.index(base, e) == '[') then - n = n + 1 - end - e = e + 1 - end - e = e + 1 - vim9.fn.add(items, vim9.slice(base, s, vim9.ops.Minus(e, 1))) - arrays = arrays + 1 - s = e - end - end - - -- # 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") - local res = {} - if vim9.fn.searchdecl(vim9.index(items, 0), false, true) == 0 then - -- # Found, now figure out the type. - -- # TODO: join previous line if it makes sense - local line = vim9.fn.getline('.') - local col = vim9.fn.charcol('.') - if vim9.fn.stridx(vim9.slice(line, nil, vim9.ops.Minus(col, 1)), ';') >= 0 then - -- # Handle multiple declarations on the same line. - local col2 = vim9.ops.Minus(col, 1) - while vim9.index(line, col2) ~= ';' do - col2 = col2 - 1 - end - line = vim9.slice(line, vim9.ops.Plus(col2, 1), nil) - col = vim9.ops.Minus(col, col2) - end - if vim9.fn.stridx(vim9.slice(line, nil, vim9.ops.Minus(col, 1)), ',') >= 0 then - -- # Handle multiple declarations on the same line in a function - -- # declaration. - local col2 = vim9.ops.Minus(col, 1) - while vim9.index(line, col2) ~= ',' do - col2 = col2 - 1 - end - if - vim9.ops.RegexpMatches( - vim9.slice(line, vim9.ops.Plus(col2, 1), vim9.ops.Minus(col, 1)), - ' *[^ ][^ ]* *[^ ]' - ) - then - line = vim9.slice(line, vim9.ops.Plus(col2, 1), nil) - col = vim9.ops.Minus(col, col2) - end - end - if vim9.fn.len(items) == 1 then - -- # Completing one word and it's a local variable: May add '[', '.' or - -- # '->'. - local match = vim9.index(items, 0) - local kind = 'v' - if vim9.fn.match(line, '\\<' .. match .. '\\s*\\[') > 0 then - match = match .. '[' - else - res = Nextitem(vim9.slice(line, nil, vim9.ops.Minus(col, 1)), { '' }, 0, true) - if vim9.fn.len(res) > 0 then - -- # There are members, thus add "." or "->". - if vim9.fn.match(line, '\\*[ \\t(]*' .. match .. '\\>') > 0 then - match = match .. '->' - else - match = match .. '.' - end - end - end - res = { { ['match'] = match, ['tagline'] = '', ['kind'] = kind, ['info'] = line } } - elseif vim9.bool(vim9.fn.len(items) == vim9.ops.Plus(arrays, 1)) then - -- # Completing one word and it's a local array variable: build tagline - -- # from declaration line - local match = vim9.index(items, 0) - local kind = 'v' - local tagline = '\t/^' .. line .. '$/' - res = { { ['match'] = match, ['tagline'] = tagline, ['kind'] = kind, ['info'] = line } } - else - -- # Completing "var.", "var.something", etc. - res = - Nextitem(vim9.slice(line, nil, vim9.ops.Minus(col, 1)), vim9.slice(items, 1, nil), 0, true) - end - end - - if vim9.fn.len(items) == 1 or vim9.fn.len(items) == vim9.ops.Plus(arrays, 1) then - -- # Only one part, no "." or "->": complete from tags file. - local tags = {} - if vim9.fn.len(items) == 1 then - tags = vim9.fn.taglist('^' .. base) - else - tags = vim9.fn.taglist('^' .. vim9.index(items, 0) .. '$') - end - - vim9.fn_mut('filter', { - vim9.fn_mut('filter', { - tags, - function(_, v) - return vim9.ternary(vim9.fn.has_key(v, 'kind'), function() - return v.kind ~= 'm' - end, true) - end, - }, { replace = 0 }), - function(_, v) - return vim9.ops.Or( - vim9.ops.Or( - vim9.prefix['Bang'](vim9.fn.has_key(v, 'static')), - vim9.prefix['Bang'](vim9.index(v, 'static')) - ), - vim9.fn.bufnr('%') == vim9.fn.bufnr(vim9.index(v, 'filename')) - ) - end, - }, { replace = 0 }) - - res = vim9.fn.extend( - res, - vim9.fn.map(tags, function(_, v) - return Tag2item(v) - end) - ) - end - - if vim9.fn.len(res) == 0 then - -- # Find the variable in the tags file(s) - local diclist = vim9.fn.filter( - vim9.fn.taglist('^' .. vim9.index(items, 0) .. '$'), - function(_, v) - return vim9.ternary(vim9.fn.has_key(v, 'kind'), function() - return v.kind ~= 'm' - end, true) - end - ) - - res = {} - - for _, i in vim9.iter(vim9.fn.range(vim9.fn.len(diclist))) do - -- # New ctags has the "typeref" field. Patched version has "typename". - if vim9.bool(vim9.fn.has_key(vim9.index(diclist, i), 'typename')) then - res = vim9.fn.extend( - res, - StructMembers( - vim9.index(vim9.index(diclist, i), 'typename'), - vim9.slice(items, 1, nil), - true - ) - ) - elseif vim9.bool(vim9.fn.has_key(vim9.index(diclist, i), 'typeref')) then - res = vim9.fn.extend( - res, - StructMembers( - vim9.index(vim9.index(diclist, i), 'typeref'), - vim9.slice(items, 1, nil), - true - ) - ) - end - - -- # For a variable use the command, which must be a search pattern that - -- # shows the declaration of the variable. - if vim9.index(vim9.index(diclist, i), 'kind') == 'v' then - local line = vim9.index(vim9.index(diclist, i), 'cmd') - if vim9.slice(line, nil, 1) == '/^' then - local col = - vim9.fn.charidx(line, vim9.fn.match(line, '\\<' .. vim9.index(items, 0) .. '\\>')) - res = vim9.fn.extend( - res, - Nextitem( - vim9.slice(line, 2, vim9.ops.Minus(col, 1)), - vim9.slice(items, 1, nil), - 0, - true - ) - ) - end - end - end - end - - if vim9.fn.len(res) == 0 and vim9.fn.searchdecl(vim9.index(items, 0), true) == 0 then - -- # Found, now figure out the type. - -- # TODO: join previous line if it makes sense - local line = vim9.fn.getline('.') - local col = vim9.fn.charcol('.') - res = - Nextitem(vim9.slice(line, nil, vim9.ops.Minus(col, 1)), vim9.slice(items, 1, nil), 0, true) - end - - -- # If the last item(s) are [...] they need to be added to the matches. - local last = vim9.fn.len(items) - 1 - local brackets = '' - while last >= 0 do - if vim9.index(vim9.index(items, last), 0) ~= '[' then - break - end - brackets = vim9.index(items, last) .. brackets - last = last - 1 - end - - return vim9.fn.map(res, function(_, v) - return Tagline2item(v, brackets) - end) -end -M['Complete'] = Complete - -GetAddition = function(line, match, memarg, bracket) - bracket = vim9.bool(bracket) - -- # Guess if the item is an array. - if vim9.bool(vim9.ops.And(bracket, vim9.fn.match(line, match .. '\\s*\\[') > 0)) then - return '[' - end - - -- # Check if the item has members. - if vim9.fn.len(SearchMembers(memarg, { '' }, false)) > 0 then - -- # If there is a '*' before the name use "->". - if vim9.fn.match(line, '\\*[ \\t(]*' .. match .. '\\>') > 0 then - return '->' - else - return '.' - end - end - return '' -end - -Tag2item = function(val) - -- # 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. - local res = vim9.convert.decl_dict({ ['match'] = vim9.index(val, 'name') }) - - res[vim9.index_expr('extra')] = - Tagcmd2extra(vim9.index(val, 'cmd'), vim9.index(val, 'name'), vim9.index(val, 'filename')) - - local s = Dict2info(val) - if s ~= '' then - res[vim9.index_expr('info')] = s - end - - res[vim9.index_expr('tagline')] = '' - if vim9.bool(vim9.fn.has_key(val, 'kind')) then - local kind = vim9.index(val, 'kind') - res[vim9.index_expr('kind')] = kind - if kind == 'v' then - res[vim9.index_expr('tagline')] = '\t' .. vim9.index(val, 'cmd') - res[vim9.index_expr('dict')] = val - elseif vim9.bool(kind == 'f') then - res[vim9.index_expr('match')] = vim9.index(val, 'name') .. '(' - end - end - - return res -end - -Dict2info = function(dict) - -- # Use all the items in dictionary for the "info" entry. - local info = '' - - for _, k in vim9.iter(vim9.fn_mut('sort', { vim9.fn.keys(dict) }, { replace = 0 })) do - info = info .. k .. vim9.fn['repeat'](' ', 10 - vim9.fn.strlen(k)) - if k == 'cmd' then - info = info - .. vim9.fn.substitute( - vim9.fn.matchstr(vim9.index(dict, 'cmd'), '/^\\s*\\zs.*\\ze$/'), - '\\\\\\(.\\)', - '\\1', - 'g' - ) - else - local dictk = vim9.index(dict, k) - if vim9.fn.typename(dictk) ~= 'string' then - info = info .. vim9.fn.string(dictk) - else - info = info .. dictk - end - end - info = info .. '\n' - end - - return info -end - -ParseTagline = function(line) - -- # Parse a tag line and return a dictionary with items like taglist() - local l = vim9.fn.split(line, '\t') - local d = vim.empty_dict() - if vim9.fn.len(l) >= 3 then - d[vim9.index_expr('name')] = vim9.index(l, 0) - d[vim9.index_expr('filename')] = vim9.index(l, 1) - d[vim9.index_expr('cmd')] = vim9.index(l, 2) - local n = 2 - if vim9.ops.RegexpMatches(vim9.index(l, 2), '^/') then - -- # Find end of cmd, it may contain Tabs. - while n < vim9.fn.len(l) and vim9.ops.NotRegexpMatches(vim9.index(l, n), '/;"$') do - n = n + 1 - d[vim9.index_expr('cmd')] = vim9.index(d, 'cmd') .. ' ' .. vim9.index(l, n) - end - end - - for _, i in vim9.iter(vim9.fn.range(vim9.ops.Plus(n, 1), vim9.fn.len(l) - 1)) do - if vim9.index(l, i) == 'file:' then - d[vim9.index_expr('static')] = 1 - elseif vim9.bool(vim9.ops.NotRegexpMatches(vim9.index(l, i), ':')) then - d[vim9.index_expr('kind')] = vim9.index(l, i) - else - d[vim9.index_expr(vim9.fn.matchstr(vim9.index(l, i), '[^:]*'))] = - vim9.fn.matchstr(vim9.index(l, i), ':\\zs.*') - end - end - end - - return d -end - -Tagline2item = function(val, brackets) - -- # 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. - local line = vim9.index(val, 'tagline') - local add = GetAddition(line, vim9.index(val, 'match'), { val }, brackets == '') - local res = vim9.convert.decl_dict({ ['word'] = vim9.index(val, 'match') .. brackets .. add }) - - if vim9.bool(vim9.fn.has_key(val, 'info')) then - -- # Use info from Tag2item(). - res[vim9.index_expr('info')] = vim9.index(val, 'info') - else - -- # Parse the tag line and add each part to the "info" entry. - local s = Dict2info(ParseTagline(line)) - if s ~= '' then - res[vim9.index_expr('info')] = s - end - end - - if vim9.bool(vim9.fn.has_key(val, 'kind')) then - res[vim9.index_expr('kind')] = vim9.index(val, 'kind') - elseif vim9.bool(add == '(') then - res[vim9.index_expr('kind')] = 'f' - else - local s = vim9.fn.matchstr(line, '\\t\\(kind:\\)\\=\\zs\\S\\ze\\(\\t\\|$\\)') - if s ~= '' then - res[vim9.index_expr('kind')] = s - end - end - - if vim9.bool(vim9.fn.has_key(val, 'extra')) then - res[vim9.index_expr('menu')] = vim9.index(val, 'extra') - return res - end - - -- # Isolate the command after the tag and filename. - local s = vim9.fn.matchstr( - line, - '[^\\t]*\\t[^\\t]*\\t\\zs\\(/^.*$/\\|[^\\t]*\\)\\ze\\(;"\\t\\|\\t\\|$\\)' - ) - if s ~= '' then - res[vim9.index_expr('menu')] = Tagcmd2extra( - s, - vim9.index(val, 'match'), - vim9.fn.matchstr(line, '[^\\t]*\\t\\zs[^\\t]*\\ze\\t') - ) - end - return res -end - -Tagcmd2extra = function(cmd, name, fname) - -- # Turn a command from a tag line to something that is useful in the menu - local x = '' - if vim9.ops.RegexpMatches(cmd, '^/^') then - -- # The command is a search command, useful to see what it is. - x = vim9.fn.substitute( - vim9.fn.substitute( - vim9.fn.matchstr(cmd, '^/^\\s*\\zs.*\\ze$/'), - '\\<' .. name .. '\\>', - '@@', - '' - ), - '\\\\\\(.\\)', - '\\1', - 'g' - ) .. ' - ' .. fname - elseif vim9.bool(vim9.ops.RegexpMatches(cmd, '^\\d*$')) then - -- # 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 - end - return x -end - -Nextitem = function(lead, items, depth, all) - all = vim9.bool(all) - -- # 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. - local tokens = vim9.fn.split(lead, '\\s\\+\\|\\<') - - -- # Try to recognize the type of the variable. This is rough guessing... - local res = {} - - local body = function(_, tidx) - -- # Skip tokens starting with a non-ID character. - if vim9.ops.NotRegexpMatches(vim9.index(tokens, tidx), '^\\h') then - return vim9.ITER_CONTINUE - end - - -- # Recognize "struct foobar" and "union foobar". - -- # Also do "class foobar" when it's C++ after all (doesn't work very well - -- # though). - if - ( - vim9.index(tokens, tidx) == 'struct' - or vim9.index(tokens, tidx) == 'union' - or vim9.index(tokens, tidx) == 'class' - ) and vim9.ops.Plus(tidx, 1) < vim9.fn.len(tokens) - then - res = StructMembers( - vim9.index(tokens, tidx) .. ':' .. vim9.index(tokens, vim9.ops.Plus(tidx, 1)), - items, - all - ) - return vim9.ITER_BREAK - end - - -- # TODO: add more reserved words - if - vim9.fn.index( - { 'int', 'short', 'char', 'float', 'double', 'static', 'unsigned', 'extern' }, - vim9.index(tokens, tidx) - ) >= 0 - then - return vim9.ITER_CONTINUE - end - - -- # Use the tags file to find out if this is a typedef. - local diclist = vim9.fn.taglist('^' .. vim9.index(tokens, tidx) .. '$') - - local body = function(_, tagidx) - local item = vim9.convert.decl_dict(vim9.index(diclist, tagidx)) - - -- # New ctags has the "typeref" field. Patched version has "typename". - if vim9.bool(vim9.fn.has_key(item, 'typeref')) then - res = vim9.fn.extend(res, StructMembers(vim9.index(item, 'typeref'), items, all)) - return vim9.ITER_CONTINUE - end - if vim9.bool(vim9.fn.has_key(item, 'typename')) then - res = vim9.fn.extend(res, StructMembers(vim9.index(item, 'typename'), items, all)) - return vim9.ITER_CONTINUE - end - - -- # Only handle typedefs here. - if vim9.index(item, 'kind') ~= 't' then - return vim9.ITER_CONTINUE - end - - -- # Skip matches local to another file. - if - vim9.bool( - vim9.ops.And( - vim9.ops.And(vim9.fn.has_key(item, 'static'), vim9.index(item, 'static')), - vim9.fn.bufnr('%') ~= vim9.fn.bufnr(vim9.index(item, 'filename')) - ) - ) - then - return vim9.ITER_CONTINUE - end - - -- # For old ctags we recognize "typedef struct aaa" and - -- # "typedef union bbb" in the tags file command. - local cmd = vim9.index(item, 'cmd') - local ei = vim9.fn.charidx(cmd, vim9.fn.matchend(cmd, 'typedef\\s\\+')) - if ei > 1 then - local cmdtokens = vim9.fn.split(vim9.slice(cmd, ei, nil), '\\s\\+\\|\\<') - if vim9.fn.len(cmdtokens) > 1 then - if - vim9.index(cmdtokens, 0) == 'struct' - or vim9.index(cmdtokens, 0) == 'union' - or vim9.index(cmdtokens, 0) == 'class' - then - local name = '' - -- # Use the first identifier after the "struct" or "union" - - for _, ti in vim9.iter(vim9.fn.range((vim9.fn.len(cmdtokens) - 1))) do - if vim9.ops.RegexpMatches(vim9.index(cmdtokens, ti), '^\\w') then - name = vim9.index(cmdtokens, ti) - break - end - end - - if name ~= '' then - res = vim9.fn.extend( - res, - StructMembers(vim9.index(cmdtokens, 0) .. ':' .. name, items, all) - ) - end - elseif vim9.bool(depth < 10) then - -- # Could be "typedef other_T some_T". - res = vim9.fn.extend( - res, - Nextitem(vim9.index(cmdtokens, 0), items, vim9.ops.Plus(depth, 1), all) - ) - end - end - end - - return vim9.ITER_DEFAULT - end - - for _, tagidx in vim9.iter(vim9.fn.range(vim9.fn.len(diclist))) do - local nvim9_status, nvim9_ret = body(_, tagidx) - if nvim9_status == vim9.ITER_BREAK then - break - elseif nvim9_status == vim9.ITER_RETURN then - return nvim9_ret - end - end - - if vim9.fn.len(res) > 0 then - return vim9.ITER_BREAK - end - - return vim9.ITER_DEFAULT - end - - for _, tidx in vim9.iter(vim9.fn.range(vim9.fn.len(tokens))) do - local nvim9_status, nvim9_ret = body(_, tidx) - if nvim9_status == vim9.ITER_BREAK then - break - elseif nvim9_status == vim9.ITER_RETURN then - return nvim9_ret - end - end - - return res -end - -StructMembers = function(atypename, items, all) - all = vim9.bool(all) - - -- # 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? - local fnames = vim9.fn.join(vim9.fn.map(vim9.fn.tagfiles(), function(_, v) - return vim9.fn.escape(v, ' \\#%') - end)) - if fnames == '' then - return {} - end - - local typename = atypename - local qflist = {} - local cached = 0 - local n = '' - if vim9.bool(vim9.prefix['Bang'](all)) then - n = '1' - if vim9.bool(vim9.fn.has_key(grepCache, typename)) then - qflist = vim9.index(grepCache, typename) - cached = 1 - end - else - n = '' - end - if vim9.bool(vim9.prefix['Bang'](cached)) then - while 1 do - vim.api.nvim_command( - 'silent! keepjumps noautocmd ' - .. n - .. 'vimgrep ' - .. '/\\t' - .. typename - .. '\\(\\t\\|$\\)/j ' - .. fnames - ) - - qflist = vim9.fn.getqflist() - if vim9.fn.len(qflist) > 0 or vim9.fn.match(typename, '::') < 0 then - break - end - -- # No match for "struct:context::name", remove "context::" and try again. - typename = vim9.fn.substitute(typename, ':[^:]*::', ':', '') - end - - if vim9.bool(vim9.prefix['Bang'](all)) then - -- # Store the result to be able to use it again later. - grepCache[vim9.index_expr(typename)] = qflist - end - end - - -- # Skip over [...] items - local idx = 0 - local target = '' - while 1 do - if idx >= vim9.fn.len(items) then - target = '' - break - end - if vim9.index(vim9.index(items, idx), 0) ~= '[' then - target = vim9.index(items, idx) - break - end - idx = idx + 1 - end - -- # Put matching members in matches[]. - local matches = {} - - for _, l in vim9.iter(qflist) do - local memb = vim9.fn.matchstr(vim9.index(l, 'text'), '[^\\t]*') - if vim9.ops.RegexpMatches(memb, '^' .. target) then - -- # Skip matches local to another file. - if - vim9.fn.match(vim9.index(l, 'text'), '\tfile:') < 0 - or vim9.fn.bufnr('%') - == vim9.fn.bufnr(vim9.fn.matchstr(vim9.index(l, 'text'), '\\t\\zs[^\\t]*')) - then - local item = - vim9.convert.decl_dict({ ['match'] = memb, ['tagline'] = vim9.index(l, 'text') }) - - -- # Add the kind of item. - local s = - vim9.fn.matchstr(vim9.index(l, 'text'), '\\t\\(kind:\\)\\=\\zs\\S\\ze\\(\\t\\|$\\)') - if s ~= '' then - item[vim9.index_expr('kind')] = s - if s == 'f' then - item[vim9.index_expr('match')] = memb .. '(' - end - end - - vim9.fn.add(matches, item) - end - end - end - - if vim9.fn.len(matches) > 0 then - -- # Skip over next [...] items - idx = idx + 1 - while 1 do - if idx >= vim9.fn.len(items) then - return matches - end - if vim9.index(vim9.index(items, idx), 0) ~= '[' then - break - end - idx = idx + 1 - end - - -- # More items following. For each of the possible members find the - -- # matching following members. - return SearchMembers(matches, vim9.slice(items, idx, nil), all) - end - - -- # Failed to find anything. - return {} -end - -SearchMembers = function(matches, items, all) - all = vim9.bool(all) - - -- # For matching members, find matches for following items. - -- # When "all" is true find all, otherwise just return 1 if there is any member. - local res = {} - - for _, i in vim9.iter(vim9.fn.range(vim9.fn.len(matches))) do - local typename = '' - local line = '' - if vim9.bool(vim9.fn.has_key(vim9.index(matches, i), 'dict')) then - if vim9.bool(vim9.fn.has_key(vim9.index(vim9.index(matches, i), 'dict'), 'typename')) then - typename = vim9.index(vim9.index(vim9.index(matches, i), 'dict'), 'typename') - elseif vim9.bool(vim9.fn.has_key(vim9.index(vim9.index(matches, i), 'dict'), 'typeref')) then - typename = vim9.index(vim9.index(vim9.index(matches, i), 'dict'), 'typeref') - end - line = '\t' .. vim9.index(vim9.index(vim9.index(matches, i), 'dict'), 'cmd') - else - line = vim9.index(vim9.index(matches, i), 'tagline') - local eb = vim9.fn.matchend(line, '\\ttypename:') - local e = vim9.fn.charidx(line, eb) - if e < 0 then - eb = vim9.fn.matchend(line, '\\ttyperef:') - e = vim9.fn.charidx(line, eb) - end - if e > 0 then - -- # Use typename field - typename = vim9.fn.matchstr(line, '[^\\t]*', eb) - end - end - - if typename ~= '' then - res = vim9.fn.extend(res, StructMembers(typename, items, all)) - else - -- # Use the search command (the declaration itself). - local sb = vim9.fn.match(line, '\\t\\zs/^') - local s = vim9.fn.charidx(line, sb) - if s > 0 then - local e = vim9.fn.charidx( - line, - vim9.fn.match(line, '\\<' .. vim9.index(vim9.index(matches, i), 'match') .. '\\>', sb) - ) - if e > 0 then - res = - vim9.fn.extend(res, Nextitem(vim9.slice(line, s, vim9.ops.Minus(e, 1)), items, 0, all)) - end - end - end - if vim9.bool(vim9.ops.And(vim9.prefix['Bang'](all), vim9.fn.len(res) > 0)) then - break - end - end - - return res -end - --- #}}}1 - --- # vim: noet sw=2 sts=2 -return M diff --git a/runtime/autoload/ccomplete.vim b/runtime/autoload/ccomplete.vim @@ -1,8 +1,639 @@ -" Generated vim file by vim9jit. Please do not edit -let s:path = expand("<script>") -let s:lua_path = fnamemodify(s:path, ":r") . ".lua" -let s:nvim_module = luaeval('require("_vim9script").autoload(_A)', s:lua_path) - -function! ccomplete#Complete(findstart, abase) abort - return s:nvim_module.Complete(a:findstart, a:abase) -endfunction +" Vim completion script +" Language: C +" Maintainer: Bram Moolenaar <Bram@vim.org> +" Last Change: 2020 Nov 14 + +let s:cpo_save = &cpo +set cpo&vim + +" This function is used for the 'omnifunc' option. +func ccomplete#Complete(findstart, base) + if a:findstart + " Locate the start of the item, including ".", "->" and "[...]". + let line = getline('.') + let start = col('.') - 1 + let lastword = -1 + while start > 0 + if line[start - 1] =~ '\w' + let start -= 1 + elseif line[start - 1] =~ '\.' + if lastword == -1 + let lastword = start + endif + let start -= 1 + elseif start > 1 && line[start - 2] == '-' && line[start - 1] == '>' + if lastword == -1 + let lastword = start + endif + let start -= 2 + elseif line[start - 1] == ']' + " Skip over [...]. + let n = 0 + let start -= 1 + while start > 0 + let start -= 1 + if line[start] == '[' + if n == 0 + break + endif + let n -= 1 + elseif line[start] == ']' " nested [] + let n += 1 + 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 s:prepended. + if lastword == -1 + let s:prepended = '' + return start + endif + let s:prepended = strpart(line, start, lastword - start) + return lastword + endif + + " Return list of matches. + + let base = s:prepended . a:base + + " 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 + let s: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. + let items = [] + let s = 0 + let arrays = 0 + while 1 + let e = match(base, '\.\|->\|\[', s) + if e < 0 + if s == 0 || base[s - 1] != ']' + call add(items, strpart(base, s)) + endif + break + endif + if s == 0 || base[s - 1] != ']' + call add(items, strpart(base, s, e - s)) + endif + if base[e] == '.' + let s = e + 1 " skip over '.' + elseif base[e] == '-' + let s = e + 2 " skip over '->' + else + " Skip over [...]. + let n = 0 + let s = e + let e += 1 + while e < len(base) + if base[e] == ']' + if n == 0 + break + endif + let n -= 1 + elseif base[e] == '[' " nested [...] + let n += 1 + endif + let e += 1 + endwhile + let e += 1 + call add(items, strpart(base, s, e - s)) + let arrays += 1 + let 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") + let res = [] + if searchdecl(items[0], 0, 1) == 0 + " Found, now figure out the type. + " TODO: join previous line if it makes sense + let line = getline('.') + let col = col('.') + if stridx(strpart(line, 0, col), ';') != -1 + " Handle multiple declarations on the same line. + let col2 = col - 1 + while line[col2] != ';' + let col2 -= 1 + endwhile + let line = strpart(line, col2 + 1) + let col -= col2 + endif + if stridx(strpart(line, 0, col), ',') != -1 + " Handle multiple declarations on the same line in a function + " declaration. + let col2 = col - 1 + while line[col2] != ',' + let col2 -= 1 + endwhile + if strpart(line, col2 + 1, col - col2 - 1) =~ ' *[^ ][^ ]* *[^ ]' + let line = strpart(line, col2 + 1) + let col -= col2 + endif + endif + if len(items) == 1 + " Completing one word and it's a local variable: May add '[', '.' or + " '->'. + let match = items[0] + let kind = 'v' + if match(line, '\<' . match . '\s*\[') > 0 + let match .= '[' + else + let res = s:Nextitem(strpart(line, 0, col), [''], 0, 1) + if len(res) > 0 + " There are members, thus add "." or "->". + if match(line, '\*[ \t(]*' . match . '\>') > 0 + let match .= '->' + else + let match .= '.' + endif + endif + endif + let 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 + let match = items[0] + let kind = 'v' + let tagline = "\t/^" . line . '$/' + let res = [{'match': match, 'tagline' : tagline, 'kind' : kind, 'info' : line}] + else + " Completing "var.", "var.something", etc. + let res = s:Nextitem(strpart(line, 0, col), items[1:], 0, 1) + endif + endif + + if len(items) == 1 || len(items) == arrays + 1 + " Only one part, no "." or "->": complete from tags file. + if len(items) == 1 + let tags = taglist('^' . base) + else + let tags = taglist('^' . items[0] . '$') + endif + + " Remove members, these can't appear without something in front. + call filter(tags, 'has_key(v:val, "kind") ? v:val["kind"] != "m" : 1') + + " Remove static matches in other files. + call filter(tags, '!has_key(v:val, "static") || !v:val["static"] || bufnr("%") == bufnr(v:val["filename"])') + + call extend(res, map(tags, 's:Tag2item(v:val)')) + endif + + if len(res) == 0 + " Find the variable in the tags file(s) + let diclist = taglist('^' . items[0] . '$') + + " Remove members, these can't appear without something in front. + call filter(diclist, 'has_key(v:val, "kind") ? v:val["kind"] != "m" : 1') + + let res = [] + for i in range(len(diclist)) + " New ctags has the "typeref" field. Patched version has "typename". + if has_key(diclist[i], 'typename') + call extend(res, s:StructMembers(diclist[i]['typename'], items[1:], 1)) + elseif has_key(diclist[i], 'typeref') + call extend(res, s:StructMembers(diclist[i]['typeref'], items[1:], 1)) + 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' + let line = diclist[i]['cmd'] + if line[0] == '/' && line[1] == '^' + let col = match(line, '\<' . items[0] . '\>') + call extend(res, s:Nextitem(strpart(line, 2, col - 2), items[1:], 0, 1)) + endif + endif + endfor + endif + + if len(res) == 0 && searchdecl(items[0], 1) == 0 + " Found, now figure out the type. + " TODO: join previous line if it makes sense + let line = getline('.') + let col = col('.') + let res = s:Nextitem(strpart(line, 0, col), items[1:], 0, 1) + endif + + " If the last item(s) are [...] they need to be added to the matches. + let last = len(items) - 1 + let brackets = '' + while last >= 0 + if items[last][0] != '[' + break + endif + let brackets = items[last] . brackets + let last -= 1 + endwhile + + return map(res, 's:Tagline2item(v:val, brackets)') +endfunc + +func s:GetAddition(line, match, memarg, bracket) + " Guess if the item is an array. + if a:bracket && match(a:line, a:match . '\s*\[') > 0 + return '[' + endif + + " Check if the item has members. + if len(s:SearchMembers(a:memarg, [''], 0)) > 0 + " If there is a '*' before the name use "->". + if match(a:line, '\*[ \t(]*' . a:match . '\>') > 0 + return '->' + else + return '.' + endif + endif + return '' +endfunc + +" 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 s:GetAddition() uses. +func s:Tag2item(val) + let res = {'match': a:val['name']} + + let res['extra'] = s:Tagcmd2extra(a:val['cmd'], a:val['name'], a:val['filename']) + + let s = s:Dict2info(a:val) + if s != '' + let res['info'] = s + endif + + let res['tagline'] = '' + if has_key(a:val, "kind") + let kind = a:val['kind'] + let res['kind'] = kind + if kind == 'v' + let res['tagline'] = "\t" . a:val['cmd'] + let res['dict'] = a:val + elseif kind == 'f' + let res['match'] = a:val['name'] . '(' + endif + endif + + return res +endfunc + +" Use all the items in dictionary for the "info" entry. +func s:Dict2info(dict) + let info = '' + for k in sort(keys(a:dict)) + let info .= k . repeat(' ', 10 - len(k)) + if k == 'cmd' + let info .= substitute(matchstr(a:dict['cmd'], '/^\s*\zs.*\ze$/'), '\\\(.\)', '\1', 'g') + else + let info .= a:dict[k] + endif + let info .= "\n" + endfor + return info +endfunc + +" Parse a tag line and return a dictionary with items like taglist() +func s:ParseTagline(line) + let l = split(a:line, "\t") + let d = {} + if len(l) >= 3 + let d['name'] = l[0] + let d['filename'] = l[1] + let d['cmd'] = l[2] + let n = 2 + if l[2] =~ '^/' + " Find end of cmd, it may contain Tabs. + while n < len(l) && l[n] !~ '/;"$' + let n += 1 + let d['cmd'] .= " " . l[n] + endwhile + endif + for i in range(n + 1, len(l) - 1) + if l[i] == 'file:' + let d['static'] = 1 + elseif l[i] !~ ':' + let d['kind'] = l[i] + else + let d[matchstr(l[i], '[^:]*')] = matchstr(l[i], ':\zs.*') + endif + endfor + endif + + return d +endfunc + +" 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. +func s:Tagline2item(val, brackets) + let line = a:val['tagline'] + let add = s:GetAddition(line, a:val['match'], [a:val], a:brackets == '') + let res = {'word': a:val['match'] . a:brackets . add } + + if has_key(a:val, 'info') + " Use info from Tag2item(). + let res['info'] = a:val['info'] + else + " Parse the tag line and add each part to the "info" entry. + let s = s:Dict2info(s:ParseTagline(line)) + if s != '' + let res['info'] = s + endif + endif + + if has_key(a:val, 'kind') + let res['kind'] = a:val['kind'] + elseif add == '(' + let res['kind'] = 'f' + else + let s = matchstr(line, '\t\(kind:\)\=\zs\S\ze\(\t\|$\)') + if s != '' + let res['kind'] = s + endif + endif + + if has_key(a:val, 'extra') + let res['menu'] = a:val['extra'] + return res + endif + + " Isolate the command after the tag and filename. + let s = matchstr(line, '[^\t]*\t[^\t]*\t\zs\(/^.*$/\|[^\t]*\)\ze\(;"\t\|\t\|$\)') + if s != '' + let res['menu'] = s:Tagcmd2extra(s, a:val['match'], matchstr(line, '[^\t]*\t\zs[^\t]*\ze\t')) + endif + return res +endfunc + +" Turn a command from a tag line to something that is useful in the menu +func s:Tagcmd2extra(cmd, name, fname) + if a:cmd =~ '^/^' + " The command is a search command, useful to see what it is. + let x = matchstr(a:cmd, '^/^\s*\zs.*\ze$/') + let x = substitute(x, '\<' . a:name . '\>', '@@', '') + let x = substitute(x, '\\\(.\)', '\1', 'g') + let x = x . ' - ' . a:fname + elseif a:cmd =~ '^\d*$' + " The command is a line number, the file name is more useful. + let x = a:fname . ' - ' . a:cmd + else + " Not recognized, use command and file name. + let x = a:cmd . ' - ' . a:fname + endif + return x +endfunc + +" 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. +func s:Nextitem(lead, items, depth, all) + + " Use the text up to the variable name and split it in tokens. + let tokens = split(a:lead, '\s\+\|\<') + + " Try to recognize the type of the variable. This is rough guessing... + let res = [] + for tidx in range(len(tokens)) + + " 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) + let res = s:StructMembers(tokens[tidx] . ':' . tokens[tidx + 1], a:items, a:all) + break + endif + + " TODO: add more reserved words + if index(['int', 'short', 'char', 'float', 'double', 'static', 'unsigned', 'extern'], tokens[tidx]) >= 0 + continue + endif + + " Use the tags file to find out if this is a typedef. + let diclist = taglist('^' . tokens[tidx] . '$') + for tagidx in range(len(diclist)) + let item = diclist[tagidx] + + " New ctags has the "typeref" field. Patched version has "typename". + if has_key(item, 'typeref') + call extend(res, s:StructMembers(item['typeref'], a:items, a:all)) + continue + endif + if has_key(item, 'typename') + call extend(res, s:StructMembers(item['typename'], a:items, a:all)) + continue + endif + + " Only handle typedefs here. + if item['kind'] != 't' + continue + endif + + " Skip matches local to another file. + if has_key(item, '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. + let cmd = item['cmd'] + let ei = matchend(cmd, 'typedef\s\+') + if ei > 1 + let cmdtokens = split(strpart(cmd, ei), '\s\+\|\<') + if len(cmdtokens) > 1 + if cmdtokens[0] == 'struct' || cmdtokens[0] == 'union' || cmdtokens[0] == 'class' + let name = '' + " Use the first identifier after the "struct" or "union" + for ti in range(len(cmdtokens) - 1) + if cmdtokens[ti] =~ '^\w' + let name = cmdtokens[ti] + break + endif + endfor + if name != '' + call extend(res, s:StructMembers(cmdtokens[0] . ':' . name, a:items, a:all)) + endif + elseif a:depth < 10 + " Could be "typedef other_T some_T". + call extend(res, s:Nextitem(cmdtokens[0], a:items, a:depth + 1, a:all)) + endif + endif + endif + endfor + if len(res) > 0 + break + endif + endfor + + return res +endfunc + + +" 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 non-zero find all, otherwise just return 1 if there is any +" member. +func s:StructMembers(typename, items, all) + " Todo: What about local structures? + let fnames = join(map(tagfiles(), 'escape(v:val, " \\#%")')) + if fnames == '' + return [] + endif + + let typename = a:typename + let qflist = [] + let cached = 0 + if a:all == 0 + let n = '1' " stop at first found match + if has_key(s:grepCache, a:typename) + let qflist = s:grepCache[a:typename] + let cached = 1 + endif + else + let n = '' + endif + if !cached + while 1 + exe 'silent! keepj noautocmd ' . n . 'vimgrep /\t' . typename . '\(\t\|$\)/j ' . fnames + + let qflist = getqflist() + if len(qflist) > 0 || match(typename, "::") < 0 + break + endif + " No match for "struct:context::name", remove "context::" and try again. + let typename = substitute(typename, ':[^:]*::', ':', '') + endwhile + + if a:all == 0 + " Store the result to be able to use it again later. + let s:grepCache[a:typename] = qflist + endif + endif + + " Skip over [...] items + let idx = 0 + while 1 + if idx >= len(a:items) + let target = '' " No further items, matching all members + break + endif + if a:items[idx][0] != '[' + let target = a:items[idx] + break + endif + let idx += 1 + endwhile + " Put matching members in matches[]. + let matches = [] + for l in qflist + let memb = matchstr(l['text'], '[^\t]*') + if memb =~ '^' . target + " Skip matches local to another file. + if match(l['text'], "\tfile:") < 0 || bufnr('%') == bufnr(matchstr(l['text'], '\t\zs[^\t]*')) + let item = {'match': memb, 'tagline': l['text']} + + " Add the kind of item. + let s = matchstr(l['text'], '\t\(kind:\)\=\zs\S\ze\(\t\|$\)') + if s != '' + let item['kind'] = s + if s == 'f' + let item['match'] = memb . '(' + endif + endif + + call add(matches, item) + endif + endif + endfor + + if len(matches) > 0 + " Skip over next [...] items + let idx += 1 + while 1 + if idx >= len(a:items) + return matches " No further items, return the result. + endif + if a:items[idx][0] != '[' + break + endif + let idx += 1 + endwhile + + " More items following. For each of the possible members find the + " matching following members. + return s:SearchMembers(matches, a:items[idx :], a:all) + endif + + " Failed to find anything. + return [] +endfunc + +" For matching members, find matches for following items. +" When "all" is non-zero find all, otherwise just return 1 if there is any +" member. +func s:SearchMembers(matches, items, all) + let res = [] + for i in range(len(a:matches)) + let typename = '' + if has_key(a:matches[i], 'dict') + if has_key(a:matches[i].dict, 'typename') + let typename = a:matches[i].dict['typename'] + elseif has_key(a:matches[i].dict, 'typeref') + let typename = a:matches[i].dict['typeref'] + endif + let line = "\t" . a:matches[i].dict['cmd'] + else + let line = a:matches[i]['tagline'] + let e = matchend(line, '\ttypename:') + if e < 0 + let e = matchend(line, '\ttyperef:') + endif + if e > 0 + " Use typename field + let typename = matchstr(line, '[^\t]*', e) + endif + endif + + if typename != '' + call extend(res, s:StructMembers(typename, a:items, a:all)) + else + " Use the search command (the declaration itself). + let s = match(line, '\t\zs/^') + if s > 0 + let e = match(line, '\<' . a:matches[i]['match'] . '\>', s) + if e > 0 + call extend(res, s:Nextitem(strpart(line, s, e - s), a:items, 0, a:all)) + endif + endif + endif + if a:all == 0 && len(res) > 0 + break + endif + endfor + return res +endfunc + +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: noet sw=2 sts=2 diff --git a/runtime/doc/faq.txt b/runtime/doc/faq.txt @@ -450,10 +450,7 @@ grow and enhance it. Changing the rules of Lua gains nothing in this context. WILL NEOVIM TRANSLATE VIMSCRIPT TO LUA, INSTEAD OF EXECUTING VIMSCRIPT DIRECTLY? ~ -- We are experimenting with vim9jit https://github.com/tjdevries/vim9jit to - transpile Vim9script (Vim9's Vimscript variant) to Lua and have used this to - port Vim9 plugins https://github.com/neovim/neovim/pull/21662 to Nvim Lua. -- We have no plans for transpiling legacy Vimscript. +We have no plans for transpiling Vimscript. It was explored in https://github.com/tjdevries/vim9jit ARE PLUGIN AUTHORS ENCOURAGED TO PORT THEIR PLUGINS FROM VIMSCRIPT TO LUA? DO YOU PLAN ON SUPPORTING VIMSCRIPT INDEFINITELY? (#1152) ~ @@ -468,4 +465,8 @@ emphatically a fork of Vim in order to leverage the work already spent on thousands of Vim plugins, while enabling new types of plugins and integrations. +That being said, reimplementing legacy plugins in Lua in order to make use of +Nvim API and to integrate with Nvim-specific features such as treesitter can +be worthwhile. + vim:tw=78:ts=8:noet:ft=help:norl: diff --git a/runtime/lua/_vim9script.lua b/runtime/lua/_vim9script.lua @@ -1,650 +0,0 @@ -------------------------------------------------------------------------------- --- This file is auto generated by vim9jit. Do not edit by hand. --- All content is in the source repository. --- Bugs should be reported to: github.com/tjdevries/vim9jit --- --- In addition, this file is considered "private" by neovim. You should --- not expect any of the APIs, functions, etc to be stable. They are subject --- to change at any time. -------------------------------------------------------------------------------- - -local vim9 = (function() - local M = {} - - M.ternary = function(cond, if_true, if_false) - if cond then - if type(if_true) == 'function' then - return if_true() - else - return if_true - end - else - if type(if_false) == 'function' then - return if_false() - else - return if_false - end - end - end - - M.fn_ref = function(module, name, copied, ...) - for _, val in ipairs({ ... }) do - table.insert(copied, val) - end - - local funcref = name - if type(funcref) == 'function' then - return funcref(unpack(copied)) - elseif type(funcref) == 'string' then - if vim.fn.exists('*' .. funcref) == 1 then - return vim.fn[funcref](unpack(copied)) - end - - if module[funcref] then - module[funcref](unpack(copied)) - end - - error('unknown function: ' .. funcref) - else - error(string.format('unable to call funcref: %s', funcref)) - end - end - - M.fn_mut = function(name, args, info) - local result = vim.fn._Vim9ScriptFn(name, args) - for idx, val in pairs(result[2]) do - M.replace(args[idx], val) - end - - -- Substitute returning the reference to the - -- returned value - if info.replace then - return args[info.replace + 1] - end - - return result[1] - end - - M.replace = function(orig, new) - if type(orig) == 'table' and type(new) == 'table' then - for k in pairs(orig) do - orig[k] = nil - end - - for k, v in pairs(new) do - orig[k] = v - end - - return orig - end - - return new - end - - M.index = function(obj, idx) - if vim.islist(obj) then - if idx < 0 then - return obj[#obj + idx + 1] - else - return obj[idx + 1] - end - elseif type(obj) == 'table' then - return obj[idx] - elseif type(obj) == 'string' then - return string.sub(obj, idx + 1, idx + 1) - end - - error('invalid type for indexing: ' .. vim.inspect(obj)) - end - - M.index_expr = function(idx) - if type(idx) == 'string' then - return idx - elseif type(idx) == 'number' then - return idx + 1 - else - error(string.format('not yet handled: %s', vim.inspect(idx))) - end - end - - M.slice = function(obj, start, finish) - if start == nil then - start = 0 - end - - if start < 0 then - start = #obj + start - end - assert(type(start) == 'number') - - if finish == nil then - finish = #obj - end - - if finish < 0 then - finish = #obj + finish - end - assert(type(finish) == 'number') - - local slicer - if vim.islist(obj) then - slicer = vim.list_slice - elseif type(obj) == 'string' then - slicer = string.sub - else - error('invalid type for slicing: ' .. vim.inspect(obj)) - end - - return slicer(obj, start + 1, finish + 1) - end - - -- Currently unused, but this could be used to embed vim9jit within a - -- running nvim application and transpile "on the fly" as files are - -- sourced. There would still need to be some work done to make that - -- work correctly with imports and what not, but overall it could - -- work well for calling ":source X" from within a vimscript/vim9script - -- function - M.make_source_cmd = function() - local group = vim.api.nvim_create_augroup('nvim.vim9script_source', {}) - vim.api.nvim_create_autocmd('SourceCmd', { - pattern = '*.vim', - group = group, - callback = function(a) - local file = vim.fn.readfile(a.file) - for _, line in ipairs(file) do - -- TODO: Or starts with def <something> - -- You can use def in legacy vim files - if vim.startswith(line, 'vim9script') then - -- TODO: Use the rust lib to actually - -- generate the corresponding lua code and then - -- execute that (instead of sourcing it directly) - return - end - end - - vim.api.nvim_exec2(table.concat(file, '\n'), { output = false }) - end, - }) - end - - M.iter = function(expr) - if vim.islist(expr) then - return ipairs(expr) - else - return pairs(expr) - end - end - - M.ITER_DEFAULT = 0 - M.ITER_CONTINUE = 1 - M.ITER_BREAK = 2 - M.ITER_RETURN = 3 - - return M -end)() - -vim.cmd([[ -function! _Vim9ScriptFn(name, args) abort - try - let ret = function(a:name, a:args)() - catch - echo "Failed..." - echo a:name - echo a:args - - throw v:errmsg - endtry - - return [ret, a:args] -endfunction -]]) - -vim9['autoload'] = (function() - return function(path) - return loadfile(path)() - end -end)() -vim9['bool'] = (function() - return function(...) - return vim9.convert.to_vim_bool(...) - end -end)() -vim9['convert'] = (function() - local M = {} - - M.decl_bool = function(val) - if type(val) == 'boolean' then - return val - elseif type(val) == 'number' then - if val == 0 then - return false - elseif val == 1 then - return true - else - error(string.format('bad number passed to bool declaration: %s', val)) - end - end - - error(string.format('invalid bool declaration: %s', vim.inspect(val))) - end - - M.decl_dict = function(val) - if type(val) == 'nil' then - return vim.empty_dict() - elseif type(val) == 'table' then - if vim.tbl_isempty(val) then - return vim.empty_dict() - elseif vim.islist(val) then - error(string.format('Cannot pass list to dictionary? %s', vim.inspect(val))) - else - return val - end - end - - error(string.format('invalid dict declaration: %s', vim.inspect(val))) - end - - M.to_vim_bool = function(val) - if type(val) == 'boolean' then - return val - elseif type(val) == 'number' then - return val ~= 0 - elseif type(val) == 'string' then - return string.len(val) ~= 0 - elseif type(val) == 'table' then - return not vim.tbl_isempty(val) - elseif val == nil then - return false - end - - error('unhandled type: ' .. vim.inspect(val)) - end - - return M -end)() -vim9['fn'] = (function() - local M = {} - - M.insert = function(list, item, idx) - if idx == nil then - idx = 1 - end - - table.insert(list, idx + 1, item) - - return list - end - - M.extend = function(left, right, expr3) - if expr3 ~= nil then - error("haven't written this code yet") - end - - if vim.islist(right) then - vim.list_extend(left, right) - return left - else - -- local result = vim.tbl_extend(left, right) - for k, v in pairs(right) do - left[k] = v - end - - return left - end - end - - M.add = function(list, item) - table.insert(list, item) - return list - end - - M.has_key = function(obj, key) - return not not obj[key] - end - - M.prop_type_add = function(...) - local args = { ... } - print('[prop_type_add]', vim.inspect(args)) - end - - do - local has_overrides = { - -- We do have vim9script ;) that's this plugin - ['vim9script'] = true, - - -- Include some vim patches that are sometimes required by various vim9script plugins - -- that we implement via vim9jit - [ [[patch-8.2.2261]] ] = true, - [ [[patch-8.2.4257]] ] = true, - } - - M.has = function(patch) - if has_overrides[patch] then - return true - end - - return vim.fn.has(patch) - end - end - - --[=[ -Currently missing patch, can be removed in the future. - -readdirex({directory} [, {expr} [, {dict}]]) *readdirex()* - Extended version of |readdir()|. - Return a list of Dictionaries with file and directory - information in {directory}. - This is useful if you want to get the attributes of file and - directory at the same time as getting a list of a directory. - This is much faster than calling |readdir()| then calling - |getfperm()|, |getfsize()|, |getftime()| and |getftype()| for - each file and directory especially on MS-Windows. - The list will by default be sorted by name (case sensitive), - the sorting can be changed by using the optional {dict} - argument, see |readdir()|. - - The Dictionary for file and directory information has the - following items: - group Group name of the entry. (Only on Unix) - name Name of the entry. - perm Permissions of the entry. See |getfperm()|. - size Size of the entry. See |getfsize()|. - time Timestamp of the entry. See |getftime()|. - type Type of the entry. - On Unix, almost same as |getftype()| except: - Symlink to a dir "linkd" - Other symlink "link" - On MS-Windows: - Normal file "file" - Directory "dir" - Junction "junction" - Symlink to a dir "linkd" - Other symlink "link" - Other reparse point "reparse" - user User name of the entry's owner. (Only on Unix) - On Unix, if the entry is a symlink, the Dictionary includes - the information of the target (except the "type" item). - On MS-Windows, it includes the information of the symlink - itself because of performance reasons. ---]=] - M.readdirex = function(dir) - local files = vim.fn.readdir(dir) - local direx = {} - for _, f in ipairs(files) do - table.insert(direx, { - name = f, - type = vim.fn.getftype(f), - }) - end - - return direx - end - - M.mapnew = function(tbl, expr) - return vim.fn.map(tbl, expr) - end - - M.typename = function(val) - local ty = type(val) - if ty == 'string' then - return 'string' - elseif ty == 'boolean' then - return 'bool' - elseif ty == 'number' then - return 'number' - else - error(string.format('typename: %s', val)) - end - end - - -- Popup menu stuff: Could be rolled into other plugin later - -- but currently is here for testing purposes (and implements - -- some very simple compat layers at the moment) - do - local pos_map = { - topleft = 'NW', - topright = 'NE', - botleft = 'SW', - botright = 'SE', - } - - M.popup_menu = function(_, options) - -- print "OPTIONS:" - - local buf = vim.api.nvim_create_buf(false, true) - local win = vim.api.nvim_open_win(buf, true, { - relative = 'editor', - style = 'minimal', - anchor = pos_map[options.pos], - height = options.maxheight or options.minheight, - width = options.maxwidth or options.minwidth, - row = options.line, - col = options.col, - }) - - if options.filter then - local loop - loop = function() - vim.cmd([[redraw!]]) - local ok, ch = pcall(vim.fn.getcharstr) - if not ok then - return - end -- interrupted - - if ch == '<C-C>' then - return - end - - if not require('vim9script').bool(options.filter(nil, ch)) then - vim.cmd.normal(ch) - end - - vim.schedule(loop) - end - - vim.schedule(loop) - end - - return win - end - - M.popup_settext = function(id, text) - if type(text) == 'string' then - -- text = vim.split(text, "\n") - error("Haven't handled string yet") - end - - local lines = {} - for _, obj in ipairs(text) do - table.insert(lines, obj.text) - end - - vim.api.nvim_buf_set_lines(vim.api.nvim_win_get_buf(id), 0, -1, false, lines) - end - - M.popup_filter_menu = function() - print('ok, just pretend we filtered the menu') - end - - M.popup_setoptions = function(id, _) - print('setting options...', id) - end - end - - M = setmetatable(M, { - __index = vim.fn, - }) - - return M -end)() -vim9['heredoc'] = (function() - local M = {} - - M.trim = function(lines) - local min_whitespace = 9999 - for _, line in ipairs(lines) do - local _, finish = string.find(line, '^%s*') - min_whitespace = math.min(min_whitespace, finish) - end - - local trimmed_lines = {} - for _, line in ipairs(lines) do - table.insert(trimmed_lines, string.sub(line, min_whitespace + 1)) - end - - return trimmed_lines - end - - return M -end)() -vim9['import'] = (function() - local imported = {} - imported.autoload = setmetatable({}, { - __index = function(_, name) - local luaname = 'autoload/' .. string.gsub(name, '%.vim$', '.lua') - local runtime_file = vim.api.nvim_get_runtime_file(luaname, false)[1] - if not runtime_file then - error('unable to find autoload file:' .. name) - end - - return imported.absolute[vim.fn.fnamemodify(runtime_file, ':p')] - end, - }) - - imported.absolute = setmetatable({}, { - __index = function(self, name) - if vim.uv.fs_stat(name) then - local result = loadfile(name)() - rawset(self, name, result) - - return result - end - - error(string.format('unabled to find absolute file: %s', name)) - end, - }) - - return function(info) - local name = info.name - - if info.autoload then - return imported.autoload[info.name] - end - - local debug_info = debug.getinfo(2, 'S') - local sourcing_path = vim.fn.fnamemodify(string.sub(debug_info.source, 2), ':p') - - -- Relative paths - if vim.startswith(name, '../') or vim.startswith(name, './') then - local luaname = string.gsub(name, '%.vim$', '.lua') - local directory = vim.fn.fnamemodify(sourcing_path, ':h') - local search = directory .. '/' .. luaname - return imported.absolute[search] - end - - if vim.startswith(name, '/') then - error('absolute path') - -- local luaname = string.gsub(name, "%.vim", ".lua") - -- local runtime_file = vim.api.nvim_get_runtime_file(luaname, false)[1] - -- if runtime_file then - -- runtime_file = vim.fn.fnamemodify(runtime_file, ":p") - -- return loadfile(runtime_file)() - -- end - end - - error('Unhandled case' .. vim.inspect(info) .. vim.inspect(debug_info)) - end -end)() -vim9['ops'] = (function() - local lib = vim9 - - local M = {} - - M['And'] = function(left, right) - return lib.bool(left) and lib.bool(right) - end - - M['Or'] = function(left, right) - return lib.bool(left) or lib.bool(right) - end - - M['Plus'] = function(left, right) - return left + right - end - - M['Multiply'] = function(left, right) - return left * right - end - - M['Divide'] = function(left, right) - return left / right - end - - M['StringConcat'] = function(left, right) - return left .. right - end - - M['EqualTo'] = function(left, right) - return left == right - end - - M['NotEqualTo'] = function(left, right) - return not M['EqualTo'](left, right) - end - - M['LessThan'] = function(left, right) - return left < right - end - - M['LessThanOrEqual'] = function(left, right) - return left <= right - end - - M['GreaterThan'] = function(left, right) - return left > right - end - - M['GreaterThanOrEqual'] = function(left, right) - return left >= right - end - - M['RegexpMatches'] = function(left, right) - return not not vim.regex(right):match_str(left) - end - - M['RegexpMatchesIns'] = function(left, right) - return not not vim.regex('\\c' .. right):match_str(left) - end - - M['NotRegexpMatches'] = function(left, right) - return not M['RegexpMatches'](left, right) - end - - M['Modulo'] = function(left, right) - return left % right - end - - M['Minus'] = function(left, right) - -- TODO: This is not right :) - return left - right - end - - return M -end)() -vim9['prefix'] = (function() - local lib = vim9 - - local M = {} - - M['Minus'] = function(right) - return -right - end - - M['Bang'] = function(right) - return not lib.bool(right) - end - - return M -end)() - -return vim9 diff --git a/runtime/pack/dist/opt/cfilter/plugin/cfilter.lua b/runtime/pack/dist/opt/cfilter/plugin/cfilter.lua @@ -1,115 +0,0 @@ ----------------------------------------- --- This file is generated via github.com/tjdevries/vim9jit --- For any bugs, please first consider reporting there. ----------------------------------------- - --- Ignore "value assigned to a local variable is unused" because --- we can't guarantee that local variables will be used by plugins --- luacheck: ignore ---- @diagnostic disable - -local vim9 = require('_vim9script') -local M = {} -local Qf_filter = nil --- vim9script - --- # cfilter.vim: Plugin to filter entries from a quickfix/location list --- # Last Change: Jun 30, 2022 --- # Maintainer: Yegappan Lakshmanan (yegappan AT yahoo DOT com) --- # Version: 2.0 --- # --- # Commands to filter the quickfix list: --- # :Cfilter[!] /{pat}/ --- # Create a new quickfix list from entries matching {pat} in the current --- # quickfix list. Both the file name and the text of the entries are --- # matched against {pat}. If ! is supplied, then entries not matching --- # {pat} are used. The pattern can be optionally enclosed using one of --- # the following characters: ', ", /. If the pattern is empty, then the --- # last used search pattern is used. --- # :Lfilter[!] /{pat}/ --- # Same as :Cfilter but operates on the current location list. --- # - -Qf_filter = function(qf, searchpat, bang) - qf = vim9.bool(qf) - local Xgetlist = function() end - local Xsetlist = function() end - local cmd = '' - local firstchar = '' - local lastchar = '' - local pat = '' - local title = '' - local Cond = function() end - local items = {} - - if vim9.bool(qf) then - Xgetlist = function(...) - return vim.fn['getqflist'](...) - end - Xsetlist = function(...) - return vim.fn['setqflist'](...) - end - cmd = ':Cfilter' .. bang - else - Xgetlist = function(...) - return vim9.fn_ref(M, 'getloclist', vim.deepcopy({ 0 }), ...) - end - - Xsetlist = function(...) - return vim9.fn_ref(M, 'setloclist', vim.deepcopy({ 0 }), ...) - end - - cmd = ':Lfilter' .. bang - end - - firstchar = vim9.index(searchpat, 0) - lastchar = vim9.slice(searchpat, -1, nil) - if firstchar == lastchar and (firstchar == '/' or firstchar == '"' or firstchar == "'") then - pat = vim9.slice(searchpat, 1, -2) - if pat == '' then - -- # Use the last search pattern - pat = vim.fn.getreg('/') - end - else - pat = searchpat - end - - if pat == '' then - return - end - - if bang == '!' then - Cond = function(_, val) - return vim9.ops.NotRegexpMatches(val.text, pat) - and vim9.ops.NotRegexpMatches(vim9.fn.bufname(val.bufnr), pat) - end - else - Cond = function(_, val) - return vim9.ops.RegexpMatches(val.text, pat) - or vim9.ops.RegexpMatches(vim9.fn.bufname(val.bufnr), pat) - end - end - - items = vim9.fn_mut('filter', { Xgetlist(), Cond }, { replace = 0 }) - title = cmd .. ' /' .. pat .. '/' - Xsetlist({}, ' ', { ['title'] = title, ['items'] = items }) -end - -vim.api.nvim_create_user_command('Cfilter', function(__vim9_arg_1) - Qf_filter(true, __vim9_arg_1.args, (__vim9_arg_1.bang and '!' or '')) -end, { - bang = true, - nargs = '+', - complete = nil, -}) - -vim.api.nvim_create_user_command('Lfilter', function(__vim9_arg_1) - Qf_filter(false, __vim9_arg_1.args, (__vim9_arg_1.bang and '!' or '')) -end, { - bang = true, - nargs = '+', - complete = nil, -}) - --- # vim: shiftwidth=2 sts=2 expandtab -return M diff --git a/runtime/pack/dist/opt/cfilter/plugin/cfilter.vim b/runtime/pack/dist/opt/cfilter/plugin/cfilter.vim @@ -0,0 +1,62 @@ +" cfilter.vim: Plugin to filter entries from a quickfix/location list +" Last Change: Aug 23, 2018 +" Maintainer: Yegappan Lakshmanan (yegappan AT yahoo DOT com) +" Version: 1.1 +" +" Commands to filter the quickfix list: +" :Cfilter[!] /{pat}/ +" Create a new quickfix list from entries matching {pat} in the current +" quickfix list. Both the file name and the text of the entries are +" matched against {pat}. If ! is supplied, then entries not matching +" {pat} are used. The pattern can be optionally enclosed using one of +" the following characters: ', ", /. If the pattern is empty, then the +" last used search pattern is used. +" :Lfilter[!] /{pat}/ +" Same as :Cfilter but operates on the current location list. +" +if exists("loaded_cfilter") + finish +endif +let loaded_cfilter = 1 + +func s:Qf_filter(qf, searchpat, bang) + if a:qf + let Xgetlist = function('getqflist') + let Xsetlist = function('setqflist') + let cmd = ':Cfilter' . a:bang + else + let Xgetlist = function('getloclist', [0]) + let Xsetlist = function('setloclist', [0]) + let cmd = ':Lfilter' . a:bang + endif + + let firstchar = a:searchpat[0] + let lastchar = a:searchpat[-1:] + if firstchar == lastchar && + \ (firstchar == '/' || firstchar == '"' || firstchar == "'") + let pat = a:searchpat[1:-2] + if pat == '' + " Use the last search pattern + let pat = @/ + endif + else + let pat = a:searchpat + endif + + if pat == '' + return + endif + + if a:bang == '!' + let cond = 'v:val.text !~# pat && bufname(v:val.bufnr) !~# pat' + else + let cond = 'v:val.text =~# pat || bufname(v:val.bufnr) =~# pat' + endif + + let items = filter(Xgetlist(), cond) + let title = cmd . ' /' . pat . '/' + call Xsetlist([], ' ', {'title' : title, 'items' : items}) +endfunc + +com! -nargs=+ -bang Cfilter call s:Qf_filter(1, <q-args>, <q-bang>) +com! -nargs=+ -bang Lfilter call s:Qf_filter(0, <q-args>, <q-bang>) diff --git a/test/functional/plugin/ccomplete_spec.lua b/test/functional/plugin/ccomplete_spec.lua @@ -1,63 +0,0 @@ -local t = require('test.testutil') -local n = require('test.functional.testnvim')() - -local clear = n.clear -local command = n.command -local eq = t.eq -local eval = n.eval -local feed = n.feed -local write_file = t.write_file - -describe('ccomplete#Complete', function() - setup(function() - -- Realistic tags generated from neovim source tree using `ctags -R *` - write_file( - 'Xtags', - [[ -augroup_del src/nvim/autocmd.c /^void augroup_del(char *name, bool stupid_legacy_mode)$/;" f typeref:typename:void -augroup_exists src/nvim/autocmd.c /^bool augroup_exists(const char *name)$/;" f typeref:typename:bool -augroup_find src/nvim/autocmd.c /^int augroup_find(const char *name)$/;" f typeref:typename:int -aupat_get_buflocal_nr src/nvim/autocmd.c /^int aupat_get_buflocal_nr(char *pat, int patlen)$/;" f typeref:typename:int -aupat_is_buflocal src/nvim/autocmd.c /^bool aupat_is_buflocal(char *pat, int patlen)$/;" f typeref:typename:bool -expand_get_augroup_name src/nvim/autocmd.c /^char *expand_get_augroup_name(expand_T *xp, int idx)$/;" f typeref:typename:char * -expand_get_event_name src/nvim/autocmd.c /^char *expand_get_event_name(expand_T *xp, int idx)$/;" f typeref:typename:char * -]] - ) - end) - - before_each(function() - clear() - command('set tags=Xtags') - end) - - teardown(function() - os.remove('Xtags') - end) - - it('can complete from Xtags', function() - local completed = eval('ccomplete#Complete(0, "a")') - eq(5, #completed) - eq('augroup_del(', completed[1].word) - eq('f', completed[1].kind) - - local aupat = eval('ccomplete#Complete(0, "aupat")') - eq(2, #aupat) - eq('aupat_get_buflocal_nr(', aupat[1].word) - eq('f', aupat[1].kind) - end) - - it('does not error when returning no matches', function() - local completed = eval('ccomplete#Complete(0, "doesnotmatch")') - eq({}, completed) - end) - - it('can find the beginning of a word for C', function() - command('set filetype=c') - feed('i int something = augroup') - local result = eval('ccomplete#Complete(1, "")') - eq(#' int something = ', result) - - local completed = eval('ccomplete#Complete(0, "augroup")') - eq(3, #completed) - end) -end)