diff --git a/package.json b/package.json index 498062e..6a41880 100644 --- a/package.json +++ b/package.json @@ -303,6 +303,16 @@ ], "default": "sameFolder", "description": "Controls where the Scene Preview will search for related scenes when viewing a script file." + }, + "godotTools.inlayHints.gdscript": { + "type": "boolean", + "default": false, + "description": "Whether to enable inlay hints in GDScript files (experimental)" + }, + "godotTools.inlayHints.gdresource": { + "type": "boolean", + "default": true, + "description": "Whether to enable inlay hints in GDResource (.tscn, .tres, etc) files" } } }, diff --git a/src/providers/inlay_hints.ts b/src/providers/inlay_hints.ts index 726cfc7..3300b33 100644 --- a/src/providers/inlay_hints.ts +++ b/src/providers/inlay_hints.ts @@ -10,10 +10,39 @@ import { ExtensionContext, } from "vscode"; import { SceneParser } from "../scene_tools"; -import { createLogger } from "../utils"; +import { createLogger, get_configuration } from "../utils"; +import { globals } from "../extension"; const log = createLogger("providers.inlay_hints"); +/** + * Returns a label from a detail string. + * E.g. `var a: int` gets parsed to ` int `. + */ +function fromDetail(detail: string): string { + const labelRegex = /: ([\w\d_]+)/; + const labelMatch = detail.match(labelRegex); + const label = labelMatch ? labelMatch[1] : "unknown"; + return ` ${label} `; +} + +async function addByHover(document: TextDocument, hoverPosition: vscode.Position, start: vscode.Position): Promise { + const response = await globals.lsp.client.sendRequest("textDocument/hover", { + textDocument: { uri: document.uri.toString() }, + position: { + line: hoverPosition.line, + character: hoverPosition.character, + } + }); + + // check if contents is an empty array; if it is, we have no hover information + if (Array.isArray(response["contents"]) && response["contents"].length === 0) { + return undefined; + } + + return new InlayHint(start, fromDetail(response["contents"].value), InlayHintKind.Type); +} + export class GDInlayHintsProvider implements InlayHintsProvider { public parser = new SceneParser(); @@ -28,11 +57,71 @@ export class GDInlayHintsProvider implements InlayHintsProvider { ); } - provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): ProviderResult { - const scene = this.parser.parse_scene(document); - const text = document.getText(); - + async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise { const hints: InlayHint[] = []; + const text = document.getText(range); + + if (document.fileName.endsWith(".gd")) { + if (!get_configuration("inlayHints.gdscript", true)) { + return hints; + } + + await globals.lsp.client.onReady(); + + const symbolsRequest = await globals.lsp.client.sendRequest("textDocument/documentSymbol", { + textDocument: { uri: document.uri.toString() }, + }) as unknown[]; + + if (symbolsRequest.length === 0) { + return hints; + } + + const symbols = (typeof symbolsRequest[0] === "object" && "children" in symbolsRequest[0]) + ? (symbolsRequest[0].children as unknown[]) // godot 4.0+ returns an array of children + : symbolsRequest; // godot 3.2 and below returns an array of symbols + + const hasDetail = symbols.some((s: any) => s.detail); + + // TODO: make sure godot reports the correct location for variable declaration symbols + // (allowing the use of regex only on ranges provided by the LSP (textDocument/documentSymbol)) + + // since neither LSP or the grammar know whether a variable is inferred or not, + // we still need to use regex to find all inferred variable declarations. + const regex = /((var|const)\s+)([\w\d_]+)\s*:=/g; + + for (const match of text.matchAll(regex)) { + if (token.isCancellationRequested) break; + // TODO: until godot supports nested document symbols, we need to send + // a hover request for each variable declaration that is nested + const start = document.positionAt(match.index + match[0].length - 1); + const hoverPosition = document.positionAt(match.index + match[1].length); + + if (hasDetail) { + const symbol = symbols.find((s: any) => s.name === match[3]); + if (symbol && symbol["detail"]) { + const hint = new InlayHint(start, fromDetail(symbol["detail"]), InlayHintKind.Type); + hints.push(hint); + } else { + const hint = await addByHover(document, hoverPosition, start); + if (hint) { + hints.push(hint); + } + } + } else { + const hint = await addByHover(document, hoverPosition, start); + if (hint) { + hints.push(hint); + } + } + } + return hints; + } + + if (!get_configuration("inlayHints.gdresource", true)) { + return hints; + } + + const scene = this.parser.parse_scene(document); for (const match of text.matchAll(/ExtResource\(\s?"?(\w+)\s?"?\)/g)) { const id = match[1];