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

Language.pudu

Pudu140 lines7.3 KB

GitHub ↗
1/** @App.Tools.Language.Handler — language server questions as tools */2module PuduLangMcp.App.Tools.Language34import Std.Io as Io5import Std.Json as Json6import Std.List as List7import Std.Math as Math8import Std.Option as Option9import Std.Path as Path10import Std.Result as Result11import PuduLangMcp.App.Context as Context12import PuduLangMcp.Constants.Server as Server13import PuduLangMcp.Domain.Catalog.Arguments as Arguments14import PuduLangMcp.Domain.Lsp.Conversation as Conversation15import PuduLangMcp.Domain.Lsp.Render as Render16import PuduLangMcp.Errors.ToolError as ToolError17import PuduLangMcp.Services.Lsp as Lsp18import PuduLangMcp.Services.Workspace as Workspace19import PuduLangMcp.Utils.TextBounds as TextBounds20import PuduLangMcp.Utils.Uri as Uri2122/// Lines of a declaration's source shown under a definition.23const EXCERPT_LINES: Int = 122425/// Most completion items answered.26const COMPLETION_LIMIT: Int = 602728/// How the inline source file is shown.29const SOURCE_LABEL: Str = "<source>"3031/// Answers the type and documentation at a position.32export fn hover(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {33  ask(context, args, "textDocument/hover", fn(result: Json.Json, shown: fn(Str) -> Str, target: Workspace.Target) -> Str { Render.hover(&result) })34}3536/// Answers where the name at a position is declared, with its source.37export fn definition(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {38  let library = context.toolchain.library39  let workspace = context.workspace40  ask(context, args, "textDocument/definition", fn(result: Json.Json, shown: fn(Str) -> Str, target: Workspace.Target) -> Str {41      let found = Render.locations(&result, shown)42      if found.isEmpty() { return "No definition at this position." }43      found.map(|held: Render.Location| Render.locationLine(&held) + excerpt(&held, &target, &workspace, &library)).join("\n\n")44    })45}4647/// Answers every use of the name at a position.48export fn references(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {49  ask(context, args, "textDocument/references", fn(result: Json.Json, shown: fn(Str) -> Str, target: Workspace.Target) -> Str {50      let found = Render.locations(&result, shown)51      if found.isEmpty() { "No references at this position." } else { found.map(|held: Render.Location| Render.locationLine(&held)).join("\n") }52    })53}5455/// Answers what could be written at a position.56export fn completion(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {57  ask(context, args, "textDocument/completion", fn(result: Json.Json, shown: fn(Str) -> Str, target: Workspace.Target) -> Str { Render.completion(&result, COMPLETION_LIMIT) })58}5960/// Answers the signature of the call around a position.61export fn signatureHelp(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {62  ask(context, args, "textDocument/signatureHelp", fn(result: Json.Json, shown: fn(Str) -> Str, target: Workspace.Target) -> Str { Render.signatureHelp(&result) })63}6465/// Answers the quick fixes offered at a position.66export fn codeActions(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {67  ask(context, args, "textDocument/codeAction", fn(result: Json.Json, shown: fn(Str) -> Str, target: Workspace.Target) -> Str { Render.codeActions(&result) })68}6970/// Answers the edits a rename would make, without applying them.71export fn renamePreview(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {72  ask(context, args, "textDocument/rename", fn(result: Json.Json, shown: fn(Str) -> Str, target: Workspace.Target) -> Str { Render.workspaceEdit(&result, shown) })73}7475/// Answers every declaration in the code.76export fn documentSymbols(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {77  ask(context, args, "textDocument/documentSymbol", fn(result: Json.Json, shown: fn(Str) -> Str, target: Workspace.Target) -> Str { Render.documentSymbols(&result) })78}7980/// Answers declarations across the workspace whose names match the query.81export fn workspaceSymbols(context: &Context.Context, args: &Arguments.Args) -> Result[Str, ToolError.ToolFailure] {82  let question = Conversation.Question{method: "workspace/symbol", line: 1, character: 1, newName: "", query: Option.unwrapOr(Arguments.text(args, "query"), "")}83  let shown = shownPath(context, None)84  let answer = Lsp.ask(&context.toolchain, Uri.fileUri(context.workspace.root), "", "", context.workspace.root, &question) ?85  match answer.error {86    case Some(message) => Ok("The language server answered: " + message)87    case None => Ok(TextBounds.bounded(Render.workspaceSymbols(&Option.unwrapOr(answer.result, Json.Null), shown), Server.DOC_CHARS))88  }89}9091/// Asks one position question about the code `source` or `path` names.92fn ask(context: &Context.Context, args: &Arguments.Args, method: Str, render: fn(Json.Json, fn(Str) -> Str, Workspace.Target) -> Str) -> Result[Str, ToolError.ToolFailure] {93  let question = Conversation.Question {94    method: method,95    line: Arguments.integerOr(args, "line", 1),96    character: Arguments.integerOr(args, "character", 1),97    newName: Option.unwrapOr(Arguments.text(args, "newName"), ""),98    query: ""99  }100  let owner = *context101  let answered = Workspace.withTarget(&context.workspace, Arguments.text(args, "source"), Arguments.text(args, "path"), fn(target: Workspace.Target) -> Result[Str, ToolError.ToolFailure] {102      let answer = Lsp.ask(&owner.toolchain, Uri.fileUri(target.directory), Uri.fileUri(target.file), target.source, target.directory, &question) ?103      match answer.error {104        case Some(message) => Ok("The language server answered: " + message)105        case None => Ok(TextBounds.bounded(render(Option.unwrapOr(answer.result, Json.Null), shownPath(&owner, if target.scratch { Some(target.file) } else { None }), target), Server.DOC_CHARS))106      }107    }) ?108  answered109}110111/// How a URI the language server names is shown to the model.112fn shownPath(context: &Context.Context, scratchFile: Option[Str]) -> fn(Str) -> Str {113  let library = context.toolchain.library114  let workspace = context.workspace115  fn(uri: Str) -> Str {116    let path = Option.unwrapOr(Uri.pathOfFileUri(uri), uri)117    if let Some(file) = scratchFile {118      if path == file { return SOURCE_LABEL }119    }120    if Path.isInside(workspace.root, path) { return Workspace.relative(&workspace, path) }121    match library {122      case Some(root) => if Path.isInside(root, path) { Option.unwrapOr(Path.relativeTo(root, path), path) } else { path }123      case None => path124    }125  }126}127128/// Source lines of a declaration, when its file is one this server may read.129fn excerpt(held: &Render.Location, target: &Workspace.Target, workspace: &Workspace.Workspace, library: &Option[Str]) -> Str {130  let path = Option.unwrapOr(Uri.pathOfFileUri(held.uri), "")131  let readable = path == target.file || Path.isInside(workspace.root, path) || (match library { case Some(root) => Path.isInside(root, path) case None => false })132  if !readable { return "" }133  let text = if path == target.file { target.source } else { Result.unwrapOr(Io.read(path), "") }134  let lines = text.split("\n")135  let start = held.line - 1136  if start >= lines.length() { return "" }137  let end = Math.min(start + EXCERPT_LINES, lines.length())138  "\n```pudu\n" + lines.slice(start, end).join("\n") + "\n```"139}140