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

300 lines
9.5 KiB
Lua
Raw Normal View History

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 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<uri, string>
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<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[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
---@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 = workspace.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