I’ve made a recursive search so it first searches for the closest node to the cursor position, then it saves the path and goes through it and collects variables
After that it processes every variable, global and outputs their types.
Though it’s still not complete. I’ve spent 2 days trying to figure everything out, and I didn’t write the code for member access yet, but it kinda works
local module = {}
-- stuff in replicated storage
local SHARED = game.ReplicatedStorage.Shared
local MLuauParser = require(SHARED.Modules.LuauParser)
local GEnvironmentTypes = require(SHARED.Global.EnvironmentTypes)
local GScriptGlobals = require(SHARED.Global.ScriptGlobals)
local function calculateSimilarity(text1:string, text2:string)
text1 = text1:lower()
text2 = text2:lower()
local len1 = #text1
local len2 = #text2
local matrix = {}
for i = 0, len1 do
matrix[i] = {[0] = i}
end
for j = 0, len2 do
matrix[0][j] = j
end
for i = 1, len1 do
for j = 1, len2 do
local cost = text1:sub(i, i) == text2:sub(j, j) and 0 or 1
matrix[i][j] = math.min(
matrix[i-1][j] + 1,
matrix[i][j-1] + 1,
matrix[i-1][j-1] + cost
)
end
end
local maxLength = math.max(len1, len2)
if maxLength == 0 then return 1 end
local distance = matrix[len1][len2]
return 1 - distance / maxLength
end
local function checkLocation(line:number, column:number, loc:{})
if line < loc.begin.line or line > loc.end_.line then return false end
if line == loc.begin.line and column < loc.begin.column then return false end
if line == loc.end_.line and column > loc.end_.column then return false end
return true
end
local function findDeepestNode(node:{[string]:any}, line:number, column:number, path:{{[string]:any}})
if not node or not node.location then return nil end
if not checkLocation(line, column, node.location) then
return nil
end
local deeperNode:{[string]:any}? = nil
table.insert(path, node)
if node.kind == "StatBlock" and node.body then
for _, stmt in pairs(node.body) do
deeperNode = findDeepestNode(stmt, line, column, path)
if deeperNode then break end
end
elseif node.kind == "StatLocal" then
if node.values then
for _, expr:{} in pairs(node.values) do
deeperNode = findDeepestNode(expr, line, column, path)
if deeperNode then break end
end
end
elseif node.kind == "ExprCall" then
deeperNode = findDeepestNode(node.func, line, column, path)
if not deeperNode and node.args then
for _, arg in pairs(node.args) do
deeperNode = findDeepestNode(arg, line, column, path)
if deeperNode then break end
end
end
elseif node.kind == "ExprIndexName" then
deeperNode = findDeepestNode(node.expr, line, column, path)
elseif node.kind == "StatExpr" then
deeperNode = findDeepestNode(node.expr, line, column, path)
elseif node.kind == "StatLocalFunction" then
deeperNode = findDeepestNode(node.func, line, column, path)
elseif node.kind == "ExprFunction" then
deeperNode = findDeepestNode(node.body, line, column, path)
elseif node.kind == "ExprIfElse" then
if node.condition then
deeperNode = findDeepestNode(node.condition, line, column, path)
end
if node.trueExpr and not deeperNode then
deeperNode = findDeepestNode(node.trueExpr, line, column, path)
end
if node.falseExpr and not deeperNode then
deeperNode = findDeepestNode(node.falseExpr, line, column, path)
end
elseif node.kind == "StatIf" then
if node.condition then
deeperNode = findDeepestNode(node.condition, line, column, path)
end
if node.thenbody and not deeperNode then
deeperNode = findDeepestNode(node.thenbody, line, column, path)
end
if node.elsebody and not deeperNode then
deeperNode = findDeepestNode(node.elsebody, line, column, path)
end
elseif node.kind == "ExprTable" then
for _, v in pairs(node.items) do
deeperNode = findDeepestNode(v, line, column, path)
if deeperNode then break end
end
elseif node.kind == "List" or node.kind == "Record" or node.kind == "General" then
if node.key then
deeperNode = findDeepestNode(node.key, line, column, path)
end
if node.value and not deeperNode then
deeperNode = findDeepestNode(node.value, line, column, path)
end
elseif node.kind == "StatWhile" then
if node.condition then
deeperNode = findDeepestNode(node.condition, line, column, path)
end
if node.body and not deeperNode then
deeperNode = findDeepestNode(node.body, line, column, path)
end
elseif node.kind == "ExprIndexExpr" then
if node.expr then
deeperNode = findDeepestNode(node.expr, line, column, path)
end
if node.index and not deeperNode then
deeperNode = findDeepestNode(node.index, line, column, path)
end
elseif node.kind == "StatReturn" then
for _, v in pairs(node.list) do
deeperNode = findDeepestNode(v, line, column, path)
if deeperNode then break end
end
elseif node.kind == "ExprBinary" then
if node.left then
deeperNode = findDeepestNode(node.left, line, column, path)
end
if node.right and not deeperNode then
deeperNode = findDeepestNode(node.right, line, column, path)
end
elseif node.kind == "ExprUnary" then
deeperNode = findDeepestNode(node.expr, line, column, path)
end
return if deeperNode then deeperNode else node
end
local function getAccessContext(node:{[string]:any}, code:string)
if not node then return nil end
if node.kind == "ExprError" or node.kind == "StatError" then
node = node.expressions[1] or {}
end
if node.kind == "ExprIndexName" then
local op = string.char(node.op)
local line = code:split("\n")[node.opPosition.line+1]
local start = node.opPosition.column+2
return {
type = "Member",
node = node.expr,
original = node,
isMethod = op == ':',
name = line:sub(start, (line:find("[%p%s]", start) or #line+1)-1),
}
elseif node.kind == "ExprGlobal" then
local line = code:split("\n")[node.location.begin.line+1]
local start = node.location.begin.column+1
return {
type = "Global",
node = node,
name = line:sub(start, (line:find("[%p%s]", start) or #line+1)-1)
}
elseif node.kind == "ExprLocal" then
local line = code:split("\n")[node.location.begin.line+1]
local start = node.location.begin.column+1
return {
type = "Local",
node = node,
name = line:sub(start, (line:find("[%p%s]", start) or #line+1)-1)
}
end
end
local function getFuncType(func:{[string]:any})
local argsStr = ""
local returnStr = ""
if func.returnAnnotation then
for i, v in pairs(func.returnAnnotation.types) do
if v.kind == "TypeReference" then
returnStr ..= v.name
else
returnStr ..= "?"
end
if i ~= #func.returnAnnotation.types then
returnStr ..= ", "
end
end
end
for i, v in pairs(func.args) do
argsStr ..= v.name
if v.annotation and v.annotation.kind == "TypeReference" then
argsStr ..= ": "..v.annotation.name
end
if i ~= #func.args then
argsStr ..= ", "
end
end
return "("..argsStr.."): "..returnStr
end
local function getPossibleNamesNodes(node:{[string]:any}, name:string, path:{{[string]:any}}, envTypes:{[string]:string})
local result:{{name:string, node:{[string]:any}, similarity:number}} = {}
--[[for i, v in pairs(GScriptGlobals.All) do
table.insert(result, {name = i, node = {kind = "GLOBAL", name = i}})
end]]
for i, v in pairs(envTypes) do
table.insert(result, {name = i, node = {kind = "GLOBAL", name = i}})
end
--[[for _, v in pairs(GScriptGlobals.Keywords) do
table.insert(result, {name = v, node = {kind = "KEYWORD", name = v}, type = "KEYWORD"})
end]]
local function checkNode(curNode:{[string]:any})
if curNode == node then return end
if curNode.kind == "StatBlock" then
for _, v in pairs(curNode.body) do
checkNode(v)
end
elseif curNode.kind == "StatLocal" then
for _, v in pairs(curNode.vars) do
v.kind = "VAR"
checkNode(v)
end
elseif curNode.kind == "ExprFunction" then
for _, v in pairs(curNode.args) do
v.kind = "VAR"
checkNode(v)
end
elseif curNode.kind == "VAR" then
table.insert(result, {name = curNode.name, node = curNode})
elseif curNode.kind == "StatLocalFunction" then
curNode.name.kind = "VAR"
table.insert(result, {name = curNode.name.name, node = curNode.name, type = getFuncType(curNode.func)})
end
end
for i=#path-1, 1, -1 do
local curNode = path[i]
checkNode(curNode)
end
for _, v in pairs(result) do
v.similarity = calculateSimilarity(v.name, name)
end
table.sort(result, function (v1, v2)
return v1.similarity > v2.similarity
end)
print(node, result)
return result
end
-- table types: detailed descriptions of types that are tables
-- env types: types of all provided globals (gets from server)
local function getPossibleType(node:{[string]:any}, envTypes:{[string]:string}, tableTypes:{[string]:{}})
if node.kind == "ExprLocal" then
if node["local"].annotation and node["local"].annotation.kind == "TypeReference" then
local name = node["local"].annotation.name
return tableTypes[name] or name
else
return "?"
end
elseif node.kind == "ExprGlobal" then
if envTypes[node.name] then
return tableTypes[envTypes[node.name]] or envTypes[node.name]
else
return "?"
end
elseif node.kind == "VAR" then
if node.annotation and node.annotation.kind == "TypeReference" then
local name = node.annotation.name
return tableTypes[name] or name
else
return "?"
end
elseif node.kind == "GLOBAL" then
if envTypes[node.name] then
return tableTypes[envTypes[node.name]] or envTypes[node.name]
else
return "?"
end
end
end
function module.getHints(code:string, cursorLine:number, cursorColumn:number, envTypes:{[string]:any})
local success, result = MLuauParser.parse(code)
local path:{{[string]:any}} = {}
local cursorNode = if result.root then findDeepestNode(result.root, cursorLine, cursorColumn, path) else nil
if cursorNode then
local context = getAccessContext(cursorNode, code)
if context then
local result:{{name:string, type:string|{[string]:string}}} = {}
for _, v in pairs(getPossibleNamesNodes(context.node, context.name, path, envTypes)) do
table.insert(result, {name = v.name, type = v.type or getPossibleType(v.node, envTypes, GEnvironmentTypes.All)})
end
return result
end
end
end
return module