332 lines
10 KiB
Lua
332 lines
10 KiB
Lua
|
local platform = require 'bee.platform'
|
|||
|
local files = require 'files'
|
|||
|
local furi = require 'file-uri'
|
|||
|
local workspace = require "workspace"
|
|||
|
local config = require 'config'
|
|||
|
local scope = require 'workspace.scope'
|
|||
|
local util = require 'utility'
|
|||
|
|
|||
|
---@class require-path
|
|||
|
local m = {}
|
|||
|
|
|||
|
---@class require-manager
|
|||
|
---@field scp scope
|
|||
|
---@field nameMap table<string, string>
|
|||
|
---@field visibleCache table<string, require-manager.visibleResult[]>
|
|||
|
---@field requireCache table<string, table>
|
|||
|
local mt = {}
|
|||
|
mt.__index = mt
|
|||
|
|
|||
|
---@alias require-manager.visibleResult { searcher: string, name: string }
|
|||
|
|
|||
|
---@param scp scope
|
|||
|
---@return require-manager
|
|||
|
local function createRequireManager(scp)
|
|||
|
return setmetatable({
|
|||
|
scp = scp,
|
|||
|
nameMap = {},
|
|||
|
visibleCache = {},
|
|||
|
requireCache = {},
|
|||
|
}, mt)
|
|||
|
end
|
|||
|
|
|||
|
--- `aaa/bbb/ccc.lua` 与 `?.lua` 将返回 `aaa.bbb.cccc`
|
|||
|
---@param path string
|
|||
|
---@param searcher string
|
|||
|
---@return string?
|
|||
|
function mt:getRequireNameByPath(path, searcher)
|
|||
|
local separator = config.get(self.scp.uri, 'Lua.completion.requireSeparator')
|
|||
|
local stemPath = path
|
|||
|
: gsub('%.[^%.]+$', '')
|
|||
|
: gsub('[/\\%.]+', separator)
|
|||
|
local stemSearcher = searcher
|
|||
|
: gsub('%.[^%.]+$', '')
|
|||
|
: gsub('[/\\]+', separator)
|
|||
|
local start = stemSearcher:match '()%?' or 1
|
|||
|
if stemPath:sub(1, start - 1) ~= stemSearcher:sub(1, start - 1) then
|
|||
|
return nil
|
|||
|
end
|
|||
|
for pos = #stemPath, start, -1 do
|
|||
|
local word = stemPath:sub(start, pos)
|
|||
|
local newSearcher = stemSearcher:gsub('%?', (word:gsub('%%', '%%%%')))
|
|||
|
if newSearcher == stemPath then
|
|||
|
return word
|
|||
|
end
|
|||
|
end
|
|||
|
return nil
|
|||
|
end
|
|||
|
|
|||
|
---@param path string
|
|||
|
---@return require-manager.visibleResult[]
|
|||
|
function mt:getRequireResultByPath(path)
|
|||
|
local vm = require 'vm'
|
|||
|
local uri = furi.encode(path)
|
|||
|
local result = {}
|
|||
|
if vm.isMetaFile(uri) then
|
|||
|
local metaName = vm.getMetaName(uri)
|
|||
|
if metaName then
|
|||
|
if vm.isMetaFileRequireable(uri) then
|
|||
|
result[#result+1] = {
|
|||
|
name = metaName,
|
|||
|
searcher = '[[meta]]',
|
|||
|
}
|
|||
|
end
|
|||
|
return result
|
|||
|
end
|
|||
|
end
|
|||
|
local searchers = config.get(self.scp.uri, 'Lua.runtime.path')
|
|||
|
local strict = config.get(self.scp.uri, 'Lua.runtime.pathStrict')
|
|||
|
local libUri = files.getLibraryUri(self.scp.uri, uri)
|
|||
|
local libraryPath = libUri and furi.decode(libUri)
|
|||
|
for _, searcher in ipairs(searchers) do
|
|||
|
local isAbsolute = searcher:match '^[/\\]'
|
|||
|
or searcher:match '^%a+%:'
|
|||
|
searcher = files.normalize(searcher)
|
|||
|
if searcher:sub(1, 1) == '.' then
|
|||
|
strict = true
|
|||
|
end
|
|||
|
local cutedPath = path
|
|||
|
local currentPath = path
|
|||
|
local head
|
|||
|
local pos = 1
|
|||
|
if not isAbsolute then
|
|||
|
if libraryPath then
|
|||
|
currentPath = currentPath:sub(#libraryPath + 2)
|
|||
|
else
|
|||
|
currentPath = workspace.getRelativePath(uri)
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
-- handle `../?.lua`
|
|||
|
local parentCount = 0
|
|||
|
for _ = 1, 1000 do
|
|||
|
if searcher:match '^%.%.[/\\]' then
|
|||
|
parentCount = parentCount + 1
|
|||
|
searcher = searcher:sub(4)
|
|||
|
else
|
|||
|
break
|
|||
|
end
|
|||
|
end
|
|||
|
if parentCount > 0 then
|
|||
|
local parentPath = libraryPath
|
|||
|
or (self.scp.uri and furi.decode(self.scp.uri))
|
|||
|
if parentPath then
|
|||
|
local tail
|
|||
|
for _ = 1, parentCount do
|
|||
|
parentPath, tail = parentPath:match '^(.+)[/\\]([^/\\]*)$'
|
|||
|
currentPath = tail .. '/' .. currentPath
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
repeat
|
|||
|
cutedPath = currentPath:sub(pos)
|
|||
|
head = currentPath:sub(1, pos - 1)
|
|||
|
pos = currentPath:match('[/\\]+()', pos)
|
|||
|
if platform.OS == 'Windows' then
|
|||
|
searcher = searcher :gsub('[/\\]+', '\\')
|
|||
|
else
|
|||
|
searcher = searcher :gsub('[/\\]+', '/')
|
|||
|
end
|
|||
|
local name = self:getRequireNameByPath(cutedPath, searcher)
|
|||
|
if name then
|
|||
|
local mySearcher = searcher
|
|||
|
if head then
|
|||
|
mySearcher = head .. searcher
|
|||
|
end
|
|||
|
result[#result+1] = {
|
|||
|
name = name,
|
|||
|
searcher = mySearcher,
|
|||
|
}
|
|||
|
end
|
|||
|
until not pos or strict
|
|||
|
end
|
|||
|
return result
|
|||
|
end
|
|||
|
|
|||
|
---@param name string
|
|||
|
function mt:addName(name)
|
|||
|
local separator = config.get(self.scp.uri, 'Lua.completion.requireSeparator')
|
|||
|
local fsname = name:gsub('%' .. separator, '/')
|
|||
|
self.nameMap[fsname] = name
|
|||
|
end
|
|||
|
|
|||
|
---@return require-manager.visibleResult[]
|
|||
|
function mt:getVisiblePath(path)
|
|||
|
local uri = furi.encode(path)
|
|||
|
if not self.scp:isChildUri(uri)
|
|||
|
and not self.scp:isLinkedUri(uri) then
|
|||
|
return {}
|
|||
|
end
|
|||
|
path = files.normalize(path)
|
|||
|
local result = self.visibleCache[path]
|
|||
|
if not result then
|
|||
|
result = self:getRequireResultByPath(path)
|
|||
|
self.visibleCache[path] = result
|
|||
|
end
|
|||
|
return result
|
|||
|
end
|
|||
|
|
|||
|
--- 查找符合指定require name的所有uri
|
|||
|
---@param name string
|
|||
|
---@return uri[]
|
|||
|
---@return table<uri, string>?
|
|||
|
function mt:searchUrisByRequireName(name)
|
|||
|
local vm = require 'vm'
|
|||
|
local searchers = config.get(self.scp.uri, 'Lua.runtime.path')
|
|||
|
local strict = config.get(self.scp.uri, 'Lua.runtime.pathStrict')
|
|||
|
local separator = config.get(self.scp.uri, 'Lua.completion.requireSeparator')
|
|||
|
local path = name:gsub('%' .. separator, '/')
|
|||
|
local results = {}
|
|||
|
local searcherMap = {}
|
|||
|
local excludes = {}
|
|||
|
|
|||
|
for uri in files.eachFile(self.scp.uri) do
|
|||
|
if vm.isMetaFileRequireable(uri) then
|
|||
|
local metaName = vm.getMetaName(uri)
|
|||
|
if metaName == name then
|
|||
|
results[#results+1] = uri
|
|||
|
return results
|
|||
|
end
|
|||
|
if metaName then
|
|||
|
excludes[uri] = true
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
for _, searcher in ipairs(searchers) do
|
|||
|
local fspath = searcher:gsub('%?', (path:gsub('%%', '%%%%')))
|
|||
|
fspath = files.normalize(fspath)
|
|||
|
local tail = '/' .. furi.encode(fspath):gsub('^file:[/]*', '')
|
|||
|
for uri in files.eachFile(self.scp.uri) do
|
|||
|
if not searcherMap[uri]
|
|||
|
and not excludes[uri]
|
|||
|
and util.stringEndWith(uri, tail)
|
|||
|
and (not vm.isMetaFile(uri) or vm.isMetaFileRequireable(uri)) then
|
|||
|
local parentUri = files.getLibraryUri(self.scp.uri, uri) or self.scp.uri
|
|||
|
if parentUri == nil or parentUri == '' then
|
|||
|
parentUri = furi.encode '/'
|
|||
|
end
|
|||
|
local relative = uri:sub(#parentUri + 1):sub(1, - #tail)
|
|||
|
if not strict
|
|||
|
or relative == '/'
|
|||
|
or relative == '' then
|
|||
|
results[#results+1] = uri
|
|||
|
searcherMap[uri] = files.normalize(relative .. searcher)
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
for uri in files.eachDll() do
|
|||
|
local opens = files.getDllOpens(uri) or {}
|
|||
|
for _, open in ipairs(opens) do
|
|||
|
if open == path then
|
|||
|
results[#results+1] = uri
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
|
|||
|
return results, searcherMap
|
|||
|
end
|
|||
|
|
|||
|
--- 查找符合指定require name的所有uri,并排除当前文件
|
|||
|
---@param suri uri
|
|||
|
---@param name string
|
|||
|
---@return uri[]
|
|||
|
---@return table<uri, string>?
|
|||
|
function mt:findUrisByRequireName(suri, name)
|
|||
|
if type(name) ~= 'string' then
|
|||
|
return {}
|
|||
|
end
|
|||
|
local cache = self.requireCache[name]
|
|||
|
if not cache then
|
|||
|
local results, searcherMap = self:searchUrisByRequireName(name)
|
|||
|
cache = {
|
|||
|
results = results,
|
|||
|
searcherMap = searcherMap,
|
|||
|
}
|
|||
|
self.requireCache[name] = cache
|
|||
|
end
|
|||
|
local results = {}
|
|||
|
local searcherMap = {}
|
|||
|
for _, uri in ipairs(cache.results) do
|
|||
|
if uri ~= suri then
|
|||
|
results[#results+1] = uri
|
|||
|
searcherMap[uri] = cache.searcherMap and cache.searcherMap[uri]
|
|||
|
end
|
|||
|
end
|
|||
|
return results, searcherMap
|
|||
|
end
|
|||
|
|
|||
|
---@param uri uri
|
|||
|
---@param path string
|
|||
|
---@return require-manager.visibleResult[]
|
|||
|
function m.getVisiblePath(uri, path)
|
|||
|
local scp = scope.getScope(uri)
|
|||
|
---@type require-manager
|
|||
|
local mgr = scp:get 'requireManager'
|
|||
|
or scp:set('requireManager', createRequireManager(scp))
|
|||
|
return mgr:getVisiblePath(path)
|
|||
|
end
|
|||
|
|
|||
|
---@param uri uri
|
|||
|
---@param name string
|
|||
|
---@return uri[]
|
|||
|
---@return table<uri, string>?
|
|||
|
function m.findUrisByRequireName(uri, name)
|
|||
|
local scp = scope.getScope(uri)
|
|||
|
---@type require-manager
|
|||
|
local mgr = scp:get 'requireManager'
|
|||
|
or scp:set('requireManager', createRequireManager(scp))
|
|||
|
return mgr:findUrisByRequireName(uri, name)
|
|||
|
end
|
|||
|
|
|||
|
---@param suri uri
|
|||
|
---@param uri uri
|
|||
|
---@param name string
|
|||
|
---@return boolean
|
|||
|
function m.isMatchedUri(suri, uri, name)
|
|||
|
local searchers = config.get(suri, 'Lua.runtime.path')
|
|||
|
local strict = config.get(suri, 'Lua.runtime.pathStrict')
|
|||
|
local separator = config.get(suri, 'Lua.completion.requireSeparator')
|
|||
|
local path = name:gsub('%' .. separator, '/')
|
|||
|
|
|||
|
for _, searcher in ipairs(searchers) do
|
|||
|
local fspath = searcher:gsub('%?', (path:gsub('%%', '%%%%')))
|
|||
|
fspath = files.normalize(fspath)
|
|||
|
local tail = '/' .. furi.encode(fspath):gsub('^file:[/]*', '')
|
|||
|
if util.stringEndWith(uri, tail) then
|
|||
|
local parentUri = files.getLibraryUri(suri, uri) or uri
|
|||
|
if parentUri == nil or parentUri == '' then
|
|||
|
parentUri = furi.encode '/'
|
|||
|
end
|
|||
|
local relative = uri:sub(#parentUri + 1):sub(1, - #tail)
|
|||
|
if not strict
|
|||
|
or relative == '/'
|
|||
|
or relative == '' then
|
|||
|
return true
|
|||
|
end
|
|||
|
end
|
|||
|
end
|
|||
|
return false
|
|||
|
end
|
|||
|
|
|||
|
files.watch(function (ev, uri)
|
|||
|
for _, scp in ipairs(workspace.folders) do
|
|||
|
scp:set('requireManager', nil)
|
|||
|
end
|
|||
|
scope.fallback:set('requireManager', nil)
|
|||
|
end)
|
|||
|
|
|||
|
config.watch(function (uri, key, value, oldValue)
|
|||
|
if key == 'Lua.completion.requireSeparator'
|
|||
|
or key == 'Lua.runtime.path'
|
|||
|
or key == 'Lua.runtime.pathStrict' then
|
|||
|
local scp = scope.getScope(uri)
|
|||
|
scp:set('requireManager', nil)
|
|||
|
end
|
|||
|
end)
|
|||
|
|
|||
|
return m
|