nixos/lua-lsp/script/core/completion/completion.lua

2273 lines
73 KiB
Lua

local define = require 'proto.define'
local files = require 'files'
local matchKey = require 'core.matchkey'
local vm = require 'vm'
local getName = require 'core.hover.name'
local getArgs = require 'core.hover.args'
local getHover = require 'core.hover'
local config = require 'config'
local util = require 'utility'
local markdown = require 'provider.markdown'
local parser = require 'parser'
local keyWordMap = require 'core.completion.keyword'
local workspace = require 'workspace'
local furi = require 'file-uri'
local rpath = require 'workspace.require-path'
local lang = require 'language'
local lookBackward = require 'core.look-backward'
local guide = require 'parser.guide'
local await = require 'await'
local postfix = require 'core.completion.postfix'
local diag = require 'proto.diagnostic'
local wssymbol = require 'core.workspace-symbol'
local findSource = require 'core.find-source'
local diagnostic = require 'provider.diagnostic'
local diagnosticModes = {
'disable-next-line',
'disable-line',
'disable',
'enable',
}
local stackID = 0
local stacks = {}
---@param callback async fun(newSource: parser.object): table
local function stack(oldSource, callback)
stackID = stackID + 1
local uri = guide.getUri(oldSource)
local pos = oldSource.start
local tp = oldSource.type
---@async
stacks[stackID] = function ()
local state = files.getState(uri)
if not state then
return
end
local newSource = findSource(state, pos, { [tp] = true })
if not newSource then
return
end
return callback(newSource)
end
return stackID
end
local function clearStack()
stacks = {}
end
---@async
local function resolveStack(id)
local callback = stacks[id]
if not callback then
log.warn('Unknown resolved id', id)
return nil
end
return callback()
end
local function trim(str)
return str:match '^%s*(%S+)%s*$'
end
local function findNearestSource(state, position)
---@type parser.object
local source
guide.eachSourceContain(state.ast, position, function (src)
source = src
end)
return source
end
local function findNearestTableField(state, position)
local uri = state.uri
local text = files.getText(uri)
if not text then
return nil
end
local offset = guide.positionToOffset(state, position)
local soffset = lookBackward.findAnyOffset(text, offset)
if not soffset then
return nil
end
local symbol = text:sub(soffset, soffset)
if symbol == '}' then
return nil
end
local sposition = guide.offsetToPosition(state, soffset)
local source
guide.eachSourceContain(state.ast, sposition, function (src)
if src.type == 'table'
or src.type == 'tablefield'
or src.type == 'tableindex'
or src.type == 'tableexp' then
source = src
end
end)
return source
end
local function findParent(state, position)
local text = state.lua
local offset = guide.positionToOffset(state, position)
for i = offset, 1, -1 do
local char = text:sub(i, i)
if lookBackward.isSpace(char) then
goto CONTINUE
end
local oop
if char == '.' then
-- `..` 的情况
if text:sub(i - 1, i - 1) == '.' then
return nil, nil
end
oop = false
elseif char == ':' then
oop = true
else
return nil, nil
end
local anyOffset = lookBackward.findAnyOffset(text, i - 1)
if not anyOffset then
return nil, nil
end
local anyPos = guide.offsetToPosition(state, anyOffset)
local parent = guide.eachSourceContain(state.ast, anyPos, function (source)
if source.finish == anyPos then
return source
end
end)
if parent then
return parent, oop
end
::CONTINUE::
end
return nil, nil
end
local function findParentInStringIndex(state, position)
local near, nearStart
guide.eachSourceContain(state.ast, position, function (source)
local start = guide.getStartFinish(source)
if not start then
return
end
if not nearStart or nearStart < start then
near = source
nearStart = start
end
end)
if not near or near.type ~= 'string' then
return
end
local parent = near.parent
if not parent or parent.index ~= near then
return
end
-- index不可能是oop模式
return parent.node, false
end
local function buildFunctionSnip(source, value, oop)
local name = (getName(source) or ''):gsub('^.+[$.:]', '')
local args = getArgs(value)
if oop then
table.remove(args, 1)
end
local snipArgs = {}
for id, arg in ipairs(args) do
local str, count = arg:gsub('^(%s*)(%.%.%.)(.+)', function (sp, word)
return ('%s${%d:%s}'):format(sp, id, word)
end)
if count == 0 then
str = arg:gsub('^(%s*)([^:]+)(.+)', function (sp, word)
return ('%s${%d:%s}'):format(sp, id, word)
end)
end
table.insert(snipArgs, str)
end
return ('%s(%s)'):format(name, table.concat(snipArgs, ', '))
end
local function buildDetail(source)
local types = vm.getInfer(source):view(guide.getUri(source))
local literals = vm.getInfer(source):viewLiterals()
if literals then
return types .. ' = ' .. literals
else
return types
end
end
local function getSnip(source)
local context = config.get(guide.getUri(source), 'Lua.completion.displayContext')
if context <= 0 then
return nil
end
local defs = vm.getDefs(source)
for _, def in ipairs(defs) do
if def ~= source and def.type == 'function' then
local uri = guide.getUri(def)
local text = files.getText(uri)
local state = files.getState(uri)
if not state then
goto CONTINUE
end
local lines = state.lines
if not text then
goto CONTINUE
end
if vm.isMetaFile(uri) then
goto CONTINUE
end
local firstRow = guide.rowColOf(def.start)
local lastRow = math.min(guide.rowColOf(def.finish) + 1, firstRow + context)
local lastOffset = lines[lastRow] and (lines[lastRow] - 1) or #text
local snip = text:sub(lines[firstRow], lastOffset)
return snip
end
::CONTINUE::
end
end
---@async
local function buildDesc(source)
local desc = markdown()
local hover = getHover.get(source)
desc:add('md', hover)
desc:splitLine()
desc:add('lua', getSnip(source))
return desc
end
local function buildFunction(results, source, value, oop, data)
local snipType = config.get(guide.getUri(source), 'Lua.completion.callSnippet')
if snipType == 'Disable' or snipType == 'Both' then
results[#results+1] = data
end
if snipType == 'Both' or snipType == 'Replace' then
local snipData = util.deepCopy(data)
snipData.kind = snipType == 'Both'
and define.CompletionItemKind.Snippet
or data.kind
snipData.insertText = buildFunctionSnip(source, value, oop)
snipData.insertTextFormat = 2
snipData.command = {
title = 'trigger signature',
command = 'editor.action.triggerParameterHints',
}
snipData.id = stack(source, function (newSource) ---@async
return {
detail = buildDetail(newSource),
description = buildDesc(newSource),
}
end)
results[#results+1] = snipData
end
end
local function isSameSource(state, source, pos)
if guide.getUri(source) ~= guide.getUri(state.ast) then
return false
end
if source.type == 'field'
or source.type == 'method' then
source = source.parent
end
return source.start <= pos and source.finish >= pos
end
local function getParams(func, oop)
if not func.args then
return '()'
end
local args = {}
for _, arg in ipairs(func.args) do
if arg.type == '...' then
args[#args+1] = '...'
elseif arg.type == 'doc.type.arg' then
args[#args+1] = arg.name[1]
else
args[#args+1] = arg[1]
end
end
if oop and args[1] ~= '...' then
table.remove(args, 1)
end
return '(' .. table.concat(args, ', ') .. ')'
end
local function checkLocal(state, word, position, results)
local locals = guide.getVisibleLocals(state.ast, position)
for name, source in util.sortPairs(locals) do
if isSameSource(state, source, position) then
goto CONTINUE
end
if not matchKey(word, name) then
goto CONTINUE
end
if name:sub(1, 1) == '@' then
goto CONTINUE
end
if vm.getInfer(source):hasFunction(state.uri) then
local defs = vm.getDefs(source)
-- make sure `function` is before `doc.type.function`
local orders = {}
for i, def in ipairs(defs) do
if def.type == 'function' then
orders[def] = i - 20000
elseif def.type == 'doc.type.function' then
orders[def] = i - 10000
else
orders[def] = i
end
end
table.sort(defs, function (a, b)
return orders[a] < orders[b]
end)
for _, def in ipairs(defs) do
if (def.type == 'function' and not vm.isVarargFunctionWithOverloads(def))
or def.type == 'doc.type.function' then
local funcLabel = name .. getParams(def, false)
buildFunction(results, source, def, false, {
label = funcLabel,
match = name,
insertText = name,
kind = define.CompletionItemKind.Function,
id = stack(source, function (newSource) ---@async
return {
detail = buildDetail(newSource),
description = buildDesc(newSource),
}
end),
})
end
end
else
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Variable,
id = stack(source, function (newSource) ---@async
return {
detail = buildDetail(newSource),
description = buildDesc(newSource),
}
end),
}
end
::CONTINUE::
end
end
local function checkModule(state, word, position, results)
if not config.get(state.uri, 'Lua.completion.autoRequire') then
return
end
local globals = util.arrayToHash(config.get(state.uri, 'Lua.diagnostics.globals'))
local locals = guide.getVisibleLocals(state.ast, position)
for uri in files.eachFile(state.uri) do
if uri == guide.getUri(state.ast) then
goto CONTINUE
end
local path = furi.decode(uri)
local fileName = path:match '[^/\\]*$'
local stemName = fileName:gsub('%..+', '')
if not locals[stemName]
and not vm.hasGlobalSets(state.uri, 'variable', stemName)
and not globals[stemName]
and stemName:match(guide.namePatternFull)
and matchKey(word, stemName) then
local targetState = files.getState(uri)
if not targetState then
goto CONTINUE
end
local targetReturns = targetState.ast.returns
if not targetReturns then
goto CONTINUE
end
local targetSource = targetReturns[1] and targetReturns[1][1]
if not targetSource then
goto CONTINUE
end
if targetSource.type ~= 'getlocal'
and targetSource.type ~= 'table'
and targetSource.type ~= 'function' then
goto CONTINUE
end
if targetSource.type == 'getlocal'
and vm.getDeprecated(targetSource.node) then
goto CONTINUE
end
results[#results+1] = {
label = stemName,
kind = define.CompletionItemKind.Variable,
commitCharacters = { '.' },
command = {
title = 'autoRequire',
command = 'lua.autoRequire',
arguments = {
{
uri = guide.getUri(state.ast),
target = uri,
name = stemName,
},
},
},
id = stack(targetSource, function (newSource) ---@async
local md = markdown()
md:add('md', lang.script('COMPLETION_IMPORT_FROM', ('[%s](%s)'):format(
workspace.getRelativePath(uri),
uri
)))
md:add('md', buildDesc(newSource))
return {
detail = buildDetail(newSource),
description = md,
--additionalTextEdits = buildInsertRequire(state, originUri, stemName),
}
end)
}
end
::CONTINUE::
end
end
local function checkFieldFromFieldToIndex(state, name, src, parent, word, startPos, position)
if name:match(guide.namePatternFull) then
if not name:match '[\x80-\xff]'
or config.get(state.uri, 'Lua.runtime.unicodeName') then
return nil
end
end
local textEdit, additionalTextEdits
local startOffset = guide.positionToOffset(state, startPos)
local offset = guide.positionToOffset(state, position)
local wordStartOffset
if word == '' then
wordStartOffset = state.lua:match('()%S', startOffset + 1)
if wordStartOffset then
wordStartOffset = wordStartOffset - 1
else
wordStartOffset = offset
end
else
wordStartOffset = offset - #word
end
local wordStartPos = guide.offsetToPosition(state, wordStartOffset)
local newText
if vm.getKeyType(src) == 'string' then
newText = ('[%q]'):format(name)
else
newText = ('[%s]'):format(name)
end
textEdit = {
start = wordStartPos,
finish = position,
newText = newText,
}
local nxt = parent.next
if nxt then
local dotStart, dotFinish
if nxt.type == 'setfield'
or nxt.type == 'getfield'
or nxt.type == 'tablefield' then
dotStart = nxt.dot.start
dotFinish = nxt.dot.finish
elseif nxt.type == 'setmethod'
or nxt.type == 'getmethod' then
dotStart = nxt.colon.start
dotFinish = nxt.colon.finish
end
if dotStart then
additionalTextEdits = {
{
start = dotStart,
finish = dotFinish,
newText = '',
}
}
end
else
if config.get(state.uri, 'Lua.runtime.version') == 'Lua 5.1'
or config.get(state.uri, 'Lua.runtime.version') == 'LuaJIT' then
textEdit.newText = '_G' .. textEdit.newText
else
textEdit.newText = '_ENV' .. textEdit.newText
end
end
return textEdit, additionalTextEdits
end
local function checkFieldThen(state, name, src, word, startPos, position, parent, oop, results)
local value = vm.getObjectFunctionValue(src) or src
local kind = define.CompletionItemKind.Field
if (value.type == 'function' and not vm.isVarargFunctionWithOverloads(value))
or value.type == 'doc.type.function' then
if oop then
kind = define.CompletionItemKind.Method
else
kind = define.CompletionItemKind.Function
end
buildFunction(results, src, value, oop, {
label = name,
kind = kind,
match = name:match '^[^(]+',
insertText = name:match '^[^(]+',
deprecated = vm.getDeprecated(src) and true or nil,
id = stack(src, function (newSrc) ---@async
return {
detail = buildDetail(newSrc),
description = buildDesc(newSrc),
}
end),
})
return
end
if oop and not vm.getInfer(src):hasFunction(state.uri) then
return
end
local literal = guide.getLiteral(value)
if literal ~= nil then
kind = define.CompletionItemKind.Enum
end
local textEdit, additionalTextEdits
if parent.next and parent.next.index then
local str = parent.next.index
textEdit = {
start = str.start + #str[2],
finish = position,
newText = name,
}
else
textEdit, additionalTextEdits = checkFieldFromFieldToIndex(state, name, src, parent, word, startPos, position)
end
results[#results+1] = {
label = name,
kind = kind,
deprecated = vm.getDeprecated(src) and true or nil,
textEdit = textEdit,
id = stack(src, function (newSrc) ---@async
return {
detail = buildDetail(newSrc),
description = buildDesc(newSrc),
}
end),
additionalTextEdits = additionalTextEdits,
}
end
---@async
local function checkFieldOfRefs(refs, state, word, startPos, position, parent, oop, results, locals, isGlobal)
local fields = {}
local funcs = {}
local count = 0
for _, src in ipairs(refs) do
local _, name = vm.viewKey(src, state.uri)
if not name then
goto CONTINUE
end
if isSameSource(state, src, startPos) then
goto CONTINUE
end
name = tostring(name)
if isGlobal and locals and locals[name] then
goto CONTINUE
end
if not matchKey(word, name, count >= 100) then
goto CONTINUE
end
if not vm.isVisible(parent, src) then
goto CONTINUE
end
local funcLabel
if config.get(state.uri, 'Lua.completion.showParams') then
--- TODO determine if getlocal should be a function here too
local value = vm.getObjectFunctionValue(src) or src
if value.type == 'function'
or value.type == 'doc.type.function' then
if not vm.isVarargFunctionWithOverloads(value) then
funcLabel = name .. getParams(value, oop)
fields[funcLabel] = src
count = count + 1
end
if value.type == 'function' and value.bindDocs then
for _, doc in ipairs(value.bindDocs) do
if doc.type == 'doc.overload' then
funcLabel = name .. getParams(doc.overload, oop)
fields[funcLabel] = doc.overload
end
end
end
funcs[name] = true
if fields[name] and not guide.isSet(fields[name]) then
fields[name] = nil
end
goto CONTINUE
end
end
local last = fields[name]
if last == nil and not funcs[name] then
fields[name] = src
count = count + 1
goto CONTINUE
end
if vm.getDeprecated(src) then
goto CONTINUE
end
if guide.isSet(src) then
fields[name] = src
goto CONTINUE
end
::CONTINUE::
end
for name, src in util.sortPairs(fields) do
if src then
checkFieldThen(state, name, src, word, startPos, position, parent, oop, results)
await.delay()
end
end
end
---@async
local function checkGlobal(state, word, startPos, position, parent, oop, results)
local locals = guide.getVisibleLocals(state.ast, position)
local globals = vm.getGlobalSets(state.uri, 'variable')
checkFieldOfRefs(globals, state, word, startPos, position, parent, oop, results, locals, 'global')
end
---@async
local function checkField(state, word, start, position, parent, oop, results)
if parent.tag == '_ENV' or parent.special == '_G' then
local globals = vm.getGlobalSets(state.uri, 'variable')
checkFieldOfRefs(globals, state, word, start, position, parent, oop, results)
else
local refs = vm.getFields(parent)
checkFieldOfRefs(refs, state, word, start, position, parent, oop, results)
end
end
local function checkTableField(state, word, start, results)
local source = guide.eachSourceContain(state.ast, start, function (source)
if source.start == start
and source.parent
and source.parent.type == 'table' then
return source
end
end)
if not source then
return
end
local used = {}
guide.eachSourceType(state.ast, 'tablefield', function (src)
if not src.field then
return
end
local key = src.field[1]
if not used[key]
and matchKey(word, key)
and src ~= source then
used[key] = true
results[#results+1] = {
label = key,
kind = define.CompletionItemKind.Property,
}
end
end)
end
local function checkCommon(state, word, position, results)
local myUri = state.uri
local showWord = config.get(state.uri, 'Lua.completion.showWord')
if showWord == 'Disable' then
return
end
results.enableCommon = true
if showWord == 'Fallback' and #results ~= 0 then
return
end
local used = {}
for _, result in ipairs(results) do
used[result.label:match '^[^(]*'] = true
end
for _, data in ipairs(keyWordMap) do
used[data[1]] = true
end
if config.get(state.uri, 'Lua.completion.workspaceWord') and #word >= 2 then
local myHead = word:sub(1, 2)
for uri in files.eachFile(state.uri) do
if #results >= 100 then
results.incomplete = true
break
end
if myUri == uri then
goto CONTINUE
end
local words = files.getWordsOfHead(uri, myHead)
if not words then
goto CONTINUE
end
for _, str in ipairs(words) do
if #results >= 100 then
break
end
if not used[str]
and str ~= word then
used[str] = true
if matchKey(word, str) then
results[#results+1] = {
label = str,
kind = define.CompletionItemKind.Text,
}
end
end
end
::CONTINUE::
end
for uri in files.eachDll() do
if #results >= 100 then
break
end
local words = files.getDllWords(uri) or {}
for _, str in ipairs(words) do
if #results >= 100 then
break
end
if #str >= 3 and not used[str] and str ~= word then
used[str] = true
if matchKey(word, str) then
results[#results+1] = {
label = str,
kind = define.CompletionItemKind.Text,
}
end
end
end
end
end
for str, offset in state.lua:gmatch('(' .. guide.namePattern .. ')()') do
if #results >= 100 then
results.incomplete = true
break
end
if #str >= 3
and not used[str]
and guide.offsetToPosition(state, offset - 1) ~= position then
used[str] = true
if matchKey(word, str) then
results[#results+1] = {
label = str,
kind = define.CompletionItemKind.Text,
}
end
end
end
end
local function checkKeyWord(state, start, position, word, hasSpace, afterLocal, results)
local text = state.lua
local snipType = config.get(state.uri, 'Lua.completion.keywordSnippet')
local symbol = lookBackward.findSymbol(text, guide.positionToOffset(state, start))
local isExp = symbol == '(' or symbol == ',' or symbol == '=' or symbol == '[' or symbol == '{'
local info = {
hasSpace = hasSpace,
isExp = isExp,
text = text,
start = start,
uri = guide.getUri(state.ast),
position = position,
state = state,
}
for _, data in ipairs(keyWordMap) do
local key = data[1]
local eq
if hasSpace then
eq = word == key
else
eq = matchKey(word, key)
end
if afterLocal and key ~= 'function' then
eq = false
end
if not eq then
goto CONTINUE
end
if isExp then
if key ~= 'nil'
and key ~= 'true'
and key ~= 'false'
and key ~= 'function' then
goto CONTINUE
end
end
local replaced
local extra
if snipType == 'Both' or snipType == 'Replace' then
local func = data[2]
if func then
replaced = func(info, results)
extra = true
end
end
if snipType == 'Both' then
replaced = false
end
if not replaced then
if not hasSpace then
local item = {
label = key,
kind = define.CompletionItemKind.Keyword,
}
if #results > 0 and extra then
table.insert(results, #results, item)
else
results[#results+1] = item
end
end
end
local checkStop = data[3]
if checkStop then
local stop = checkStop(info)
if stop then
return true
end
end
::CONTINUE::
end
end
local function checkProvideLocal(state, word, start, results)
local block
guide.eachSourceContain(state.ast, start, function (source)
if source.type == 'function'
or source.type == 'main' then
block = source
end
end)
if not block then
return
end
local used = {}
guide.eachSourceType(block, 'getglobal', function (source)
if source.start > start
and not used[source[1]]
and matchKey(word, source[1]) then
used[source[1]] = true
results[#results+1] = {
label = source[1],
kind = define.CompletionItemKind.Variable,
}
end
end)
guide.eachSourceType(block, 'getlocal', function (source)
if source.start > start
and not used[source[1]]
and matchKey(word, source[1]) then
used[source[1]] = true
results[#results+1] = {
label = source[1],
kind = define.CompletionItemKind.Variable,
}
end
end)
end
local function checkFunctionArgByDocParam(state, word, startPos, results)
local func = guide.eachSourceContain(state.ast, startPos, function (source)
if source.type == 'function' then
return source
end
end)
if not func then
return
end
local docs = func.bindDocs
if not docs then
return
end
local params = {}
for _, doc in ipairs(docs) do
if doc.type == 'doc.param' then
params[#params+1] = doc
end
end
local firstArg = func.args and func.args[1]
if not firstArg
or firstArg.start <= startPos and firstArg.finish >= startPos then
local firstParam = params[1]
if firstParam and matchKey(word, firstParam.param[1]) then
local label = {}
for _, param in ipairs(params) do
label[#label+1] = param.param[1]
end
results[#results+1] = {
label = table.concat(label, ', '),
match = firstParam.param[1],
kind = define.CompletionItemKind.Snippet,
}
end
end
for _, doc in ipairs(params) do
if matchKey(word, doc.param[1]) then
results[#results+1] = {
label = doc.param[1],
kind = define.CompletionItemKind.Interface,
}
end
end
end
local function isAfterLocal(state, startPos)
local text = state.lua
local offset = guide.positionToOffset(state, startPos)
local pos = lookBackward.skipSpace(text, offset)
local word = lookBackward.findWord(text, pos)
return word == 'local'
end
local function collectRequireNames(mode, myUri, literal, source, smark, position, results)
local collect = {}
if mode == 'require' then
for uri in files.eachFile(myUri) do
if myUri == uri then
goto CONTINUE
end
local path = furi.decode(uri)
local infos = rpath.getVisiblePath(myUri, path)
local relative = workspace.getRelativePath(path)
for _, info in ipairs(infos) do
if matchKey(literal, info.name) then
if not collect[info.name] then
collect[info.name] = {
textEdit = {
start = smark and (source.start + #smark) or position,
finish = smark and (source.finish - #smark) or position,
newText = smark and info.name or util.viewString(info.name),
},
path = relative,
}
end
if vm.isMetaFile(uri) then
collect[info.name][#collect[info.name]+1] = ('* [[meta]](%s)'):format(uri)
else
collect[info.name][#collect[info.name]+1] = ([=[* [%s](%s) %s]=]):format(
relative,
uri,
lang.script('HOVER_USE_LUA_PATH', info.searcher)
)
end
end
end
::CONTINUE::
end
for uri in files.eachDll() do
local opens = files.getDllOpens(uri) or {}
local path = workspace.getRelativePath(uri)
for _, open in ipairs(opens) do
if matchKey(literal, open) then
if not collect[open] then
collect[open] = {
textEdit = {
start = smark and (source.start + #smark) or position,
finish = smark and (source.finish - #smark) or position,
newText = smark and open or util.viewString(open),
},
path = path,
}
end
collect[open][#collect[open]+1] = ([=[* [%s](%s)]=]):format(
path,
uri
)
end
end
end
else
for uri in files.eachFile(myUri) do
if myUri == uri then
goto CONTINUE
end
if vm.isMetaFile(uri) then
goto CONTINUE
end
local path = workspace.getRelativePath(uri)
path = path:gsub('\\', '/')
if matchKey(literal, path) then
if not collect[path] then
collect[path] = {
textEdit = {
start = smark and (source.start + #smark) or position,
finish = smark and (source.finish - #smark) or position,
newText = smark and path or util.viewString(path),
}
}
end
collect[path][#collect[path]+1] = ([=[[%s](%s)]=]):format(
path,
uri
)
end
::CONTINUE::
end
end
for label, infos in util.sortPairs(collect) do
local mark = {}
local des = {}
for _, info in ipairs(infos) do
if not mark[info] then
mark[info] = true
des[#des+1] = info
end
end
results[#results+1] = {
label = label,
detail = infos.path,
kind = define.CompletionItemKind.File,
description = table.concat(des, '\n'),
textEdit = infos.textEdit,
}
end
end
local function checkUri(state, position, results)
local myUri = guide.getUri(state.ast)
guide.eachSourceContain(state.ast, position, function (source)
if source.type ~= 'string' then
return
end
local callargs = source.parent
if not callargs or callargs.type ~= 'callargs' then
return
end
if callargs[1] ~= source then
return
end
local call = callargs.parent
local func = call.node
local literal = guide.getLiteral(source)
local libName = vm.getLibraryName(func)
if not libName then
return
end
if libName == 'require'
or libName == 'dofile'
or libName == 'loadfile' then
collectRequireNames(libName, myUri, literal, source, source[2], position, results)
end
end)
end
local function checkLenPlusOne(state, position, results)
local text = state.lua
guide.eachSourceContain(state.ast, position, function (source)
if source.type == 'getindex'
or source.type == 'setindex' then
local finish = guide.positionToOffset(state, source.node.finish)
local _, offset = text:find('%s*%[%s*%#', finish)
if not offset then
return
end
local start = guide.positionToOffset(state, source.node.start) + 1
local nodeText = text:sub(start, finish)
local writingText = trim(text:sub(offset + 1, guide.positionToOffset(state, position))) or ''
if not matchKey(writingText, nodeText) then
return
end
local offsetPos = guide.offsetToPosition(state, offset) - 1
if source.parent == guide.getParentBlock(source) then
local sourceFinish = guide.positionToOffset(state, source.finish)
-- state
local label = text:match('%#[ \t]*', offset) .. nodeText .. '+1'
local eq = text:find('^%s*%]?%s*%=', sourceFinish)
local newText = label .. ']'
if not eq then
newText = newText .. ' = '
end
results[#results+1] = {
label = label,
match = nodeText,
kind = define.CompletionItemKind.Snippet,
textEdit = {
start = offsetPos,
finish = source.finish,
newText = newText,
},
}
else
-- exp
local label = text:match('%#[ \t]*', offset) .. nodeText
local newText = label .. ']'
results[#results+1] = {
label = label,
kind = define.CompletionItemKind.Snippet,
textEdit = {
start = offsetPos,
finish = source.finish,
newText = newText,
},
}
end
end
end)
end
local function tryLabelInString(label, source)
if not source or source.type ~= 'string' then
return label
end
local state = parser.compile(label, 'String')
if not state or not state.ast then
return label
end
if not matchKey(source[1], state.ast[1]--[[@as string]]) then
return nil
end
return util.viewString(state.ast[1], source[2])
end
local function cleanEnums(enums, source)
for i = #enums, 1, -1 do
local enum = enums[i]
local label = tryLabelInString(enum.label, source)
if label then
enum.label = label
enum.textEdit = source and {
start = source.start,
finish = source.finish,
newText = enum.insertText or label,
}
end
end
return enums
end
---@param state parser.state
---@param pos integer
---@param doc vm.node.object
---@param enums table[]
---@return table[]?
local function insertDocEnum(state, pos, doc, enums)
local tbl = doc.bindSource
if not tbl then
return nil
end
local parent = tbl.parent
local parentName
if vm.getGlobalNode(parent) then
parentName = vm.getGlobalNode(parent):getCodeName()
else
local locals = guide.getVisibleLocals(state.ast, pos)
for _, loc in pairs(locals) do
if util.arrayHas(vm.getDefs(loc), tbl) then
parentName = loc[1]
break
end
end
end
local valueEnums = {}
for _, field in ipairs(tbl) do
if field.type == 'tablefield'
or field.type == 'tableindex' then
if not field.value then
goto CONTINUE
end
local key = guide.getKeyName(field)
if not key then
goto CONTINUE
end
if parentName then
enums[#enums+1] = {
label = parentName .. '.' .. key,
kind = define.CompletionItemKind.EnumMember,
id = stack(field, function (newField) ---@async
return {
detail = buildDetail(newField),
description = buildDesc(newField),
}
end),
}
end
for nd in vm.compileNode(field.value):eachObject() do
if nd.type == 'boolean'
or nd.type == 'number'
or nd.type == 'integer'
or nd.type == 'string' then
valueEnums[#valueEnums+1] = {
label = util.viewLiteral(nd[1]),
kind = define.CompletionItemKind.EnumMember,
id = stack(field, function (newField) ---@async
return {
detail = buildDetail(newField),
description = buildDesc(newField),
}
end),
}
end
end
::CONTINUE::
end
end
for _, enum in ipairs(valueEnums) do
enums[#enums+1] = enum
end
return enums
end
local function buildInsertDocFunction(doc)
local args = {}
for i, arg in ipairs(doc.args) do
args[i] = ('${%d:%s}'):format(i, arg.name[1])
end
return ("\z
function (%s)\
\t$0\
end"):format(table.concat(args, ', '))
end
---@param state parser.state
---@param pos integer
---@param src vm.node.object
---@param enums table[]
---@param isInArray boolean?
---@param mark table?
local function insertEnum(state, pos, src, enums, isInArray, mark)
mark = mark or {}
if mark[src] then
return
end
mark[src] = true
if src.type == 'doc.type.string'
or src.type == 'doc.type.integer'
or src.type == 'doc.type.boolean' then
---@cast src parser.object
enums[#enums+1] = {
label = vm.viewObject(src, state.uri),
description = src.comment,
kind = define.CompletionItemKind.EnumMember,
}
elseif src.type == 'doc.type.code' then
enums[#enums+1] = {
label = src[1],
description = src.comment,
kind = define.CompletionItemKind.EnumMember,
}
elseif src.type == 'doc.type.function' then
---@cast src parser.object
local insertText = buildInsertDocFunction(src)
local description
if src.comment then
description = src.comment
else
local descText = insertText:gsub('%$%{%d+:([^}]+)%}', function (val)
return val
end):gsub('%$%{?%d+%}?', '')
description = markdown()
: add('lua', descText)
: string()
end
enums[#enums+1] = {
label = vm.getInfer(src):view(state.uri),
description = description,
kind = define.CompletionItemKind.Function,
insertText = insertText,
}
elseif isInArray and src.type == 'doc.type.array' then
for i, d in ipairs(vm.getDefs(src.node)) do
insertEnum(state, pos, d, enums, isInArray, mark)
end
elseif src.type == 'global' and src.cate == 'type' then
for _, set in ipairs(src:getSets(state.uri)) do
if set.type == 'doc.enum' then
insertDocEnum(state, pos, set, enums)
end
end
end
end
local function checkTypingEnum(state, position, defs, str, results, isInArray)
local enums = {}
for _, def in ipairs(defs) do
insertEnum(state, position, def, enums, isInArray)
end
cleanEnums(enums, str)
for _, res in ipairs(enums) do
results[#results+1] = res
end
end
local function checkEqualEnumLeft(state, position, source, results, isInArray)
if not source then
return
end
local str = guide.eachSourceContain(state.ast, position, function (src)
if src.type == 'string' then
return src
end
end)
local defs = vm.getDefs(source)
checkTypingEnum(state, position, defs, str, results, isInArray)
end
local function checkEqualEnum(state, position, results)
local text = state.lua
local start = lookBackward.findTargetSymbol(text, guide.positionToOffset(state, position), '=')
if not start then
return
end
local eqOrNeq
if text:sub(start - 1, start - 1) == '='
or text:sub(start - 1, start - 1) == '~' then
start = start - 1
eqOrNeq = true
end
start = lookBackward.skipSpace(text, start - 1)
local source = findNearestSource(state, guide.offsetToPosition(state, start))
if not source then
return
end
if source.type == 'callargs' then
source = source.parent
end
if source.type == 'call' and not eqOrNeq then
return
end
checkEqualEnumLeft(state, position, source, results)
end
local function checkEqualEnumInString(state, position, results)
local source = findNearestSource(state, position)
local parent = source.parent
if parent.type == 'binary' then
if source ~= parent[2] then
return
end
if not parent.op then
return
end
if parent.op.type ~= '==' and parent.op.type ~= '~=' then
return
end
checkEqualEnumLeft(state, position, parent[1], results)
end
if (parent.type == 'tableexp') then
checkEqualEnumLeft(state, position, parent.parent.parent, results, true)
return
end
if parent.type == 'local' then
checkEqualEnumLeft(state, position, parent, results)
end
if parent.type == 'setlocal'
or parent.type == 'setglobal'
or parent.type == 'setfield'
or parent.type == 'setindex' then
checkEqualEnumLeft(state, position, parent.node, results)
end
if parent.type == 'tablefield'
or parent.type == 'tableindex' then
checkEqualEnumLeft(state, position, parent, results)
end
end
local function isFuncArg(state, position)
return guide.eachSourceContain(state.ast, position, function (source)
if source.type == 'funcargs' then
return true
end
end)
end
local function trySpecial(state, position, results)
if guide.isInString(state.ast, position) then
checkUri(state, position, results)
checkEqualEnumInString(state, position, results)
return
end
-- x[#x+1]
checkLenPlusOne(state, position, results)
-- type(o) ==
checkEqualEnum(state, position, results)
end
---@async
local function tryIndex(state, position, results)
local parent, oop = findParentInStringIndex(state, position)
if not parent then
return
end
local word = parent.next and parent.next.index and parent.next.index[1]
if not word then
return
end
checkField(state, word, position, position, parent, oop, results)
end
---@async
local function tryWord(state, position, triggerCharacter, results)
if triggerCharacter == '('
or triggerCharacter == '#'
or triggerCharacter == ','
or triggerCharacter == '{' then
return
end
local text = state.lua
local offset = guide.positionToOffset(state, position)
local finish = lookBackward.skipSpace(text, offset)
local word, start = lookBackward.findWord(text, offset)
local startPos
if not word then
word = ''
startPos = position
else
startPos = guide.offsetToPosition(state, start - 1)
end
local hasSpace = triggerCharacter ~= nil and finish ~= offset
if guide.isInString(state.ast, position) then
if not hasSpace then
if #results == 0 then
checkCommon(state, word, position, results)
end
end
else
local parent, oop = findParent(state, startPos)
if parent then
checkField(state, word, startPos, position, parent, oop, results)
elseif isFuncArg(state, position) then
checkProvideLocal(state, word, startPos, results)
checkFunctionArgByDocParam(state, word, startPos, results)
else
local afterLocal = isAfterLocal(state, startPos)
local stop = checkKeyWord(state, startPos, position, word, hasSpace, afterLocal, results)
if stop then
return
end
if not hasSpace then
if afterLocal then
checkProvideLocal(state, word, startPos, results)
else
checkLocal(state, word, startPos, results)
checkTableField(state, word, startPos, results)
local env = guide.getENV(state.ast, startPos)
checkGlobal(state, word, startPos, position, env, false, results)
checkModule(state, word, startPos, results)
end
end
end
if not hasSpace and (#results == 0 or word ~= '') then
checkCommon(state, word, position, results)
end
end
end
---@async
local function trySymbol(state, position, results)
local text = state.lua
local symbol, start = lookBackward.findSymbol(text, guide.positionToOffset(state, position))
if not symbol then
return nil
end
if guide.isInString(state.ast, position) then
return nil
end
local startPos = guide.offsetToPosition(state, start)
--if symbol == '.'
--or symbol == ':' then
-- local parent, oop = findParent(state, startPos)
-- if parent then
-- tracy.ZoneBeginN 'completion.trySymbol'
-- checkField(state, '', startPos, position, parent, oop, results)
-- tracy.ZoneEnd()
-- end
--end
if symbol == '(' then
checkFunctionArgByDocParam(state, '', startPos, results)
end
end
local function findCall(state, position)
local call
guide.eachSourceContain(state.ast, position, function (src)
if src.type == 'call' then
if not call or call.start < src.start then
call = src
end
end
end)
return call
end
local function getCallArgInfo(call, position)
if not call.args then
return 1, nil
end
for index, arg in ipairs(call.args) do
if arg.start <= position and arg.finish >= position then
return index, arg
end
end
return #call.args + 1, nil
end
local function checkTableLiteralField(state, position, tbl, fields, results)
local text = state.lua
local mark = {}
for _, field in ipairs(tbl) do
if field.type == 'tablefield'
or field.type == 'tableindex'
or field.type == 'tableexp' then
local name = guide.getKeyName(field)
if name then
mark[name] = true
end
end
end
table.sort(fields, function (a, b)
return tostring(guide.getKeyName(a)) < tostring(guide.getKeyName(b))
end)
-- {$}
local left = lookBackward.findWord(text, guide.positionToOffset(state, position))
if not left then
local pos = lookBackward.findAnyOffset(text, guide.positionToOffset(state, position))
local char = text:sub(pos, pos)
if char == '{' or char == ',' or char == ';' then
left = ''
end
end
if left then
local hasResult = false
for _, field in ipairs(fields) do
local name = guide.getKeyName(field)
if name
and not mark[name]
and matchKey(left, tostring(name)) then
hasResult = true
results[#results+1] = {
label = guide.getKeyName(field),
kind = define.CompletionItemKind.Property,
id = stack(field, function (newField) ---@async
return {
detail = buildDetail(newField),
description = buildDesc(newField),
}
end),
}
end
end
return hasResult
end
end
local function tryCallArg(state, position, results)
local call = findCall(state, position)
if not call then
return
end
local argIndex, arg = getCallArgInfo(call, position)
if arg and arg.type == 'function' then
return
end
local node = vm.compileCallArg({ type = 'dummyarg' }, call, argIndex)
if not node then
return
end
local enums = {}
for src in node:eachObject() do
insertEnum(state, position, src, enums, arg and arg.type == 'table')
end
cleanEnums(enums, arg)
for _, enum in ipairs(enums) do
results[#results+1] = enum
end
end
local function tryTable(state, position, results)
local source = findNearestTableField(state, position)
if not source then
return false
end
if source.type ~= 'table'
and (not source.parent or source.parent.type ~= 'table') then
return
end
local mark = {}
local fields = {}
local tbl = source
if source.type ~= 'table' then
tbl = source.parent
end
local defs = vm.getFields(tbl)
for _, field in ipairs(defs) do
local name = guide.getKeyName(field)
if name and not mark[name] then
mark[name] = true
fields[#fields+1] = field
end
end
if checkTableLiteralField(state, position, tbl, fields, results) then
return true
end
return false
end
local function tryArray(state, position, results)
local source = findNearestSource(state, position)
if not source then
return
end
if source.type ~= 'table' and (not source.parent or source.parent.type ~= 'table') then
return
end
local tbl = source
if source.type ~= 'table' then
tbl = source.parent
end
if source.parent.type == 'callargs' and source.parent.parent.type == 'call' then
return
end
-- { } inside when enum
checkEqualEnumLeft(state, position, tbl, results, true)
end
local function getComment(state, position)
local offset = guide.positionToOffset(state, position)
local symbolOffset = lookBackward.findAnyOffset(state.lua, offset, true)
if not symbolOffset then
return
end
local symbolPosition = guide.offsetToPosition(state, symbolOffset)
for _, comm in ipairs(state.comms) do
if symbolPosition > comm.start and symbolPosition <= comm.finish then
return comm
end
end
return nil
end
local function getLuaDoc(state, position)
local offset = guide.positionToOffset(state, position)
local symbolOffset = lookBackward.findAnyOffset(state.lua, offset, true)
if not symbolOffset then
return
end
local symbolPosition = guide.offsetToPosition(state, symbolOffset)
for _, doc in ipairs(state.ast.docs) do
if symbolPosition >= doc.start and symbolPosition <= doc.range then
return doc
end
end
return nil
end
local function tryluaDocCate(word, results)
for _, docType in ipairs {
'class',
'type',
'alias',
'param',
'return',
'field',
'generic',
'vararg',
'overload',
'deprecated',
'meta',
'version',
'see',
'diagnostic',
'module',
'async',
'nodiscard',
'cast',
'operator',
'source',
'enum',
'package',
'private',
'protected'
} do
if matchKey(word, docType) then
results[#results+1] = {
label = docType,
kind = define.CompletionItemKind.Event,
description = lang.script('LUADOC_DESC_' .. docType:upper())
}
end
end
end
local function getluaDocByContain(state, position)
local result
local range = math.huge
guide.eachSourceContain(state.ast.docs, position, function (src)
if not src.start then
return
end
if range >= position - src.start
and position <= src.finish then
range = position - src.start
result = src
end
end)
return result
end
local function getluaDocByErr(state, start, position)
local targetError
for _, err in ipairs(state.errs) do
if err.finish <= position
and err.start >= start then
if not state.lua:sub(err.finish + 1, position):find '%S' then
targetError = err
break
end
end
end
if not targetError then
return nil
end
local targetDoc
for i = #state.ast.docs, 1, -1 do
local doc = state.ast.docs[i]
if doc.finish <= targetError.start then
targetDoc = doc
break
end
end
return targetError, targetDoc
end
---@async
local function tryluaDocBySource(state, position, source, results)
if source.type == 'doc.extends.name' then
if source.parent.type == 'doc.class' then
local used = {}
for _, doc in ipairs(vm.getDocSets(state.uri)) do
local name = doc.type == 'doc.class' and doc.class[1]
if name
and name ~= source.parent.class[1]
and not used[name]
and matchKey(source[1], name) then
used[name] = true
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Class,
textEdit = name:find '[^%w_]' and {
start = source.start,
finish = position,
newText = name,
},
}
end
end
end
return true
elseif source.type == 'doc.type.name' then
local used = {}
for _, doc in ipairs(vm.getDocSets(state.uri)) do
local name = (doc.type == 'doc.class' and doc.class[1])
or (doc.type == 'doc.alias' and doc.alias[1])
or (doc.type == 'doc.enum' and doc.enum[1])
if name
and not used[name]
and matchKey(source[1], name) then
used[name] = true
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Class,
textEdit = name:find '[^%w_]' and {
start = source.start,
finish = position,
newText = name,
},
}
end
end
return true
elseif source.type == 'doc.param.name' then
local funcs = {}
guide.eachSourceBetween(state.ast, position, math.huge, function (src)
if src.type == 'function' and src.start > position then
funcs[#funcs+1] = src
end
end)
table.sort(funcs, function (a, b)
return a.start < b.start
end)
local func = funcs[1]
if not func or not func.args then
return
end
for _, arg in ipairs(func.args) do
if arg[1] and matchKey(source[1], arg[1]) then
results[#results+1] = {
label = arg[1],
kind = define.CompletionItemKind.Interface,
}
end
end
return true
elseif source.type == 'doc.diagnostic' then
for _, mode in ipairs(diagnosticModes) do
if matchKey(source.mode, mode) then
results[#results+1] = {
label = mode,
kind = define.CompletionItemKind.Enum,
textEdit = {
start = source.start,
finish = source.start + #source.mode - 1,
newText = mode,
},
}
end
end
return true
elseif source.type == 'doc.diagnostic.name' then
for name in util.sortPairs(define.DiagnosticDefaultSeverity) do
if matchKey(source[1], name) then
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Value,
textEdit = {
start = source.start,
finish = source.start + #source[1] - 1,
newText = name,
},
}
end
end
return true
elseif source.type == 'doc.module' then
collectRequireNames('require', state.uri, source.module or '', source, source.smark, position, results)
return true
elseif source.type == 'doc.cast.name' then
local locals = guide.getVisibleLocals(state.ast, position)
for name, loc in util.sortPairs(locals) do
if matchKey(source[1], name) then
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Variable,
id = stack(loc, function (newLoc) ---@async
return {
detail = buildDetail(newLoc),
description = buildDesc(newLoc),
}
end),
}
end
end
return true
elseif source.type == 'doc.operator.name' then
for _, name in ipairs(vm.UNARY_OP) do
if matchKey(source[1], name) then
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Operator,
description = ('```lua\n%s\n```'):format(vm.OP_UNARY_MAP[name]),
}
end
end
for _, name in ipairs(vm.BINARY_OP) do
if matchKey(source[1], name) then
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Operator,
description = ('```lua\n%s\n```'):format(vm.OP_BINARY_MAP[name]),
}
end
end
for _, name in ipairs(vm.OTHER_OP) do
if matchKey(source[1], name) then
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Operator,
description = ('```lua\n%s\n```'):format(vm.OP_OTHER_MAP[name]),
}
end
end
return true
elseif source.type == 'doc.see.name' then
local symbolds = wssymbol(source[1], state.uri)
table.sort(symbolds, function (a, b)
return a.name < b.name
end)
for _, symbol in ipairs(symbolds) do
results[#results+1] = {
label = symbol.name,
kind = symbol.ckind,
id = stack(symbol.source, function (newSource) ---@async
return {
detail = buildDetail(newSource),
description = buildDesc(newSource),
}
end),
textEdit = {
start = source.start,
finish = source.finish,
newText = symbol.name,
},
}
end
end
return false
end
---@async
local function tryluaDocByErr(state, position, err, docState, results)
if err.type == 'LUADOC_MISS_CLASS_EXTENDS_NAME' then
local used = {}
for _, doc in ipairs(vm.getDocSets(state.uri)) do
if doc.type == 'doc.class'
and not used[doc.class[1]]
and doc.class[1] ~= docState.class[1] then
used[doc.class[1]] = true
results[#results+1] = {
label = doc.class[1],
kind = define.CompletionItemKind.Class,
}
end
end
elseif err.type == 'LUADOC_MISS_TYPE_NAME' then
local used = {}
for _, doc in ipairs(vm.getDocSets(state.uri)) do
if doc.type == 'doc.class'
and not used[doc.class[1]] then
used[doc.class[1]] = true
results[#results+1] = {
label = doc.class[1],
kind = define.CompletionItemKind.Class,
}
end
if doc.type == 'doc.alias'
and not used[doc.alias[1]] then
used[doc.alias[1]] = true
results[#results+1] = {
label = doc.alias[1],
kind = define.CompletionItemKind.Class,
}
end
if doc.type == 'doc.enum'
and not used[doc.enum[1]] then
used[doc.enum[1]] = true
results[#results+1] = {
label = doc.enum[1],
kind = define.CompletionItemKind.Enum,
}
end
end
elseif err.type == 'LUADOC_MISS_PARAM_NAME' then
local funcs = {}
guide.eachSourceBetween(state.ast, position, math.huge, function (src)
if src.type == 'function' and src.start > position then
funcs[#funcs+1] = src
end
end)
table.sort(funcs, function (a, b)
return a.start < b.start
end)
local func = funcs[1]
if not func or not func.args then
return
end
local label = {}
local insertText = {}
for i, arg in ipairs(func.args) do
if arg[1] and arg.type ~= 'self' then
label[#label+1] = arg[1]
if #label == 1 then
insertText[#insertText+1] = ('%s ${%d:any}'):format(arg[1], #label)
else
insertText[#insertText+1] = ('---@param %s ${%d:any}'):format(arg[1], #label)
end
end
end
results[#results+1] = {
label = table.concat(label, ', '),
kind = define.CompletionItemKind.Snippet,
insertTextFormat = 2,
insertText = table.concat(insertText, '\n'),
}
for i, arg in ipairs(func.args) do
if arg[1] then
results[#results+1] = {
label = arg[1],
kind = define.CompletionItemKind.Interface,
}
end
end
elseif err.type == 'LUADOC_MISS_DIAG_MODE' then
for _, mode in ipairs(diagnosticModes) do
results[#results+1] = {
label = mode,
kind = define.CompletionItemKind.Enum,
}
end
elseif err.type == 'LUADOC_MISS_DIAG_NAME' then
for name in util.sortPairs(diag.getDiagAndErrNameMap()) do
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Value,
}
end
elseif err.type == 'LUADOC_MISS_MODULE_NAME' then
collectRequireNames('require', state.uri, '', docState, nil, position, results)
elseif err.type == 'LUADOC_MISS_LOCAL_NAME' then
local locals = guide.getVisibleLocals(state.ast, position)
for name, loc in util.sortPairs(locals) do
if name ~= '_ENV' then
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Variable,
id = stack(loc, function (newLoc) ---@async
return {
detail = buildDetail(newLoc),
description = buildDesc(newLoc),
}
end),
}
end
end
elseif err.type == 'LUADOC_MISS_OPERATOR_NAME' then
for _, name in ipairs(vm.UNARY_OP) do
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Operator,
description = ('```lua\n%s\n```'):format(vm.OP_UNARY_MAP[name]),
}
end
for _, name in ipairs(vm.BINARY_OP) do
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Operator,
description = ('```lua\n%s\n```'):format(vm.OP_BINARY_MAP[name]),
}
end
for _, name in ipairs(vm.OTHER_OP) do
results[#results+1] = {
label = name,
kind = define.CompletionItemKind.Operator,
description = ('```lua\n%s\n```'):format(vm.OP_OTHER_MAP[name]),
}
end
elseif err.type == 'LUADOC_MISS_SEE_NAME' then
local symbolds = wssymbol('', state.uri)
table.sort(symbolds, function (a, b)
return a.name < b.name
end)
for _, symbol in ipairs(symbolds) do
results[#results+1] = {
label = symbol.name,
kind = symbol.ckind,
id = stack(symbol.source, function (newSource) ---@async
return {
detail = buildDetail(newSource),
description = buildDesc(newSource),
}
end),
}
end
end
end
local function buildluaDocOfFunction(func)
local index = 1
local buf = {}
buf[#buf+1] = '${1:comment}'
local args = {}
local returns = {}
if func.args then
for _, arg in ipairs(func.args) do
args[#args+1] = vm.getInfer(arg):view(guide.getUri(func))
end
end
if func.returns then
for _, rtns in ipairs(func.returns) do
for n = 1, #rtns do
if not returns[n] then
returns[n] = vm.getInfer(rtns[n]):view(guide.getUri(func))
end
end
end
end
for n, arg in ipairs(args) do
local funcArg = func.args[n]
if funcArg[1] and funcArg.type ~= 'self' then
index = index + 1
buf[#buf+1] = ('---@param %s ${%d:%s}'):format(
funcArg[1],
index,
arg
)
end
end
for _, rtn in ipairs(returns) do
index = index + 1
buf[#buf+1] = ('---@return ${%d:%s}'):format(
index,
rtn
)
end
local insertText = table.concat(buf, '\n')
return insertText
end
local function tryluaDocOfFunction(doc, results)
if not doc.bindSource then
return
end
local func = (doc.bindSource.type == 'function' and doc.bindSource)
or (doc.bindSource.value and doc.bindSource.value.type == 'function' and doc.bindSource.value)
or nil
if not func then
return
end
for _, otherDoc in ipairs(doc.bindGroup) do
if otherDoc.type == 'doc.return' then
return
end
end
if func.args then
for _, param in ipairs(func.args) do
if param.bindDocs then
return
end
end
end
local insertText = buildluaDocOfFunction(func)
results[#results+1] = {
label = '@param;@return',
kind = define.CompletionItemKind.Snippet,
insertTextFormat = 2,
filterText = '---',
insertText = insertText
}
end
---@async
local function tryLuaDoc(state, position, results)
local doc = getLuaDoc(state, position)
if not doc then
return
end
if doc.type == 'doc.comment' then
local line = doc.originalComment.text
-- 尝试 ---$
if line == '-' then
tryluaDocOfFunction(doc, results)
return
end
-- 尝试 ---@$
local cate = line:match('^-+%s*@(%a*)$')
if cate then
tryluaDocCate(cate, results)
return
end
end
-- 根据输入中的source来补全
local source = getluaDocByContain(state, position)
if source then
local suc = tryluaDocBySource(state, position, source, results)
if suc then
return
end
end
-- 根据附近的错误消息来补全
local err, expectDoc = getluaDocByErr(state, doc.start, position)
if err then
tryluaDocByErr(state, position, err, expectDoc, results)
return
end
end
local function tryComment(state, position, results)
if #results > 0 then
return
end
local word = lookBackward.findWord(state.lua, guide.positionToOffset(state, position))
local doc = getLuaDoc(state, position)
if not word then
local comment = getComment(state, position)
if not comment then
return
end
if comment.type == 'comment.short'
or comment.type == 'comment.cshort' then
if comment.text == '' then
results[#results+1] = {
label = '#region',
kind = define.CompletionItemKind.Snippet,
}
results[#results+1] = {
label = '#endregion',
kind = define.CompletionItemKind.Snippet,
}
end
end
return
end
if doc and doc.type ~= 'doc.comment' then
return
end
checkCommon(state, word, position, results)
end
---@async
local function tryCompletions(state, position, triggerCharacter, results)
if getComment(state, position) then
tryLuaDoc(state, position, results)
tryComment(state, position, results)
return
end
if postfix(state, position, results) then
return
end
if tryTable(state, position, results) then
return
end
trySpecial(state, position, results)
tryCallArg(state, position, results)
tryArray(state, position, results)
tryWord(state, position, triggerCharacter, results)
tryIndex(state, position, results)
trySymbol(state, position, results)
end
---@async
local function completion(uri, position, triggerCharacter)
local state = files.getLastState(uri) or files.getState(uri)
if not state then
return nil
end
clearStack()
diagnostic.pause()
local _ <close> = diagnostic.resume
local results = {}
tracy.ZoneBeginN 'completion #2'
tryCompletions(state, position, triggerCharacter, results)
tracy.ZoneEnd()
if #results == 0 then
return nil
end
return results
end
---@async
local function resolve(id)
local item = resolveStack(id)
return item
end
return {
completion = completion,
resolve = resolve,
}