summaryrefslogtreecommitdiff
path: root/server/src/core/hover.lua
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/core/hover.lua')
-rw-r--r--server/src/core/hover.lua480
1 files changed, 480 insertions, 0 deletions
diff --git a/server/src/core/hover.lua b/server/src/core/hover.lua
new file mode 100644
index 00000000..4d64647d
--- /dev/null
+++ b/server/src/core/hover.lua
@@ -0,0 +1,480 @@
+local findLib = require 'core.find_lib'
+
+local OriginTypes = {
+ ['any'] = true,
+ ['nil'] = true,
+ ['integer'] = true,
+ ['number'] = true,
+ ['boolean'] = true,
+ ['string'] = true,
+ ['thread'] = true,
+ ['userdata'] = true,
+ ['table'] = true,
+ ['function'] = true,
+}
+
+local function buildLibArgs(lib, oo, select)
+ if not lib.args then
+ return ''
+ end
+ local start
+ if oo then
+ start = 2
+ if select then
+ select = select + 1
+ end
+ else
+ start = 1
+ end
+ local strs = {}
+ local argLabel
+ for i = start, #lib.args do
+ local arg = lib.args[i]
+ if arg.optional then
+ if i > start then
+ strs[#strs+1] = ' ['
+ else
+ strs[#strs+1] = '['
+ end
+ end
+ if i > start then
+ strs[#strs+1] = ', '
+ end
+
+ local argStr = {}
+ if arg.name then
+ argStr[#argStr+1] = ('%s: '):format(arg.name)
+ end
+ if type(arg.type) == 'table' then
+ argStr[#argStr+1] = table.concat(arg.type, '/')
+ else
+ argStr[#argStr+1] = arg.type or 'any'
+ end
+ if arg.default then
+ argStr[#argStr+1] = ('(%q)'):format(arg.default)
+ end
+
+ for _, str in ipairs(argStr) do
+ strs[#strs+1] = str
+ end
+ if arg.optional == 'self' then
+ strs[#strs+1] = ']'
+ end
+ if i == select then
+ argLabel = table.concat(argStr)
+ end
+ end
+ for _, arg in ipairs(lib.args) do
+ if arg.optional == 'after' then
+ strs[#strs+1] = ']'
+ end
+ end
+ return table.concat(strs), argLabel
+end
+
+local function buildLibReturns(lib)
+ if not lib.returns then
+ return ''
+ end
+ local strs = {}
+ for i, rtn in ipairs(lib.returns) do
+ if rtn.optional then
+ if i > 1 then
+ strs[#strs+1] = ' ['
+ else
+ strs[#strs+1] = '['
+ end
+ end
+ if i > 1 then
+ strs[#strs+1] = ', '
+ end
+ if rtn.name then
+ strs[#strs+1] = ('%s: '):format(rtn.name)
+ end
+ if type(rtn.type) == 'table' then
+ strs[#strs+1] = table.concat(rtn.type, '/')
+ else
+ strs[#strs+1] = rtn.type or 'any'
+ end
+ if rtn.default then
+ strs[#strs+1] = ('(%q)'):format(rtn.default)
+ end
+ if rtn.optional == 'self' then
+ strs[#strs+1] = ']'
+ end
+ end
+ for _, rtn in ipairs(lib.returns) do
+ if rtn.optional == 'after' then
+ strs[#strs+1] = ']'
+ end
+ end
+ return '\n -> ' .. table.concat(strs)
+end
+
+local function buildEnum(lib)
+ if not lib.enums then
+ return ''
+ end
+ local container = table.container()
+ for _, enum in ipairs(lib.enums) do
+ if not enum.name or (not enum.enum and not enum.code) then
+ goto NEXT_ENUM
+ end
+ if not container[enum.name] then
+ container[enum.name] = {}
+ if lib.args then
+ for _, arg in ipairs(lib.args) do
+ if arg.name == enum.name then
+ container[enum.name].type = arg.type
+ break
+ end
+ end
+ end
+ if lib.returns then
+ for _, rtn in ipairs(lib.returns) do
+ if rtn.name == enum.name then
+ container[enum.name].type = rtn.type
+ break
+ end
+ end
+ end
+ end
+ table.insert(container[enum.name], enum)
+ ::NEXT_ENUM::
+ end
+ local strs = {}
+ for name, enums in pairs(container) do
+ local tp
+ if type(enums.type) == 'table' then
+ tp = table.concat(enums.type, '/')
+ else
+ tp = enums.type
+ end
+ strs[#strs+1] = ('\n%s: %s'):format(name, tp or 'any')
+ for _, enum in ipairs(enums) do
+ if enum.default then
+ strs[#strs+1] = '\n -> '
+ else
+ strs[#strs+1] = '\n | '
+ end
+ if enum.code then
+ strs[#strs+1] = tostring(enum.code)
+ else
+ strs[#strs+1] = ('%q'):format(enum.enum)
+ end
+ if enum.description then
+ strs[#strs+1] = ' -- ' .. enum.description
+ end
+ end
+ end
+ return table.concat(strs)
+end
+
+local function buildValueName(result, source)
+ local func = result.value
+ local declarat = func.declarat or source
+ if declarat then
+ local key
+ if declarat.type == 'name' then
+ key = declarat[1]
+ elseif declarat.type == 'string' then
+ key = ('%q'):format(declarat[1])
+ elseif declarat.type == 'number' or declarat.type == 'boolean' then
+ key = tostring(declarat[1])
+ elseif func.type == 'function' then
+ key = ''
+ elseif type(result.key) == 'string' then
+ key = result.key
+ else
+ key = ''
+ end
+
+ local parentName = declarat.parentName
+
+ if not parentName then
+ return key or ''
+ end
+
+ if parentName == '?' then
+ local parentType = result.parentValue and result.parentValue.type
+ if parentType == 'table' then
+ else
+ parentName = '*' .. parentType
+ end
+ end
+ if source.object then
+ return parentName .. ':' .. key
+ else
+ if parentName then
+ if declarat.index then
+ return parentName .. '[' .. key .. ']'
+ else
+ return parentName .. '.' .. key
+ end
+ else
+ return key
+ end
+ end
+ end
+ return result.key or ''
+end
+
+local function buildValueArgs(func, source, select)
+ local names = {}
+ local values = {}
+ if func.args then
+ for i, arg in ipairs(func.args) do
+ names[i] = arg.key
+ end
+ end
+ if func.argValues then
+ for i, value in ipairs(func.argValues) do
+ values[i] = value.type
+ end
+ end
+ local strs = {}
+ local argLabel
+ local start = 1
+ if source.object then
+ start = 2
+ if select then
+ select = select + 1
+ end
+ end
+ local max
+ if func.built then
+ max = #names
+ else
+ max = math.max(#names, #values)
+ end
+ for i = start, max do
+ local name = names[i]
+ local value = values[i] or 'any'
+ if name then
+ strs[#strs+1] = name .. ': ' .. value
+ else
+ strs[#strs+1] = value
+ end
+ if i == select then
+ argLabel = strs[#strs]
+ end
+ end
+ if func.hasDots then
+ strs[#strs+1] = '...'
+ end
+ return table.concat(strs, ', '), argLabel
+end
+
+local function buildValueReturns(func)
+ if not func.hasReturn then
+ return ''
+ end
+ local strs = {}
+ if func.returns then
+ for i, rtn in ipairs(func.returns) do
+ strs[i] = rtn.type
+ end
+ end
+ if #strs == 0 then
+ strs[1] = 'any'
+ end
+ return '\n -> ' .. table.concat(strs, ', ')
+end
+
+local function getFunctionHover(name, func, source, lib, oo, select)
+ local args = ''
+ local returns
+ local enum
+ local tip
+ local argLabel
+ if lib then
+ args, argLabel = buildLibArgs(lib, oo, select)
+ returns = buildLibReturns(lib)
+ enum = buildEnum(lib)
+ tip = lib.description
+ else
+ args, argLabel = buildValueArgs(func, source, select)
+ returns = buildValueReturns(func)
+ end
+ local title = ('function %s(%s)%s'):format(name, args, returns)
+ return {
+ label = title,
+ description = tip,
+ enum = enum,
+ argLabel = argLabel,
+ }
+end
+
+local function findClass(result)
+ -- 根据部分字段尝试找出自定义类型
+ local metatable = result.value.metatable
+ if not metatable or not metatable.child then
+ return nil
+ end
+ -- 查找meta表的 __name 字段
+ local name = metatable.child['__name']
+ -- 值必须是字符串
+ if name and name.value and type(name.value.value) == 'string' then
+ return name.value.value
+ end
+ -- 查找meta表 __index 里的字段
+ local index = metatable.child['__index']
+ if index and index.value and index.value.child then
+ for key, field in pairs(index.value.child) do
+ -- 键值类型必须均为字符串
+ if type(key) ~= 'string' then
+ goto CONTINUE
+ end
+ if not field.value or type(field.value.value) ~= 'string' then
+ goto CONTINUE
+ end
+ local lKey = key:lower()
+ if lKey == 'type' or lKey == 'name' or lKey == 'class' then
+ -- 必须只有过一次赋值
+ local hasSet = false
+ for _, info in ipairs(field) do
+ if info.type == 'set' then
+ if hasSet then
+ goto CONTINUE
+ end
+ hasSet = true
+ end
+ end
+ return field.value.value
+ end
+ ::CONTINUE::
+ end
+ end
+ return nil
+end
+
+local function unpackTable(result)
+ local child = result.value and result.value.child
+ if not child then
+ return '{}'
+ end
+ local lines = {'{'}
+ for key, field in pairs(child) do
+ local kType = type(key)
+ if kType == 'table' then
+ key = ('[*%s]'):format(key.type)
+ else
+ if kType ~= 'string' then
+ key = ('[%s]'):format(key)
+ end
+ end
+
+ local value = field.value
+ if not value then
+ lines[#lines+1] = (' %s: %s,'):format(key, 'any')
+ goto CONTINUE
+ end
+
+ local vType = type(value.value)
+ if vType == 'boolean' or vType == 'integer' or vType == 'number' or vType == 'string' then
+ lines[#lines+1] = (' %s: %s = %q,'):format(key, value.type, value.value)
+ else
+ lines[#lines+1] = (' %s: %s,'):format(key, value.type)
+ end
+ ::CONTINUE::
+ end
+ lines[#lines+1] = '}'
+ return table.concat(lines, '\r\n')
+end
+
+local function getValueHover(name, valueType, result, source, lib)
+ if not lib then
+ local class = findClass(result)
+ if class then
+ valueType = class
+ end
+ end
+
+ if not OriginTypes[valueType] then
+ valueType = '*' .. valueType
+ end
+
+ local value
+ local tip
+ if lib then
+ value = lib.code or (lib.value and ('%q'):format(lib.value))
+ tip = lib.description
+ else
+ value = result.value.value and ('%q'):format(result.value.value)
+ end
+
+ local tp = result.type
+ if tp == 'field' then
+ if result.parent and result.parent.value and result.parent.value.ENV then
+ tp = 'global'
+ end
+ end
+
+ local text
+ if valueType == 'table' then
+ text = ('%s %s: %s'):format(tp, name, unpackTable(result))
+ else
+ if value == nil then
+ text = ('%s %s: %s'):format(tp, name, valueType)
+ else
+ text = ('%s %s: %s = %s'):format(tp, name, valueType, value)
+ end
+ end
+ return {
+ label = text,
+ description = tip,
+ }
+end
+
+local function getStringHover(result, lsp)
+ if not result.uri then
+ return nil
+ end
+ if not lsp or not lsp.workspace then
+ return nil
+ end
+ local path = lsp.workspace:relativePathByUri(result.uri)
+ return {
+ description = ('[%s](%s)'):format(path:string(), result.uri),
+ }
+end
+
+return function (result, source, lsp, select)
+ if not result.value then
+ return
+ end
+
+ if result.key == '' then
+ return
+ end
+
+ if result.type == 'string' then
+ return getStringHover(result, lsp)
+ end
+
+ if result.type ~= 'local' and result.type ~= 'field' then
+ return
+ end
+
+ local lib, fullKey, oo = findLib(result)
+ local valueType = lib and lib.type
+ if valueType then
+ if type(valueType) == 'table' then
+ valueType = valueType[1]
+ end
+ else
+ valueType = result.value.type or 'nil'
+ end
+ local name = fullKey or buildValueName(result, source)
+ local hover
+ if valueType == 'function' then
+ hover = getFunctionHover(name, result.value, source, lib, oo, select)
+ else
+ hover = getValueHover(name, valueType, result, source, lib)
+ end
+ if not hover then
+ return
+ end
+ hover.name = name
+ return hover
+end