diff --git a/lang/src/arr/compiler/query.arr b/lang/src/arr/compiler/query.arr index 85aae99f1..80cf7e6ee 100644 --- a/lang/src/arr/compiler/query.arr +++ b/lang/src/arr/compiler/query.arr @@ -149,6 +149,32 @@ fun jump-to-def(cache-manager, uri :: String, line :: Number, col :: Number) -> end end +# this is a lot like jump-to-def +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) => + 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({ name: name.toname(), docstring: vb.doc, ann: 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..c7e016776 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 @@ -166,6 +167,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 +223,26 @@ 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) => + ann-json = if A.is-a-blank(hover-info.ann): + J.j-null + else: + J.j-str(hover-info.ann.tosource().pretty(40).join-str("\n")) + end + d = [SD.string-dict: + "type", J.j-str("hover-success"), + "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 | query == "document-symbols" then: cases(E.Either) info-result block: | left(error-str) => 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/lsp/src/server-node-tmp.ts b/lsp/src/server-node-tmp.ts index b45c6292a..a635410d2 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,65 @@ connection.onDefinition(async (params) => { } }); +interface HoverResult { + name: string; + ann: string; + doc: string; +} + +function parseHover(msg: any): HoverResult | null { + if (msg.type === "hover-success") { + return { name: msg.name, 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; + + const parts: string[] = []; + if (result.ann) { + parts.push(`\`\`\`pyret\n${result.name} :: ${result.ann}\n\`\`\``); + } + 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: parts.join("\n\n"), + }, + }; + } 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 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"