-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
LSP: Handling the 'initialize' method
This adds a ton of code to the `json` std module to support json encoding/decoding of the `initialize` request and response json rpc messages as part of the LSP lifecycle. This can correctly receive an `initialize` request and decode it to a data structure, and respond with an `InitializeResult` object. This code currently fails when it is sent the next message type ( `textDocument/didOpen`) which is not yet implemented.
- Loading branch information
Showing
8 changed files
with
958 additions
and
15 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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()) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 => {} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<RequestMessage, JsonError> { | ||
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<NotificationMessage, JsonError> { | ||
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ResponseMessage?, ResponseError> { | ||
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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}'); |
Oops, something went wrong.