nixos/lua-lsp/script/core/hint.lua

338 lines
9.2 KiB
Lua

local files = require 'files'
local vm = require 'vm'
local config = require 'config'
local guide = require 'parser.guide'
local await = require 'await'
local define = require 'proto.define'
local lang = require 'language'
local substr = require 'core.substring'
---@async
local function typeHint(uri, results, start, finish)
local state = files.getState(uri)
if not state then
return
end
local mark = {}
guide.eachSourceBetween(state.ast, start, finish, function (source) ---@async
if source.type ~= 'local'
and source.type ~= 'setglobal'
and source.type ~= 'tablefield'
and source.type ~= 'tableindex'
and source.type ~= 'setfield'
and source.type ~= 'setindex' then
return
end
if source[1] == '_' then
return
end
if source.value and guide.isLiteral(source.value) then
return
end
if source.parent.type == 'funcargs' then
if not config.get(uri, 'Lua.hint.paramType') then
return
end
else
if not config.get(uri, 'Lua.hint.setType') then
return
end
end
await.delay()
local view = vm.getInfer(source):view(uri)
if view == 'any'
or view == 'unknown'
or view == 'nil' then
return
end
local src = source
if source.type == 'tablefield' then
src = source.field
elseif source.type == 'tableindex' then
src = source.index
end
if not src then
return
end
if mark[src] then
return
end
mark[src] = true
results[#results+1] = {
text = ':' .. view,
offset = src.finish,
kind = define.InlayHintKind.Type,
where = 'right',
source = source,
}
end)
end
local function getParams(func)
if not func.args or #func.args == 0 then
return nil
end
local params = {}
for _, arg in ipairs(func.args) do
if arg.type == '...' then
break
end
params[#params+1] = arg
end
if #params == 0 then
return nil
end
return params
end
local function hasLiteralArgInCall(call)
if not call.args then
return false
end
for _, arg in ipairs(call.args) do
if guide.isLiteral(arg) then
return true
end
end
return false
end
---@async
local function paramName(uri, results, start, finish)
local paramConfig = config.get(uri, 'Lua.hint.paramName')
if not paramConfig or paramConfig == 'Disable' then
return
end
local state = files.getState(uri)
if not state then
return
end
local mark = {}
guide.eachSourceBetween(state.ast, start, finish, function (source) ---@async
if source.type ~= 'call' then
return
end
if paramConfig == 'Literal' and not hasLiteralArgInCall(source) then
return
end
if not source.args then
return
end
await.delay()
local defs = vm.getDefs(source.node)
if not defs then
return
end
local params
for _, def in ipairs(defs) do
if def.type == 'function' then
params = getParams(def)
if params then
break
end
end
end
if not params then
return
end
local firstIndex = 1
if source.node and source.node.type == 'getmethod' then
firstIndex = 2
end
for i = firstIndex, #source.args do
local arg = source.args[i]
if not mark[arg]
and (paramConfig ~= 'Literal' or guide.isLiteral(arg)) then
mark[arg] = true
local param = params[i]
if param and param[1] then
results[#results+1] = {
text = param[1] .. ':',
offset = arg.start,
kind = define.InlayHintKind.Parameter,
where = 'left',
source = param,
}
end
end
end
end)
end
---@async
local function arrayIndex(uri, results, start, finish)
local state = files.getState(uri)
if not state then
return
end
local option = config.get(uri, 'Lua.hint.arrayIndex')
if option == 'Disable' then
return
end
local mixedOrLargeTable = {}
local function isMixedOrLargeTable(tbl)
if mixedOrLargeTable[tbl] ~= nil then
return mixedOrLargeTable[tbl]
end
if #tbl > 3 then
mixedOrLargeTable[tbl] = true
return true
end
for _, child in ipairs(tbl) do
if child.type ~= 'tableexp' then
mixedOrLargeTable[tbl] = true
return true
end
end
mixedOrLargeTable[tbl] = false
return false
end
---@async
guide.eachSourceType(state.ast, 'table', function (source)
if source.finish < start or source.start > finish then
return
end
await.delay()
if option == 'Auto' then
if not isMixedOrLargeTable(source) then
return
end
end
local list = {}
local max = 0
for _, field in ipairs(source) do
if field.type == 'tableexp'
and field.start < finish
and field.finish > start then
list[#list+1] = field
if field.tindex > max then
max = field.tindex
end
end
end
if #list > 0 then
local length = #tostring(max)
local fmt = '[%0' .. length .. 'd]'
for _, field in ipairs(list) do
results[#results+1] = {
text = fmt:format(field.tindex),
offset = field.start,
kind = define.InlayHintKind.Other,
where = 'left',
source = field.parent,
}
end
end
end)
end
---@async
local function awaitHint(uri, results, start, finish)
local awaitConfig = config.get(uri, 'Lua.hint.await')
if not awaitConfig then
return
end
local state = files.getState(uri)
if not state then
return
end
guide.eachSourceBetween(state.ast, start, finish, function (source) ---@async
if source.type ~= 'call' then
return
end
await.delay()
local node = source.node
if not vm.isAsyncCall(source) then
return
end
results[#results+1] = {
text = 'await ',
offset = node.start,
kind = define.InlayHintKind.Other,
where = 'left',
tooltip = lang.script.HOVER_AWAIT_TOOLTIP,
}
end)
end
local blockTypes = {
'main',
'function',
'for',
'loop',
'in',
'do',
'repeat',
'while',
'ifblock',
'elseifblock',
'elseblock',
}
---@async
local function semicolonHint(uri, results, start, finish)
local state = files.getState(uri)
if not state then
return
end
local mode = config.get(uri, 'Lua.hint.semicolon')
if mode == 'Disable' then
return
end
local subber = substr(state)
---@async
guide.eachSourceTypes(state.ast, blockTypes, function (src)
await.delay()
for i = 1, #src - 1 do
local current = src[i]
local next = src[i+1]
local left = current.range or current.finish
local right = next.start
local text = subber(current.finish, right)
if mode == 'All' then
if not text:find '[,;]' then
results[#results+1] = {
text = ';',
offset = left,
kind = define.InlayHintKind.Other,
where = 'right',
}
end
elseif mode == 'SameLine' then
if not text:find '[,;\r\n]' then
results[#results+1] = {
text = ';',
offset = left,
kind = define.InlayHintKind.Other,
where = 'right',
}
end
end
end
if mode == 'All' then
local last = src[#src]
results[#results+1] = {
text = ';',
offset = last.range or last.finish,
kind = define.InlayHintKind.Other,
where = 'right',
}
end
end)
end
---@async
return function (uri, start, finish)
local results = {}
typeHint(uri, results, start, finish)
paramName(uri, results, start, finish)
awaitHint(uri, results, start, finish)
arrayIndex(uri, results, start, finish)
semicolonHint(uri, results, start, finish)
return results
end