
Render.pudu
Pudu246 lines9.9 KB
1/** @Domain.Lsp.Render.Module — language server results as text */2module PuduLangMcp.Domain.Lsp.Render34import Std.Json as Json5import Std.List as List6import Std.Map as Map7import Std.Option as Option8import PuduLangMcp.Utils.JsonAccess as Access910/** @Domain.Lsp.Render.Location — a 1-based position in a shown path */11export type Location = { uri: Str, path: Str, line: Int, character: Int, endLine: Int }121314const SYMBOL_KINDS: Map[Int, Str] = mapOf([15 (1, "file"), (2, "module"), (3, "namespace"), (4, "package"), (5, "class"), (6, "method"),16 (7, "property"), (8, "field"), (9, "constructor"), (10, "enum"), (11, "interface"), (12, "function"),17 (13, "variable"), (14, "constant"), (15, "string"), (16, "number"), (17, "boolean"), (18, "array"),18 (19, "object"), (20, "key"), (21, "null"), (22, "enum member"), (23, "struct"), (24, "event"),19 (25, "operator"), (26, "type parameter")20 ])212223const SEVERITIES: Map[Int, Str] = mapOf([(1, "error"), (2, "warning"), (3, "information"), (4, "hint")])242526export fn hover(result: &Json.Json) -> Str {27 match result {28 case Json.Null => "No information at this position."29 case _ => match Access.member(result, "contents") {30 case Some(contents) => markup(&contents)31 case None => Json.encode(result)32 }33 }34}353637export fn locations(result: &Json.Json, shown: fn(Str) -> Str) -> Array[Location] {38 let items = match result {39 case Json.List(values) => values40 case Json.Null => []41 case _ => [*result]42 }43 var found: Array[Location] = []44 for item in items {45 let uri = Option.orElse(Access.text(&item, "uri"), Access.text(&item, "targetUri"))46 let range = Option.orElse(Access.member(&item, "range"), Access.member(&item, "targetRange"))47 match (uri, range) {48 case (Some(target), Some(span)) => {49 let start = startOf(&span)50 found = found.push(Location{uri: target, path: shown(target), line: start[0], character: start[1], endLine: endOf(&span)[0]})51 }52 case _ => ()53 }54 }55 found56}575859export fn locationLine(held: &Location) -> Str {60 held.path + ":" + show(held.line) + ":" + show(held.character)61}626364export fn completion(result: &Json.Json, limit: Int) -> Str {65 let items = match result {66 case Json.List(values) => values67 case Json.Null => []68 case _ => match Access.member(result, "items") { case Some(Json.List(values)) => values case _ => [] }69 }70 if items.isEmpty() { return "No completions at this position." }71 let shownItems = List.take(&items, limit).map(|item: Json.Json| {72 let label = Option.unwrapOr(Access.text(&item, "label"), "?")73 match Access.text(&item, "detail") {74 case Some(detail) => label + " — " + detail75 case None => label76 }77 })78 let rest = items.length() - shownItems.length()79 shownItems.join("\n") + (if rest > 0 { "\n… and " + show(rest) + " more" } else { "" })80}818283export fn signatureHelp(result: &Json.Json) -> Str {84 let signatures = match Access.member(result, "signatures") { case Some(Json.List(values)) => values case _ => [] }85 if signatures.isEmpty() { return "No signature at this position." }86 let active = Access.integer(result, "activeSignature")87 let chosen = Option.unwrapOr(List.get(&signatures, Option.unwrapOr(active, 0)), signatures[0])88 let label = Option.unwrapOr(Access.text(&chosen, "label"), "")89 var lines = [label]90 let parameters = match Access.member(&chosen, "parameters") { case Some(Json.List(values)) => values case _ => [] }91 let activeParameter = Option.orElse(Access.integer(result, "activeParameter"), Access.integer(&chosen, "activeParameter"))92 if let Some(index) = activeParameter {93 if let Some(parameter) = List.get(¶meters, index) {94 match Access.member(¶meter, "label") {95 case Some(Json.Text(text)) => { lines = lines.push("active parameter: " + text) }96 case Some(Json.List(bounds)) => {97 match (List.get(&bounds, 0), List.get(&bounds, 1)) {98 case (Some(Json.Number(from)), Some(Json.Number(to))) => { lines = lines.push("active parameter: " + label.slice(from, to)) }99 case _ => ()100 }101 }102 case _ => ()103 }104 }105 }106 if let Some(documentation) = Access.member(&chosen, "documentation") { lines = lines.push(markup(&documentation)) }107 lines.join("\n")108}109110111export fn codeActions(result: &Json.Json) -> Str {112 let actions = match result { case Json.List(values) => values case _ => [] }113 if actions.isEmpty() { return "No code actions at this position." }114 actions.map(|action: Json.Json| {115 let title = Option.unwrapOr(Access.text(&action, "title"), "?")116 match Access.text(&action, "kind") {117 case Some(kind) => title + " (" + kind + ")"118 case None => title119 }120 }).join("\n")121}122123124export fn workspaceEdit(result: &Json.Json, shown: fn(Str) -> Str) -> Str {125 var lines: Array[Str] = []126 for change in Access.members(&Option.unwrapOr(Access.member(result, "changes"), Json.Null)) {127 lines = lines.concat(editLines(shown(change[0]), &change[1]))128 }129 match Access.member(result, "documentChanges") {130 case Some(Json.List(documents)) => {131 for document in documents {132 let uri = Option.unwrapOr(Access.pathText(&document, &["textDocument", "uri"]), "")133 lines = lines.concat(editLines(shown(uri), &Option.unwrapOr(Access.member(&document, "edits"), Json.Null)))134 }135 }136 case _ => ()137 }138 if lines.isEmpty() { "No edits." } else { lines.join("\n") }139}140141142export fn documentSymbols(result: &Json.Json) -> Str {143 let lines = symbolLines(result, "")144 if lines.isEmpty() { "No declarations." } else { lines.join("\n") }145}146147148export fn workspaceSymbols(result: &Json.Json, shown: fn(Str) -> Str) -> Str {149 let symbols = match result { case Json.List(values) => values case _ => [] }150 if symbols.isEmpty() { return "No matching declarations." }151 symbols.map(|symbol: Json.Json| {152 let name = Option.unwrapOr(Access.text(&symbol, "name"), "?")153 let kind = kindName(Access.integer(&symbol, "kind"))154 let placed = match Access.member(&symbol, "location") {155 case Some(place) => {156 let found = locations(&place, shown)157 match List.first(&found) { case Some(first) => " — " + locationLine(&first) case None => "" }158 }159 case None => ""160 }161 kind + " " + name + placed162 }).join("\n")163}164165166export fn diagnostics(items: &Array[Json.Json]) -> Str {167 if items.isEmpty() { return "No diagnostics." }168 items.map(|item: Json.Json| {169 let start = startOf(&Option.unwrapOr(Access.member(&item, "range"), Json.Null))170 let severity = Map.getOr(&SEVERITIES, Option.unwrapOr(Access.integer(&item, "severity"), 1), "error")171 let code = match Access.member(&item, "code") {172 case Some(Json.Text(text)) => " [" + text + "]"173 case Some(Json.Number(number)) => " [" + show(number) + "]"174 case _ => ""175 }176 show(start[0]) + ":" + show(start[1]) + " " + severity + code + " " + Option.unwrapOr(Access.text(&item, "message"), "")177 }).join("\n")178}179180181fn markup(contents: &Json.Json) -> Str {182 match contents {183 case Json.Text(text) => text184 case Json.List(parts) => parts.map(|part: Json.Json| markup(&part)).join("\n\n")185 case _ => match Access.text(contents, "value") {186 case Some(text) => text187 case None => Json.encode(contents)188 }189 }190}191192193fn symbolLines(result: &Json.Json, indent: Str) -> Array[Str] {194 let symbols = match result { case Json.List(values) => values case _ => [] }195 var lines: Array[Str] = []196 for symbol in symbols {197 let name = Option.unwrapOr(Access.text(&symbol, "name"), "?")198 let kind = kindName(Access.integer(&symbol, "kind"))199 let detail = match Access.text(&symbol, "detail") { case Some(text) => if text.isEmpty() { "" } else { " — " + text } case None => "" }200 let span = Option.orElse(Access.member(&symbol, "selectionRange"), Option.orElse(Access.member(&symbol, "range"), Json.path(&symbol, &["location", "range"])))201 let line = match span { case Some(held) => " (line " + show(startOf(&held)[0]) + ")" case None => "" }202 lines = lines.push(indent + kind + " " + name + detail + line)203 match Access.member(&symbol, "children") {204 case Some(children) => { lines = lines.concat(symbolLines(&children, indent + " ")) }205 case None => ()206 }207 }208 lines209}210211212fn editLines(path: Str, edits: &Json.Json) -> Array[Str] {213 let items = match edits { case Json.List(values) => values case _ => [] }214 items.map(|edit: Json.Json| {215 let span = Option.unwrapOr(Access.member(&edit, "range"), Json.Null)216 let start = startOf(&span)217 let end = endOf(&span)218 path + ":" + show(start[0]) + ":" + show(start[1]) + "-" + show(end[0]) + ":" + show(end[1]) + " → " + Option.unwrapOr(Access.text(&edit, "newText"), "")219 })220}221222223fn kindName(kind: Option[Int]) -> Str {224 match kind {225 case Some(number) => Map.getOr(&SYMBOL_KINDS, number, "symbol")226 case None => "symbol"227 }228}229230231fn startOf(span: &Json.Json) -> (Int, Int) {232 pointOf(span, "start")233}234235236fn endOf(span: &Json.Json) -> (Int, Int) {237 pointOf(span, "end")238}239240241fn pointOf(span: &Json.Json, name: Str) -> (Int, Int) {242 let line = Option.unwrapOr(Access.pathInt(span, &[name, "line"]), 0)243 let character = Option.unwrapOr(Access.pathInt(span, &[name, "character"]), 0)244 (line + 1, character + 1)245}246