add cache and rename some files

This commit is contained in:
2026-01-15 15:41:19 +01:00
parent 7879f15726
commit b64815d9ab
4753 changed files with 931902 additions and 1 deletions

View File

@@ -0,0 +1,119 @@
local lang = require 'language'
local platform = require 'bee.platform'
local subprocess = require 'bee.subprocess'
local json = require 'json'
local jsonb = require 'json-beautify'
local util = require 'utility'
local export = {}
local function logFileForThread(threadId)
return LOGPATH .. '/check-partial-' .. threadId .. '.json'
end
local function buildArgs(exe, numThreads, threadId, format, quiet)
local args = {exe}
local skipNext = false
for i = 1, #arg do
local arg = arg[i]
-- --check needs to be transformed into --check_worker
if arg:lower():match('^%-%-check$') or arg:lower():match('^%-%-check=') then
args[#args + 1] = arg:gsub('%-%-%w*', '--check_worker')
-- --check_out_path needs to be removed if we have more than one thread
elseif arg:lower():match('%-%-check_out_path') and numThreads > 1 then
if not arg:match('%-%-[%w_]*=') then
skipNext = true
end
else
if skipNext then
skipNext = false
else
args[#args + 1] = arg
end
end
end
args[#args + 1] = '--thread_id'
args[#args + 1] = tostring(threadId)
if numThreads > 1 then
if quiet then
args[#args + 1] = '--quiet'
end
if format then
args[#args + 1] = '--check_format=' .. format
end
args[#args + 1] = '--check_out_path'
args[#args + 1] = logFileForThread(threadId)
end
return args
end
function export.runCLI()
local numThreads = tonumber(NUM_THREADS or 1)
local exe
local minIndex = -1
while arg[minIndex] do
exe = arg[minIndex]
minIndex = minIndex - 1
end
-- TODO: is this necessary? got it from the shell.lua helper in bee.lua tests
if platform.os == 'windows' and not exe:match('%.[eE][xX][eE]$') then
exe = exe..'.exe'
end
if not QUIET and numThreads > 1 then
print(lang.script('CLI_CHECK_MULTIPLE_WORKERS', numThreads))
end
local procs = {}
for i = 1, numThreads do
local process, err = subprocess.spawn({buildArgs(exe, numThreads, i, CHECK_FORMAT, QUIET)})
if err then
print(err)
end
if process then
procs[#procs + 1] = process
end
end
local checkPassed = true
for _, process in ipairs(procs) do
checkPassed = process:wait() == 0 and checkPassed
end
if numThreads > 1 then
local mergedResults = {}
local count = 0
for i = 1, numThreads do
local result = json.decode(util.loadFile(logFileForThread(i)) or '[]')
for k, v in pairs(result) do
local entries = mergedResults[k] or {}
mergedResults[k] = entries
for _, entry in ipairs(v) do
entries[#entries + 1] = entry
count = count + 1
end
end
end
local outpath = nil
if CHECK_FORMAT == 'json' or CHECK_OUT_PATH then
outpath = CHECK_OUT_PATH or LOGPATH .. '/check.json'
util.saveFile(outpath, jsonb.beautify(mergedResults))
end
if not QUIET then
if count == 0 then
print(lang.script('CLI_CHECK_SUCCESS'))
elseif outpath then
print(lang.script('CLI_CHECK_RESULTS_OUTPATH', count, outpath))
else
print(lang.script('CLI_CHECK_RESULTS_PRETTY', count))
end
end
end
return checkPassed and 0 or 1
end
return export

View File

@@ -0,0 +1,295 @@
local lclient = require 'lclient'()
local furi = require 'file-uri'
local ws = require 'workspace'
local files = require 'files'
local diag = require 'provider.diagnostic'
local util = require 'utility'
local jsonb = require 'json-beautify'
local lang = require 'language'
local define = require 'proto.define'
local protoDiag = require 'proto.diagnostic'
local config = require 'config.config'
local fs = require 'bee.filesystem'
local provider = require 'provider'
local await = require 'await'
require 'plugin'
require 'vm'
local export = {}
local colors
if not os.getenv('NO_COLOR') then
colors = {
red = '\27[31m',
green = '\27[32m',
yellow = '\27[33m',
blue = '\27[34m',
magenta = '\27[35m',
white = '\27[37m',
grey = '\27[90m',
reset = '\27[0m'
}
else
colors = {
red = '',
green = '',
yellow = '',
blue = '',
magenta = '',
white = '',
grey = '',
reset = ''
}
end
--- @type table<DiagnosticSeverity, string>
local severity_colors = {
Error = colors.red,
Warning = colors.yellow,
Information = colors.white,
Hint = colors.white,
}
local severity_str = {} --- @type table<integer,DiagnosticSeverity>
for k, v in pairs(define.DiagnosticSeverity) do
severity_str[v] = k
end
local pwd
---@param path string
---@return string
local function relpath(path)
if not pwd then
pwd = furi.decode(furi.encode(fs.current_path():string()))
end
if pwd and path:sub(1, #pwd) == pwd then
path = path:sub(#pwd + 2)
end
return path
end
local function report_pretty(uri, diags)
local path = relpath(furi.decode(uri))
local lines = {} --- @type string[]
pcall(function()
for line in io.lines(path) do
table.insert(lines, line)
end
end)
for _, d in ipairs(diags) do
local rstart = d.range.start
local rend = d.range['end']
local severity = severity_str[d.severity]
print(
('%s%s:%s:%s%s [%s%s%s] %s %s(%s)%s'):format(
colors.blue,
path,
rstart.line + 1, -- Use 1-based indexing
rstart.character + 1, -- Use 1-based indexing
colors.reset,
severity_colors[severity],
severity,
colors.reset,
d.message,
colors.magenta,
d.code,
colors.reset
)
)
if #lines > 0 then
io.write(' ', lines[rstart.line + 1], '\n')
io.write(' ', colors.grey, (' '):rep(rstart.character), '^')
if rstart.line == rend.line then
io.write(('^'):rep(rend.character - rstart.character - 1))
end
io.write(colors.reset, '\n')
end
end
end
local function clear_line()
-- Write out empty space to ensure that the previous lien is cleared.
io.write('\x0D', (' '):rep(80), '\x0D')
end
--- @param i integer
--- @param max integer
--- @param results table<string, table[]>
local function report_progress(i, max, results)
local filesWithErrors = 0
local errors = 0
for _, diags in pairs(results) do
filesWithErrors = filesWithErrors + 1
errors = errors + #diags
end
clear_line()
io.write(
('>'):rep(math.ceil(i / max * 20)),
('='):rep(20 - math.ceil(i / max * 20)),
' ',
('0'):rep(#tostring(max) - #tostring(i)),
tostring(i),
'/',
tostring(max)
)
if errors > 0 then
io.write(' [', lang.script('CLI_CHECK_PROGRESS', errors, filesWithErrors), ']')
end
io.flush()
end
--- @param uri string
--- @param checkLevel integer
local function apply_check_level(uri, checkLevel)
local config_disables = util.arrayToHash(config.get(uri, 'Lua.diagnostics.disable'))
local config_severities = config.get(uri, 'Lua.diagnostics.severity')
for name, serverity in pairs(define.DiagnosticDefaultSeverity) do
serverity = config_severities[name] or serverity
if serverity:sub(-1) == '!' then
serverity = serverity:sub(1, -2)
end
if define.DiagnosticSeverity[serverity] > checkLevel then
config_disables[name] = true
end
end
config.set(uri, 'Lua.diagnostics.disable', util.getTableKeys(config_disables, true))
end
local function downgrade_checks_to_opened(uri)
local diagStatus = config.get(uri, 'Lua.diagnostics.neededFileStatus')
for d, status in pairs(diagStatus) do
if status == 'Any' or status == 'Any!' then
diagStatus[d] = 'Opened!'
end
end
for d, status in pairs(protoDiag.getDefaultStatus()) do
if status == 'Any' or status == 'Any!' then
diagStatus[d] = 'Opened!'
end
end
config.set(uri, 'Lua.diagnostics.neededFileStatus', diagStatus)
end
function export.runCLI()
lang(LOCALE)
local numThreads = tonumber(NUM_THREADS or 1)
local threadId = tonumber(THREAD_ID or 1)
local quiet = QUIET or numThreads > 1
if type(CHECK_WORKER) ~= 'string' then
print(lang.script('CLI_CHECK_ERROR_TYPE', type(CHECK_WORKER)))
return
end
local rootPath = fs.canonical(fs.path(CHECK_WORKER)):string()
local rootUri = furi.encode(rootPath)
if not rootUri then
print(lang.script('CLI_CHECK_ERROR_URI', rootPath))
return
end
rootUri = rootUri:gsub("/$", "")
if CHECKLEVEL and not define.DiagnosticSeverity[CHECKLEVEL] then
print(lang.script('CLI_CHECK_ERROR_LEVEL', 'Error, Warning, Information, Hint'))
return
end
local checkLevel = define.DiagnosticSeverity[CHECKLEVEL] or define.DiagnosticSeverity.Warning
util.enableCloseFunction()
local lastClock = os.clock()
local results = {} --- @type table<string, table[]>
local function errorhandler(err)
print(err)
print(debug.traceback())
end
---@async
xpcall(lclient.start, errorhandler, lclient, function (client)
await.disable()
client:registerFakers()
client:initialize {
rootUri = rootUri,
}
client:register('textDocument/publishDiagnostics', function (params)
results[params.uri] = params.diagnostics
if not QUIET and (CHECK_FORMAT == nil or CHECK_FORMAT == 'pretty') then
clear_line()
report_pretty(params.uri, params.diagnostics)
end
end)
if not quiet then
io.write(lang.script('CLI_CHECK_INITING'))
end
provider.updateConfig(rootUri)
ws.awaitReady(rootUri)
-- Disable any diagnostics that are above the check level
apply_check_level(rootUri, checkLevel)
-- Downgrade file opened status to Opened for everything to avoid
-- reporting during compilation on files that do not belong to this thread
downgrade_checks_to_opened(rootUri)
local uris = files.getChildFiles(rootUri)
local max = #uris
table.sort(uris) -- sort file list to ensure the work distribution order across multiple threads
for i, uri in ipairs(uris) do
if (i % numThreads + 1) == threadId and not ws.isIgnored(uri) then
files.open(uri)
diag.doDiagnostic(uri, true)
-- Print regularly but always print the last entry to ensure
-- that logs written to files don't look incomplete.
if not quiet and (os.clock() - lastClock > 0.2 or i == #uris) then
lastClock = os.clock()
client:update()
report_progress(i, max, results)
end
end
end
if not quiet then
clear_line()
end
end)
local count = 0
for uri, result in pairs(results) do
count = count + #result
if #result == 0 then
results[uri] = nil
end
end
local outpath = nil
if CHECK_FORMAT == 'json' or CHECK_OUT_PATH then
outpath = CHECK_OUT_PATH or LOGPATH .. '/check.json'
-- Always write result, even if it's empty to make sure no one accidentally looks at an old output after a successful run.
util.saveFile(outpath, jsonb.beautify(results))
end
if not quiet then
if count == 0 then
print(lang.script('CLI_CHECK_SUCCESS'))
elseif outpath then
print(lang.script('CLI_CHECK_RESULTS_OUTPATH', count, outpath))
else
print(lang.script('CLI_CHECK_RESULTS_PRETTY', count))
end
end
return count == 0 and 0 or 1
end
return export

View File

@@ -0,0 +1,362 @@
---@diagnostic disable: await-in-sync, param-type-mismatch
local ws = require 'workspace'
local vm = require 'vm'
local guide = require 'parser.guide'
local getDesc = require 'core.hover.description'
local getLabel = require 'core.hover.label'
local jsonb = require 'json-beautify'
local util = require 'utility'
local markdown = require 'provider.markdown'
local fs = require 'bee.filesystem'
local furi = require 'file-uri'
---@alias doctype
---| 'doc.alias'
---| 'doc.class'
---| 'doc.field'
---| 'doc.field.name'
---| 'doc.type.arg.name'
---| 'doc.type.function'
---| 'doc.type.table'
---| 'funcargs'
---| 'function'
---| 'function.return'
---| 'global.type'
---| 'global.variable'
---| 'local'
---| 'luals.config'
---| 'self'
---| 'setfield'
---| 'setglobal'
---| 'setindex'
---| 'setmethod'
---| 'tableindex'
---| 'type'
---@class docUnion broadest possible collection of exported docs, these are never all together.
---@field [1] string in name when table, always the same as view
---@field args docUnion[] list of argument docs passed to function
---@field async boolean has @async tag
---@field defines docUnion[] list of places where this is doc is defined and how its defined there
---@field deprecated boolean has @deprecated tag
---@field desc string code commentary
---@field extends string | docUnion ? what type this 'is'. string:<Parent_Class> for type: 'type', docUnion for type: 'function', string<primative> for other type 's
---@field fields docUnion[] class's fields
---@field file string path to where this token is defined
---@field finish [integer, integer] 0-indexed [line, column] position of end of token
---@field name string canonical name
---@field rawdesc string same as desc, but may have other things for types doc.retun andr doc.param (unused?)
---@field returns docUnion | docUnion[] list of docs for return values. if singluar, then always {type: 'undefined'}? might be a bug.
---@field start [integer, integer] 0-indexed [line, column] position of start of token
---@field type doctype role that this token plays in documentation. different from the 'type'/'class' this token is
---@field types docUnion[] type union? unclear. seems to be related to alias, maybe
---@field view string full method name, class, basal type, or unknown. in name table same as [1]
---@field visible 'package'|'private'|'protected'|'public' visibilty tag
local export = {}
function export.getLocalPath(uri)
local file_canonical = fs.canonical(furi.decode(uri)):string()
local doc_canonical = fs.canonical(DOC):string()
local relativePath = fs.relative(file_canonical, doc_canonical):string()
if relativePath == "" or relativePath:sub(1, 2) == '..' then
-- not under project directory
return '[FOREIGN] ' .. file_canonical
end
return relativePath
end
function export.positionOf(rowcol)
return type(rowcol) == 'table' and guide.positionOf(rowcol[1], rowcol[2]) or -1
end
function export.sortDoc(a,b)
if a.name ~= b.name then
return a.name < b.name
end
if a.file ~= b.file then
return a.file < b.file
end
return export.positionOf(a.start) < export.positionOf(b.start)
end
--- recursively generate documentation all parser objects downstream of `source`
---@async
---@param source parser.object | vm.global
---@param has_seen table? keeps track of visited nodes in documentation tree
---@return docUnion | [docUnion] | string | number | boolean | nil
function export.documentObject(source, has_seen)
--is this a primative type? then we dont need to process it.
if type(source) ~= 'table' then return source end
--set up/check recursion
if not has_seen then has_seen = {} end
if has_seen[source] then
return nil
end
has_seen[source] = true
--is this an array type? then process each array item and collect it
if (#source > 0 and next(source, #source) == nil) then
local objs = {} --make a pure numerical array
for i, child in ipairs(source) do
objs[i] = export.documentObject(child, has_seen)
end
return objs
end
--if neither, then this is a singular docUnion
local obj = export.makeDocObject['INIT'](source, has_seen)
--check if this source has a type (no type sources are usually autogen'd anon functions's return values that are not explicitly stated)
if not obj.type then return obj end
local res = export.makeDocObject[obj.type](source, obj, has_seen)
if res == false then
return nil
end
return res or obj
end
---Switch statement table. functions can be overriden by user file.
---@table
export.makeDocObject = setmetatable({}, {__index = function(t, k)
return function()
--print('DocError: no type "'..k..'"')
end
end})
export.makeDocObject['INIT'] = function(source, has_seen)
---@as docUnion
local ok, desc = pcall(getDesc, source)
local rawok, rawdesc = pcall(getDesc, source, true)
return {
type = source.cate or source.type,
name = export.documentObject((source.getCodeName and source:getCodeName()) or source.name, has_seen),
start = source.start and {guide.rowColOf(source.start)},
finish = source.finish and {guide.rowColOf(source.finish)},
types = export.documentObject(source.types, has_seen),
view = vm.getInfer(source):view(ws.rootUri),
desc = ok and desc or nil,
rawdesc = rawok and rawdesc or nil,
}
end
export.makeDocObject['doc.alias'] = function(source, obj, has_seen)
end
export.makeDocObject['doc.field'] = function(source, obj, has_seen)
if source.field.type == 'doc.field.name' then
obj.name = source.field[1]
else
obj.name = ('[%s]'):format(vm.getInfer(source.field):view(ws.rootUri))
end
obj.file = export.getLocalPath(guide.getUri(source))
obj.extends = source.extends and export.documentObject(source.extends, has_seen) --check if bug?
obj.async = vm.isAsync(source, true) and true or false --if vm.isAsync(set, true) then result.defines[#result.defines].extends['async'] = true end
obj.deprecated = vm.getDeprecated(source) and true or false -- if (depr and not depr.versions) the result.defines[#result.defines].extends['deprecated'] = true end
obj.visible = vm.getVisibleType(source)
end
export.makeDocObject['doc.class'] = function(source, obj, has_seen)
local extends = source.extends or source.value --doc.class or other
local field = source.field or source.method
obj.name = type(field) == 'table' and field[1] or nil
obj.file = export.getLocalPath(guide.getUri(source))
obj.extends = extends and export.documentObject(extends, has_seen)
obj.async = vm.isAsync(source, true) and true or false
obj.deprecated = vm.getDeprecated(source) and true or false
obj.visible = vm.getVisibleType(source)
end
export.makeDocObject['doc.field.name'] = function(source, obj, has_seen)
obj['[1]'] = export.documentObject(source[1], has_seen)
obj.view = source[1]
end
export.makeDocObject['doc.type.arg.name'] = export.makeDocObject['doc.field.name']
export.makeDocObject['doc.type.function'] = function(source, obj, has_seen)
obj.args = export.documentObject(source.args, has_seen)
obj.returns = export.documentObject(source.returns, has_seen)
end
export.makeDocObject['doc.type.table'] = function(source, obj, has_seen)
obj.fields = export.documentObject(source.fields, has_seen)
end
export.makeDocObject['funcargs'] = function(source, obj, has_seen)
local objs = {} --make a pure numerical array
for i, child in ipairs(source) do
objs[i] = export.documentObject(child, has_seen)
end
return objs
end
export.makeDocObject['function'] = function(source, obj, has_seen)
obj.args = export.documentObject(source.args, has_seen)
obj.view = getLabel(source, source.parent.type == 'setmethod', 1)
local _, _, max = vm.countReturnsOfFunction(source)
if max > 0 then obj.returns = {} end
for i = 1, max do
obj.returns[i] = export.documentObject(vm.getReturnOfFunction(source, i), has_seen) --check if bug?
end
end
export.makeDocObject['function.return'] = function(source, obj, has_seen)
obj.desc = source.comment and getDesc(source.comment)
obj.rawdesc = source.comment and getDesc(source.comment, true)
end
export.makeDocObject['local'] = function(source, obj, has_seen)
obj.name = source[1]
end
export.makeDocObject['self'] = export.makeDocObject['local']
export.makeDocObject['setfield'] = export.makeDocObject['doc.class']
export.makeDocObject['setglobal'] = export.makeDocObject['doc.class']
export.makeDocObject['setindex'] = export.makeDocObject['doc.class']
export.makeDocObject['setmethod'] = export.makeDocObject['doc.class']
export.makeDocObject['tableindex'] = function(source, obj, has_seen)
obj.name = source.index[1]
end
export.makeDocObject['type'] = function(source, obj, has_seen)
if export.makeDocObject['variable'](source, obj, has_seen) == false then
return false
end
obj.fields = {}
vm.getClassFields(ws.rootUri, source, vm.ANY, function (next_source, mark)
if next_source.type == 'doc.field'
or next_source.type == 'setfield'
or next_source.type == 'setmethod'
or next_source.type == 'tableindex'
then
table.insert(obj.fields, export.documentObject(next_source, has_seen))
end
end)
table.sort(obj.fields, export.sortDoc)
end
export.makeDocObject['variable'] = function(source, obj, has_seen)
obj.defines = {}
for _, set in ipairs(source:getSets(ws.rootUri)) do
if set.type == 'setglobal'
or set.type == 'setfield'
or set.type == 'setmethod'
or set.type == 'setindex'
or set.type == 'doc.alias'
or set.type == 'doc.class'
then
table.insert(obj.defines, export.documentObject(set, has_seen))
end
end
if #obj.defines == 0 then return false end
table.sort(obj.defines, export.sortDoc)
end
---gathers the globals that are to be exported in documentation
---@async
---@return table globals
function export.gatherGlobals()
local all_globals = vm.getAllGlobals()
local globals = {}
for _, g in pairs(all_globals) do
table.insert(globals, g)
end
return globals
end
---builds a lua table of based on `globals` and their elements
---@async
---@param globals table
---@param callback fun(i, max)
function export.makeDocs(globals, callback)
local docs = {}
for i, global in ipairs(globals) do
table.insert(docs, export.documentObject(global))
callback(i, #globals)
end
docs[#docs+1] = export.getLualsConfig()
table.sort(docs, export.sortDoc)
return docs
end
function export.getLualsConfig()
return {
name = 'LuaLS',
type = 'luals.config',
DOC = fs.canonical(fs.path(DOC)):string(),
defines = {},
fields = {}
}
end
---takes the table from `makeDocs`, serializes it, and exports it
---@async
---@param docs table
---@param outputDir string
---@return boolean ok, string[] outputPaths, (string|nil)[]? errs
function export.serializeAndExport(docs, outputDir)
local jsonPath = outputDir .. '/doc.json'
local mdPath = outputDir .. '/doc.md'
--export to json
local old_jsonb_supportSparseArray = jsonb.supportSparseArray
jsonb.supportSparseArray = true
local jsonOk, jsonErr = util.saveFile(jsonPath, jsonb.beautify(docs))
jsonb.supportSparseArray = old_jsonb_supportSparseArray
--export to markdown
local md = markdown()
for _, class in ipairs(docs) do
md:add('md', '# ' .. class.name)
md:emptyLine()
md:add('md', class.desc)
md:emptyLine()
if class.defines then
for _, define in ipairs(class.defines) do
if define.extends then
md:add('lua', define.extends.view)
md:emptyLine()
end
end
end
if class.fields then
local mark = {}
for _, field in ipairs(class.fields) do
if not mark[field.name] then
mark[field.name] = true
md:add('md', '## ' .. field.name)
md:emptyLine()
md:add('lua', field.extends.view)
md:emptyLine()
md:add('md', field.desc)
md:emptyLine()
end
end
end
md:splitLine()
end
local mdOk, mdErr = util.saveFile(mdPath, md:string())
--error checking save file
if( not (jsonOk and mdOk) ) then
return false, {jsonPath, mdPath}, {jsonErr, mdErr}
end
return true, {jsonPath, mdPath}
end
return export

View File

@@ -0,0 +1,258 @@
local lclient = require 'lclient'
local furi = require 'file-uri'
local ws = require 'workspace'
local files = require 'files'
local util = require 'utility'
local lang = require 'language'
local config = require 'config.config'
local await = require 'await'
local progress = require 'progress'
local fs = require 'bee.filesystem'
local doc = {}
---Find file 'doc.json'.
---@return fs.path
local function findDocJson()
local doc_json_path
if type(DOC_UPDATE) == 'string' then
doc_json_path = fs.canonical(fs.path(DOC_UPDATE)) .. '/doc.json'
else
doc_json_path = fs.current_path() .. '/doc.json'
end
if fs.exists(doc_json_path) then
return doc_json_path
else
error(string.format('Error: File "%s" not found.', doc_json_path))
end
end
---@return string # path of 'doc.json'
---@return string # path to be documented
local function getPathDocUpdate()
local doc_json_path = findDocJson()
local ok, doc_path = pcall(
function ()
local json = require('json')
local json_file = io.open(doc_json_path:string(), 'r'):read('*all')
local json_data = json.decode(json_file)
for _, section in ipairs(json_data) do
if section.type == 'luals.config' then
return section.DOC
end
end
end)
if ok then
local doc_json_dir = doc_json_path:string():gsub('/doc.json', '')
return doc_json_dir, doc_path
else
error(string.format('Error: Cannot update "%s".', doc_json_path))
end
end
---clones a module and assigns any internal upvalues pointing to the module to the new clone
---useful for sandboxing
---@param tbl any module to be cloned
---@return any module_clone the cloned module
local function reinstantiateModule(tbl, _new_module, _old_module, _has_seen)
_old_module = _old_module or tbl --remember old module only at root
_has_seen = _has_seen or {} --remember visited indecies
if(type(tbl) == 'table') then
if _has_seen[tbl] then return _has_seen[tbl] end
local clone = {}
_has_seen[tbl] = true
for key, value in pairs(tbl) do
clone[key] = reinstantiateModule(value, _new_module or clone, _old_module, _has_seen)
end
setmetatable(clone, getmetatable(tbl))
return clone
elseif(type(tbl) == 'function') then
local func = tbl
if _has_seen[func] then return _has_seen[func] end --copy function pointers instead of building clones
local upvalues = {}
local i = 1
while true do
local label, value = debug.getupvalue(func, i)
if not value then break end
upvalues[i] = value == _old_module and _new_module or value
i = i + 1
end
local new_func = load(string.dump(func))--, 'function@reinstantiateModule()', 'b', _ENV)
assert(new_func, 'could not load dumped function')
for index, upvalue in ipairs(upvalues) do
debug.setupvalue(new_func, index, upvalue)
end
_has_seen[func] = new_func
return new_func
else
return tbl
end
end
--these modules need to be loaded by the time this function is created
--im leaving them here since this is a pretty strange function that might get moved somewhere else later
--so make sure to bring these with you!
require 'workspace'
require 'vm'
require 'parser.guide'
require 'core.hover.description'
require 'core.hover.label'
require 'json-beautify'
require 'utility'
require 'provider.markdown'
---Gets config file's doc gen overrides.
---@return table dirty_module clone of the export module modified by user buildscript
local function injectBuildScript()
local sub_path = config.get(ws.rootUri, 'Lua.docScriptPath')
local module = reinstantiateModule( ( require 'cli.doc.export' ) )
--if default, then no build script modifications
if sub_path == '' then
return module
end
local resolved_path = fs.absolute(fs.path(DOC)):string() .. sub_path
local f <close> = io.open(resolved_path, 'r')
if not f then
error('could not open config file at '..tostring(resolved_path))
end
--include all `require`s in script.cli.doc.export in enviroment
--NOTE: allows access to the global enviroment!
local data, err = loadfile(resolved_path, 't', setmetatable({
export = module,
ws = require 'workspace',
vm = require 'vm',
guide = require 'parser.guide',
getDesc = require 'core.hover.description',
getLabel = require 'core.hover.label',
jsonb = require 'json-beautify',
util = require 'utility',
markdown = require 'provider.markdown'
},
{__index = _G}))
if err or not data then
error(err, 0)
end
data()
return module
end
---runtime call for documentation exporting
---@async
---@param outputPath string
function doc.makeDoc(outputPath)
ws.awaitReady(ws.rootUri)
local expandAlias = config.get(ws.rootUri, 'Lua.hover.expandAlias')
config.set(ws.rootUri, 'Lua.hover.expandAlias', false)
local _ <close> = function ()
config.set(ws.rootUri, 'Lua.hover.expandAlias', expandAlias)
end
await.sleep(0.1)
-- ready --
local prog <close> = progress.create(ws.rootUri, lang.script('CLI_DOC_WORKING'), 0)
local dirty_export = injectBuildScript()
local globals = dirty_export.gatherGlobals()
local docs = dirty_export.makeDocs(globals, function (i, max)
prog:setMessage(('%d/%d'):format(i, max))
prog:setPercentage((i) / max * 100)
end)
local ok, outPaths, err = dirty_export.serializeAndExport(docs, outputPath)
if not ok then
error(err)
end
return table.unpack(outPaths)
end
---CLI call for documentation (parameter '--DOC=...' is passed to server)
function doc.runCLI()
lang(LOCALE)
if DOC_UPDATE then
DOC_OUT_PATH, DOC = getPathDocUpdate()
end
if type(DOC) ~= 'string' then
print(lang.script('CLI_CHECK_ERROR_TYPE', type(DOC)))
return
end
local rootUri = furi.encode(fs.canonical(fs.path(DOC)):string())
if not rootUri then
print(lang.script('CLI_CHECK_ERROR_URI', DOC))
return
end
print('root uri = ' .. rootUri)
--- If '--configpath' is specified, get the folder path of the '.luarc.doc.json' configuration file (without the file name)
--- 如果指定了'--configpath',则获取`.luarc.doc.json` 配置文件的文件夹路径(不包含文件名)
--- This option is passed into the callback function of the initialized method in provide.
--- 该选项会被传入到`provide`中的`initialized`方法的回调函数中
local luarcParentUri
if CONFIGPATH then
luarcParentUri = furi.encode(fs.absolute(fs.path(CONFIGPATH)):parent_path():string())
end
util.enableCloseFunction()
local lastClock = os.clock()
---@async
lclient():start(function (client)
client:registerFakers()
client:initialize {
rootUri = rootUri,
luarcParentUri = luarcParentUri,
}
io.write(lang.script('CLI_DOC_INITING'))
config.set(nil, 'Lua.diagnostics.enable', false)
config.set(nil, 'Lua.hover.expandAlias', false)
ws.awaitReady(rootUri)
await.sleep(0.1)
--ready--
local dirty_export = injectBuildScript()
local globals = dirty_export.gatherGlobals()
local docs = dirty_export.makeDocs(globals, function (i, max)
if os.clock() - lastClock > 0.2 then
lastClock = os.clock()
local output = '\x0D'
.. ('>'):rep(math.ceil(i / max * 20))
.. ('='):rep(20 - math.ceil(i / max * 20))
.. ' '
.. ('0'):rep(#tostring(max) - #tostring(i))
.. tostring(i) .. '/' .. tostring(max)
io.write(output)
end
end)
io.write('\x0D')
if not DOC_OUT_PATH then
DOC_OUT_PATH = fs.current_path():string()
end
local ok, outPaths, err = dirty_export.serializeAndExport(docs, DOC_OUT_PATH)
print(lang.script('CLI_DOC_DONE'))
for i, path in ipairs(outPaths) do
local this_err = (type(err) == 'table') and err[i] or nil
print(this_err or files.normalize(path))
end
end)
end
return doc

View File

@@ -0,0 +1,170 @@
local util = require 'utility'
--- @class cli.arg
--- @field type? string|string[]
--- @field description string Description of the argument in markdown format.
--- @field example? string
--- @field default? any
--- @type table<string, cli.arg>
local args = {
['--help'] = {
description = [[
Print this message.
]],
},
['--check'] = {
type = 'string',
description = [[
Perform a "diagnosis report" where the results of the diagnosis are written to the logpath.
]],
example = [[--check=C:\Users\Me\path\to\workspace]]
},
['--checklevel'] = {
type = 'string',
description = [[
To be used with --check. The minimum level of diagnostic that should be logged.
Items with lower priority than the one listed here will not be written to the file.
Options include, in order of priority:
- Error
- Warning
- Information
- Hint
]],
default = 'Warning',
example = [[--checklevel=Information]]
},
['--check_format'] = {
type = { 'json', 'pretty' },
description = [[
Output format for the check results.
- 'pretty': results are displayed to stdout in a human-readable format.
- 'json': results are written to a file in JSON format. See --check_out_path
]],
default = 'pretty'
},
['--version'] = {
type = 'boolean',
description = [[
Get the version of the Lua language server.
This will print it to the command line and immediately exit.
]],
},
['--doc'] = {
type = 'string',
description = [[
Generate documentation from a workspace.
The files will be written to the documentation output path passed
in --doc_out_path.
]],
example = [[--doc=C:/Users/Me/Documents/myLuaProject/]]
},
['--doc_out_path'] = {
type = 'string',
description = [[
The path to output generated documentation at.
If --doc_out_path is missing, the documentation will be written
to the current directory.
See --doc for more info.
]],
example = [[--doc_out_path=C:/Users/Me/Documents/myLuaProjectDocumentation]]
},
['--doc_update'] = {
type = 'string',
description = [[
Update existing documentation files at the given path.
]]
},
['--logpath'] = {
type = 'string',
description = [[
Where the log should be written to.
]],
default = './log',
example = [[--logpath=D:/luaServer/logs]]
},
['--loglevel'] = {
type = 'string',
description = [[
The minimum level of logging that should appear in the logfile.
Can be used to log more detailed info for debugging and error reporting.
Options:
- error
- warn
- info
- debug
- trace
]],
example = [[--loglevel=trace]]
},
['--metapath'] = {
type = 'string',
description = [[
Where the standard Lua library definition files should be generated to.
]],
default = './meta',
example = [[--metapath=D:/sumnekoLua/metaDefintions]]
},
['--locale'] = {
type = 'string',
description = [[
The language to use. Defaults to en-us.
Options can be found in locale/ .
]],
example = [[--locale=zh-cn]]
},
['--configpath'] = {
type = 'string',
description = [[
The location of the configuration file that will be loaded.
Can be relative to the workspace.
When provided, config files from elsewhere (such as from VS Code) will no longer be loaded.
]],
example = [[--configpath=sumnekoLuaConfig.lua]]
},
['--force-accept-workspace'] = {
type = 'boolean',
description = [[
Allows the use of root/home directory as the workspace.
]]
},
['--socket'] = {
type = 'number',
description = [[
Will communicate to a client over the specified TCP port instead of through stdio.
]],
example = [[--socket=5050]]
},
['--develop'] = {
type = 'boolean',
description = [[
Enables development mode. This allows plugins to write to the logpath.
]]
}
}
for nm, attrs in util.sortPairs(args) do
if attrs.type == 'boolean' then
print(nm)
else
print(nm .. "=<value>")
end
if attrs.description then
local normalized_description = attrs.description:gsub("^%s+", ""):gsub("\n%s+", "\n"):gsub("%s+$", "")
print("\n " .. normalized_description:gsub('\n', '\n '))
end
local attr_type = attrs.type
if type(attr_type) == "table" then
print("\n Values: " .. table.concat(attr_type, ', '))
end
if attrs.default then
print("\n Default: " .. tostring(attrs.default))
end
if attrs.example then
print("\n Example: " .. attrs.example)
end
print()
end

View File

@@ -0,0 +1,34 @@
if _G['HELP'] then
require 'cli.help'
os.exit(0, true)
end
if _G['VERSION'] then
require 'cli.version'
os.exit(0, true)
end
if _G['CHECK'] then
local ret = require 'cli.check'.runCLI()
os.exit(ret, true)
end
if _G['CHECK_WORKER'] then
local ret = require 'cli.check_worker'.runCLI()
os.exit(ret or 0, true)
end
if _G['DOC_UPDATE'] then
require 'cli.doc' .runCLI()
os.exit(0, true)
end
if _G['DOC'] then
require 'cli.doc' .runCLI()
os.exit(0, true)
end
if _G['VISUALIZE'] then
local ret = require 'cli.visualize' .runCLI()
os.exit(ret or 0, true)
end

View File

@@ -0,0 +1,2 @@
local version = require 'version'
print(version.getVersion())

View File

@@ -0,0 +1,103 @@
local lang = require 'language'
local parser = require 'parser'
local guide = require 'parser.guide'
local function nodeId(node)
return node.type .. ':' .. node.start .. ':' .. node.finish
end
local function shorten(str)
if type(str) ~= 'string' then
return str
end
str = str:gsub('\n', '\\\\n')
if #str <= 20 then
return str
else
return str:sub(1, 17) .. '...'
end
end
local function getTooltipLine(k, v)
if type(v) == 'table' then
if v.type then
v = '<node ' .. v.type .. '>'
else
v = '<table>'
end
end
v = tostring(v)
v = v:gsub('"', '\\"')
return k .. ': ' .. shorten(v) .. '\\n'
end
local function getTooltip(node)
local str = ''
local skipNodes = {parent = true, start = true, finish = true, type = true}
str = str .. getTooltipLine('start', node.start)
str = str .. getTooltipLine('finish', node.finish)
for k, v in pairs(node) do
if type(k) ~= 'number' and not skipNodes[k] then
str = str .. getTooltipLine(k, v)
end
end
for i = 1, math.min(#node, 15) do
str = str .. getTooltipLine(i, node[i])
end
if #node > 15 then
str = str .. getTooltipLine('15..' .. #node, '(...)')
end
return str
end
local nodeEntry = '\t"%s" [\n\t\tlabel="%s\\l%s\\l"\n\t\ttooltip="%s"\n\t]'
local function getNodeLabel(node)
local keyName = guide.getKeyName(node)
if node.type == 'binary' or node.type == 'unary' then
keyName = node.op.type
elseif node.type == 'label' or node.type == 'goto' then
keyName = node[1]
end
return nodeEntry:format(nodeId(node), node.type, shorten(keyName) or '', getTooltip(node))
end
local function getVisualizeVisitor(writer)
local function visitNode(node, parent)
if node == nil then return end
writer:write(getNodeLabel(node))
writer:write('\n')
if parent then
writer:write(('\t"%s" -> "%s"'):format(nodeId(parent), nodeId(node)))
writer:write('\n')
end
guide.eachChild(node, function(child)
visitNode(child, node)
end)
end
return visitNode
end
local export = {}
function export.visualizeAst(code, writer)
local state = parser.compile(code, 'Lua', _G['LUA_VER'] or 'Lua 5.4')
writer:write('digraph AST {\n')
writer:write('\tnode [shape = rect]\n')
getVisualizeVisitor(writer)(state.ast)
writer:write('}\n')
end
function export.runCLI()
lang(LOCALE)
local file = _G['VISUALIZE']
local code, err = io.open(file)
if not code then
io.stderr:write('failed to open ' .. file .. ': ' .. err)
return 1
end
code = code:read('a')
return export.visualizeAst(code, io.stdout)
end
return export