diff options
Diffstat (limited to 'server/src/workspace.lua')
-rw-r--r-- | server/src/workspace.lua | 507 |
1 files changed, 507 insertions, 0 deletions
diff --git a/server/src/workspace.lua b/server/src/workspace.lua new file mode 100644 index 00000000..c69b6399 --- /dev/null +++ b/server/src/workspace.lua @@ -0,0 +1,507 @@ +local fs = require 'bee.filesystem' +local async = require 'async' +local config = require 'config' +local ll = require 'lpeglabel' +local platform = require 'bee.platform' + +local TrueName = {} + +local function getFileName(path) + local name = path:string() + if platform.OS == 'Windows' then + local lname = name:lower() + TrueName[lname] = name + return lname + else + return name + end +end + +local function getTrueName(name) + return TrueName[name] or name +end + +local function fileNameEq(a, b) + if platform.OS == 'Windows' then + return a:lower() == b:lower() + else + return a == b + end +end + +local function split(str, sep) + local t = {} + for s in str:gmatch('[^' .. sep .. ']+') do + t[#t+1] = s + end + return t +end + +local function similarity(a, b) + local ta = split(a, '/\\') + local tb = split(b, '/\\') + for i = 1, #ta do + if ta[i] ~= tb[i] then + return i - 1 + end + end + return #ta +end + +local mt = {} +mt.__index = mt + +function mt:uriDecode(uri) + -- Unix-like系统根是/ + if uri:sub(1, 9) == 'file:////' then + return fs.path(uri:sub(9)) + end + if uri:sub(1, 8) ~= 'file:///' then + log.error('uri decode failed: ', uri) + return nil + end + local names = {} + for name in uri:sub(9):gmatch '[^%/]+' do + names[#names+1] = name:gsub('%%([0-9a-fA-F][0-9a-fA-F])', function (hex) + return string.char(tonumber(hex, 16)) + end) + end + if #names == 0 then + log.error('uri decode failed: ', uri) + return nil + end + -- 盘符后面加个斜杠 + local path = fs.path(names[1] .. '\\') + for i = 2, #names do + path = path / names[i] + end + return fs.absolute(path) +end + +function mt:uriEncode(path) + local names = {} + local cur = fs.absolute(path) + while true do + local name = cur:filename():string() + if name == '' then + -- 盘符,去掉一个斜杠 + name = cur:string():sub(1, -2) + end + name = name:gsub([=[[^%w%-%_%.%~]]=], function (char) + return ('%%%02X'):format(string.byte(char)) + end) + table.insert(names, 1, name) + if cur == cur:parent_path() then + break + end + cur = cur:parent_path() + end + return 'file:///' .. table.concat(names, '/') +end + +function mt:listenLoadFile() + self._loadFileRequest = async.run('loadfile', nil, function (filename, buf) + local path = fs.path(filename) + local name = getFileName(path) + local uri = self:uriEncode(path) + self.files[name] = uri + self.lsp:readText(uri, path, buf, self._currentScanCompiled) + end) +end + +function mt:buildScanPattern() + local pattern = {} + + -- config.workspace.ignoreDir + for path in pairs(config.config.workspace.ignoreDir) do + pattern[#pattern+1] = path + end + -- config.files.exclude + for path, ignore in pairs(config.other.exclude) do + if ignore then + pattern[#pattern+1] = path + end + end + -- config.workspace.ignoreSubmodules + if config.config.workspace.ignoreSubmodules then + local buf = io.load(self.root / '.gitmodules') + if buf then + for path in buf:gmatch('path = ([^\r\n]+)') do + log.info('忽略子模块:', path) + pattern[#pattern+1] = path + end + end + end + -- config.workspace.useGitIgnore + if config.config.workspace.useGitIgnore then + local buf = io.load(self.root / '.gitignore') + if buf then + for line in buf:gmatch '[^\r\n]+' do + pattern[#pattern+1] = line + end + end + end + -- config.files.associations + pattern[#pattern+1] = '*.*' + pattern[#pattern+1] = '!*.lua' + for k, v in pairs(config.other.associations) do + if fileNameEq(v, 'lua') then + pattern[#pattern+1] = '!' .. k + end + end + + return pattern +end + +function mt:scanFiles() + if self._scanRequest then + log.info('中断上次扫描文件任务') + self._scanRequest:push('stop') + self._scanRequest = nil + self._complete = false + self:reset() + end + + local pattern = self:buildScanPattern() + log.info('忽略文件:\r\n' .. table.concat(pattern, '\r\n')) + log.info('开始扫描文件任务') + self._currentScanCompiled = {} + local count = 0 + self._scanRequest = async.run('scanfiles', { + root = self.root:string(), + pattern = pattern, + options = { + ignoreCase = platform.OS == 'Windows', + } + }, function (mode, ...) + if mode == 'ok' then + log.info('扫描文件任务完成,共', count, '个文件。') + self._complete = true + self._scanRequest = nil + self:reset() + return true + elseif mode == 'log' then + log.debug(...) + elseif mode == 'path' then + local path = fs.path(...) + self._loadFileRequest:push(path:string()) + count = count + 1 + elseif mode == 'stop' then + log.info('扫描文件任务中断') + return false + end + end) +end + +function mt:init(rootUri) + self.root = self:uriDecode(rootUri) + self.uri = rootUri + if not self.root then + return + end + log.info('Workspace inited, root: ', self.root) + local logPath = ROOT / 'log' / (rootUri:gsub('[/:]+', '_') .. '.log') + log.info('Log path: ', logPath) + log.init(ROOT, logPath) + + self:scanFiles() +end + +function mt:isComplete() + return not not self._complete +end + +function mt:isLuaFile(path) + local ext = path:extension():string() + for k, v in pairs(config.other.associations) do + if fileNameEq(ext, k:match('[^%*]+$')) then + if v == 'lua' then + return true + else + return false + end + end + end + if fileNameEq(ext, '.lua') then + return true + end + return false +end + +function mt:addFile(path) + if not self:isLuaFile(path) then + return + end + local name = getFileName(path) + local uri = self:uriEncode(path) + self.files[name] = uri + self.lsp:readText(uri, path) +end + +function mt:removeFile(path) + local name = getFileName(path) + if not self.files[name] then + return + end + self.files[name] = nil + local uri = self:uriEncode(path) + self.lsp:removeText(uri) +end + +function mt:findPath(baseUri, searchers) + local results = {} + local baseName = getFileName(self:uriDecode(baseUri)) + for filename, uri in pairs(self.files) do + if filename ~= baseName then + for _, searcher in ipairs(searchers) do + if filename:sub(-#searcher) == searcher then + local sep = filename:sub(-#searcher-1, -#searcher-1) + if sep == '/' or sep == '\\' then + results[#results+1] = uri + end + end + end + end + end + + if #results == 0 then + return nil + end + local uri + if #results == 1 then + uri = results[1] + else + table.sort(results, function (a, b) + return similarity(a, baseUri) > similarity(b, baseUri) + end) + uri = results[1] + end + return uri +end + +function mt:createCompiler(str) + local state = { + 'Main', + } + local function push(c) + if state.Main then + state.Main = state.Main * c + else + state.Main = c + end + end + local count = 0 + local function code() + count = count + 1 + local name = 'C' .. tostring(count) + local nextName = 'C' .. tostring(count + 1) + state[name] = ll.P(1) * (#ll.V(nextName) + ll.V(name)) + return ll.V(name) + end + local function static(c) + count = count + 1 + local name = 'C' .. tostring(count) + local nextName = 'C' .. tostring(count + 1) + local catch = #ll.V(nextName) + if platform.OS == 'Windows' then + for i = #c, 1, -1 do + local char = c:sub(i, i) + local u = char:upper() + local l = char:lower() + if u == l then + catch = ll.P(char) * catch + else + catch = (ll.P(u) + ll.P(l)) * catch + end + end + else + catch = ll.P(c) * catch + end + state[name] = catch + return ll.V(name) + end + local function eof() + count = count + 1 + local name = 'C' .. tostring(count) + state[name] = ll.Cmt(ll.P(1) + ll.Cp(), function (_, _, c) + return type(c) == 'number' + end) + return ll.V(name) + end + local isFirstCode = true + local firstCode + local compiler = ll.P { + 'Result', + Result = (ll.V'Code' + ll.V'Static')^1, + Code = ll.P'?' / function () + if isFirstCode then + isFirstCode = false + push(ll.Cmt(ll.C(code()), function (_, pos, code) + firstCode = code + return pos, code + end)) + else + push(ll.Cmt( + ll.C(code()), + function (_, _, me) + return firstCode == me + end + )) + end + end, + Static = (1 - ll.P'?')^1 / function (c) + push(static(c)) + end, + } + compiler:match(str) + push(eof()) + return ll.P(state) +end + +function mt:compileLuaPath() + for i, luapath in ipairs(config.config.runtime.path) do + self.pathMatcher[i] = self:createCompiler(luapath) + end +end + +function mt:convertPathAsRequire(filename, start) + local list + for _, matcher in ipairs(self.pathMatcher) do + local str = matcher:match(filename:sub(start)) + if str then + if not list then + list = {} + end + list[#list+1] = str:gsub('/', '.') + end + end + return list +end + +function mt:matchPath(baseUri, input) + local first = input:match '[^%.]+' + if not first then + return nil + end + local baseName = getFileName(self:uriDecode(baseUri)) + local rootLen = #self.root:string() + local map = {} + for filename in pairs(self.files) do + if filename ~= baseName then + local trueFilename = getTrueName(filename) + local start + if platform.OS == 'Windows' then + start = filename:find('[/\\]' .. first:lower(), rootLen + 1) + else + start = trueFilename:find('[/\\]' .. first, rootLen + 1) + end + if start then + local list = self:convertPathAsRequire(trueFilename, start + 1) + if list then + for _, str in ipairs(list) do + if #str >= #input and fileNameEq(str:sub(1, #input), input) then + if not map[str] then + map[str] = trueFilename + else + local s1 = similarity(trueFilename, baseName) + local s2 = similarity(map[str], baseName) + if s1 > s2 then + map[str] = trueFilename + elseif s1 == s2 then + if trueFilename < map[str] then + map[str] = trueFilename + end + end + end + end + end + end + end + end + end + + local list = {} + for str in pairs(map) do + list[#list+1] = str + map[str] = map[str]:sub(rootLen + 2) + end + if #list == 0 then + return nil + end + table.sort(list, function (a, b) + local sa = similarity(map[a], baseName) + local sb = similarity(map[b], baseName) + if sa == sb then + return a < b + else + return sa > sb + end + end) + return list, map +end + +function mt:searchPath(baseUri, str) + str = getFileName(fs.path(str)) + if self.searched[baseUri] and self.searched[baseUri][str] then + return self.searched[baseUri][str] + end + str = str:gsub('%.', '/') + local searchers = {} + for i, luapath in ipairs(config.config.runtime.path) do + searchers[i] = luapath:gsub('%?', str) + end + + local uri = self:findPath(baseUri, searchers) + if uri then + if not self.searched[baseUri] then + self.searched[baseUri] = {} + end + self.searched[baseUri][str] = uri + end + return uri +end + +function mt:loadPath(baseUri, str) + local ok, relative = pcall(fs.relative, fs.absolute(self.root / str), self.root) + if not ok then + return nil + end + str = getFileName(relative) + if self.loaded[str] then + return self.loaded[str] + end + + local searchers = { str } + + local uri = self:findPath(baseUri, searchers) + if uri then + self.loaded[str] = uri + end + return uri +end + +function mt:reset() + self.searched = {} + self.loaded = {} + self.lsp:reCompile() +end + +function mt:relativePathByUri(uri) + local path = self:uriDecode(uri) + local relate = fs.relative(path, self.root) + return relate +end + +return function (lsp, name) + local workspace = setmetatable({ + lsp = lsp, + name = name, + files = {}, + searched = {}, + loaded = {}, + pathMatcher = {} + }, mt) + workspace:compileLuaPath() + workspace:listenLoadFile() + return workspace +end |