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 ---@field visibleCache table ---@field requireCache 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 uri = furi.encode(path) 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) local result = {} for _, searcher in ipairs(searchers) do local isAbsolute = searcher:match '^[/\\]' or searcher:match '^%a+%:' searcher = workspace.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 = workspace.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 function mt:searchUrisByRequireName(name) 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 = {} for _, searcher in ipairs(searchers) do local fspath = searcher:gsub('%?', (path:gsub('%%', '%%%%'))) fspath = workspace.normalize(fspath) local tail = '/' .. furi.encode(fspath):gsub('^file:[/]*', '') for uri in files.eachFile(self.scp.uri) do if not searcherMap[uri] and util.stringEndWith(uri, tail) 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] = workspace.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? 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[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 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 files.watch(function (ev, uri) if ev == 'create' or ev == 'delete' then for _, scp in ipairs(workspace.folders) do scp:set('requireManager', nil) end scope.fallback:set('requireManager', nil) end 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