623 lines
16 KiB
Lua
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
|