diff options
Diffstat (limited to 'script/method')
25 files changed, 1430 insertions, 0 deletions
diff --git a/script/method/exit.lua b/script/method/exit.lua new file mode 100644 index 00000000..fa550243 --- /dev/null +++ b/script/method/exit.lua @@ -0,0 +1,4 @@ +return function () + log.info('Server exited.') + os.exit(true) +end diff --git a/script/method/init.lua b/script/method/init.lua new file mode 100644 index 00000000..8827768b --- /dev/null +++ b/script/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/method/initialize.lua b/script/method/initialize.lua new file mode 100644 index 00000000..02a96695 --- /dev/null +++ b/script/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/method/initialized.lua b/script/method/initialized.lua new file mode 100644 index 00000000..d84a2159 --- /dev/null +++ b/script/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/method/shutdown.lua b/script/method/shutdown.lua new file mode 100644 index 00000000..bb81306e --- /dev/null +++ b/script/method/shutdown.lua @@ -0,0 +1,4 @@ +return function () + log.info('Server shutdown.') + return true +end diff --git a/script/method/textDocument/codeAction.lua b/script/method/textDocument/codeAction.lua new file mode 100644 index 00000000..3c6e8d49 --- /dev/null +++ b/script/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/method/textDocument/completion.lua b/script/method/textDocument/completion.lua new file mode 100644 index 00000000..4c7581df --- /dev/null +++ b/script/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/method/textDocument/definition.lua b/script/method/textDocument/definition.lua new file mode 100644 index 00000000..dbf9e41c --- /dev/null +++ b/script/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/method/textDocument/didChange.lua b/script/method/textDocument/didChange.lua new file mode 100644 index 00000000..82e6c096 --- /dev/null +++ b/script/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/method/textDocument/didClose.lua b/script/method/textDocument/didClose.lua new file mode 100644 index 00000000..589b212f --- /dev/null +++ b/script/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/method/textDocument/didOpen.lua b/script/method/textDocument/didOpen.lua new file mode 100644 index 00000000..e2a67fd2 --- /dev/null +++ b/script/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/method/textDocument/documentHighlight.lua b/script/method/textDocument/documentHighlight.lua new file mode 100644 index 00000000..377ffcdf --- /dev/null +++ b/script/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/method/textDocument/documentSymbol.lua b/script/method/textDocument/documentSymbol.lua new file mode 100644 index 00000000..a4b0c3b7 --- /dev/null +++ b/script/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/method/textDocument/foldingRange.lua b/script/method/textDocument/foldingRange.lua new file mode 100644 index 00000000..0320b422 --- /dev/null +++ b/script/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/method/textDocument/hover.lua b/script/method/textDocument/hover.lua new file mode 100644 index 00000000..f8dba27c --- /dev/null +++ b/script/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/method/textDocument/implementation.lua b/script/method/textDocument/implementation.lua new file mode 100644 index 00000000..14e2f24c --- /dev/null +++ b/script/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/method/textDocument/onTypeFormatting.lua b/script/method/textDocument/onTypeFormatting.lua new file mode 100644 index 00000000..fc9cbdc9 --- /dev/null +++ b/script/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/method/textDocument/publishDiagnostics.lua b/script/method/textDocument/publishDiagnostics.lua new file mode 100644 index 00000000..c767e934 --- /dev/null +++ b/script/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/method/textDocument/references.lua b/script/method/textDocument/references.lua new file mode 100644 index 00000000..0a198323 --- /dev/null +++ b/script/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/method/textDocument/rename.lua b/script/method/textDocument/rename.lua new file mode 100644 index 00000000..6da9c721 --- /dev/null +++ b/script/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/method/textDocument/signatureHelp.lua b/script/method/textDocument/signatureHelp.lua new file mode 100644 index 00000000..01d6289d --- /dev/null +++ b/script/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/method/workspace/didChangeConfiguration.lua b/script/method/workspace/didChangeConfiguration.lua new file mode 100644 index 00000000..ecaa9182 --- /dev/null +++ b/script/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/method/workspace/didChangeWatchedFiles.lua b/script/method/workspace/didChangeWatchedFiles.lua new file mode 100644 index 00000000..3ce68924 --- /dev/null +++ b/script/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/method/workspace/didChangeWorkspaceFolders.lua b/script/method/workspace/didChangeWorkspaceFolders.lua new file mode 100644 index 00000000..01a28abd --- /dev/null +++ b/script/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/method/workspace/executeCommand.lua b/script/method/workspace/executeCommand.lua new file mode 100644 index 00000000..cfa4023e --- /dev/null +++ b/script/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 |