diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/src/async/scanfiles.lua | 64 | ||||
-rw-r--r-- | server/src/glob/gitignore.lua | 222 | ||||
-rw-r--r-- | server/src/glob/glob.lua | 120 | ||||
-rw-r--r-- | server/src/glob/init.lua | 4 | ||||
-rw-r--r-- | server/src/glob/matcher.lua | 146 | ||||
-rw-r--r-- | server/src/path_filter.lua | 72 | ||||
-rw-r--r-- | server/src/workspace.lua (renamed from server/src/workspace/workspace.lua) | 54 | ||||
-rw-r--r-- | server/src/workspace/init.lua | 1 |
8 files changed, 554 insertions, 129 deletions
diff --git a/server/src/async/scanfiles.lua b/server/src/async/scanfiles.lua index f54b0866..7932ac31 100644 --- a/server/src/async/scanfiles.lua +++ b/server/src/async/scanfiles.lua @@ -1,53 +1,41 @@ local args = ... -require 'utility' local fs = require 'bee.filesystem' -local path_filter = require 'path_filter' +local glob = require 'glob' +local root = fs.path(args.root) -local function scan(root, filter) - local result = {} - local len = #root:string() - local name = root:string():sub(len+2):gsub('/', '\\') - if filter(name) then - OUT:push('log', '过滤文件:', root:string()) +local session = glob.gitignore(args.pattern, args.options) +session:setInterface('type', function (path) + local fullpath = root / path + if not fs.exists(fullpath) then + return nil + end + if fs.is_directory(fullpath) then + return 'directory' else - result[#result+1] = root + return 'file' end - local i = 0 - return function () - i = i + 1 - local current = result[i] - if not current then - return nil - end - if fs.is_directory(current) then - for path in current:list_directory() do - local name = path:string():sub(len+2):gsub('/', '\\') - if filter(name) then - OUT:push('log', '过滤文件:', path:string()) - else - result[#result+1] = path - end - end - end - return current + return nil +end) +session:setInterface('list', function (path) + local fullpath = root / path + if not fs.exists(fullpath) then + return nil end -end - -local ignore = {} -for _, name in ipairs(args.ignored) do - if name:sub(1, 1) ~= '!' then - ignore[#ignore+1] = name + local list = {} + for child in fullpath:list_directory() do + list[#list+1] = child:string() end -end -local filter = path_filter(ignore) -for path in scan(fs.path(args.root), filter) do + return list +end) + +session:scan(function (path) local ok, msg = IN:pop() if ok and msg == 'stop' then OUT:push 'stop' return end - OUT:push('path', fs.absolute(path):string()) -end + OUT:push('path', fs.absolute(root / path):string()) +end) OUT:push 'ok' diff --git a/server/src/glob/gitignore.lua b/server/src/glob/gitignore.lua new file mode 100644 index 00000000..9ba2d564 --- /dev/null +++ b/server/src/glob/gitignore.lua @@ -0,0 +1,222 @@ +local m = require 'lpeglabel' +local matcher = require 'glob.matcher' + +local function prop(name, pat) + return m.Cg(m.Cc(true), name) * pat +end + +local function object(type, pat) + return m.Ct( + m.Cg(m.Cc(type), 'type') * + m.Cg(pat, 'value') + ) +end + +local function expect(p, err) + return p + m.T(err) +end + +local parser = m.P { + 'Main', + ['Sp'] = m.S(' \t')^0, + ['Slash'] = m.S('/\\')^1, + ['Main'] = m.Ct(m.V'Sp' * m.P'{' * m.V'Pattern' * (',' * expect(m.V'Pattern', 'Miss exp after ","'))^0 * m.P'}') + + m.Ct(m.V'Pattern') + + m.T'Main Failed' + , + ['Pattern'] = m.Ct(m.V'Sp' * prop('neg', m.P'!') * expect(m.V'Unit', 'Miss exp after "!"')) + + m.Ct(m.V'Unit') + , + ['NeedRoot'] = prop('root', (m.P'.' * m.V'Slash' + m.V'Slash')), + ['Unit'] = m.V'Sp' * m.V'NeedRoot'^-1 * expect(m.V'Exp', 'Miss exp') * m.V'Sp', + ['Exp'] = m.V'Sp' * (m.V'FSymbol' + object('/', m.V'Slash') + m.V'Word')^0 * m.V'Sp', + ['Word'] = object('word', m.Ct((m.V'CSymbol' + m.V'Char' - m.V'FSymbol')^1)), + ['CSymbol'] = object('*', m.P'*') + + object('?', m.P'?') + + object('[]', m.V'Range') + , + ['Char'] = object('char', (1 - m.S',{}[]*?/\\')^1), + ['FSymbol'] = object('**', m.P'**'), + ['Range'] = m.P'[' * m.Ct(m.V'RangeUnit'^0) * m.P']'^-1, + ['RangeUnit'] = m.Ct(- m.P']' * m.C(m.P(1)) * (m.P'-' * - m.P']' * m.C(m.P(1)))^-1), +} + +---@class gitignore +local mt = {} +mt.__index = mt +mt.__name = 'gitignore' + +function mt:addPattern(pat) + if type(pat) ~= 'string' then + return + end + self.pattern[#self.pattern+1] = pat + if self.options.ignoreCase then + pat = pat:lower() + end + local states, err = parser:match(pat) + if not states then + self.errors[#self.errors+1] = { + pattern = pat, + message = err + } + return + end + for _, state in ipairs(states) do + self.matcher[#self.matcher+1] = matcher(state) + end +end + +function mt:setOption(op, val) + if val == nil then + val = true + end + self.options[op] = val +end + +---@param key string | "'type'" | "'list'" +---@param func function | "function (path) end" +function mt:setInterface(key, func) + if type(func) ~= 'function' then + return + end + self.interface[key] = func +end + +function mt:callInterface(name, ...) + local func = self.interface[name] + return func(...) +end + +function mt:hasInterface(name) + return self.interface[name] ~= nil +end + +function mt:checkDirectory(catch, path, matcher) + if not self:hasInterface 'type' then + return true + end + if not matcher:isNeedDirectory() then + return true + end + if #catch < #path then + -- if path is 'a/b/c' and catch is 'a/b' + -- then the catch must be a directory + return true + else + return self:callInterface('type', path) == 'directory' + end +end + +function mt:simpleMatch(path) + for i = #self.matcher, 1, -1 do + local matcher = self.matcher[i] + local catch = matcher(path) + if catch and self:checkDirectory(catch, path, matcher) then + if matcher:isNegative() then + return false + else + return true + end + end + end +end + +function mt:finishMatch(path) + local paths = {} + for filename in path:gmatch '[^/\\]+' do + paths[#paths+1] = filename + end + for i = 1, #paths do + local newPath = table.concat(paths, '/', 1, i) + local passed = self:simpleMatch(newPath) + if passed == true then + return true + elseif passed == false then + return false + end + end + return false +end + +function mt:scan(callback) + local files = {} + if type(callback) ~= 'function' then + callback = nil + end + local list = {} + local result = self:callInterface('list', '') + if type(result) ~= 'table' then + return files + end + for _, path in ipairs(result) do + list[#list+1] = path + end + while #list > 0 do + local current = list[#list] + if not current then + break + end + list[#list] = nil + if not self:simpleMatch(current) then + local fileType = self:callInterface('type', current) + if fileType == 'file' then + if callback then + callback(current) + end + files[#files+1] = current + elseif fileType == 'directory' then + local result = self:callInterface('list', current) + if type(result) == 'table' then + for _, path in ipairs(result) do + local filename = path:match '([^/\\]+)[/\\]*$' + if filename then + list[#list+1] = current .. '\\' .. filename + end + end + end + end + end + end + table.sort(files) + return files +end + +function mt:__call(path) + if self.options.ignoreCase then + path = path:lower() + end + return self:finishMatch(path) +end + +return function (pattern, options, interface) + local self = setmetatable({ + pattern = {}, + options = {}, + matcher = {}, + errors = {}, + interface = {}, + }, mt) + + if type(pattern) == 'table' then + for _, pat in ipairs(pattern) do + self:addPattern(pat) + end + else + self:addPattern(pattern) + end + + if type(options) == 'table' then + for op, val in pairs(options) do + self:setOption(op, val) + end + end + + if type(interface) == 'table' then + for key, func in pairs(interface) do + self:setInterface(key, func) + end + end + + return self +end diff --git a/server/src/glob/glob.lua b/server/src/glob/glob.lua new file mode 100644 index 00000000..8c82d186 --- /dev/null +++ b/server/src/glob/glob.lua @@ -0,0 +1,120 @@ +local m = require 'lpeglabel' +local matcher = require 'glob.matcher' + +local function prop(name, pat) + return m.Cg(m.Cc(true), name) * pat +end + +local function object(type, pat) + return m.Ct( + m.Cg(m.Cc(type), 'type') * + m.Cg(pat, 'value') + ) +end + +local function expect(p, err) + return p + m.T(err) +end + +local parser = m.P { + 'Main', + ['Sp'] = m.S(' \t')^0, + ['Slash'] = m.S('/\\')^1, + ['Main'] = m.Ct(m.V'Sp' * m.P'{' * m.V'Pattern' * (',' * expect(m.V'Pattern', 'Miss exp after ","'))^0 * m.P'}') + + m.Ct(m.V'Pattern') + + m.T'Main Failed' + , + ['Pattern'] = m.Ct(m.V'Sp' * prop('neg', m.P'!') * expect(m.V'Unit', 'Miss exp after "!"')) + + m.Ct(m.V'Unit') + , + ['NeedRoot'] = prop('root', (m.P'.' * m.V'Slash' + m.V'Slash')), + ['Unit'] = m.V'Sp' * m.V'NeedRoot'^-1 * expect(m.V'Exp', 'Miss exp') * m.V'Sp', + ['Exp'] = m.V'Sp' * (m.V'FSymbol' + object('/', m.V'Slash') + m.V'Word')^0 * m.V'Sp', + ['Word'] = object('word', m.Ct((m.V'CSymbol' + m.V'Char' - m.V'FSymbol')^1)), + ['CSymbol'] = object('*', m.P'*') + + object('?', m.P'?') + + object('[]', m.V'Range') + , + ['Char'] = object('char', (1 - m.S',{}[]*?/\\')^1), + ['FSymbol'] = object('**', m.P'**'), + ['Range'] = m.P'[' * m.Ct(m.V'RangeUnit'^0) * m.P']'^-1, + ['RangeUnit'] = m.Ct(- m.P']' * m.C(m.P(1)) * (m.P'-' * - m.P']' * m.C(m.P(1)))^-1), +} + +local mt = {} +mt.__index = mt +mt.__name = 'glob' + +function mt:addPattern(pat) + if type(pat) ~= 'string' then + return + end + self.pattern[#self.pattern+1] = pat + if self.options.ignoreCase then + pat = pat:lower() + end + local states, err = parser:match(pat) + if not states then + self.errors[#self.errors+1] = { + pattern = pat, + message = err + } + return + end + for _, state in ipairs(states) do + if state.neg then + self.refused[#self.refused+1] = matcher(state) + else + self.passed[#self.passed+1] = matcher(state) + end + end +end + +function mt:setOption(op, val) + if val == nil then + val = true + end + self.options[op] = val +end + +function mt:__call(path) + if self.options.ignoreCase then + path = path:lower() + end + for _, refused in ipairs(self.refused) do + if refused(path) then + return false + end + end + for _, passed in ipairs(self.passed) do + if passed(path) then + return true + end + end + return false +end + +return function (pattern, options) + local self = setmetatable({ + pattern = {}, + options = {}, + passed = {}, + refused = {}, + errors = {}, + }, mt) + + if type(pattern) == 'table' then + for _, pat in ipairs(pattern) do + self:addPattern(pat) + end + else + self:addPattern(pattern) + end + + if type(options) == 'table' then + for op, val in pairs(options) do + self:setOption(op, val) + end + end + return self +end diff --git a/server/src/glob/init.lua b/server/src/glob/init.lua new file mode 100644 index 00000000..6578a0d4 --- /dev/null +++ b/server/src/glob/init.lua @@ -0,0 +1,4 @@ +return { + glob = require 'glob.glob', + gitignore = require 'glob.gitignore', +} diff --git a/server/src/glob/matcher.lua b/server/src/glob/matcher.lua new file mode 100644 index 00000000..2e07cf95 --- /dev/null +++ b/server/src/glob/matcher.lua @@ -0,0 +1,146 @@ +local m = require 'lpeglabel' + +local Slash = m.S('/\\')^1 +local Symbol = m.S',{}[]*?/\\' +local Char = 1 - Symbol +local Path = Char^1 * Slash +local NoWord = #(m.P(-1) + Symbol) +local function whatHappened() + return m.Cmt(m.P(1)^1, function (...) + print(...) + end) +end + +local mt = {} +mt.__index = mt +mt.__name = 'matcher' + +function mt:exp(state, index) + local exp = state[index] + if not exp then + return + end + if exp.type == 'word' then + return self:word(exp, state, index + 1) + elseif exp.type == 'char' then + return self:char(exp, state, index + 1) + elseif exp.type == '**' then + return self:anyPath(exp, state, index + 1) + elseif exp.type == '*' then + return self:anyChar(exp, state, index + 1) + elseif exp.type == '?' then + return self:oneChar(exp, state, index + 1) + elseif exp.type == '[]' then + return self:range(exp, state, index + 1) + elseif exp.type == '/' then + return self:slash(exp, state, index + 1) + end +end + +function mt:word(exp, state, index) + local current = self:exp(exp.value, 1) + local after = self:exp(state, index) + if after then + return current * Slash * after + else + return current + end +end + +function mt:char(exp, state, index) + local current = m.P(exp.value) + local after = self:exp(state, index) + if after then + return current * after * NoWord + else + return current * NoWord + end +end + +function mt:anyPath(_, state, index) + local after = self:exp(state, index) + if after then + return m.P { + 'Main', + Main = after + + Path * m.V'Main' + } + else + return Path^0 + end +end + +function mt:anyChar(_, state, index) + local after = self:exp(state, index) + if after then + return m.P { + 'Main', + Main = after + + Char * m.V'Main' + } + else + return Char^0 + end +end + +function mt:oneChar(_, state, index) + local after = self:exp(state, index) + if after then + return Char * after + else + return Char + end +end + +function mt:range(exp, state, index) + local after = self:exp(state, index) + local ranges = {} + for _, range in ipairs(exp.value) do + ranges[#ranges+1] = range[1] .. range[2] + end + local current = m.R(table.unpack(ranges)) + if after then + return current * after + else + return current + end +end + +function mt:slash(_, state, index) + local after = self:exp(state, index) + if after then + return after + else + self.needDirectory = true + return nil + end +end + +function mt:pattern(state) + if state.root then + return m.C(self:exp(state, 1)) + else + return m.C(self:anyPath(nil, state, 1)) + end +end + +function mt:isNeedDirectory() + return self.needDirectory == true +end + +function mt:isNegative() + return self.state.neg == true +end + +function mt:__call(path) + return self.matcher:match(path) +end + +return function (state, options) + local self = setmetatable({ + options = options, + state = state, + }, mt) + self.matcher = self:pattern(state) + return self +end diff --git a/server/src/path_filter.lua b/server/src/path_filter.lua deleted file mode 100644 index 403c938c..00000000 --- a/server/src/path_filter.lua +++ /dev/null @@ -1,72 +0,0 @@ -local m = require 'lpeglabel' - -local m_cut = m.S'\\' -local m_path = (1-m_cut)^1 - -local function match_any(p, pass) - return m.P{p + pass * m.V(1)} -end - -local function compile_format(fmt) - local function next(cur) - local pos, fn = fmt:find('%*+', cur) - if not pos then - return m.P(fmt:sub(cur)) * -1 - end - local word = m.P(fmt:sub(cur, pos-1)) - return word * match_any(next(fn+1), 1) - end - return next(1) -end - -local function compile_exp(exp) - exp = exp:gsub('/', '\\') - if exp:sub(1, 1) == '\\' then - exp = exp:sub(2) - else - exp = '**\\' .. exp - end - if exp:sub(-1) == '\\' then - exp = exp:sub(1, -2) - end - local function next(cur) - local pos, fn = exp:find('[\\]+', cur) - if not pos then - return compile_format(exp:sub(cur)) - end - local fmt = exp:sub(cur, pos-1) - if fmt == '**' then - return match_any(next(fn+1), m_path * m_cut) - elseif fmt == '' then - return m_cut - else - if fn < #exp then - return m.P(fmt) * m_cut * next(fn+1) - else - return m.P(fmt) * m_cut - end - end - end - return next(1) -end - -local function compile_exps(exp) - local matcher - for _, exp in ipairs(exp) do - exp = exp:lower() - if matcher then - matcher = matcher + compile_exp(exp) - else - matcher = compile_exp(exp) - end - end - return matcher -end - -return function (exp) - local matcher = compile_exps(exp) - return function (path) - local filename = path:lower() - return not not matcher:match(filename) - end -end diff --git a/server/src/workspace/workspace.lua b/server/src/workspace.lua index 6bd5c79e..c69b6399 100644 --- a/server/src/workspace/workspace.lua +++ b/server/src/workspace.lua @@ -109,49 +109,70 @@ function mt:listenLoadFile() end) end -function mt:scanFiles() - if self._scanRequest then - log.info('中断上次扫描文件任务') - self._scanRequest:push('stop') - self._scanRequest = nil - self._complete = false - self:reset() - end +function mt:buildScanPattern() + local pattern = {} - local ignored = {'.git'} + -- config.workspace.ignoreDir for path in pairs(config.config.workspace.ignoreDir) do - ignored[#ignored+1] = path + pattern[#pattern+1] = path end + -- config.files.exclude for path, ignore in pairs(config.other.exclude) do if ignore then - ignored[#ignored+1] = path + 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) - ignored[#ignored+1] = 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 - ignored[#ignored+1] = line + 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 - log.info('忽略文件:\r\n' .. table.concat(ignored, '\r\n')) + 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(), - ignored = ignored, + pattern = pattern, + options = { + ignoreCase = platform.OS == 'Windows', + } }, function (mode, ...) if mode == 'ok' then log.info('扫描文件任务完成,共', count, '个文件。') @@ -163,9 +184,6 @@ function mt:scanFiles() log.debug(...) elseif mode == 'path' then local path = fs.path(...) - if not self:isLuaFile(path) then - return - end self._loadFileRequest:push(path:string()) count = count + 1 elseif mode == 'stop' then diff --git a/server/src/workspace/init.lua b/server/src/workspace/init.lua deleted file mode 100644 index fa8bc6d9..00000000 --- a/server/src/workspace/init.lua +++ /dev/null @@ -1 +0,0 @@ -return require 'workspace.workspace' |