diff options
Diffstat (limited to 'script/core/hover')
-rw-r--r-- | script/core/hover/arg.lua | 71 | ||||
-rw-r--r-- | script/core/hover/description.lua | 204 | ||||
-rw-r--r-- | script/core/hover/init.lua | 164 | ||||
-rw-r--r-- | script/core/hover/label.lua | 211 | ||||
-rw-r--r-- | script/core/hover/name.lua | 101 | ||||
-rw-r--r-- | script/core/hover/return.lua | 125 | ||||
-rw-r--r-- | script/core/hover/table.lua | 257 |
7 files changed, 1133 insertions, 0 deletions
diff --git a/script/core/hover/arg.lua b/script/core/hover/arg.lua new file mode 100644 index 00000000..9cd19f02 --- /dev/null +++ b/script/core/hover/arg.lua @@ -0,0 +1,71 @@ +local guide = require 'parser.guide' +local vm = require 'vm' + +local function optionalArg(arg) + if not arg.bindDocs then + return false + end + local name = arg[1] + for _, doc in ipairs(arg.bindDocs) do + if doc.type == 'doc.param' and doc.param[1] == name then + return doc.optional + end + end +end + +local function asFunction(source, oop) + if not source.args then + return '' + end + local args = {} + for i = 1, #source.args do + local arg = source.args[i] + local name = arg.name or guide.getName(arg) + if name then + args[i] = ('%s%s: %s'):format( + name, + optionalArg(arg) and '?' or '', + vm.getInferType(arg) + ) + else + args[i] = ('%s'):format(vm.getInferType(arg)) + end + end + local methodDef + local parent = source.parent + if parent and parent.type == 'setmethod' then + methodDef = true + end + if not methodDef and oop then + return table.concat(args, ', ', 2) + else + return table.concat(args, ', ') + end +end + +local function asDocFunction(source) + if not source.args then + return '' + end + local args = {} + for i = 1, #source.args do + local arg = source.args[i] + local name = arg.name[1] + args[i] = ('%s%s: %s'):format( + name, + arg.optional and '?' or '', + vm.getInferType(arg.extends) + ) + end + return table.concat(args, ', ') +end + +return function (source, oop) + if source.type == 'function' then + return asFunction(source, oop) + end + if source.type == 'doc.type.function' then + return asDocFunction(source) + end + return '' +end diff --git a/script/core/hover/description.lua b/script/core/hover/description.lua new file mode 100644 index 00000000..7d89ee6c --- /dev/null +++ b/script/core/hover/description.lua @@ -0,0 +1,204 @@ +local vm = require 'vm' +local ws = require 'workspace' +local furi = require 'file-uri' +local files = require 'files' +local guide = require 'parser.guide' +local markdown = require 'provider.markdown' +local config = require 'config' +local lang = require 'language' + +local function asStringInRequire(source, literal) + local rootPath = ws.path or '' + local parent = source.parent + if parent and parent.type == 'callargs' then + local result, searchers + local call = parent.parent + local func = call.node + local libName = vm.getLibraryName(func) + if not libName then + return + end + if libName == 'require' then + result, searchers = ws.findUrisByRequirePath(literal) + elseif libName == 'dofile' + or libName == 'loadfile' then + result = ws.findUrisByFilePath(literal) + end + if result and #result > 0 then + for i, uri in ipairs(result) do + local searcher = searchers and furi.decode(searchers[uri]) + uri = files.getOriginUri(uri) + local path = furi.decode(uri) + if files.eq(path:sub(1, #rootPath), rootPath) then + path = path:sub(#rootPath + 1) + end + path = path:gsub('^[/\\]*', '') + if vm.isMetaFile(uri) then + result[i] = ('* [[meta]](%s)'):format(uri) + elseif searcher then + searcher = searcher:sub(#rootPath + 1) + searcher = ws.normalize(searcher) + result[i] = ('* [%s](%s) %s'):format(path, uri, lang.script('HOVER_USE_LUA_PATH', searcher)) + else + result[i] = ('* [%s](%s)'):format(path, uri) + end + end + table.sort(result) + local md = markdown() + md:add('md', table.concat(result, '\n')) + return md:string() + end + end +end + +local function asStringView(source, literal) + -- 内部包含转义符? + local rawLen = source.finish - source.start - 2 * #source[2] + 1 + if config.config.hover.viewString + and (source[2] == '"' or source[2] == "'") + and rawLen > #literal then + local view = literal + local max = config.config.hover.viewStringMax + if #view > max then + view = view:sub(1, max) .. '...' + end + local md = markdown() + md:add('txt', view) + return md:string() + end +end + +local function asString(source) + local literal = guide.getLiteral(source) + if type(literal) ~= 'string' then + return nil + end + return asStringInRequire(source, literal) + or asStringView(source, literal) +end + +local function getBindComment(docGroup, base) + local lines = {} + for _, doc in ipairs(docGroup) do + if doc.type == 'doc.comment' then + lines[#lines+1] = doc.comment.text:sub(2) + elseif #lines > 0 and not base then + break + elseif doc == base then + break + else + lines = {} + end + end + if #lines == 0 then + return nil + end + return table.concat(lines, '\n') +end + +local function buildEnumChunk(docType, name) + local enums = vm.getDocEnums(docType) + if #enums == 0 then + return + end + local types = {} + for _, tp in ipairs(docType.types) do + types[#types+1] = tp[1] + end + local lines = {} + lines[#lines+1] = ('%s: %s'):format(name, table.concat(types)) + for _, enum in ipairs(enums) do + lines[#lines+1] = (' %s %s%s'):format( + (enum.default and '->') + or (enum.additional and '+>') + or ' |', + enum[1], + enum.comment and (' -- %s'):format(enum.comment) or '' + ) + end + return table.concat(lines, '\n') +end + +local function getBindEnums(docGroup) + local mark = {} + local chunks = {} + local returnIndex = 0 + for _, doc in ipairs(docGroup) do + if doc.type == 'doc.param' then + local name = doc.param[1] + if mark[name] then + goto CONTINUE + end + mark[name] = true + chunks[#chunks+1] = buildEnumChunk(doc.extends, name) + elseif doc.type == 'doc.return' then + for _, rtn in ipairs(doc.returns) do + returnIndex = returnIndex + 1 + local name = rtn.name and rtn.name[1] or ('(return %d)'):format(returnIndex) + if mark[name] then + goto CONTINUE + end + mark[name] = true + chunks[#chunks+1] = buildEnumChunk(rtn, name) + end + end + ::CONTINUE:: + end + if #chunks == 0 then + return nil + end + return table.concat(chunks, '\n\n') +end + +local function tryDocFieldUpComment(source) + if source.type ~= 'doc.field' then + return + end + if not source.bindGroup then + return + end + local comment = getBindComment(source.bindGroup, source) + return comment +end + +local function tryDocComment(source) + if not source.bindDocs then + return + end + local comment = getBindComment(source.bindDocs) + local enums = getBindEnums(source.bindDocs) + local md = markdown() + if comment then + md:add('md', comment) + end + if enums then + md:add('lua', enums) + end + return md:string() +end + +local function tryDocOverloadToComment(source) + if source.type ~= 'doc.type.function' then + return + end + local doc = source.parent + if doc.type ~= 'doc.overload' + or not doc.bindSources then + return + end + for _, src in ipairs(doc.bindSources) do + local md = tryDocComment(src) + if md then + return md + end + end +end + +return function (source) + if source.type == 'string' then + return asString(source) + end + return tryDocOverloadToComment(source) + or tryDocFieldUpComment(source) + or tryDocComment(source) +end diff --git a/script/core/hover/init.lua b/script/core/hover/init.lua new file mode 100644 index 00000000..96e01ab5 --- /dev/null +++ b/script/core/hover/init.lua @@ -0,0 +1,164 @@ +local files = require 'files' +local guide = require 'parser.guide' +local vm = require 'vm' +local getLabel = require 'core.hover.label' +local getDesc = require 'core.hover.description' +local util = require 'utility' +local findSource = require 'core.find-source' +local lang = require 'language' + +local function eachFunctionAndOverload(value, callback) + callback(value) + if not value.bindDocs then + return + end + for _, doc in ipairs(value.bindDocs) do + if doc.type == 'doc.overload' then + callback(doc.overload) + end + end +end + +local function getHoverAsFunction(source) + local values = vm.getDefs(source, 'deep') + local desc = getDesc(source) + local labels = {} + local defs = 0 + local protos = 0 + local other = 0 + local oop = source.type == 'method' + or source.type == 'getmethod' + or source.type == 'setmethod' + local mark = {} + for _, def in ipairs(values) do + def = guide.getObjectValue(def) or def + if def.type == 'function' + or def.type == 'doc.type.function' then + eachFunctionAndOverload(def, function (value) + if mark[value] then + return + end + mark[value] =true + local label = getLabel(value, oop) + if label then + defs = defs + 1 + labels[label] = (labels[label] or 0) + 1 + if labels[label] == 1 then + protos = protos + 1 + end + end + desc = desc or getDesc(value) + end) + elseif def.type == 'table' + or def.type == 'boolean' + or def.type == 'string' + or def.type == 'number' then + other = other + 1 + desc = desc or getDesc(def) + end + end + + if defs == 1 and other == 0 then + return { + label = next(labels), + source = source, + description = desc, + } + end + + local lines = {} + if defs > 1 then + lines[#lines+1] = lang.script('HOVER_MULTI_DEF_PROTO', defs, protos) + end + if other > 0 then + lines[#lines+1] = lang.script('HOVER_MULTI_PROTO_NOT_FUNC', other) + end + if defs > 1 then + for label, count in util.sortPairs(labels) do + lines[#lines+1] = ('(%d) %s'):format(count, label) + end + else + lines[#lines+1] = next(labels) + end + local label = table.concat(lines, '\n') + return { + label = label, + source = source, + description = desc, + } +end + +local function getHoverAsValue(source) + local oop = source.type == 'method' + or source.type == 'getmethod' + or source.type == 'setmethod' + local label = getLabel(source, oop) + local desc = getDesc(source) + if not desc then + local values = vm.getDefs(source, 'deep') + for _, def in ipairs(values) do + desc = getDesc(def) + if desc then + break + end + end + end + return { + label = label, + source = source, + description = desc, + } +end + +local function getHoverAsDocName(source) + local label = getLabel(source) + local desc = getDesc(source) + return { + label = label, + source = source, + description = desc, + } +end + +local function getHover(source) + if source.type == 'doc.type.name' then + return getHoverAsDocName(source) + end + local isFunction = vm.hasInferType(source, 'function', 'deep') + if isFunction then + return getHoverAsFunction(source) + else + return getHoverAsValue(source) + end +end + +local accept = { + ['local'] = true, + ['setlocal'] = true, + ['getlocal'] = true, + ['setglobal'] = true, + ['getglobal'] = true, + ['field'] = true, + ['method'] = true, + ['string'] = true, + ['number'] = true, + ['doc.type.name'] = true, +} + +local function getHoverByUri(uri, offset) + local ast = files.getAst(uri) + if not ast then + return nil + end + local source = findSource(ast, offset, accept) + if not source then + return nil + end + local hover = getHover(source) + return hover +end + +return { + get = getHover, + byUri = getHoverByUri, +} diff --git a/script/core/hover/label.lua b/script/core/hover/label.lua new file mode 100644 index 00000000..d785bc27 --- /dev/null +++ b/script/core/hover/label.lua @@ -0,0 +1,211 @@ +local buildName = require 'core.hover.name' +local buildArg = require 'core.hover.arg' +local buildReturn = require 'core.hover.return' +local buildTable = require 'core.hover.table' +local vm = require 'vm' +local util = require 'utility' +local guide = require 'parser.guide' +local lang = require 'language' +local config = require 'config' +local files = require 'files' + +local function asFunction(source, oop) + local name = buildName(source, oop) + local arg = buildArg(source, oop) + local rtn = buildReturn(source) + local lines = {} + lines[1] = ('function %s(%s)'):format(name, arg) + lines[2] = rtn + return table.concat(lines, '\n') +end + +local function asDocFunction(source) + local name = buildName(source) + local arg = buildArg(source) + local rtn = buildReturn(source) + local lines = {} + lines[1] = ('function %s(%s)'):format(name, arg) + lines[2] = rtn + return table.concat(lines, '\n') +end + +local function asDocTypeName(source) + for _, doc in ipairs(vm.getDocTypes(source[1])) do + if doc.type == 'doc.class.name' then + return 'class ' .. source[1] + end + if doc.type == 'doc.alias.name' then + local extends = doc.parent.extends + return lang.script('HOVER_EXTENDS', vm.getInferType(extends)) + end + end +end + +local function asValue(source, title) + local name = buildName(source) + local infers = vm.getInfers(source, 'deep') + local type = vm.getInferType(source, 'deep') + local class = vm.getClass(source, 'deep') + local literal = vm.getInferLiteral(source, 'deep') + local cont + if type ~= 'string' and not type:find('%[%]$') then + if #vm.getFields(source, 'deep') > 0 + or vm.hasInferType(source, 'table', 'deep') then + cont = buildTable(source) + end + end + local pack = {} + pack[#pack+1] = title + pack[#pack+1] = name .. ':' + if cont and type == 'table' then + type = nil + end + if class then + pack[#pack+1] = class + else + pack[#pack+1] = type + end + if literal then + pack[#pack+1] = '=' + pack[#pack+1] = literal + end + if cont then + pack[#pack+1] = cont + end + return table.concat(pack, ' ') +end + +local function asLocal(source) + return asValue(source, 'local') +end + +local function asGlobal(source) + return asValue(source, 'global') +end + +local function isGlobalField(source) + if source.type == 'field' + or source.type == 'method' then + source = source.parent + end + if source.type == 'setfield' + or source.type == 'getfield' + or source.type == 'setmethod' + or source.type == 'getmethod' then + local node = source.node + if node.type == 'setglobal' + or node.type == 'getglobal' then + return true + end + return isGlobalField(node) + elseif source.type == 'tablefield' then + local parent = source.parent + if parent.type == 'setglobal' + or parent.type == 'getglobal' then + return true + end + return isGlobalField(parent) + else + return false + end +end + +local function asField(source) + if isGlobalField(source) then + return asGlobal(source) + end + return asValue(source, 'field') +end + +local function asDocField(source) + local name = source.field[1] + local class + for _, doc in ipairs(source.bindGroup) do + if doc.type == 'doc.class' then + class = doc + break + end + end + if not class then + return ('field ?.%s: %s'):format( + name, + vm.getInferType(source.extends) + ) + end + return ('field %s.%s: %s'):format( + class.class[1], + name, + vm.getInferType(source.extends) + ) +end + +local function asString(source) + local str = source[1] + if type(str) ~= 'string' then + return '' + end + local len = #str + local charLen = util.utf8Len(str, 1, -1) + if len == charLen then + return lang.script('HOVER_STRING_BYTES', len) + else + return lang.script('HOVER_STRING_CHARACTERS', len, charLen) + end +end + +local function formatNumber(n) + local str = ('%.10f'):format(n) + str = str:gsub('%.?0*$', '') + return str +end + +local function asNumber(source) + if not config.config.hover.viewNumber then + return nil + end + local num = source[1] + if type(num) ~= 'number' then + return nil + end + local uri = guide.getUri(source) + local text = files.getText(uri) + if not text then + return nil + end + local raw = text:sub(source.start, source.finish) + if not raw or not raw:find '[^%-%d%.]' then + return nil + end + return formatNumber(num) +end + +return function (source, oop) + if source.type == 'function' then + return asFunction(source, oop) + elseif source.type == 'local' + or source.type == 'getlocal' + or source.type == 'setlocal' then + return asLocal(source) + elseif source.type == 'setglobal' + or source.type == 'getglobal' then + return asGlobal(source) + elseif source.type == 'getfield' + or source.type == 'setfield' + or source.type == 'getmethod' + or source.type == 'setmethod' + or source.type == 'tablefield' + or source.type == 'field' + or source.type == 'method' then + return asField(source) + elseif source.type == 'string' then + return asString(source) + elseif source.type == 'number' then + return asNumber(source) + elseif source.type == 'doc.type.function' then + return asDocFunction(source) + elseif source.type == 'doc.type.name' then + return asDocTypeName(source) + elseif source.type == 'doc.field' then + return asDocField(source) + end +end diff --git a/script/core/hover/name.lua b/script/core/hover/name.lua new file mode 100644 index 00000000..9ad32e09 --- /dev/null +++ b/script/core/hover/name.lua @@ -0,0 +1,101 @@ +local guide = require 'parser.guide' +local vm = require 'vm' + +local buildName + +local function asLocal(source) + local name = guide.getName(source) + if not source.attrs then + return name + end + local label = {} + label[#label+1] = name + for _, attr in ipairs(source.attrs) do + label[#label+1] = ('<%s>'):format(attr[1]) + end + return table.concat(label, ' ') +end + +local function asField(source, oop) + local class + if source.node.type ~= 'getglobal' then + class = vm.getClass(source.node, 'deep') + end + local node = class or guide.getName(source.node) or '?' + local method = guide.getName(source) + if oop then + return ('%s:%s'):format(node, method) + else + return ('%s.%s'):format(node, method) + end +end + +local function asTableField(source) + if not source.field then + return + end + return guide.getName(source.field) +end + +local function asGlobal(source) + return guide.getName(source) +end + +local function asDocFunction(source) + local doc = guide.getParentType(source, 'doc.type') + or guide.getParentType(source, 'doc.overload') + if not doc or not doc.bindSources then + return '' + end + for _, src in ipairs(doc.bindSources) do + local name = buildName(src) + if name ~= '' then + return name + end + end + return '' +end + +local function asDocField(source) + return source.field[1] +end + +function buildName(source, oop) + if oop == nil then + oop = source.type == 'setmethod' + or source.type == 'getmethod' + end + if source.type == 'local' + or source.type == 'getlocal' + or source.type == 'setlocal' then + return asLocal(source) or '' + end + if source.type == 'setglobal' + or source.type == 'getglobal' then + return asGlobal(source) or '' + end + if source.type == 'setmethod' + or source.type == 'getmethod' then + return asField(source, true) or '' + end + if source.type == 'setfield' + or source.type == 'getfield' then + return asField(source, oop) or '' + end + if source.type == 'tablefield' then + return asTableField(source) or '' + end + if source.type == 'doc.type.function' then + return asDocFunction(source) + end + if source.type == 'doc.field' then + return asDocField(source) + end + local parent = source.parent + if parent then + return buildName(parent, oop) + end + return '' +end + +return buildName diff --git a/script/core/hover/return.lua b/script/core/hover/return.lua new file mode 100644 index 00000000..3829dbed --- /dev/null +++ b/script/core/hover/return.lua @@ -0,0 +1,125 @@ +local guide = require 'parser.guide' +local vm = require 'vm' + +local function mergeTypes(returns) + if type(returns) == 'string' then + return returns + end + return guide.mergeTypes(returns) +end + +local function getReturnDualByDoc(source) + local docs = source.bindDocs + if not docs then + return + end + local dual + for _, doc in ipairs(docs) do + if doc.type == 'doc.return' then + for _, rtn in ipairs(doc.returns) do + if not dual then + dual = {} + end + dual[#dual+1] = { rtn } + end + end + end + return dual +end + +local function getReturnDualByGrammar(source) + if not source.returns then + return nil + end + local dual + for _, rtn in ipairs(source.returns) do + if not dual then + dual = {} + end + for n = 1, #rtn do + if not dual[n] then + dual[n] = {} + end + dual[n][#dual[n]+1] = rtn[n] + end + end + return dual +end + +local function asFunction(source) + local dual = getReturnDualByDoc(source) + or getReturnDualByGrammar(source) + if not dual then + return + end + local returns = {} + for i, rtn in ipairs(dual) do + local line = {} + local types = {} + if i == 1 then + line[#line+1] = ' -> ' + else + line[#line+1] = ('% 3d. '):format(i) + end + for n = 1, #rtn do + local values = vm.getInfers(rtn[n]) + for _, value in ipairs(values) do + if value.type then + for tp in value.type:gmatch '[^|]+' do + types[#types+1] = tp + end + end + end + end + if #types > 0 or rtn[1] then + local tp = mergeTypes(types) or 'any' + if rtn[1].name then + line[#line+1] = ('%s%s: %s'):format( + rtn[1].name[1], + rtn[1].optional and '?' or '', + tp + ) + else + line[#line+1] = ('%s%s'):format( + tp, + rtn[1].optional and '?' or '' + ) + end + else + break + end + returns[i] = table.concat(line) + end + if #returns == 0 then + return nil + end + return table.concat(returns, '\n') +end + +local function asDocFunction(source) + if not source.returns or #source.returns == 0 then + return nil + end + local returns = {} + for i, rtn in ipairs(source.returns) do + local rtnText = ('%s%s'):format( + vm.getInferType(rtn), + rtn.optional and '?' or '' + ) + if i == 1 then + returns[#returns+1] = (' -> %s'):format(rtnText) + else + returns[#returns+1] = ('% 3d. %s'):format(i, rtnText) + end + end + return table.concat(returns, '\n') +end + +return function (source) + if source.type == 'function' then + return asFunction(source) + end + if source.type == 'doc.type.function' then + return asDocFunction(source) + end +end diff --git a/script/core/hover/table.lua b/script/core/hover/table.lua new file mode 100644 index 00000000..02be5271 --- /dev/null +++ b/script/core/hover/table.lua @@ -0,0 +1,257 @@ +local vm = require 'vm' +local util = require 'utility' +local guide = require 'parser.guide' +local config = require 'config' +local lang = require 'language' + +local function getKey(src) + local key = vm.getKeyName(src) + if not key or #key <= 2 then + if not src.index then + return '[any]' + end + local class = vm.getClass(src.index) + if class then + return ('[%s]'):format(class) + end + local tp = vm.getInferType(src.index) + if tp then + return ('[%s]'):format(tp) + end + return '[any]' + end + local ktype = key:sub(1, 2) + key = key:sub(3) + if ktype == 's|' then + if key:match '^[%a_][%w_]*$' then + return key + else + return ('[%s]'):format(util.viewLiteral(key)) + end + end + return ('[%s]'):format(key) +end + +local function getFieldFast(src) + local value = guide.getObjectValue(src) or src + if not value then + return 'any' + end + if value.type == 'boolean' then + return value.type, util.viewLiteral(value[1]) + end + if value.type == 'number' + or value.type == 'integer' then + if math.tointeger(value[1]) then + if config.config.runtime.version == 'Lua 5.3' + or config.config.runtime.version == 'Lua 5.4' then + return 'integer', util.viewLiteral(value[1]) + end + end + return value.type, util.viewLiteral(value[1]) + end + if value.type == 'table' + or value.type == 'function' then + return value.type + end + if value.type == 'string' then + local literal = value[1] + if type(literal) == 'string' and #literal >= 50 then + literal = literal:sub(1, 47) .. '...' + end + return value.type, util.viewLiteral(literal) + end +end + +local function getFieldFull(src) + local tp = vm.getInferType(src) + --local class = vm.getClass(src) + local literal = vm.getInferLiteral(src) + if type(literal) == 'string' and #literal >= 50 then + literal = literal:sub(1, 47) .. '...' + end + return tp, literal +end + +local function getField(src, timeUp, mark, key) + if src.type == 'table' + or src.type == 'function' then + return nil + end + if src.parent then + if src.type == 'string' + or src.type == 'boolean' + or src.type == 'number' + or src.type == 'integer' then + if src.parent.type == 'tableindex' + or src.parent.type == 'setindex' + or src.parent.type == 'getindex' then + if src.parent.index == src then + src = src.parent + end + end + end + end + local tp, literal + tp, literal = getFieldFast(src) + if tp then + return tp, literal + end + if timeUp or mark[key] then + return nil + end + mark[key] = true + tp, literal = getFieldFull(src) + if tp then + return tp, literal + end + return nil +end + +local function buildAsHash(classes, literals) + local keys = {} + for k in pairs(classes) do + keys[#keys+1] = k + end + table.sort(keys) + local lines = {} + lines[#lines+1] = '{' + for _, key in ipairs(keys) do + local class = classes[key] + local literal = literals[key] + if literal then + lines[#lines+1] = (' %s: %s = %s,'):format(key, class, literal) + else + lines[#lines+1] = (' %s: %s,'):format(key, class) + end + end + lines[#lines+1] = '}' + return table.concat(lines, '\n') +end + +local function buildAsConst(classes, literals) + local keys = {} + for k in pairs(classes) do + keys[#keys+1] = k + end + table.sort(keys, function (a, b) + return tonumber(literals[a]) < tonumber(literals[b]) + end) + local lines = {} + lines[#lines+1] = '{' + for _, key in ipairs(keys) do + local class = classes[key] + local literal = literals[key] + if literal then + lines[#lines+1] = (' %s: %s = %s,'):format(key, class, literal) + else + lines[#lines+1] = (' %s: %s,'):format(key, class) + end + end + lines[#lines+1] = '}' + return table.concat(lines, '\n') +end + +local function mergeLiteral(literals) + local results = {} + local mark = {} + for _, value in ipairs(literals) do + if not mark[value] then + mark[value] = true + results[#results+1] = value + end + end + if #results == 0 then + return nil + end + table.sort(results) + return table.concat(results, '|') +end + +local function mergeTypes(types) + local results = {} + local mark = { + -- 讲道理table的keyvalue不会是nil + ['nil'] = true, + } + for _, tv in ipairs(types) do + for tp in tv:gmatch '[^|]+' do + if not mark[tp] then + mark[tp] = true + results[#results+1] = tp + end + end + end + return guide.mergeTypes(results) +end + +local function clearClasses(classes) + classes['[nil]'] = nil + classes['[any]'] = nil + classes['[string]'] = nil +end + +return function (source) + local literals = {} + local classes = {} + local clock = os.clock() + local timeUp + local mark = {} + local fields = vm.getFields(source, 'deep') + local keyCount = 0 + for _, src in ipairs(fields) do + local key = getKey(src) + if not key then + goto CONTINUE + end + if not classes[key] then + classes[key] = {} + keyCount = keyCount + 1 + end + if not literals[key] then + literals[key] = {} + end + if not TEST and os.clock() - clock > config.config.hover.fieldInfer / 1000.0 then + timeUp = true + end + local class, literal = getField(src, timeUp, mark, key) + if literal == 'nil' then + literal = nil + end + classes[key][#classes[key]+1] = class + literals[key][#literals[key]+1] = literal + if keyCount >= 1000 then + break + end + ::CONTINUE:: + end + + clearClasses(classes) + + for key, class in pairs(classes) do + literals[key] = mergeLiteral(literals[key]) + classes[key] = mergeTypes(class) + end + + if not next(classes) then + return '{}' + end + + local intValue = true + for key, class in pairs(classes) do + if class ~= 'integer' or not tonumber(literals[key]) then + intValue = false + break + end + end + local result + if intValue then + result = buildAsConst(classes, literals) + else + result = buildAsHash(classes, literals) + end + if timeUp then + result = ('\n--%s\n%s'):format(lang.script.HOVER_TABLE_TIME_UP, result) + end + return result +end |