summaryrefslogtreecommitdiff
path: root/script/src/method
diff options
context:
space:
mode:
Diffstat (limited to 'script/src/method')
-rw-r--r--script/src/method/exit.lua4
-rw-r--r--script/src/method/init.lua32
-rw-r--r--script/src/method/initialize.lua50
-rw-r--r--script/src/method/initialized.lua69
-rw-r--r--script/src/method/shutdown.lua4
-rw-r--r--script/src/method/textDocument/codeAction.lua23
-rw-r--r--script/src/method/textDocument/completion.lua104
-rw-r--r--script/src/method/textDocument/definition.lua88
-rw-r--r--script/src/method/textDocument/didChange.lua16
-rw-r--r--script/src/method/textDocument/didClose.lua5
-rw-r--r--script/src/method/textDocument/didOpen.lua5
-rw-r--r--script/src/method/textDocument/documentHighlight.lua37
-rw-r--r--script/src/method/textDocument/documentSymbol.lua72
-rw-r--r--script/src/method/textDocument/foldingRange.lua57
-rw-r--r--script/src/method/textDocument/hover.lua44
-rw-r--r--script/src/method/textDocument/implementation.lua108
-rw-r--r--script/src/method/textDocument/onTypeFormatting.lua14
-rw-r--r--script/src/method/textDocument/publishDiagnostics.lua163
-rw-r--r--script/src/method/textDocument/references.lua86
-rw-r--r--script/src/method/textDocument/rename.lua50
-rw-r--r--script/src/method/textDocument/signatureHelp.lua50
-rw-r--r--script/src/method/workspace/didChangeConfiguration.lua27
-rw-r--r--script/src/method/workspace/didChangeWatchedFiles.lua44
-rw-r--r--script/src/method/workspace/didChangeWorkspaceFolders.lua20
-rw-r--r--script/src/method/workspace/executeCommand.lua258
25 files changed, 1430 insertions, 0 deletions
diff --git a/script/src/method/exit.lua b/script/src/method/exit.lua
new file mode 100644
index 00000000..fa550243
--- /dev/null
+++ b/script/src/method/exit.lua
@@ -0,0 +1,4 @@
+return function ()
+ log.info('Server exited.')
+ os.exit(true)
+end
diff --git a/script/src/method/init.lua b/script/src/method/init.lua
new file mode 100644
index 00000000..8827768b
--- /dev/null
+++ b/script/src/method/init.lua
@@ -0,0 +1,32 @@
+local method = {}
+
+local function init(name)
+ method[name] = require('method.' .. name:gsub('/', '.'))
+end
+
+init 'exit'
+init 'initialize'
+init 'initialized'
+init 'shutdown'
+init 'textDocument/codeAction'
+init 'textDocument/completion'
+init 'textDocument/definition'
+init 'textDocument/didOpen'
+init 'textDocument/didChange'
+init 'textDocument/didClose'
+init 'textDocument/documentHighlight'
+init 'textDocument/documentSymbol'
+init 'textDocument/foldingRange'
+init 'textDocument/hover'
+init 'textDocument/implementation'
+init 'textDocument/onTypeFormatting'
+init 'textDocument/publishDiagnostics'
+init 'textDocument/rename'
+init 'textDocument/references'
+init 'textDocument/signatureHelp'
+init 'workspace/didChangeConfiguration'
+init 'workspace/didChangeWatchedFiles'
+init 'workspace/didChangeWorkspaceFolders'
+init 'workspace/executeCommand'
+
+return method
diff --git a/script/src/method/initialize.lua b/script/src/method/initialize.lua
new file mode 100644
index 00000000..02a96695
--- /dev/null
+++ b/script/src/method/initialize.lua
@@ -0,0 +1,50 @@
+local function allWords()
+ local str = [[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.:('"[,#*@| ]]
+ local list = {}
+ for c in str:gmatch '.' do
+ list[#list+1] = c
+ end
+ return list
+end
+
+return function (lsp)
+ lsp._inited = true
+ return {
+ capabilities = {
+ hoverProvider = true,
+ definitionProvider = true,
+ referencesProvider = true,
+ renameProvider = true,
+ documentSymbolProvider = true,
+ documentHighlightProvider = true,
+ codeActionProvider = true,
+ foldingRangeProvider = true,
+ signatureHelpProvider = {
+ triggerCharacters = { '(', ',' },
+ },
+ -- 文本同步方式
+ textDocumentSync = {
+ -- 打开关闭文本时通知
+ openClose = true,
+ -- 文本改变时完全通知 TODO 支持差量更新(2)
+ change = 1,
+ },
+ workspace = {
+ workspaceFolders = {
+ supported = true,
+ changeNotifications = true,
+ }
+ },
+ documentOnTypeFormattingProvider = {
+ firstTriggerCharacter = '}',
+ },
+ executeCommandProvider = {
+ commands = {
+ 'config',
+ 'removeSpace',
+ 'solve',
+ },
+ },
+ }
+ }
+end
diff --git a/script/src/method/initialized.lua b/script/src/method/initialized.lua
new file mode 100644
index 00000000..d84a2159
--- /dev/null
+++ b/script/src/method/initialized.lua
@@ -0,0 +1,69 @@
+local rpc = require 'rpc'
+local workspace = require 'workspace'
+
+local function initAfterConfig(lsp, firstScope)
+ if firstScope then
+ lsp.workspace = workspace(lsp, firstScope.name)
+ lsp.workspace:init(firstScope.uri)
+ end
+ -- 必须动态注册的事件:
+ rpc:request('client/registerCapability', {
+ registrations = {
+ -- 监视文件变化
+ {
+ id = '0',
+ method = 'workspace/didChangeWatchedFiles',
+ registerOptions = {
+ watchers = {
+ {
+ globPattern = '**/',
+ kind = 1 | 2 | 4,
+ }
+ },
+ },
+ },
+ -- 配置变化
+ {
+ id = '1',
+ method = 'workspace/didChangeConfiguration',
+ }
+ }
+ }, function ()
+ log.debug('client/registerCapability Success!')
+ end)
+end
+
+return function (lsp)
+ -- 请求工作目录
+ rpc:request('workspace/workspaceFolders', nil, function (folders)
+ local firstScope
+ if folders then
+ firstScope = folders[1]
+ end
+ local uri = firstScope and firstScope.uri
+ -- 请求配置
+ rpc:request('workspace/configuration', {
+ items = {
+ {
+ scopeUri = uri,
+ section = 'Lua',
+ },
+ {
+ scopeUri = uri,
+ section = 'files.associations',
+ },
+ {
+ scopeUri = uri,
+ section = 'files.exclude',
+ }
+ },
+ }, function (configs)
+ lsp:onUpdateConfig(configs[1], {
+ associations = configs[2],
+ exclude = configs[3],
+ })
+ initAfterConfig(lsp, firstScope)
+ end)
+ end)
+ return true
+end
diff --git a/script/src/method/shutdown.lua b/script/src/method/shutdown.lua
new file mode 100644
index 00000000..bb81306e
--- /dev/null
+++ b/script/src/method/shutdown.lua
@@ -0,0 +1,4 @@
+return function ()
+ log.info('Server shutdown.')
+ return true
+end
diff --git a/script/src/method/textDocument/codeAction.lua b/script/src/method/textDocument/codeAction.lua
new file mode 100644
index 00000000..3c6e8d49
--- /dev/null
+++ b/script/src/method/textDocument/codeAction.lua
@@ -0,0 +1,23 @@
+local core = require 'core'
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local vm, lines = lsp:getVM(uri)
+ if not vm then
+ return
+ end
+ local diagnostics = params.context.diagnostics
+ local range = params.range
+
+ local results = core.codeAction(lsp
+ , uri
+ , diagnostics
+ , range
+ )
+
+ if #results == 0 then
+ return nil
+ end
+
+ return results
+end
diff --git a/script/src/method/textDocument/completion.lua b/script/src/method/textDocument/completion.lua
new file mode 100644
index 00000000..4c7581df
--- /dev/null
+++ b/script/src/method/textDocument/completion.lua
@@ -0,0 +1,104 @@
+local core = require 'core'
+local parser = require 'parser'
+
+local function posToRange(lines, start, finish)
+ local start_row, start_col = lines:rowcol(start)
+ local finish_row, finish_col = lines:rowcol(finish)
+ return {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ character = finish_col,
+ },
+ }
+end
+
+local function fastCompletion(lsp, params, lines)
+ local uri = params.textDocument.uri
+ local text, oldText = lsp:getText(uri)
+ -- lua是从1开始的,因此都要+1
+ local position = lines:positionAsChar(params.position.line + 1, params.position.character)
+
+ local vm = lsp:getVM(uri)
+ if not vm then
+ vm = lsp:loadVM(uri)
+ if not vm then
+ return nil
+ end
+ end
+
+ local items = core.completion(vm, text, position, oldText)
+ if not items or #items == 0 then
+ vm = lsp:loadVM(uri)
+ if not vm then
+ return nil
+ end
+ items = core.completion(vm, text, position)
+ if not items or #items == 0 then
+ return nil
+ end
+ end
+
+ return items
+end
+
+local function finishCompletion(lsp, params, lines)
+ local uri = params.textDocument.uri
+ local text = lsp:getText(uri)
+ -- lua是从1开始的,因此都要+1
+ local position = lines:positionAsChar(params.position.line + 1, params.position.character)
+
+ local vm = lsp:loadVM(uri)
+ if not vm then
+ return nil
+ end
+
+ local items = core.completion(vm, text, position)
+ if not items or #items == 0 then
+ return nil
+ end
+
+ return items
+end
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local text, oldText = lsp:getText(uri)
+ if not text then
+ return nil
+ end
+
+ local lines = parser:lines(text, 'utf8')
+ local items = fastCompletion(lsp, params, lines)
+ --local items = finishCompletion(lsp, params, lines)
+ if not items then
+ return nil
+ end
+
+ for i, item in ipairs(items) do
+ item.sortText = ('%04d'):format(i)
+ item.insertTextFormat = 2
+ item.insertText = item.insertText or item.label
+ if item.textEdit then
+ item.textEdit.range = posToRange(lines, item.textEdit.start, item.textEdit.finish)
+ item.textEdit.start = nil
+ item.textEdit.finish = nil
+ end
+ if item.additionalTextEdits then
+ for _, textEdit in ipairs(item.additionalTextEdits) do
+ textEdit.range = posToRange(lines, textEdit.start, textEdit.finish)
+ textEdit.start = nil
+ textEdit.finish = nil
+ end
+ end
+ end
+
+ local response = {
+ isIncomplete = true,
+ items = items,
+ }
+ return response
+end
diff --git a/script/src/method/textDocument/definition.lua b/script/src/method/textDocument/definition.lua
new file mode 100644
index 00000000..dbf9e41c
--- /dev/null
+++ b/script/src/method/textDocument/definition.lua
@@ -0,0 +1,88 @@
+local core = require 'core'
+
+local function findResult(lsp, uri, position)
+ local vm = lsp:getVM(uri)
+
+ local positions, isGlobal = core.definition(vm, position, 'definition')
+ if not positions then
+ return nil, isGlobal
+ end
+
+ local locations = {}
+ for i, position in ipairs(positions) do
+ local start, finish, valueUri = position[1], position[2], (position[3] or uri)
+ local vm, valueLines = lsp:getVM(valueUri)
+ if valueLines then
+ local start_row, start_col = valueLines:rowcol(start)
+ local finish_row, finish_col = valueLines:rowcol(finish)
+ locations[#locations+1] = {
+ uri = valueUri,
+ range = {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ -- 这里不用-1,因为前端期待的是匹配完成后的位置
+ character = finish_col,
+ },
+ }
+ }
+ elseif vm then
+ locations[#locations+1] = {
+ uri = valueUri,
+ range = {
+ start = {
+ line = 0,
+ character = 0,
+ },
+ ['end'] = {
+ line = 0,
+ character = 0,
+ },
+ }
+ }
+ end
+ end
+
+ if #locations == 0 then
+ return nil, isGlobal
+ end
+
+ return locations, isGlobal
+end
+
+local LastTask
+
+---@param lsp LSP
+---@param params table
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local vm, lines = lsp:loadVM(uri)
+ if not vm then
+ return nil
+ end
+
+ if LastTask then
+ LastTask:remove()
+ LastTask = nil
+ end
+
+ -- lua是从1开始的,因此都要+1
+ local position = lines:positionAsChar(params.position.line + 1, params.position.character)
+
+ return function (response)
+ local clock = os.clock()
+ LastTask = ac.loop(0.1, function ()
+ local result, isGlobal = findResult(lsp, uri, position)
+ if isGlobal and lsp:isWaitingCompile() and os.clock() - clock < 1 then
+ return
+ end
+ response(result)
+ LastTask:remove()
+ LastTask = nil
+ end)
+ LastTask:onTimer()
+ end
+end
diff --git a/script/src/method/textDocument/didChange.lua b/script/src/method/textDocument/didChange.lua
new file mode 100644
index 00000000..82e6c096
--- /dev/null
+++ b/script/src/method/textDocument/didChange.lua
@@ -0,0 +1,16 @@
+return function (lsp, params)
+ local doc = params.textDocument
+ local change = params.contentChanges
+ if lsp.workspace then
+ local path = lsp.workspace:relativePathByUri(doc.uri)
+ if not path or not lsp.workspace:isLuaFile(path) then
+ return
+ end
+ if not lsp:isOpen(doc.uri) and lsp.workspace.gitignore(path:string()) then
+ return
+ end
+ end
+ -- TODO 支持差量更新
+ lsp:saveText(doc.uri, doc.version, change[1].text)
+ return true
+end
diff --git a/script/src/method/textDocument/didClose.lua b/script/src/method/textDocument/didClose.lua
new file mode 100644
index 00000000..589b212f
--- /dev/null
+++ b/script/src/method/textDocument/didClose.lua
@@ -0,0 +1,5 @@
+return function (lsp, params)
+ local doc = params.textDocument
+ lsp:close(doc.uri)
+ return true
+end
diff --git a/script/src/method/textDocument/didOpen.lua b/script/src/method/textDocument/didOpen.lua
new file mode 100644
index 00000000..e2a67fd2
--- /dev/null
+++ b/script/src/method/textDocument/didOpen.lua
@@ -0,0 +1,5 @@
+return function (lsp, params)
+ local doc = params.textDocument
+ lsp:open(doc.uri, doc.version, doc.text)
+ return true
+end
diff --git a/script/src/method/textDocument/documentHighlight.lua b/script/src/method/textDocument/documentHighlight.lua
new file mode 100644
index 00000000..377ffcdf
--- /dev/null
+++ b/script/src/method/textDocument/documentHighlight.lua
@@ -0,0 +1,37 @@
+local core = require 'core'
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local vm, lines = lsp:loadVM(uri)
+ if not vm then
+ return nil
+ end
+ local position = lines:positionAsChar(params.position.line + 1, params.position.character)
+ local positions = core.highlight(vm, position)
+ if not positions then
+ return nil
+ end
+
+ local result = {}
+ for i, position in ipairs(positions) do
+ local start, finish = position[1], position[2]
+ local start_row, start_col = lines:rowcol(start)
+ local finish_row, finish_col = lines:rowcol(finish)
+ result[i] = {
+ range = {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ -- 这里不用-1,因为前端期待的是匹配完成后的位置
+ character = finish_col,
+ },
+ },
+ kind = position[3],
+ }
+ end
+
+ return result
+end
diff --git a/script/src/method/textDocument/documentSymbol.lua b/script/src/method/textDocument/documentSymbol.lua
new file mode 100644
index 00000000..a4b0c3b7
--- /dev/null
+++ b/script/src/method/textDocument/documentSymbol.lua
@@ -0,0 +1,72 @@
+local core = require 'core'
+local lang = require 'language'
+
+local timerCache = {}
+
+local function posToRange(lines, start, finish)
+ local start_row, start_col = lines:rowcol(start)
+ local finish_row, finish_col = lines:rowcol(finish)
+ return {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ character = finish_col,
+ },
+ }
+end
+
+local function convertRange(lines, symbol)
+ symbol.range = posToRange(lines, symbol.range[1], symbol.range[2])
+ symbol.selectionRange = posToRange(lines, symbol.selectionRange[1], symbol.selectionRange[2])
+ if symbol.name == '' then
+ symbol.name = lang.script.SYMBOL_ANONYMOUS
+ end
+
+ if symbol.children then
+ for _, child in ipairs(symbol.children) do
+ convertRange(lines, child)
+ end
+ end
+end
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+
+ if timerCache[uri] then
+ timerCache[uri]:remove()
+ timerCache[uri] = nil
+ end
+
+ return function (response)
+ local clock = os.clock()
+ timerCache[uri] = ac.loop(0.1, function (t)
+ local vm, lines = lsp:getVM(uri)
+ if not vm then
+ if os.clock() - clock > 10 then
+ t:remove()
+ timerCache[uri] = nil
+ response(nil)
+ end
+ return
+ end
+
+ t:remove()
+ timerCache[uri] = nil
+
+ local symbols = core.documentSymbol(vm)
+ if not symbols then
+ response(nil)
+ return
+ end
+
+ for _, symbol in ipairs(symbols) do
+ convertRange(lines, symbol)
+ end
+
+ response(symbols)
+ end)
+ end
+end
diff --git a/script/src/method/textDocument/foldingRange.lua b/script/src/method/textDocument/foldingRange.lua
new file mode 100644
index 00000000..0320b422
--- /dev/null
+++ b/script/src/method/textDocument/foldingRange.lua
@@ -0,0 +1,57 @@
+local core = require 'core'
+
+local timerCache = {}
+
+local function convertRange(lines, range)
+ local start_row, start_col = lines:rowcol(range.start)
+ local finish_row, finish_col = lines:rowcol(range.finish)
+ local result = {
+ startLine = start_row - 1,
+ endLine = finish_row - 2,
+ kind = range.kind,
+ }
+ if result.startLine >= result.endLine then
+ return nil
+ end
+ return result
+end
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ if timerCache[uri] then
+ timerCache[uri]:remove()
+ timerCache[uri] = nil
+ end
+
+ return function (response)
+ local clock = os.clock()
+ timerCache[uri] = ac.loop(0.1, function (t)
+ local vm, lines = lsp:getVM(uri)
+ if not vm then
+ if os.clock() - clock > 10 then
+ t:remove()
+ timerCache[uri] = nil
+ response(nil)
+ end
+ return
+ end
+
+ t:remove()
+ timerCache[uri] = nil
+
+ local comments = lsp:getComments(uri)
+ local ranges = core.foldingRange(vm, comments)
+ if not ranges then
+ response(nil)
+ return
+ end
+
+ local results = {}
+ for _, range in ipairs(ranges) do
+ results[#results+1] = convertRange(lines, range)
+ end
+
+ response(results)
+ end)
+ end
+end
diff --git a/script/src/method/textDocument/hover.lua b/script/src/method/textDocument/hover.lua
new file mode 100644
index 00000000..f8dba27c
--- /dev/null
+++ b/script/src/method/textDocument/hover.lua
@@ -0,0 +1,44 @@
+local core = require 'core'
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local vm, lines = lsp:loadVM(uri)
+ if not vm then
+ return nil
+ end
+ -- lua是从1开始的,因此都要+1
+ local position = lines:positionAsChar(params.position.line + 1, params.position.character)
+
+ local source = core.findSource(vm, position)
+ if not source then
+ return nil
+ end
+
+ local hover = core.hover(source, lsp)
+ if not hover then
+ return nil
+ end
+
+ local text = ([[
+```lua
+%s
+```
+```lua
+%s
+```
+%s
+```lua
+%s
+```
+%s
+]]):format(hover.label or '', hover.overloads or '', hover.description or '', hover.enum or '', hover.doc or '')
+
+ local response = {
+ contents = {
+ value = text:gsub("```lua\n\n```", ""),
+ kind = 'markdown',
+ }
+ }
+
+ return response
+end
diff --git a/script/src/method/textDocument/implementation.lua b/script/src/method/textDocument/implementation.lua
new file mode 100644
index 00000000..14e2f24c
--- /dev/null
+++ b/script/src/method/textDocument/implementation.lua
@@ -0,0 +1,108 @@
+local core = require 'core'
+
+local function checkWorkSpaceComplete(lsp, source)
+ if not source:bindValue() then
+ return
+ end
+ if not source:bindValue():get 'cross file' then
+ return
+ end
+ lsp:checkWorkSpaceComplete()
+end
+
+local function findResult(lsp, params)
+ local uri = params.textDocument.uri
+ local vm, lines = lsp:loadVM(uri)
+ if not vm then
+ return nil
+ end
+ -- lua是从1开始的,因此都要+1
+ local position = lines:positionAsChar(params.position.line + 1, params.position.character)
+ local source = core.findSource(vm, position)
+ if not source then
+ return nil
+ end
+
+ checkWorkSpaceComplete(lsp, source)
+
+ local positions = core.implementation(vm, source, lsp)
+ if not positions then
+ return nil
+ end
+
+ local locations = {}
+ for i, position in ipairs(positions) do
+ local start, finish, valueUri = position[1], position[2], (position[3] or uri)
+ local _, valueLines = lsp:loadVM(valueUri)
+ if valueLines then
+ local start_row, start_col = valueLines:rowcol(start)
+ local finish_row, finish_col = valueLines:rowcol(finish)
+ locations[#locations+1] = {
+ uri = valueUri,
+ range = {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ -- 这里不用-1,因为前端期待的是匹配完成后的位置
+ character = finish_col,
+ },
+ }
+ }
+ else
+ locations[#locations+1] = {
+ uri = valueUri,
+ range = {
+ start = {
+ line = 0,
+ character = 0,
+ },
+ ['end'] = {
+ line = 0,
+ character = 0,
+ },
+ }
+ }
+ end
+ end
+
+ if #locations == 0 then
+ return nil
+ end
+
+ return locations
+end
+
+local LastTask
+
+return function (lsp, params)
+ if LastTask then
+ LastTask:remove()
+ LastTask = nil
+ end
+ local result = findResult(lsp, params)
+ if result then
+ return result
+ end
+ return function (response)
+ local count = 0
+ LastTask = ac.loop(0.1, function ()
+ local result = findResult(lsp, params)
+ if result then
+ LastTask:remove()
+ LastTask = nil
+ response(result)
+ return
+ end
+ count = count + 1
+ if lsp:isWaitingCompile() and count < 10 then
+ return
+ end
+ LastTask:remove()
+ LastTask = nil
+ response(nil)
+ end)
+ end
+end
diff --git a/script/src/method/textDocument/onTypeFormatting.lua b/script/src/method/textDocument/onTypeFormatting.lua
new file mode 100644
index 00000000..fc9cbdc9
--- /dev/null
+++ b/script/src/method/textDocument/onTypeFormatting.lua
@@ -0,0 +1,14 @@
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local vm, lines = lsp:loadVM(uri)
+ --log.debug(table.dump(params))
+ if not vm then
+ return nil
+ end
+ local position = lines:position(params.position.line + 1, params.position.character)
+ local ch = params.ch
+ local options = params.options
+ local tabSize = options.tabSize
+ local insertSpaces = options.insertSpaces
+ return nil
+end
diff --git a/script/src/method/textDocument/publishDiagnostics.lua b/script/src/method/textDocument/publishDiagnostics.lua
new file mode 100644
index 00000000..c767e934
--- /dev/null
+++ b/script/src/method/textDocument/publishDiagnostics.lua
@@ -0,0 +1,163 @@
+local core = require 'core'
+local lang = require 'language'
+local config = require 'config'
+
+local DiagnosticSeverity = {
+ Error = 1,
+ Warning = 2,
+ Information = 3,
+ Hint = 4,
+}
+
+--[[
+/**
+ * Represents a related message and source code location for a diagnostic. This should be
+ * used to point to code locations that cause or related to a diagnostics, e.g when duplicating
+ * a symbol in a scope.
+ */
+export interface DiagnosticRelatedInformation {
+ /**
+ * The location of this related diagnostic information.
+ */
+ location: Location;
+
+ /**
+ * The message of this related diagnostic information.
+ */
+ message: string;
+}
+]]--
+
+local function getRange(start, finish, lines)
+ local start_row, start_col = lines:rowcol(start)
+ local finish_row, finish_col = lines:rowcol(finish)
+ return {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ -- 这里不用-1,因为前端期待的是匹配完成后的位置
+ character = finish_col,
+ },
+ }
+end
+
+local function createInfo(lsp, data, lines)
+ local diagnostic = {
+ source = lang.script.DIAG_DIAGNOSTICS,
+ range = getRange(data.start, data.finish, lines),
+ severity = data.level,
+ message = data.message,
+ code = data.code,
+ tags = data.tags,
+ }
+ if data.related then
+ local related = {}
+ for _, info in ipairs(data.related) do
+ local _, lines = lsp:getVM(info.uri)
+ if lines then
+ local message = info.message
+ if not message then
+ local start_line = lines:rowcol(info.start)
+ local finish_line = lines:rowcol(info.finish)
+ local chars = {}
+ for n = start_line, finish_line do
+ chars[#chars+1] = lines:line(n)
+ end
+ message = table.concat(chars, '\n')
+ end
+ related[#related+1] = {
+ message = message,
+ location = {
+ uri = info.uri,
+ range = getRange(info.start, info.finish, lines),
+ }
+ }
+ end
+ end
+ diagnostic.relatedInformation = related
+ end
+ return diagnostic
+end
+
+local function buildError(err, lines, uri)
+ local diagnostic = {
+ source = lang.script.DIAG_SYNTAX_CHECK,
+ message = lang.script('PARSER_'..err.type, err.info)
+ }
+ if err.version then
+ local currentVersion = err.info and err.info.version or config.config.runtime.version
+ if type(err.version) == 'table' then
+ diagnostic.message = ('%s(%s)'):format(diagnostic.message, lang.script('DIAG_NEED_VERSION', table.concat(err.version, '/'), currentVersion))
+ else
+ diagnostic.message = ('%s(%s)'):format(diagnostic.message, lang.script('DIAG_NEED_VERSION', err.version, currentVersion))
+ end
+ end
+ if err.level == 'error' then
+ diagnostic.severity = DiagnosticSeverity.Error
+ else
+ diagnostic.severity = DiagnosticSeverity.Warning
+ end
+ local startrow, startcol = lines:rowcol(err.start)
+ local endrow, endcol = lines:rowcol(err.finish)
+ if err.type == 'UNKNOWN' then
+ local _, max = lines:range(endrow)
+ endcol = max
+ end
+ local range = {
+ start = {
+ line = startrow - 1,
+ character = startcol - 1,
+ },
+ ['end'] = {
+ line = endrow - 1,
+ character = endcol,
+ },
+ }
+ diagnostic.range = range
+
+ local related = err.info and err.info.related
+ if related then
+ local start_line = lines:rowcol(related[1])
+ local finish_line = lines:rowcol(related[2])
+ local chars = {}
+ for n = start_line, finish_line do
+ chars[#chars+1] = lines:line(n)
+ end
+ local message = table.concat(chars, '\n')
+ diagnostic.relatedInformation = {
+ {
+ message = message,
+ location = {
+ uri = uri,
+ range = getRange(related[1], related[2], lines),
+ }
+ }
+ }
+ end
+ return diagnostic
+end
+
+return function (lsp, params)
+ local vm = params.vm
+ local lines = params.lines
+ local uri = params.uri
+ local errs = lsp:getAstErrors(uri)
+
+ local diagnostics = {}
+ if vm then
+ local datas = core.diagnostics(vm, lines, uri)
+ for _, data in ipairs(datas) do
+ diagnostics[#diagnostics+1] = createInfo(lsp, data, lines)
+ end
+ end
+ if errs then
+ for _, err in ipairs(errs) do
+ diagnostics[#diagnostics+1] = buildError(err, lines, uri)
+ end
+ end
+
+ return diagnostics
+end
diff --git a/script/src/method/textDocument/references.lua b/script/src/method/textDocument/references.lua
new file mode 100644
index 00000000..0a198323
--- /dev/null
+++ b/script/src/method/textDocument/references.lua
@@ -0,0 +1,86 @@
+local core = require 'core'
+local LastTask
+
+local function findReferences(lsp, uri, position)
+ local vm = lsp:getVM(uri)
+
+ local positions, isGlobal = core.definition(vm, position, 'reference')
+ if not positions then
+ return nil, isGlobal
+ end
+
+ local locations = {}
+ for i, position in ipairs(positions) do
+ local start, finish, valueUri = position[1], position[2], (position[3] or uri)
+ local vm, valueLines = lsp:getVM(valueUri)
+ if valueLines then
+ local start_row, start_col = valueLines:rowcol(start)
+ local finish_row, finish_col = valueLines:rowcol(finish)
+ locations[#locations+1] = {
+ uri = valueUri,
+ range = {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ -- 这里不用-1,因为前端期待的是匹配完成后的位置
+ character = finish_col,
+ },
+ }
+ }
+ elseif vm then
+ locations[#locations+1] = {
+ uri = valueUri,
+ range = {
+ start = {
+ line = 0,
+ character = 0,
+ },
+ ['end'] = {
+ line = 0,
+ character = 0,
+ },
+ }
+ }
+ end
+ end
+
+ if #locations == 0 then
+ return nil, isGlobal
+ end
+
+ return locations, isGlobal
+end
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local declarat = params.context.includeDeclaration
+ local vm, lines = lsp:loadVM(uri)
+ if not vm then
+ return nil
+ end
+
+ if LastTask then
+ LastTask:remove()
+ LastTask = nil
+ end
+
+ -- lua是从1开始的,因此都要+1
+ local position = lines:positionAsChar(params.position.line + 1, params.position.character)
+
+ return function (response)
+ local clock = os.clock()
+ LastTask = ac.loop(0.1, function ()
+ local positions, isGlobal = findReferences(lsp, uri, position)
+ if isGlobal and lsp:isWaitingCompile() and os.clock() - clock < 5 then
+ return
+ end
+ response(positions)
+ LastTask:remove()
+ LastTask = nil
+ end)
+ LastTask:onTimer()
+ end
+end
diff --git a/script/src/method/textDocument/rename.lua b/script/src/method/textDocument/rename.lua
new file mode 100644
index 00000000..6da9c721
--- /dev/null
+++ b/script/src/method/textDocument/rename.lua
@@ -0,0 +1,50 @@
+local core = require 'core'
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local newName = params.newName
+ local vm, lines = lsp:loadVM(uri)
+ if not vm then
+ return {}
+ end
+ local position = lines:positionAsChar(params.position.line + 1, params.position.character)
+ local positions = core.rename(vm, position, newName)
+ if not positions then
+ return {}
+ end
+
+ local changes = {}
+ for _, position in ipairs(positions) do
+ local start, finish, uri = position[1], position[2], position[3]
+ local _, lines = lsp:getVM(uri)
+ if not lines then
+ goto CONTINUE
+ end
+ local start_row, start_col = lines:rowcol(start)
+ local finish_row, finish_col = lines:rowcol(finish)
+ if not changes[uri] then
+ changes[uri] = {}
+ end
+ changes[uri][#changes[uri]+1] = {
+ newText = newName,
+ range = {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ -- 这里不用-1,因为前端期待的是匹配完成后的位置
+ character = finish_col,
+ },
+ }
+ }
+ ::CONTINUE::
+ end
+
+ local response = {
+ changes = changes,
+ }
+
+ return response
+end
diff --git a/script/src/method/textDocument/signatureHelp.lua b/script/src/method/textDocument/signatureHelp.lua
new file mode 100644
index 00000000..01d6289d
--- /dev/null
+++ b/script/src/method/textDocument/signatureHelp.lua
@@ -0,0 +1,50 @@
+local core = require 'core'
+
+return function (lsp, params)
+ local uri = params.textDocument.uri
+ local vm, lines = lsp:loadVM(uri)
+ if not vm then
+ return
+ end
+ local position = lines:position(params.position.line + 1, params.position.character + 1)
+ local hovers = core.signature(vm, position)
+ if not hovers then
+ return
+ end
+
+ local hover = hovers[1]
+ local desc = {}
+ desc[#desc+1] = hover.description
+ local active
+ local signatures = {}
+ for i, hover in ipairs(hovers) do
+ local signature = {
+ label = hover.label,
+ documentation = {
+ kind = 'markdown',
+ value = table.concat(desc, '\n'),
+ },
+ }
+ if hover.argLabel then
+ if not active then
+ active = i
+ end
+ signature.parameters = {
+ {
+ label = {
+ hover.argLabel[1] - 1,
+ hover.argLabel[2],
+ }
+ }
+ }
+ end
+ signatures[i] = signature
+ end
+
+ local response = {
+ signatures = signatures,
+ activeSignature = active and active - 1 or 0,
+ }
+
+ return response
+end
diff --git a/script/src/method/workspace/didChangeConfiguration.lua b/script/src/method/workspace/didChangeConfiguration.lua
new file mode 100644
index 00000000..ecaa9182
--- /dev/null
+++ b/script/src/method/workspace/didChangeConfiguration.lua
@@ -0,0 +1,27 @@
+local rpc = require 'rpc'
+
+return function (lsp)
+ local uri = lsp.workspace and lsp.workspace.uri
+ -- 请求配置
+ rpc:request('workspace/configuration', {
+ items = {
+ {
+ scopeUri = uri,
+ section = 'Lua',
+ },
+ {
+ scopeUri = uri,
+ section = 'files.associations',
+ },
+ {
+ scopeUri = uri,
+ section = 'files.exclude',
+ }
+ },
+ }, function (configs)
+ lsp:onUpdateConfig(configs[1], {
+ associations = configs[2],
+ exclude = configs[3],
+ })
+ end)
+end
diff --git a/script/src/method/workspace/didChangeWatchedFiles.lua b/script/src/method/workspace/didChangeWatchedFiles.lua
new file mode 100644
index 00000000..3ce68924
--- /dev/null
+++ b/script/src/method/workspace/didChangeWatchedFiles.lua
@@ -0,0 +1,44 @@
+local fs = require 'bee.filesystem'
+local uric = require 'uri'
+
+local FileChangeType = {
+ Created = 1,
+ Changed = 2,
+ Deleted = 3,
+}
+
+return function (lsp, params)
+ if not lsp.workspace then
+ return
+ end
+ local needReset
+ for _, change in ipairs(params.changes) do
+ local path = uric.decode(change.uri)
+ if not path then
+ goto CONTINUE
+ end
+ if change.type == FileChangeType.Created then
+ lsp.workspace:addFile(path)
+ if lsp:getVM(change.uri) then
+ needReset = true
+ end
+ elseif change.type == FileChangeType.Deleted then
+ lsp.workspace:removeFile(path)
+ if lsp:getVM(change.uri) then
+ needReset = true
+ end
+ end
+ -- 排除类文件发生更改需要重新扫描
+ local filename = path:filename():string()
+ if lsp.workspace:fileNameEq(filename, '.gitignore')
+ or lsp.workspace:fileNameEq(filename, '.gitmodules')
+ then
+ lsp:reScanFiles()
+ end
+ ::CONTINUE::
+ end
+ -- 缓存过的文件发生变化后,重新计算
+ if needReset then
+ lsp.workspace:reset()
+ end
+end
diff --git a/script/src/method/workspace/didChangeWorkspaceFolders.lua b/script/src/method/workspace/didChangeWorkspaceFolders.lua
new file mode 100644
index 00000000..01a28abd
--- /dev/null
+++ b/script/src/method/workspace/didChangeWorkspaceFolders.lua
@@ -0,0 +1,20 @@
+local rpc = require 'rpc'
+local lang = require 'language'
+
+return function ()
+ -- 暂不支持多个工作目录,因此当工作目录切换时,暴力结束服务,让前端重启服务
+ rpc:requestWait('window/showMessageRequest', {
+ type = 3,
+ message = lang.script('MWS_NOT_SUPPORT', '[Lua]'),
+ actions = {
+ {
+ title = lang.script.MWS_RESTART,
+ }
+ }
+ }, function ()
+ os.exit(true)
+ end)
+ ac.wait(5, function ()
+ os.exit(true)
+ end)
+end
diff --git a/script/src/method/workspace/executeCommand.lua b/script/src/method/workspace/executeCommand.lua
new file mode 100644
index 00000000..cfa4023e
--- /dev/null
+++ b/script/src/method/workspace/executeCommand.lua
@@ -0,0 +1,258 @@
+local fs = require 'bee.filesystem'
+local json = require 'json'
+local config = require 'config'
+local rpc = require 'rpc'
+local lang = require 'language'
+local platform = require 'bee.platform'
+
+local command = {}
+
+local function isContainPos(obj, start, finish)
+ if obj.start <= start and obj.finish >= finish then
+ return true
+ end
+ return false
+end
+
+local function isInString(vm, start, finish)
+ return vm:eachSource(function (source)
+ if source.type == 'string' and isContainPos(source, start, finish) then
+ return true
+ end
+ end)
+end
+
+local function posToRange(lines, start, finish)
+ local start_row, start_col = lines:rowcol(start)
+ local finish_row, finish_col = lines:rowcol(finish)
+ return {
+ start = {
+ line = start_row - 1,
+ character = start_col - 1,
+ },
+ ['end'] = {
+ line = finish_row - 1,
+ character = finish_col,
+ },
+ }
+end
+
+function command.config(lsp, data)
+ local def = config.config
+ for _, k in ipairs(data.key) do
+ def = def[k]
+ if not def then
+ return
+ end
+ end
+ if data.action == 'add' then
+ if type(def) ~= 'table' then
+ return
+ end
+ end
+
+ local vscodePath
+ local mode
+ if lsp.workspace then
+ vscodePath = lsp.workspace.root / '.vscode'
+ mode = 'workspace'
+ else
+ if platform.OS == 'Windows' then
+ vscodePath = fs.path(os.getenv 'USERPROFILE') / 'AppData' / 'Roaming' / 'Code' / 'User'
+ else
+ vscodePath = fs.path(os.getenv 'HOME') / '.vscode-server' / 'data' / 'Machine'
+ end
+ mode = 'user'
+ if not fs.exists(vscodePath) then
+ rpc:notify('window/showMessage', {
+ type = 3,
+ message = lang.script.MWS_UCONFIG_FAILED,
+ })
+ return
+ end
+ end
+
+ local settingBuf = io.load(vscodePath / 'settings.json')
+ if not settingBuf then
+ fs.create_directories(vscodePath)
+ end
+
+ local setting = json.decode(settingBuf or '', true) or {}
+ local key = 'Lua.' .. table.concat(data.key, '.')
+ local attr = setting[key]
+
+ if data.action == 'add' then
+ if attr == nil then
+ attr = {}
+ elseif type(attr) == 'string' then
+ attr = {}
+ for str in attr:gmatch '[^;]+' do
+ attr[#attr+1] = str
+ end
+ elseif type(attr) == 'table' then
+ else
+ return
+ end
+
+ attr[#attr+1] = data.value
+ setting[key] = attr
+ elseif data.action == 'set' then
+ setting[key] = data.value
+ end
+
+ io.save(vscodePath / 'settings.json', json.encode(setting) .. '\r\n')
+
+ if mode == 'workspace' then
+ rpc:notify('window/showMessage', {
+ type = 3,
+ message = lang.script.MWS_WCONFIG_UPDATED,
+ })
+ elseif mode == 'user' then
+ rpc:notify('window/showMessage', {
+ type = 3,
+ message = lang.script.MWS_UCONFIG_UPDATED,
+ })
+ end
+end
+
+function command.removeSpace(lsp, data)
+ local uri = data.uri
+ local vm, lines = lsp:getVM(uri)
+ if not vm then
+ return
+ end
+
+ local textEdit = {}
+ for i = 1, #lines do
+ local line = lines:line(i)
+ local pos = line:find '[ \t]+$'
+ if pos then
+ local start, finish = lines:range(i)
+ start = start + pos - 1
+ if isInString(vm, start, finish) then
+ goto NEXT_LINE
+ end
+ textEdit[#textEdit+1] = {
+ range = posToRange(lines, start, finish),
+ newText = '',
+ }
+ goto NEXT_LINE
+ end
+
+ ::NEXT_LINE::
+ end
+
+ if #textEdit == 0 then
+ return
+ end
+
+ rpc:request('workspace/applyEdit', {
+ label = lang.script.COMMAND_REMOVE_SPACE,
+ edit = {
+ changes = {
+ [uri] = textEdit,
+ }
+ },
+ })
+end
+
+local opMap = {
+ ['+'] = true,
+ ['-'] = true,
+ ['*'] = true,
+ ['/'] = true,
+ ['//'] = true,
+ ['^'] = true,
+ ['<<'] = true,
+ ['>>'] = true,
+ ['&'] = true,
+ ['|'] = true,
+ ['~'] = true,
+ ['..'] = true,
+}
+
+local literalMap = {
+ ['number'] = true,
+ ['boolean'] = true,
+ ['string'] = true,
+ ['table'] = true,
+}
+
+function command.solve(lsp, data)
+ local uri = data.uri
+ local vm, lines = lsp:getVM(uri)
+ if not vm then
+ return
+ end
+
+ local start = lines:position(data.range.start.line + 1, data.range.start.character + 1)
+ local finish = lines:position(data.range['end'].line + 1, data.range['end'].character)
+
+ local result = vm:eachSource(function (source)
+ if not isContainPos(source, start, finish) then
+ return
+ end
+ if source.op ~= 'or' then
+ return
+ end
+ local first = source[1]
+ local second = source[2]
+ -- (a + b) or 0 --> a + (b or 0)
+ do
+ if opMap[first.op]
+ and first.type ~= 'unary'
+ and not second.op
+ and literalMap[second.type]
+ then
+ return {
+ start = source[1][2].start,
+ finish = source[2].finish,
+ }
+ end
+ end
+ -- a or (b + c) --> (a or b) + c
+ do
+ if opMap[second.op]
+ and second.type ~= 'unary'
+ and not first.op
+ and literalMap[second[1].type]
+ then
+ return {
+ start = source[1].start,
+ finish = source[2][1].finish,
+ }
+ end
+ end
+ end)
+
+ if not result then
+ return
+ end
+
+ rpc:request('workspace/applyEdit', {
+ label = lang.script.COMMAND_ADD_BRACKETS,
+ edit = {
+ changes = {
+ [uri] = {
+ {
+ range = posToRange(lines, result.start, result.start - 1),
+ newText = '(',
+ },
+ {
+ range = posToRange(lines, result.finish + 1, result.finish),
+ newText = ')',
+ },
+ }
+ }
+ },
+ })
+end
+
+return function (lsp, params)
+ local name = params.command
+ if not command[name] then
+ return
+ end
+ local result = command[name](lsp, params.arguments[1])
+ return result
+end