local await = require 'await' local files = require 'files' local guide = require 'parser.guide' local define = require 'proto.define' local util = require 'utility' local function buildName(source, text) local uri = guide.getUri(source) local state = files.getState(uri) local startOffset = guide.positionToOffset(state, source.start) if source.type == 'setmethod' or source.type == 'getmethod' then if source.method then local finishOffset = guide.positionToOffset(state, source.method.finish) return text:sub(startOffset + 1, finishOffset) end end if source.type == 'setfield' or source.type == 'tablefield' or source.type == 'getfield' then if source.field then local finishOffset = guide.positionToOffset(state, source.field.finish) return text:sub(startOffset + 1, finishOffset) end end local finishOffset = guide.positionToOffset(state, source.finish) return text:sub(startOffset + 1, finishOffset) end local function buildFunctionParams(func) if not func.args then return '' end local params = {} for _, arg in ipairs(func.args) do if arg.type == 'self' then goto CONTINUE end if arg.type == '...' then params[#params+1] = '...' else params[#params+1] = arg[1] or '' end ::CONTINUE:: end return table.concat(params, ', ') end local function buildFunction(source, text, symbols) local name = buildName(source, text) local func = source.value if source.type == 'tablefield' or source.type == 'setfield' then source = source.field if not source then return end end local range, kind if func.start > source.finish then -- a = function() range = { source.start, func.finish } else -- function f() range = { func.start, func.finish } end if source.type == 'setmethod' then kind = define.SymbolKind.Method else kind = define.SymbolKind.Function end symbols[#symbols+1] = { name = name, detail = ('function (%s)'):format(buildFunctionParams(func)), kind = kind, range = range, selectionRange = { source.start, source.finish }, valueRange = { func.start, func.finish }, } end local function buildTable(tbl) local buf = {} for i = 1, 3 do local field = tbl[i] if not field then break end if field.type == 'tablefield' and field.field then buf[#buf+1] = ('%s'):format(field.field[1]) end end return table.concat(buf, ', ') end local function buildValue(source, text, symbols) local name = buildName(source, text) local range, sRange, valueRange, kind local details = {} if source.type == 'local' then if source.parent.type == 'funcargs' then details[1] = 'param' range = { source.start, source.finish } sRange = { source.start, source.finish } kind = define.SymbolKind.Constant else details[1] = 'local' range = { source.start, source.finish } sRange = { source.start, source.finish } kind = define.SymbolKind.Variable end elseif source.type == 'setlocal' then details[1] = 'setlocal' range = { source.start, source.finish } sRange = { source.start, source.finish } kind = define.SymbolKind.Variable elseif source.type == 'setglobal' then details[1] = 'global' range = { source.start, source.finish } sRange = { source.start, source.finish } kind = define.SymbolKind.Class elseif source.type == 'tablefield' then if not source.field then return end details[1] = 'field' range = { source.field.start, source.field.finish } sRange = { source.field.start, source.field.finish } kind = define.SymbolKind.Property elseif source.type == 'setfield' then if not source.field then return end details[1] = 'field' range = { source.field.start, source.field.finish } sRange = { source.field.start, source.field.finish } kind = define.SymbolKind.Field else return end if source.value then local literal = source.value[1] if source.value.type == 'boolean' then details[2] = ' boolean' if literal ~= nil then details[3] = ' = ' details[4] = util.viewLiteral(source.value[1]) end elseif source.value.type == 'string' then details[2] = ' string' if literal ~= nil then details[3] = ' = ' details[4] = util.viewLiteral(source.value[1]) end elseif source.value.type == 'number' or source.value.type == 'integer' then details[2] = ' number' if literal ~= nil then details[3] = ' = ' details[4] = util.viewLiteral(source.value[1]) end elseif source.value.type == 'table' then details[2] = ' {' details[3] = buildTable(source.value) details[4] = '}' valueRange = { source.value.start, source.value.finish } elseif source.value.type == 'select' then if source.value.vararg and source.value.vararg.type == 'call' then valueRange = { source.value.start, source.value.finish } end end range = { range[1], source.value.finish } end symbols[#symbols+1] = { name = name, detail = table.concat(details), kind = kind, range = range, selectionRange = sRange, valueRange = valueRange, } end local function buildSet(source, text, used, symbols) local value = source.value if value and value.type == 'function' then used[value] = true buildFunction(source, text, symbols) else buildValue(source, text, symbols) end end local function buildAnonymousFunction(source, text, used, symbols) if used[source] then return end used[source] = true local head = '' local parent = source.parent if parent.type == 'return' then head = 'return ' elseif parent.type == 'callargs' then local call = parent.parent local node = call.node head = buildName(node, text) .. ' -> ' end symbols[#symbols+1] = { name = '', detail = ('%sfunction (%s)'):format(head, buildFunctionParams(source)), kind = define.SymbolKind.Function, range = { source.start, source.finish }, selectionRange = { source.keyword[1], source.keyword[2] }, valueRange = { source.start, source.finish }, } end ---@async local function buildSource(source, text, used, symbols) if source.type == 'local' or source.type == 'setlocal' or source.type == 'setglobal' or source.type == 'setfield' or source.type == 'setmethod' or source.type == 'tablefield' then await.delay() buildSet(source, text, used, symbols) elseif source.type == 'function' then await.delay() buildAnonymousFunction(source, text, used, symbols) end end ---@async local function makeSymbol(uri) local ast = files.getState(uri) local text = files.getText(uri) if not ast or not text then return nil end local symbols = {} local used = {} guide.eachSource(ast.ast, function (source) ---@async buildSource(source, text, used, symbols) end) return symbols end local function packChild(symbols) local index = 1 local function insertChilds(min, max) local list while true do local symbol = symbols[index] if not symbol then break end if symbol.selectionRange[1] < min or symbol.selectionRange[2] > max then break end if not list then list = {} end list[#list+1] = symbol index = index + 1 if symbol.valueRange then symbol.children = insertChilds(symbol.valueRange[1], symbol.valueRange[2]) end end return list end local root = insertChilds(0, math.maxinteger) return root end ---@async local function packSymbols(symbols) await.delay() table.sort(symbols, function (a, b) local o1 = a.valueRange and a.valueRange[1] or a.selectionRange[1] local o2 = b.valueRange and b.valueRange[1] or b.selectionRange[1] return o1 < o2 end) await.delay() -- 处理嵌套 return packChild(symbols) end ---@async return function (uri) local symbols = makeSymbol(uri) if not symbols then return nil end local packedSymbols = packSymbols(symbols) return packedSymbols end