nixos/lua-lsp/script/workspace/workspace.lua

605 lines
16 KiB
Lua
Raw Normal View History

local pub = require 'pub'
local fs = require 'bee.filesystem'
local furi = require 'file-uri'
local files = require 'files'
local config = require 'config'
local glob = require 'glob'
local platform = require 'bee.platform'
local await = require 'await'
local client = require 'client'
local util = require 'utility'
local fw = require 'filewatch'
local scope = require 'workspace.scope'
local loading = require 'workspace.loading'
local inspect = require 'inspect'
local lang = require 'language'
---@class workspace
local m = {}
m.type = 'workspace'
m.watchList = {}
--- 注册事件
---@param callback async fun(ev: string, uri: uri)
function m.watch(callback)
m.watchList[#m.watchList+1] = callback
end
function m.onWatch(ev, uri)
for _, callback in ipairs(m.watchList) do
await.call(function ()
callback(ev, uri)
end)
end
end
function m.initRoot(uri)
m.rootUri = uri
log.info('Workspace init root: ', uri)
local logPath = fs.path(LOGPATH) / (uri:gsub('[/:]+', '_') .. '.log')
client.logMessage('Log', 'Log path: ' .. furi.encode(logPath:string()))
log.info('Log path: ', logPath)
log.init(ROOT, logPath)
end
--- 初始化工作区
function m.create(uri)
if furi.isValid(uri) then
uri = furi.normalize(uri)
end
log.info('Workspace create: ', uri)
local scp = scope.createFolder(uri)
m.folders[#m.folders+1] = scp
if uri == furi.encode '/'
or uri == furi.encode(os.getenv 'HOME' or '') then
client.showMessage('Error', lang.script('WORKSPACE_NOT_ALLOWED', furi.decode(uri)))
scp:set('bad root', true)
end
end
function m.remove(uri)
log.info('Workspace remove: ', uri)
for i, scp in ipairs(m.folders) do
if scp.uri == uri then
scp:remove()
table.remove(m.folders, i)
scp:set('ready', false)
scp:set('nativeMatcher', nil)
scp:set('libraryMatcher', nil)
scp:removeAllLinks()
m.flushFiles(scp)
return
end
end
end
function m.reset()
---@type scope[]
m.folders = {}
m.rootUri = nil
end
m.reset()
function m.getRootUri(uri)
local scp = scope.getScope(uri)
return scp.uri
end
local globInteferFace = {
type = function (path)
local result
pcall(function ()
if fs.is_directory(path) then
result = 'directory'
else
result = 'file'
end
end)
return result
end,
list = function (path)
local fullPath = fs.path(path)
if not fs.is_directory(fullPath) then
return nil
end
local paths = {}
pcall(function ()
for fullpath in fs.pairs(fullPath) do
paths[#paths+1] = fullpath:string()
end
end)
return paths
end
}
--- 创建排除文件匹配器
---@param scp scope
function m.getNativeMatcher(scp)
if scp:get 'nativeMatcher' then
return scp:get 'nativeMatcher'
end
local pattern = {}
for path, ignore in pairs(config.get(scp.uri, 'files.exclude')) do
if ignore then
log.debug('Ignore by exclude:', path)
pattern[#pattern+1] = path
end
end
if scp.uri and config.get(scp.uri, 'Lua.workspace.useGitIgnore') then
local buf = util.loadFile(furi.decode(scp.uri) .. '/.gitignore')
if buf then
for line in buf:gmatch '[^\r\n]+' do
if line:sub(1, 1) ~= '#' then
log.debug('Ignore by .gitignore:', line)
pattern[#pattern+1] = line
end
end
end
buf = util.loadFile(furi.decode(scp.uri).. '/.git/info/exclude')
if buf then
for line in buf:gmatch '[^\r\n]+' do
if line:sub(1, 1) ~= '#' then
log.debug('Ignore by .git/info/exclude:', line)
pattern[#pattern+1] = line
end
end
end
end
if scp.uri and config.get(scp.uri, 'Lua.workspace.ignoreSubmodules') then
local buf = util.loadFile(furi.decode(scp.uri) .. '/.gitmodules')
if buf then
for path in buf:gmatch('path = ([^\r\n]+)') do
log.debug('Ignore by .gitmodules:', path)
pattern[#pattern+1] = path
end
end
end
for _, path in ipairs(config.get(scp.uri, 'Lua.workspace.library')) do
path = m.getAbsolutePath(scp.uri, path)
if path then
log.debug('Ignore by library:', path)
debug[#pattern+1] = path
end
end
for _, path in ipairs(config.get(scp.uri, 'Lua.workspace.ignoreDir')) do
log.debug('Ignore directory:', path)
pattern[#pattern+1] = path
end
local matcher = glob.gitignore(pattern, {
root = scp.uri and furi.decode(scp.uri),
ignoreCase = platform.OS == 'Windows',
}, globInteferFace)
scp:set('nativeMatcher', matcher)
return matcher
end
--- 创建代码库筛选器
---@param scp scope
function m.getLibraryMatchers(scp)
if scp:get 'libraryMatcher' then
return scp:get 'libraryMatcher'
end
log.debug('Build library matchers:', scp)
local pattern = {}
for path, ignore in pairs(config.get(scp.uri, 'files.exclude')) do
if ignore then
log.debug('Ignore by exclude:', path)
pattern[#pattern+1] = path
end
end
for _, path in ipairs(config.get(scp.uri, 'Lua.workspace.ignoreDir')) do
log.debug('Ignore directory:', path)
pattern[#pattern+1] = path
end
local librarys = {}
for _, path in ipairs(config.get(scp.uri, 'Lua.workspace.library')) do
path = m.getAbsolutePath(scp.uri, path)
if path then
librarys[m.normalize(path)] = true
end
end
log.debug('meta path:', scp:get 'metaPath')
if scp:get 'metaPath' then
librarys[m.normalize(scp:get 'metaPath')] = true
end
local matchers = {}
for path in pairs(librarys) do
if fs.exists(fs.path(path)) then
local nPath = fs.absolute(fs.path(path)):string()
local matcher = glob.gitignore(pattern, {
root = path,
ignoreCase = platform.OS == 'Windows',
}, globInteferFace)
matchers[#matchers+1] = {
uri = furi.encode(nPath),
matcher = matcher
}
end
end
scp:set('libraryMatcher', matchers)
log.debug('library matcher:', inspect(matchers))
return matchers
end
--- 文件是否被忽略
---@async
---@param uri uri
function m.isIgnored(uri)
local scp = scope.getScope(uri)
local path = m.getRelativePath(uri)
local ignore = m.getNativeMatcher(scp)
if not ignore then
return false
end
return ignore(path)
end
---@async
function m.isValidLuaUri(uri)
if not files.isLua(uri) then
return false
end
if m.isIgnored(uri)
and not files.isLibrary(uri) then
return false
end
return true
end
---@async
function m.awaitLoadFile(uri)
m.awaitReady(uri)
local scp = scope.getScope(uri)
local ld <close> = loading.create(scp)
local native = m.getNativeMatcher(scp)
log.info('Scan files at:', uri)
---@async
native:scan(furi.decode(uri), function (path)
local uri = files.getRealUri(furi.encode(path))
scp:get('cachedUris')[uri] = true
ld:loadFile(uri)
end)
ld:loadAll(uri)
end
function m.removeFile(uri)
for _, scp in ipairs(m.folders) do
if scp:isChildUri(uri)
or scp:isLinkedUri(uri) then
local cachedUris = scp:get 'cachedUris'
if cachedUris and cachedUris[uri] then
cachedUris[uri] = nil
files.delRef(uri)
end
end
end
end
--- 预读工作区内所有文件
---@async
---@param scp scope
function m.awaitPreload(scp)
await.close('preload:' .. scp:getName())
await.setID('preload:' .. scp:getName())
await.sleep(0.1)
scp:flushGC()
if scp:isRemoved() then
return
end
local ld <close> = loading.create(scp)
scp:set('loading', ld)
log.info('Preload start:', scp:getName())
local native = m.getNativeMatcher(scp)
local librarys = m.getLibraryMatchers(scp)
if scp.uri and not scp:get('bad root') then
log.info('Scan files at:', scp:getName())
scp:gc(fw.watch(m.normalize(furi.decode(scp.uri))))
local count = 0
---@async
native:scan(furi.decode(scp.uri), function (path)
local uri = files.getRealUri(furi.encode(path))
scp:get('cachedUris')[uri] = true
ld:loadFile(uri)
end, function (_) ---@async
count = count + 1
if count == 100000 then
client.showMessage('Warning', lang.script('WORKSPACE_SCAN_TOO_MUCH', count, furi.decode(scp.uri)))
end
end)
end
for _, libMatcher in ipairs(librarys) do
log.info('Scan library at:', libMatcher.uri)
local count = 0
scp:gc(fw.watch(furi.decode(libMatcher.uri)))
scp:addLink(libMatcher.uri)
---@async
libMatcher.matcher:scan(furi.decode(libMatcher.uri), function (path)
local uri = files.getRealUri(furi.encode(path))
scp:get('cachedUris')[uri] = true
ld:loadFile(uri, libMatcher.uri)
end, function () ---@async
count = count + 1
if count == 100000 then
client.showMessage('Warning', lang.script('WORKSPACE_SCAN_TOO_MUCH', count, furi.decode(libMatcher.uri)))
end
end)
end
-- must wait for other scopes to add library
await.sleep(0.1)
log.info(('Found %d files at:'):format(ld.max), scp:getName())
ld:loadAll(scp:getName())
log.info('Preload finish at:', scp:getName())
end
--- 查找符合指定file path的所有uri
---@param path string
function m.findUrisByFilePath(path)
if type(path) ~= 'string' then
return {}
end
local myUri = furi.encode(path)
local vm = require 'vm'
local resultCache = vm.getCache 'findUrisByFilePath.result'
if resultCache[path] then
return resultCache[path]
end
local results = {}
for uri in files.eachFile() do
if uri == myUri then
results[#results+1] = uri
end
end
resultCache[path] = results
return results
end
---@param path string
---@return string
function m.normalize(path)
path = path:gsub('%$%{(.-)%}', function (key)
if key == '3rd' then
return (ROOT / 'meta' / '3rd'):string()
end
if key:sub(1, 4) == 'env:' then
local env = os.getenv(key:sub(5))
return env
end
end)
path = util.expandPath(path)
path = path:gsub('^%.[/\\]+', '')
for _ = 1, 1000 do
if path:sub(1, 2) == '..' then
break
end
local count
path, count = path:gsub('[^/\\]+[/\\]+%.%.[/\\]', '/', 1)
if count == 0 then
break
end
end
if platform.OS == 'Windows' then
path = path:gsub('[/\\]+', '\\')
:gsub('[/\\]+$', '')
:gsub('^(%a:)$', '%1\\')
else
path = path:gsub('[/\\]+', '/')
:gsub('[/\\]+$', '')
end
return path
end
---@param folderUri? uri
---@param path string
---@return string?
function m.getAbsolutePath(folderUri, path)
path = m.normalize(path)
if fs.path(path):is_relative() then
if not folderUri then
return nil
end
local folderPath = furi.decode(folderUri)
path = m.normalize(folderPath .. '/' .. path)
end
return path
end
---@param uriOrPath uri|string
---@return string
---@return boolean suc
function m.getRelativePath(uriOrPath)
local path, uri
if uriOrPath:sub(1, 5) == 'file:' then
path = furi.decode(uriOrPath)
uri = uriOrPath
else
path = uriOrPath
uri = furi.encode(uriOrPath)
end
local scp = scope.getScope(uri)
if not scp.uri then
local relative = m.normalize(path)
return relative:gsub('^[/\\]+', ''), false
end
local _, pos = m.normalize(path):find(furi.decode(scp.uri), 1, true)
if pos then
return m.normalize(path:sub(pos + 1)):gsub('^[/\\]+', ''), true
else
return m.normalize(path):gsub('^[/\\]+', ''), false
end
end
---@param scp scope
function m.reload(scp)
---@async
await.call(function ()
m.awaitReload(scp)
end)
end
function m.init()
if m.rootUri then
for _, folder in ipairs(scope.folders) do
m.reload(folder)
end
end
m.reload(scope.fallback)
end
---@param scp scope
function m.flushFiles(scp)
local cachedUris = scp:get 'cachedUris'
scp:set('cachedUris', {})
if not cachedUris then
return
end
for uri in pairs(cachedUris) do
files.delRef(uri)
end
collectgarbage()
collectgarbage()
-- TODO: wait maillist
collectgarbage 'restart'
end
---@param scp scope
function m.resetFiles(scp)
local cachedUris = scp:get 'cachedUris'
if cachedUris then
for uri in pairs(cachedUris) do
files.resetText(uri)
end
end
for uri in pairs(files.openMap) do
if scope.getScope(uri) == scp then
files.resetText(uri)
end
end
end
---@async
---@param scp scope
function m.awaitReload(scp)
scp:set('ready', false)
scp:set('nativeMatcher', nil)
scp:set('libraryMatcher', nil)
scp:removeAllLinks()
m.flushFiles(scp)
m.onWatch('startReload', scp.uri)
m.awaitPreload(scp)
scp:set('ready', true)
local waiting = scp:get('waitingReady')
if waiting then
scp:set('waitingReady', nil)
for _, waker in ipairs(waiting) do
waker()
end
end
m.onWatch('reload', scp.uri)
end
---@return scope
function m.getFirstScope()
return m.folders[1] or scope.fallback
end
---等待工作目录加载完成
---@async
function m.awaitReady(uri)
if m.isReady(uri) then
return
end
local scp = scope.getScope(uri)
local waitingReady = scp:get('waitingReady')
or scp:set('waitingReady', {})
await.wait(function (waker)
waitingReady[#waitingReady+1] = waker
end)
end
---@param uri uri
function m.isReady(uri)
local scp = scope.getScope(uri)
return scp:get('ready') == true
end
function m.getLoadingProcess(uri)
local scp = scope.getScope(uri)
---@type workspace.loading
local ld = scp:get 'loading'
if ld then
return ld.read, ld.max
else
return 0, 0
end
end
config.watch(function (uri, key, value, oldValue)
if key:find '^Lua.runtime'
or key:find '^Lua.workspace'
or key:find '^Lua.type'
or key:find '^files' then
if value ~= oldValue then
local scp = scope.getScope(uri)
m.reload(scp)
m.resetFiles(scp)
end
end
end)
fw.event(function (ev, path) ---@async
local uri = furi.encode(path)
if ev == 'create' then
log.debug('FileChangeType.Created', uri)
m.awaitLoadFile(uri)
elseif ev == 'delete' then
log.debug('FileChangeType.Deleted', uri)
files.remove(uri)
m.removeFile(uri)
local childs = files.getChildFiles(uri)
for _, curi in ipairs(childs) do
log.debug('FileChangeType.Deleted.Child', curi)
files.remove(curi)
m.removeFile(uri)
end
elseif ev == 'change' then
if m.isValidLuaUri(uri) then
-- 如果文件处于关闭状态则立即更新否则等待didChange协议来更新
if not files.isOpen(uri) then
files.setText(uri, pub.awaitTask('loadFile', furi.decode(uri)), false)
end
end
end
local filename = fs.path(path):filename():string()
-- 排除类文件发生更改需要重新扫描
if filename == '.gitignore'
or filename == '.gitmodules' then
local scp = scope.getScope(uri)
if scp.type ~= 'fallback' then
m.reload(scp)
end
end
end)
return m