summaryrefslogtreecommitdiff
path: root/script/provider
diff options
context:
space:
mode:
author最萌小汐 <sumneko@hotmail.com>2020-11-20 21:57:09 +0800
committer最萌小汐 <sumneko@hotmail.com>2020-11-20 21:57:09 +0800
commit4ca61ec457822dd14966afa0752340ae8ce180a1 (patch)
treeae8adb1ad82c717868e551e699fd3cf3bb290089 /script/provider
parentc63b2e404d8d2bb984afe3678a5ba2b2836380cc (diff)
downloadlua-language-server-4ca61ec457822dd14966afa0752340ae8ce180a1.zip
no longer beta
Diffstat (limited to 'script/provider')
-rw-r--r--script/provider/capability.lua61
-rw-r--r--script/provider/client.lua18
-rw-r--r--script/provider/completion.lua54
-rw-r--r--script/provider/diagnostic.lua303
-rw-r--r--script/provider/init.lua1
-rw-r--r--script/provider/markdown.lua26
-rw-r--r--script/provider/provider.lua642
-rw-r--r--script/provider/semantic-tokens.lua64
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,
+}