Skip to content

Commit

Permalink
LSP: Handling the 'initialize' method
Browse files Browse the repository at this point in the history
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
kengorab committed Dec 12, 2024
1 parent 93454ad commit c03b361
Show file tree
Hide file tree
Showing 8 changed files with 958 additions and 15 deletions.
487 changes: 487 additions & 0 deletions projects/lsp/message.json

Large diffs are not rendered by default.

47 changes: 47 additions & 0 deletions projects/lsp/src/example.abra
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())
25 changes: 25 additions & 0 deletions projects/lsp/src/handlers.abra
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 => {}
}
}
12 changes: 12 additions & 0 deletions projects/lsp/src/log.abra
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)
}
}
164 changes: 164 additions & 0 deletions projects/lsp/src/lsp_spec.abra
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)
}
}
95 changes: 83 additions & 12 deletions projects/lsp/src/main.abra
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()
8 changes: 8 additions & 0 deletions projects/lsp/src/test.js
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}');
Loading

0 comments on commit c03b361

Please sign in to comment.