nixos/lua-lsp/script/core/document-symbol.lua

412 lines
14 KiB
Lua

local await = require 'await'
local files = require 'files'
local guide = require 'parser.guide'
local define = require 'proto.define'
local util = require 'utility'
local subber = require 'core.substring'
---@param text string
---@return string
local function clipLastLine(text)
if text:find '[\r\n]' then
return '... ' .. util.trim(text:match '[^\r\n]*$')
else
return text
end
end
local function buildName(source, sub)
if source.type == 'setmethod'
or source.type == 'getmethod' then
if source.method then
return clipLastLine(sub(source.start + 1, source.method.finish))
end
end
if source.type == 'setfield'
or source.type == 'tablefield'
or source.type == 'getfield' then
if source.field then
return clipLastLine(sub(source.start + 1, source.field.finish))
end
end
if source.type == 'tableindex' then
if source.index then
return ('[%s]'):format(clipLastLine(sub(source.index.start + 1, source.index.finish)))
end
end
if source.type == 'tableexp' then
return ('[%d]'):format(source.tindex)
end
return clipLastLine(sub(source.start + 1, source.finish))
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 buildTable(tbl, sub)
local buf = {}
for i = 1, 5 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])
elseif field.type == 'tableindex'
and field.index then
buf[#buf+1] = ('[%s]'):format(sub(field.index.start + 1, field.index.finish))
elseif field.type == 'tableexp' then
buf[#buf+1] = ('[%s]'):format(field.tindex)
end
end
if #tbl > 5 then
buf[#buf+1] = ('...(+%d)'):format(#tbl - 5)
end
return table.concat(buf, ', ')
end
local function buildArray(tbl, sub)
local buf = {}
for i = 1, 5 do
local field = tbl[i]
if not field then
break
end
buf[#buf+1] = sub(field.start + 1, field.finish)
end
if #tbl > 5 then
buf[#buf+1] = ('...(+%d)'):format(#tbl - 5)
end
return table.concat(buf, ', ')
end
local function buildValue(source, sub, used, symbols)
local name = buildName(source, sub)
local range, sRange, valueRange, kind
local details = {}
if source.type == 'local' then
if source.parent.type == 'funcargs' then
kind = define.SymbolKind.Constant
else
kind = define.SymbolKind.Variable
end
range = { source.start, source.finish }
sRange = { source.start, source.finish }
elseif source.type == 'setlocal' then
range = { source.start, source.finish }
sRange = { source.start, source.finish }
elseif source.type == 'setglobal' then
range = { source.start, source.finish }
sRange = { source.start, source.finish }
elseif source.type == 'tablefield' then
if not source.field then
return
end
range = { source.field.start, source.field.finish }
sRange = { source.field.start, source.field.finish }
elseif source.type == 'tableindex' then
if not source.index then
return
end
range = { source.index.start, source.index.finish }
sRange = { source.index.start, source.index.finish }
elseif source.type == 'tableexp' then
range = { source.value.start, source.value.finish }
sRange = { source.value.start, source.value.finish }
elseif source.type == 'setfield' then
if not source.field then
return
end
range = { source.field.start, source.field.finish }
sRange = { source.field.start, source.field.finish }
elseif source.type == 'setmethod' then
if not source.method then
return
end
range = { source.method.start, source.method.finish }
sRange = { source.start, source.finish }
else
return
end
if source.value then
used[source.value] = true
local literal = source.value[1]
if source.value.type == 'boolean' then
kind = define.SymbolKind.Boolean
if literal ~= nil then
details[#details+1] = util.viewLiteral(source.value[1])
end
elseif source.value.type == 'string' then
kind = define.SymbolKind.String
if literal ~= nil then
details[#details+1] = util.viewLiteral(source.value[1])
end
elseif source.value.type == 'number'
or source.value.type == 'integer' then
kind = define.SymbolKind.Number
if literal ~= nil then
details[#details+1] = util.viewLiteral(source.value[1])
end
elseif source.value.type == 'table' then
kind = define.SymbolKind.Object
local lastField = source.value[#source.value]
if #source.value > 0 then
if lastField.type == 'tableexp'
and lastField.tindex == #source.value then
-- Array
kind = define.SymbolKind.Array
details[#details+1] = '['
details[#details+1] = buildArray(source.value, sub)
details[#details+1] = ']'
else
-- Object
details[#details+1] = '{'
details[#details+1] = buildTable(source.value, sub)
details[#details+1] = '}'
end
end
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
elseif source.value.type == 'function' then
details[#details+1] = ('function (%s)'):format(buildFunctionParams(source.value))
if source.type == 'setmethod' then
kind = define.SymbolKind.Method
else
kind = define.SymbolKind.Function
end
valueRange = { source.value.start, source.value.finish }
range[1] = math.min(source.value.start, source.start)
end
range = { range[1], source.value.finish }
end
symbols[#symbols+1] = {
name = name,
detail = table.concat(details),
kind = kind or define.SymbolKind.Variable,
range = range,
selectionRange = sRange,
valueRange = valueRange,
}
end
local function buildAnonymous(source, sub, used, symbols)
if used[source] then
return
end
used[source] = true
local head = ''
local detail = ''
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, sub)
detail = '-> '
end
if source.type == 'function' then
symbols[#symbols+1] = {
name = head,
detail = detail .. ('function (%s)'):format(buildFunctionParams(source)),
kind = define.SymbolKind.Function,
range = { source.start, source.finish },
selectionRange = { source.keyword[1], source.keyword[2] },
valueRange = { source.start, source.finish },
}
elseif source.type == 'table' then
local kind = define.SymbolKind.Object
local details = {}
local lastField = source[#source]
if lastField then
if lastField.type == 'tableexp'
and lastField.tindex == #source then
-- Array
kind = define.SymbolKind.Array
details[#details+1] = '['
details[#details+1] = buildArray(source, sub)
details[#details+1] = ']'
else
-- Object
details[#details+1] = '{'
details[#details+1] = buildTable(source, sub)
details[#details+1] = '}'
end
end
symbols[#symbols+1] = {
name = head,
detail = detail .. table.concat(details),
kind = kind,
range = { source.start, source.finish },
selectionRange = { source.start, source.finish },
valueRange = { source.start, source.finish },
}
end
end
local function buildBlock(source, sub, used, symbols)
if used[source] then
return
end
used[source] = true
if source.type == 'if' then
for _, block in ipairs(source) do
symbols[#symbols+1] = {
name = block.type:gsub('block$', ''),
detail = sub(block.start + 1, block.keyword[4] or block.keyword[2]),
kind = define.SymbolKind.Package,
range = { block.start, block.finish },
valueRange = { block.start, block.finish },
selectionRange = { block.keyword[1], block.keyword[2] },
}
end
elseif source.type == 'while' then
symbols[#symbols+1] = {
name = 'while',
detail = sub(source.start + 1, source.keyword[4] or source.keyword[2]),
kind = define.SymbolKind.Package,
range = { source.start, source.finish },
valueRange = { source.start, source.finish },
selectionRange = { source.keyword[1], source.keyword[2] },
}
elseif source.type == 'repeat' then
symbols[#symbols+1] = {
name = 'repeat',
detail = source.filter and sub(source.keyword[3] + 1, source.filter.finish) or '',
kind = define.SymbolKind.Package,
range = { source.start, source.finish },
valueRange = { source.start, source.finish },
selectionRange = { source.keyword[1], source.keyword[2] },
}
elseif source.type == 'loop'
or source.type == 'in' then
symbols[#symbols+1] = {
name = 'for',
detail = sub(source.start, source.keyword[4] or source.keyword[2]),
kind = define.SymbolKind.Package,
range = { source.start, source.finish },
valueRange = { source.start, source.finish },
selectionRange = { source.keyword[1], source.keyword[2] },
}
end
end
local function buildSource(source, sub, 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'
or source.type == 'tableexp'
or source.type == 'tableindex' then
buildValue(source, sub, used, symbols)
elseif source.type == 'function'
or source.type == 'table' then
buildAnonymous(source, sub, used, symbols)
elseif source.type == 'if'
or source.type == 'while'
or source.type == 'in'
or source.type == 'loop'
or source.type == 'repeat' then
buildBlock(source, sub, used, symbols)
end
end
---@async
local function makeSymbol(uri)
local state = files.getState(uri)
if not state then
return nil
end
local sub = subber(state)
local symbols = {}
local used = {}
local i = 0
---@async
guide.eachSource(state.ast, function (source)
buildSource(source, sub, used, symbols)
i = i + 1
if i % 1000 == 0 then
await.delay()
end
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