minetest_x_bows/bin/lua-language-server-3.5.6-l.../script/provider/diagnostic.lua
2022-11-03 11:37:33 -04:00

623 lines
16 KiB
Lua

local await = require 'await'
local proto = require 'proto.proto'
local define = require 'proto.define'
local lang = require 'language'
local files = require 'files'
local config = require 'config'
local core = require 'core.diagnostics'
local util = require 'utility'
local ws = require 'workspace'
local progress = require "progress"
local client = require 'client'
local converter = require 'proto.converter'
local loading = require 'workspace.loading'
local scope = require 'workspace.scope'
local time = require 'bee.time'
local ltable = require 'linked-table'
local furi = require 'file-uri'
local json = require 'json'
local fw = require 'filewatch'
local vm = require 'vm.vm'
---@class diagnosticProvider
local m = {}
m.cache = {}
m.sleepRest = 0.0
local function concat(t, sep)
if type(t) ~= 'table' then
return t
end
return table.concat(t, sep)
end
local function buildSyntaxError(uri, err)
local text = files.getText(uri)
if not text then
return
end
local message = lang.script('PARSER_' .. err.type, err.info)
if err.version then
local version = err.info and err.info.version or config.get(uri, 'Lua.runtime.version')
message = message .. ('(%s)'):format(lang.script('DIAG_NEED_VERSION'
, concat(err.version, '/')
, version
))
end
local related = err.info and err.info.related
local relatedInformation
if related then
relatedInformation = {}
for _, rel in ipairs(related) do
local rmessage
if rel.message then
rmessage = lang.script('PARSER_' .. rel.message)
else
rmessage = text:sub(rel.start, rel.finish)
end
local relUri = rel.uri or uri
relatedInformation[#relatedInformation+1] = {
message = rmessage,
location = converter.location(relUri, converter.packRange(relUri, rel.start, rel.finish)),
}
end
end
return {
code = err.type:lower():gsub('_', '-'),
range = converter.packRange(uri, err.start, err.finish),
severity = define.DiagnosticSeverity[err.level],
source = lang.script.DIAG_SYNTAX_CHECK,
message = message,
data = 'syntax',
relatedInformation = relatedInformation,
}
end
local function buildDiagnostic(uri, diag)
if not files.exists(uri) then
return
end
local relatedInformation
if diag.related then
relatedInformation = {}
for _, rel in ipairs(diag.related) do
local rtext = files.getText(rel.uri)
if not rtext then
goto CONTINUE
end
relatedInformation[#relatedInformation+1] = {
message = rel.message or rtext:sub(rel.start, rel.finish),
location = converter.location(rel.uri, converter.packRange(rel.uri, rel.start, rel.finish))
}
::CONTINUE::
end
end
return {
range = converter.packRange(uri, diag.start, diag.finish),
source = lang.script.DIAG_DIAGNOSTICS,
severity = diag.level,
message = diag.message,
code = diag.code,
tags = diag.tags,
data = diag.data,
relatedInformation = relatedInformation,
}
end
local function mergeDiags(a, b, c)
if not a and not b and not c then
return nil
end
local t = {}
local function merge(diags)
if not diags then
return
end
for i = 1, #diags do
local diag = diags[i]
local severity = diag.severity
if severity == define.DiagnosticSeverity.Hint
or severity == define.DiagnosticSeverity.Information then
if #t > 10000 then
goto CONTINUE
end
end
t[#t+1] = diag
::CONTINUE::
end
end
merge(a)
merge(b)
merge(c)
if #t == 0 then
return nil
end
return t
end
-- enable `push`, disable `clear`
function m.clear(uri, force)
await.close('diag:' .. uri)
if m.cache[uri] == nil and not force then
return
end
m.cache[uri] = nil
proto.notify('textDocument/publishDiagnostics', {
uri = uri,
diagnostics = {},
})
log.info('clearDiagnostics', uri)
end
function m.clearCacheExcept(uris)
local excepts = {}
for _, uri in ipairs(uris) do
excepts[uri] = true
end
for uri in pairs(m.cache) do
if not excepts[uri] then
m.cache[uri] = false
end
end
end
function m.clearAll(force)
if force then
for luri in files.eachFile() do
m.clear(luri, force)
end
else
for luri in pairs(m.cache) do
m.clear(luri)
end
end
end
function m.syntaxErrors(uri, ast)
if #ast.errs == 0 then
return nil
end
local results = {}
pcall(function ()
local disables = util.arrayToHash(config.get(uri, 'Lua.diagnostics.disable'))
for _, err in ipairs(ast.errs) do
local id = err.type:lower():gsub('_', '-')
if not disables[id]
and not vm.isDiagDisabledAt(uri, err.start, id, true) then
results[#results+1] = buildSyntaxError(uri, err)
end
end
end)
return results
end
local function copyDiagsWithoutSyntax(diags)
if not diags then
return nil
end
local copyed = {}
for _, diag in ipairs(diags) do
if diag.data ~= 'syntax' then
copyed[#copyed+1] = diag
end
end
return copyed
end
---@async
---@param uri uri
---@return boolean
local function isValid(uri)
if not config.get(uri, 'Lua.diagnostics.enable') then
return false
end
if files.isLibrary(uri, true) then
local status = config.get(uri, 'Lua.diagnostics.libraryFiles')
if status == 'Disable' then
return false
elseif status == 'Opened' then
if not files.isOpen(uri) then
return false
end
end
end
if ws.isIgnored(uri) then
local status = config.get(uri, 'Lua.diagnostics.ignoredFiles')
if status == 'Disable' then
return false
elseif status == 'Opened' then
if not files.isOpen(uri) then
return false
end
end
end
local scheme = furi.split(uri)
local disableScheme = config.get(uri, 'Lua.diagnostics.disableScheme')
if util.arrayHas(disableScheme, scheme) then
return false
end
return true
end
---@async
function m.doDiagnostic(uri, isScopeDiag)
if not isValid(uri) then
return
end
await.delay()
local state = files.getState(uri)
if not state then
m.clear(uri)
return
end
local version = files.getVersion(uri)
local prog <close> = progress.create(uri, lang.script.WINDOW_DIAGNOSING, 0.5)
prog:setMessage(ws.getRelativePath(uri))
--log.debug('Diagnostic file:', uri)
local syntax = m.syntaxErrors(uri, state)
local diags = {}
local lastDiag = copyDiagsWithoutSyntax(m.cache[uri])
local function pushResult()
tracy.ZoneBeginN 'mergeSyntaxAndDiags'
local _ <close> = tracy.ZoneEnd
local full = mergeDiags(syntax, lastDiag, diags)
--log.debug(('Pushed [%d] results'):format(full and #full or 0))
if not full then
m.clear(uri)
return
end
if util.equal(m.cache[uri], full) then
return
end
m.cache[uri] = full
proto.notify('textDocument/publishDiagnostics', {
uri = uri,
version = version,
diagnostics = full,
})
log.debug('publishDiagnostics', uri, #full)
end
pushResult()
local lastPushClock = time.time()
---@async
xpcall(core, log.error, uri, isScopeDiag, function (result)
diags[#diags+1] = buildDiagnostic(uri, result)
if not isScopeDiag and time.time() - lastPushClock >= 500 then
lastPushClock = time.time()
pushResult()
end
end, function (checkedName)
if not lastDiag then
return
end
for i, diag in ipairs(lastDiag) do
if diag.code == checkedName then
lastDiag[i] = lastDiag[#lastDiag]
lastDiag[#lastDiag] = nil
end
end
end)
lastDiag = nil
pushResult()
end
---@param uri uri
function m.resendDiagnostic(uri)
local full = m.cache[uri]
if not full then
return
end
local version = files.getVersion(uri)
proto.notify('textDocument/publishDiagnostics', {
uri = uri,
version = version,
diagnostics = full,
})
log.debug('publishDiagnostics', uri, #full)
end
---@async
---@return table|nil result
---@return boolean? unchanged
function m.pullDiagnostic(uri, isScopeDiag)
if not isValid(uri) then
return nil, util.equal(m.cache[uri], nil)
end
await.delay()
local state = files.getState(uri)
if not state then
return nil, util.equal(m.cache[uri], nil)
end
local prog <close> = progress.create(uri, lang.script.WINDOW_DIAGNOSING, 0.5)
prog:setMessage(ws.getRelativePath(uri))
local syntax = m.syntaxErrors(uri, state)
local diags = {}
xpcall(core, log.error, uri, isScopeDiag, function (result)
diags[#diags+1] = buildDiagnostic(uri, result)
end)
local full = mergeDiags(syntax, diags)
if util.equal(m.cache[uri], full) then
return full, true
end
m.cache[uri] = full
return full
end
---@param uri uri
function m.refresh(uri)
if not ws.isReady(uri) then
return
end
await.close('diag:' .. uri)
---@async
await.call(function ()
await.setID('diag:' .. uri)
await.sleep(0.1)
xpcall(m.doDiagnostic, log.error, uri)
end)
local scp = scope.getScope(uri)
local scopeID = 'diagnosticsScope:' .. scp:getName()
await.close(scopeID)
---@async
await.call(function ()
local delay = config.get(uri, 'Lua.diagnostics.workspaceDelay') / 1000
if delay < 0 then
return
end
await.sleep(math.max(delay, 0.2))
m.diagnosticsScope(uri)
end)
end
---@async
local function askForDisable(uri)
if m.dontAskedForDisable then
return
end
local delay = 30
local delayTitle = lang.script('WINDOW_DELAY_WS_DIAGNOSTIC', delay)
local item = proto.awaitRequest('window/showMessageRequest', {
type = define.MessageType.Info,
message = lang.script.WINDOW_SETTING_WS_DIAGNOSTIC,
actions = {
{
title = lang.script.WINDOW_DONT_SHOW_AGAIN,
},
{
title = delayTitle,
},
{
title = lang.script.WINDOW_DISABLE_DIAGNOSTIC,
},
}
})
if not item then
return
end
if item.title == lang.script.WINDOW_DONT_SHOW_AGAIN then
m.dontAskedForDisable = true
elseif item.title == delayTitle then
client.setConfig {
{
key = 'Lua.diagnostics.workspaceDelay',
action = 'set',
value = delay * 1000,
uri = uri,
}
}
elseif item.title == lang.script.WINDOW_DISABLE_DIAGNOSTIC then
client.setConfig {
{
key = 'Lua.diagnostics.workspaceDelay',
action = 'set',
value = -1,
uri = uri,
}
}
end
end
---@async
function m.awaitDiagnosticsScope(suri, callback)
local scp = scope.getScope(suri)
while loading.count() > 0 do
await.sleep(1.0)
end
local clock = os.clock()
local bar <close> = progress.create(suri, lang.script.WORKSPACE_DIAGNOSTIC, 1)
local cancelled
bar:onCancel(function ()
log.info('Cancel workspace diagnostics')
cancelled = true
---@async
await.call(function ()
askForDisable(suri)
end)
end)
local uris = files.getAllUris(suri)
local sortedUris = ltable()
for _, uri in ipairs(uris) do
if files.isOpen(uri) then
sortedUris:pushHead(uri)
else
sortedUris:pushTail(uri)
end
end
log.info(('Diagnostics scope [%s], files count:[%d]'):format(scp:getName(), #uris))
local i = 0
for uri in sortedUris:pairs() do
while loading.count() > 0 do
await.sleep(1.0)
end
i = i + 1
bar:setMessage(('%d/%d'):format(i, #uris))
bar:setPercentage(i / #uris * 100)
callback(uri)
await.delay()
if cancelled then
log.info('Break workspace diagnostics')
break
end
end
bar:remove()
log.info(('Diagnostics scope [%s] finished, takes [%.3f] sec.'):format(scp:getName(), os.clock() - clock))
end
function m.diagnosticsScope(uri, force)
if not ws.isReady(uri) then
return
end
if not force and not config.get(uri, 'Lua.diagnostics.enable') then
m.clearAll()
return
end
if not force and config.get(uri, 'Lua.diagnostics.workspaceDelay') < 0 then
return
end
local scp = scope.getScope(uri)
local id = 'diagnosticsScope:' .. scp:getName()
await.close(id)
await.call(function () ---@async
await.sleep(0.0)
m.awaitDiagnosticsScope(uri, function (fileUri)
xpcall(m.doDiagnostic, log.error, fileUri, true)
end)
end, id)
end
---@async
function m.pullDiagnosticScope(callback)
local processing = 0
for _, scp in ipairs(scope.folders) do
if ws.isReady(scp.uri)
and config.get(scp.uri, 'Lua.diagnostics.enable') then
local id = 'diagnosticsScope:' .. scp:getName()
await.close(id)
await.call(function () ---@async
processing = processing + 1
local _ <close> = util.defer(function ()
processing = processing - 1
end)
local delay = config.get(scp.uri, 'Lua.diagnostics.workspaceDelay') / 1000
if delay < 0 then
return
end
print(delay)
await.sleep(math.max(delay, 0.2))
print('start')
m.awaitDiagnosticsScope(scp.uri, function (fileUri)
local suc, result, unchanged = xpcall(m.pullDiagnostic, log.error, fileUri, true)
if suc then
callback {
uri = fileUri,
result = result,
unchanged = unchanged,
version = files.getVersion(fileUri),
}
end
end)
end, id)
end
end
-- sleep for ever
while true do
await.sleep(1.0)
end
end
function m.refreshClient()
log.debug('Refresh client diagnostics')
proto.request('workspace/diagnostic/refresh', json.null)
end
ws.watch(function (ev, uri)
if ev == 'reload' then
m.diagnosticsScope(uri)
m.refreshClient()
end
end)
files.watch(function (ev, uri) ---@async
if ev == 'remove' then
m.clear(uri)
m.refresh(uri)
elseif ev == 'update' then
m.refresh(uri)
elseif ev == 'open' then
if ws.isReady(uri) then
m.resendDiagnostic(uri)
xpcall(m.doDiagnostic, log.error, uri)
end
elseif ev == 'close' then
if files.isLibrary(uri, true)
or ws.isIgnored(uri) then
m.clear(uri)
end
end
end)
config.watch(function (uri, key, value, oldValue)
if util.stringStartWith(key, 'Lua.diagnostics')
or util.stringStartWith(key, 'Lua.spell') then
if value ~= oldValue then
m.diagnosticsScope(uri)
m.refreshClient()
end
end
end)
fw.event(function (ev, path)
if util.stringEndWith(path, '.editorconfig') then
for _, scp in ipairs(ws.folders) do
m.diagnosticsScope(scp.uri)
m.refreshClient()
end
end
end)
return m