add cache and rename some files
This commit is contained in:
@@ -0,0 +1,668 @@
|
||||
local fs = require 'bee.filesystem'
|
||||
local nonil = require 'without-check-nil'
|
||||
local util = require 'utility'
|
||||
local lang = require 'language'
|
||||
local proto = require 'proto'
|
||||
local define = require 'proto.define'
|
||||
local config = require 'config'
|
||||
local converter = require 'proto.converter'
|
||||
local await = require 'await'
|
||||
local scope = require 'workspace.scope'
|
||||
local inspect = require 'inspect'
|
||||
local jsone = require 'json-edit'
|
||||
local jsonc = require 'jsonc'
|
||||
|
||||
local m = {}
|
||||
m._eventList = {}
|
||||
|
||||
function m.client(newClient)
|
||||
if newClient then
|
||||
m._client = newClient
|
||||
else
|
||||
return m._client
|
||||
end
|
||||
end
|
||||
|
||||
function m.isVSCode()
|
||||
if not m._client then
|
||||
return false
|
||||
end
|
||||
if m._isvscode == nil then
|
||||
local lname = m._client:lower()
|
||||
if lname:find 'vscode'
|
||||
or lname:find 'visual studio code' then
|
||||
m._isvscode = true
|
||||
else
|
||||
m._isvscode = false
|
||||
end
|
||||
end
|
||||
return m._isvscode
|
||||
end
|
||||
|
||||
function m.getOption(name)
|
||||
nonil.enable()
|
||||
local option = m.info.initializationOptions[name]
|
||||
nonil.disable()
|
||||
return option
|
||||
end
|
||||
|
||||
function m.getAbility(name)
|
||||
if not m.info
|
||||
or not m.info.capabilities then
|
||||
return nil
|
||||
end
|
||||
local current = m.info.capabilities
|
||||
while true do
|
||||
local parent, nextPos = name:match '^([^%.]+)()'
|
||||
if not parent then
|
||||
break
|
||||
end
|
||||
current = current[parent]
|
||||
if not current then
|
||||
return current
|
||||
end
|
||||
if nextPos > #name then
|
||||
break
|
||||
else
|
||||
name = name:sub(nextPos + 1)
|
||||
end
|
||||
end
|
||||
return current
|
||||
end
|
||||
|
||||
function m.getOffsetEncoding()
|
||||
if m._offsetEncoding then
|
||||
return m._offsetEncoding
|
||||
end
|
||||
local clientEncodings = m.getAbility 'offsetEncoding'
|
||||
if type(clientEncodings) == 'table' then
|
||||
for _, encoding in ipairs(clientEncodings) do
|
||||
if encoding == 'utf-8' then
|
||||
m._offsetEncoding = 'utf-8'
|
||||
return m._offsetEncoding
|
||||
end
|
||||
end
|
||||
end
|
||||
m._offsetEncoding = 'utf-16'
|
||||
return m._offsetEncoding
|
||||
end
|
||||
|
||||
local function packMessage(...)
|
||||
local strs = table.pack(...)
|
||||
for i = 1, strs.n do
|
||||
strs[i] = tostring(strs[i])
|
||||
end
|
||||
return table.concat(strs, '\t')
|
||||
end
|
||||
|
||||
---@alias message.type '"Error"'|'"Warning"'|'"Info"'|'"Log"'
|
||||
|
||||
---show message to client
|
||||
---@param type message.type
|
||||
function m.showMessage(type, ...)
|
||||
local message = packMessage(...)
|
||||
proto.notify('window/showMessage', {
|
||||
type = define.MessageType[type] or 3,
|
||||
message = message,
|
||||
})
|
||||
proto.notify('window/logMessage', {
|
||||
type = define.MessageType[type] or 3,
|
||||
message = message,
|
||||
})
|
||||
log.info('ShowMessage', type, message)
|
||||
end
|
||||
|
||||
---@param type message.type
|
||||
---@param message string
|
||||
---@param titles string[]
|
||||
---@param callback fun(action?: string, index?: integer)
|
||||
function m.requestMessage(type, message, titles, callback)
|
||||
proto.notify('window/logMessage', {
|
||||
type = define.MessageType[type] or 3,
|
||||
message = message,
|
||||
})
|
||||
local map = {}
|
||||
local actions = {}
|
||||
for i, title in ipairs(titles) do
|
||||
actions[i] = {
|
||||
title = title,
|
||||
}
|
||||
map[title] = i
|
||||
end
|
||||
log.info('requestMessage', type, message)
|
||||
proto.request('window/showMessageRequest', {
|
||||
type = define.MessageType[type] or 3,
|
||||
message = message,
|
||||
actions = actions,
|
||||
}, function (item)
|
||||
log.info('responseMessage', message, item and item.title or nil)
|
||||
if item then
|
||||
callback(item.title, map[item.title])
|
||||
else
|
||||
callback(nil, nil)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
---@param type message.type
|
||||
---@param message string
|
||||
---@param titles string[]
|
||||
---@return string action
|
||||
---@return integer index
|
||||
---@async
|
||||
function m.awaitRequestMessage(type, message, titles)
|
||||
return await.wait(function (waker)
|
||||
m.requestMessage(type, message, titles, waker)
|
||||
end)
|
||||
end
|
||||
|
||||
---@param type message.type
|
||||
function m.logMessage(type, ...)
|
||||
local message = packMessage(...)
|
||||
proto.notify('window/logMessage', {
|
||||
type = define.MessageType[type] or 4,
|
||||
message = message,
|
||||
})
|
||||
end
|
||||
|
||||
function m.watchFiles(path)
|
||||
path = path:gsub('\\', '/')
|
||||
:gsub('[%[%]%{%}%*%?]', '\\%1')
|
||||
local registration = {
|
||||
id = path,
|
||||
method = 'workspace/didChangeWatchedFiles',
|
||||
registerOptions = {
|
||||
watchers = {
|
||||
{
|
||||
globPattern = path .. '/**',
|
||||
kind = 1 | 2 | 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
proto.request('client/registerCapability', {
|
||||
registrations = {
|
||||
registration,
|
||||
}
|
||||
})
|
||||
|
||||
return function ()
|
||||
local unregisteration = {
|
||||
id = path,
|
||||
method = 'workspace/didChangeWatchedFiles',
|
||||
}
|
||||
proto.request('client/registerCapability', {
|
||||
unregisterations = {
|
||||
unregisteration,
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
---@class config.change
|
||||
---@field key string
|
||||
---@field prop? string
|
||||
---@field value any
|
||||
---@field action '"add"'|'"set"'|'"prop"'
|
||||
---@field global? boolean
|
||||
---@field uri? uri
|
||||
|
||||
---@param uri uri?
|
||||
---@param changes config.change[]
|
||||
---@return config.change[]
|
||||
local function getValidChanges(uri, changes)
|
||||
local newChanges = {}
|
||||
if not uri then
|
||||
return changes
|
||||
end
|
||||
local scp = scope.getScope(uri)
|
||||
for _, change in ipairs(changes) do
|
||||
if scp:isChildUri(change.uri)
|
||||
or scp:isLinkedUri(change.uri) then
|
||||
newChanges[#newChanges+1] = change
|
||||
end
|
||||
end
|
||||
return newChanges
|
||||
end
|
||||
|
||||
---@class json.patch
|
||||
---@field op 'add' | 'remove' | 'replace'
|
||||
---@field path string
|
||||
---@field value any
|
||||
|
||||
---@class json.patchInfo
|
||||
---@field key string
|
||||
---@field value any
|
||||
|
||||
---@param cfg table
|
||||
---@param rawKey string
|
||||
---@return json.patchInfo
|
||||
local function searchPatchInfo(cfg, rawKey)
|
||||
|
||||
---@param key string
|
||||
---@param parentKey string
|
||||
---@param parentValue table
|
||||
---@return json.patchInfo?
|
||||
local function searchOnce(key, parentKey, parentValue)
|
||||
if parentValue == nil then
|
||||
return nil
|
||||
end
|
||||
if type(parentValue) ~= 'table' then
|
||||
return {
|
||||
key = parentKey,
|
||||
value = parentValue,
|
||||
}
|
||||
end
|
||||
if parentValue[key] then
|
||||
return {
|
||||
key = parentKey .. '/' .. key,
|
||||
value = parentValue[key],
|
||||
}
|
||||
end
|
||||
for pos in key:gmatch '()%.' do
|
||||
local k = key:sub(1, pos - 1)
|
||||
local v = parentValue[k]
|
||||
local info = searchOnce(key:sub(pos + 1), parentKey .. '/' .. k, v)
|
||||
if info then
|
||||
return info
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return searchOnce(rawKey, '', cfg)
|
||||
or searchOnce(rawKey:gsub('^Lua%.', ''), '', cfg)
|
||||
or {
|
||||
key = '/' .. rawKey:gsub('^Lua%.', ''),
|
||||
value = nil,
|
||||
}
|
||||
end
|
||||
|
||||
---@param uri? uri
|
||||
---@param cfg table
|
||||
---@param change config.change
|
||||
---@return json.patch?
|
||||
local function makeConfigPatch(uri, cfg, change)
|
||||
local info = searchPatchInfo(cfg, change.key)
|
||||
if change.action == 'add' then
|
||||
if type(info.value) == 'table' and #info.value > 0 then
|
||||
return {
|
||||
op = 'add',
|
||||
path = info.key .. '/-',
|
||||
value = change.value,
|
||||
}
|
||||
else
|
||||
return makeConfigPatch(uri, cfg, {
|
||||
action = 'set',
|
||||
key = change.key,
|
||||
value = config.get(uri, change.key),
|
||||
})
|
||||
end
|
||||
elseif change.action == 'set' then
|
||||
if info.value ~= nil then
|
||||
return {
|
||||
op = 'replace',
|
||||
path = info.key,
|
||||
value = change.value,
|
||||
}
|
||||
else
|
||||
return {
|
||||
op = 'add',
|
||||
path = info.key,
|
||||
value = change.value,
|
||||
}
|
||||
end
|
||||
elseif change.action == 'prop' then
|
||||
if type(info.value) == 'table' and next(info.value) then
|
||||
return {
|
||||
op = 'add',
|
||||
path = info.key .. '/' .. change.prop,
|
||||
value = change.value,
|
||||
}
|
||||
else
|
||||
return makeConfigPatch(uri, cfg, {
|
||||
action = 'set',
|
||||
key = change.key,
|
||||
value = config.get(uri, change.key),
|
||||
})
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
---@param uri? uri
|
||||
---@param path string
|
||||
---@param changes config.change[]
|
||||
---@return string?
|
||||
local function editConfigJson(uri, path, changes)
|
||||
local text = util.loadFile(path)
|
||||
if not text then
|
||||
m.showMessage('Error', lang.script('CONFIG_LOAD_FAILED', path))
|
||||
return nil
|
||||
end
|
||||
local suc, res = pcall(jsonc.decode_jsonc, text)
|
||||
if not suc then
|
||||
m.showMessage('Error', lang.script('CONFIG_MODIFY_FAIL_SYNTAX_ERROR', path .. res:match 'ERROR(.+)$'))
|
||||
return nil
|
||||
end
|
||||
if type(res) ~= 'table' then
|
||||
res = {}
|
||||
end
|
||||
---@cast res table
|
||||
for _, change in ipairs(changes) do
|
||||
local patch = makeConfigPatch(uri, res, change)
|
||||
if patch then
|
||||
text = jsone.edit(text, patch, { indent = ' ' })
|
||||
end
|
||||
end
|
||||
return text
|
||||
end
|
||||
|
||||
---@param changes config.change[]
|
||||
---@param applied config.change[]
|
||||
local function removeAppliedChanges(changes, applied)
|
||||
local appliedMap = {}
|
||||
for _, change in ipairs(applied) do
|
||||
appliedMap[change] = true
|
||||
end
|
||||
for i = #changes, 1, -1 do
|
||||
if appliedMap[changes[i]] then
|
||||
table.remove(changes, i)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function tryModifySpecifiedConfig(uri, finalChanges)
|
||||
if #finalChanges == 0 then
|
||||
return false
|
||||
end
|
||||
log.info('tryModifySpecifiedConfig', uri, inspect(finalChanges))
|
||||
local workspace = require 'workspace'
|
||||
local scp = scope.getScope(uri)
|
||||
if scp:get('lastLocalType') ~= 'json' then
|
||||
log.info('lastLocalType ~= json')
|
||||
return false
|
||||
end
|
||||
local validChanges = getValidChanges(uri, finalChanges)
|
||||
if #validChanges == 0 then
|
||||
log.info('No valid changes')
|
||||
return false
|
||||
end
|
||||
local path = workspace.getAbsolutePath(uri, CONFIGPATH)
|
||||
if not path then
|
||||
log.info('Can not get absolute path')
|
||||
return false
|
||||
end
|
||||
local newJson = editConfigJson(uri, path, validChanges)
|
||||
if not newJson then
|
||||
log.info('Can not edit config json')
|
||||
return false
|
||||
end
|
||||
util.saveFile(path, newJson)
|
||||
log.info('Apply changes to config file', inspect(validChanges))
|
||||
removeAppliedChanges(finalChanges, validChanges)
|
||||
return true
|
||||
end
|
||||
|
||||
local function tryModifyRC(uri, finalChanges, create)
|
||||
if #finalChanges == 0 then
|
||||
return false
|
||||
end
|
||||
log.info('tryModifyRC', uri, inspect(finalChanges))
|
||||
local workspace = require 'workspace'
|
||||
local path = workspace.getAbsolutePath(uri, '.luarc.jsonc')
|
||||
if not path then
|
||||
log.info('Can not get absolute path of .luarc.jsonc')
|
||||
return false
|
||||
end
|
||||
path = fs.exists(fs.path(path)) and path or workspace.getAbsolutePath(uri, '.luarc.json')
|
||||
if not path then
|
||||
log.info('Can not get absolute path of .luarc.json')
|
||||
return false
|
||||
end
|
||||
local buf = util.loadFile(path)
|
||||
if not buf and not create then
|
||||
log.info('Can not load .luarc.json and not create')
|
||||
return false
|
||||
end
|
||||
local validChanges = getValidChanges(uri, finalChanges)
|
||||
if #validChanges == 0 then
|
||||
log.info('No valid changes')
|
||||
return false
|
||||
end
|
||||
if not buf then
|
||||
util.saveFile(path, '')
|
||||
end
|
||||
local newJson = editConfigJson(uri, path, validChanges)
|
||||
if not newJson then
|
||||
log.info('Can not edit config json')
|
||||
return false
|
||||
end
|
||||
util.saveFile(path, newJson)
|
||||
log.info('Apply changes to .luarc.json', inspect(validChanges))
|
||||
removeAppliedChanges(finalChanges, validChanges)
|
||||
return true
|
||||
end
|
||||
|
||||
local function tryModifyClient(uri, finalChanges)
|
||||
if #finalChanges == 0 then
|
||||
return false
|
||||
end
|
||||
log.info('tryModifyClient', uri, inspect(finalChanges))
|
||||
if not m.getOption 'changeConfiguration' then
|
||||
return false
|
||||
end
|
||||
local scp = scope.getScope(uri)
|
||||
local scpChanges = {}
|
||||
for _, change in ipairs(finalChanges) do
|
||||
if change.uri
|
||||
and (scp:isChildUri(change.uri) or scp:isLinkedUri(change.uri)) then
|
||||
scpChanges[#scpChanges+1] = change
|
||||
end
|
||||
end
|
||||
if #scpChanges == 0 then
|
||||
log.info('No changes in client scope')
|
||||
return false
|
||||
end
|
||||
proto.notify('$/command', {
|
||||
command = 'lua.config',
|
||||
data = scpChanges,
|
||||
})
|
||||
log.info('Apply client changes', uri, inspect(scpChanges))
|
||||
removeAppliedChanges(finalChanges, scpChanges)
|
||||
return true
|
||||
end
|
||||
|
||||
---@param finalChanges config.change[]
|
||||
local function tryModifyClientGlobal(finalChanges)
|
||||
if #finalChanges == 0 then
|
||||
return
|
||||
end
|
||||
log.info('tryModifyClientGlobal', inspect(finalChanges))
|
||||
if not m.getOption 'changeConfiguration' then
|
||||
log.info('Client dose not support modifying config')
|
||||
return
|
||||
end
|
||||
local changes = {}
|
||||
for _, change in ipairs(finalChanges) do
|
||||
if change.global then
|
||||
changes[#changes+1] = change
|
||||
end
|
||||
end
|
||||
if #changes == 0 then
|
||||
log.info('No global changes')
|
||||
return
|
||||
end
|
||||
proto.notify('$/command', {
|
||||
command = 'lua.config',
|
||||
data = changes,
|
||||
})
|
||||
log.info('Apply client global changes', inspect(changes))
|
||||
removeAppliedChanges(finalChanges, changes)
|
||||
end
|
||||
|
||||
---@param changes config.change[]
|
||||
---@return string
|
||||
local function buildMaunuallyMessage(changes)
|
||||
local message = {}
|
||||
for _, change in ipairs(changes) do
|
||||
if change.action == 'add' then
|
||||
message[#message+1] = '* ' .. lang.script('WINDOW_MANUAL_CONFIG_ADD', change.key, change.value)
|
||||
elseif change.action == 'set' then
|
||||
message[#message+1] = '* ' .. lang.script('WINDOW_MANUAL_CONFIG_SET', change.key, change.value)
|
||||
elseif change.action == 'prop' then
|
||||
message[#message+1] = '* ' .. lang.script('WINDOW_MANUAL_CONFIG_PROP', change.key, change.prop, change.value)
|
||||
end
|
||||
end
|
||||
return table.concat(message, '\n')
|
||||
end
|
||||
|
||||
---@param changes config.change[]
|
||||
---@param onlyMemory? boolean
|
||||
function m.setConfig(changes, onlyMemory)
|
||||
local finalChanges = {}
|
||||
for _, change in ipairs(changes) do
|
||||
if change.action == 'add' then
|
||||
local suc = config.add(change.uri, change.key, change.value)
|
||||
if suc then
|
||||
finalChanges[#finalChanges+1] = change
|
||||
end
|
||||
elseif change.action == 'set' then
|
||||
local suc = config.set(change.uri, change.key, change.value)
|
||||
if suc then
|
||||
finalChanges[#finalChanges+1] = change
|
||||
end
|
||||
elseif change.action == 'prop' then
|
||||
local suc = config.prop(change.uri, change.key, change.prop, change.value)
|
||||
if suc then
|
||||
finalChanges[#finalChanges+1] = change
|
||||
end
|
||||
end
|
||||
end
|
||||
if onlyMemory then
|
||||
return
|
||||
end
|
||||
if #finalChanges == 0 then
|
||||
return
|
||||
end
|
||||
log.info('Modify config', inspect(finalChanges))
|
||||
xpcall(function ()
|
||||
local ws = require 'workspace'
|
||||
tryModifyClientGlobal(finalChanges)
|
||||
if #ws.folders == 0 then
|
||||
tryModifySpecifiedConfig(nil, finalChanges)
|
||||
tryModifyClient(nil, finalChanges)
|
||||
if #finalChanges > 0 then
|
||||
local manuallyModifyConfig = buildMaunuallyMessage(finalChanges)
|
||||
m.showMessage('Warning', lang.script('CONFIG_MODIFY_FAIL_NO_WORKSPACE', manuallyModifyConfig))
|
||||
end
|
||||
else
|
||||
for _, scp in ipairs(ws.folders) do
|
||||
tryModifySpecifiedConfig(scp.uri, finalChanges)
|
||||
tryModifyRC(scp.uri, finalChanges, false)
|
||||
tryModifyClient(scp.uri, finalChanges)
|
||||
tryModifyRC(scp.uri, finalChanges, true)
|
||||
end
|
||||
if #finalChanges > 0 then
|
||||
m.showMessage('Warning', lang.script('CONFIG_MODIFY_FAIL', buildMaunuallyMessage(finalChanges)))
|
||||
log.warn('Config modify fail', inspect(finalChanges))
|
||||
end
|
||||
end
|
||||
end, log.error)
|
||||
end
|
||||
|
||||
---@alias textEditor {start: integer, finish: integer, text: string}
|
||||
|
||||
---@param uri uri
|
||||
---@param edits textEditor[]
|
||||
function m.editText(uri, edits)
|
||||
local files = require 'files'
|
||||
local state = files.getState(uri)
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
local textEdits = {}
|
||||
for i, edit in ipairs(edits) do
|
||||
textEdits[i] = converter.textEdit(converter.packRange(state, edit.start, edit.finish), edit.text)
|
||||
end
|
||||
local params = {
|
||||
edit = {
|
||||
changes = {
|
||||
[uri] = textEdits,
|
||||
}
|
||||
}
|
||||
}
|
||||
proto.request('workspace/applyEdit', params)
|
||||
log.info('workspace/applyEdit', inspect(params))
|
||||
end
|
||||
|
||||
---@alias textMultiEditor {uri: uri, start: integer, finish: integer, text: string}
|
||||
|
||||
---@param editors textMultiEditor[]
|
||||
function m.editMultiText(editors)
|
||||
local files = require 'files'
|
||||
local changes = {}
|
||||
for _, editor in ipairs(editors) do
|
||||
local uri = editor.uri
|
||||
local state = files.getState(uri)
|
||||
if state then
|
||||
if not changes[uri] then
|
||||
changes[uri] = {}
|
||||
end
|
||||
local edit = converter.textEdit(converter.packRange(state, editor.start, editor.finish), editor.text)
|
||||
table.insert(changes[uri], edit)
|
||||
end
|
||||
end
|
||||
local params = {
|
||||
edit = {
|
||||
changes = changes,
|
||||
}
|
||||
}
|
||||
proto.request('workspace/applyEdit', params)
|
||||
log.info('workspace/applyEdit', inspect(params))
|
||||
end
|
||||
|
||||
---@param callback async fun(ev: string)
|
||||
function m.event(callback)
|
||||
m._eventList[#m._eventList+1] = callback
|
||||
end
|
||||
|
||||
function m._callEvent(ev)
|
||||
for _, callback in ipairs(m._eventList) do
|
||||
await.call(function ()
|
||||
callback(ev)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function m.setReady()
|
||||
m._ready = true
|
||||
m._callEvent('ready')
|
||||
end
|
||||
|
||||
function m.isReady()
|
||||
return m._ready == true
|
||||
end
|
||||
|
||||
local function hookPrint()
|
||||
if TEST or CLI then
|
||||
return
|
||||
end
|
||||
print = function (...)
|
||||
m.logMessage('Log', ...)
|
||||
end
|
||||
end
|
||||
|
||||
function m.init(t)
|
||||
log.info('Client init', inspect(t))
|
||||
m.info = t
|
||||
nonil.enable()
|
||||
m.client(t.clientInfo.name)
|
||||
nonil.disable()
|
||||
lang(LOCALE or t.locale)
|
||||
converter.setOffsetEncoding(m.getOffsetEncoding())
|
||||
hookPrint()
|
||||
m._callEvent('init')
|
||||
end
|
||||
|
||||
return m
|
||||
Reference in New Issue
Block a user