summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/src/async/scanfiles.lua64
-rw-r--r--server/src/glob/gitignore.lua222
-rw-r--r--server/src/glob/glob.lua120
-rw-r--r--server/src/glob/init.lua4
-rw-r--r--server/src/glob/matcher.lua146
-rw-r--r--server/src/path_filter.lua72
-rw-r--r--server/src/workspace.lua (renamed from server/src/workspace/workspace.lua)54
-rw-r--r--server/src/workspace/init.lua1
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'