diff options
author | 最萌小汐 <sumneko@hotmail.com> | 2019-03-06 15:21:07 +0800 |
---|---|---|
committer | 最萌小汐 <sumneko@hotmail.com> | 2019-03-06 15:21:07 +0800 |
commit | da9766054cad4b47f5bc1534f1c9a13a922e17bc (patch) | |
tree | 7e773ea59e1388d195274e6fdde3ff7ad4963c7c /server/src/core | |
parent | a6807742cc7128b6eab4a078feac11af4039e1f9 (diff) | |
download | lua-language-server-da9766054cad4b47f5bc1534f1c9a13a922e17bc.zip |
更新自动完成的支持
Diffstat (limited to 'server/src/core')
-rw-r--r-- | server/src/core/completion.lua | 482 | ||||
-rw-r--r-- | server/src/core/completion.old.lua | 578 |
2 files changed, 612 insertions, 448 deletions
diff --git a/server/src/core/completion.lua b/server/src/core/completion.lua index 57e006e9..5b658df4 100644 --- a/server/src/core/completion.lua +++ b/server/src/core/completion.lua @@ -98,481 +98,67 @@ local function matchKey(me, other) return true end -local function searchLocals(vm, pos, name, callback) - for _, loc in ipairs(vm.results.locals) do - if loc.source.start == 0 then +local function searchLocals(vm, source, word, callback) + for _, src in ipairs(vm.sources) do + local loc = src:bindLocal() + if not loc then goto CONTINUE end - if not loc.close then - log.debug('Miss loc close', table.dump(loc)) - 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 sortPairs(t) - local keys = {} - for k in pairs(t) do - keys[#keys+1] = k - end - table.sort(keys) - local i = 0 - return function () - i = i + 1 - local k = keys[i] - return k, t[k] - end -end - -local function searchFields(name, parentValue, source, callback) - if not parentValue then - return - end - if type(name) ~= 'string' then - return - end - local map = {} - parentValue:eachField(function (key, field) - if type(key) ~= 'string' then - goto CONTINUE - end - if source.object and field.value:getType() ~= 'function' then - goto CONTINUE - end - if matchKey(name, key) then - map[key] = field - end - ::CONTINUE:: - end) - for _, field in sortPairs(map) do - callback(field) - 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:getType() == 'function' then - return CompletionItemKind.Function - end - end - if default == CompletionItemKind.Field then - local tp = type(value:getValue()) - if tp == 'number' or tp == 'integer' or tp == 'string' then - return CompletionItemKind.Enum - end - if value:getType() == 'function' then - if value.object then - return CompletionItemKind.Method - else - return CompletionItemKind.Function - end + if src.start <= source.start + and loc:close() >= source.finish + and matchKey(word, loc:getName()) + then + callback(loc:getName(), src, CompletionItemKind.Variable) end + :: CONTINUE :: end - return default end -local function getDetail(var) - local tp = type(var.value:getValue()) - if tp == 'boolean' then - return ('= %q'):format(var.value:getValue()) - elseif tp == 'number' then - if math.type(var.value:getValue()) == 'integer' then - return ('= %q'):format(var.value:getValue()) - else - local str = ('= %.10f'):format(var.value:getValue()) - 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:getValue()) - end - return nil -end - -local function getDocument(var, source) - if not source then - return nil - end - if var.value:getType() == 'function' then - local hvr = hover(var, source) - if not hvr then - return nil - end - local text = ([[ -```lua -%s -``` -%s -```lua -%s -``` -]]):format(hvr.label or '', hvr.description or '', hvr.enum or '') - return { - kind = 'markdown', - value = text, - } - end - return nil -end - -local function searchAsLocal(vm, word, pos, result, source, callback) - searchFields(word, vm.env, source, function (var) - callback(var, CompletionItemKind.Variable) - end) - - -- 支持 local function - if matchKey(word, 'function') then - callback('function', CompletionItemKind.Keyword) - end -end - -local function searchAsArg(vm, word, pos, result, source, callback) - searchFields(word, vm.env, source, function (var) - if var.value.lib then - return - end - callback(var, CompletionItemKind.Variable) - end) -end - -local function searchAsGlobal(vm, word, pos, result, source, callback) +local function searchAsGlobal(vm, source, word, callback) if word == '' or word == nil then return end - searchLocals(vm, pos, word, function (var) - callback(var, CompletionItemKind.Variable) - end) - searchFields(word, result.parentValue, source, function (var) - callback(var, CompletionItemKind.Field) - end) - searchKeyWords(word, function (name) - callback(name, CompletionItemKind.Keyword) - end) + searchLocals(vm, source, word, callback) end -local function searchAsSuffix(result, source, word, callback) - searchFields(word, result.parentValue, source, 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 or not inString 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, { - textEdit = { - start = inString.start+1, - finish = inString.finish-1, - newText = ('%q'):format(v):sub(2, -2), - } - }) - 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 +local function searchSource(vm, source, word, callback) + if source:get 'global' then + searchAsGlobal(vm, source, word, callback) end end -local function searchAsIndex(vm, word, pos, result, source, callback) - searchLocals(vm, pos, word, function (var) - callback(var, CompletionItemKind.Variable) - end) - for _, source in ipairs(vm.results.sources) do - if source.isIndex then - local index = source.bind - if matchKey(word, index.key) then - callback(index.key, CompletionItemKind.Property) - end - end - end - searchFields(word, result.parentValue, source, function (var) - callback(var, CompletionItemKind.Field) - end) -end - -local function findClosePos(vm, pos) - local curDis = math.maxinteger - local parent = nil - local inputSource = nil - local function found(object, source) - local dis = pos - source.finish - if dis > 1 and dis < curDis then - curDis = dis - parent = object - inputSource = source - end - end - for _, source in ipairs(vm.results.sources) do - found(source.bind, source) - end - if not parent then - return nil - end - if parent.type ~= 'local' and parent.type ~= 'field' then - return nil - end - local sep = inputSource.dot or inputSource.colon - if not sep then - return nil - end - if sep.finish > pos then - return nil - end - -- 造个假的 DirtyName - local source = { - type = 'name', - start = pos, - finish = pos, - object = sep.type == ':' and parent, - isSuffix = true, - [1] = '', - } - local result = { - type = 'field', - parent = parent, - parentValue = parent.value, - key = '', - } - 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, word, pos) - local finishPos = pos + #word - 1 - for _, source in ipairs(vm.results.strings) do - if isContainPos(source, finishPos) 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, word, pos) - local finishPos = pos + #word - 1 - local results = {} - for _, call in ipairs(vm.results.calls) do - if isContainPos(call.args, finishPos) then - local n = findArgCount(call.args, finishPos) - local var = call.lastObj.bind - 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(list, source) +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 function (name, src, kind, data) + if src == source then return end - mark[key] = true - data = data or {} - if var == key then - data.label = var - data.kind = defualt - elseif var.source ~= source then - data.label = var.key - data.kind = getKind(var, defualt) - data.detail = data.detail or getDetail(var) - data.documentation = data.documentation or getDocument(var, source) - else + if mark[name] then return end - list[#list+1] = data - end - return callback -end - -local function searchInResult(result, word, source, vm, pos, callback) - if result.type == 'local' then - if result.link then - result = result.link - end - if source.isArg then - searchAsArg(vm, word, pos, result, source, callback) - elseif source.isLocal then - searchAsLocal(vm, word, pos, result, source, callback) - else - searchAsGlobal(vm, word, pos, result, source, callback) - end - elseif result.type == 'field' then - if source.isIndex then - searchAsIndex(vm, word, pos, result, source, callback) - elseif source.isSuffix then - searchAsSuffix(result, source, word, callback) - else - searchAsGlobal(vm, word, pos, result, source, callback) + mark[name] = true + if not data then + data = {} end - end -end - -local function searchAllWords(text, vm, callback) - if text == '' then - return - end - if type(text) ~= 'string' then - return - end - for _, source in ipairs(vm.results.sources) do - if source.type == 'name' then - if text ~= source[1] and matchKey(text, source[1]) then - callback(source[1], CompletionItemKind.Text) - end - end - end -end - -local function searchSpecial(vm, pos, callback) - -- 尝试 # - local _, source = findSource(vm, pos, 2) - if source and source.indexName and source[1].op == '#' - then - local label = source.indexName .. '+1' - callback(label, CompletionItemKind.Snippet, { - textEdit = { - start = source.start, - finish = source.finish, - newText = ('[#%s] = '):format(label), - } - }) - end -end - -local function clearList(list, source) - local key = source[1] - -- 清除自己 - for i, v in ipairs(list) do - if v.label == key then - table.remove(list, i) - return - end - end -end - -local function isValidResult(result) - if not result then - return false - end - if result.type == 'local' or result.type == 'field' then - return true - end - return false + data.label = name + data.kind = kind + list[#list+1] = data + end, list end return function (vm, pos, word) - local list = {} - local callback = makeList(list) - local inCall = findCall(vm, word, pos) - local inString = findString(vm, word, pos) - if inCall then - searchInArg(vm, inCall, inString, callback) - end - searchSpecial(vm, pos, callback) - if not inString then - local result, source = findSource(vm, pos) - if not isValidResult(result) then - result, source = findClosePos(vm, pos) - end - if isValidResult(result) then - callback = makeList(list, source) - searchInResult(result, word, source, vm, pos, callback) - searchAllWords(result.key, vm, callback) - clearList(list, source) - else - searchAllWords(word, vm, callback) - end + local source = findSource(vm, pos) + if not source then + return nil end + local callback, list = makeList(source) + searchSource(vm, source, word, callback) + if #list == 0 then return nil end + return list end diff --git a/server/src/core/completion.old.lua b/server/src/core/completion.old.lua new file mode 100644 index 00000000..57e006e9 --- /dev/null +++ b/server/src/core/completion.old.lua @@ -0,0 +1,578 @@ +local findSource = require 'core.find_source' +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 true + 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 = {} + local cur = 1 + local lookup + local researched + for i = 1, #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 = 1, 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 + return i == #lMe + 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 not loc.close then + log.debug('Miss loc close', table.dump(loc)) + 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 sortPairs(t) + local keys = {} + for k in pairs(t) do + keys[#keys+1] = k + end + table.sort(keys) + local i = 0 + return function () + i = i + 1 + local k = keys[i] + return k, t[k] + end +end + +local function searchFields(name, parentValue, source, callback) + if not parentValue then + return + end + if type(name) ~= 'string' then + return + end + local map = {} + parentValue:eachField(function (key, field) + if type(key) ~= 'string' then + goto CONTINUE + end + if source.object and field.value:getType() ~= 'function' then + goto CONTINUE + end + if matchKey(name, key) then + map[key] = field + end + ::CONTINUE:: + end) + for _, field in sortPairs(map) do + callback(field) + 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:getType() == 'function' then + return CompletionItemKind.Function + end + end + if default == CompletionItemKind.Field then + local tp = type(value:getValue()) + if tp == 'number' or tp == 'integer' or tp == 'string' then + return CompletionItemKind.Enum + end + if value:getType() == 'function' then + if value.object then + return CompletionItemKind.Method + else + return CompletionItemKind.Function + end + end + end + return default +end + +local function getDetail(var) + local tp = type(var.value:getValue()) + if tp == 'boolean' then + return ('= %q'):format(var.value:getValue()) + elseif tp == 'number' then + if math.type(var.value:getValue()) == 'integer' then + return ('= %q'):format(var.value:getValue()) + else + local str = ('= %.10f'):format(var.value:getValue()) + 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:getValue()) + end + return nil +end + +local function getDocument(var, source) + if not source then + return nil + end + if var.value:getType() == 'function' then + local hvr = hover(var, source) + if not hvr then + return nil + end + local text = ([[ +```lua +%s +``` +%s +```lua +%s +``` +]]):format(hvr.label or '', hvr.description or '', hvr.enum or '') + return { + kind = 'markdown', + value = text, + } + end + return nil +end + +local function searchAsLocal(vm, word, pos, result, source, callback) + searchFields(word, vm.env, source, function (var) + callback(var, CompletionItemKind.Variable) + end) + + -- 支持 local function + if matchKey(word, 'function') then + callback('function', CompletionItemKind.Keyword) + end +end + +local function searchAsArg(vm, word, pos, result, source, callback) + searchFields(word, vm.env, source, function (var) + if var.value.lib then + return + end + callback(var, CompletionItemKind.Variable) + end) +end + +local function searchAsGlobal(vm, word, pos, result, source, callback) + if word == '' or word == nil then + return + end + searchLocals(vm, pos, word, function (var) + callback(var, CompletionItemKind.Variable) + end) + searchFields(word, result.parentValue, source, function (var) + callback(var, CompletionItemKind.Field) + end) + searchKeyWords(word, function (name) + callback(name, CompletionItemKind.Keyword) + end) +end + +local function searchAsSuffix(result, source, word, callback) + searchFields(word, result.parentValue, source, 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 or not inString 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, { + textEdit = { + start = inString.start+1, + finish = inString.finish-1, + newText = ('%q'):format(v):sub(2, -2), + } + }) + 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, word, pos, result, source, callback) + searchLocals(vm, pos, word, function (var) + callback(var, CompletionItemKind.Variable) + end) + for _, source in ipairs(vm.results.sources) do + if source.isIndex then + local index = source.bind + if matchKey(word, index.key) then + callback(index.key, CompletionItemKind.Property) + end + end + end + searchFields(word, result.parentValue, source, function (var) + callback(var, CompletionItemKind.Field) + end) +end + +local function findClosePos(vm, pos) + local curDis = math.maxinteger + local parent = nil + local inputSource = nil + local function found(object, source) + local dis = pos - source.finish + if dis > 1 and dis < curDis then + curDis = dis + parent = object + inputSource = source + end + end + for _, source in ipairs(vm.results.sources) do + found(source.bind, source) + end + if not parent then + return nil + end + if parent.type ~= 'local' and parent.type ~= 'field' then + return nil + end + local sep = inputSource.dot or inputSource.colon + if not sep then + return nil + end + if sep.finish > pos then + return nil + end + -- 造个假的 DirtyName + local source = { + type = 'name', + start = pos, + finish = pos, + object = sep.type == ':' and parent, + isSuffix = true, + [1] = '', + } + local result = { + type = 'field', + parent = parent, + parentValue = parent.value, + key = '', + } + 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, word, pos) + local finishPos = pos + #word - 1 + for _, source in ipairs(vm.results.strings) do + if isContainPos(source, finishPos) 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, word, pos) + local finishPos = pos + #word - 1 + local results = {} + for _, call in ipairs(vm.results.calls) do + if isContainPos(call.args, finishPos) then + local n = findArgCount(call.args, finishPos) + local var = call.lastObj.bind + 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(list, source) + 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 {} + if var == key then + data.label = var + data.kind = defualt + elseif var.source ~= source then + data.label = var.key + data.kind = getKind(var, defualt) + data.detail = data.detail or getDetail(var) + data.documentation = data.documentation or getDocument(var, source) + else + return + end + list[#list+1] = data + end + return callback +end + +local function searchInResult(result, word, source, vm, pos, callback) + if result.type == 'local' then + if result.link then + result = result.link + end + if source.isArg then + searchAsArg(vm, word, pos, result, source, callback) + elseif source.isLocal then + searchAsLocal(vm, word, pos, result, source, callback) + else + searchAsGlobal(vm, word, pos, result, source, callback) + end + elseif result.type == 'field' then + if source.isIndex then + searchAsIndex(vm, word, pos, result, source, callback) + elseif source.isSuffix then + searchAsSuffix(result, source, word, callback) + else + searchAsGlobal(vm, word, pos, result, source, callback) + end + end +end + +local function searchAllWords(text, vm, callback) + if text == '' then + return + end + if type(text) ~= 'string' then + return + end + for _, source in ipairs(vm.results.sources) do + if source.type == 'name' then + if text ~= source[1] and matchKey(text, source[1]) then + callback(source[1], CompletionItemKind.Text) + end + end + end +end + +local function searchSpecial(vm, pos, callback) + -- 尝试 # + local _, source = findSource(vm, pos, 2) + if source and source.indexName and source[1].op == '#' + then + local label = source.indexName .. '+1' + callback(label, CompletionItemKind.Snippet, { + textEdit = { + start = source.start, + finish = source.finish, + newText = ('[#%s] = '):format(label), + } + }) + end +end + +local function clearList(list, source) + local key = source[1] + -- 清除自己 + for i, v in ipairs(list) do + if v.label == key then + table.remove(list, i) + return + end + end +end + +local function isValidResult(result) + if not result then + return false + end + if result.type == 'local' or result.type == 'field' then + return true + end + return false +end + +return function (vm, pos, word) + local list = {} + local callback = makeList(list) + local inCall = findCall(vm, word, pos) + local inString = findString(vm, word, pos) + if inCall then + searchInArg(vm, inCall, inString, callback) + end + searchSpecial(vm, pos, callback) + if not inString then + local result, source = findSource(vm, pos) + if not isValidResult(result) then + result, source = findClosePos(vm, pos) + end + if isValidResult(result) then + callback = makeList(list, source) + searchInResult(result, word, source, vm, pos, callback) + searchAllWords(result.key, vm, callback) + clearList(list, source) + else + searchAllWords(word, vm, callback) + end + end + if #list == 0 then + return nil + end + return list +end |