summaryrefslogtreecommitdiff
path: root/script/core/hover
diff options
context:
space:
mode:
Diffstat (limited to 'script/core/hover')
-rw-r--r--script/core/hover/arg.lua71
-rw-r--r--script/core/hover/description.lua204
-rw-r--r--script/core/hover/init.lua164
-rw-r--r--script/core/hover/label.lua211
-rw-r--r--script/core/hover/name.lua101
-rw-r--r--script/core/hover/return.lua125
-rw-r--r--script/core/hover/table.lua257
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