574 lines
16 KiB
Lua
574 lines
16 KiB
Lua
|
local fs = require 'bee.filesystem'
|
||
|
local plat = require 'bee.platform'
|
||
|
local config = require 'config'
|
||
|
local util = require 'utility'
|
||
|
local lang = require 'language'
|
||
|
local client = require 'client'
|
||
|
local lloader = require 'locale-loader'
|
||
|
local fsu = require 'fs-utility'
|
||
|
local define = require "proto.define"
|
||
|
local files = require 'files'
|
||
|
local await = require 'await'
|
||
|
local timer = require 'timer'
|
||
|
local encoder = require 'encoder'
|
||
|
local ws = require 'workspace.workspace'
|
||
|
local scope = require 'workspace.scope'
|
||
|
local inspect = require 'inspect'
|
||
|
|
||
|
local m = {}
|
||
|
|
||
|
m.metaPaths = {}
|
||
|
|
||
|
local function getDocFormater(uri)
|
||
|
local version = config.get(uri, 'Lua.runtime.version')
|
||
|
if client.isVSCode() then
|
||
|
if version == 'Lua 5.1' then
|
||
|
return 'HOVER_NATIVE_DOCUMENT_LUA51'
|
||
|
elseif version == 'Lua 5.2' then
|
||
|
return 'HOVER_NATIVE_DOCUMENT_LUA52'
|
||
|
elseif version == 'Lua 5.3' then
|
||
|
return 'HOVER_NATIVE_DOCUMENT_LUA53'
|
||
|
elseif version == 'Lua 5.4' then
|
||
|
return 'HOVER_NATIVE_DOCUMENT_LUA54'
|
||
|
elseif version == 'LuaJIT' then
|
||
|
return 'HOVER_NATIVE_DOCUMENT_LUAJIT'
|
||
|
end
|
||
|
else
|
||
|
if version == 'Lua 5.1' then
|
||
|
return 'HOVER_DOCUMENT_LUA51'
|
||
|
elseif version == 'Lua 5.2' then
|
||
|
return 'HOVER_DOCUMENT_LUA52'
|
||
|
elseif version == 'Lua 5.3' then
|
||
|
return 'HOVER_DOCUMENT_LUA53'
|
||
|
elseif version == 'Lua 5.4' then
|
||
|
return 'HOVER_DOCUMENT_LUA54'
|
||
|
elseif version == 'LuaJIT' then
|
||
|
return 'HOVER_DOCUMENT_LUAJIT'
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function convertLink(uri, text)
|
||
|
local fmt = getDocFormater(uri)
|
||
|
return text:gsub('%$([%.%w]+)', function (name)
|
||
|
local lastDot = ''
|
||
|
if name:sub(-1) == '.' then
|
||
|
name = name:sub(1, -2)
|
||
|
lastDot = '.'
|
||
|
end
|
||
|
if fmt then
|
||
|
return ('[%s](%s)'):format(name, lang.script(fmt, 'pdf-' .. name)) .. lastDot
|
||
|
else
|
||
|
return ('`%s`'):format(name) .. lastDot
|
||
|
end
|
||
|
end):gsub('§([%.%w]+)', function (name)
|
||
|
local lastDot = ''
|
||
|
if name:sub(-1) == '.' then
|
||
|
name = name:sub(1, -2)
|
||
|
lastDot = '.'
|
||
|
end
|
||
|
if fmt then
|
||
|
return ('[§%s](%s)'):format(name, lang.script(fmt, name)) .. lastDot
|
||
|
else
|
||
|
return ('`%s`'):format(name) .. lastDot
|
||
|
end
|
||
|
end)
|
||
|
end
|
||
|
|
||
|
local function createViewDocument(name)
|
||
|
local fmt = getDocFormater()
|
||
|
if not fmt then
|
||
|
return nil
|
||
|
end
|
||
|
name = name:match '[%w_%.]+'
|
||
|
if name:sub(-1) == '.' then
|
||
|
name = name:sub(1, -2)
|
||
|
end
|
||
|
return ('[%s](%s)'):format(lang.script.HOVER_VIEW_DOCUMENTS, lang.script(fmt, 'pdf-' .. name))
|
||
|
end
|
||
|
|
||
|
local function compileSingleMetaDoc(uri, script, metaLang, status)
|
||
|
if not script then
|
||
|
log.error('no meta?', uri)
|
||
|
return nil
|
||
|
end
|
||
|
|
||
|
local middleBuf = {}
|
||
|
local compileBuf = {}
|
||
|
|
||
|
local last = 1
|
||
|
for start, lua, finish in script:gmatch '()%-%-%-%#([^\n\r]*)()' do
|
||
|
middleBuf[#middleBuf+1] = ('PUSH [===[%s]===]'):format(script:sub(last, start - 1))
|
||
|
middleBuf[#middleBuf+1] = lua
|
||
|
last = finish
|
||
|
end
|
||
|
middleBuf[#middleBuf+1] = ('PUSH [===[%s]===]'):format(script:sub(last))
|
||
|
local middleScript = table.concat(middleBuf, '\n')
|
||
|
local version, jit
|
||
|
if config.get(uri, 'Lua.runtime.version') == 'LuaJIT' then
|
||
|
version = 5.1
|
||
|
jit = true
|
||
|
else
|
||
|
version = tonumber(config.get(uri, 'Lua.runtime.version'):sub(-3)) or 5.4
|
||
|
jit = false
|
||
|
end
|
||
|
|
||
|
local disable = false
|
||
|
local env = setmetatable({
|
||
|
VERSION = version,
|
||
|
JIT = jit,
|
||
|
PUSH = function (text)
|
||
|
compileBuf[#compileBuf+1] = text
|
||
|
end,
|
||
|
DES = function (name)
|
||
|
local des = metaLang[name]
|
||
|
if not des then
|
||
|
des = ('Miss locale <%s>'):format(name)
|
||
|
end
|
||
|
compileBuf[#compileBuf+1] = '---\n'
|
||
|
for line in util.eachLine(des) do
|
||
|
compileBuf[#compileBuf+1] = '---'
|
||
|
compileBuf[#compileBuf+1] = convertLink(uri, line)
|
||
|
compileBuf[#compileBuf+1] = '\n'
|
||
|
end
|
||
|
local viewDocument = createViewDocument(name)
|
||
|
if viewDocument then
|
||
|
compileBuf[#compileBuf+1] = '---\n---'
|
||
|
compileBuf[#compileBuf+1] = viewDocument
|
||
|
compileBuf[#compileBuf+1] = '\n'
|
||
|
end
|
||
|
compileBuf[#compileBuf+1] = '---\n'
|
||
|
end,
|
||
|
DESTAIL = function (name)
|
||
|
local des = metaLang[name]
|
||
|
if not des then
|
||
|
des = ('Miss locale <%s>'):format(name)
|
||
|
end
|
||
|
compileBuf[#compileBuf+1] = convertLink(uri, des)
|
||
|
compileBuf[#compileBuf+1] = '\n'
|
||
|
end,
|
||
|
ALIVE = function (str)
|
||
|
local isAlive
|
||
|
for piece in str:gmatch '[^%,]+' do
|
||
|
if piece:sub(1, 1) == '>' then
|
||
|
local alive = tonumber(piece:sub(2))
|
||
|
if not alive or version >= alive then
|
||
|
isAlive = true
|
||
|
break
|
||
|
end
|
||
|
elseif piece:sub(1, 1) == '<' then
|
||
|
local alive = tonumber(piece:sub(2))
|
||
|
if not alive or version <= alive then
|
||
|
isAlive = true
|
||
|
break
|
||
|
end
|
||
|
else
|
||
|
local alive = tonumber(piece)
|
||
|
if not alive or version == alive then
|
||
|
isAlive = true
|
||
|
break
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
if not isAlive then
|
||
|
compileBuf[#compileBuf+1] = '---@deprecated\n'
|
||
|
end
|
||
|
end,
|
||
|
DISABLE = function ()
|
||
|
disable = true
|
||
|
end,
|
||
|
}, { __index = _ENV })
|
||
|
|
||
|
util.saveFile((ROOT / 'log' / 'middleScript.lua'):string(), middleScript)
|
||
|
|
||
|
local suc = xpcall(function ()
|
||
|
assert(load(middleScript, middleScript, 't', env))()
|
||
|
end, log.error)
|
||
|
if not suc then
|
||
|
log.debug('MiddleScript:\n', middleScript)
|
||
|
end
|
||
|
if disable and status == 'default' then
|
||
|
return nil
|
||
|
end
|
||
|
return table.concat(compileBuf)
|
||
|
end
|
||
|
|
||
|
local function loadMetaLocale(langID, result)
|
||
|
result = result or {}
|
||
|
local path = (ROOT / 'locale' / langID / 'meta.lua'):string()
|
||
|
local localeContent = util.loadFile(path)
|
||
|
if localeContent then
|
||
|
xpcall(lloader, log.error, localeContent, path, result)
|
||
|
end
|
||
|
return result
|
||
|
end
|
||
|
|
||
|
local function initBuiltIn(uri)
|
||
|
log.info('Init builtin library at:', uri)
|
||
|
local scp = scope.getScope(uri)
|
||
|
local langID = lang.id
|
||
|
local version = config.get(uri, 'Lua.runtime.version')
|
||
|
local encoding = config.get(uri, 'Lua.runtime.fileEncoding')
|
||
|
---@type fs.path
|
||
|
local metaPath = fs.path(METAPATH) / config.get(uri, 'Lua.runtime.meta'):gsub('%$%{(.-)%}', {
|
||
|
version = version,
|
||
|
language = langID,
|
||
|
encoding = encoding,
|
||
|
})
|
||
|
|
||
|
local metaLang = loadMetaLocale('en-US')
|
||
|
if langID ~= 'en-US' then
|
||
|
loadMetaLocale(langID, metaLang)
|
||
|
end
|
||
|
|
||
|
if scp:get('metaPath') == metaPath:string() then
|
||
|
log.debug('Has meta path, skip:', metaPath:string())
|
||
|
return
|
||
|
end
|
||
|
scp:set('metaPath', metaPath:string())
|
||
|
local suc = xpcall(function ()
|
||
|
if not fs.exists(metaPath) then
|
||
|
fs.create_directories(metaPath)
|
||
|
end
|
||
|
end, log.error)
|
||
|
if not suc then
|
||
|
log.warn('Init builtin failed.')
|
||
|
return
|
||
|
end
|
||
|
local out = fsu.dummyFS()
|
||
|
local templateDir = ROOT / 'meta' / 'template'
|
||
|
for libName, status in pairs(define.BuiltIn) do
|
||
|
status = config.get(uri, 'Lua.runtime.builtin')[libName] or status
|
||
|
log.debug('Builtin status:', libName, status)
|
||
|
if status == 'disable' then
|
||
|
goto CONTINUE
|
||
|
end
|
||
|
|
||
|
---@type fs.path
|
||
|
local libPath = templateDir / (libName .. '.lua')
|
||
|
local metaDoc = compileSingleMetaDoc(uri, fsu.loadFile(libPath), metaLang, status)
|
||
|
if metaDoc then
|
||
|
metaDoc = encoder.encode(encoding, metaDoc, 'auto')
|
||
|
|
||
|
local outputLibName = libName:gsub('%.', '/') .. '.lua'
|
||
|
if outputLibName ~= libName then
|
||
|
out:createDirectories(fs.path(outputLibName):parent_path())
|
||
|
end
|
||
|
|
||
|
local ok, err = out:saveFile(outputLibName, metaDoc)
|
||
|
if not ok then
|
||
|
log.debug("Save Meta File:", err)
|
||
|
goto CONTINUE
|
||
|
end
|
||
|
|
||
|
local outputPath = metaPath / outputLibName
|
||
|
m.metaPaths[outputPath:string()] = true
|
||
|
log.debug('Meta path:', outputPath:string())
|
||
|
end
|
||
|
::CONTINUE::
|
||
|
end
|
||
|
local result = fsu.fileSync(out, metaPath)
|
||
|
if #result.err > 0 then
|
||
|
log.warn('File sync error:', inspect(result))
|
||
|
end
|
||
|
end
|
||
|
|
||
|
---@param libraryDir fs.path
|
||
|
local function loadSingle3rdConfig(libraryDir)
|
||
|
local configText = fsu.loadFile(libraryDir / 'config.lua')
|
||
|
if not configText then
|
||
|
return nil
|
||
|
end
|
||
|
|
||
|
local env = setmetatable({}, { __index = _G })
|
||
|
assert(load(configText, '@' .. libraryDir:string(), 't', env))()
|
||
|
|
||
|
local cfg = {}
|
||
|
|
||
|
cfg.path = libraryDir:filename():string()
|
||
|
cfg.name = cfg.name or cfg.path
|
||
|
|
||
|
if fs.exists(libraryDir / 'plugin.lua') then
|
||
|
cfg.plugin = true
|
||
|
end
|
||
|
|
||
|
for k, v in pairs(env) do
|
||
|
cfg[k] = v
|
||
|
end
|
||
|
|
||
|
if cfg.words then
|
||
|
for i, word in ipairs(cfg.words) do
|
||
|
cfg.words[i] = '()' .. word .. '()'
|
||
|
end
|
||
|
end
|
||
|
if cfg.files then
|
||
|
for i, filename in ipairs(cfg.files) do
|
||
|
if plat.OS == 'Windows' then
|
||
|
filename = filename:gsub('/', '\\')
|
||
|
else
|
||
|
filename = filename:gsub('\\', '/')
|
||
|
end
|
||
|
cfg.files[i] = '()' .. filename .. '()'
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return cfg
|
||
|
end
|
||
|
|
||
|
local innerThirdDir = ROOT / 'meta' / '3rd'
|
||
|
|
||
|
local function load3rdConfigInDir(dir, configs, inner)
|
||
|
if not fs.is_directory(dir) then
|
||
|
return
|
||
|
end
|
||
|
for libraryDir in fs.pairs(dir) do
|
||
|
local suc, res = xpcall(loadSingle3rdConfig, log.error, libraryDir)
|
||
|
if suc and res then
|
||
|
if inner then
|
||
|
res.dirname = ('${3rd}/%s'):format(res.path)
|
||
|
else
|
||
|
res.dirname = ('%s/%s'):format(dir:string(), res.path)
|
||
|
end
|
||
|
configs[#configs+1] = res
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function load3rdConfig(uri)
|
||
|
local scp = scope.getScope(uri)
|
||
|
local configs = scp:get 'thirdConfigsCache'
|
||
|
if configs then
|
||
|
return configs
|
||
|
end
|
||
|
configs = {}
|
||
|
scp:set('thirdConfigsCache', configs)
|
||
|
load3rdConfigInDir(innerThirdDir, configs, true)
|
||
|
local thirdDirs = config.get(uri, 'Lua.workspace.userThirdParty')
|
||
|
for _, thirdDir in ipairs(thirdDirs) do
|
||
|
load3rdConfigInDir(fs.path(thirdDir), configs)
|
||
|
end
|
||
|
return configs
|
||
|
end
|
||
|
|
||
|
local function apply3rd(uri, cfg, onlyMemory)
|
||
|
local changes = {}
|
||
|
if cfg.configs then
|
||
|
for _, change in ipairs(cfg.configs) do
|
||
|
changes[#changes+1] = {
|
||
|
key = change.key,
|
||
|
action = change.action,
|
||
|
prop = change.prop,
|
||
|
value = change.value,
|
||
|
uri = uri,
|
||
|
}
|
||
|
end
|
||
|
end
|
||
|
|
||
|
if cfg.plugin then
|
||
|
changes[#changes+1] = {
|
||
|
key = 'Lua.runtime.plugin',
|
||
|
action = 'set',
|
||
|
value = ('%s/plugin.lua'):format(cfg.dirname),
|
||
|
uri = uri,
|
||
|
}
|
||
|
end
|
||
|
|
||
|
changes[#changes+1] = {
|
||
|
key = 'Lua.workspace.library',
|
||
|
action = 'add',
|
||
|
value = ('%s/library'):format(cfg.dirname),
|
||
|
uri = uri,
|
||
|
}
|
||
|
|
||
|
client.setConfig(changes, onlyMemory)
|
||
|
end
|
||
|
|
||
|
local hasAsked
|
||
|
---@async
|
||
|
local function askFor3rd(uri, cfg)
|
||
|
if hasAsked then
|
||
|
return nil
|
||
|
end
|
||
|
hasAsked = true
|
||
|
local yes1 = lang.script.WINDOW_APPLY_WHIT_SETTING
|
||
|
local yes2 = lang.script.WINDOW_APPLY_WHITOUT_SETTING
|
||
|
local no = lang.script.WINDOW_DONT_SHOW_AGAIN
|
||
|
local result = client.awaitRequestMessage('Info'
|
||
|
, lang.script('WINDOW_ASK_APPLY_LIBRARY', cfg.name)
|
||
|
, {yes1, yes2, no}
|
||
|
)
|
||
|
if not result then
|
||
|
return nil
|
||
|
end
|
||
|
if result == yes1 then
|
||
|
apply3rd(uri, cfg, false)
|
||
|
client.setConfig({
|
||
|
{
|
||
|
key = 'Lua.workspace.checkThirdParty',
|
||
|
action = 'set',
|
||
|
value = false,
|
||
|
uri = uri,
|
||
|
},
|
||
|
}, false)
|
||
|
elseif result == yes2 then
|
||
|
apply3rd(uri, cfg, true)
|
||
|
client.setConfig({
|
||
|
{
|
||
|
key = 'Lua.workspace.checkThirdParty',
|
||
|
action = 'set',
|
||
|
value = false,
|
||
|
uri = uri,
|
||
|
},
|
||
|
}, true)
|
||
|
else
|
||
|
client.setConfig({
|
||
|
{
|
||
|
key = 'Lua.workspace.checkThirdParty',
|
||
|
action = 'set',
|
||
|
value = false,
|
||
|
uri = uri,
|
||
|
},
|
||
|
}, false)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
---@param a string
|
||
|
---@param b string
|
||
|
---@return boolean
|
||
|
local function wholeMatch(a, b)
|
||
|
local pos1, pos2 = a:match(b)
|
||
|
if not pos1 then
|
||
|
return false
|
||
|
end
|
||
|
local left = a:sub(pos1 - 1, pos1 - 1)
|
||
|
local right = a:sub(pos2, pos2)
|
||
|
if left:match '[%w_]'
|
||
|
or right:match '[%w_]' then
|
||
|
return false
|
||
|
end
|
||
|
return true
|
||
|
end
|
||
|
|
||
|
local function check3rdByWords(uri, configs)
|
||
|
if hasAsked then
|
||
|
return
|
||
|
end
|
||
|
if not files.isLua(uri) then
|
||
|
return
|
||
|
end
|
||
|
local id = 'check3rdByWords:' .. uri
|
||
|
await.close(id)
|
||
|
await.call(function () ---@async
|
||
|
await.sleep(0.1)
|
||
|
local text = files.getText(uri)
|
||
|
if not text then
|
||
|
return
|
||
|
end
|
||
|
for _, cfg in ipairs(configs) do
|
||
|
if cfg.words then
|
||
|
for _, word in ipairs(cfg.words) do
|
||
|
await.delay()
|
||
|
if wholeMatch(text, word) then
|
||
|
askFor3rd(uri, cfg)
|
||
|
return
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end, id)
|
||
|
end
|
||
|
|
||
|
local function check3rdByFileName(uri, configs)
|
||
|
if hasAsked then
|
||
|
return
|
||
|
end
|
||
|
local path = ws.getRelativePath(uri)
|
||
|
if not path then
|
||
|
return
|
||
|
end
|
||
|
local id = 'check3rdByFileName:' .. uri
|
||
|
await.close(id)
|
||
|
await.call(function () ---@async
|
||
|
await.sleep(0.1)
|
||
|
for _, cfg in ipairs(configs) do
|
||
|
if cfg.files then
|
||
|
for _, filename in ipairs(cfg.files) do
|
||
|
await.delay()
|
||
|
if wholeMatch(path, filename) then
|
||
|
askFor3rd(uri, cfg)
|
||
|
return
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
end, id)
|
||
|
end
|
||
|
|
||
|
---@async
|
||
|
local function check3rd(uri)
|
||
|
if hasAsked then
|
||
|
return
|
||
|
end
|
||
|
if ws.isIgnored(uri) then
|
||
|
return
|
||
|
end
|
||
|
if not config.get(uri, 'Lua.workspace.checkThirdParty') then
|
||
|
return
|
||
|
end
|
||
|
local scp = scope.getScope(uri)
|
||
|
if not scp:get 'canCheckThirdParty' then
|
||
|
return
|
||
|
end
|
||
|
local thirdConfigs = load3rdConfig(uri) or false
|
||
|
if not thirdConfigs then
|
||
|
return
|
||
|
end
|
||
|
check3rdByWords(uri, thirdConfigs)
|
||
|
check3rdByFileName(uri, thirdConfigs)
|
||
|
end
|
||
|
|
||
|
local function check3rdOfWorkspace(suri)
|
||
|
local scp = scope.getScope(suri)
|
||
|
scp:set('thirdConfigsCache', nil)
|
||
|
scp:set('canCheckThirdParty', true)
|
||
|
local id = 'check3rdOfWorkspace:' .. scp:getName()
|
||
|
await.close(id)
|
||
|
---@async
|
||
|
await.call(function ()
|
||
|
ws.awaitReady(suri)
|
||
|
for uri in files.eachFile(suri) do
|
||
|
check3rd(uri)
|
||
|
end
|
||
|
for uri in files.eachDll() do
|
||
|
check3rd(uri)
|
||
|
end
|
||
|
end, id)
|
||
|
end
|
||
|
|
||
|
config.watch(function (uri, key, value, oldValue)
|
||
|
if key:find '^Lua.runtime' then
|
||
|
initBuiltIn(uri)
|
||
|
end
|
||
|
if key == 'Lua.workspace.checkThirdParty'
|
||
|
or key == 'Lua.workspace.userThirdParty'
|
||
|
or key == '' then
|
||
|
check3rdOfWorkspace(uri)
|
||
|
end
|
||
|
end)
|
||
|
|
||
|
---@async
|
||
|
files.watch(function (ev, uri)
|
||
|
if ev == 'update'
|
||
|
or ev == 'dll' then
|
||
|
check3rd(uri)
|
||
|
end
|
||
|
end)
|
||
|
|
||
|
ws.watch(function (ev, uri)
|
||
|
if ev == 'startReload' then
|
||
|
initBuiltIn(uri)
|
||
|
end
|
||
|
end)
|
||
|
|
||
|
return m
|