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 library = require 'library' local markdown = require 'provider.markdown' local client = require 'client' local furi = require 'file-uri' local pub = require 'pub' local fs = require 'bee.filesystem' local lang = require 'language' local progress = require 'progress' local tm = require 'text-merger' local nonil = require 'without-check-nil' local cfgLoader = require 'config.loader' local function updateConfig() local new if CONFIGPATH then new = cfgLoader.loadLocalConfig(CONFIGPATH) log.debug('load config from local', CONFIGPATH) else new = cfgLoader.loadClientConfig() log.debug('load config from client') end if not new then log.warn('load config failed!') return end config.update(new) log.debug('loaded config dump:', util.dump(new)) end local function isValidLuaUri(uri) if not files.isLua(uri) then return false end if not files.isOpen(uri) then return false end if workspace.isIgnored(uri) and not files.isLibrary(uri) then return false end return true end proto.on('initialize', function (params) client.init(params) config.init() workspace.initPath(params.rootUri) return { capabilities = cap.getIniter(), serverInfo = { name = 'sumneko.lua', }, } end) proto.on('initialized', function (params) files.init() local _ = progress.create(lang.script.WINDOW_INITIALIZING, 0.5) updateConfig() local registrations = {} nonil.enable() if client.info.capabilities.workspace.didChangeWatchedFiles.dynamicRegistration then -- 监视文件变化 registrations[#registrations+1] = { id = 'workspace/didChangeWatchedFiles', method = 'workspace/didChangeWatchedFiles', registerOptions = { watchers = { { globPattern = '**/', kind = 1 | 2 | 4, } }, }, } end if client.info.capabilities.workspace.didChangeConfiguration.dynamicRegistration then -- 监视配置变化 registrations[#registrations+1] = { id = 'workspace/didChangeConfiguration', method = 'workspace/didChangeConfiguration', } end nonil.disable() if #registrations ~= 0 then proto.awaitRequest('client/registerCapability', { registrations = registrations }) end library.init() workspace.init() return true end) proto.on('exit', function () log.info('Server exited.') os.exit(true) end) proto.on('shutdown', function () log.info('Server shutdown.') return true end) proto.on('workspace/didChangeConfiguration', function () if CONFIGPATH then return end updateConfig() end) proto.on('workspace/didChangeWatchedFiles', function (params) workspace.awaitReady() for _, change in ipairs(params.changes) do local uri = change.uri if not workspace.isWorkspaceUri(uri) then goto CONTINUE end if change.type == define.FileChangeType.Created then log.debug('FileChangeType.Created', uri) workspace.awaitLoadFile(uri) elseif change.type == define.FileChangeType.Deleted then log.debug('FileChangeType.Deleted', uri) files.remove(uri) local childs = files.getChildFiles(uri) for _, curi in ipairs(childs) do log.debug('FileChangeType.Deleted.Child', curi) files.remove(curi) end elseif change.type == define.FileChangeType.Changed then -- 如果文件处于关闭状态,则立即更新;否则等待didChange协议来更新 if isValidLuaUri(uri) then files.setText(uri, pub.awaitTask('loadFile', uri), false) else local path = furi.decode(uri) local filename = fs.path(path):filename():string() -- 排除类文件发生更改需要重新扫描 if files.eq(filename, '.gitignore') or files.eq(filename, '.gitmodules') then workspace.reload() break end end end ::CONTINUE:: end end) proto.on('workspace/didCreateFiles', function (params) log.debug('workspace/didCreateFiles', util.dump(params)) for _, file in ipairs(params.files) do if isValidLuaUri(file.uri) then files.setText(file.uri, pub.awaitTask('loadFile', file.uri), false) end end end) proto.on('workspace/didDeleteFiles', function (params) log.debug('workspace/didDeleteFiles', util.dump(params)) for _, file in ipairs(params.files) do files.remove(file.uri) local childs = files.getChildFiles(file.uri) for _, uri in ipairs(childs) do log.debug('workspace/didDeleteFiles#child', uri) files.remove(uri) end end end) proto.on('workspace/didRenameFiles', function (params) log.debug('workspace/didRenameFiles', util.dump(params)) for _, file in ipairs(params.files) do local text = files.getOriginText(file.oldUri) if text then files.remove(file.oldUri) if isValidLuaUri(file.newUri) then files.setText(file.newUri, text, false) end end local childs = files.getChildFiles(file.oldUri) for _, uri in ipairs(childs) do local ctext = files.getOriginText(uri) if ctext then local ouri = files.getOriginUri(uri) local tail = ouri:sub(#file.oldUri) local nuri = file.newUri .. tail log.debug('workspace/didRenameFiles#child', ouri, nuri) files.remove(uri) if isValidLuaUri(nuri) then files.setText(nuri, text, false) end end end end end) proto.on('textDocument/didOpen', function (params) workspace.awaitReady() local doc = params.textDocument local uri = doc.uri local text = doc.text log.debug('didOpen', uri) files.setText(uri, text, true) files.open(uri) end) proto.on('textDocument/didClose', function (params) local doc = params.textDocument local uri = doc.uri log.debug('didClose', uri) files.close(uri) if not files.isLua(uri) then files.remove(uri) end end) proto.on('textDocument/didChange', function (params) workspace.awaitReady() local doc = params.textDocument local changes = params.contentChanges local uri = doc.uri --log.debug('changes', util.dump(changes)) local text = tm(uri, changes) files.setText(uri, text, true) end) proto.on('textDocument/hover', function (params) await.close 'hover' await.setID 'hover' if not workspace.isReady() then local count, max = workspace.getLoadProcess() return { contents = { value = lang.script('HOVER_WS_LOADING', count, max), kind = 'markdown', } } end local _ = progress.create(lang.script.WINDOW_PROCESSING_HOVER, 0.5) local core = require 'core.hover' local doc = params.textDocument local uri = doc.uri if not files.exists(uri) then return nil end local offset = files.offsetOfWord(uri, params.position) local hover = core.byUri(uri, offset) if not hover then return nil end local md = markdown() md:add('lua', hover.label) md:splitLine() md:add('md', hover.description) return { contents = { value = md:string(), kind = 'markdown', }, range = files.range(uri, hover.source.start, hover.source.finish), } end) proto.on('textDocument/definition', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_DEFINITION, 0.5) local core = require 'core.definition' local uri = params.textDocument.uri if not files.exists(uri) then return nil end local offset = files.offsetOfWord(uri, params.position) local result = core(uri, offset) if not result then return nil end local response = {} for i, info in ipairs(result) do local targetUri = info.uri if targetUri then if files.exists(targetUri) then if client.getAbility 'textDocument.definition.linkSupport' then response[i] = define.locationLink(targetUri , files.range(targetUri, info.target.start, info.target.finish) , files.range(targetUri, info.target.start, info.target.finish) , files.range(uri, info.source.start, info.source.finish) ) else response[i] = define.location(targetUri , files.range(targetUri, info.target.start, info.target.finish) ) end end end end return response end) proto.on('textDocument/typeDefinition', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_TYPE_DEFINITION, 0.5) local core = require 'core.type-definition' local uri = params.textDocument.uri if not files.exists(uri) then return nil end local offset = files.offsetOfWord(uri, params.position) local result = core(uri, offset) if not result then return nil end local response = {} for i, info in ipairs(result) do local targetUri = info.uri if targetUri then if files.exists(targetUri) then if client.getAbility 'textDocument.typeDefinition.linkSupport' then response[i] = define.locationLink(targetUri , files.range(targetUri, info.target.start, info.target.finish) , files.range(targetUri, info.target.start, info.target.finish) , files.range(uri, info.source.start, info.source.finish) ) else response[i] = define.location(targetUri , files.range(targetUri, info.target.start, info.target.finish) ) end end end end return response end) proto.on('textDocument/references', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_REFERENCE, 0.5) local core = require 'core.reference' local uri = params.textDocument.uri if not files.exists(uri) then return nil end local offset = files.offsetOfWord(uri, params.position) local result = core(uri, offset) if not result then return nil end local response = {} for i, info in ipairs(result) do local targetUri = info.uri response[i] = define.location(targetUri , files.range(targetUri, info.target.start, info.target.finish) ) end return response end) proto.on('textDocument/documentHighlight', function (params) local core = require 'core.highlight' local uri = params.textDocument.uri if not files.exists(uri) then return nil end local offset = files.offsetOfWord(uri, params.position) local result = core(uri, offset) if not result then return nil end local response = {} for _, info in ipairs(result) do response[#response+1] = { range = files.range(uri, info.start, info.finish), kind = info.kind, } end return response end) proto.on('textDocument/rename', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_RENAME, 0.5) local core = require 'core.rename' local uri = params.textDocument.uri if not files.exists(uri) then return nil end local offset = files.offsetOfWord(uri, params.position) local result = core.rename(uri, offset, params.newName) if not result then return nil end local workspaceEdit = { changes = {}, } for _, info in ipairs(result) do local ruri = info.uri if not workspaceEdit.changes[ruri] then workspaceEdit.changes[ruri] = {} end local textEdit = define.textEdit(files.range(ruri, info.start, info.finish), info.text) workspaceEdit.changes[ruri][#workspaceEdit.changes[ruri]+1] = textEdit end return workspaceEdit end) proto.on('textDocument/prepareRename', function (params) local core = require 'core.rename' local uri = params.textDocument.uri if not files.exists(uri) then return nil end local offset = files.offsetOfWord(uri, params.position) local result = core.prepareRename(uri, offset) if not result then return nil end return { range = files.range(uri, result.start, result.finish), placeholder = result.text, } end) proto.on('textDocument/completion', function (params) await.close 'completion' await.setID 'completion' if not workspace.isReady() then local count, max = workspace.getLoadProcess() return { { label = lang.script('HOVER_WS_LOADING', count, max),textEdit = { range = { start = params.position, ['end'] = params.position, }, newText = '', }, } } end local _ = progress.create(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 uri = params.textDocument.uri if not files.exists(uri) then return nil end local triggerCharacter = params.context and params.context.triggerCharacter if config.get 'editor.acceptSuggestionOnEnter' ~= 'off' then if triggerCharacter == '\n' or triggerCharacter == '{' or triggerCharacter == ',' then return end end await.setPriority(1000) local clock = os.clock() local offset = files.offset(uri, params.position) local result = core.completion(uri, offset - 1, 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 _ = 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 = ('%04d'):format(i), filterText = res.filterText, insertText = res.insertText, insertTextFormat = 2, commitCharacters = res.commitCharacters, command = res.command, textEdit = res.textEdit and { range = files.range( uri, 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 = files.range( uri, edit.start, edit.finish ), newText = edit.newText, } end return t end)(), documentation = res.description and { value = 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 = resolved.description, kind = 'markdown', } end else easy = false item.data = { uri = uri, id = res.id, } end end items[i] = item end return { isIncomplete = false, items = items, } end) proto.on('completionItem/resolve', function (item) local core = require 'core.completion' if not item.data then return item end await.close 'completion.resolve' await.setID 'completion.resolve' local id = item.data.id local uri = item.data.uri --await.setPriority(1000) 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 = 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 = files.range( uri, edit.start, edit.finish ), newText = edit.newText, } end return t end)() or item.additionalTextEdits return item end) proto.on('textDocument/signatureHelp', function (params) if not config.get 'Lua.signatureHelp.enable' then return nil end workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_SIGNATURE, 0.5) local uri = params.textDocument.uri if not files.exists(uri) then return nil end await.close('signatureHelp') await.setID('signatureHelp') local offset = files.offset(uri, params.position) local core = require 'core.signature' local results = core(uri, offset - 1) 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] - 1, param.label[2], } } end infos[i] = { label = result.label, parameters = parameters, activeParameter = result.index - 1, documentation = result.description and { value = result.description, kind = 'markdown', }, } end return { signatures = infos, } end) proto.on('textDocument/documentSymbol', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_SYMBOL, 0.5) local core = require 'core.document-symbol' local uri = params.textDocument.uri local symbols = core(uri) if not symbols then return nil end local function convert(symbol) await.delay() symbol.range = files.range( uri, symbol.range[1], symbol.range[2] ) symbol.selectionRange = files.range( uri, symbol.selectionRange[1], symbol.selectionRange[2] ) if symbol.name == '' then symbol.name = lang.script.SYMBOL_ANONYMOUS 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) proto.on('textDocument/codeAction', function (params) local core = require 'core.code-action' local uri = params.textDocument.uri local range = params.range local diagnostics = params.context.diagnostics if not files.exists(uri) then return nil end local start, finish = files.unrange(uri, 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 for turi, changes in pairs(res.edit.changes) do for _, change in ipairs(changes) do change.range = files.range(turi, change.start, change.finish) change.start = nil change.finish = nil end end end end return results end) proto.on('workspace/executeCommand', 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) proto.on('workspace/symbol', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_WS_SYMBOL, 0.5) local core = require 'core.workspace-symbol' await.close('workspace/symbol') await.setID('workspace/symbol') local symbols = core(params.query) if not symbols or #symbols == 0 then return nil end local function convert(symbol) symbol.location = define.location( symbol.uri, files.range( symbol.uri, symbol.range[1], symbol.range[2] ) ) symbol.uri = nil end for _, symbol in ipairs(symbols) do convert(symbol) end return symbols end) proto.on('textDocument/semanticTokens/full', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_SEMANTIC_FULL, 0.5) local core = require 'core.semantic-tokens' local uri = params.textDocument.uri local text = files.getText(uri) if not text then return nil end local results = core(uri, 0, #text) return { data = results } end) proto.on('textDocument/semanticTokens/range', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_SEMANTIC_RANGE, 0.5) local core = require 'core.semantic-tokens' local uri = params.textDocument.uri local cache = files.getOpenedCache(uri) local start, finish if cache and not cache['firstSemantic'] then cache['firstSemantic'] = true start = 0 finish = #files.getText(uri) else start = files.offsetOfWord(uri, params.range.start) finish = files.offsetOfWord(uri, params.range['end']) end local results = core(uri, start, finish) return { data = results } end) proto.on('textDocument/foldingRange', function (params) local core = require 'core.folding' local uri = params.textDocument.uri if not files.exists(uri) 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 = files.position(uri, region.start, 'left').line local endLine = files.position(uri, region.finish, 'right').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) proto.on('window/workDoneProgress/cancel', function (params) progress.cancel(params.token) end) proto.on('$/didChangeVisibleRanges', function (params) files.setVisibles(params.uri, params.ranges) end) proto.on('$/status/click', function () -- TODO: translate local titleDiagnostic = '进行工作区诊断' local result = client.awaitRequestMessage('Info', 'xxx', { titleDiagnostic, }) if not result then return end if result == titleDiagnostic then local diagnostic = require 'provider.diagnostic' diagnostic.diagnosticsAll(true) end end) proto.on('textDocument/onTypeFormatting', function (params) workspace.awaitReady() local _ = progress.create(lang.script.WINDOW_PROCESSING_TYPE_FORMATTING, 0.5) local ch = params.ch local uri = params.textDocument.uri if not files.exists(uri) then return nil end local core = require 'core.type-formatting' local offset = files.offset(uri, params.position) local edits = core(uri, offset - 1, ch) 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 = files.range(uri, edit.start, edit.finish), newText = edit.text:gsub('\t', tab), } end return results end) -- Hint do local function updateHint(uri) local awaitID = 'hint:' .. uri await.close(awaitID) if not config.get 'Lua.hint.enable' then return end await.setID(awaitID) workspace.awaitReady() local visibles = files.getVisibles(uri) if not visibles then return end local edits = {} local hint = require 'core.hint' local _ = progress.create(lang.script.WINDOW_PROCESSING_HINT, 0.5) for _, visible in ipairs(visibles) do local piece = hint(uri, visible.start, visible.finish) if piece then for _, edit in ipairs(piece) do edits[#edits+1] = { newText = edit.newText, range = files.range(uri, edit.start, edit.finish) } end end end proto.notify('$/hint', { uri = uri, edits = edits, }) end files.watch(function (ev, uri) if ev == 'update' or ev == 'updateVisible' then await.call(function () updateHint(uri) end) end end) end config.watch(function (key, value) if key == 'Lua.window.statusBar' then if value then proto.notify('$/status/show') else proto.notify('$/status/hide') end end end)