Răsfoiți Sursa

Add basic inlay hint support for GDScript (#589)

* Add support for inlay hints in GDScript files
* Add "godotTools.inlayHints.gdscript" and "godotTools.inlayHints.gdresource" settings
* GDScript inlay hints are disabled by default, and marked as experimental
Tristan F 1 an în urmă
părinte
comite
b0f7220f41
2 a modificat fișierele cu 104 adăugiri și 5 ștergeri
  1. 10 0
      package.json
  2. 94 5
      src/providers/inlay_hints.ts

+ 10 - 0
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"
 				}
 			}
 		},

+ 94 - 5
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<InlayHint | undefined> {
+	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<InlayHint[]> {
-		const scene = this.parser.parse_scene(document);
-		const text = document.getText();
-
+	async provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): Promise<InlayHint[]> {
 		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];