
Conversation.pudu
Pudu139 lines5.5 KB
1/** @Domain.Lsp.Conversation.Module — one-shot language server conversations */2module PuduLangMcp.Domain.Lsp.Conversation34import Std.Json as Json5import Std.Math as Math6import Std.Text as Text7import PuduLangMcp.Utils.JsonAccess as Access89/** @Domain.Lsp.Conversation.Question — one question at 1-based position */10export type Question = { method: Str, line: Int, character: Int, newName: Str, query: Str }1112/** @Domain.Lsp.Conversation.Answer — result and diagnostics from a transcript */13export type Answer = { result: Option[Json.Json], diagnostics: Array[Json.Json], error: Option[Str] }141516const INITIALIZE_ID: Int = 11718const QUESTION_ID: Int = 21920const SHUTDOWN_ID: Int = 32122const INVOKED: Int = 12324const HEADER: Str = "Content-Length:"2526const SEPARATOR: Str = "\r\n\r\n"2728const WORKSPACE_SYMBOL: Str = "workspace/symbol"2930const DIAGNOSTICS: Str = "textDocument/publishDiagnostics"313233export fn conversation(rootUri: Str, uri: Str, source: Str, question: &Question) -> Str {34 var messages = [35 request(INITIALIZE_ID, "initialize", Json.object(&[36 ("processId", Json.Null),37 ("rootUri", Json.Text(rootUri)),38 ("capabilities", Json.object(&[]))39 ])),40 notification("initialized", Json.object(&[]))41 ]42 let opens = !(question.method == WORKSPACE_SYMBOL && source.isEmpty())43 if opens {44 messages = messages.push(notification("textDocument/didOpen", Json.object(&[45 ("textDocument", Json.object(&[46 ("uri", Json.Text(uri)),47 ("languageId", Json.Text("pudu")),48 ("version", Json.Number(1)),49 ("text", Json.Text(source))50 ]))51 ])))52 }53 messages = messages.push(request(QUESTION_ID, question.method, params(uri, question)))54 messages = messages.push(request(SHUTDOWN_ID, "shutdown", Json.Null))55 messages = messages.push(notification("exit", Json.Null))56 messages.map(|message: Json.Json| frame(&message)).join("")57}585960export fn params(uri: Str, question: &Question) -> Json.Json {61 if question.method == WORKSPACE_SYMBOL { return Json.object(&[("query", Json.Text(question.query))]) }62 let document = ("textDocument", Json.object(&[("uri", Json.Text(uri))]))63 if question.method == "textDocument/documentSymbol" { return Json.object(&[document]) }64 let at = position(question)65 if question.method == "textDocument/codeAction" {66 return Json.object(&[67 document,68 ("range", Json.object(&[("start", at), ("end", at)])),69 ("context", Json.object(&[("diagnostics", Json.list(&[]))]))70 ])71 }72 var fields = [document, ("position", at)]73 if question.method == "textDocument/references" {74 fields = fields.push(("context", Json.object(&[("includeDeclaration", Json.Boolean(true))])))75 }76 if question.method == "textDocument/completion" {77 fields = fields.push(("context", Json.object(&[("triggerKind", Json.Number(INVOKED))])))78 }79 if question.method == "textDocument/rename" {80 fields = fields.push(("newName", Json.Text(question.newName)))81 }82 Json.object(&fields)83}848586export fn answerIn(transcript: Str) -> Answer {87 var result: Option[Json.Json] = None88 var error: Option[Str] = None89 var diagnostics: Array[Json.Json] = []90 for piece in transcript.split(HEADER) {91 if let Some(start) = Text.find(piece, SEPARATOR) {92 match Json.decode(piece.drop(start + SEPARATOR.length()).trim()) {93 case Ok(message) => {94 if Access.integer(&message, "id") == Some(QUESTION_ID) {95 match Access.member(&message, "error") {96 case Some(problem) => { error = Some(match Access.text(&problem, "message") { case Some(text) => text case None => "the language server refused the request" }) }97 case None => { result = Some(match Access.member(&message, "result") { case Some(value) => value case None => Json.Null }) }98 }99 }100 if Access.text(&message, "method") == Some(DIAGNOSTICS) {101 match Json.path(&message, &["params", "diagnostics"]) {102 case Some(Json.List(items)) => { diagnostics = items }103 case _ => ()104 }105 }106 }107 case Err(_) => ()108 }109 }110 }111 Answer{result: result, diagnostics: diagnostics, error: error}112}113114115export fn frame(message: &Json.Json) -> Str {116 let body = Json.encode(message)117 HEADER + " " + show(body.toBytes().length()) + SEPARATOR + body118}119120121fn position(question: &Question) -> Json.Json {122 Json.object(&[("line", Json.Number(Math.max(question.line - 1, 0))), ("character", Json.Number(Math.max(question.character - 1, 0)))])123}124125126fn request(id: Int, method: Str, parameters: Json.Json) -> Json.Json {127 Json.object(&[128 ("jsonrpc", Json.Text("2.0")),129 ("id", Json.Number(id)),130 ("method", Json.Text(method)),131 ("params", parameters)132 ])133}134135136fn notification(method: Str, parameters: Json.Json) -> Json.Json {137 Json.object(&[("jsonrpc", Json.Text("2.0")), ("method", Json.Text(method)), ("params", parameters)])138}139