nixos/lua-lsp/script/provider/provider.lua

1574 lines
48 KiB
Lua
Raw Normal View History

local util = require 'utility'
local cap = require 'provider.capability'
local await = require 'await'
local files = require 'files'
local proto = require 'proto.proto'
local define = require 'proto.define'
local workspace = require 'workspace'
local config = require 'config'
local client = require 'client'
local pub = require 'pub'
local lang = require 'language'
local progress = require 'progress'
local tm = require 'text-merger'
local cfgLoader = require 'config.loader'
local converter = require 'proto.converter'
local filewatch = require 'filewatch'
local json = require 'json'
local scope = require 'workspace.scope'
local furi = require 'file-uri'
local inspect = require 'inspect'
local guide = require 'parser.guide'
local fs = require 'bee.filesystem'
require 'library'
---@async
local function updateConfig(uri)
config.addNullSymbol(json.null)
local specified = cfgLoader.loadLocalConfig(uri, CONFIGPATH)
if specified then
log.info('Load config from specified', CONFIGPATH)
log.info(inspect(specified))
-- watch directory
filewatch.watch(workspace.getAbsolutePath(uri, CONFIGPATH):gsub('[^/\\]+$', ''))
config.update(scope.override, specified)
end
for _, folder in ipairs(scope.folders) do
local clientConfig = cfgLoader.loadClientConfig(folder.uri)
if clientConfig then
log.info('Load config from client', folder.uri)
log.info(inspect(clientConfig))
end
local rc = cfgLoader.loadRCConfig(folder.uri, '.luarc.json')
or cfgLoader.loadRCConfig(folder.uri, '.luarc.jsonc')
if rc then
log.info('Load config from .luarc.json/.luarc.jsonc', folder.uri)
log.info(inspect(rc))
end
config.update(folder, clientConfig, rc)
end
local global = cfgLoader.loadClientConfig()
log.info('Load config from client', 'fallback')
log.info(inspect(global))
config.update(scope.fallback, global)
end
---@class provider
local m = {}
m.attributes = {}
function m.register(method)
return function (attrs)
m.attributes[method] = attrs
if attrs.preview and not PREVIEW then
return
end
if attrs.capability then
cap.filling(attrs.capability)
end
proto.on(method, attrs[1])
end
end
filewatch.event(function (ev, path) ---@async
if (CONFIGPATH and util.stringEndWith(path, CONFIGPATH)) then
for _, scp in ipairs(workspace.folders) do
local configPath = workspace.getAbsolutePath(scp.uri, CONFIGPATH)
if path == configPath then
updateConfig(scp.uri)
end
end
end
if util.stringEndWith(path, '.luarc.json') then
for _, scp in ipairs(workspace.folders) do
local rcPath = workspace.getAbsolutePath(scp.uri, '.luarc.json')
if path == rcPath then
updateConfig(scp.uri)
end
end
end
if util.stringEndWith(path, '.luarc.jsonc') then
for _, scp in ipairs(workspace.folders) do
local rcPath = workspace.getAbsolutePath(scp.uri, '.luarc.jsonc')
if path == rcPath then
updateConfig(scp.uri)
end
end
end
end)
m.register 'initialize' {
function(params)
client.init(params)
if params.rootUri then
workspace.initRoot(params.rootUri)
cap.resolve('ROOT_URI', furi.decode(params.rootUri):gsub('\\', '/') .. '/')
end
if params.workspaceFolders then
for _, folder in ipairs(params.workspaceFolders) do
workspace.create(folder.uri)
end
elseif params.rootUri then
workspace.create(params.rootUri)
end
local response = {
capabilities = cap.getProvider(),
serverInfo = {
name = 'sumneko.lua',
},
}
log.debug('Server init', inspect(response))
return response
end
}
m.register 'initialized'{
---@async
function (params)
files.init()
local _ <close> = progress.create(workspace.getFirstScope().uri, lang.script.WINDOW_INITIALIZING, 0.5)
updateConfig()
local registrations = {}
if client.getAbility 'workspace.didChangeConfiguration.dynamicRegistration' then
-- 监视配置变化
registrations[#registrations+1] = {
id = 'workspace/didChangeConfiguration',
method = 'workspace/didChangeConfiguration',
}
end
if #registrations ~= 0 then
proto.awaitRequest('client/registerCapability', {
registrations = registrations
})
end
client.setReady()
workspace.init()
return true
end
}
m.register 'exit' {
function ()
log.info('Server exited.')
os.exit(0, true)
end
}
m.register 'shutdown' {
function ()
log.info('Server shutdown.')
return true
end
}
m.register 'workspace/didChangeConfiguration' {
function () ---@async
if CONFIGPATH then
return
end
updateConfig()
end
}
m.register 'workspace/didRenameFiles' {
capability = {
workspace = {
fileOperations = {
didRename = {
filters = {
{
pattern = {
glob = '{ROOT_URI}**',
options = {
ignoreCase = true,
}
},
},
},
},
},
},
},
---@async
function (params)
log.debug('workspace/didRenameFiles', inspect(params))
local renames = {}
for _, file in ipairs(params.files) do
local oldUri = furi.normalize(file.oldUri)
local newUri = furi.normalize(file.newUri)
if workspace.isValidLuaUri(oldUri)
and workspace.isValidLuaUri(newUri) then
renames[#renames+1] = {
oldUri = oldUri,
newUri = newUri,
}
end
local childs = files.getChildFiles(oldUri)
for _, uri in ipairs(childs) do
if files.exists(uri) then
local ouri = uri
local tail = ouri:sub(#oldUri)
local nuri = file.newUri .. tail
renames[#renames+1] = {
oldUri = ouri,
newUri = nuri,
}
end
end
end
local core = require 'core.modifyRequirePath'
core(renames)
end
}
m.register 'workspace/didChangeWorkspaceFolders' {
capability = {
workspace = {
workspaceFolders = {
supported = true,
changeNotifications = true,
},
},
},
---@async
function (params)
log.debug('workspace/didChangeWorkspaceFolders', inspect(params))
for _, folder in ipairs(params.event.added) do
workspace.create(folder.uri)
updateConfig()
workspace.reload(scope.getScope(folder.uri))
end
for _, folder in ipairs(params.event.removed) do
workspace.remove(folder.uri)
end
end
}
m.register 'textDocument/didOpen' {
function (params)
local doc = params.textDocument
local scheme = furi.split(doc.uri)
local supports = config.get(doc.uri, 'Lua.workspace.supportScheme')
if not util.arrayHas(supports, scheme) then
return
end
local uri = files.getRealUri(doc.uri)
log.debug('didOpen', uri)
local text = doc.text
files.setText(uri, text, true, function (file)
file.version = doc.version
end)
files.open(uri)
end
}
m.register 'textDocument/didClose' {
function (params)
local doc = params.textDocument
local uri = files.getRealUri(doc.uri)
log.debug('didClose', uri)
files.close(uri)
if not files.isLua(uri) then
files.remove(uri)
end
end
}
m.register 'textDocument/didChange' {
---@async
function (params)
local doc = params.textDocument
local scheme = furi.split(doc.uri)
local supports = config.get(doc.uri, 'Lua.workspace.supportScheme')
if not util.arrayHas(supports, scheme) then
return
end
local changes = params.contentChanges
local uri = files.getRealUri(doc.uri)
local text = files.getOriginText(uri)
if not text then
files.setText(uri, pub.awaitTask('loadFile', furi.decode(uri)), false)
return
end
local rows = files.getCachedRows(uri)
text, rows = tm(text, rows, changes)
files.setText(uri, text, true, function (file)
file.version = doc.version
end)
files.setCachedRows(uri, rows)
end
}
m.register 'textDocument/didSave' {
capability = {
textDocumentSync = {
save = {
includeText = false,
},
}
},
---@async
function (params)
local doc = params.textDocument
local uri = files.getRealUri(doc.uri)
files.onWatch('save', uri)
end
}
m.register 'textDocument/hover' {
capability = {
hoverProvider = true,
},
abortByFileUpdate = true,
---@async
function (params)
local doc = params.textDocument
local uri = files.getRealUri(doc.uri)
if not config.get(uri, 'Lua.hover.enable') then
return
end
if not workspace.isReady(uri) then
local count, max = workspace.getLoadingProcess(uri)
return {
contents = {
value = lang.script('HOVER_WS_LOADING', count, max),
kind = 'markdown',
}
}
end
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_HOVER, 0.5)
local core = require 'core.hover'
local state = files.getState(uri)
if not state then
return nil
end
local pos = converter.unpackPosition(state, params.position)
local hover, source = core.byUri(uri, pos)
if not hover or not source then
return nil
end
return {
contents = {
value = tostring(hover),
kind = 'markdown',
},
range = converter.packRange(state, source.start, source.finish),
}
end
}
m.register 'textDocument/definition' {
capability = {
definitionProvider = true,
},
abortByFileUpdate = true,
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_DEFINITION, 0.5)
local state = files.getState(uri)
if not state then
return nil
end
local core = require 'core.definition'
local pos = converter.unpackPosition(state, params.position)
local result = core(uri, pos)
if not result then
return nil
end
local response = {}
for i, info in ipairs(result) do
---@type uri
local targetUri = info.uri
if targetUri then
local targetState = files.getState(targetUri)
if targetState then
if client.getAbility 'textDocument.definition.linkSupport' then
response[i] = converter.locationLink(targetUri
, converter.packRange(targetState, info.target.start, info.target.finish)
, converter.packRange(targetState, info.target.start, info.target.finish)
, converter.packRange(state, info.source.start, info.source.finish)
)
else
response[i] = converter.location(targetUri
, converter.packRange(targetState, info.target.start, info.target.finish)
)
end
else
response[i] = converter.location(
targetUri,
converter.range(
converter.position(guide.rowColOf(info.target.start)),
converter.position(guide.rowColOf(info.target.finish))
)
)
end
end
end
return response
end
}
m.register 'textDocument/typeDefinition' {
capability = {
typeDefinitionProvider = true,
},
abortByFileUpdate = true,
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_TYPE_DEFINITION, 0.5)
local state = files.getState(uri)
if not state then
return
end
local core = require 'core.type-definition'
local pos = converter.unpackPosition(state, params.position)
local result = core(uri, pos)
if not result then
return nil
end
local response = {}
for i, info in ipairs(result) do
---@type uri
local targetUri = info.uri
if targetUri then
local targetState = files.getState(targetUri)
if targetState then
if client.getAbility 'textDocument.typeDefinition.linkSupport' then
response[i] = converter.locationLink(targetUri
, converter.packRange(targetState, info.target.start, info.target.finish)
, converter.packRange(targetState, info.target.start, info.target.finish)
, converter.packRange(state, info.source.start, info.source.finish)
)
else
response[i] = converter.location(targetUri
, converter.packRange(targetState, info.target.start, info.target.finish)
)
end
end
end
end
return response
end
}
m.register 'textDocument/references' {
capability = {
referencesProvider = true,
},
abortByFileUpdate = true,
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_REFERENCE, 0.5)
local state = files.getState(uri)
if not state then
return nil
end
local core = require 'core.reference'
local pos = converter.unpackPosition(state, params.position)
local result = core(uri, pos, params.context.includeDeclaration)
if not result then
return nil
end
local response = {}
for i, info in ipairs(result) do
---@type uri
local targetUri = info.uri
local targetState = files.getState(targetUri)
if targetState then
response[#response+1] = converter.location(targetUri
, converter.packRange(targetState, info.target.start, info.target.finish)
)
end
end
return response
end
}
m.register 'textDocument/documentHighlight' {
capability = {
documentHighlightProvider = true,
},
---@async
function (params)
local core = require 'core.highlight'
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local state = files.getState(uri)
if not state then
return nil
end
local pos = converter.unpackPosition(state, params.position)
local result = core(uri, pos)
if not result then
return nil
end
local response = {}
for _, info in ipairs(result) do
response[#response+1] = {
range = converter.packRange(state, info.start, info.finish),
kind = info.kind,
}
end
return response
end
}
m.register 'textDocument/rename' {
capability = {
renameProvider = {
prepareProvider = true,
},
},
abortByFileUpdate = true,
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_RENAME, 0.5)
local state = files.getState(uri)
if not state then
return nil
end
local core = require 'core.rename'
local pos = converter.unpackPosition(state, params.position)
local result = core.rename(uri, pos, params.newName)
if not result then
return nil
end
local workspaceEdit = {
changes = {},
}
for _, info in ipairs(result) do
---@type uri
local ruri = info.uri
local rstate = files.getState(ruri)
if rstate then
if not workspaceEdit.changes[ruri] then
workspaceEdit.changes[ruri] = {}
end
local textEdit = converter.textEdit(converter.packRange(rstate, info.start, info.finish), info.text)
workspaceEdit.changes[ruri][#workspaceEdit.changes[ruri]+1] = textEdit
end
end
return workspaceEdit
end
}
m.register 'textDocument/prepareRename' {
abortByFileUpdate = true,
function (params)
local core = require 'core.rename'
local uri = files.getRealUri(params.textDocument.uri)
local state = files.getState(uri)
if not state then
return nil
end
local pos = converter.unpackPosition(state, params.position)
local result = core.prepareRename(uri, pos)
if not result then
return nil
end
return {
range = converter.packRange(state, result.start, result.finish),
placeholder = result.text,
}
end
}
m.register 'textDocument/completion' {
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
if not workspace.isReady(uri) then
local count, max = workspace.getLoadingProcess(uri)
return {
{
label = lang.script('HOVER_WS_LOADING', count, max),
textEdit = {
range = {
start = params.position,
['end'] = params.position,
},
newText = '',
},
}
}
end
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_COMPLETION, 0.5)
--log.info(util.dump(params))
local core = require 'core.completion'
--log.debug('textDocument/completion')
--log.debug('completion:', params.context and params.context.triggerKind, params.context and params.context.triggerCharacter)
local state = files.getState(uri)
if not state then
return
end
local triggerCharacter = params.context and params.context.triggerCharacter
if config.get(uri, 'editor.acceptSuggestionOnEnter') ~= 'off' then
if triggerCharacter == '\n'
or triggerCharacter == '{'
or triggerCharacter == ',' then
return
end
end
--await.setPriority(1000)
local clock = os.clock()
local pos = converter.unpackPosition(state, params.position)
local result = core.completion(uri, pos, triggerCharacter)
local passed = os.clock() - clock
if passed > 0.1 then
log.warn(('Completion takes %.3f sec.'):format(passed))
end
if not result then
return nil
end
tracy.ZoneBeginN 'completion make'
local _ <close> = tracy.ZoneEnd
local easy = false
local items = {}
for i, res in ipairs(result) do
local item = {
label = res.label,
kind = res.kind,
detail = res.detail,
deprecated = res.deprecated,
sortText = res.sortText or ('%04d'):format(i),
filterText = res.filterText,
insertText = res.insertText,
insertTextFormat = 2,
commitCharacters = res.commitCharacters,
command = res.command,
textEdit = res.textEdit and {
range = converter.packRange(
state,
res.textEdit.start,
res.textEdit.finish
),
newText = res.textEdit.newText,
},
additionalTextEdits = res.additionalTextEdits and (function ()
local t = {}
for j, edit in ipairs(res.additionalTextEdits) do
t[j] = {
range = converter.packRange(
state,
edit.start,
edit.finish
),
newText = edit.newText,
}
end
return t
end)(),
documentation = res.description and {
value = tostring(res.description),
kind = 'markdown',
},
}
if res.id then
if easy and os.clock() - clock < 0.05 then
local resolved = core.resolve(res.id)
if resolved then
item.detail = resolved.detail
item.documentation = resolved.description and {
value = tostring(resolved.description),
kind = 'markdown',
}
end
else
easy = false
item.data = {
uri = uri,
id = res.id,
}
end
end
items[i] = item
end
if result.incomplete == nil then
result.incomplete = false
end
return {
isIncomplete = result.incomplete,
items = items,
}
end
}
m.register 'completionItem/resolve' {
---@async
function (item)
local core = require 'core.completion'
if not item.data then
return item
end
local id = item.data.id
local uri = item.data.uri
--await.setPriority(1000)
local state = files.getState(uri)
if not state then
return nil
end
local resolved = core.resolve(id)
if not resolved then
return nil
end
item.detail = resolved.detail or item.detail
item.documentation = resolved.description and {
value = tostring(resolved.description),
kind = 'markdown',
} or item.documentation
item.additionalTextEdits = resolved.additionalTextEdits and (function ()
local t = {}
for j, edit in ipairs(resolved.additionalTextEdits) do
t[j] = {
range = converter.packRange(
state,
edit.start,
edit.finish
),
newText = edit.newText,
}
end
return t
end)() or item.additionalTextEdits
return item
end
}
m.register 'textDocument/signatureHelp' {
capability = {
signatureHelpProvider = {
triggerCharacters = { '(', ',' },
},
},
abortByFileUpdate = true,
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
if not config.get(uri, 'Lua.signatureHelp.enable') then
return nil
end
workspace.awaitReady(uri)
local state = files.getState(uri)
if not state then
return nil
end
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_SIGNATURE, 0.5)
local pos = converter.unpackPosition(state, params.position)
local core = require 'core.signature'
local results = core(uri, pos)
if not results then
return nil
end
local infos = {}
for i, result in ipairs(results) do
local parameters = {}
for j, param in ipairs(result.params) do
parameters[j] = {
label = {
param.label[1],
param.label[2],
}
}
end
infos[i] = {
label = result.label,
parameters = parameters,
activeParameter = result.index - 1,
documentation = result.description and {
value = tostring(result.description),
kind = 'markdown',
},
}
end
return {
signatures = infos,
}
end
}
m.register 'textDocument/documentSymbol' {
capability = {
documentSymbolProvider = true,
},
abortByFileUpdate = true,
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_SYMBOL, 0.5)
local state = files.getState(uri)
if not state then
return nil
end
local core = require 'core.document-symbol'
local symbols = core(uri)
if not symbols then
return nil
end
---@async
local function convert(symbol)
await.delay()
symbol.range = converter.packRange(
state,
symbol.range[1],
symbol.range[2]
)
symbol.selectionRange = converter.packRange(
state,
symbol.selectionRange[1],
symbol.selectionRange[2]
)
if symbol.name == '' then
symbol.name = ' '
end
symbol.valueRange = nil
if symbol.children then
for _, child in ipairs(symbol.children) do
convert(child)
end
end
end
for _, symbol in ipairs(symbols) do
convert(symbol)
end
return symbols
end
}
m.register 'textDocument/codeAction' {
capability = {
codeActionProvider = {
codeActionKinds = {
'',
'quickfix',
'refactor.rewrite',
'refactor.extract',
},
resolveProvider = false,
},
},
abortByFileUpdate = true,
---@async
function (params)
local core = require 'core.code-action'
local uri = files.getRealUri(params.textDocument.uri)
local range = params.range
local diagnostics = params.context.diagnostics
workspace.awaitReady(uri)
local state = files.getState(uri)
if not state then
return nil
end
local start, finish = converter.unpackRange(state, range)
local results = core(uri, start, finish, diagnostics)
if not results or #results == 0 then
return nil
end
for _, res in ipairs(results) do
if res.edit then
---@param turi uri
for turi, changes in pairs(res.edit.changes) do
local tstate = files.getState(turi)
if tstate then
for _, change in ipairs(changes) do
change.range = converter.packRange(tstate, change.start, change.finish)
change.start = nil
change.finish = nil
end
end
end
end
end
return results
end
}
m.register 'workspace/executeCommand' {
capability = {
executeCommandProvider = {
commands = {
'lua.removeSpace',
'lua.solve',
'lua.jsonToLua',
'lua.setConfig',
'lua.autoRequire',
},
},
},
---@async
function (params)
local command = params.command:gsub(':.+', '')
if command == 'lua.removeSpace' then
local core = require 'core.command.removeSpace'
return core(params.arguments[1])
elseif command == 'lua.solve' then
local core = require 'core.command.solve'
return core(params.arguments[1])
elseif command == 'lua.jsonToLua' then
local core = require 'core.command.jsonToLua'
return core(params.arguments[1])
elseif command == 'lua.setConfig' then
local core = require 'core.command.setConfig'
return core(params.arguments[1])
elseif command == 'lua.autoRequire' then
local core = require 'core.command.autoRequire'
return core(params.arguments[1])
end
end
}
m.register 'workspace/symbol' {
capability = {
workspaceSymbolProvider = true,
},
abortByFileUpdate = true,
---@async
function (params)
local _ <close> = progress.create(workspace.getFirstScope().uri, lang.script.WINDOW_PROCESSING_WS_SYMBOL, 0.5)
local core = require 'core.workspace-symbol'
local symbols = core(params.query, nil, true)
if not symbols or #symbols == 0 then
return nil
end
local function convert(symbol)
local uri = guide.getUri(symbol.source)
local state = files.getState(uri)
if not state then
return nil
end
return {
name = symbol.name,
kind = symbol.skind,
location = converter.location(
uri,
converter.packRange(
state,
symbol.source.start,
symbol.source.finish
)
)
}
end
local results = {}
for _, symbol in ipairs(symbols) do
results[#results+1] = convert(symbol)
end
return results
end
}
local function toArray(map)
local array = {}
for k in pairs(map) do
array[#array+1] = k
end
table.sort(array, function (a, b)
return map[a] < map[b]
end)
return array
end
client.event(function (ev)
if ev == 'init' then
if not client.isVSCode() then
m.register 'textDocument/semanticTokens/full' {
capability = {
semanticTokensProvider = {
legend = {
tokenTypes = toArray(define.TokenTypes),
tokenModifiers = toArray(define.TokenModifiers),
},
full = true,
},
},
abortByFileUpdate = true,
---@async
function (params)
log.debug('textDocument/semanticTokens/full')
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
await.sleep(0.0)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_SEMANTIC_FULL, 0.5)
local core = require 'core.semantic-tokens'
local results = core(uri, 0, math.huge)
return {
data = results
}
end
}
end
end
end)
m.register 'textDocument/semanticTokens/range' {
capability = {
semanticTokensProvider = {
legend = {
tokenTypes = toArray(define.TokenTypes),
tokenModifiers = toArray(define.TokenModifiers),
},
range = true,
},
},
abortByFileUpdate = true,
---@async
function (params)
log.debug('textDocument/semanticTokens/range')
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_SEMANTIC_RANGE, 0.5)
await.sleep(0.0)
local state = files.getState(uri)
if not state then
return nil
end
local core = require 'core.semantic-tokens'
local start, finish = converter.unpackRange(state, params.range)
local results = core(uri, start, finish)
return {
data = results
}
end
}
m.register 'textDocument/foldingRange' {
capability = {
foldingRangeProvider = true,
},
---@async
function (params)
local core = require 'core.folding'
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
if not files.exists(uri) then
return nil
end
local state = files.getState(uri)
if not state then
return nil
end
local regions = core(uri)
if not regions then
return nil
end
local results = {}
for _, region in ipairs(regions) do
local startLine = converter.packPosition(state, region.start).line
local endLine = converter.packPosition(state, region.finish).line
if not region.hideLastLine then
endLine = endLine - 1
end
if startLine < endLine then
results[#results+1] = {
startLine = startLine,
endLine = endLine,
kind = region.kind,
}
end
end
return results
end
}
m.register 'textDocument/documentColor' {
capability = {
colorProvider = true
},
---@async
function (params)
local color = require 'core.color'
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local state = files.getState(uri)
if not state then
return nil
end
local colors = color.colors(uri)
if not colors then
return nil
end
local results = {}
for _, colorValue in ipairs(colors) do
results[#results+1] = {
range = converter.packRange(state, colorValue.start, colorValue.finish),
color = colorValue.color
}
end
return results
end
}
m.register 'textDocument/colorPresentation' {
function (params)
local color = (require 'core.color').colorToText(params.color)
return {{label = color}}
end
}
m.register 'window/workDoneProgress/cancel' {
function (params)
log.debug('close proto(cancel):', params.token)
progress.cancel(params.token)
end
}
m.register '$/status/click' {
---@async
function ()
local titleDiagnostic = lang.script.WINDOW_LUA_STATUS_DIAGNOSIS_TITLE
local result = client.awaitRequestMessage('Info', lang.script.WINDOW_LUA_STATUS_DIAGNOSIS_MSG, {
titleDiagnostic,
DEVELOP and 'Restart Server' or nil,
DEVELOP and 'Clear Node Cache' or nil,
})
if not result then
return
end
if result == titleDiagnostic then
local diagnostic = require 'provider.diagnostic'
for _, scp in ipairs(workspace.folders) do
diagnostic.diagnosticsScope(scp.uri, true)
end
elseif result == 'Restart Server' then
local diag = require 'provider.diagnostic'
diag.clearAll(nil, true)
os.exit(0, true)
elseif result == 'Clear Node Cache' then
local vm = require 'vm'
vm.clearNodeCache()
collectgarbage()
collectgarbage()
end
end
}
m.register 'textDocument/formatting' {
capability = {
documentFormattingProvider = true,
},
---@async
function(params)
local uri = files.getRealUri(params.textDocument.uri)
local state = files.getState(uri)
if not state then
return nil
end
if not config.get(uri, 'Lua.format.enable') then
return nil
end
local pformatting = require 'provider.formatting'
pformatting.updateConfig(uri)
local core = require 'core.formatting'
local edits = core(uri, params.options)
if not edits or #edits == 0 then
return nil
end
local results = {}
for i, edit in ipairs(edits) do
results[i] = {
range = converter.packRange(state, edit.start, edit.finish),
newText = edit.text,
}
end
return results
end
}
m.register 'textDocument/rangeFormatting' {
capability = {
documentRangeFormattingProvider = true,
},
---@async
function(params)
local uri = files.getRealUri(params.textDocument.uri)
local state = files.getState(uri)
if not state then
return nil
end
if not config.get(uri, 'Lua.format.enable') then
return nil
end
local pformatting = require 'provider.formatting'
pformatting.updateConfig(uri)
local core = require 'core.rangeformatting'
local edits = core(uri, params.range, params.options)
if not edits or #edits == 0 then
return nil
end
local results = {}
for i, edit in ipairs(edits) do
results[i] = {
range = converter.packRange(state, edit.start, edit.finish),
newText = edit.text,
}
end
return results
end
}
m.register 'textDocument/onTypeFormatting' {
capability = {
documentOnTypeFormattingProvider = {
firstTriggerCharacter = '\n',
moreTriggerCharacter = nil, -- string[]
},
},
abortByFileUpdate = true,
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_TYPE_FORMATTING, 0.5)
local ch = params.ch
local state = files.getState(uri)
if not state then
return nil
end
local core = require 'core.type-formatting'
local pos = converter.unpackPosition(state, params.position)
local edits = core(uri, pos, ch, params.options)
if not edits or #edits == 0 then
return nil
end
local tab = '\t'
if params.options.insertSpaces then
tab = (' '):rep(params.options.tabSize)
end
local results = {}
for i, edit in ipairs(edits) do
results[i] = {
range = converter.packRange(state, edit.start, edit.finish),
newText = edit.text:gsub('\t', tab),
}
end
return results
end
}
m.register '$/cancelRequest' {
function (params)
proto.close(params.id, define.ErrorCodes.RequestCancelled, 'Request cancelled.')
end
}
m.register '$/requestHint' {
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
if not config.get(uri, 'Lua.hint.enable') then
return
end
workspace.awaitReady(uri)
local state = files.getState(uri)
if not state then
return
end
local core = require 'core.hint'
local start, finish = converter.unpackRange(state, params.range)
local results = core(uri, start, finish)
local hintResults = {}
for i, res in ipairs(results) do
hintResults[i] = {
text = res.text,
pos = converter.packPosition(state, res.offset),
kind = res.kind,
}
end
return hintResults
end
}
m.register 'textDocument/inlayHint' {
capability = {
inlayHintProvider = {
resolveProvider = true,
},
},
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
if not config.get(uri, 'Lua.hint.enable') then
return nil
end
workspace.awaitReady(uri)
local core = require 'core.hint'
local state = files.getState(uri)
if not state then
return nil
end
local start, finish = converter.unpackRange(state, params.range)
local results = core(uri, start, finish)
local hintResults = {}
for i, res in ipairs(results) do
local luri = res.source and guide.getUri(res.source)
local lstate = files.getState(luri)
hintResults[i] = {
label = {
{
value = res.text,
tooltip = res.tooltip,
location = lstate and converter.location(
luri,
converter.packRange(
lstate,
res.source.start,
res.source.finish
)
),
},
},
position = converter.packPosition(state, res.offset),
kind = res.kind,
paddingLeft = true,
paddingRight = true,
}
end
return hintResults
end
}
m.register 'inlayHint/resolve' {
capability = {
inlayHintProvider = {
resolveProvider = true,
},
},
---@async
function (hint)
return hint
end
}
m.register 'textDocument/diagnostic' {
preview = true,
capability = {
diagnosticProvider = {
identifier = 'identifier',
interFileDependencies = true,
workspaceDiagnostics = false,
}
},
---@async
function (params)
local uri = files.getRealUri(params.textDocument.uri)
workspace.awaitReady(uri)
local core = require 'provider.diagnostic'
-- TODO: do some trick
core.doDiagnostic(uri)
return {
kind = 'unchanged',
resultId = uri,
}
--if not params.previousResultId then
-- core.clearCache(uri)
--end
--local results, unchanged = core.pullDiagnostic(uri, false)
--if unchanged then
-- return {
-- kind = 'unchanged',
-- resultId = uri,
-- }
--else
-- return {
-- kind = 'full',
-- resultId = uri,
-- items = results or {},
-- }
--end
end
}
m.register 'workspace/diagnostic' {
--preview = true,
--capability = {
-- diagnosticProvider = {
-- workspaceDiagnostics = false,
-- }
--},
---@async
function (params)
local core = require 'provider.diagnostic'
local excepts = {}
for _, id in ipairs(params.previousResultIds) do
excepts[#excepts+1] = id.value
end
core.clearCacheExcept(excepts)
local function convertItem(result)
if result.unchanged then
return {
kind = 'unchanged',
resultId = result.uri,
uri = result.uri,
version = result.version,
}
else
return {
kind = 'full',
resultId = result.uri,
items = result.result or {},
uri = result.uri,
version = result.version,
}
end
end
core.pullDiagnosticScope(function (result)
proto.notify('$/progress', {
token = params.partialResultToken,
value = {
items = {
convertItem(result)
}
}
})
end)
return { items = {} }
end
}
m.register '$/api/report' {
---@async
function (params)
local buildMeta = require 'provider.build-meta'
local SDBMHash = require 'SDBMHash'
await.close 'api/report'
await.setID 'api/report'
local name = params.name or 'default'
local uri = workspace.getFirstScope().uri
local hash = uri and ('%08x'):format(SDBMHash():hash(uri))
local encoding = config.get(nil, 'Lua.runtime.fileEncoding')
local nameBuf = {}
nameBuf[#nameBuf+1] = name
nameBuf[#nameBuf+1] = hash
nameBuf[#nameBuf+1] = encoding
local fileDir = METAPATH .. '/' .. table.concat(nameBuf, ' ')
fs.create_directories(fs.path(fileDir))
buildMeta.build(fileDir, params)
client.setConfig {
{
key = 'Lua.workspace.library',
action = 'add',
value = fileDir,
uri = uri,
}
}
end
}
m.register '$/psi/view' {
---@async
function (params)
local uri = files.getRealUri(params.uri)
workspace.awaitReady(uri)
local _ <close> = progress.create(uri, lang.script.WINDOW_PROCESSING_TYPE_FORMATTING, 0.5)
if not files.exists(uri) then
return nil
end
local core = require 'core.view.psi-view'
local result = core(uri)
return result
end
}
m.register '$/psi/select' {
---@async
function(params)
local uri = files.getRealUri(params.uri)
workspace.awaitReady(uri)
local _<close> = progress.create(uri, lang.script.WINDOW_PROCESSING_TYPE_FORMATTING, 0.5)
if not files.exists(uri) then
return nil
end
local core = require 'core.view.psi-select'
local result = core(uri, params.position)
return result
end
}
local function refreshStatusBar()
local valid = config.get(nil, 'Lua.window.statusBar')
for _, scp in ipairs(workspace.folders) do
if not config.get(scp.uri, 'Lua.window.statusBar') then
valid = false
break
end
end
if valid then
proto.notify('$/status/show')
else
proto.notify('$/status/hide')
end
end
config.watch(function (uri, key, value)
if key == 'Lua.window.statusBar'
or key == '' then
refreshStatusBar()
end
end)
m.register '$/status/refresh' { refreshStatusBar }
files.watch(function (ev, uri)
if not workspace.isReady(uri) then
return
end
if ev == 'update'
or ev == 'remove' then
for id, p in pairs(proto.holdon) do
if m.attributes[p.method].abortByFileUpdate then
log.debug('close proto(ContentModified):', id, p.method)
proto.close(id, define.ErrorCodes.ContentModified, 'Content modified.')
end
end
end
end)