diff options
Diffstat (limited to 'server/src/core/completion.lua')
-rw-r--r-- | server/src/core/completion.lua | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/server/src/core/completion.lua b/server/src/core/completion.lua new file mode 100644 index 00000000..cdb68580 --- /dev/null +++ b/server/src/core/completion.lua @@ -0,0 +1,501 @@ +local findResult = require 'core.find_result' +local hover = require 'core.hover' + +local CompletionItemKind = { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} + +local function matchKey(me, other) + if me == other then + return false + end + if me == '' then + return true + end + if #me > #other then + return false + end + local lMe = me:lower() + local lOther = other:lower() + if lMe:sub(1, 1) ~= lOther:sub(1, 1) then + return false + end + if lMe == lOther:sub(1, #lMe) then + return true + end + local used = { + [1] = true, + } + local cur = 2 + local lookup + local researched + for i = 2, #lMe do + local c = lMe:sub(i, i) + -- 1. 看当前字符是否匹配 + if c == lOther:sub(cur, cur) then + used[cur] = true + goto NEXT + end + -- 2. 看前一个字符是否匹配 + if not used[cur-1] then + if c == lOther:sub(cur-1, cur-1) then + used[cur-1] = true + goto NEXT + end + end + -- 3. 向后找这个字 + lookup = lOther:find(c, cur+1, true) + if lookup then + cur = lookup + used[cur] = true + goto NEXT + end + + -- 4. 重新搜索整个字符串,但是只允许1次,否则失败.如果找不到也失败 + if researched then + return false + else + researched = true + for j = 2, cur - 2 do + if c == lOther:sub(j, j) then + used[j] = true + goto NEXT + end + end + return false + end + -- 5. 找到下一个可用的字,如果超出长度就算成功 + ::NEXT:: + repeat + cur = cur + 1 + until not used[cur] + if cur > #lOther then + break + end + end + return true +end + +local function searchLocals(vm, pos, name, callback) + for _, loc in ipairs(vm.results.locals) do + if loc.source.start == 0 then + goto CONTINUE + end + if loc.source.start <= pos and loc.close >= pos then + if matchKey(name, loc.key) then + callback(loc) + end + end + ::CONTINUE:: + end +end + +local function searchFields(name, parent, object, callback) + if not parent or not parent.value or not parent.value.child then + return + end + for key, field in pairs(parent.value.child) do + if type(key) ~= 'string' then + goto CONTINUE + end + if object then + if not field.value or field.value.type ~= 'function' then + goto CONTINUE + end + end + if type(name) == 'string' and matchKey(name, key) then + callback(field) + end + ::CONTINUE:: + end +end + +local KEYS = {'and', 'break', 'do', 'else', 'elseif', 'end', 'false', 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', 'return', 'then', 'true', 'until', 'while', 'toclose'} +local function searchKeyWords(name, callback) + for _, key in ipairs(KEYS) do + if matchKey(name, key) then + callback(key) + end + end +end + +local function getKind(var, default) + local value = var.value + if default == CompletionItemKind.Variable then + if value.type == 'function' then + return CompletionItemKind.Function + end + end + if default == CompletionItemKind.Field then + local tp = type(value.value) + if tp == 'number' or tp == 'integer' or tp == 'string' then + return CompletionItemKind.Enum + end + if value.type == 'function' then + if var.parent and var.parent.value and var.parent.value.ENV ~= true then + return CompletionItemKind.Method + else + return CompletionItemKind.Function + end + end + end + return default +end + +local function getDetail(var) + local tp = type(var.value.value) + if tp == 'boolean' then + return ('= %q'):format(var.value.value) + elseif tp == 'number' then + if math.type(var.value.value) == 'integer' then + return ('= %q'):format(var.value.value) + else + local str = ('= %.10f'):format(var.value.value) + local dot = str:find('.', 1, true) + local suffix = str:find('[0]+$', dot+2) + if suffix then + return str:sub(1, suffix-1) + else + return str + end + end + elseif tp == 'string' then + return ('= %q'):format(var.value.value) + end + return nil +end + +local function getDocument(var, source) + if var.value.type == 'function' then + return { + kind = 'markdown', + value = hover(var, source), + } + end + return nil +end + +local function searchAsLocal(vm, pos, result, callback) + searchFields(result.key, vm.results.locals[1], nil, function (var) + callback(var, CompletionItemKind.Variable) + end) + + -- 支持 local function + if matchKey(result.key, 'function') then + callback('function', CompletionItemKind.Keyword) + end +end + +local function searchAsArg(vm, pos, result, callback) + searchFields(result.key, vm.results.locals[1], nil, function (var) + if var.value.lib then + return + end + callback(var, CompletionItemKind.Variable) + end) +end + +local function searchAsGlobal(vm, pos, result, callback) + if result.key == '' then + return + end + searchLocals(vm, pos, result.key, function (var) + callback(var, CompletionItemKind.Variable) + end) + searchFields(result.key, vm.results.locals[1], nil, function (var) + callback(var, CompletionItemKind.Field) + end) + searchKeyWords(result.key, function (name) + callback(name, CompletionItemKind.Keyword) + end) +end + +local function searchAsSuffix(result, callback) + searchFields(result.key, result.parent, result.source.object, function (var) + callback(var, CompletionItemKind.Field) + end) +end + +local function searchInArg(vm, inCall, inString, callback) + local lib = inCall.func.lib + if not lib then + return + end + + -- require列举出可以引用到的文件 + if lib.special == 'require' then + if not vm.lsp or not vm.lsp.workspace then + return + end + local results = vm.lsp.workspace:matchPath(vm.uri, inString[1]) + if not results then + return + end + for _, v in ipairs(results) do + if v ~= inString[1] then + callback(v, CompletionItemKind.File) + end + end + end + + -- 其他库函数,根据参数位置找枚举值 + if lib.args and lib.enums then + local arg = lib.args[inCall.select] + local name = arg and arg.name + for _, enum in ipairs(lib.enums) do + if enum.name == name and enum.enum then + if inString then + callback(enum.enum, CompletionItemKind.EnumMember, { + documentation = enum.description + }) + else + callback(('%q'):format(enum.enum), CompletionItemKind.EnumMember, { + documentation = enum.description + }) + end + end + end + end +end + +local function searchAsIndex(vm, pos, result, callback) + searchLocals(vm, pos, result.key, function (var) + callback(var, CompletionItemKind.Variable) + end) + for _, index in ipairs(vm.results.indexs) do + if matchKey(result.key, index) then + callback(index, CompletionItemKind.Property) + end + end + searchFields(result.key, vm.results.locals[1], nil, function (var) + callback(var, CompletionItemKind.Field) + end) +end + +local function findClosePos(vm, pos) + local curDis = math.maxinteger + local parent = nil + local function found(object, source) + local dis = pos - source.finish + if dis > 1 and dis < curDis then + curDis = dis + parent = object + end + end + for sources, object in pairs(vm.results.sources) do + if sources.type == 'multi-source' then + for _, source in ipairs(sources) do + if source.type ~= 'simple' then + found(object, source) + end + end + else + found(object, sources) + end + end + if not parent then + return nil + end + if parent.type ~= 'local' and parent.type ~= 'field' then + return nil + end + -- 造个假的 DirtyName + local source = { + type = 'name', + start = pos, + finish = pos, + [1] = '', + } + local result = { + type = 'field', + parent = parent, + key = '', + source = source, + } + return result, source +end + +local function isContainPos(obj, pos) + if obj.start <= pos and obj.finish + 1 >= pos then + return true + end + return false +end + +local function findString(vm, pos) + for _, source in ipairs(vm.results.strings) do + if isContainPos(source, pos) then + return source + end + end + return nil +end + +local function findArgCount(args, pos) + for i, arg in ipairs(args) do + if isContainPos(arg, pos) then + return i + end + end + return #args + 1 +end + +-- 找出范围包含pos的call +local function findCall(vm, pos) + local results = {} + for _, call in ipairs(vm.results.calls) do + if isContainPos(call.args, pos) then + local n = findArgCount(call.args, pos) + local var = vm.results.sources[call.lastObj] + if var then + results[#results+1] = { + func = call.func, + var = var, + source = call.lastObj, + select = n, + args = call.args, + } + end + end + end + if #results == 0 then + return nil + end + -- 可能处于 'func1(func2(' 的嵌套中,因此距离越远的函数层级越低 + table.sort(results, function (a, b) + return a.args.start < b.args.start + end) + return results[#results] +end + +local function makeList(source) + local list = {} + local mark = {} + local function callback(var, defualt, data) + local key + if type(var) == 'string' then + key = var + else + key = var.key + end + if mark[key] then + return + end + mark[key] = true + data = data or {} + list[#list+1] = data + if var == key then + data.label = var + data.kind = defualt + else + data.label = var.key + data.kind = getKind(var, defualt) + data.detail = data.detail or getDetail(var) + data.documentation = data.documentation or getDocument(var, source) + end + end + return list, callback +end + +local function searchInResult(result, source, vm, pos, callback) + if result.type == 'local' then + if source.isArg then + searchAsArg(vm, pos, result, callback) + elseif source.isLocal then + searchAsLocal(vm, pos, result, callback) + else + searchAsGlobal(vm, pos, result, callback) + end + elseif result.type == 'field' then + if source.isIndex then + searchAsIndex(vm, pos, result, callback) + elseif result.parent and result.parent.value and result.parent.value.ENV == true then + searchAsGlobal(vm, pos, result, callback) + else + searchAsSuffix(result, callback) + end + end +end + +local function searchSpecial(vm, pos, callback) + -- 尝试 # + local result, source = findResult(vm, pos, 2) + if source and source.type == 'index' + and result.source and result.source.op == '#' + then + local name = {} + local var = result + while true do + var = var.parent + if not var then + break + end + if var.value and var.value.ENV then + break + end + local key = var.key + if type(key) ~= 'string' or key == '' then + return + end + table.insert(name, 1, key) + end + local label = table.concat(name, '.') .. '+1' + callback(label, CompletionItemKind.Snippet, { + textEdit = { + start = result.source.start + 1, + finish = source.finish, + newText = ('%s] = '):format(label), + } + }) + end +end + +return function (vm, pos) + local result, source = findResult(vm, pos) + if not result then + result, source = findClosePos(vm, pos) + end + + if not result then + return nil + end + + local list, callback = makeList(source) + local inCall = findCall(vm, pos) + local inString = findString(vm, pos) + if inCall then + searchInArg(vm, inCall, inString, callback) + end + searchSpecial(vm, pos, callback) + if not inString then + searchInResult(result, source, vm, pos, callback) + end + if #list == 0 then + return nil + end + return list +end |