From 0366feaed976ee3ca256052a7398cf815c1c1d98 Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Sun, 12 Apr 2026 12:26:39 -0400 Subject: [PATCH 1/4] basic wiring for hover --- lang/src/arr/compiler/query.arr | 25 ++++++++++++++++ lang/src/arr/compiler/server.arr | 20 +++++++++++++ lsp/src/server-node-tmp.ts | 49 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) diff --git a/lang/src/arr/compiler/query.arr b/lang/src/arr/compiler/query.arr index 85aae99f1..cb0b41c11 100644 --- a/lang/src/arr/compiler/query.arr +++ b/lang/src/arr/compiler/query.arr @@ -149,6 +149,31 @@ fun jump-to-def(cache-manager, uri :: String, line :: Number, col :: Number) -> end end +# this is a lot like jump-to-def +# TODO: abstract suitably! +fun hover(cache-manager, uri :: String, line :: Number, col :: Number) -> E.Either: + cases(Option) cache-manager.get-surface-ast(uri): + | none => E.left("AST not available") + | some(ast) => + cases(Option) cache-manager.get-named-result(uri): + | none => E.left("Resolved AST not available") + | some(named-result) => + cases(Option) find-name-at(ast, line, col): + | none => E.left("Did not select a name") + | some(name) => + cases(Option) find-name-key-by-srcloc(named-result.ast, name.l): + | none => E.left("Post-resolution name not found") + | some(key) => + cases(Option) named-result.env.bindings.get-now(key): + | none => E.left("No value identifier binding found") + | some(vb) => E.right({vb.doc; vb.ann}) + end + end + end + end + end +end + fun document-symbols(cache-manager, uri :: String) -> E.Either>: cases(Option) cache-manager.get-named-result(uri): diff --git a/lang/src/arr/compiler/server.arr b/lang/src/arr/compiler/server.arr index 10cc22f1b..c5be09af8 100644 --- a/lang/src/arr/compiler/server.arr +++ b/lang/src/arr/compiler/server.arr @@ -166,6 +166,10 @@ fun on-query(pyret-dir, cache-manager, query, compile-opts, query-opts, send-mes line = query-opts.get-value("line") col = query-opts.get-value("col") Q.jump-to-def(cache-manager, base-uri, line, col) + | query == "hover" then: + line = query-opts.get-value("line") + col = query-opts.get-value("col") + Q.hover(cache-manager, base-uri, line, col) | query == "document-symbols" then: Q.document-symbols(cache-manager, base-uri) | query == "check" then: @@ -218,6 +222,22 @@ fun on-query(pyret-dir, cache-manager, query, compile-opts, query-opts, send-mes ] send-message(J.j-obj(d).serialize()) end + | query == "hover" then: + cases(E.Either) info-result block: + | left(error-str) => + err("hover: no result (errors: " + error-str + ")\n") + d = [SD.string-dict: "type", J.j-str("hover-failure")] + send-message(J.j-obj(d).serialize()) + | right(hover-info) => + # TODO: return an object instead of a tuple in hover and jump + d = [SD.string-dict: + "type", J.j-str("hover-success"), + # TODO: slightly better serialization! and prune out stuff too! + "ann", J.j-str(hover-info.{1}.tosource().pretty(1000).join-str("")), + "doc", J.j-str(hover-info.{0}), + ] + send-message(J.j-obj(d).serialize()) + end | query == "document-symbols" then: cases(E.Either) info-result block: | left(error-str) => diff --git a/lsp/src/server-node-tmp.ts b/lsp/src/server-node-tmp.ts index b45c6292a..e93b0c1d5 100644 --- a/lsp/src/server-node-tmp.ts +++ b/lsp/src/server-node-tmp.ts @@ -113,6 +113,7 @@ connection.onInitialize(async (_params) => { const capabilities: ServerCapabilities = { textDocumentSync: TextDocumentSyncKind.Incremental, definitionProvider: true, + hoverProvider: true, documentSymbolProvider: true, diagnosticProvider: { interFileDependencies: false, @@ -312,6 +313,8 @@ function sendCheckRequest( } // #region LSP request handlers +// TODO: there is a lot of redundancy here, we should probably develop better +// abstractions here! especially for the off-by-one location issues (hacked in rn) connection.onDocumentSymbol(async (params) => { const portFile = getSocketPath(); @@ -365,6 +368,7 @@ connection.onDefinition(async (params) => { } // LSP positions are 0-indexed; Pyret srclocs are 1-indexed + // TODO: fix these awful off-by-one errors! const line = params.position.line + 1; const col = params.position.character + 1; const filePath = uriToFilePath(params.textDocument.uri); @@ -386,6 +390,51 @@ connection.onDefinition(async (params) => { } }); +interface HoverResult { + ann: string; + doc: string; +} + +function parseHover(msg: any): HoverResult | null { + if (msg.type === "hover-success") { + return { ann: msg.ann, doc: msg.doc }; + } + return null; +} + +connection.onHover(async (params) => { + const portFile = getSocketPath(); + if (!fs.existsSync(portFile)) { + connection.console.error("Pyret server not running, cannot get hover info"); + return null; + } + + const line = params.position.line + 1; + const col = params.position.character + 1; + const filePath = uriToFilePath(params.textDocument.uri); + + try { + const result = await sendQueryRequest( + portFile, + "hover", + filePath, + { line, col }, + parseHover, + ); + if (!result) return null; + + return { + contents: { + kind: "markdown", + value: `ann:\`${result.ann}\`\n\ndoc: ${result.doc}`, + }, + }; + } catch (err) { + connection.console.error(`hover error: ${err}`); + return null; + } +}); + const documents = new TextDocuments(TextDocument); // Track the document version at last save per URI. VS Code triggers From a899f048534497ca41cf5cf924ed544df2a4490d Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Sun, 12 Apr 2026 12:47:40 -0400 Subject: [PATCH 2/4] better printing in hover --- lang/src/arr/compiler/query.arr | 7 ++++--- lang/src/arr/compiler/server.arr | 13 +++++++++---- lsp/src/server-node-tmp.ts | 18 ++++++++++++++++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/lang/src/arr/compiler/query.arr b/lang/src/arr/compiler/query.arr index cb0b41c11..80cf7e6ee 100644 --- a/lang/src/arr/compiler/query.arr +++ b/lang/src/arr/compiler/query.arr @@ -150,8 +150,9 @@ fun jump-to-def(cache-manager, uri :: String, line :: Number, col :: Number) -> end # this is a lot like jump-to-def -# TODO: abstract suitably! -fun hover(cache-manager, uri :: String, line :: Number, col :: Number) -> E.Either: +type HoverResult = { name :: String, docstring :: String, ann :: A.Ann } + +fun hover(cache-manager, uri :: String, line :: Number, col :: Number) -> E.Either: cases(Option) cache-manager.get-surface-ast(uri): | none => E.left("AST not available") | some(ast) => @@ -166,7 +167,7 @@ fun hover(cache-manager, uri :: String, line :: Number, col :: Number) -> E.Eith | some(key) => cases(Option) named-result.env.bindings.get-now(key): | none => E.left("No value identifier binding found") - | some(vb) => E.right({vb.doc; vb.ann}) + | some(vb) => E.right({ name: name.toname(), docstring: vb.doc, ann: vb.ann }) end end end diff --git a/lang/src/arr/compiler/server.arr b/lang/src/arr/compiler/server.arr index c5be09af8..40f1480a0 100644 --- a/lang/src/arr/compiler/server.arr +++ b/lang/src/arr/compiler/server.arr @@ -13,6 +13,7 @@ import js-file("server") as S import file("./cli-module-loader.arr") as CLI import file("./compile-structs.arr") as CS import file("./compile-lib.arr") as CL +import ast as A import file("./query.arr") as Q import file("locators/builtin.arr") as B @@ -229,12 +230,16 @@ fun on-query(pyret-dir, cache-manager, query, compile-opts, query-opts, send-mes d = [SD.string-dict: "type", J.j-str("hover-failure")] send-message(J.j-obj(d).serialize()) | right(hover-info) => - # TODO: return an object instead of a tuple in hover and jump + ann-json = if A.is-a-blank(hover-info.ann): + J.j-null + else: + J.j-str(hover-info.ann.tosource().pretty(1000).join-str("")) + end d = [SD.string-dict: "type", J.j-str("hover-success"), - # TODO: slightly better serialization! and prune out stuff too! - "ann", J.j-str(hover-info.{1}.tosource().pretty(1000).join-str("")), - "doc", J.j-str(hover-info.{0}), + "name", J.j-str(hover-info.name), + "ann", ann-json, + "doc", J.j-str(hover-info.docstring), ] send-message(J.j-obj(d).serialize()) end diff --git a/lsp/src/server-node-tmp.ts b/lsp/src/server-node-tmp.ts index e93b0c1d5..6f173ba47 100644 --- a/lsp/src/server-node-tmp.ts +++ b/lsp/src/server-node-tmp.ts @@ -391,13 +391,14 @@ connection.onDefinition(async (params) => { }); interface HoverResult { + name: string; ann: string; doc: string; } function parseHover(msg: any): HoverResult | null { if (msg.type === "hover-success") { - return { ann: msg.ann, doc: msg.doc }; + return { name: msg.name, ann: msg.ann, doc: msg.doc }; } return null; } @@ -423,10 +424,23 @@ connection.onHover(async (params) => { ); if (!result) return null; + const parts: string[] = []; + if (result.ann) { + parts.push(`\`${result.name} :: ${result.ann}\``); + } + if (result.doc) { + if (result.ann) { + parts.push(result.doc); + } else { + parts.push(`${result.name}: ${result.doc}`); + } + } + if (parts.length === 0) return null; + return { contents: { kind: "markdown", - value: `ann:\`${result.ann}\`\n\ndoc: ${result.doc}`, + value: parts.join("\n\n"), }, }; } catch (err) { From 684814088ab089e1194e960bb52d474f58ccc2db Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Sun, 12 Apr 2026 12:52:17 -0400 Subject: [PATCH 3/4] prettier rendering --- lang/src/arr/compiler/server.arr | 2 +- lsp/src/server-node-tmp.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/src/arr/compiler/server.arr b/lang/src/arr/compiler/server.arr index 40f1480a0..c7e016776 100644 --- a/lang/src/arr/compiler/server.arr +++ b/lang/src/arr/compiler/server.arr @@ -233,7 +233,7 @@ fun on-query(pyret-dir, cache-manager, query, compile-opts, query-opts, send-mes ann-json = if A.is-a-blank(hover-info.ann): J.j-null else: - J.j-str(hover-info.ann.tosource().pretty(1000).join-str("")) + J.j-str(hover-info.ann.tosource().pretty(40).join-str("\n")) end d = [SD.string-dict: "type", J.j-str("hover-success"), diff --git a/lsp/src/server-node-tmp.ts b/lsp/src/server-node-tmp.ts index 6f173ba47..a635410d2 100644 --- a/lsp/src/server-node-tmp.ts +++ b/lsp/src/server-node-tmp.ts @@ -426,7 +426,7 @@ connection.onHover(async (params) => { const parts: string[] = []; if (result.ann) { - parts.push(`\`${result.name} :: ${result.ann}\``); + parts.push(`\`\`\`pyret\n${result.name} :: ${result.ann}\n\`\`\``); } if (result.doc) { if (result.ann) { From 560682326e8f16eba01c5498dfd23bd88aaa71f1 Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Sun, 12 Apr 2026 13:06:45 -0400 Subject: [PATCH 4/4] tweaks --- lang/src/arr/trove/ast.arr | 1 + vscode/sampleFiles/lsp/hover.arr | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/lang/src/arr/trove/ast.arr b/lang/src/arr/trove/ast.arr index 3bbf5e4c1..7de833a16 100644 --- a/lang/src/arr/trove/ast.arr +++ b/lang/src/arr/trove/ast.arr @@ -1055,6 +1055,7 @@ data Expr: method tosource(self): PP.str("!") + self.id.tosource() end | s-id-letrec(l :: Loc, id :: Name, safe :: Boolean) with: method label(self): "s-id-letrec" end, + # This is not actually to-source! There is a difference between tosource and pretty-print method tosource(self): PP.str("~") + self.id.tosource() end | s-id-var-modref(l :: Loc, id :: Name, uri :: String, name :: String) with: method label(self): "s-id-var-modref" end, diff --git a/vscode/sampleFiles/lsp/hover.arr b/vscode/sampleFiles/lsp/hover.arr index 26c33f530..755a413f4 100644 --- a/vscode/sampleFiles/lsp/hover.arr +++ b/vscode/sampleFiles/lsp/hover.arr @@ -10,6 +10,13 @@ end not-zero +fun doc-no-ann(asd): + doc: "hi mom" + asd +end + +doc-no-ann + div-refine :: Number, Number%(not-zero) -> Number fun div-refine(num, den): doc: "divides the things"