diff --git a/projects/lsp/message.json b/projects/lsp/message.json new file mode 100644 index 00000000..8f63e53c --- /dev/null +++ b/projects/lsp/message.json @@ -0,0 +1,487 @@ +{ + "jsonrpc": "2.0", + "id": 0, + "method": "initialize", + "params": { + "processId": 12608, + "clientInfo": { + "name": "Visual Studio Code", + "version": "1.95.2" + }, + "locale": "en", + "rootPath": "/Users/kengorab/Desktop/abra-vscode/examples", + "rootUri": "file:///Users/kengorab/Desktop/abra-vscode/examples", + "capabilities": { + "workspace": { + "applyEdit": true, + "workspaceEdit": { + "documentChanges": true, + "resourceOperations": [ + "create", + "rename", + "delete" + ], + "failureHandling": "textOnlyTransactional", + "normalizesLineEndings": true, + "changeAnnotationSupport": { + "groupsOnLabel": true + } + }, + "configuration": true, + "didChangeWatchedFiles": { + "dynamicRegistration": true, + "relativePatternSupport": true + }, + "symbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "resolveSupport": { + "properties": [ + "location.range" + ] + } + }, + "codeLens": { + "refreshSupport": true + }, + "executeCommand": { + "dynamicRegistration": true + }, + "didChangeConfiguration": { + "dynamicRegistration": true + }, + "workspaceFolders": true, + "foldingRange": { + "refreshSupport": true + }, + "semanticTokens": { + "refreshSupport": true + }, + "fileOperations": { + "dynamicRegistration": true, + "didCreate": true, + "didRename": true, + "didDelete": true, + "willCreate": true, + "willRename": true, + "willDelete": true + }, + "inlineValue": { + "refreshSupport": true + }, + "inlayHint": { + "refreshSupport": true + }, + "diagnostics": { + "refreshSupport": true + } + }, + "textDocument": { + "publishDiagnostics": { + "relatedInformation": true, + "versionSupport": false, + "tagSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "codeDescriptionSupport": true, + "dataSupport": true + }, + "synchronization": { + "dynamicRegistration": true, + "willSave": true, + "willSaveWaitUntil": true, + "didSave": true + }, + "completion": { + "dynamicRegistration": true, + "contextSupport": true, + "completionItem": { + "snippetSupport": true, + "commitCharactersSupport": true, + "documentationFormat": [ + "markdown", + "plaintext" + ], + "deprecatedSupport": true, + "preselectSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "insertReplaceSupport": true, + "resolveSupport": { + "properties": [ + "documentation", + "detail", + "additionalTextEdits" + ] + }, + "insertTextModeSupport": { + "valueSet": [ + 1, + 2 + ] + }, + "labelDetailsSupport": true + }, + "insertTextMode": 2, + "completionItemKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25 + ] + }, + "completionList": { + "itemDefaults": [ + "commitCharacters", + "editRange", + "insertTextFormat", + "insertTextMode", + "data" + ] + } + }, + "hover": { + "dynamicRegistration": true, + "contentFormat": [ + "markdown", + "plaintext" + ] + }, + "signatureHelp": { + "dynamicRegistration": true, + "signatureInformation": { + "documentationFormat": [ + "markdown", + "plaintext" + ], + "parameterInformation": { + "labelOffsetSupport": true + }, + "activeParameterSupport": true + }, + "contextSupport": true + }, + "definition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "references": { + "dynamicRegistration": true + }, + "documentHighlight": { + "dynamicRegistration": true + }, + "documentSymbol": { + "dynamicRegistration": true, + "symbolKind": { + "valueSet": [ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 25, + 26 + ] + }, + "hierarchicalDocumentSymbolSupport": true, + "tagSupport": { + "valueSet": [ + 1 + ] + }, + "labelSupport": true + }, + "codeAction": { + "dynamicRegistration": true, + "isPreferredSupport": true, + "disabledSupport": true, + "dataSupport": true, + "resolveSupport": { + "properties": [ + "edit" + ] + }, + "codeActionLiteralSupport": { + "codeActionKind": { + "valueSet": [ + "", + "quickfix", + "refactor", + "refactor.extract", + "refactor.inline", + "refactor.rewrite", + "source", + "source.organizeImports" + ] + } + }, + "honorsChangeAnnotations": true + }, + "codeLens": { + "dynamicRegistration": true + }, + "formatting": { + "dynamicRegistration": true + }, + "rangeFormatting": { + "dynamicRegistration": true, + "rangesSupport": true + }, + "onTypeFormatting": { + "dynamicRegistration": true + }, + "rename": { + "dynamicRegistration": true, + "prepareSupport": true, + "prepareSupportDefaultBehavior": 1, + "honorsChangeAnnotations": true + }, + "documentLink": { + "dynamicRegistration": true, + "tooltipSupport": true + }, + "typeDefinition": { + "dynamicRegistration": true, + "linkSupport": true + }, + "implementation": { + "dynamicRegistration": true, + "linkSupport": true + }, + "colorProvider": { + "dynamicRegistration": true + }, + "foldingRange": { + "dynamicRegistration": true, + "rangeLimit": 5000, + "lineFoldingOnly": true, + "foldingRangeKind": { + "valueSet": [ + "comment", + "imports", + "region" + ] + }, + "foldingRange": { + "collapsedText": false + } + }, + "declaration": { + "dynamicRegistration": true, + "linkSupport": true + }, + "selectionRange": { + "dynamicRegistration": true + }, + "callHierarchy": { + "dynamicRegistration": true + }, + "semanticTokens": { + "dynamicRegistration": true, + "tokenTypes": [ + "namespace", + "type", + "class", + "enum", + "interface", + "struct", + "typeParameter", + "parameter", + "variable", + "property", + "enumMember", + "event", + "function", + "method", + "macro", + "keyword", + "modifier", + "comment", + "string", + "number", + "regexp", + "operator", + "decorator" + ], + "tokenModifiers": [ + "declaration", + "definition", + "readonly", + "static", + "deprecated", + "abstract", + "async", + "modification", + "documentation", + "defaultLibrary" + ], + "formats": [ + "relative" + ], + "requests": { + "range": true, + "full": { + "delta": true + } + }, + "multilineTokenSupport": false, + "overlappingTokenSupport": false, + "serverCancelSupport": true, + "augmentsSyntaxTokens": true + }, + "linkedEditingRange": { + "dynamicRegistration": true + }, + "typeHierarchy": { + "dynamicRegistration": true + }, + "inlineValue": { + "dynamicRegistration": true + }, + "inlayHint": { + "dynamicRegistration": true, + "resolveSupport": { + "properties": [ + "tooltip", + "textEdits", + "label.tooltip", + "label.location", + "label.command" + ] + } + }, + "diagnostic": { + "dynamicRegistration": true, + "relatedDocumentSupport": false + } + }, + "window": { + "showMessage": { + "messageActionItem": { + "additionalPropertiesSupport": true + } + }, + "showDocument": { + "support": true + }, + "workDoneProgress": true + }, + "general": { + "staleRequestSupport": { + "cancel": true, + "retryOnContentModified": [ + "textDocument/semanticTokens/full", + "textDocument/semanticTokens/range", + "textDocument/semanticTokens/full/delta" + ] + }, + "regularExpressions": { + "engine": "ECMAScript", + "version": "ES2020" + }, + "markdown": { + "parser": "marked", + "version": "1.1.0" + }, + "positionEncodings": [ + "utf-16" + ] + }, + "notebookDocument": { + "synchronization": { + "dynamicRegistration": true, + "executionSummarySupport": true + } + } + }, + "trace": "off", + "workspaceFolders": [ + { + "uri": "file:///Users/kengorab/Desktop/abra-vscode/examples", + "name": "examples" + } + ] + } +} \ No newline at end of file diff --git a/projects/lsp/src/example.abra b/projects/lsp/src/example.abra new file mode 100644 index 00000000..b6c9236c --- /dev/null +++ b/projects/lsp/src/example.abra @@ -0,0 +1,47 @@ +import "process" as process +import "fs" as fs +import "json" as json +import "./lsp_spec" as lsp + +val cwd = fs.getCurrentWorkingDirectory() +val file = match fs.readFile("$cwd/message.json") { + Ok(v) => v + Err(e) => { + println(e) + process.exit(1) + } +} + +val jsonValue = match json.JsonParser.parseString(file) { + Ok(v) => v + Err(e) => { + println(e) + process.exit(1) + } +} + +val message = match lsp.RequestMessage.fromJson(jsonValue) { + Ok(v) => v + Err(e) => { + println(e) + process.exit(1) + } +} + +println(message) + +val msg = lsp.ResponseMessage.Success( + id: 0, + result: Some(lsp.ResponseResult.Initialize( + capabilities: lsp.ServerCapabilities( + textDocumentSync: Some(lsp.TextDocumentSyncKind.Full), + ), + serverInfo: lsp.ServerInfo( + name: "abra-lsp", + version: Some("0.0.1") + ) + )) +) +val msgJson = msg.toJson() +println(msgJson) +println(msgJson.encode()) diff --git a/projects/lsp/src/handlers.abra b/projects/lsp/src/handlers.abra new file mode 100644 index 00000000..e28c7c27 --- /dev/null +++ b/projects/lsp/src/handlers.abra @@ -0,0 +1,25 @@ +import RequestMessage, NotificationMessage, ResponseMessage, ResponseResult, ServerCapabilities, TextDocumentSyncKind, ServerInfo from "./lsp_spec" + +export func handleRequest(req: RequestMessage): ResponseMessage { + match req { + RequestMessage.Initialize(id, _, _) => { + val result = ResponseResult.Initialize( + capabilities: ServerCapabilities( + textDocumentSync: Some(TextDocumentSyncKind.Full), + ), + serverInfo: ServerInfo( + name: "abra-lsp", + version: Some("0.0.1") + ) + ) + + ResponseMessage.Success(id: id, result: Some(result)) + } + } +} + +export func handleNotification(req: NotificationMessage) { + match req { + NotificationMessage.Initialized => {} + } +} \ No newline at end of file diff --git a/projects/lsp/src/log.abra b/projects/lsp/src/log.abra new file mode 100644 index 00000000..c7ed423b --- /dev/null +++ b/projects/lsp/src/log.abra @@ -0,0 +1,12 @@ +import "process" as process +import "fs" as fs + +val cwd = "/Users/kengorab/Desktop/abra-lang/projects/lsp" //fs.getCurrentWorkingDirectory() +val logFilePath = "$cwd/log.txt" +export val log = match fs.createFile(logFilePath, fs.AccessMode.WriteOnly) { + Ok(v) => v + Err(e) => { + println(e) + process.exit(1) + } +} diff --git a/projects/lsp/src/lsp_spec.abra b/projects/lsp/src/lsp_spec.abra new file mode 100644 index 00000000..a7c15723 --- /dev/null +++ b/projects/lsp/src/lsp_spec.abra @@ -0,0 +1,164 @@ +import log from "./log" +import JsonValue, JsonError, JsonObject from "json" + +export enum RequestMessage { + Initialize(id: Int, processId: Int?, rootPath: String?) + + func fromJson(json: JsonValue): Result { + val obj = try json.asObject() + val id = match try obj.getNumberRequired("id") { + Either.Left(int) => int + Either.Right(float) => float.asInt() + } + val method = try obj.getStringRequired("method") + val params = obj.getObject("params") + + match method { + "initialize" => { + val params = try obj.getObjectRequired("params") + val processId = match params.getNumber("processId") { + None => None + Either.Left(int) => Some(int) + Either.Right(float) => Some(float.asInt()) + } + val rootPath = params.getString("rootPath") + + Ok(RequestMessage.Initialize(id: id, processId: processId, rootPath: rootPath)) + } + else => { + log.writeln("Error: Unimplemented RequestMessage method '$method'") + todo("[RequestMessage.fromJson]: method='$method'") + } + } + } +} + +export enum NotificationMessage { + Initialized + + func fromJson(json: JsonValue): Result { + val obj = try json.asObject() + val method = try obj.getStringRequired("method") + + match method { + "initialized" => Ok(NotificationMessage.Initialized) + else => { + log.writeln("Error: Unimplemented NotificationMessage method '$method'") + todo("[NotificationMessage.fromJson]: method='$method'") + } + } + } +} + +export enum ResponseMessage { + Success(id: Int, result: ResponseResult?) + Error(id: Int, error: ResponseError) + + func toJson(self): JsonValue { + val obj = JsonObject() + + match self { + ResponseMessage.Success(id, result) => { + obj.set("id", JsonValue.Number(Either.Left(id))) + obj.set("result", result?.toJson() ?: JsonValue.Null) + } + ResponseMessage.Error(id, error) => { + obj.set("id", JsonValue.Number(Either.Left(id))) + } + } + + JsonValue.Object(obj) + } +} + +export enum ResponseResult { + Initialize(capabilities: ServerCapabilities, serverInfo: ServerInfo) + + func toJson(self): JsonValue { + val obj = JsonObject() + + match self { + ResponseResult.Initialize(capabilities, serverInfo) => { + obj.set("capabilities", capabilities.toJson()) + obj.set("serverInfo", serverInfo.toJson()) + } + } + + JsonValue.Object(obj) + } +} + +export type ResponseError { + code: ResponseErrorCode + message: String +} + +export enum ResponseErrorCode { + ParseError + InvalidRequest + MethodNotFound + InvalidParams + InternalError + ServerNotInitialized + Unknown + RequestFailed + ServerCancelled + ContentModified + RequestCancelled + + func intVal(self): Int = match self { + ResponseErrorCode.ParseError => -32700 + ResponseErrorCode.InvalidRequest => -32600 + ResponseErrorCode.MethodNotFound => -32601 + ResponseErrorCode.InvalidParams => -32602 + ResponseErrorCode.InternalError => -32603 + ResponseErrorCode.ServerNotInitialized => -32002 + ResponseErrorCode.Unknown => -32001 + ResponseErrorCode.RequestFailed => -32803 + ResponseErrorCode.ServerCancelled => -32802 + ResponseErrorCode.ContentModified => -32801 + ResponseErrorCode.RequestCancelled => -32800 + } +} + +export type ServerCapabilities { + textDocumentSync: TextDocumentSyncKind? = None + + func toJson(self): JsonValue { + val obj = JsonObject() + + if self.textDocumentSync |tds| { + obj.set("textDocumentSync", JsonValue.Number(Either.Left(tds.intVal()))) + } + + JsonValue.Object(obj) + } +} + +export enum TextDocumentSyncKind { + None_ + Full + Incremental + + func intVal(self): Int = match self { + TextDocumentSyncKind.None_ => 0 + TextDocumentSyncKind.Full => 1 + TextDocumentSyncKind.Incremental => 2 + } +} + +export type ServerInfo { + name: String + version: String? = None + + func toJson(self): JsonValue { + val obj = JsonObject() + + obj.set("name", JsonValue.String(self.name)) + if self.version |version| { + obj.set("version", JsonValue.String(version)) + } + + JsonValue.Object(obj) + } +} diff --git a/projects/lsp/src/main.abra b/projects/lsp/src/main.abra index 8e7fd105..c5f3272f 100644 --- a/projects/lsp/src/main.abra +++ b/projects/lsp/src/main.abra @@ -1,18 +1,89 @@ import "process" as process -import "fs" as fs - -val cwd = fs.getCurrentWorkingDirectory() -val logFilePath = "$cwd/log.txt" -val logFile = match fs.createFile(logFilePath, fs.AccessMode.WriteOnly) { - Ok(v) => v - Err(e) => { - println(e) - process.exit(1) +import log from "./log" +import JsonParser from "json" +import RequestMessage, NotificationMessage, ResponseMessage, ResponseError, ResponseErrorCode from "./lsp_spec" +import "./handlers" as handlers + +val contentLengthHeader = "Content-Length: " +val bogusMessageId = -999 + +func processMessage(message: String): Result { + log.writeln("received message:") + log.writeln(message) + + val msgJson = try JsonParser.parseString(message) else |e| return Err(ResponseError(code: ResponseErrorCode.ParseError, message: e.toString())) + val obj = try msgJson.asObject() else |e| return Err(ResponseError(code: ResponseErrorCode.ParseError, message: e.toString())) + val res = if obj.getNumber("id") { + val req = try RequestMessage.fromJson(msgJson) else |e| return Err(ResponseError(code: ResponseErrorCode.ParseError, message: e.toString())) + Some(handlers.handleRequest(req)) + } else { + val notif = try NotificationMessage.fromJson(msgJson) else |e| return Err(ResponseError(code: ResponseErrorCode.ParseError, message: e.toString())) + handlers.handleNotification(notif) + None } + + Ok(res) } -val stdin = process.stdin() +func sendResponse(res: ResponseMessage) { + val resJson = res.toJson() + val resJsonStr = resJson.encode() + val resLen = resJsonStr.length -while stdin.readAsString() |str| { - logFile.writeln(str) + val resMsg = "$contentLengthHeader$resLen\r\n\r\n$resJsonStr" + + log.writeln("responded with:") + log.writeln(resMsg) + stdoutWrite(resMsg) } + +func main() { + val stdin = process.stdin() + + var contentLength = 0 + var seenLen = 0 + var buf: String[] = [] + while stdin.readAsString() |chunk| { + val input = if chunk.startsWith(contentLengthHeader) { + val chars = chunk.chars() + // skip content-length header + for _ in range(0, contentLengthHeader.length) chars.next() + var offset = contentLengthHeader.length + + // parse content-length as integer (can assume ascii encoding) + contentLength = 0 + while chars.next() |ch| { + if ch.isDigit() { + offset += 1 + contentLength *= 10 + contentLength += (ch.asInt() - '0'.asInt()) + } else { + break + } + } + offset += 4 // skip over \r\n\r\n + + chunk[offset:] + } else { + chunk + } + + seenLen += input.length + buf.push(input) + if seenLen >= contentLength { + val message = buf.join() + contentLength = 0 + seenLen = 0 + buf = [] + match processMessage(message) { + Ok(res) => if res |res| sendResponse(res) + Err(err) => { + log.writeln("sending error for bogus id: $err") + sendResponse(ResponseMessage.Error(id: bogusMessageId, error: err)) + } + } + } + } +} + +main() diff --git a/projects/lsp/src/test.js b/projects/lsp/src/test.js new file mode 100644 index 00000000..34fea682 --- /dev/null +++ b/projects/lsp/src/test.js @@ -0,0 +1,8 @@ +const spawn = require('child_process').spawn; +const child = spawn('/Users/kengorab/Desktop/abra-lang/projects/lsp/._abra/abra-lsp'); + +child.stdin.setEncoding('utf-8'); +child.stdout.pipe(process.stdout); + +child.stdin.write('Content-Length: 8\r\n\r\n{"id":'); +child.stdin.write('0}'); diff --git a/projects/std/src/json.abra b/projects/std/src/json.abra index b7a8ef56..44995ba7 100644 --- a/projects/std/src/json.abra +++ b/projects/std/src/json.abra @@ -5,13 +5,142 @@ export enum JsonValue { Boolean(value: Bool) Array(items: JsonValue[]) Object(obj: JsonObject) + + func encode(self): String { + match self { + JsonValue.Null => "null" + JsonValue.Number(value) => match value { + Either.Left(int) => int.toString() + Either.Right(float) => float.toString() + } + JsonValue.String(value) => "\"$value\"" + JsonValue.Boolean(value) => value.toString() + JsonValue.Array(items) => { + val itemStrs: String[] = Array.withCapacity(items.length) + for item in items { + itemStrs.push(item.encode()) + } + "[${itemStrs.join(",")}]" + } + JsonValue.Object(obj) => { + val itemStrs: String[] = Array.withCapacity(obj._map.size) + for (k, v) in obj._map { + itemStrs.push("\"$k\": ${v.encode()}") + } + "{${itemStrs.join(",")}}" + } + } + } + + func kind(self): JsonValueKind = match self { + JsonValue.Null => JsonValueKind.Null + JsonValue.Number => JsonValueKind.Number + JsonValue.String => JsonValueKind.String + JsonValue.Boolean => JsonValueKind.Boolean + JsonValue.Array => JsonValueKind.Array + JsonValue.Object => JsonValueKind.Object + } + + func asNumber(self): Result, JsonError> = match self { + JsonValue.Number(v) => Ok(v) + else => Err(JsonError.TypeMismatch(expected: JsonValueKind.Number, actual: self.kind())) + } + + func asString(self): Result = match self { + JsonValue.String(v) => Ok(v) + else => Err(JsonError.TypeMismatch(expected: JsonValueKind.String, actual: self.kind())) + } + + func asBoolean(self): Result = match self { + JsonValue.Boolean(v) => Ok(v) + else => Err(JsonError.TypeMismatch(expected: JsonValueKind.Boolean, actual: self.kind())) + } + + func asArray(self): Result = match self { + JsonValue.Array(v) => Ok(v) + else => Err(JsonError.TypeMismatch(expected: JsonValueKind.Array, actual: self.kind())) + } + + func asObject(self): Result = match self { + JsonValue.Object(v) => Ok(v) + else => Err(JsonError.TypeMismatch(expected: JsonValueKind.Object, actual: self.kind())) + } +} + +export enum JsonError { + TypeMismatch(expected: JsonValueKind, actual: JsonValueKind) + NoSuchKey(key: String) } export type JsonObject { - map: Map = {} + _map: Map = {} + + func set(self, key: String, value: JsonValue) { + self._map[key] = value + } + + func getValue(self, key: String): JsonValue? = self._map[key] + + func getNumberRequired(self, key: String): Result, JsonError> { + match self._map[key] { + None => Err(JsonError.NoSuchKey(key)) + JsonValue.Number(v) => Ok(v) + else v => Err(JsonError.TypeMismatch(expected: JsonValueKind.Number, actual: v.kind())) + } + } + + func getNumber(self, key: String): Either? = + match self.getNumberRequired(key) { Ok(v) => Some(v), Err => None } + + func getStringRequired(self, key: String): Result { + match self._map[key] { + None => Err(JsonError.NoSuchKey(key)) + JsonValue.String(v) => Ok(v) + else v => Err(JsonError.TypeMismatch(expected: JsonValueKind.String, actual: v.kind())) + } + } + + func getString(self, key: String): String? = + match self.getStringRequired(key) { Ok(v) => Some(v), Err => None } + + func getBooleanRequired(self, key: String): Result { + match self._map[key] { + None => Err(JsonError.NoSuchKey(key)) + JsonValue.Boolean(v) => Ok(v) + else v => Err(JsonError.TypeMismatch(expected: JsonValueKind.Boolean, actual: v.kind())) + } + } + + func getArrayRequired(self, key: String): Result { + match self._map[key] { + None => Err(JsonError.NoSuchKey(key)) + JsonValue.Array(v) => Ok(v) + else v => Err(JsonError.TypeMismatch(expected: JsonValueKind.Array, actual: v.kind())) + } + } + + func getObjectRequired(self, key: String): Result { + match self._map[key] { + None => Err(JsonError.NoSuchKey(key)) + JsonValue.Object(v) => Ok(v) + else v => Err(JsonError.TypeMismatch(expected: JsonValueKind.Object, actual: v.kind())) + } + } + + func getObject(self, key: String): JsonObject? = + match self.getObjectRequired(key) { Ok(v) => Some(v), Err => None } +} + +export enum JsonValueKind { + Null + Number + String + Boolean + Array + Object } -enum JsonParseError { +export enum JsonParseError { UnterminatedString InvalidCharacterInString UnexpectedEndOfNumber @@ -133,7 +262,7 @@ export type JsonParser { val value = try self._parseJsonValue() - obj.map[key] = value + obj._map[key] = value if self._chars.peek() |peekChar| { if peekChar == ',' {