diff options
author | 最萌小汐 <sumneko@hotmail.com> | 2020-11-20 21:57:09 +0800 |
---|---|---|
committer | 最萌小汐 <sumneko@hotmail.com> | 2020-11-20 21:57:09 +0800 |
commit | 4ca61ec457822dd14966afa0752340ae8ce180a1 (patch) | |
tree | ae8adb1ad82c717868e551e699fd3cf3bb290089 /script/provider | |
parent | c63b2e404d8d2bb984afe3678a5ba2b2836380cc (diff) | |
download | lua-language-server-4ca61ec457822dd14966afa0752340ae8ce180a1.zip |
no longer beta
Diffstat (limited to 'script/provider')
-rw-r--r-- | script/provider/capability.lua | 61 | ||||
-rw-r--r-- | script/provider/client.lua | 18 | ||||
-rw-r--r-- | script/provider/completion.lua | 54 | ||||
-rw-r--r-- | script/provider/diagnostic.lua | 303 | ||||
-rw-r--r-- | script/provider/init.lua | 1 | ||||
-rw-r--r-- | script/provider/markdown.lua | 26 | ||||
-rw-r--r-- | script/provider/provider.lua | 642 | ||||
-rw-r--r-- | script/provider/semantic-tokens.lua | 64 |
8 files changed, 1169 insertions, 0 deletions
diff --git a/script/provider/capability.lua b/script/provider/capability.lua new file mode 100644 index 00000000..23ec27b0 --- /dev/null +++ b/script/provider/capability.lua @@ -0,0 +1,61 @@ +local sp = require 'bee.subprocess' +local nonil = require 'without-check-nil' +local client = require 'provider.client' + +local m = {} + +local function allWords() + local str = [[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.:('"[,#*@| ]] + local list = {} + for c in str:gmatch '.' do + list[#list+1] = c + end + return list +end + +function m.getIniter() + local initer = { + -- 文本同步方式 + textDocumentSync = { + -- 打开关闭文本时通知 + openClose = true, + -- 文本改变时完全通知 TODO 支持差量更新(2) + change = 1, + }, + + hoverProvider = true, + definitionProvider = true, + referencesProvider = true, + renameProvider = { + prepareProvider = true, + }, + documentSymbolProvider = true, + workspaceSymbolProvider = true, + documentHighlightProvider = true, + codeActionProvider = true, + signatureHelpProvider = { + triggerCharacters = { '(', ',' }, + }, + executeCommandProvider = { + commands = { + 'lua.removeSpace:' .. sp:get_id(), + 'lua.solve:' .. sp:get_id(), + }, + } + --documentOnTypeFormattingProvider = { + -- firstTriggerCharacter = '}', + --}, + } + + nonil.enable() + if not client.info.capabilities.textDocument.completion.dynamicRegistration then + initer.completionProvider = { + triggerCharacters = allWords(), + } + end + nonil.disable() + + return initer +end + +return m diff --git a/script/provider/client.lua b/script/provider/client.lua new file mode 100644 index 00000000..c1b16f0f --- /dev/null +++ b/script/provider/client.lua @@ -0,0 +1,18 @@ +local nonil = require 'without-check-nil' +local util = require 'utility' + +local m = {} + +function m.client() + nonil.enable() + local name = m.info.clientInfo.name + nonil.disable() + return name +end + +function m.init(t) + log.debug('Client init', util.dump(t)) + m.info = t +end + +return m diff --git a/script/provider/completion.lua b/script/provider/completion.lua new file mode 100644 index 00000000..e506cd7b --- /dev/null +++ b/script/provider/completion.lua @@ -0,0 +1,54 @@ +local proto = require 'proto' + +local isEnable = false + +local function allWords() + local str = [[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.:('"[,#*@| ]] + local list = {} + for c in str:gmatch '.' do + list[#list+1] = c + end + return list +end + +local function enable() + -- TODO 检查客户端是否支持动态注册自动完成 + if isEnable then + return + end + isEnable = true + log.debug('Enable completion.') + proto.awaitRequest('client/registerCapability', { + registrations = { + { + id = 'completion', + method = 'textDocument/completion', + registerOptions = { + resolveProvider = true, + triggerCharacters = allWords(), + }, + }, + } + }) +end + +local function disable() + if not isEnable then + return + end + isEnable = false + log.debug('Disable completion.') + proto.awaitRequest('client/unregisterCapability', { + unregisterations = { + { + id = 'completion', + method = 'textDocument/completion', + }, + } + }) +end + +return { + enable = enable, + disable = disable, +} diff --git a/script/provider/diagnostic.lua b/script/provider/diagnostic.lua new file mode 100644 index 00000000..845b9f44 --- /dev/null +++ b/script/provider/diagnostic.lua @@ -0,0 +1,303 @@ +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 m = {} +m._start = false +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 lines = files.getLines(uri) + local text = files.getText(uri) + local message = lang.script('PARSER_'..err.type, err.info) + + if err.version then + local version = err.info and err.info.version or config.config.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 + relatedInformation[#relatedInformation+1] = { + message = rmessage, + location = define.location(uri, define.range(lines, text, rel.start, rel.finish)), + } + end + end + + return { + code = err.type:lower():gsub('_', '-'), + range = define.range(lines, text, err.start, err.finish), + severity = define.DiagnosticSeverity.Error, + source = lang.script.DIAG_SYNTAX_CHECK, + message = message, + relatedInformation = relatedInformation, + } +end + +local function buildDiagnostic(uri, diag) + local lines = files.getLines(uri) + local text = files.getText(uri) + + local relatedInformation + if diag.related then + relatedInformation = {} + for _, rel in ipairs(diag.related) do + local rtext = files.getText(rel.uri) + local rlines = files.getLines(rel.uri) + relatedInformation[#relatedInformation+1] = { + message = rel.message or rtext:sub(rel.start, rel.finish), + location = define.location(rel.uri, define.range(rlines, rtext, rel.start, rel.finish)) + } + end + end + + return { + range = define.range(lines, text, diag.start, diag.finish), + source = lang.script.DIAG_DIAGNOSTICS, + severity = diag.level, + message = diag.message, + code = diag.code, + tags = diag.tags, + relatedInformation = relatedInformation, + } +end + +local function merge(a, b) + if not a and not b then + return nil + end + local t = {} + if a then + for i = 1, #a do + t[#t+1] = a[i] + end + end + if b then + for i = 1, #b do + t[#t+1] = b[i] + end + end + return t +end + +function m.clear(uri) + local luri = uri:lower() + if not m.cache[luri] then + return + end + m.cache[luri] = nil + proto.notify('textDocument/publishDiagnostics', { + uri = files.getOriginUri(luri) or uri, + diagnostics = {}, + }) +end + +function m.clearAll() + for luri in pairs(m.cache) do + m.clear(luri) + end +end + +function m.syntaxErrors(uri, ast) + if #ast.errs == 0 then + return nil + end + + local results = {} + + for _, err in ipairs(ast.errs) do + results[#results+1] = buildSyntaxError(uri, err) + end + + return results +end + +function m.diagnostics(uri, diags) + if not m._start then + return + end + + core(uri, function (results) + if #results == 0 then + return + end + for i = 1, #results do + diags[#diags+1] = buildDiagnostic(uri, results[i]) + end + end) +end + +function m.doDiagnostic(uri) + if not config.config.diagnostics.enable then + return + end + uri = uri:lower() + if files.isLibrary(uri) then + return + end + + await.delay() + + local ast = files.getAst(uri) + if not ast then + m.clear(uri) + return + end + + local syntax = m.syntaxErrors(uri, ast) + local diags = {} + + local function pushResult() + local full = merge(syntax, diags) + if not full then + m.clear(uri) + return + end + + if util.equal(m.cache, full) then + return + end + m.cache[uri] = full + + proto.notify('textDocument/publishDiagnostics', { + uri = files.getOriginUri(uri), + diagnostics = full, + }) + end + + if await.hasID 'diagnosticsAll' then + m.checkStepResult = nil + else + local clock = os.clock() + m.checkStepResult = function () + if os.clock() - clock >= 0.2 then + pushResult() + clock = os.clock() + end + end + end + + m.diagnostics(uri, diags) + pushResult() +end + +function m.refresh(uri) + if not m._start then + return + end + await.call(function () + if uri then + m.doDiagnostic(uri) + end + m.diagnosticsAll() + end, 'files.version') +end + +function m.diagnosticsAll() + if not config.config.diagnostics.enable then + m.clearAll() + return + end + if not m._start then + return + end + local delay = config.config.diagnostics.workspaceDelay / 1000 + if delay < 0 then + return + end + await.close 'diagnosticsAll' + await.call(function () + await.sleep(delay) + m.diagnosticsAllClock = os.clock() + local clock = os.clock() + for uri in files.eachFile() do + m.doDiagnostic(uri) + await.delay() + end + log.debug('全文诊断耗时:', os.clock() - clock) + end, 'files.version', 'diagnosticsAll') +end + +function m.start() + m._start = true + m.diagnosticsAll() +end + +function m.checkStepResult() + if await.hasID 'diagnosticsAll' then + return + end +end + +function m.checkWorkspaceDiag() + if not await.hasID 'diagnosticsAll' then + return + end + local speedRate = config.config.diagnostics.workspaceRate + if speedRate <= 0 or speedRate >= 100 then + return + end + local currentClock = os.clock() + local passed = currentClock - m.diagnosticsAllClock + local sleepTime = passed * (100 - speedRate) / speedRate + m.sleepRest + m.sleepRest = 0.0 + if sleepTime < 0.001 then + m.sleepRest = m.sleepRest + sleepTime + return + end + if sleepTime > 0.1 then + m.sleepRest = sleepTime - 0.1 + sleepTime = 0.1 + end + await.sleep(sleepTime) + m.diagnosticsAllClock = os.clock() + return false +end + +files.watch(function (ev, uri) + if ev == 'remove' then + m.clear(uri) + elseif ev == 'update' then + m.refresh(uri) + elseif ev == 'open' then + m.doDiagnostic(uri) + end +end) + +await.watch(function (ev, co) + if ev == 'delay' then + if m.checkStepResult then + m.checkStepResult() + end + return m.checkWorkspaceDiag() + end +end) + +return m diff --git a/script/provider/init.lua b/script/provider/init.lua new file mode 100644 index 00000000..7eafb70a --- /dev/null +++ b/script/provider/init.lua @@ -0,0 +1 @@ +require 'provider.provider' diff --git a/script/provider/markdown.lua b/script/provider/markdown.lua new file mode 100644 index 00000000..ca76ec89 --- /dev/null +++ b/script/provider/markdown.lua @@ -0,0 +1,26 @@ +local mt = {} +mt.__index = mt +mt.__name = 'markdown' + +function mt:add(language, text) + if not text or #text == 0 then + return + end + if language == 'md' then + if self._last == 'md' then + self[#self+1] = '' + end + self[#self+1] = text + else + self[#self+1] = ('```%s\n%s\n```'):format(language, text) + end + self._last = language +end + +function mt:string() + return table.concat(self, '\n') +end + +return function () + return setmetatable({}, mt) +end diff --git a/script/provider/provider.lua b/script/provider/provider.lua new file mode 100644 index 00000000..3508116f --- /dev/null +++ b/script/provider/provider.lua @@ -0,0 +1,642 @@ +local util = require 'utility' +local cap = require 'provider.capability' +local completion= require 'provider.completion' +local semantic = require 'provider.semantic-tokens' +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 'provider.client' +local furi = require 'file-uri' +local pub = require 'pub' +local fs = require 'bee.filesystem' +local lang = require 'language' + +local function updateConfig() + local diagnostics = require 'provider.diagnostic' + local vm = require 'vm' + local configs = proto.awaitRequest('workspace/configuration', { + items = { + { + scopeUri = workspace.uri, + section = 'Lua', + }, + { + scopeUri = workspace.uri, + section = 'files.associations', + }, + { + scopeUri = workspace.uri, + section = 'files.exclude', + } + }, + }) + + local updated = configs[1] + local other = { + associations = configs[2], + exclude = configs[3], + } + + local oldConfig = util.deepCopy(config.config) + local oldOther = util.deepCopy(config.other) + config.setConfig(updated, other) + local newConfig = config.config + local newOther = config.other + if not util.equal(oldConfig.runtime, newConfig.runtime) then + library.init() + workspace.reload() + end + if not util.equal(oldConfig.diagnostics, newConfig.diagnostics) then + diagnostics.diagnosticsAll() + end + if not util.equal(oldConfig.plugin, newConfig.plugin) then + end + if not util.equal(oldConfig.workspace, newConfig.workspace) + or not util.equal(oldConfig.plugin, newConfig.plugin) + or not util.equal(oldOther.associations, newOther.associations) + or not util.equal(oldOther.exclude, newOther.exclude) + then + workspace.reload() + end + if not util.equal(oldConfig.luadoc, newConfig.luadoc) then + files.flushCache() + end + if not util.equal(oldConfig.intelliSense, newConfig.intelliSense) then + files.flushCache() + end + + if newConfig.completion.enable then + completion.enable() + else + completion.disable() + end + if newConfig.color.mode == 'Semantic' then + semantic.enable() + else + semantic.disable() + end +end + +proto.on('initialize', function (params) + client.init(params) + library.init() + workspace.init(params.rootUri) + return { + capabilities = cap.getIniter(), + serverInfo = { + name = 'sumneko.lua', + }, + } +end) + +proto.on('initialized', function (params) + updateConfig() + proto.awaitRequest('client/registerCapability', { + registrations = { + -- 监视文件变化 + { + id = '0', + method = 'workspace/didChangeWatchedFiles', + registerOptions = { + watchers = { + { + globPattern = '**/', + kind = 1 | 2 | 4, + } + }, + }, + }, + -- 配置变化 + { + id = '1', + method = 'workspace/didChangeConfiguration', + } + } + }) + await.call(workspace.awaitPreload) + 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 () + updateConfig() +end) + +proto.on('workspace/didChangeWatchedFiles', function (params) + for _, change in ipairs(params.changes) do + local uri = change.uri + -- TODO 创建文件与删除文件直接重新扫描(文件改名、文件夹删除等情况太复杂了) + if change.type == define.FileChangeType.Created + or change.type == define.FileChangeType.Deleted then + workspace.reload() + break + elseif change.type == define.FileChangeType.Changed then + -- 如果文件处于关闭状态,则立即更新;否则等待didChange协议来更新 + if files.isLua(uri) and not files.isOpen(uri) then + files.setText(uri, pub.awaitTask('loadFile', uri)) + 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 + end +end) + +proto.on('textDocument/didOpen', function (params) + local doc = params.textDocument + local uri = doc.uri + local text = doc.text + files.open(uri) + files.setText(uri, text) +end) + +proto.on('textDocument/didClose', function (params) + local doc = params.textDocument + local uri = doc.uri + files.close(uri) + if not files.isLua(uri) then + files.remove(uri) + end +end) + +proto.on('textDocument/didChange', function (params) + local doc = params.textDocument + local change = params.contentChanges + local uri = doc.uri + local text = change[1].text + if files.isLua(uri) or files.isOpen(uri) then + --log.debug('didChange:', uri) + files.setText(uri, text) + --log.debug('setText:', #text) + end +end) + +proto.on('textDocument/hover', function (params) + await.close 'hover' + await.setID 'hover' + local core = require 'core.hover' + local doc = params.textDocument + local uri = doc.uri + if not files.exists(uri) then + return nil + end + local lines = files.getLines(uri) + local text = files.getText(uri) + local offset = define.offsetOfWord(lines, text, params.position) + local hover = core.byUri(uri, offset) + if not hover then + return nil + end + local md = markdown() + md:add('lua', hover.label) + md:add('md', hover.description) + return { + contents = { + value = md:string(), + kind = 'markdown', + }, + range = define.range(lines, text, hover.source.start, hover.source.finish), + } +end) + +proto.on('textDocument/definition', function (params) + local core = require 'core.definition' + local uri = params.textDocument.uri + if not files.exists(uri) then + return nil + end + local lines = files.getLines(uri) + local text = files.getText(uri) + local offset = define.offsetOfWord(lines, text, 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 + local targetLines = files.getLines(targetUri) + local targetText = files.getText(targetUri) + response[i] = define.locationLink(targetUri + , define.range(targetLines, targetText, info.target.start, info.target.finish) + , define.range(targetLines, targetText, info.target.start, info.target.finish) + , define.range(lines, text, info.source.start, info.source.finish) + ) + end + end + return response +end) + +proto.on('textDocument/references', function (params) + local core = require 'core.reference' + local uri = params.textDocument.uri + if not files.exists(uri) then + return nil + end + local lines = files.getLines(uri) + local text = files.getText(uri) + local offset = define.offsetOfWord(lines, text, 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 + local targetLines = files.getLines(targetUri) + local targetText = files.getText(targetUri) + response[i] = define.location(targetUri + , define.range(targetLines, targetText, 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 lines = files.getLines(uri) + local text = files.getText(uri) + local offset = define.offsetOfWord(lines, text, 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 = define.range(lines, text, info.start, info.finish), + kind = info.kind, + } + end + return response +end) + +proto.on('textDocument/rename', function (params) + local core = require 'core.rename' + local uri = params.textDocument.uri + if not files.exists(uri) then + return nil + end + local lines = files.getLines(uri) + local text = files.getText(uri) + local offset = define.offsetOfWord(lines, text, 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 + local rlines = files.getLines(ruri) + local rtext = files.getText(ruri) + if not workspaceEdit.changes[ruri] then + workspaceEdit.changes[ruri] = {} + end + local textEdit = define.textEdit(define.range(rlines, rtext, 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 lines = files.getLines(uri) + local text = files.getText(uri) + local offset = define.offsetOfWord(lines, text, params.position) + local result = core.prepareRename(uri, offset) + if not result then + return nil + end + return { + range = define.range(lines, text, result.start, result.finish), + placeholder = result.text, + } +end) + +proto.on('textDocument/completion', function (params) + --log.info(util.dump(params)) + local core = require 'core.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 + await.setPriority(1000) + local clock = os.clock() + local lines = files.getLines(uri) + local text = files.getText(uri) + local offset = define.offset(lines, text, params.position) + local result = core.completion(uri, offset) + 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 + local easy = false + local items = {} + for i, res in ipairs(result) do + local item = { + label = res.label, + kind = res.kind, + deprecated = res.deprecated, + sortText = ('%04d'):format(i), + insertText = res.insertText, + insertTextFormat = res.insertTextFormat, + textEdit = res.textEdit and { + range = define.range( + lines, + text, + 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 = define.range( + lines, + text, + edit.start, + edit.finish + ) + } + 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 = { + version = files.globalVersion, + 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 + local globalVersion = item.data.version + local id = item.data.id + if globalVersion ~= files.globalVersion then + return item + end + --await.setPriority(1000) + local resolved = core.resolve(id) + if not resolved then + return nil + end + item.detail = resolved.detail + item.documentation = resolved.description and { + value = resolved.description, + kind = 'markdown', + } + return item +end) + +proto.on('textDocument/signatureHelp', function (params) + if not config.config.signatureHelp.enable then + return nil + end + local uri = params.textDocument.uri + if not files.exists(uri) then + return nil + end + await.close('signatureHelp') + await.setID('signatureHelp') + local lines = files.getLines(uri) + local text = files.getText(uri) + local offset = define.offset(lines, text, params.position) + local core = require 'core.signature' + local results = core(uri, offset) + 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) + local core = require 'core.document-symbol' + local uri = params.textDocument.uri + local lines = files.getLines(uri) + local text = files.getText(uri) + while not lines or not text do + await.sleep(0.1) + lines = files.getLines(uri) + text = files.getText(uri) + end + + local symbols = core(uri) + if not symbols then + return nil + end + + local function convert(symbol) + await.delay() + symbol.range = define.range( + lines, + text, + symbol.range[1], + symbol.range[2] + ) + symbol.selectionRange = define.range( + lines, + text, + 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 + local results = core(uri, range, diagnostics) + + if not results or #results == 0 then + return nil + 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]) + end +end) + +proto.on('workspace/symbol', function (params) + 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, + define.range( + files.getLines(symbol.uri), + files.getText(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) + local core = require 'core.semantic-tokens' + local uri = params.textDocument.uri + log.debug('semanticTokens/full', uri) + local text = files.getText(uri) + while not text do + await.sleep(0.1) + text = files.getText(uri) + end + local results = core(uri, 0, #text) + if not results or #results == 0 then + return nil + end + return { + data = results + } +end) + +proto.on('textDocument/semanticTokens/range', function (params) + local core = require 'core.semantic-tokens' + local uri = params.textDocument.uri + log.debug('semanticTokens/range', uri) + local lines = files.getLines(uri) + local text = files.getText(uri) + while not lines or not text do + await.sleep(0.1) + lines = files.getLines(uri) + text = files.getText(uri) + end + local start = define.offset(lines, text, params.range.start) + local finish = define.offset(lines, text, params.range['end']) + local results = core(uri, start, finish) + if not results or #results == 0 then + return nil + end + return { + data = results + } +end) diff --git a/script/provider/semantic-tokens.lua b/script/provider/semantic-tokens.lua new file mode 100644 index 00000000..17985bcd --- /dev/null +++ b/script/provider/semantic-tokens.lua @@ -0,0 +1,64 @@ +local proto = require 'proto' +local define = require 'proto.define' +local client = require 'provider.client' + +local isEnable = false + +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 + +local function enable() + if isEnable then + return + end + if not client.info.capabilities.textDocument.semanticTokens then + return + end + isEnable = true + log.debug('Enable semantic tokens.') + proto.awaitRequest('client/registerCapability', { + registrations = { + { + id = 'semantic-tokens', + method = 'textDocument/semanticTokens', + registerOptions = { + legend = { + tokenTypes = toArray(define.TokenTypes), + tokenModifiers = toArray(define.TokenModifiers), + }, + range = true, + full = true, + }, + }, + } + }) +end + +local function disable() + if not isEnable then + return + end + isEnable = false + log.debug('Disable semantic tokens.') + proto.awaitRequest('client/unregisterCapability', { + unregisterations = { + { + id = 'semantic-tokens', + method = 'textDocument/semanticTokens', + }, + } + }) +end + +return { + enable = enable, + disable = disable, +} |