Pudu programming language
Menu
Package

@chrismichaelps / pudu-lang-mcp

Model Context Protocol server for Pudu language documentation and compiler tools

0.1.1Apache-2.01

InstallClose

Render.pudu

Pudu246 lines9.9 KB

GitHub ↗
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 }1213/// LSP symbol kinds by number.14const 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  ])2122/// LSP diagnostic severities by number.23const SEVERITIES: Map[Int, Str] = mapOf([(1, "error"), (2, "warning"), (3, "information"), (4, "hint")])2425/// The hover contents as text.26export 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}3536/// Every location in a definition or references result.37export 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}5758/// A location as `path:line:character`.59export fn locationLine(held: &Location) -> Str {60  held.path + ":" + show(held.line) + ":" + show(held.character)61}6263/// Completion items, one per line, at most `limit`.64export 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}8182/// The active signature, its active parameter, and its documentation.83export 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(&parameters, index) {94      match Access.member(&parameter, "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}109110/// Code action titles, one per line.111export 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}122123/// The edits of a rename, one per line.124export 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}140141/// A document's declarations, nested ones indented.142export fn documentSymbols(result: &Json.Json) -> Str {143  let lines = symbolLines(result, "")144  if lines.isEmpty() { "No declarations." } else { lines.join("\n") }145}146147/// Matching declarations with where each is.148export 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}164165/// Diagnostics, one per line with position, severity, and code.166export 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}179180/// Markup content, a string, or a list of either, as text.181fn 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}191192/// Lines for symbols and their children.193fn 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}210211/// Lines for the text edits of one file.212fn 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}221222/// The name of a symbol kind.223fn kindName(kind: Option[Int]) -> Str {224  match kind {225    case Some(number) => Map.getOr(&SYMBOL_KINDS, number, "symbol")226    case None => "symbol"227  }228}229230/// The 1-based start of a range.231fn startOf(span: &Json.Json) -> (Int, Int) {232  pointOf(span, "start")233}234235/// The 1-based end of a range.236fn endOf(span: &Json.Json) -> (Int, Int) {237  pointOf(span, "end")238}239240/// One end of a range, 1-based.241fn 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