
Language.pudu
Pudu140 lines7.3 KB
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 Uri212223const EXCERPT_LINES: Int = 12242526const COMPLETION_LIMIT: Int = 60272829const SOURCE_LABEL: Str = "<source>"303132export 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}353637export 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}464748export 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}545556export 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}596061export 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}646566export 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}697071export 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}747576export 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}798081export 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}909192fn 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}110111112fn 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}127128129fn 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